2017-01-11 19:24:02 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\Process;
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
use Amp\Loop;
|
2017-09-14 19:34:18 +02:00
|
|
|
use Amp\Process\Internal\Posix\Runner as PosixProcessRunner;
|
|
|
|
use Amp\Process\Internal\ProcessHandle;
|
|
|
|
use Amp\Process\Internal\ProcessRunner;
|
|
|
|
use Amp\Process\Internal\ProcessStatus;
|
|
|
|
use Amp\Process\Internal\Windows\Runner as WindowsProcessRunner;
|
2017-06-15 19:02:50 +02:00
|
|
|
use Amp\Promise;
|
2018-10-15 06:24:35 +02:00
|
|
|
use function Amp\call;
|
2017-01-11 19:24:02 +01:00
|
|
|
|
2018-10-20 18:34:05 +02:00
|
|
|
final class Process
|
2018-10-15 06:16:09 +02:00
|
|
|
{
|
2017-09-14 19:34:18 +02:00
|
|
|
/** @var ProcessRunner */
|
2017-09-17 19:07:13 +02:00
|
|
|
private $processRunner;
|
2017-01-11 19:24:02 +01:00
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $command;
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $cwd = "";
|
|
|
|
|
|
|
|
/** @var array */
|
|
|
|
private $env = [];
|
|
|
|
|
|
|
|
/** @var array */
|
|
|
|
private $options;
|
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
/** @var ProcessHandle */
|
|
|
|
private $handle;
|
2017-01-16 19:37:00 +01:00
|
|
|
|
2018-10-15 06:24:35 +02:00
|
|
|
/** @var int|null */
|
|
|
|
private $pid;
|
|
|
|
|
2017-01-11 19:24:02 +01:00
|
|
|
/**
|
2017-09-17 17:58:05 +02:00
|
|
|
* @param string|string[] $command Command to run.
|
|
|
|
* @param string|null $cwd Working directory or use an empty string to use the working directory of the
|
|
|
|
* parent.
|
|
|
|
* @param mixed[] $env Environment variables or use an empty array to inherit from the parent.
|
|
|
|
* @param mixed[] $options Options for `proc_open()`.
|
|
|
|
*
|
|
|
|
* @throws \Error If the arguments are invalid.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function __construct($command, string $cwd = null, array $env = [], array $options = [])
|
|
|
|
{
|
2017-09-17 17:58:05 +02:00
|
|
|
$command = \is_array($command)
|
2019-02-26 05:50:25 +01:00
|
|
|
? \implode(" ", \array_map(__NAMESPACE__ . "\\escapeArguments", $command))
|
2017-09-17 17:58:05 +02:00
|
|
|
: (string) $command;
|
|
|
|
|
|
|
|
$cwd = $cwd ?? "";
|
|
|
|
|
|
|
|
$envVars = [];
|
|
|
|
foreach ($env as $key => $value) {
|
|
|
|
if (\is_array($value)) {
|
|
|
|
throw new \Error("\$env cannot accept array values");
|
|
|
|
}
|
|
|
|
|
|
|
|
$envVars[(string) $key] = (string) $value;
|
|
|
|
}
|
|
|
|
|
2017-01-11 19:24:02 +01:00
|
|
|
$this->command = $command;
|
2017-09-14 19:34:18 +02:00
|
|
|
$this->cwd = $cwd;
|
2017-09-17 17:58:05 +02:00
|
|
|
$this->env = $envVars;
|
2017-01-11 19:24:02 +01:00
|
|
|
$this->options = $options;
|
2017-09-17 19:07:13 +02:00
|
|
|
|
|
|
|
$this->processRunner = Loop::getState(self::class);
|
|
|
|
|
|
|
|
if ($this->processRunner === null) {
|
2019-02-26 05:38:36 +01:00
|
|
|
$this->processRunner = IS_WINDOWS
|
2017-09-17 19:07:13 +02:00
|
|
|
? new WindowsProcessRunner
|
|
|
|
: new PosixProcessRunner;
|
|
|
|
|
|
|
|
Loop::setState(self::class, $this->processRunner);
|
|
|
|
}
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the process if it is still running.
|
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function __destruct()
|
|
|
|
{
|
2017-09-14 19:34:18 +02:00
|
|
|
if ($this->handle !== null) {
|
2017-09-17 19:07:13 +02:00
|
|
|
$this->processRunner->destroy($this->handle);
|
2017-06-15 22:38:07 +02:00
|
|
|
}
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
2018-10-15 06:16:09 +02:00
|
|
|
public function __clone()
|
|
|
|
{
|
2017-09-17 17:58:05 +02:00
|
|
|
throw new \Error("Cloning is not allowed!");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-17 17:58:05 +02:00
|
|
|
* Start the process.
|
2017-09-14 19:34:18 +02:00
|
|
|
*
|
2018-10-15 06:24:35 +02:00
|
|
|
* @return Promise<int> Resolves with the PID.
|
|
|
|
*
|
2017-09-17 17:58:05 +02:00
|
|
|
* @throws StatusError If the process has already been started.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:24:35 +02:00
|
|
|
public function start(): Promise
|
2018-10-15 06:16:09 +02:00
|
|
|
{
|
2017-09-17 17:58:05 +02:00
|
|
|
if ($this->handle) {
|
|
|
|
throw new StatusError("Process has already been started.");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
2018-10-15 06:24:35 +02:00
|
|
|
return call(function () {
|
|
|
|
$this->handle = $this->processRunner->start($this->command, $this->cwd, $this->env, $this->options);
|
|
|
|
return $this->pid = yield $this->handle->pidDeferred->promise();
|
|
|
|
});
|
2017-03-17 04:19:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-17 17:58:05 +02:00
|
|
|
* Wait for the process to end.
|
2017-09-14 19:34:18 +02:00
|
|
|
*
|
|
|
|
* @return Promise <int> Succeeds with process exit code or fails with a ProcessException if the process is killed.
|
2017-09-17 17:58:05 +02:00
|
|
|
*
|
|
|
|
* @throws StatusError If the process has already been started.
|
2017-03-17 04:19:15 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function join(): Promise
|
|
|
|
{
|
2017-09-17 17:58:05 +02:00
|
|
|
if (!$this->handle) {
|
|
|
|
throw new StatusError("Process has not been started.");
|
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
return $this->processRunner->join($this->handle);
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-14 19:34:18 +02:00
|
|
|
* Forcibly end the process.
|
|
|
|
*
|
2017-09-17 17:58:05 +02:00
|
|
|
* @throws StatusError If the process is not running.
|
|
|
|
* @throws ProcessException If terminating the process fails.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function kill()
|
|
|
|
{
|
2017-09-14 19:34:18 +02:00
|
|
|
if (!$this->isRunning()) {
|
2017-12-05 06:02:41 +01:00
|
|
|
throw new StatusError("Process is not running.");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
2017-09-14 19:34:18 +02:00
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
$this->processRunner->kill($this->handle);
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-14 19:34:18 +02:00
|
|
|
* Send a signal signal to the process.
|
2017-01-11 19:24:02 +01:00
|
|
|
*
|
|
|
|
* @param int $signo Signal number to send to process.
|
2017-09-17 17:58:05 +02:00
|
|
|
*
|
|
|
|
* @throws StatusError If the process is not running.
|
|
|
|
* @throws ProcessException If sending the signal fails.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function signal(int $signo)
|
|
|
|
{
|
2017-01-11 19:24:02 +01:00
|
|
|
if (!$this->isRunning()) {
|
2017-12-05 06:02:41 +01:00
|
|
|
throw new StatusError("Process is not running.");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
$this->processRunner->signal($this->handle, $signo);
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-09-14 19:34:18 +02:00
|
|
|
* Returns the PID of the child process.
|
2017-01-11 19:24:02 +01:00
|
|
|
*
|
2018-10-15 06:24:35 +02:00
|
|
|
* @return int
|
2017-09-17 17:58:05 +02:00
|
|
|
*
|
2018-10-15 06:24:35 +02:00
|
|
|
* @throws StatusError If the process has not started or has not completed starting.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:24:35 +02:00
|
|
|
public function getPid(): int
|
2018-10-15 06:16:09 +02:00
|
|
|
{
|
2018-10-15 06:24:35 +02:00
|
|
|
if (!$this->pid) {
|
|
|
|
throw new StatusError("Process has not been started or has not completed starting.");
|
2017-09-17 17:58:05 +02:00
|
|
|
}
|
|
|
|
|
2018-10-15 06:24:35 +02:00
|
|
|
return $this->pid;
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the command to execute.
|
|
|
|
*
|
|
|
|
* @return string The command to execute.
|
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getCommand(): string
|
|
|
|
{
|
2017-01-11 19:24:02 +01:00
|
|
|
return $this->command;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the current working directory.
|
|
|
|
*
|
2017-01-15 16:23:32 +01:00
|
|
|
* @return string The current working directory an empty string if inherited from the current PHP process.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getWorkingDirectory(): string
|
|
|
|
{
|
2017-01-11 19:24:02 +01:00
|
|
|
if ($this->cwd === "") {
|
|
|
|
return \getcwd() ?: "";
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->cwd;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the environment variables array.
|
|
|
|
*
|
2017-01-15 16:23:32 +01:00
|
|
|
* @return string[] Array of environment variables.
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getEnv(): array
|
|
|
|
{
|
2017-01-11 19:24:02 +01:00
|
|
|
return $this->env;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the options to pass to proc_open().
|
|
|
|
*
|
|
|
|
* @return mixed[] Array of options.
|
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getOptions(): array
|
|
|
|
{
|
2017-01-11 19:24:02 +01:00
|
|
|
return $this->options;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if the process is still running.
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function isRunning(): bool
|
|
|
|
{
|
2017-09-17 17:58:05 +02:00
|
|
|
return $this->handle && $this->handle->status !== ProcessStatus::ENDED;
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the process input stream (STDIN).
|
|
|
|
*
|
2017-09-17 17:58:05 +02:00
|
|
|
* @return ProcessOutputStream
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getStdin(): ProcessOutputStream
|
|
|
|
{
|
2018-10-15 16:47:28 +02:00
|
|
|
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
|
|
|
throw new StatusError("Process has not been started or has not completed starting.");
|
2017-09-19 16:16:48 +02:00
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
return $this->handle->stdin;
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the process output stream (STDOUT).
|
|
|
|
*
|
2017-09-17 17:58:05 +02:00
|
|
|
* @return ProcessInputStream
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getStdout(): ProcessInputStream
|
|
|
|
{
|
2018-10-15 16:47:28 +02:00
|
|
|
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
|
|
|
throw new StatusError("Process has not been started or has not completed starting.");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
return $this->handle->stdout;
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the process error stream (STDERR).
|
|
|
|
*
|
2017-09-17 17:58:05 +02:00
|
|
|
* @return ProcessInputStream
|
2017-01-11 19:24:02 +01:00
|
|
|
*/
|
2018-10-15 06:16:09 +02:00
|
|
|
public function getStderr(): ProcessInputStream
|
|
|
|
{
|
2018-10-15 16:47:28 +02:00
|
|
|
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
|
|
|
throw new StatusError("Process has not been started or has not completed starting.");
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
return $this->handle->stderr;
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|
2018-10-15 17:12:17 +02:00
|
|
|
|
|
|
|
public function __debugInfo(): array
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
'command' => $this->getCommand(),
|
|
|
|
'cwd' => $this->getWorkingDirectory(),
|
|
|
|
'env' => $this->getEnv(),
|
|
|
|
'options' => $this->getOptions(),
|
|
|
|
'pid' => $this->pid,
|
|
|
|
'status' => $this->handle ? $this->handle->status : -1,
|
|
|
|
];
|
|
|
|
}
|
2017-01-11 19:24:02 +01:00
|
|
|
}
|