<?php
/**
* Process.
*
* The Process class is a cross-platform OOP wrapper for the PHP's built-in
* program execution functions (proc_* and the like).
*
* The constructor creates a native process, which is represented by a
* Process object. This object can then be used to control the process and
* obtain information about it.
*
* The subprocess is not killed when there are no more references to the
* Process object.
*
* Aimed at PHP 5.2.0 or greater.
*
* @author Jaka Jancar [hide@address.com] [http://jaka.kubje.org/]
* @link http://jaka.kubje.org/projects/process/
* @version 1.0
*/
class Process {
/**
* Process resurce.
*
* @var resource
*/
private $process;
/**
* Standard input stream of the process.
*
* @var resource
*/
private $stdin;
/**
* Standard output stream of the process.
*
* @var resource
*/
private $stdout;
/**
* Standard error stream of the process.
*
* @var resource
*/
private $stderr;
// The following fields are updated by updateStatus()
/**
* Process id
*
* @var int
*/
private $pid;
/**
* The last known state of the process.
*
* @var bool
*/
private $isRunning;
/**
* Exit code of the process if it has terminated.
*
* @var int
*/
private $exitCode;
/**
* Starts a new process.
*
* Possible options:
* - no_capture_stderr (bool)
* Do not capture stderr. Default: false.
* - proc_open_options (array)
* The array of options to pass to proc_open(). default: array()
*
* @param string $cmd
* @param string|NULL $cwd
* @param array|NULL $env
* @param array $options
*/
public function __construct($cmd, $cwd=NULL, $env=NULL, $options=array()) {
$descriptorspec = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
);
$captureStderr = (isset($options['no_capture_stderr']) and ($options['no_capture_stderr'] === true)) ? false : true;
if ($captureStderr)
$descriptorspec[2] = array('pipe', 'w');
$procOpenOptions = isset($options['proc_open_options']) ? $options['proc_open_options'] : array();
$this->process = proc_open($cmd, $descriptorspec, $pipes, $cwd, $env, $procOpenOptions);
if ($this->process === false)
throw new Exception("Error while opening the process.");
if (!is_resource($this->process))
throw new Exception("Return value of proc_open is not a resource.");
$this->isRunning = true;
$this->stdin = $pipes[0];
$this->stdout = $pipes[1];
$this->stderr = $captureStderr ? $pipes[2] : NULL;
}
/**
* Gets the input stream of the process.
*
* @return resource
*/
public function getInputStream() {
return $this->stdin;
}
/**
* Gets the output stream of the process.
*
* @return resource
*/
public function getOutputStream() {
return $this->stdout;
}
/**
* Gets the error stream of the process.
*
* @return resource
*/
public function getErrorStream() {
return $this->stderr;
}
/**
* Returns the PID of the process.
*
* @return int
*/
public function getPid() {
$this->updateStatus();
return $this->pid;
}
/**
* Returns the status of the process.
*
* If the process is still running, true is returned. If the process has
* already terminated, false is returned.
*
* @return bool
*/
public function isRunning() {
$this->updateStatus();
return $this->isRunning;
}
/**
* Wait, if necessary, until the process has terminated.
*
* This method returns immediately if the subprocess has already
* terminated.
*
* If the subprocess has not yet terminated, the call will block until
* the subprocess exits.
*
* Returns the exit code returned by the process.
*
* @return int
*/
public function close() {
$this->updateStatus();
if (is_null($this->process))
return $this->exitCode; // process resource already destroyed
// Close standard streams
fclose($this->stdin);
fclose($this->stdout);
if (isset($this->stderr)) // not set if option no_capture_stderr is used
fclose($this->stderr);
$termStatus = proc_close($this->process);
// See: http://bugs.php.net/bug.php?id=17538
// If it was us that terminated the process, we must store the exit
// code, since it won't be avaialble the next time proc_get_status()
// is called (the resource will be invalid).
if ($this->isRunning) {
$this->exitCode = ($termStatus >> 8) & 0xff;
$this->isRunning = false;
}
// destroy resource
$this->process = NULL;
return $this->exitCode;
}
/**
* Kills the process.
*
* The process represented by this Process object is forcibly terminated.
*
*/
public function kill() {
// Update status to check if it was already terminated or to store
// the pid before termination.
$this->updateStatus();
if (!$this->isRunning)
return; // process not running anymore
if (!proc_terminate($this->process, SIGKILL))
throw new Exception("Cannot kill the process.");
// proc_terminate() does not destroy the resource on php 5.2.1 or
// earlier. We destroy it manually to be more deterministic.
$this->process = NULL;
}
/**
* The exit code returned by the process.
*
* @return int
* @throws Exception if process is still running.
*/
public function getExitCode() {
$this->updateStatus();
if ($this->isRunning)
throw new Exception("Cannot get exit code, the process with PID $this->pid is still running.");
return $this->exitCode;
}
/**
* Updates the status of the process.
*
* This currently includes the following properties:
* - process resource
* - pid
* - isRunning
* - exitCode
*
* Do not call proc_get_status() directly, use this function instead.
* This is needed because the exit code returned by the process is
* returned only on the first call to proc_get_status() (subsequent
* calls return -1), and so the value must be stored, if it is received
* at any time.
*
*/
private function updateStatus() {
if (!$this->isRunning)
return; // process not running anymore, no more updating
$status = proc_get_status($this->process);
// Store pid if we don't have it yet
if (!isset($this->pid))
$this->pid = $status['pid'];
// Store exit code if process exited, and set it as not running
if ($status['running'] === false) {
$this->exitCode = $status['exitcode'];
$this->isRunning = false;
}
}
}