. * * @package LibreNMS * @link http://librenms.org * @copyright 2016 Tony Murray * @author Tony Murray */ namespace LibreNMS; use Exception; class Proc { /** * @var resource the process this object is responsible for */ private $_process; /** * @var array array of process pipes [stdin,stdout,stderr] */ private $_pipes; /** * @var bool if this process is synchronous (waits for output) */ private $_synchronous; /** * @var int|null hold the exit code, we can only get this on the first process_status after exit */ private $_exitcode = null; /** * Create and run a new process * Most arguments match proc_open() * * @param string $cmd the command to execute * @param array $descriptorspec the definition of pipes to initialize * @param null $cwd working directory to change to * @param array|null $env array of environment variables to set * @param bool $blocking set the output pipes to blocking (default: false) * @throws Exception the command was unable to execute */ public function __construct( $cmd, $descriptorspec = array( 0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w") ), $cwd = null, $env = null, $blocking = false ) { $this->_process = proc_open($cmd, $descriptorspec, $this->_pipes, $cwd, $env); if (!is_resource($this->_process)) { throw new Exception("Command failed: $cmd"); } stream_set_blocking($this->_pipes[1], $blocking); stream_set_blocking($this->_pipes[2], $blocking); $this->_synchronous = true; } /** * Called when this object goes out of scope or php exits * If it is still running, terminate the process */ public function __destruct() { if ($this->isRunning()) { $this->terminate(); } } /** * Get one of the pipes * 0 - stdin * 1 - stdout * 2 - stderr * * @param int $nr pipe number (0-2) * @return resource the pipe handle */ public function pipe($nr) { return $this->_pipes[$nr]; } /** * Send a command to this process and return the output * the output may not correspond to this command if this * process is not synchronous * If the command isn't terminated with a newline, add one * * @param $command * @return array */ public function sendCommand($command) { $this->sendInput($this->checkAddEOL($command)); return $this->getOutput(); } /** * Send data to stdin * * @param string $data the string to send */ public function sendInput($data) { fwrite($this->_pipes[0], $data); } /** * Gets the current output of the process * If this process is set to synchronous, wait for output * * @param int $timeout time to wait for output, only applies if this process is synchronous * @return array [stdout, stderr] */ public function getOutput($timeout = 15) { if ($this->_synchronous) { $pipes = array($this->_pipes[1], $this->_pipes[2]); $w = null; $x = null; stream_select($pipes, $w, $x, $timeout); } return array(stream_get_contents($this->_pipes[1]), stream_get_contents($this->_pipes[2])); } /** * Close all pipes for this process */ private function closePipes() { foreach ($this->_pipes as $pipe) { if (is_resource($pipe)) { fclose($pipe); } } } /** * Attempt to gracefully close this process * optionally send one last piece of input * such as a quit command * * ** Warning: this will block until the process closes. * Some processes might not close on their own. * * @param string $command the final command to send (appends newline if one is ommited) * @return int the exit status of this process (-1 means error) */ public function close($command = null) { if (isset($command)) { $this->sendInput($this->checkAddEOL($command)); } $this->closePipes(); return proc_close($this->_process); } /** * Forcibly close this process * Please attempt to run close() instead of this * This will be called when this object is destroyed if the process is still running * * @param int $timeout how many microseconds to wait before terminating (SIGKILL) * @param int $signal the signal to send * @throws Exception */ public function terminate($timeout = 3000, $signal = 15) { $status = $this->getStatus(); $this->closePipes(); $closed = proc_terminate($this->_process, $signal); $time = 0; while ($time < $timeout) { $closed = !$this->isRunning(); if ($closed) { break; } usleep(100); $time += 100; } if (!$closed) { // try harder if (function_exists('posix_kill')) { $killed = posix_kill($status['pid'], 9); //9 is the SIGKILL signal } else { $killed = proc_terminate($this->_process, 9); } proc_close($this->_process); if (!$killed && $this->isRunning()) { throw new Exception("Terminate failed!"); } } } /** * Get the status of this process * see proc_get_status() * * @return array status array */ public function getStatus() { $status = proc_get_status($this->_process); if ($status['running'] === false && is_null($this->_exitcode)) { $this->_exitcode = $status['exitcode']; } return $status; } /** * Check if this process is running * * @return bool */ public function isRunning() { if (!is_resource($this->_process)) { return false; } $st = $this->getStatus(); return isset($st['running']) && $st['running']; } /** * Returns the exit code from the process. * Will return null unless isRunning() or getStatus() has been run and returns false. * * @return int|null */ public function getExitCode() { return $this->_exitcode; } /** * If this process waits for output * @return boolean */ public function isSynchronous() { return $this->_synchronous; } /** * Set this process as synchronous, by default processes are synchronous * It is advisable not to change this mid way as output could get mixed up * or you could end up blocking until the getOutput timeout expires * * @param boolean $synchronous */ public function setSynchronous($synchronous) { $this->_synchronous = $synchronous; } /** * Add and end of line character to a string if * it doesn't already end with one * * @param $string * @return string */ private function checkAddEOL($string) { if (!ends_with($string, PHP_EOL)) { $string .= PHP_EOL; } return $string; } }