Added a ProcessControl interface and implementations.

This commit is contained in:
Adirelle 2015-05-03 23:05:17 +02:00
parent b87dac145d
commit 8d57de1909
11 changed files with 464 additions and 38 deletions

View file

@ -11,6 +11,8 @@
namespace PHPCI\Command;
use Monolog\Logger;
use PHPCI\ProcessControl\Factory;
use PHPCI\ProcessControl\ProcessControlInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -41,10 +43,16 @@ class DaemonCommand extends Command
*/
protected $logFilePath;
public function __construct(Logger $logger, $name = null)
/**
* @var ProcessControlInterface
*/
protected $processControl;
public function __construct(Logger $logger, ProcessControlInterface $processControl = null, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
$this->processControl = $processControl ?: Factory::getInstance();
}
protected function configure()
@ -107,11 +115,11 @@ class DaemonCommand extends Command
$cmd = "nohup %s/daemonise phpci:daemonise > %s 2>&1 &";
$command = sprintf($cmd, PHPCI_DIR, $this->logFilePath);
$output = $exitCode = null;
exec($command, $output, $exitCode);
if ($exitCode !== 0) {
$this->logger->error(sprintf("daemonise exited with status %d",
$exitCode));
$this->logger->error(sprintf("daemonise exited with status %d", $exitCode));
return "notstarted";
}
@ -137,7 +145,7 @@ class DaemonCommand extends Command
}
$this->logger->info("Trying to terminate the daemon", array('pid' => $pid));
$this->kill($pid, 15);
$this->processControl->kill($pid);
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
@ -145,7 +153,7 @@ class DaemonCommand extends Command
if ($pid) {
$this->logger->warning("The daemon is resiting, trying to kill it", array('pid' => $pid));
$this->kill($pid, 9);
$this->processControl->kill($pid, true);
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
@ -184,42 +192,11 @@ class DaemonCommand extends Command
$pid = intval(trim(file_get_contents($this->pidFilePath)));
if (function_exists('posix_kill')) {
// Use posix_kill with signal 0
if (@posix_kill($pid, 0)) {
// Signal 0 isn't sent but posix_kill checks the process anyway
return $pid;
}
} elseif (is_dir('/proc')) {
// Use linux's /proc filesystem
if (is_dir('/proc/' . $pid)) {
return $pid;
}
} else {
// Last resort: the ps command
exec(sprintf('ps %d', $pid), $output, $exitCode);
if ($exitCode === 0) {
return $pid;
}
if($this->processControl->isRunning($pid, true)) {
return $pid;
}
// Not found, remove the stale PID file
unlink($this->pidFilePath);
}
/** Kill a process
*
* @param int $pid
* @param int $signal
*/
protected function kill($pid, $signal = 15)
{
if (function_exists('posix_kill')) {
return posix_kill($pid, $signal);
}
exec(sprintf('kill -%d %d', $signal, $pid), null, $exitCode);
return $exitCode === 0;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\ProcessControl;
/**
* Construct an appropriate ProcessControl instance.
*
* @author Adirelle <adirelle@gmail.com>
*/
class Factory
{
/**
* ProcessControl singleton.
*
* @var ProcessControlInterface
*/
protected static $instance = null;
/**
* Returns the ProcessControl singleton.
*
* @return ProcessControlInterface
*/
public static function getInstance()
{
if (static::$instance === null) {
static::$instance = static::createProcessControl();
}
return static::$instance;
}
/**
* Create a ProcessControl depending on available extensions and the underlying OS.
*
* Check PosixProcessControl, WindowsProcessControl and UnixProcessControl, in that order.
*
* @return ProcessControlInterface
*
* @internal
*/
public static function createProcessControl()
{
switch(true) {
case PosixProcessControl::isAvailable():
return new PosixProcessControl();
case WindowsProcessControl::isAvailable():
return new WindowsProcessControl();
case UnixProcessControl::isAvailable():
return new UnixProcessControl();
}
throw new \Exception("No ProcessControl implementation available.");
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\ProcessControl;
/**
* Control process using the POSIX extension.
*
* @author Adirelle <adirelle@gmail.com>
*/
class PosixProcessControl implements ProcessControlInterface
{
/**
*
* @param int $pid
* @return bool
*/
public function isRunning($pid)
{
// Signal "0" is not sent to the process, but posix_kill checks the process anyway;
return posix_kill($pid, 0);
}
/**
* Sends a TERMINATE or KILL signal to the process using posix_kill.
*
* @param int $pid
* @param bool $forcefully Whetehr to send TERMINATE (false) or KILL (true).
*/
public function kill($pid, $forcefully = false)
{
posix_kill($pid, $forcefully ? 9 : 15);
}
/**
* Check whether this posix_kill is available.
*
* @return bool
*
* @internal
*/
public static function isAvailable()
{
return function_exists('posix_kill');
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\ProcessControl;
/**
* A stateless service to check and kill system processes.
*
* @author Adirelle <adirelle@gmail.com>
*/
interface ProcessControlInterface
{
/** Checks if a process exists.
*
* @param int $pid The process identifier.
*
* @return boolean true is the process is running, else false.
*/
public function isRunning($pid);
/** Terminate a running process.
*
* @param int $pid The process identifier.
* @param bool $forcefully Whether to gently (false) or forcefully (true) terminate the process.
*/
public function kill($pid, $forcefully = false);
}

View file

@ -0,0 +1,54 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\ProcessControl;
/**
* Control processes using the "ps" and "kill" commands.
*
* @author Adirelle <adirelle@gmail.com>
*/
class UnixProcessControl implements ProcessControlInterface
{
/**
* Check process using the "ps" command.
*
* @param int $pid
* @return boolean
*/
public function isRunning($pid)
{
$output = $exitCode = null;
exec(sprintf("ps %d", $pid), $output, $exitCode);
return $exitCode === 0;
}
/**
* Sends a signal using the "kill" command.
*
* @param int $pid
* @param bool $forcefully
*/
public function kill($pid, $forcefully = false)
{
exec(sprintf("kill -%d %d", $forcefully ? 9 : 15, $pid));
}
/**
* Check whether the commands "ps" and "kill" are available.
*
* @return bool
*
* @internal
*/
public static function isAvailable()
{
return DIRECTORY_SEPARATOR === '/' && exec("which ps") && exec("which kill");
}
}

View file

@ -0,0 +1,54 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\ProcessControl;
/**
* Control processes using the "tasklist" and "taskkill" commands.
*
* @author Adirelle <adirelle@gmail.com>
*/
class WindowsProcessControl implements ProcessControlInterface
{
/**
* Check if the process is running using the "tasklist" command.
*
* @param type $pid
* @return bool
*/
public function isRunning($pid)
{
$lastLine = exec(sprintf('tasklist /fi "PID eq %d" /nh /fo csv 2>nul:', $pid));
$record = str_getcsv($lastLine);
return isset($record[1]) && intval($record[1]) === $pid;
}
/**
* Terminate the process using the "taskkill" command.
*
* @param type $pid
* @param bool $forcefully
*/
public function kill($pid, $forcefully = false)
{
exec(sprintf("taskkill /t /pid %d %s 2>nul:", $pid, $forcefully ? '/f' : ''));
}
/**
* Check whether the commands "tasklist" and "taskkill" are available.
*
* @return bool
*
* @internal
*/
public static function isAvailable()
{
return DIRECTORY_SEPARATOR === '\\' && exec("where tasklist") && exec("where taskkill");
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Tests\PHPCI\ProcessControl;
use PHPCI\ProcessControl\PosixProcessControl;
class PosixProcessControlTest extends UnixProcessControlTest
{
protected function setUp()
{
$this->object = new PosixProcessControl();
}
public function testIsAvailable()
{
$this->assertEquals(function_exists('posix_kill'), PosixProcessControl::isAvailable());
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Tests\PHPCI\ProcessControl;
/**
* Some helpers to
*/
abstract class ProcessControlTest extends \PHPUnit_Framework_TestCase
{
/**
* @var type
*/
protected $process;
/**
* @var array
*/
protected $pipes;
/**
* @var \PHPCI\ProcessControl\ProcessControlInterface
*/
protected $object;
/** Starts a process.
*
* @return int The PID of the process.
*/
protected function startProcess()
{
$desc = array(array("pipe", "r"), array("pipe", "w"), array("pipe", "w"));
$this->pipes = array();
$this->process = proc_open($this->getTestCommand(), $desc, $this->pipes);
usleep(500);
$this->assertTrue(is_resource($this->process));
$this->assertTrue($this->isRunning());
$status = proc_get_status($this->process);
return $status['pid'];
}
/** End the running process.
*
* @return int
*/
protected function endProcess()
{
if (!is_resource($this->process)) {
return;
}
array_map('fclose', $this->pipes);
$exitCode = proc_close($this->process);
$this->assertFalse($this->isRunning());
$this->process = null;
return $exitCode;
}
/**
* @return bool
*/
protected function isRunning()
{
if (!is_resource($this->process)) {
return false;
}
$status = proc_get_status($this->process);
return $status['running'];
}
public function testIsRunning()
{
if (!$this->object->isAvailable()) {
$this->markTestSkipped();
}
$pid = $this->startProcess();
$this->assertTrue($this->object->isRunning($pid));
fwrite($this->pipes[0], PHP_EOL);
$exitCode = $this->endProcess();
$this->assertEquals(0, $exitCode);
$this->assertFalse($this->object->isRunning($pid));
}
public function testSoftKill()
{
if (!$this->object->isAvailable()) {
$this->markTestSkipped();
}
$pid = $this->startProcess();
$this->object->kill($pid);
usleep(500);
$this->assertFalse($this->isRunning());
}
public function testForcefullyKill()
{
if (!$this->object->isAvailable()) {
$this->markTestSkipped();
}
$pid = $this->startProcess();
$this->object->kill($pid, true);
usleep(500);
$this->assertFalse($this->isRunning());
}
abstract public function testIsAvailable();
abstract public function getTestCommand();
protected function tearDown()
{
parent::tearDown();
$this->endProcess();
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Tests\PHPCI\ProcessControl;
use PHPCI\ProcessControl\UnixProcessControl;
class UnixProcessControlTest extends ProcessControlTest
{
protected function setUp()
{
$this->object = new UnixProcessControl();
}
public function getTestCommand()
{
return "read SOMETHING";
}
public function testIsAvailable()
{
$this->assertEquals(DIRECTORY_SEPARATOR === '/', UnixProcessControl::isAvailable());
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Tests\PHPCI\ProcessControl;
use PHPCI\ProcessControl\WindowsProcessControl;
class WindowsProcessControlTest extends ProcessControlTest
{
protected function setUp()
{
$this->object = new WindowsProcessControl;
}
public function getTestCommand()
{
return "pause";
}
public function testIsAvailable()
{
$this->assertEquals(DIRECTORY_SEPARATOR === '\\', WindowsProcessControl::isAvailable());
}
}

View file

@ -28,6 +28,12 @@
}
},
"autoload-dev": {
"psr-4": {
"Tests\\PHPCI\\": "Tests/PHPCI/"
}
},
"require": {
"php": ">=5.3.8",
"ext-pdo": "*",