diff --git a/PHPCI/Command/DaemonCommand.php b/PHPCI/Command/DaemonCommand.php index e51f31f1..cb303eff 100644 --- a/PHPCI/Command/DaemonCommand.php +++ b/PHPCI/Command/DaemonCommand.php @@ -1,4 +1,5 @@ -* @package PHPCI -* @subpackage Console -*/ + * Daemon that loops and call the run-command. + * @author Gabriel Baker + * @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("Not a valid choice, please use start, stop or status"); 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); + } } diff --git a/PHPCI/ProcessControl/Factory.php b/PHPCI/ProcessControl/Factory.php new file mode 100644 index 00000000..a1aa2354 --- /dev/null +++ b/PHPCI/ProcessControl/Factory.php @@ -0,0 +1,63 @@ + + */ +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."); + } +} diff --git a/PHPCI/ProcessControl/PosixProcessControl.php b/PHPCI/ProcessControl/PosixProcessControl.php new file mode 100644 index 00000000..710ba9f1 --- /dev/null +++ b/PHPCI/ProcessControl/PosixProcessControl.php @@ -0,0 +1,52 @@ + + */ +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'); + } +} diff --git a/PHPCI/ProcessControl/ProcessControlInterface.php b/PHPCI/ProcessControl/ProcessControlInterface.php new file mode 100644 index 00000000..709e0bee --- /dev/null +++ b/PHPCI/ProcessControl/ProcessControlInterface.php @@ -0,0 +1,33 @@ + + */ +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); +} diff --git a/PHPCI/ProcessControl/UnixProcessControl.php b/PHPCI/ProcessControl/UnixProcessControl.php new file mode 100644 index 00000000..8b638073 --- /dev/null +++ b/PHPCI/ProcessControl/UnixProcessControl.php @@ -0,0 +1,54 @@ + + */ +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"); + } +} diff --git a/PHPCI/ProcessControl/WindowsProcessControl.php b/PHPCI/ProcessControl/WindowsProcessControl.php new file mode 100644 index 00000000..e750d321 --- /dev/null +++ b/PHPCI/ProcessControl/WindowsProcessControl.php @@ -0,0 +1,54 @@ + + */ +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"); + } +} diff --git a/Tests/PHPCI/ProcessControl/PosixProcessControlTest.php b/Tests/PHPCI/ProcessControl/PosixProcessControlTest.php new file mode 100644 index 00000000..96bbf5b4 --- /dev/null +++ b/Tests/PHPCI/ProcessControl/PosixProcessControlTest.php @@ -0,0 +1,17 @@ +object = new PosixProcessControl(); + } + + public function testIsAvailable() + { + $this->assertEquals(function_exists('posix_kill'), PosixProcessControl::isAvailable()); + } +} diff --git a/Tests/PHPCI/ProcessControl/ProcessControlTest.php b/Tests/PHPCI/ProcessControl/ProcessControlTest.php new file mode 100644 index 00000000..52022743 --- /dev/null +++ b/Tests/PHPCI/ProcessControl/ProcessControlTest.php @@ -0,0 +1,126 @@ +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(); + } +} diff --git a/Tests/PHPCI/ProcessControl/UnixProcessControlTest.php b/Tests/PHPCI/ProcessControl/UnixProcessControlTest.php new file mode 100644 index 00000000..9b102a73 --- /dev/null +++ b/Tests/PHPCI/ProcessControl/UnixProcessControlTest.php @@ -0,0 +1,22 @@ +object = new UnixProcessControl(); + } + + public function getTestCommand() + { + return "read SOMETHING"; + } + + public function testIsAvailable() + { + $this->assertEquals(DIRECTORY_SEPARATOR === '/', UnixProcessControl::isAvailable()); + } +} diff --git a/Tests/PHPCI/ProcessControl/WindowsProcessControlTest.php b/Tests/PHPCI/ProcessControl/WindowsProcessControlTest.php new file mode 100644 index 00000000..ed10fc95 --- /dev/null +++ b/Tests/PHPCI/ProcessControl/WindowsProcessControlTest.php @@ -0,0 +1,22 @@ +object = new WindowsProcessControl; + } + + public function getTestCommand() + { + return "pause"; + } + + public function testIsAvailable() + { + $this->assertEquals(DIRECTORY_SEPARATOR === '\\', WindowsProcessControl::isAvailable()); + } +} diff --git a/composer.json b/composer.json index c6d8e92b..eadd9a81 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,12 @@ } }, + "autoload-dev": { + "psr-4": { + "Tests\\PHPCI\\": "Tests/PHPCI/" + } + }, + "require": { "php": ">=5.3.8", "ext-pdo": "*",