mirror of
https://github.com/danog/process.git
synced 2024-11-30 04:39:04 +01:00
More refactoring for Windows compatibility with improved BC
This commit is contained in:
parent
6d2b28f4f5
commit
88f8865a6a
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "amphp/process",
|
"name": "amphp/process",
|
||||||
"homepage": "https://github.com/amphp/process",
|
"homepage": "https://github.com/amphp/process",
|
||||||
"description": "Asynchronous process manager",
|
"description": "Asynchronous process manager.",
|
||||||
"require": {
|
"require": {
|
||||||
|
"php": ">=7",
|
||||||
"amphp/amp": "^2",
|
"amphp/amp": "^2",
|
||||||
"amphp/byte-stream": "^1"
|
"amphp/byte-stream": "^1"
|
||||||
},
|
},
|
||||||
@ -21,6 +22,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Aaron Piotrowski",
|
"name": "Aaron Piotrowski",
|
||||||
"email": "aaron@trowski.com"
|
"email": "aaron@trowski.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Niklas Keller",
|
||||||
|
"email": "me@kelunik.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -5,25 +5,21 @@ namespace Amp\Process\Internal\Posix;
|
|||||||
use Amp\Deferred;
|
use Amp\Deferred;
|
||||||
use Amp\Process\Internal\ProcessHandle;
|
use Amp\Process\Internal\ProcessHandle;
|
||||||
|
|
||||||
final class Handle extends ProcessHandle
|
final class Handle extends ProcessHandle {
|
||||||
{
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->startDeferred = new Deferred;
|
$this->pidDeferred = new Deferred;
|
||||||
$this->endDeferred = new Deferred;
|
$this->joinDeferred = new Deferred;
|
||||||
$this->originalParentPid = \getmypid();
|
$this->originalParentPid = \getmypid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Deferred */
|
/** @var Deferred */
|
||||||
public $endDeferred;
|
public $joinDeferred;
|
||||||
|
|
||||||
/** @var Deferred */
|
|
||||||
public $startDeferred;
|
|
||||||
|
|
||||||
/** @var resource */
|
/** @var resource */
|
||||||
public $proc;
|
public $proc;
|
||||||
|
|
||||||
/** @var resource[] */
|
/** @var resource */
|
||||||
public $pipes;
|
public $extraDataPipe;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $extraDataPipeWatcher;
|
public $extraDataPipeWatcher;
|
||||||
|
@ -4,15 +4,17 @@ namespace Amp\Process\Internal\Posix;
|
|||||||
|
|
||||||
use Amp\ByteStream\ResourceInputStream;
|
use Amp\ByteStream\ResourceInputStream;
|
||||||
use Amp\ByteStream\ResourceOutputStream;
|
use Amp\ByteStream\ResourceOutputStream;
|
||||||
|
use Amp\Deferred;
|
||||||
use Amp\Loop;
|
use Amp\Loop;
|
||||||
use Amp\Process\Internal\ProcessHandle;
|
use Amp\Process\Internal\ProcessHandle;
|
||||||
use Amp\Process\Internal\ProcessRunner;
|
use Amp\Process\Internal\ProcessRunner;
|
||||||
use Amp\Process\Internal\ProcessStatus;
|
use Amp\Process\Internal\ProcessStatus;
|
||||||
use Amp\Process\ProcessException;
|
use Amp\Process\ProcessException;
|
||||||
|
use Amp\Process\ProcessInputStream;
|
||||||
|
use Amp\Process\ProcessOutputStream;
|
||||||
use Amp\Promise;
|
use Amp\Promise;
|
||||||
|
|
||||||
final class Runner implements ProcessRunner
|
final class Runner implements ProcessRunner {
|
||||||
{
|
|
||||||
const FD_SPEC = [
|
const FD_SPEC = [
|
||||||
["pipe", "r"], // stdin
|
["pipe", "r"], // stdin
|
||||||
["pipe", "w"], // stdout
|
["pipe", "w"], // stdout
|
||||||
@ -20,44 +22,51 @@ final class Runner implements ProcessRunner
|
|||||||
["pipe", "w"], // exit code pipe
|
["pipe", "w"], // exit code pipe
|
||||||
];
|
];
|
||||||
|
|
||||||
public function onProcessEndExtraDataPipeReadable($watcher, $stream, Handle $handle) {
|
public static function onProcessEndExtraDataPipeReadable($watcher, $stream, Handle $handle) {
|
||||||
Loop::cancel($watcher);
|
Loop::cancel($watcher);
|
||||||
|
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
|
|
||||||
if (!\is_resource($stream) || \feof($stream)) {
|
if (!\is_resource($stream) || \feof($stream)) {
|
||||||
$handle->endDeferred->fail(new ProcessException("Process ended unexpectedly"));
|
$handle->joinDeferred->fail(new ProcessException("Process ended unexpectedly"));
|
||||||
} else {
|
} else {
|
||||||
$handle->endDeferred->resolve((int) \rtrim(@\stream_get_contents($stream)));
|
$handle->joinDeferred->resolve((int) \rtrim(@\stream_get_contents($stream)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onProcessStartExtraDataPipeReadable($watcher, $stream, Handle $handle) {
|
public static function onProcessStartExtraDataPipeReadable($watcher, $stream, $data) {
|
||||||
Loop::cancel($watcher);
|
Loop::cancel($watcher);
|
||||||
|
|
||||||
$pid = \rtrim(@\fgets($stream));
|
$pid = \rtrim(@\fgets($stream));
|
||||||
|
|
||||||
|
/** @var $deferreds Deferred[] */
|
||||||
|
list($handle, $pipes, $deferreds) = $data;
|
||||||
|
|
||||||
if (!$pid || !\is_numeric($pid)) {
|
if (!$pid || !\is_numeric($pid)) {
|
||||||
$handle->startDeferred->fail(new ProcessException("Could not determine PID"));
|
$error = new ProcessException("Could not determine PID");
|
||||||
|
$handle->pidDeferred->fail($error);
|
||||||
|
$handle->joinDeferred->fail($error);
|
||||||
|
foreach ($deferreds as $deferred) {
|
||||||
|
/** @var $deferred Deferred */
|
||||||
|
$deferred->fail($error);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle->status = ProcessStatus::RUNNING;
|
$handle->status = ProcessStatus::RUNNING;
|
||||||
$handle->pid = (int) $pid;
|
$handle->pidDeferred->resolve((int) $pid);
|
||||||
$handle->stdin = new ResourceOutputStream($handle->pipes[0]);
|
$deferreds[0]->resolve(new ResourceOutputStream($pipes[0]));
|
||||||
$handle->stdout = new ResourceInputStream($handle->pipes[1]);
|
$deferreds[1]->resolve(new ResourceInputStream($pipes[1]));
|
||||||
$handle->stderr = new ResourceInputStream($handle->pipes[2]);
|
$deferreds[2]->resolve(new ResourceInputStream($pipes[2]));
|
||||||
|
|
||||||
$handle->extraDataPipeWatcher = Loop::onReadable($stream, [$this, 'onProcessEndExtraDataPipeReadable'], $handle);
|
$handle->extraDataPipeWatcher = Loop::onReadable($stream, [self::class, 'onProcessEndExtraDataPipeReadable'], $handle);
|
||||||
Loop::unreference($handle->extraDataPipeWatcher);
|
Loop::unreference($handle->extraDataPipeWatcher);
|
||||||
|
|
||||||
$handle->startDeferred->resolve($handle);
|
$handle->sockets->resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function start(string $command, string $cwd = null, array $env = [], array $options = []): ProcessHandle {
|
||||||
*/
|
|
||||||
public function start(string $command, string $cwd = null, array $env = [], array $options = []): Promise {
|
|
||||||
$command = \sprintf(
|
$command = \sprintf(
|
||||||
'{ (%s) <&3 3<&- 3>/dev/null & } 3<&0;' .
|
'{ (%s) <&3 3<&- 3>/dev/null & } 3<&0;' .
|
||||||
'pid=$!; echo $pid >&3; wait $pid; RC=$?; echo $RC >&3; exit $RC',
|
'pid=$!; echo $pid >&3; wait $pid; RC=$?; echo $RC >&3; exit $RC',
|
||||||
@ -65,8 +74,7 @@ final class Runner implements ProcessRunner
|
|||||||
);
|
);
|
||||||
|
|
||||||
$handle = new Handle;
|
$handle = new Handle;
|
||||||
|
$handle->proc = @\proc_open($command, self::FD_SPEC, $pipes, $cwd ?: null, $env ?: null, $options);
|
||||||
$handle->proc = @\proc_open($command, self::FD_SPEC, $handle->pipes, $cwd ?: null, $env ?: null, $options);
|
|
||||||
|
|
||||||
if (!\is_resource($handle->proc)) {
|
if (!\is_resource($handle->proc)) {
|
||||||
$message = "Could not start process";
|
$message = "Could not start process";
|
||||||
@ -83,36 +91,40 @@ final class Runner implements ProcessRunner
|
|||||||
throw new ProcessException("Could not get process status");
|
throw new ProcessException("Could not get process status");
|
||||||
}
|
}
|
||||||
|
|
||||||
\stream_set_blocking($handle->pipes[3], false);
|
$stdinDeferred = new Deferred;
|
||||||
|
$handle->stdin = new ProcessOutputStream($stdinDeferred->promise());
|
||||||
|
|
||||||
/* It's fine to use an instance method here because this object is assigned to a static var in Process and never
|
$stdoutDeferred = new Deferred;
|
||||||
needs to be dtor'd before the process ends */
|
$handle->stdout = new ProcessInputStream($stdoutDeferred->promise());
|
||||||
Loop::onReadable($handle->pipes[3], [$this, 'onProcessStartExtraDataPipeReadable'], $handle);
|
|
||||||
|
|
||||||
return $handle->startDeferred->promise();
|
$stderrDeferred = new Deferred;
|
||||||
|
$handle->stderr = new ProcessInputStream($stderrDeferred->promise());
|
||||||
|
|
||||||
|
$handle->extraDataPipe = $pipes[3];
|
||||||
|
|
||||||
|
\stream_set_blocking($pipes[3], false);
|
||||||
|
|
||||||
|
Loop::onReadable($pipes[3], [self::class, 'onProcessStartExtraDataPipeReadable'], [$handle, $pipes, [
|
||||||
|
$stdinDeferred, $stdoutDeferred, $stderrDeferred
|
||||||
|
]]);
|
||||||
|
|
||||||
|
return $handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function join(ProcessHandle $handle): Promise {
|
public function join(ProcessHandle $handle): Promise {
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
|
||||||
if ($handle->extraDataPipeWatcher !== null) {
|
if ($handle->extraDataPipeWatcher !== null) {
|
||||||
Loop::reference($handle->extraDataPipeWatcher);
|
Loop::reference($handle->extraDataPipeWatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $handle->endDeferred->promise();
|
return $handle->joinDeferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function kill(ProcessHandle $handle) {
|
public function kill(ProcessHandle $handle) {
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
if (!\proc_terminate($handle->proc, 9)) { // Forcefully kill the process using SIGKILL.
|
||||||
// Forcefully kill the process using SIGKILL.
|
|
||||||
if (!\proc_terminate($handle->proc, 9)) {
|
|
||||||
throw new ProcessException("Terminating process failed");
|
throw new ProcessException("Terminating process failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,28 +132,21 @@ final class Runner implements ProcessRunner
|
|||||||
$handle->extraDataPipeWatcher = null;
|
$handle->extraDataPipeWatcher = null;
|
||||||
|
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
|
$handle->joinDeferred->fail(new ProcessException("The process was killed"));
|
||||||
$handle->endDeferred->fail(new ProcessException("The process was killed"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function signal(ProcessHandle $handle, int $signo) {
|
public function signal(ProcessHandle $handle, int $signo) {
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
|
||||||
if (!\proc_terminate($handle->proc, $signo)) {
|
if (!\proc_terminate($handle->proc, $signo)) {
|
||||||
throw new ProcessException("Sending signal to process failed");
|
throw new ProcessException("Sending signal to process failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function destroy(ProcessHandle $handle) {
|
public function destroy(ProcessHandle $handle) {
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
if ($handle->status < ProcessStatus::ENDED && \getmypid() === $handle->originalParentPid) {
|
||||||
if (\getmypid() === $handle->originalParentPid && $handle->status < ProcessStatus::ENDED) {
|
|
||||||
$this->kill($handle);
|
$this->kill($handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,12 +154,14 @@ final class Runner implements ProcessRunner
|
|||||||
Loop::cancel($handle->extraDataPipeWatcher);
|
Loop::cancel($handle->extraDataPipeWatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
for ($i = 0; $i < 4; $i++) {
|
if (\is_resource($handle->extraDataPipe)) {
|
||||||
if (\is_resource($handle->pipes[$i] ?? null)) {
|
\fclose($handle->extraDataPipe);
|
||||||
\fclose($handle->pipes[$i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$handle->stdin->close();
|
||||||
|
$handle->stdout->close();
|
||||||
|
$handle->stderr->close();
|
||||||
|
|
||||||
if (\is_resource($handle->proc)) {
|
if (\is_resource($handle->proc)) {
|
||||||
\proc_close($handle->proc);
|
\proc_close($handle->proc);
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,25 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal;
|
namespace Amp\Process\Internal;
|
||||||
|
|
||||||
use Amp\ByteStream\ResourceInputStream;
|
use Amp\Deferred;
|
||||||
use Amp\ByteStream\ResourceOutputStream;
|
use Amp\Process\ProcessInputStream;
|
||||||
|
use Amp\Process\ProcessOutputStream;
|
||||||
|
use Amp\Struct;
|
||||||
|
|
||||||
abstract class ProcessHandle
|
abstract class ProcessHandle {
|
||||||
{
|
use Struct;
|
||||||
/** @var ResourceOutputStream */
|
|
||||||
|
/** @var ProcessOutputStream */
|
||||||
public $stdin;
|
public $stdin;
|
||||||
|
|
||||||
/** @var ResourceInputStream */
|
/** @var ProcessInputStream */
|
||||||
public $stdout;
|
public $stdout;
|
||||||
|
|
||||||
/** @var ResourceInputStream */
|
/** @var ProcessInputStream */
|
||||||
public $stderr;
|
public $stderr;
|
||||||
|
|
||||||
/** @var int */
|
/** @var Deferred */
|
||||||
public $pid = 0;
|
public $pidDeferred;
|
||||||
|
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
public $status = ProcessStatus::STARTING;
|
public $status = ProcessStatus::STARTING;
|
||||||
|
@ -2,54 +2,56 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal;
|
namespace Amp\Process\Internal;
|
||||||
|
|
||||||
|
use Amp\Process\ProcessException;
|
||||||
use Amp\Promise;
|
use Amp\Promise;
|
||||||
|
|
||||||
interface ProcessRunner
|
interface ProcessRunner {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Start a process using the supplied parameters
|
* Start a process using the supplied parameters.
|
||||||
*
|
*
|
||||||
* @param string $command The command to execute
|
* @param string $command The command to execute.
|
||||||
* @param string|null $cwd The working directory for the child process
|
* @param string|null $cwd The working directory for the child process.
|
||||||
* @param array $env Environment variables to pass to the child process
|
* @param array $env Environment variables to pass to the child process.
|
||||||
* @param array $options proc_open() options
|
* @param array $options `proc_open()` options.
|
||||||
* @return Promise <ProcessInfo> Succeeds with a process descriptor or fails if the process cannot be started
|
*
|
||||||
* @throws \Amp\Process\ProcessException If starting the process fails.
|
* @return ProcessHandle
|
||||||
|
*
|
||||||
|
* @throws ProcessException If starting the process fails.
|
||||||
*/
|
*/
|
||||||
function start(string $command, string $cwd = null, array $env = [], array $options = []): Promise;
|
public function start(string $command, string $cwd = null, array $env = [], array $options = []): ProcessHandle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the child process to end
|
* Wait for the child process to end.
|
||||||
|
*
|
||||||
|
* @param ProcessHandle $handle The process descriptor.
|
||||||
*
|
*
|
||||||
* @param ProcessHandle $handle The process descriptor
|
|
||||||
* @return Promise <int> Succeeds with exit code of the process or fails if the process is killed.
|
* @return Promise <int> Succeeds with exit code of the process or fails if the process is killed.
|
||||||
*/
|
*/
|
||||||
function join(ProcessHandle $handle): Promise;
|
public function join(ProcessHandle $handle): Promise;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcibly end the child process
|
* Forcibly end the child process.
|
||||||
*
|
*
|
||||||
* @param ProcessHandle $handle The process descriptor
|
* @param ProcessHandle $handle The process descriptor.
|
||||||
* @return void
|
*
|
||||||
* @throws \Amp\Process\ProcessException If terminating the process fails
|
* @throws ProcessException If terminating the process fails.
|
||||||
*/
|
*/
|
||||||
function kill(ProcessHandle $handle);
|
public function kill(ProcessHandle $handle);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a signal signal to the child process
|
* Send a signal signal to the child process
|
||||||
*
|
*
|
||||||
* @param ProcessHandle $handle The process descriptor
|
* @param ProcessHandle $handle The process descriptor.
|
||||||
* @param int $signo Signal number to send to process.
|
* @param int $signo Signal number to send to process.
|
||||||
* @return void
|
*
|
||||||
* @throws \Amp\Process\ProcessException If sending the signal fails.
|
* @throws ProcessException If sending the signal fails.
|
||||||
*/
|
*/
|
||||||
function signal(ProcessHandle $handle, int $signo);
|
public function signal(ProcessHandle $handle, int $signo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release all resources held by the process handle
|
* Release all resources held by the process handle.
|
||||||
*
|
*
|
||||||
* @param ProcessHandle $handle The process descriptor
|
* @param ProcessHandle $handle The process descriptor.
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
function destroy(ProcessHandle $handle);
|
public function destroy(ProcessHandle $handle);
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal;
|
namespace Amp\Process\Internal;
|
||||||
|
|
||||||
final class ProcessStatus
|
final class ProcessStatus {
|
||||||
{
|
|
||||||
const STARTING = 0;
|
const STARTING = 0;
|
||||||
const RUNNING = 1;
|
const RUNNING = 1;
|
||||||
const ENDED = 2;
|
const ENDED = 2;
|
||||||
|
|
||||||
private function __construct() { }
|
private function __construct() {
|
||||||
|
// empty to prevent instances of this class
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,18 +5,13 @@ namespace Amp\Process\Internal\Windows;
|
|||||||
use Amp\Deferred;
|
use Amp\Deferred;
|
||||||
use Amp\Process\Internal\ProcessHandle;
|
use Amp\Process\Internal\ProcessHandle;
|
||||||
|
|
||||||
final class Handle extends ProcessHandle
|
final class Handle extends ProcessHandle {
|
||||||
{
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->startDeferred = new Deferred;
|
$this->joinDeferred = new Deferred;
|
||||||
$this->endDeferred = new Deferred;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var Deferred */
|
/** @var Deferred */
|
||||||
public $startDeferred;
|
public $joinDeferred;
|
||||||
|
|
||||||
/** @var Deferred */
|
|
||||||
public $endDeferred;
|
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $exitCodeWatcher;
|
public $exitCodeWatcher;
|
||||||
@ -33,6 +28,9 @@ final class Handle extends ProcessHandle
|
|||||||
/** @var resource[] */
|
/** @var resource[] */
|
||||||
public $sockets;
|
public $sockets;
|
||||||
|
|
||||||
|
/** @var Deferred[] */
|
||||||
|
public $stdioDeferreds;
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
public $connectTimeoutWatcher;
|
public $connectTimeoutWatcher;
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal\Windows;
|
namespace Amp\Process\Internal\Windows;
|
||||||
|
|
||||||
final class HandshakeStatus
|
final class HandshakeStatus {
|
||||||
{
|
|
||||||
const SUCCESS = 0;
|
const SUCCESS = 0;
|
||||||
const SIGNAL_UNEXPECTED = 0x01;
|
const SIGNAL_UNEXPECTED = 0x01;
|
||||||
const INVALID_STREAM_ID = 0x02;
|
const INVALID_STREAM_ID = 0x02;
|
||||||
@ -11,5 +10,7 @@ final class HandshakeStatus
|
|||||||
const DUPLICATE_STREAM_ID = 0x04;
|
const DUPLICATE_STREAM_ID = 0x04;
|
||||||
const INVALID_CLIENT_TOKEN = 0x05;
|
const INVALID_CLIENT_TOKEN = 0x05;
|
||||||
|
|
||||||
private function __construct() { }
|
private function __construct() {
|
||||||
|
// empty to prevent instances of this class
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal\Windows;
|
namespace Amp\Process\Internal\Windows;
|
||||||
|
|
||||||
final class PendingSocketClient
|
use Amp\Struct;
|
||||||
{
|
|
||||||
|
final class PendingSocketClient {
|
||||||
|
use Struct;
|
||||||
|
|
||||||
public $readWatcher;
|
public $readWatcher;
|
||||||
public $timeoutWatcher;
|
public $timeoutWatcher;
|
||||||
public $recievedDataBuffer = '';
|
public $receivedDataBuffer = '';
|
||||||
public $pid;
|
public $pid;
|
||||||
public $streamId;
|
public $streamId;
|
||||||
}
|
}
|
||||||
|
@ -2,22 +2,25 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal\Windows;
|
namespace Amp\Process\Internal\Windows;
|
||||||
|
|
||||||
|
use Amp\Deferred;
|
||||||
use Amp\Loop;
|
use Amp\Loop;
|
||||||
use Amp\Process\Internal\ProcessHandle;
|
use Amp\Process\Internal\ProcessHandle;
|
||||||
use Amp\Process\Internal\ProcessRunner;
|
use Amp\Process\Internal\ProcessRunner;
|
||||||
use Amp\Process\Internal\ProcessStatus;
|
use Amp\Process\Internal\ProcessStatus;
|
||||||
use Amp\Process\ProcessException;
|
use Amp\Process\ProcessException;
|
||||||
|
use Amp\Process\ProcessInputStream;
|
||||||
|
use Amp\Process\ProcessOutputStream;
|
||||||
use Amp\Promise;
|
use Amp\Promise;
|
||||||
use const Amp\Process\BIN_DIR;
|
use const Amp\Process\BIN_DIR;
|
||||||
|
|
||||||
final class Runner implements ProcessRunner
|
final class Runner implements ProcessRunner {
|
||||||
{
|
|
||||||
const FD_SPEC = [
|
const FD_SPEC = [
|
||||||
["pipe", "r"], // stdin
|
["pipe", "r"], // stdin
|
||||||
["pipe", "w"], // stdout
|
["pipe", "w"], // stdout
|
||||||
["pipe", "w"], // stderr
|
["pipe", "w"], // stderr
|
||||||
["pipe", "w"], // exit code pipe
|
["pipe", "w"], // exit code pipe
|
||||||
];
|
];
|
||||||
|
|
||||||
const WRAPPER_EXE_PATH = PHP_INT_SIZE === 8
|
const WRAPPER_EXE_PATH = PHP_INT_SIZE === 8
|
||||||
? BIN_DIR . '\\windows\\ProcessWrapper64.exe'
|
? BIN_DIR . '\\windows\\ProcessWrapper64.exe'
|
||||||
: BIN_DIR . '\\windows\\ProcessWrapper.exe';
|
: BIN_DIR . '\\windows\\ProcessWrapper.exe';
|
||||||
@ -26,8 +29,8 @@ final class Runner implements ProcessRunner
|
|||||||
|
|
||||||
private function makeCommand(string $command, string $workingDirectory): string {
|
private function makeCommand(string $command, string $workingDirectory): string {
|
||||||
$result = sprintf(
|
$result = sprintf(
|
||||||
'"%s" --address=%s --port=%d --token-size=%d',
|
'%s --address=%s --port=%d --token-size=%d',
|
||||||
self::WRAPPER_EXE_PATH,
|
\escapeshellarg(self::WRAPPER_EXE_PATH),
|
||||||
$this->socketConnector->address,
|
$this->socketConnector->address,
|
||||||
$this->socketConnector->port,
|
$this->socketConnector->port,
|
||||||
SocketConnector::SECURITY_TOKEN_SIZE
|
SocketConnector::SECURITY_TOKEN_SIZE
|
||||||
@ -46,11 +49,8 @@ final class Runner implements ProcessRunner
|
|||||||
$this->socketConnector = new SocketConnector;
|
$this->socketConnector = new SocketConnector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function start(string $command, string $cwd = null, array $env = [], array $options = []): ProcessHandle {
|
||||||
*/
|
|
||||||
public function start(string $command, string $cwd = null, array $env = [], array $options = []): Promise
|
|
||||||
{
|
|
||||||
$command = $this->makeCommand($command, $cwd ?? '');
|
$command = $this->makeCommand($command, $cwd ?? '');
|
||||||
|
|
||||||
$options['bypass_shell'] = true;
|
$options['bypass_shell'] = true;
|
||||||
@ -90,33 +90,34 @@ final class Runner implements ProcessRunner
|
|||||||
$handle->wrapperPid = $status['pid'];
|
$handle->wrapperPid = $status['pid'];
|
||||||
$handle->wrapperStderrPipe = $pipes[2];
|
$handle->wrapperStderrPipe = $pipes[2];
|
||||||
|
|
||||||
|
$stdinDeferred = new Deferred;
|
||||||
|
$handle->stdioDeferreds[] = new ProcessOutputStream($stdinDeferred->promise());
|
||||||
|
|
||||||
|
$stdoutDeferred = new Deferred;
|
||||||
|
$handle->stdioDeferreds[] = new ProcessInputStream($stdoutDeferred->promise());
|
||||||
|
|
||||||
|
$stderrDeferred = new Deferred;
|
||||||
|
$handle->stdioDeferreds[] = new ProcessInputStream($stderrDeferred->promise());
|
||||||
|
|
||||||
$this->socketConnector->registerPendingProcess($handle);
|
$this->socketConnector->registerPendingProcess($handle);
|
||||||
|
|
||||||
return $handle->startDeferred->promise();
|
return $handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function join(ProcessHandle $handle): Promise {
|
||||||
*/
|
|
||||||
public function join(ProcessHandle $handle): Promise
|
|
||||||
{
|
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
|
||||||
if ($handle->exitCodeWatcher !== null) {
|
if ($handle->exitCodeWatcher !== null) {
|
||||||
Loop::reference($handle->exitCodeWatcher);
|
Loop::reference($handle->exitCodeWatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $handle->endDeferred->promise();
|
return $handle->joinDeferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function kill(ProcessHandle $handle) {
|
||||||
*/
|
|
||||||
public function kill(ProcessHandle $handle)
|
|
||||||
{
|
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
// todo: send a signal to the wrapper to kill the child instead?
|
||||||
// todo: send a signal to the wrapper to kill the child instead ?
|
|
||||||
if (!\proc_terminate($handle->proc)) {
|
if (!\proc_terminate($handle->proc)) {
|
||||||
throw new ProcessException("Terminating process failed");
|
throw new ProcessException("Terminating process failed");
|
||||||
}
|
}
|
||||||
@ -125,25 +126,17 @@ final class Runner implements ProcessRunner
|
|||||||
$handle->exitCodeWatcher = null;
|
$handle->exitCodeWatcher = null;
|
||||||
|
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
|
$handle->joinDeferred->fail(new ProcessException("The process was killed"));
|
||||||
$handle->endDeferred->fail(new ProcessException("The process was killed"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function signal(ProcessHandle $handle, int $signo) {
|
||||||
*/
|
|
||||||
public function signal(ProcessHandle $handle, int $signo)
|
|
||||||
{
|
|
||||||
throw new ProcessException('Signals are not supported on Windows');
|
throw new ProcessException('Signals are not supported on Windows');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
public function destroy(ProcessHandle $handle) {
|
||||||
*/
|
|
||||||
public function destroy(ProcessHandle $handle)
|
|
||||||
{
|
|
||||||
/** @var Handle $handle */
|
/** @var Handle $handle */
|
||||||
|
|
||||||
if ($handle->status < ProcessStatus::ENDED) {
|
if ($handle->status < ProcessStatus::ENDED) {
|
||||||
$this->kill($handle);
|
$this->kill($handle);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace Amp\Process\Internal\Windows;
|
namespace Amp\Process\Internal\Windows;
|
||||||
|
|
||||||
final class SignalCode
|
final class SignalCode {
|
||||||
{
|
|
||||||
const HANDSHAKE = 0x01;
|
const HANDSHAKE = 0x01;
|
||||||
const HANDSHAKE_ACK = 0x02;
|
const HANDSHAKE_ACK = 0x02;
|
||||||
const CHILD_PID = 0x03;
|
const CHILD_PID = 0x03;
|
||||||
const EXIT_CODE = 0x04;
|
const EXIT_CODE = 0x04;
|
||||||
|
|
||||||
private function __construct() { }
|
private function __construct() {
|
||||||
|
// empty to prevent instances of this class
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,7 @@ use Amp\Loop;
|
|||||||
use Amp\Process\Internal\ProcessStatus;
|
use Amp\Process\Internal\ProcessStatus;
|
||||||
use Amp\Process\ProcessException;
|
use Amp\Process\ProcessException;
|
||||||
|
|
||||||
final class SocketConnector
|
final class SocketConnector {
|
||||||
{
|
|
||||||
const SERVER_SOCKET_URI = 'tcp://127.0.0.1:0';
|
const SERVER_SOCKET_URI = 'tcp://127.0.0.1:0';
|
||||||
const SECURITY_TOKEN_SIZE = 16;
|
const SECURITY_TOKEN_SIZE = 16;
|
||||||
const CONNECT_TIMEOUT = 1000;
|
const CONNECT_TIMEOUT = 1000;
|
||||||
@ -29,12 +28,9 @@ final class SocketConnector
|
|||||||
/** @var int */
|
/** @var int */
|
||||||
public $port;
|
public $port;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->server = \stream_socket_server(
|
$flags = \STREAM_SERVER_LISTEN | \STREAM_SERVER_BIND;
|
||||||
self::SERVER_SOCKET_URI,
|
$this->server = \stream_socket_server(self::SERVER_SOCKET_URI, $errNo, $errStr, $flags);
|
||||||
$errNo, $errStr,
|
|
||||||
\STREAM_SERVER_LISTEN | \STREAM_SERVER_BIND
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$this->server) {
|
if (!$this->server) {
|
||||||
throw new \Error("Failed to create TCP server socket for process wrapper: {$errNo}: {$errStr}");
|
throw new \Error("Failed to create TCP server socket for process wrapper: {$errNo}: {$errStr}");
|
||||||
@ -45,61 +41,64 @@ final class SocketConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
list($this->address, $this->port) = \explode(':', \stream_socket_get_name($this->server, false));
|
list($this->address, $this->port) = \explode(':', \stream_socket_get_name($this->server, false));
|
||||||
$this->port = (int)$this->port;
|
$this->port = (int) $this->port;
|
||||||
|
|
||||||
Loop::unreference(Loop::onReadable($this->server, [$this, 'onServerSocketReadable']));
|
Loop::unreference(Loop::onReadable($this->server, [$this, 'onServerSocketReadable']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function failClientHandshake($socket, int $code): void {
|
||||||
private function failClientHandshake($socket, int $code): void
|
|
||||||
{
|
|
||||||
\fwrite($socket, \chr(SignalCode::HANDSHAKE_ACK) . \chr($code));
|
\fwrite($socket, \chr(SignalCode::HANDSHAKE_ACK) . \chr($code));
|
||||||
\fclose($socket);
|
\fclose($socket);
|
||||||
unset($this->pendingClients[(int)$socket]);
|
|
||||||
|
unset($this->pendingClients[(int) $socket]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function failHandleStart(Handle $handle, string $message, ...$args)
|
private function failHandleStart(Handle $handle, string $message, ...$args) {
|
||||||
{
|
|
||||||
Loop::cancel($handle->connectTimeoutWatcher);
|
Loop::cancel($handle->connectTimeoutWatcher);
|
||||||
|
|
||||||
unset($this->pendingProcesses[$handle->wrapperPid]);
|
unset($this->pendingProcesses[$handle->wrapperPid]);
|
||||||
|
|
||||||
foreach ($handle->sockets as $socket) {
|
foreach ($handle->sockets as $socket) {
|
||||||
\fclose($socket);
|
\fclose($socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle->startDeferred->fail(new ProcessException(\vsprintf($message, $args)));
|
$error = new ProcessException(\vsprintf($message, $args));
|
||||||
|
|
||||||
|
foreach ($handle->stdioDeferreds as $deferred) {
|
||||||
|
$deferred->fail($error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read data from a client socket
|
* 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.
|
* This method cleans up internal state as appropriate. Returns null if the read fails or needs to be repeated.
|
||||||
*
|
*
|
||||||
* @param resource $socket
|
* @param resource $socket
|
||||||
* @param int $length
|
* @param int $length
|
||||||
* @param PendingSocketClient $state
|
* @param PendingSocketClient $state
|
||||||
|
*
|
||||||
* @return string|null
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
private function readDataFromPendingClient($socket, int $length, PendingSocketClient $state)
|
private function readDataFromPendingClient($socket, int $length, PendingSocketClient $state) {
|
||||||
{
|
|
||||||
$data = \fread($socket, $length);
|
$data = \fread($socket, $length);
|
||||||
|
|
||||||
if ($data === false || $data === '') {
|
if ($data === false || $data === '') {
|
||||||
\fclose($socket);
|
\fclose($socket);
|
||||||
Loop::cancel($state->readWatcher);
|
Loop::cancel($state->readWatcher);
|
||||||
Loop::cancel($state->timeoutWatcher);
|
Loop::cancel($state->timeoutWatcher);
|
||||||
unset($this->pendingClients[(int)$socket]);
|
unset($this->pendingClients[(int) $socket]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $state->recievedDataBuffer . $data;
|
$data = $state->receivedDataBuffer . $data;
|
||||||
|
|
||||||
if (\strlen($data) < $length) {
|
if (\strlen($data) < $length) {
|
||||||
$state->recievedDataBuffer = $data;
|
$state->receivedDataBuffer = $data;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$state->recievedDataBuffer = '';
|
$state->receivedDataBuffer = '';
|
||||||
|
|
||||||
Loop::cancel($state->readWatcher);
|
Loop::cancel($state->readWatcher);
|
||||||
Loop::cancel($state->timeoutWatcher);
|
Loop::cancel($state->timeoutWatcher);
|
||||||
@ -107,8 +106,8 @@ final class SocketConnector
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onReadable_Handshake($watcher, $socket) {
|
public function onReadableHandshake($watcher, $socket) {
|
||||||
$socketId = (int)$socket;
|
$socketId = (int) $socket;
|
||||||
$pendingClient = $this->pendingClients[$socketId];
|
$pendingClient = $this->pendingClients[$socketId];
|
||||||
|
|
||||||
if (null === $data = $this->readDataFromPendingClient($socket, self::SECURITY_TOKEN_SIZE + 6, $pendingClient)) {
|
if (null === $data = $this->readDataFromPendingClient($socket, self::SECURITY_TOKEN_SIZE + 6, $pendingClient)) {
|
||||||
@ -146,7 +145,7 @@ final class SocketConnector
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($packet['client_token'] !== $handle->securityTokens[$packet['stream_id']]) {
|
if (!\hash_equals($packet['client_token'], $handle->securityTokens[$packet['stream_id']])) {
|
||||||
$this->failClientHandshake($socket, HandshakeStatus::INVALID_CLIENT_TOKEN);
|
$this->failClientHandshake($socket, HandshakeStatus::INVALID_CLIENT_TOKEN);
|
||||||
$this->failHandleStart($handle, "Invalid client security token for stream #%d", $packet['stream_id']);
|
$this->failHandleStart($handle, "Invalid client security token for stream #%d", $packet['stream_id']);
|
||||||
return;
|
return;
|
||||||
@ -164,13 +163,11 @@ final class SocketConnector
|
|||||||
|
|
||||||
$pendingClient->pid = $packet['pid'];
|
$pendingClient->pid = $packet['pid'];
|
||||||
$pendingClient->streamId = $packet['stream_id'];
|
$pendingClient->streamId = $packet['stream_id'];
|
||||||
|
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadableHandshakeAck']);
|
||||||
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadable_HandshakeAck']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onReadable_HandshakeAck($watcher, $socket)
|
public function onReadableHandshakeAck($watcher, $socket) {
|
||||||
{
|
$socketId = (int) $socket;
|
||||||
$socketId = (int)$socket;
|
|
||||||
$pendingClient = $this->pendingClients[$socketId];
|
$pendingClient = $this->pendingClients[$socketId];
|
||||||
|
|
||||||
// can happen if the start promise was failed
|
// can happen if the start promise was failed
|
||||||
@ -202,12 +199,11 @@ final class SocketConnector
|
|||||||
$handle->sockets[$pendingClient->streamId] = $socket;
|
$handle->sockets[$pendingClient->streamId] = $socket;
|
||||||
|
|
||||||
if (count($handle->sockets) === 3) {
|
if (count($handle->sockets) === 3) {
|
||||||
$pendingClient->readWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadable_ChildPid'], $handle);
|
$pendingClient->readWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadableChildPid'], $handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onReadable_ChildPid($watcher, $socket, Handle $handle)
|
public function onReadableChildPid($watcher, $socket, Handle $handle) {
|
||||||
{
|
|
||||||
Loop::cancel($watcher);
|
Loop::cancel($watcher);
|
||||||
Loop::cancel($handle->connectTimeoutWatcher);
|
Loop::cancel($handle->connectTimeoutWatcher);
|
||||||
|
|
||||||
@ -220,7 +216,7 @@ final class SocketConnector
|
|||||||
|
|
||||||
if (\strlen($data) !== 5) {
|
if (\strlen($data) !== 5) {
|
||||||
$this->failHandleStart(
|
$this->failHandleStart(
|
||||||
$handle, 'Failed to read PID from wrapper: Recieved %d of 5 expected bytes', \strlen($data)
|
$handle, 'Failed to read PID from wrapper: Received %d of 5 expected bytes', \strlen($data)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -235,20 +231,18 @@ final class SocketConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
$handle->status = ProcessStatus::RUNNING;
|
$handle->status = ProcessStatus::RUNNING;
|
||||||
$handle->pid = $packet['pid'];
|
$handle->pidDeferred->resolve($packet['pid']);
|
||||||
$handle->stdin = new ResourceOutputStream($handle->sockets[0]);
|
$handle->stdioDeferreds[0]->resolve(new ResourceOutputStream($handle->sockets[0]));
|
||||||
$handle->stdout = new ResourceInputStream($handle->sockets[1]);
|
$handle->stdioDeferreds[1]->resolve(new ResourceInputStream($handle->sockets[1]));
|
||||||
$handle->stderr = new ResourceInputStream($handle->sockets[2]);
|
$handle->stdioDeferreds[2]->resolve(new ResourceInputStream($handle->sockets[2]));
|
||||||
|
|
||||||
$handle->exitCodeWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadable_ExitCode'], $handle);
|
$handle->exitCodeWatcher = Loop::onReadable($handle->sockets[0], [$this, 'onReadableExitCode'], $handle);
|
||||||
Loop::unreference($handle->exitCodeWatcher);
|
Loop::unreference($handle->exitCodeWatcher);
|
||||||
|
|
||||||
unset($this->pendingProcesses[$handle->wrapperPid]);
|
unset($this->pendingProcesses[$handle->wrapperPid]);
|
||||||
$handle->startDeferred->resolve($handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onReadable_ExitCode($watcher, $socket, Handle $handle)
|
public function onReadableExitCode($watcher, $socket, Handle $handle) {
|
||||||
{
|
|
||||||
$handle->exitCodeWatcher = null;
|
$handle->exitCodeWatcher = null;
|
||||||
Loop::cancel($watcher);
|
Loop::cancel($watcher);
|
||||||
|
|
||||||
@ -256,13 +250,13 @@ final class SocketConnector
|
|||||||
|
|
||||||
if ($data === false || $data === '') {
|
if ($data === false || $data === '') {
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
$handle->endDeferred->fail(new ProcessException('Failed to read exit code from wrapper: No data received'));
|
$handle->joinDeferred->fail(new ProcessException('Failed to read exit code from wrapper: No data received'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\strlen($data) !== 5) {
|
if (\strlen($data) !== 5) {
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
$handle->endDeferred->fail(new ProcessException(
|
$handle->joinDeferred->fail(new ProcessException(
|
||||||
\sprintf('Failed to read exit code from wrapper: Recieved %d of 5 expected bytes', \strlen($data))
|
\sprintf('Failed to read exit code from wrapper: Recieved %d of 5 expected bytes', \strlen($data))
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
@ -278,11 +272,11 @@ final class SocketConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
$handle->status = ProcessStatus::ENDED;
|
$handle->status = ProcessStatus::ENDED;
|
||||||
$handle->endDeferred->resolve($packet['code']);
|
$handle->joinDeferred->resolve($packet['code']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onClientSocketConnectTimeout($watcher, $socket) {
|
public function onClientSocketConnectTimeout($watcher, $socket) {
|
||||||
$id = (int)$socket;
|
$id = (int) $socket;
|
||||||
|
|
||||||
Loop::cancel($this->pendingClients[$id]->readWatcher);
|
Loop::cancel($this->pendingClients[$id]->readWatcher);
|
||||||
unset($this->pendingClients[$id]);
|
unset($this->pendingClients[$id]);
|
||||||
@ -298,10 +292,10 @@ final class SocketConnector
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pendingClient = new PendingSocketClient;
|
$pendingClient = new PendingSocketClient;
|
||||||
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadable_Handshake']);
|
$pendingClient->readWatcher = Loop::onReadable($socket, [$this, 'onReadableHandshake']);
|
||||||
$pendingClient->timeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onClientSocketConnectTimeout'], $socket);
|
$pendingClient->timeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onClientSocketConnectTimeout'], $socket);
|
||||||
|
|
||||||
$this->pendingClients[(int)$socket] = $pendingClient;
|
$this->pendingClients[(int) $socket] = $pendingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onProcessConnectTimeout($watcher, Handle $handle) {
|
public function onProcessConnectTimeout($watcher, Handle $handle) {
|
||||||
@ -319,11 +313,13 @@ final class SocketConnector
|
|||||||
\fclose($socket);
|
\fclose($socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle->startDeferred->fail(new ProcessException(\trim($error)));
|
$error = new ProcessException(\trim($error));
|
||||||
|
foreach ($handle->stdioDeferreds as $deferred) {
|
||||||
|
$deferred->fail($error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function registerPendingProcess(Handle $handle)
|
public function registerPendingProcess(Handle $handle) {
|
||||||
{
|
|
||||||
$handle->connectTimeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onProcessConnectTimeout'], $handle);
|
$handle->connectTimeoutWatcher = Loop::delay(self::CONNECT_TIMEOUT, [$this, 'onProcessConnectTimeout'], $handle);
|
||||||
|
|
||||||
$this->pendingProcesses[$handle->wrapperPid] = $handle;
|
$this->pendingProcesses[$handle->wrapperPid] = $handle;
|
||||||
|
159
lib/Process.php
159
lib/Process.php
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
namespace Amp\Process;
|
namespace Amp\Process;
|
||||||
|
|
||||||
use Amp\ByteStream\ResourceInputStream;
|
|
||||||
use Amp\ByteStream\ResourceOutputStream;
|
|
||||||
use Amp\Deferred;
|
|
||||||
use Amp\Process\Internal\Posix\Runner as PosixProcessRunner;
|
use Amp\Process\Internal\Posix\Runner as PosixProcessRunner;
|
||||||
use Amp\Process\Internal\ProcessHandle;
|
use Amp\Process\Internal\ProcessHandle;
|
||||||
use Amp\Process\Internal\ProcessRunner;
|
use Amp\Process\Internal\ProcessRunner;
|
||||||
@ -32,52 +29,15 @@ class Process {
|
|||||||
private $handle;
|
private $handle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $command Command to run.
|
|
||||||
* @param string $cwd Working directory of child process.
|
|
||||||
* @param array $env Environment variables for child process.
|
|
||||||
* @param array $options Options for proc_open().
|
|
||||||
* @param ProcessHandle $handle Handle for the created process.
|
|
||||||
*/
|
|
||||||
private function __construct(string $command, string $cwd, array $env, array $options, ProcessHandle $handle) {
|
|
||||||
$this->command = $command;
|
|
||||||
$this->cwd = $cwd;
|
|
||||||
$this->env = $env;
|
|
||||||
$this->options = $options;
|
|
||||||
$this->handle = $handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the process if it is still running.
|
|
||||||
*/
|
|
||||||
public function __destruct() {
|
|
||||||
if ($this->handle !== null) {
|
|
||||||
self::$processRunner->destroy($this->handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Throw to prevent cloning
|
|
||||||
*
|
|
||||||
* @throws \Error
|
|
||||||
*/
|
|
||||||
public function __clone() {
|
|
||||||
throw new \Error(self::class . ' instances cannot be cloned');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new process.
|
|
||||||
*
|
|
||||||
* @param string|string[] $command Command to run.
|
* @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 current
|
* @param string|null $cwd Working directory or use an empty string to use the working directory of the
|
||||||
* PHP process.
|
* parent.
|
||||||
* @param mixed[] $env Environment variables or use an empty array to inherit from the current PHP process.
|
* @param mixed[] $env Environment variables or use an empty array to inherit from the parent.
|
||||||
* @param mixed[] $options Options for proc_open().
|
* @param mixed[] $options Options for `proc_open()`.
|
||||||
* @return Promise <Process> Fails with a ProcessException if starting the process fails.
|
*
|
||||||
* @throws \Error If the arguments are invalid.
|
* @throws \Error If the arguments are invalid.
|
||||||
* @throws \Amp\Process\StatusError If the process is already running.
|
|
||||||
* @throws \Amp\Process\ProcessException If starting the process fails.
|
|
||||||
*/
|
*/
|
||||||
public static function start($command, string $cwd = null, array $env = [], array $options = []): Promise {
|
public function __construct($command, string $cwd = null, array $env = [], array $options = []) {
|
||||||
$command = \is_array($command)
|
$command = \is_array($command)
|
||||||
? \implode(" ", \array_map("escapeshellarg", $command))
|
? \implode(" ", \array_map("escapeshellarg", $command))
|
||||||
: (string) $command;
|
: (string) $command;
|
||||||
@ -93,35 +53,58 @@ class Process {
|
|||||||
$envVars[(string) $key] = (string) $value;
|
$envVars[(string) $key] = (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
$deferred = new Deferred;
|
$this->command = $command;
|
||||||
|
$this->cwd = $cwd;
|
||||||
self::$processRunner->start($command, $cwd, $env, $options)
|
$this->env = $envVars;
|
||||||
->onResolve(function($error, $handle) use($deferred, $command, $cwd, $env, $options) {
|
$this->options = $options;
|
||||||
if ($error) {
|
|
||||||
$deferred->fail($error);
|
|
||||||
} else {
|
|
||||||
$deferred->resolve(new Process($command, $cwd, $env, $options, $handle));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $deferred->promise();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the process to end..
|
* Stops the process if it is still running.
|
||||||
|
*/
|
||||||
|
public function __destruct() {
|
||||||
|
if ($this->handle !== null) {
|
||||||
|
self::$processRunner->destroy($this->handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __clone() {
|
||||||
|
throw new \Error("Cloning is not allowed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the process.
|
||||||
|
*
|
||||||
|
* @throws StatusError If the process has already been started.
|
||||||
|
*/
|
||||||
|
public function start() {
|
||||||
|
if ($this->handle) {
|
||||||
|
throw new StatusError("Process has already been started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->handle = self::$processRunner->start($this->command, $this->cwd, $this->env, $this->options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the process to end.
|
||||||
*
|
*
|
||||||
* @return Promise <int> Succeeds with process exit code or fails with a ProcessException if the process is killed.
|
* @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 {
|
public function join(): Promise {
|
||||||
|
if (!$this->handle) {
|
||||||
|
throw new StatusError("Process has not been started.");
|
||||||
|
}
|
||||||
|
|
||||||
return self::$processRunner->join($this->handle);
|
return self::$processRunner->join($this->handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcibly end the process.
|
* Forcibly end the process.
|
||||||
*
|
*
|
||||||
* @return void
|
* @throws StatusError If the process is not running.
|
||||||
* @throws \Amp\Process\StatusError If the process is not running.
|
* @throws ProcessException If terminating the process fails.
|
||||||
* @throws \Amp\Process\ProcessException If terminating the process fails.
|
|
||||||
*/
|
*/
|
||||||
public function kill() {
|
public function kill() {
|
||||||
if (!$this->isRunning()) {
|
if (!$this->isRunning()) {
|
||||||
@ -135,9 +118,9 @@ class Process {
|
|||||||
* Send a signal signal to the process.
|
* Send a signal signal to the process.
|
||||||
*
|
*
|
||||||
* @param int $signo Signal number to send to process.
|
* @param int $signo Signal number to send to process.
|
||||||
* @return void
|
*
|
||||||
* @throws \Amp\Process\StatusError If the process is not running.
|
* @throws StatusError If the process is not running.
|
||||||
* @throws \Amp\Process\ProcessException If sending the signal fails.
|
* @throws ProcessException If sending the signal fails.
|
||||||
*/
|
*/
|
||||||
public function signal(int $signo) {
|
public function signal(int $signo) {
|
||||||
if (!$this->isRunning()) {
|
if (!$this->isRunning()) {
|
||||||
@ -150,11 +133,16 @@ class Process {
|
|||||||
/**
|
/**
|
||||||
* Returns the PID of the child process.
|
* Returns the PID of the child process.
|
||||||
*
|
*
|
||||||
* @return int
|
* @return Promise<int>
|
||||||
* @throws \Amp\Process\StatusError If the process has not started.
|
*
|
||||||
|
* @throws StatusError If the process has not started.
|
||||||
*/
|
*/
|
||||||
public function getPid(): int {
|
public function getPid(): Promise {
|
||||||
return $this->handle->pid;
|
if (!$this->handle) {
|
||||||
|
throw new StatusError("The process has not been started");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->handle->pidDeferred->promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -203,55 +191,48 @@ class Process {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isRunning(): bool {
|
public function isRunning(): bool {
|
||||||
return $this->handle->status === ProcessStatus::RUNNING;
|
return $this->handle && $this->handle->status !== ProcessStatus::ENDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the process input stream (STDIN).
|
* Gets the process input stream (STDIN).
|
||||||
*
|
*
|
||||||
* @return \Amp\ByteStream\ResourceOutputStream
|
* @return ProcessOutputStream
|
||||||
* @throws \Amp\Process\StatusError If the process is not running.
|
|
||||||
*/
|
*/
|
||||||
public function getStdin(): ResourceOutputStream {
|
public function getStdin(): ProcessOutputStream {
|
||||||
if (!$this->isRunning()) {
|
return $this->stdin;
|
||||||
throw new StatusError("The process is not running");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->handle->stdin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the process output stream (STDOUT).
|
* Gets the process output stream (STDOUT).
|
||||||
*
|
*
|
||||||
* @return \Amp\ByteStream\ResourceInputStream
|
* @return ProcessInputStream
|
||||||
* @throws \Amp\Process\StatusError If the process is not running.
|
|
||||||
*/
|
*/
|
||||||
public function getStdout(): ResourceInputStream {
|
public function getStdout(): ProcessInputStream {
|
||||||
if (!$this->isRunning()) {
|
if (!$this->isRunning()) {
|
||||||
throw new StatusError("The process is not running");
|
throw new StatusError("The process is not running");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->handle->stdout;
|
return $this->stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the process error stream (STDERR).
|
* Gets the process error stream (STDERR).
|
||||||
*
|
*
|
||||||
* @return \Amp\ByteStream\ResourceInputStream
|
* @return ProcessInputStream
|
||||||
* @throws \Amp\Process\StatusError If the process is not running.
|
|
||||||
*/
|
*/
|
||||||
public function getStderr(): ResourceInputStream {
|
public function getStderr(): ProcessInputStream {
|
||||||
if (!$this->isRunning()) {
|
if (!$this->isRunning()) {
|
||||||
throw new StatusError("The process is not running");
|
throw new StatusError("The process is not running");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->handle->stderr;
|
return $this->stderr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(function() {
|
(function () {
|
||||||
/** @noinspection PhpUndefinedClassInspection */
|
/** @noinspection PhpUndefinedClassInspection */
|
||||||
self::$processRunner = \strncasecmp(\PHP_OS, "WIN", 3) === 0
|
self::$processRunner = \strncasecmp(\PHP_OS, "WIN", 3) === 0
|
||||||
? new WindowsProcessRunner()
|
? new WindowsProcessRunner
|
||||||
: new PosixProcessRunner();
|
: new PosixProcessRunner;
|
||||||
})->bindTo(null, Process::class)();
|
})->bindTo(null, Process::class)();
|
||||||
|
80
lib/ProcessInputStream.php
Normal file
80
lib/ProcessInputStream.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Process;
|
||||||
|
|
||||||
|
use Amp\ByteStream\InputStream;
|
||||||
|
use Amp\ByteStream\PendingReadError;
|
||||||
|
use Amp\ByteStream\ResourceInputStream;
|
||||||
|
use Amp\ByteStream\StreamException;
|
||||||
|
use Amp\Deferred;
|
||||||
|
use Amp\Failure;
|
||||||
|
use Amp\Promise;
|
||||||
|
|
||||||
|
class ProcessInputStream implements InputStream {
|
||||||
|
/** @var Deferred */
|
||||||
|
private $initialRead;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $shouldClose = false;
|
||||||
|
|
||||||
|
/** @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);
|
||||||
|
$this->initialRead->fail($this->error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resourceStream = $resourceStream;
|
||||||
|
|
||||||
|
if ($this->shouldClose) {
|
||||||
|
$this->resourceStream->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->initialRead) {
|
||||||
|
$initialRead = $this->initialRead;
|
||||||
|
$this->initialRead = null;
|
||||||
|
$initialRead->resolve($this->shouldClose ? null : "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->initialRead = new Deferred;
|
||||||
|
|
||||||
|
return $this->initialRead->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close() {
|
||||||
|
$this->shouldClose = true;
|
||||||
|
|
||||||
|
if ($this->resourceStream) {
|
||||||
|
$this->resourceStream->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
lib/ProcessOutputStream.php
Normal file
104
lib/ProcessOutputStream.php
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Process;
|
||||||
|
|
||||||
|
use Amp\ByteStream\ClosedException;
|
||||||
|
use Amp\ByteStream\OutputStream;
|
||||||
|
use Amp\ByteStream\ResourceOutputStream;
|
||||||
|
use Amp\ByteStream\StreamException;
|
||||||
|
use Amp\Deferred;
|
||||||
|
use Amp\Failure;
|
||||||
|
use Amp\Promise;
|
||||||
|
|
||||||
|
class ProcessOutputStream implements OutputStream {
|
||||||
|
/** @var array */
|
||||||
|
private $queuedWrites;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $shouldClose = false;
|
||||||
|
|
||||||
|
/** @var ResourceOutputStream */
|
||||||
|
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);
|
||||||
|
|
||||||
|
while ($write = array_shift($this->queuedWrites)) {
|
||||||
|
/** @var $deferred Deferred */
|
||||||
|
list(, $deferred) = $write;
|
||||||
|
$deferred->fail($this->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->resourceStream = $resourceStream;
|
||||||
|
|
||||||
|
$queue = $this->queuedWrites;
|
||||||
|
$this->queuedWrites = [];
|
||||||
|
|
||||||
|
foreach ($queue as list($data, $deferred)) {
|
||||||
|
$deferred->resolve($this->resourceStream->write($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
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[] = [$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[] = [$finalData, $deferred];
|
||||||
|
|
||||||
|
$this->shouldClose = true;
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close() {
|
||||||
|
$this->shouldClose = true;
|
||||||
|
|
||||||
|
if ($this->resourceStream) {
|
||||||
|
$this->resourceStream->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,6 @@ class ProcessTest extends TestCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testExecuteResolvesToExitCode() {
|
public function testExecuteResolvesToExitCode() {
|
||||||
Loop::run(function () {
|
Loop::run(function () {
|
||||||
$process = new Process("exit 42");
|
$process = new Process("exit 42");
|
||||||
|
Loading…
Reference in New Issue
Block a user