Initial Commit
This commit is contained in:
48
dependencies/amphp/process/lib/Internal/Posix/Handle.php
vendored
Normal file
48
dependencies/amphp/process/lib/Internal/Posix/Handle.php
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Posix;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessHandle;
|
||||
/** @internal */
|
||||
final class Handle extends ProcessHandle
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->pidDeferred = new Deferred();
|
||||
$this->joinDeferred = new Deferred();
|
||||
$this->originalParentPid = \getmypid();
|
||||
}
|
||||
/** @var Deferred */
|
||||
public $joinDeferred;
|
||||
/** @var resource */
|
||||
public $proc;
|
||||
/** @var resource */
|
||||
public $extraDataPipe;
|
||||
/** @var string */
|
||||
public $extraDataPipeWatcher;
|
||||
/** @var string */
|
||||
public $extraDataPipeStartWatcher;
|
||||
/** @var int */
|
||||
public $originalParentPid;
|
||||
/** @var int */
|
||||
public $shellPid;
|
||||
public function wait()
|
||||
{
|
||||
if ($this->shellPid === 0) {
|
||||
return;
|
||||
}
|
||||
$pid = $this->shellPid;
|
||||
$this->shellPid = 0;
|
||||
Loop::unreference(Loop::repeat(100, static function (string $watcherId) use($pid) {
|
||||
if (!\extension_loaded('pcntl') || \pcntl_waitpid($pid, $status, \WNOHANG) !== 0) {
|
||||
Loop::cancel($watcherId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
$this->wait();
|
||||
}
|
||||
}
|
207
dependencies/amphp/process/lib/Internal/Posix/Runner.php
vendored
Normal file
207
dependencies/amphp/process/lib/Internal/Posix/Runner.php
vendored
Normal file
@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Posix;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessHandle;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessRunner;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessStatus;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessException;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/** @internal */
|
||||
final class Runner implements ProcessRunner
|
||||
{
|
||||
const FD_SPEC = [
|
||||
["pipe", "r"],
|
||||
// stdin
|
||||
["pipe", "w"],
|
||||
// stdout
|
||||
["pipe", "w"],
|
||||
// stderr
|
||||
["pipe", "w"],
|
||||
];
|
||||
/** @var string|null */
|
||||
private static $fdPath;
|
||||
public static function onProcessEndExtraDataPipeReadable($watcher, $stream, Handle $handle)
|
||||
{
|
||||
Loop::cancel($watcher);
|
||||
$handle->extraDataPipeWatcher = null;
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
if (!\is_resource($stream) || \feof($stream)) {
|
||||
$handle->joinDeferred->fail(new ProcessException("Process ended unexpectedly"));
|
||||
} else {
|
||||
$handle->joinDeferred->resolve((int) \rtrim(@\stream_get_contents($stream)));
|
||||
}
|
||||
$handle->wait();
|
||||
}
|
||||
public static function onProcessStartExtraDataPipeReadable($watcher, $stream, $data)
|
||||
{
|
||||
Loop::cancel($watcher);
|
||||
$pid = \rtrim(@\fgets($stream));
|
||||
/** @var $deferreds Deferred[] */
|
||||
list($handle, $pipes, $deferreds) = $data;
|
||||
if (!$pid || !\is_numeric($pid)) {
|
||||
$error = new ProcessException("Could not determine PID");
|
||||
$handle->pidDeferred->fail($error);
|
||||
foreach ($deferreds as $deferred) {
|
||||
/** @var $deferred Deferred */
|
||||
$deferred->fail($error);
|
||||
}
|
||||
if ($handle->status < ProcessStatus::ENDED) {
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
$handle->joinDeferred->fail($error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$handle->status = ProcessStatus::RUNNING;
|
||||
$handle->pidDeferred->resolve((int) $pid);
|
||||
$deferreds[0]->resolve($pipes[0]);
|
||||
$deferreds[1]->resolve($pipes[1]);
|
||||
$deferreds[2]->resolve($pipes[2]);
|
||||
if ($handle->extraDataPipeWatcher !== null) {
|
||||
Loop::enable($handle->extraDataPipeWatcher);
|
||||
}
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function start(string $command, string $cwd = null, array $env = [], array $options = []) : ProcessHandle
|
||||
{
|
||||
$command = \sprintf('{ (%s) <&3 3<&- 3>/dev/null & } 3<&0; trap "" INT TERM QUIT HUP;' . 'pid=$!; echo $pid >&3; wait $pid; RC=$?; echo $RC >&3; exit $RC', $command);
|
||||
$handle = new Handle();
|
||||
$handle->proc = @\proc_open($command, $this->generateFds(), $pipes, $cwd ?: null, $env ?: null, $options);
|
||||
if (!\is_resource($handle->proc)) {
|
||||
$message = "Could not start process";
|
||||
if ($error = \error_get_last()) {
|
||||
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||
}
|
||||
throw new ProcessException($message);
|
||||
}
|
||||
$status = \proc_get_status($handle->proc);
|
||||
if (!$status) {
|
||||
\proc_close($handle->proc);
|
||||
throw new ProcessException("Could not get process status");
|
||||
}
|
||||
$handle->shellPid = \proc_get_status($handle->proc)['pid'];
|
||||
$stdinDeferred = new Deferred();
|
||||
$handle->stdin = new ProcessOutputStream($stdinDeferred->promise());
|
||||
$stdoutDeferred = new Deferred();
|
||||
$handle->stdout = new ProcessInputStream($stdoutDeferred->promise());
|
||||
$stderrDeferred = new Deferred();
|
||||
$handle->stderr = new ProcessInputStream($stderrDeferred->promise());
|
||||
$handle->extraDataPipe = $pipes[3];
|
||||
\stream_set_blocking($pipes[3], \false);
|
||||
$handle->extraDataPipeStartWatcher = Loop::onReadable($pipes[3], [self::class, 'onProcessStartExtraDataPipeReadable'], [$handle, [new ResourceOutputStream($pipes[0]), new ResourceInputStream($pipes[1]), new ResourceInputStream($pipes[2])], [$stdinDeferred, $stdoutDeferred, $stderrDeferred]]);
|
||||
$handle->extraDataPipeWatcher = Loop::onReadable($pipes[3], [self::class, 'onProcessEndExtraDataPipeReadable'], $handle);
|
||||
Loop::unreference($handle->extraDataPipeWatcher);
|
||||
Loop::disable($handle->extraDataPipeWatcher);
|
||||
return $handle;
|
||||
}
|
||||
private function generateFds() : array
|
||||
{
|
||||
if (self::$fdPath === null) {
|
||||
self::$fdPath = \file_exists("/dev/fd") ? "/dev/fd" : "/proc/self/fd";
|
||||
}
|
||||
$fdList = @\scandir(self::$fdPath, \SCANDIR_SORT_NONE);
|
||||
if ($fdList === \false) {
|
||||
throw new ProcessException("Unable to list open file descriptors");
|
||||
}
|
||||
$fdList = \array_filter($fdList, function (string $path) : bool {
|
||||
return $path !== "." && $path !== "..";
|
||||
});
|
||||
$fds = [];
|
||||
foreach ($fdList as $id) {
|
||||
$fds[(int) $id] = ["file", "/dev/null", "r"];
|
||||
}
|
||||
return self::FD_SPEC + $fds;
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function join(ProcessHandle $handle) : Promise
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
if ($handle->extraDataPipeWatcher !== null) {
|
||||
Loop::reference($handle->extraDataPipeWatcher);
|
||||
}
|
||||
return $handle->joinDeferred->promise();
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function kill(ProcessHandle $handle)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
if ($handle->extraDataPipeWatcher !== null) {
|
||||
Loop::cancel($handle->extraDataPipeWatcher);
|
||||
$handle->extraDataPipeWatcher = null;
|
||||
}
|
||||
/** @var Handle $handle */
|
||||
if ($handle->extraDataPipeStartWatcher !== null) {
|
||||
Loop::cancel($handle->extraDataPipeStartWatcher);
|
||||
$handle->extraDataPipeStartWatcher = null;
|
||||
}
|
||||
if (!\proc_terminate($handle->proc, 9)) {
|
||||
// Forcefully kill the process using SIGKILL.
|
||||
throw new ProcessException("Terminating process failed");
|
||||
}
|
||||
$handle->pidDeferred->promise()->onResolve(function ($error, $pid) {
|
||||
// The function should not call posix_kill() if $pid is null (i.e., there was an error starting the process).
|
||||
if ($error) {
|
||||
return;
|
||||
}
|
||||
// ignore errors because process not always detached
|
||||
@\posix_kill($pid, 9);
|
||||
});
|
||||
if ($handle->status < ProcessStatus::ENDED) {
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
$handle->joinDeferred->fail(new ProcessException("The process was killed"));
|
||||
}
|
||||
$this->free($handle);
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function signal(ProcessHandle $handle, int $signo)
|
||||
{
|
||||
$handle->pidDeferred->promise()->onResolve(function ($error, $pid) use($signo) {
|
||||
if ($error) {
|
||||
return;
|
||||
}
|
||||
@\posix_kill($pid, $signo);
|
||||
});
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function destroy(ProcessHandle $handle)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
if ($handle->status < ProcessStatus::ENDED && \getmypid() === $handle->originalParentPid) {
|
||||
try {
|
||||
$this->kill($handle);
|
||||
return;
|
||||
} catch (ProcessException $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
$this->free($handle);
|
||||
}
|
||||
private function free(Handle $handle)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
if ($handle->extraDataPipeWatcher !== null) {
|
||||
Loop::cancel($handle->extraDataPipeWatcher);
|
||||
$handle->extraDataPipeWatcher = null;
|
||||
}
|
||||
/** @var Handle $handle */
|
||||
if ($handle->extraDataPipeStartWatcher !== null) {
|
||||
Loop::cancel($handle->extraDataPipeStartWatcher);
|
||||
$handle->extraDataPipeStartWatcher = null;
|
||||
}
|
||||
if (\is_resource($handle->extraDataPipe)) {
|
||||
\fclose($handle->extraDataPipe);
|
||||
}
|
||||
$handle->stdin->close();
|
||||
$handle->stdout->close();
|
||||
$handle->stderr->close();
|
||||
if (\is_resource($handle->proc)) {
|
||||
\proc_close($handle->proc);
|
||||
}
|
||||
}
|
||||
}
|
22
dependencies/amphp/process/lib/Internal/ProcessHandle.php
vendored
Normal file
22
dependencies/amphp/process/lib/Internal/ProcessHandle.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Struct;
|
||||
abstract class ProcessHandle
|
||||
{
|
||||
use Struct;
|
||||
/** @var ProcessOutputStream */
|
||||
public $stdin;
|
||||
/** @var ProcessInputStream */
|
||||
public $stdout;
|
||||
/** @var ProcessInputStream */
|
||||
public $stderr;
|
||||
/** @var Deferred */
|
||||
public $pidDeferred;
|
||||
/** @var int */
|
||||
public $status = ProcessStatus::STARTING;
|
||||
}
|
53
dependencies/amphp/process/lib/Internal/ProcessRunner.php
vendored
Normal file
53
dependencies/amphp/process/lib/Internal/ProcessRunner.php
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessException;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
interface ProcessRunner
|
||||
{
|
||||
/**
|
||||
* Start a process using the supplied parameters.
|
||||
*
|
||||
* @param string $command The command to execute.
|
||||
* @param string|null $cwd The working directory for the child process.
|
||||
* @param array $env Environment variables to pass to the child process.
|
||||
* @param array $options `proc_open()` options.
|
||||
*
|
||||
* @return ProcessHandle
|
||||
*
|
||||
* @throws ProcessException If starting the process fails.
|
||||
*/
|
||||
public function start(string $command, string $cwd = null, array $env = [], array $options = []) : ProcessHandle;
|
||||
/**
|
||||
* Wait for the child process to end.
|
||||
*
|
||||
* @param ProcessHandle $handle The process descriptor.
|
||||
*
|
||||
* @return Promise <int> Succeeds with exit code of the process or fails if the process is killed.
|
||||
*/
|
||||
public function join(ProcessHandle $handle) : Promise;
|
||||
/**
|
||||
* Forcibly end the child process.
|
||||
*
|
||||
* @param ProcessHandle $handle The process descriptor.
|
||||
*
|
||||
* @throws ProcessException If terminating the process fails.
|
||||
*/
|
||||
public function kill(ProcessHandle $handle);
|
||||
/**
|
||||
* Send a signal signal to the child process.
|
||||
*
|
||||
* @param ProcessHandle $handle The process descriptor.
|
||||
* @param int $signo Signal number to send to process.
|
||||
*
|
||||
* @throws ProcessException If sending the signal fails.
|
||||
*/
|
||||
public function signal(ProcessHandle $handle, int $signo);
|
||||
/**
|
||||
* Release all resources held by the process handle.
|
||||
*
|
||||
* @param ProcessHandle $handle The process descriptor.
|
||||
*/
|
||||
public function destroy(ProcessHandle $handle);
|
||||
}
|
14
dependencies/amphp/process/lib/Internal/ProcessStatus.php
vendored
Normal file
14
dependencies/amphp/process/lib/Internal/ProcessStatus.php
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal;
|
||||
|
||||
final class ProcessStatus
|
||||
{
|
||||
const STARTING = 0;
|
||||
const RUNNING = 1;
|
||||
const ENDED = 2;
|
||||
private function __construct()
|
||||
{
|
||||
// empty to prevent instances of this class
|
||||
}
|
||||
}
|
40
dependencies/amphp/process/lib/Internal/Windows/Handle.php
vendored
Normal file
40
dependencies/amphp/process/lib/Internal/Windows/Handle.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessHandle;
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class Handle extends ProcessHandle
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->joinDeferred = new Deferred();
|
||||
$this->pidDeferred = new Deferred();
|
||||
}
|
||||
/** @var Deferred */
|
||||
public $joinDeferred;
|
||||
/** @var string */
|
||||
public $exitCodeWatcher;
|
||||
/** @var bool */
|
||||
public $exitCodeRequested = \false;
|
||||
/** @var resource */
|
||||
public $proc;
|
||||
/** @var int */
|
||||
public $wrapperPid;
|
||||
/** @var resource */
|
||||
public $wrapperStderrPipe;
|
||||
/** @var resource[] */
|
||||
public $sockets = [];
|
||||
/** @var Deferred[] */
|
||||
public $stdioDeferreds;
|
||||
/** @var string */
|
||||
public $childPidWatcher;
|
||||
/** @var string */
|
||||
public $connectTimeoutWatcher;
|
||||
/** @var string[] */
|
||||
public $securityTokens;
|
||||
}
|
21
dependencies/amphp/process/lib/Internal/Windows/HandshakeStatus.php
vendored
Normal file
21
dependencies/amphp/process/lib/Internal/Windows/HandshakeStatus.php
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class HandshakeStatus
|
||||
{
|
||||
const SUCCESS = 0;
|
||||
const SIGNAL_UNEXPECTED = 0x1;
|
||||
const INVALID_STREAM_ID = 0x2;
|
||||
const INVALID_PROCESS_ID = 0x3;
|
||||
const DUPLICATE_STREAM_ID = 0x4;
|
||||
const INVALID_CLIENT_TOKEN = 0x5;
|
||||
private function __construct()
|
||||
{
|
||||
// empty to prevent instances of this class
|
||||
}
|
||||
}
|
18
dependencies/amphp/process/lib/Internal/Windows/PendingSocketClient.php
vendored
Normal file
18
dependencies/amphp/process/lib/Internal/Windows/PendingSocketClient.php
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Struct;
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class PendingSocketClient
|
||||
{
|
||||
use Struct;
|
||||
public $readWatcher;
|
||||
public $timeoutWatcher;
|
||||
public $receivedDataBuffer = '';
|
||||
public $pid;
|
||||
public $streamId;
|
||||
}
|
179
dependencies/amphp/process/lib/Internal/Windows/Runner.php
vendored
Normal file
179
dependencies/amphp/process/lib/Internal/Windows/Runner.php
vendored
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessHandle;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessRunner;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessStatus;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessException;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use const WP_Ultimo\Dependencies\Amp\Process\BIN_DIR;
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class Runner implements ProcessRunner
|
||||
{
|
||||
const FD_SPEC = [
|
||||
["pipe", "r"],
|
||||
// stdin
|
||||
["pipe", "w"],
|
||||
// stdout
|
||||
["pipe", "w"],
|
||||
// stderr
|
||||
["pipe", "w"],
|
||||
];
|
||||
const WRAPPER_EXE_PATH = \PHP_INT_SIZE === 8 ? BIN_DIR . '\\windows\\ProcessWrapper64.exe' : BIN_DIR . '\\windows\\ProcessWrapper.exe';
|
||||
private static $pharWrapperPath;
|
||||
private $socketConnector;
|
||||
private function makeCommand(string $workingDirectory) : string
|
||||
{
|
||||
$wrapperPath = self::WRAPPER_EXE_PATH;
|
||||
// We can't execute the exe from within the PHAR, so copy it out...
|
||||
if (\strncmp($wrapperPath, "phar://", 7) === 0) {
|
||||
if (self::$pharWrapperPath === null) {
|
||||
$fileHash = \hash_file('sha1', self::WRAPPER_EXE_PATH);
|
||||
self::$pharWrapperPath = \sys_get_temp_dir() . "/amphp-process-wrapper-" . $fileHash;
|
||||
if (!\file_exists(self::$pharWrapperPath) || \hash_file('sha1', self::$pharWrapperPath) !== $fileHash) {
|
||||
\copy(self::WRAPPER_EXE_PATH, self::$pharWrapperPath);
|
||||
}
|
||||
}
|
||||
$wrapperPath = self::$pharWrapperPath;
|
||||
}
|
||||
$result = \sprintf('%s --address=%s --port=%d --token-size=%d', \escapeshellarg($wrapperPath), $this->socketConnector->address, $this->socketConnector->port, SocketConnector::SECURITY_TOKEN_SIZE);
|
||||
if ($workingDirectory !== '') {
|
||||
$result .= ' ' . \escapeshellarg('--cwd=' . \rtrim($workingDirectory, '\\'));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
$this->socketConnector = new SocketConnector();
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function start(string $command, string $cwd = null, array $env = [], array $options = []) : ProcessHandle
|
||||
{
|
||||
if (\strpos($command, "\x00") !== \false) {
|
||||
throw new ProcessException("Can't execute commands that contain null bytes.");
|
||||
}
|
||||
$options['bypass_shell'] = \true;
|
||||
$handle = new Handle();
|
||||
$handle->proc = @\proc_open($this->makeCommand($cwd ?? ''), self::FD_SPEC, $pipes, $cwd ?: null, $env ?: null, $options);
|
||||
if (!\is_resource($handle->proc)) {
|
||||
$message = "Could not start process";
|
||||
if ($error = \error_get_last()) {
|
||||
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||
}
|
||||
throw new ProcessException($message);
|
||||
}
|
||||
$status = \proc_get_status($handle->proc);
|
||||
if (!$status) {
|
||||
\proc_close($handle->proc);
|
||||
throw new ProcessException("Could not get process status");
|
||||
}
|
||||
$securityTokens = \random_bytes(SocketConnector::SECURITY_TOKEN_SIZE * 6);
|
||||
$written = \fwrite($pipes[0], $securityTokens . "\x00" . $command . "\x00");
|
||||
\fclose($pipes[0]);
|
||||
\fclose($pipes[1]);
|
||||
if ($written !== SocketConnector::SECURITY_TOKEN_SIZE * 6 + \strlen($command) + 2) {
|
||||
\fclose($pipes[2]);
|
||||
\proc_close($handle->proc);
|
||||
throw new ProcessException("Could not send security tokens / command to process wrapper");
|
||||
}
|
||||
$handle->securityTokens = \str_split($securityTokens, SocketConnector::SECURITY_TOKEN_SIZE);
|
||||
$handle->wrapperPid = $status['pid'];
|
||||
$handle->wrapperStderrPipe = $pipes[2];
|
||||
$stdinDeferred = new Deferred();
|
||||
$handle->stdioDeferreds[] = $stdinDeferred;
|
||||
$handle->stdin = new ProcessOutputStream($stdinDeferred->promise());
|
||||
$stdoutDeferred = new Deferred();
|
||||
$handle->stdioDeferreds[] = $stdoutDeferred;
|
||||
$handle->stdout = new ProcessInputStream($stdoutDeferred->promise());
|
||||
$stderrDeferred = new Deferred();
|
||||
$handle->stdioDeferreds[] = $stderrDeferred;
|
||||
$handle->stderr = new ProcessInputStream($stderrDeferred->promise());
|
||||
$this->socketConnector->registerPendingProcess($handle);
|
||||
return $handle;
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function join(ProcessHandle $handle) : Promise
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
$handle->exitCodeRequested = \true;
|
||||
if ($handle->exitCodeWatcher !== null) {
|
||||
Loop::reference($handle->exitCodeWatcher);
|
||||
}
|
||||
return $handle->joinDeferred->promise();
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function kill(ProcessHandle $handle)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
\exec('taskkill /F /T /PID ' . $handle->wrapperPid . ' 2>&1', $output, $exitCode);
|
||||
$failStart = \false;
|
||||
if ($handle->childPidWatcher !== null) {
|
||||
Loop::cancel($handle->childPidWatcher);
|
||||
$handle->childPidWatcher = null;
|
||||
$handle->pidDeferred->fail(new ProcessException("The process was killed"));
|
||||
$failStart = \true;
|
||||
}
|
||||
if ($handle->exitCodeWatcher !== null) {
|
||||
Loop::cancel($handle->exitCodeWatcher);
|
||||
$handle->exitCodeWatcher = null;
|
||||
$handle->joinDeferred->fail(new ProcessException("The process was killed"));
|
||||
}
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
if ($failStart || $handle->stdioDeferreds) {
|
||||
$this->socketConnector->failHandleStart($handle, "The process was killed");
|
||||
}
|
||||
$this->free($handle);
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function signal(ProcessHandle $handle, int $signo)
|
||||
{
|
||||
throw new ProcessException('Signals are not supported on Windows');
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function destroy(ProcessHandle $handle)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
if ($handle->status < ProcessStatus::ENDED && \is_resource($handle->proc)) {
|
||||
try {
|
||||
$this->kill($handle);
|
||||
return;
|
||||
} catch (ProcessException $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
$this->free($handle);
|
||||
}
|
||||
private function free(Handle $handle)
|
||||
{
|
||||
if ($handle->childPidWatcher !== null) {
|
||||
Loop::cancel($handle->childPidWatcher);
|
||||
$handle->childPidWatcher = null;
|
||||
}
|
||||
if ($handle->exitCodeWatcher !== null) {
|
||||
Loop::cancel($handle->exitCodeWatcher);
|
||||
$handle->exitCodeWatcher = null;
|
||||
}
|
||||
$handle->stdin->close();
|
||||
$handle->stdout->close();
|
||||
$handle->stderr->close();
|
||||
foreach ($handle->sockets as $socket) {
|
||||
if (\is_resource($socket)) {
|
||||
@\fclose($socket);
|
||||
}
|
||||
}
|
||||
if (\is_resource($handle->wrapperStderrPipe)) {
|
||||
@\fclose($handle->wrapperStderrPipe);
|
||||
}
|
||||
if (\is_resource($handle->proc)) {
|
||||
\proc_close($handle->proc);
|
||||
}
|
||||
}
|
||||
}
|
19
dependencies/amphp/process/lib/Internal/Windows/SignalCode.php
vendored
Normal file
19
dependencies/amphp/process/lib/Internal/Windows/SignalCode.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class SignalCode
|
||||
{
|
||||
const HANDSHAKE = 0x1;
|
||||
const HANDSHAKE_ACK = 0x2;
|
||||
const CHILD_PID = 0x3;
|
||||
const EXIT_CODE = 0x4;
|
||||
private function __construct()
|
||||
{
|
||||
// empty to prevent instances of this class
|
||||
}
|
||||
}
|
273
dependencies/amphp/process/lib/Internal/Windows/SocketConnector.php
vendored
Normal file
273
dependencies/amphp/process/lib/Internal/Windows/SocketConnector.php
vendored
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process\Internal\Windows;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessStatus;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\ProcessException;
|
||||
/**
|
||||
* @internal
|
||||
* @codeCoverageIgnore Windows only.
|
||||
*/
|
||||
final class SocketConnector
|
||||
{
|
||||
const SERVER_SOCKET_URI = 'tcp://127.0.0.1:0';
|
||||
const SECURITY_TOKEN_SIZE = 16;
|
||||
const CONNECT_TIMEOUT = 1000;
|
||||
/** @var resource */
|
||||
private $server;
|
||||
/** @var PendingSocketClient[] */
|
||||
private $pendingClients = [];
|
||||
/** @var Handle[] */
|
||||
private $pendingProcesses = [];
|
||||
/** @var string */
|
||||
public $address;
|
||||
/** @var int */
|
||||
public $port;
|
||||
public function __construct()
|
||||
{
|
||||
$flags = \STREAM_SERVER_LISTEN | \STREAM_SERVER_BIND;
|
||||
$this->server = \stream_socket_server(self::SERVER_SOCKET_URI, $errNo, $errStr, $flags);
|
||||
if (!$this->server) {
|
||||
throw new \Error("Failed to create TCP server socket for process wrapper: {$errNo}: {$errStr}");
|
||||
}
|
||||
if (!\stream_set_blocking($this->server, \false)) {
|
||||
throw new \Error("Failed to set server socket to non-blocking mode");
|
||||
}
|
||||
list($this->address, $this->port) = \explode(':', \stream_socket_get_name($this->server, \false));
|
||||
$this->port = (int) $this->port;
|
||||
Loop::unreference(Loop::onReadable($this->server, [$this, 'onServerSocketReadable']));
|
||||
}
|
||||
private function failClientHandshake($socket, int $code)
|
||||
{
|
||||
\fwrite($socket, \chr(SignalCode::HANDSHAKE_ACK) . \chr($code));
|
||||
\fclose($socket);
|
||||
unset($this->pendingClients[(int) $socket]);
|
||||
}
|
||||
public function failHandleStart(Handle $handle, string $message, ...$args)
|
||||
{
|
||||
Loop::cancel($handle->connectTimeoutWatcher);
|
||||
unset($this->pendingProcesses[$handle->wrapperPid]);
|
||||
foreach ($handle->sockets as $socket) {
|
||||
\fclose($socket);
|
||||
}
|
||||
$error = new ProcessException(\vsprintf($message, $args));
|
||||
$deferreds = $handle->stdioDeferreds;
|
||||
$deferreds[] = $handle->joinDeferred;
|
||||
$handle->stdioDeferreds = [];
|
||||
foreach ($deferreds as $deferred) {
|
||||
$deferred->fail($error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read data from a client socket.
|
||||
*
|
||||
* This method cleans up internal state as appropriate. Returns null if the read fails or needs to be repeated.
|
||||
*
|
||||
* @param resource $socket
|
||||
* @param int $length
|
||||
* @param PendingSocketClient $state
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function readDataFromPendingClient($socket, int $length, PendingSocketClient $state)
|
||||
{
|
||||
$data = \fread($socket, $length);
|
||||
if ($data === \false || $data === '') {
|
||||
return null;
|
||||
}
|
||||
$data = $state->receivedDataBuffer . $data;
|
||||
if (\strlen($data) < $length) {
|
||||
$state->receivedDataBuffer = $data;
|
||||
return null;
|
||||
}
|
||||
$state->receivedDataBuffer = '';
|
||||
Loop::cancel($state->readWatcher);
|
||||
return $data;
|
||||
}
|
||||
public function onReadableHandshake($watcher, $socket)
|
||||
{
|
||||
$socketId = (int) $socket;
|
||||
$pendingClient = $this->pendingClients[$socketId];
|
||||
if (null === ($data = $this->readDataFromPendingClient($socket, self::SECURITY_TOKEN_SIZE + 6, $pendingClient))) {
|
||||
return;
|
||||
}
|
||||
$packet = \unpack('Csignal/Npid/Cstream_id/a*client_token', $data);
|
||||
// validate the client's handshake
|
||||
if ($packet['signal'] !== SignalCode::HANDSHAKE) {
|
||||
$this->failClientHandshake($socket, HandshakeStatus::SIGNAL_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
if ($packet['stream_id'] > 2) {
|
||||
$this->failClientHandshake($socket, HandshakeStatus::INVALID_STREAM_ID);
|
||||
return;
|
||||
}
|
||||
if (!isset($this->pendingProcesses[$packet['pid']])) {
|
||||
$this->failClientHandshake($socket, HandshakeStatus::INVALID_PROCESS_ID);
|
||||
return;
|
||||
}
|
||||
$handle = $this->pendingProcesses[$packet['pid']];
|
||||
if (isset($handle->sockets[$packet['stream_id']])) {
|
||||
$this->failClientHandshake($socket, HandshakeStatus::DUPLICATE_STREAM_ID);
|
||||
\trigger_error(\sprintf("%s: Received duplicate socket for process #%s stream #%d", self::class, $handle->pid, $packet['stream_id']), \E_USER_WARNING);
|
||||
return;
|
||||
}
|
||||
if (!\hash_equals($packet['client_token'], $handle->securityTokens[$packet['stream_id']])) {
|
||||
$this->failClientHandshake($socket, HandshakeStatus::INVALID_CLIENT_TOKEN);
|
||||
$this->failHandleStart($handle, "Invalid client security token for stream #%d", $packet['stream_id']);
|
||||
return;
|
||||
}
|
||||
$ackData = \chr(SignalCode::HANDSHAKE_ACK) . \chr(HandshakeStatus::SUCCESS) . $handle->securityTokens[$packet['stream_id'] + 3];
|
||||
// Unless we set the security token size so high that it won't fit in the
|
||||
// buffer, this probably shouldn't ever happen unless something has gone wrong
|
||||
if (\fwrite($socket, $ackData) !== self::SECURITY_TOKEN_SIZE + 2) {
|
||||
unset($this->pendingClients[$socketId]);
|
||||
return;
|
||||
}
|
||||
$pendingClient->pid = $packet['pid'];
|
||||
$pendingClient->streamId = $packet['stream_id'];
|
||||
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadableHandshakeAck']);
|
||||
}
|
||||
public function onReadableHandshakeAck($watcher, $socket)
|
||||
{
|
||||
$socketId = (int) $socket;
|
||||
$pendingClient = $this->pendingClients[$socketId];
|
||||
// can happen if the start promise was failed
|
||||
if (!isset($this->pendingProcesses[$pendingClient->pid]) || $this->pendingProcesses[$pendingClient->pid]->status === ProcessStatus::ENDED) {
|
||||
\fclose($socket);
|
||||
Loop::cancel($watcher);
|
||||
Loop::cancel($pendingClient->timeoutWatcher);
|
||||
unset($this->pendingClients[$socketId]);
|
||||
return;
|
||||
}
|
||||
if (null === ($data = $this->readDataFromPendingClient($socket, 2, $pendingClient))) {
|
||||
return;
|
||||
}
|
||||
Loop::cancel($pendingClient->timeoutWatcher);
|
||||
unset($this->pendingClients[$socketId]);
|
||||
$handle = $this->pendingProcesses[$pendingClient->pid];
|
||||
$packet = \unpack('Csignal/Cstatus', $data);
|
||||
if ($packet['signal'] !== SignalCode::HANDSHAKE_ACK || $packet['status'] !== HandshakeStatus::SUCCESS) {
|
||||
$this->failHandleStart($handle, "Client rejected handshake with code %d for stream #%d", $packet['status'], $pendingClient->streamId);
|
||||
return;
|
||||
}
|
||||
$handle->sockets[$pendingClient->streamId] = $socket;
|
||||
if (\count($handle->sockets) === 3) {
|
||||
$handle->childPidWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadableChildPid'], $handle);
|
||||
$deferreds = $handle->stdioDeferreds;
|
||||
$handle->stdioDeferreds = [];
|
||||
// clear, so there's no double resolution if process spawn fails
|
||||
$deferreds[0]->resolve(new ResourceOutputStream($handle->sockets[0]));
|
||||
$deferreds[1]->resolve(new ResourceInputStream($handle->sockets[1]));
|
||||
$deferreds[2]->resolve(new ResourceInputStream($handle->sockets[2]));
|
||||
}
|
||||
}
|
||||
public function onReadableChildPid($watcher, $socket, Handle $handle)
|
||||
{
|
||||
$data = \fread($socket, 5);
|
||||
if ($data === \false || $data === '') {
|
||||
return;
|
||||
}
|
||||
Loop::cancel($handle->childPidWatcher);
|
||||
Loop::cancel($handle->connectTimeoutWatcher);
|
||||
$handle->childPidWatcher = null;
|
||||
if (\strlen($data) !== 5) {
|
||||
$this->failHandleStart($handle, 'Failed to read PID from wrapper: Received %d of 5 expected bytes', \strlen($data));
|
||||
return;
|
||||
}
|
||||
$packet = \unpack('Csignal/Npid', $data);
|
||||
if ($packet['signal'] !== SignalCode::CHILD_PID) {
|
||||
$this->failHandleStart($handle, "Failed to read PID from wrapper: Unexpected signal code %d", $packet['signal']);
|
||||
return;
|
||||
}
|
||||
// Required, because a process might be destroyed while starting
|
||||
if ($handle->status === ProcessStatus::STARTING) {
|
||||
$handle->status = ProcessStatus::RUNNING;
|
||||
$handle->exitCodeWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadableExitCode'], $handle);
|
||||
if (!$handle->exitCodeRequested) {
|
||||
Loop::unreference($handle->exitCodeWatcher);
|
||||
}
|
||||
}
|
||||
$handle->pidDeferred->resolve($packet['pid']);
|
||||
unset($this->pendingProcesses[$handle->wrapperPid]);
|
||||
}
|
||||
public function onReadableExitCode($watcher, $socket, Handle $handle)
|
||||
{
|
||||
$data = \fread($socket, 5);
|
||||
if ($data === \false || $data === '') {
|
||||
return;
|
||||
}
|
||||
Loop::cancel($handle->exitCodeWatcher);
|
||||
$handle->exitCodeWatcher = null;
|
||||
if (\strlen($data) !== 5) {
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
$handle->joinDeferred->fail(new ProcessException(\sprintf('Failed to read exit code from wrapper: Received %d of 5 expected bytes', \strlen($data))));
|
||||
return;
|
||||
}
|
||||
$packet = \unpack('Csignal/Ncode', $data);
|
||||
if ($packet['signal'] !== SignalCode::EXIT_CODE) {
|
||||
$this->failHandleStart($handle, "Failed to read exit code from wrapper: Unexpected signal code %d", $packet['signal']);
|
||||
return;
|
||||
}
|
||||
$handle->status = ProcessStatus::ENDED;
|
||||
$handle->joinDeferred->resolve($packet['code']);
|
||||
$handle->stdin->close();
|
||||
$handle->stdout->close();
|
||||
$handle->stderr->close();
|
||||
// Explicitly \fclose() sockets, as resource streams shut only one side down.
|
||||
foreach ($handle->sockets as $sock) {
|
||||
// Ensure socket is still open before attempting to close.
|
||||
if (\is_resource($sock)) {
|
||||
@\fclose($sock);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function onClientSocketConnectTimeout($watcher, $socket)
|
||||
{
|
||||
$id = (int) $socket;
|
||||
Loop::cancel($this->pendingClients[$id]->readWatcher);
|
||||
unset($this->pendingClients[$id]);
|
||||
\fclose($socket);
|
||||
}
|
||||
public function onServerSocketReadable()
|
||||
{
|
||||
$socket = \stream_socket_accept($this->server);
|
||||
if (!\stream_set_blocking($socket, \false)) {
|
||||
throw new \Error("Failed to set client socket to non-blocking mode");
|
||||
}
|
||||
$pendingClient = new PendingSocketClient();
|
||||
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadableHandshake']);
|
||||
$pendingClient->timeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onClientSocketConnectTimeout'], $socket);
|
||||
$this->pendingClients[(int) $socket] = $pendingClient;
|
||||
}
|
||||
public function onProcessConnectTimeout($watcher, Handle $handle)
|
||||
{
|
||||
$running = \is_resource($handle->proc) && \proc_get_status($handle->proc)['running'];
|
||||
$error = null;
|
||||
if (!$running) {
|
||||
$error = \stream_get_contents($handle->wrapperStderrPipe);
|
||||
}
|
||||
$error = $error ?: 'Process did not connect to server before timeout elapsed';
|
||||
foreach ($handle->sockets as $socket) {
|
||||
\fclose($socket);
|
||||
}
|
||||
$error = new ProcessException(\trim($error));
|
||||
foreach ($handle->stdioDeferreds as $deferred) {
|
||||
$deferred->fail($error);
|
||||
}
|
||||
\fclose($handle->wrapperStderrPipe);
|
||||
\proc_close($handle->proc);
|
||||
$handle->joinDeferred->fail($error);
|
||||
}
|
||||
public function registerPendingProcess(Handle $handle)
|
||||
{
|
||||
// Use Loop::defer() to start the timeout only after the loop has ticked once. This prevents issues with many
|
||||
// things started at once, see https://github.com/amphp/process/issues/21.
|
||||
$handle->connectTimeoutWatcher = Loop::defer(function () use($handle) {
|
||||
$handle->connectTimeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onProcessConnectTimeout'], $handle);
|
||||
});
|
||||
$this->pendingProcesses[$handle->wrapperPid] = $handle;
|
||||
}
|
||||
}
|
233
dependencies/amphp/process/lib/Process.php
vendored
Normal file
233
dependencies/amphp/process/lib/Process.php
vendored
Normal file
@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\Posix\Runner as PosixProcessRunner;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessHandle;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessRunner;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\ProcessStatus;
|
||||
use WP_Ultimo\Dependencies\Amp\Process\Internal\Windows\Runner as WindowsProcessRunner;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class Process
|
||||
{
|
||||
/** @var ProcessRunner */
|
||||
private $processRunner;
|
||||
/** @var string */
|
||||
private $command;
|
||||
/** @var string */
|
||||
private $cwd = "";
|
||||
/** @var array */
|
||||
private $env = [];
|
||||
/** @var array */
|
||||
private $options;
|
||||
/** @var ProcessHandle */
|
||||
private $handle;
|
||||
/** @var int|null */
|
||||
private $pid;
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public function __construct($command, string $cwd = null, array $env = [], array $options = [])
|
||||
{
|
||||
$command = \is_array($command) ? \implode(" ", \array_map(__NAMESPACE__ . "\\escapeArguments", $command)) : (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;
|
||||
}
|
||||
$this->command = $command;
|
||||
$this->cwd = $cwd;
|
||||
$this->env = $envVars;
|
||||
$this->options = $options;
|
||||
$this->processRunner = Loop::getState(self::class);
|
||||
if ($this->processRunner === null) {
|
||||
$this->processRunner = IS_WINDOWS ? new WindowsProcessRunner() : new PosixProcessRunner();
|
||||
Loop::setState(self::class, $this->processRunner);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stops the process if it is still running.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->handle !== null) {
|
||||
$this->processRunner->destroy($this->handle);
|
||||
}
|
||||
}
|
||||
public function __clone()
|
||||
{
|
||||
throw new \Error("Cloning is not allowed!");
|
||||
}
|
||||
/**
|
||||
* Start the process.
|
||||
*
|
||||
* @return Promise<int> Resolves with the PID.
|
||||
*
|
||||
* @throws StatusError If the process has already been started.
|
||||
*/
|
||||
public function start() : Promise
|
||||
{
|
||||
if ($this->handle) {
|
||||
throw new StatusError("Process has already been started.");
|
||||
}
|
||||
return call(function () {
|
||||
$this->handle = $this->processRunner->start($this->command, $this->cwd, $this->env, $this->options);
|
||||
return $this->pid = (yield $this->handle->pidDeferred->promise());
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Wait for the process to end.
|
||||
*
|
||||
* @return Promise <int> Succeeds with process exit code or fails with a ProcessException if the process is killed.
|
||||
*
|
||||
* @throws StatusError If the process has already been started.
|
||||
*/
|
||||
public function join() : Promise
|
||||
{
|
||||
if (!$this->handle) {
|
||||
throw new StatusError("Process has not been started.");
|
||||
}
|
||||
return $this->processRunner->join($this->handle);
|
||||
}
|
||||
/**
|
||||
* Forcibly end the process.
|
||||
*
|
||||
* @throws StatusError If the process is not running.
|
||||
* @throws ProcessException If terminating the process fails.
|
||||
*/
|
||||
public function kill()
|
||||
{
|
||||
if (!$this->isRunning()) {
|
||||
throw new StatusError("Process is not running.");
|
||||
}
|
||||
$this->processRunner->kill($this->handle);
|
||||
}
|
||||
/**
|
||||
* Send a signal signal to the process.
|
||||
*
|
||||
* @param int $signo Signal number to send to process.
|
||||
*
|
||||
* @throws StatusError If the process is not running.
|
||||
* @throws ProcessException If sending the signal fails.
|
||||
*/
|
||||
public function signal(int $signo)
|
||||
{
|
||||
if (!$this->isRunning()) {
|
||||
throw new StatusError("Process is not running.");
|
||||
}
|
||||
$this->processRunner->signal($this->handle, $signo);
|
||||
}
|
||||
/**
|
||||
* Returns the PID of the child process.
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @throws StatusError If the process has not started or has not completed starting.
|
||||
*/
|
||||
public function getPid() : int
|
||||
{
|
||||
if (!$this->pid) {
|
||||
throw new StatusError("Process has not been started or has not completed starting.");
|
||||
}
|
||||
return $this->pid;
|
||||
}
|
||||
/**
|
||||
* Returns the command to execute.
|
||||
*
|
||||
* @return string The command to execute.
|
||||
*/
|
||||
public function getCommand() : string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
/**
|
||||
* Gets the current working directory.
|
||||
*
|
||||
* @return string The current working directory an empty string if inherited from the current PHP process.
|
||||
*/
|
||||
public function getWorkingDirectory() : string
|
||||
{
|
||||
if ($this->cwd === "") {
|
||||
return \getcwd() ?: "";
|
||||
}
|
||||
return $this->cwd;
|
||||
}
|
||||
/**
|
||||
* Gets the environment variables array.
|
||||
*
|
||||
* @return string[] Array of environment variables.
|
||||
*/
|
||||
public function getEnv() : array
|
||||
{
|
||||
return $this->env;
|
||||
}
|
||||
/**
|
||||
* Gets the options to pass to proc_open().
|
||||
*
|
||||
* @return mixed[] Array of options.
|
||||
*/
|
||||
public function getOptions() : array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
/**
|
||||
* Determines if the process is still running.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isRunning() : bool
|
||||
{
|
||||
return $this->handle && $this->handle->status !== ProcessStatus::ENDED;
|
||||
}
|
||||
/**
|
||||
* Gets the process input stream (STDIN).
|
||||
*
|
||||
* @return ProcessOutputStream
|
||||
*/
|
||||
public function getStdin() : ProcessOutputStream
|
||||
{
|
||||
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
||||
throw new StatusError("Process has not been started or has not completed starting.");
|
||||
}
|
||||
return $this->handle->stdin;
|
||||
}
|
||||
/**
|
||||
* Gets the process output stream (STDOUT).
|
||||
*
|
||||
* @return ProcessInputStream
|
||||
*/
|
||||
public function getStdout() : ProcessInputStream
|
||||
{
|
||||
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
||||
throw new StatusError("Process has not been started or has not completed starting.");
|
||||
}
|
||||
return $this->handle->stdout;
|
||||
}
|
||||
/**
|
||||
* Gets the process error stream (STDERR).
|
||||
*
|
||||
* @return ProcessInputStream
|
||||
*/
|
||||
public function getStderr() : ProcessInputStream
|
||||
{
|
||||
if (!$this->handle || $this->handle->status === ProcessStatus::STARTING) {
|
||||
throw new StatusError("Process has not been started or has not completed starting.");
|
||||
}
|
||||
return $this->handle->stderr;
|
||||
}
|
||||
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];
|
||||
}
|
||||
}
|
7
dependencies/amphp/process/lib/ProcessException.php
vendored
Normal file
7
dependencies/amphp/process/lib/ProcessException.php
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
class ProcessException extends \Exception
|
||||
{
|
||||
}
|
102
dependencies/amphp/process/lib/ProcessInputStream.php
vendored
Normal file
102
dependencies/amphp/process/lib/ProcessInputStream.php
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\PendingReadError;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Failure;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
final class ProcessInputStream implements InputStream
|
||||
{
|
||||
/** @var Deferred */
|
||||
private $initialRead;
|
||||
/** @var bool */
|
||||
private $shouldClose = \false;
|
||||
/** @var bool */
|
||||
private $referenced = \true;
|
||||
/** @var ResourceInputStream */
|
||||
private $resourceStream;
|
||||
/** @var StreamException|null */
|
||||
private $error;
|
||||
public function __construct(Promise $resourceStreamPromise)
|
||||
{
|
||||
$resourceStreamPromise->onResolve(function ($error, $resourceStream) {
|
||||
if ($error) {
|
||||
$this->error = new StreamException("Failed to launch process", 0, $error);
|
||||
if ($this->initialRead) {
|
||||
$initialRead = $this->initialRead;
|
||||
$this->initialRead = null;
|
||||
$initialRead->fail($this->error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$this->resourceStream = $resourceStream;
|
||||
if (!$this->referenced) {
|
||||
$this->resourceStream->unreference();
|
||||
}
|
||||
if ($this->shouldClose) {
|
||||
$this->resourceStream->close();
|
||||
}
|
||||
if ($this->initialRead) {
|
||||
$initialRead = $this->initialRead;
|
||||
$this->initialRead = null;
|
||||
$initialRead->resolve($this->shouldClose ? null : $this->resourceStream->read());
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Reads data from the stream.
|
||||
*
|
||||
* @return Promise Resolves with a string when new data is available or `null` if the stream has closed.
|
||||
*
|
||||
* @throws PendingReadError Thrown if another read operation is still pending.
|
||||
*/
|
||||
public function read() : Promise
|
||||
{
|
||||
if ($this->initialRead) {
|
||||
throw new PendingReadError();
|
||||
}
|
||||
if ($this->error) {
|
||||
return new Failure($this->error);
|
||||
}
|
||||
if ($this->resourceStream) {
|
||||
return $this->resourceStream->read();
|
||||
}
|
||||
if ($this->shouldClose) {
|
||||
return new Success();
|
||||
// Resolve reads on closed streams with null.
|
||||
}
|
||||
$this->initialRead = new Deferred();
|
||||
return $this->initialRead->promise();
|
||||
}
|
||||
public function reference()
|
||||
{
|
||||
$this->referenced = \true;
|
||||
if ($this->resourceStream) {
|
||||
$this->resourceStream->reference();
|
||||
}
|
||||
}
|
||||
public function unreference()
|
||||
{
|
||||
$this->referenced = \false;
|
||||
if ($this->resourceStream) {
|
||||
$this->resourceStream->unreference();
|
||||
}
|
||||
}
|
||||
public function close()
|
||||
{
|
||||
$this->shouldClose = \true;
|
||||
if ($this->initialRead) {
|
||||
$initialRead = $this->initialRead;
|
||||
$this->initialRead = null;
|
||||
$initialRead->resolve();
|
||||
}
|
||||
if ($this->resourceStream) {
|
||||
$this->resourceStream->close();
|
||||
}
|
||||
}
|
||||
}
|
94
dependencies/amphp/process/lib/ProcessOutputStream.php
vendored
Normal file
94
dependencies/amphp/process/lib/ProcessOutputStream.php
vendored
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ClosedException;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\OutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ResourceOutputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Failure;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
final class ProcessOutputStream implements OutputStream
|
||||
{
|
||||
/** @var \SplQueue */
|
||||
private $queuedWrites;
|
||||
/** @var bool */
|
||||
private $shouldClose = \false;
|
||||
/** @var ResourceOutputStream */
|
||||
private $resourceStream;
|
||||
/** @var StreamException|null */
|
||||
private $error;
|
||||
public function __construct(Promise $resourceStreamPromise)
|
||||
{
|
||||
$this->queuedWrites = new \SplQueue();
|
||||
$resourceStreamPromise->onResolve(function ($error, $resourceStream) {
|
||||
if ($error) {
|
||||
$this->error = new StreamException("Failed to launch process", 0, $error);
|
||||
while (!$this->queuedWrites->isEmpty()) {
|
||||
list(, $deferred) = $this->queuedWrites->shift();
|
||||
$deferred->fail($this->error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (!$this->queuedWrites->isEmpty()) {
|
||||
/**
|
||||
* @var string $data
|
||||
* @var \Amp\Deferred $deferred
|
||||
*/
|
||||
list($data, $deferred) = $this->queuedWrites->shift();
|
||||
$deferred->resolve($resourceStream->write($data));
|
||||
}
|
||||
$this->resourceStream = $resourceStream;
|
||||
if ($this->shouldClose) {
|
||||
$this->resourceStream->close();
|
||||
}
|
||||
});
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function write(string $data) : Promise
|
||||
{
|
||||
if ($this->resourceStream) {
|
||||
return $this->resourceStream->write($data);
|
||||
}
|
||||
if ($this->error) {
|
||||
return new Failure($this->error);
|
||||
}
|
||||
if ($this->shouldClose) {
|
||||
throw new ClosedException("Stream has already been closed.");
|
||||
}
|
||||
$deferred = new Deferred();
|
||||
$this->queuedWrites->push([$data, $deferred]);
|
||||
return $deferred->promise();
|
||||
}
|
||||
/** @inheritdoc */
|
||||
public function end(string $finalData = "") : Promise
|
||||
{
|
||||
if ($this->resourceStream) {
|
||||
return $this->resourceStream->end($finalData);
|
||||
}
|
||||
if ($this->error) {
|
||||
return new Failure($this->error);
|
||||
}
|
||||
if ($this->shouldClose) {
|
||||
throw new ClosedException("Stream has already been closed.");
|
||||
}
|
||||
$deferred = new Deferred();
|
||||
$this->queuedWrites->push([$finalData, $deferred]);
|
||||
$this->shouldClose = \true;
|
||||
return $deferred->promise();
|
||||
}
|
||||
public function close()
|
||||
{
|
||||
$this->shouldClose = \true;
|
||||
if ($this->resourceStream) {
|
||||
$this->resourceStream->close();
|
||||
} elseif (!$this->queuedWrites->isEmpty()) {
|
||||
$error = new ClosedException("Stream closed.");
|
||||
do {
|
||||
list(, $deferred) = $this->queuedWrites->shift();
|
||||
$deferred->fail($error);
|
||||
} while (!$this->queuedWrites->isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
7
dependencies/amphp/process/lib/StatusError.php
vendored
Normal file
7
dependencies/amphp/process/lib/StatusError.php
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
class StatusError extends \Error
|
||||
{
|
||||
}
|
35
dependencies/amphp/process/lib/functions.php
vendored
Normal file
35
dependencies/amphp/process/lib/functions.php
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Process;
|
||||
|
||||
const BIN_DIR = __DIR__ . \DIRECTORY_SEPARATOR . '..' . \DIRECTORY_SEPARATOR . 'bin';
|
||||
const IS_WINDOWS = (\PHP_OS & "\xdf\xdf\xdf") === 'WIN';
|
||||
if (!\function_exists(__NAMESPACE__ . '\\escapeArguments')) {
|
||||
if (IS_WINDOWS) {
|
||||
/**
|
||||
* Escapes the command argument for safe inclusion into a Windows command string.
|
||||
*
|
||||
* @param string $arg
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function escapeArguments(string $arg) : string
|
||||
{
|
||||
return '"' . \preg_replace_callback('(\\\\*("|$))', function (array $m) : string {
|
||||
return \str_repeat('\\', \strlen($m[0])) . $m[0];
|
||||
}, $arg) . '"';
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Escapes the command argument for safe inclusion into a Posix shell command string.
|
||||
*
|
||||
* @param string $arg
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function escapeArguments(string $arg) : string
|
||||
{
|
||||
return \escapeshellarg($arg);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user