. * * @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; /** * 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 ) { echo "Starting process: $cmd"; $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 $signal the signal to send * @throws Exception */ public function terminate($signal = 15) { $status = $this->getStatus(); $this->closePipes(); $closed = proc_terminate($this->_process, $signal); if (!$closed) { // try harder $pid = $status['pid']; $killed = posix_kill($pid, 9); //9 is the SIGKILL signal proc_close($this->_process); if (!$killed) { throw new Exception("Terminate failed!"); } } } /** * Get the status of this process * see proc_get_status() * * @return array status array */ public function getStatus() { return proc_get_status($this->_process); } /** * 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']); } /** * 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; } }