2017-09-14 19:34:18 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\Process\Internal\Windows;
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
use Amp\Deferred;
|
2017-09-14 19:34:18 +02:00
|
|
|
use Amp\Loop;
|
|
|
|
use Amp\Process\Internal\ProcessHandle;
|
|
|
|
use Amp\Process\Internal\ProcessRunner;
|
|
|
|
use Amp\Process\Internal\ProcessStatus;
|
|
|
|
use Amp\Process\ProcessException;
|
2017-09-17 17:58:05 +02:00
|
|
|
use Amp\Process\ProcessInputStream;
|
|
|
|
use Amp\Process\ProcessOutputStream;
|
2017-09-14 19:34:18 +02:00
|
|
|
use Amp\Promise;
|
|
|
|
use const Amp\Process\BIN_DIR;
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
final class Runner implements ProcessRunner {
|
2017-09-14 19:34:18 +02:00
|
|
|
const FD_SPEC = [
|
|
|
|
["pipe", "r"], // stdin
|
|
|
|
["pipe", "w"], // stdout
|
|
|
|
["pipe", "w"], // stderr
|
|
|
|
["pipe", "w"], // exit code pipe
|
|
|
|
];
|
2017-09-17 17:58:05 +02:00
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
const WRAPPER_EXE_PATH = PHP_INT_SIZE === 8
|
|
|
|
? BIN_DIR . '\\windows\\ProcessWrapper64.exe'
|
|
|
|
: BIN_DIR . '\\windows\\ProcessWrapper.exe';
|
|
|
|
|
|
|
|
private $socketConnector;
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
private function makeCommand(string $workingDirectory): string {
|
2017-09-14 19:34:18 +02:00
|
|
|
$result = sprintf(
|
2017-09-17 17:58:05 +02:00
|
|
|
'%s --address=%s --port=%d --token-size=%d',
|
|
|
|
\escapeshellarg(self::WRAPPER_EXE_PATH),
|
2017-09-14 19:34:18 +02:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
public function start(string $command, string $cwd = null, array $env = [], array $options = []): ProcessHandle {
|
2017-09-17 19:07:13 +02:00
|
|
|
if (strpos($command, "\0") !== false) {
|
|
|
|
throw new ProcessException("Can't execute commands that contain null bytes.");
|
|
|
|
}
|
2017-09-14 19:34:18 +02:00
|
|
|
|
|
|
|
$options['bypass_shell'] = true;
|
|
|
|
|
|
|
|
$handle = new Handle;
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->proc = @\proc_open($this->makeCommand($cwd ?? ''), self::FD_SPEC, $pipes, $cwd ?: null, $env ?: null, $options);
|
2017-09-14 19:34:18 +02:00
|
|
|
|
|
|
|
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);
|
2017-09-17 19:07:13 +02:00
|
|
|
$written = \fwrite($pipes[0], $securityTokens . "\0" . $command . "\0");
|
2017-09-14 19:34:18 +02:00
|
|
|
|
|
|
|
\fclose($pipes[0]);
|
|
|
|
\fclose($pipes[1]);
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
if ($written !== SocketConnector::SECURITY_TOKEN_SIZE * 6 + \strlen($command) + 2) {
|
2017-09-14 19:34:18 +02:00
|
|
|
\fclose($pipes[2]);
|
|
|
|
\proc_close($handle->proc);
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
throw new ProcessException("Could not send security tokens / command to process wrapper");
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$handle->securityTokens = \str_split($securityTokens, SocketConnector::SECURITY_TOKEN_SIZE);
|
|
|
|
$handle->wrapperPid = $status['pid'];
|
|
|
|
$handle->wrapperStderrPipe = $pipes[2];
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
$stdinDeferred = new Deferred;
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->stdioDeferreds[] = $stdinDeferred;
|
|
|
|
$handle->stdin = new ProcessOutputStream($stdinDeferred->promise());
|
2017-09-17 17:58:05 +02:00
|
|
|
|
|
|
|
$stdoutDeferred = new Deferred;
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->stdioDeferreds[] = $stdoutDeferred;
|
|
|
|
$handle->stdout = new ProcessInputStream($stdoutDeferred->promise());
|
2017-09-17 17:58:05 +02:00
|
|
|
|
|
|
|
$stderrDeferred = new Deferred;
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->stdioDeferreds[] = $stderrDeferred;
|
|
|
|
$handle->stderr = new ProcessInputStream($stderrDeferred->promise());
|
2017-09-17 17:58:05 +02:00
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
$this->socketConnector->registerPendingProcess($handle);
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
return $handle;
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
public function join(ProcessHandle $handle): Promise {
|
2017-09-14 19:34:18 +02:00
|
|
|
/** @var Handle $handle */
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->exitCodeRequested = true;
|
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
if ($handle->exitCodeWatcher !== null) {
|
|
|
|
Loop::reference($handle->exitCodeWatcher);
|
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
return $handle->joinDeferred->promise();
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
public function kill(ProcessHandle $handle) {
|
2017-09-14 19:34:18 +02:00
|
|
|
/** @var Handle $handle */
|
2017-09-17 17:58:05 +02:00
|
|
|
// todo: send a signal to the wrapper to kill the child instead?
|
2017-09-14 19:34:18 +02:00
|
|
|
if (!\proc_terminate($handle->proc)) {
|
|
|
|
throw new ProcessException("Terminating process failed");
|
|
|
|
}
|
|
|
|
|
2017-09-19 20:37:11 +02:00
|
|
|
if ($handle->childPidWatcher !== null) {
|
|
|
|
Loop::cancel($handle->childPidWatcher);
|
|
|
|
$handle->childPidWatcher = null;
|
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
if ($handle->exitCodeWatcher !== null) {
|
|
|
|
Loop::cancel($handle->exitCodeWatcher);
|
|
|
|
$handle->exitCodeWatcher = null;
|
|
|
|
}
|
2017-09-14 19:34:18 +02:00
|
|
|
|
|
|
|
$handle->status = ProcessStatus::ENDED;
|
2017-09-17 17:58:05 +02:00
|
|
|
$handle->joinDeferred->fail(new ProcessException("The process was killed"));
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
public function signal(ProcessHandle $handle, int $signo) {
|
2017-09-14 19:34:18 +02:00
|
|
|
throw new ProcessException('Signals are not supported on Windows');
|
|
|
|
}
|
|
|
|
|
2017-09-17 17:58:05 +02:00
|
|
|
/** @inheritdoc */
|
|
|
|
public function destroy(ProcessHandle $handle) {
|
2017-09-14 19:34:18 +02:00
|
|
|
/** @var Handle $handle */
|
2017-09-17 19:07:13 +02:00
|
|
|
if ($handle->status < ProcessStatus::ENDED && \is_resource($handle->proc)) {
|
|
|
|
try {
|
|
|
|
$this->kill($handle);
|
|
|
|
} catch (ProcessException $e) {
|
|
|
|
// ignore
|
|
|
|
}
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-19 20:16:06 +02:00
|
|
|
if ($handle->childPidWatcher !== null) {
|
|
|
|
Loop::cancel($handle->childPidWatcher);
|
|
|
|
$handle->childPidWatcher = null;
|
|
|
|
}
|
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
if ($handle->exitCodeWatcher !== null) {
|
|
|
|
Loop::cancel($handle->exitCodeWatcher);
|
2017-09-17 19:07:13 +02:00
|
|
|
$handle->exitCodeWatcher = null;
|
2017-09-14 19:34:18 +02:00
|
|
|
}
|
|
|
|
|
2017-09-19 19:15:04 +02:00
|
|
|
$handle->stdin->close();
|
|
|
|
$handle->stdout->close();
|
|
|
|
$handle->stderr->close();
|
|
|
|
|
2017-09-14 19:34:18 +02:00
|
|
|
for ($i = 0; $i < 4; $i++) {
|
|
|
|
if (\is_resource($handle->sockets[$i] ?? null)) {
|
|
|
|
\fclose($handle->sockets[$i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-17 19:07:13 +02:00
|
|
|
@\stream_get_contents($handle->wrapperStderrPipe);
|
|
|
|
@\fclose($handle->wrapperStderrPipe);
|
2017-09-14 19:34:18 +02:00
|
|
|
|
|
|
|
if (\is_resource($handle->proc)) {
|
|
|
|
\proc_close($handle->proc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|