Reworked the DaemonCommand.

* Accepts options for PID and log file.
* Uses posix_kill whenever available.
* Checks that the daemon actually started or stopped.
* Try to terminate then kill the daemon.
* Uses the logger or output instead of "echo".

Added a ProcessControl interface and implementations.

Closed #908
This commit is contained in:
Adirelle 2015-04-09 08:51:45 +02:00 committed by Tobias van Beek
parent 15b6917f68
commit 4edefee761
11 changed files with 573 additions and 46 deletions

View file

@ -1,4 +1,5 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
@ -10,28 +11,48 @@
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;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
class DaemonCommand extends Command
{
/**
* @var \Monolog\Logger
* @var Logger
*/
protected $logger;
public function __construct(Logger $logger, $name = null)
/**
* @var string
*/
protected $pidFilePath;
/**
* @var string
*/
protected $logFilePath;
/**
* @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()
@ -40,17 +61,30 @@ class DaemonCommand extends Command
->setName('phpci:daemon')
->setDescription('Initiates the daemon to run commands.')
->addArgument(
'state',
InputArgument::REQUIRED,
'start|stop|status'
);
'state', InputArgument::REQUIRED, 'start|stop|status'
)
->addOption(
'pid-file', 'p', InputOption::VALUE_REQUIRED,
'Path of the PID file',
implode(DIRECTORY_SEPARATOR,
array(PHPCI_DIR, 'daemon', 'daemon.pid'))
)
->addOption(
'log-file', 'l', InputOption::VALUE_REQUIRED,
'Path of the log file',
implode(DIRECTORY_SEPARATOR,
array(PHPCI_DIR, 'daemon', 'daemon.log'))
);
}
/**
* Loops through running.
*/
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->pidFilePath = $input->getOption('pid-file');
$this->logFilePath = $input->getOption('log-file');
$state = $input->getArgument('state');
switch ($state) {
@ -61,64 +95,108 @@ class DaemonCommand extends Command
$this->stopDaemon();
break;
case 'status':
$this->statusDaemon();
$this->statusDaemon($output);
break;
default:
echo "Not a valid choice, please use start stop or status";
$this->output->writeln("<error>Not a valid choice, please use start, stop or status</error>");
break;
}
}
protected function startDaemon()
{
if (file_exists(PHPCI_DIR.'/daemon/daemon.pid')) {
echo "Already started\n";
$this->logger->warning("Daemon already started");
$pid = $this->getRunningPid();
if ($pid) {
$this->logger->notice("Daemon already started", array('pid' => $pid));
return "alreadystarted";
}
$logfile = PHPCI_DIR."/daemon/daemon.log";
$this->logger->info("Trying to start the daemon");
$cmd = "nohup %s/daemonise phpci:daemonise > %s 2>&1 &";
$command = sprintf($cmd, PHPCI_DIR, $logfile);
$this->logger->info("Daemon started");
exec($command);
$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));
return "notstarted";
}
for ($i = 0; !($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
if (!$pid) {
$this->logger->error("Could not start the daemon");
return "notstarted";
}
$this->logger->notice("Daemon started", array('pid' => $pid));
return "started";
}
protected function stopDaemon()
{
if (!file_exists(PHPCI_DIR.'/daemon/daemon.pid')) {
echo "Not started\n";
$this->logger->warning("Can't stop daemon as not started");
$pid = $this->getRunningPid();
if (!$pid) {
$this->logger->notice("Cannot stop the daemon as it is not started");
return "notstarted";
}
$cmd = "kill $(cat %s/daemon/daemon.pid)";
$command = sprintf($cmd, PHPCI_DIR);
exec($command);
$this->logger->info("Daemon stopped");
unlink(PHPCI_DIR.'/daemon/daemon.pid');
}
$this->logger->info("Trying to terminate the daemon", array('pid' => $pid));
$this->processControl->kill($pid);
protected function statusDaemon()
{
if (!file_exists(PHPCI_DIR.'/daemon/daemon.pid')) {
echo "Not running\n";
return "notrunning";
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
$pid = trim(file_get_contents(PHPCI_DIR.'/daemon/daemon.pid'));
$pidcheck = sprintf("/proc/%s", $pid);
if (is_dir($pidcheck)) {
echo "Running\n";
if ($pid) {
$this->logger->warning("The daemon is resiting, trying to kill it", array('pid' => $pid));
$this->processControl->kill($pid, true);
for ($i = 0; ($pid = $this->getRunningPid()) && $i < 5; $i++) {
sleep(1);
}
}
if (!$pid) {
$this->logger->notice("Daemon stopped");
return "stopped";
}
$this->logger->error("Could not stop the daemon");
}
protected function statusDaemon(OutputInterface $output)
{
$pid = $this->getRunningPid();
if ($pid) {
$output->writeln(sprintf('The daemon is running, PID: %d', $pid));
return "running";
}
unlink(PHPCI_DIR.'/daemon/daemon.pid');
echo "Not running\n";
$output->writeln('The daemon is not running');
return "notrunning";
}
/** Check if there is a running daemon
*
* @return int|null
*/
protected function getRunningPid()
{
if (!file_exists($this->pidFilePath)) {
return;
}
$pid = intval(trim(file_get_contents($this->pidFilePath)));
if($this->processControl->isRunning($pid, true)) {
return $pid;
}
// Not found, remove the stale PID file
unlink($this->pidFilePath);
}
}

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": "*",