2018-10-06 17:05:49 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\Parallel\Context\Internal;
|
|
|
|
|
|
|
|
use Amp\Deferred;
|
|
|
|
use Amp\Loop;
|
2018-10-09 16:12:23 +02:00
|
|
|
use Amp\Parallel\Context\ContextException;
|
2018-10-06 17:05:49 +02:00
|
|
|
use Amp\Parallel\Sync\ChannelledSocket;
|
|
|
|
use Amp\Promise;
|
2018-10-09 16:12:23 +02:00
|
|
|
use Amp\TimeoutException;
|
2018-10-06 17:05:49 +02:00
|
|
|
use function Amp\call;
|
|
|
|
|
2019-04-29 17:19:32 +02:00
|
|
|
class ProcessHub
|
2018-10-07 16:50:45 +02:00
|
|
|
{
|
2018-10-09 16:12:23 +02:00
|
|
|
const PROCESS_START_TIMEOUT = 5000;
|
2018-10-11 01:31:34 +02:00
|
|
|
const KEY_RECEIVE_TIMEOUT = 1000;
|
2018-10-09 16:12:23 +02:00
|
|
|
|
2018-10-06 17:05:49 +02:00
|
|
|
/** @var resource|null */
|
|
|
|
private $server;
|
|
|
|
|
|
|
|
/** @var string|null */
|
|
|
|
private $uri;
|
|
|
|
|
2018-10-11 18:14:14 +02:00
|
|
|
/** @var int[] */
|
2018-10-11 01:31:34 +02:00
|
|
|
private $keys;
|
|
|
|
|
2018-10-06 17:05:49 +02:00
|
|
|
/** @var string|null */
|
|
|
|
private $watcher;
|
|
|
|
|
2018-10-11 01:31:34 +02:00
|
|
|
/** @var Deferred[] */
|
|
|
|
private $acceptor = [];
|
2018-10-06 17:05:49 +02:00
|
|
|
|
2018-10-07 16:50:45 +02:00
|
|
|
public function __construct()
|
|
|
|
{
|
2018-10-09 16:12:23 +02:00
|
|
|
$isWindows = \strncasecmp(\PHP_OS, "WIN", 3) === 0;
|
|
|
|
|
|
|
|
if ($isWindows) {
|
2018-10-21 17:09:28 +02:00
|
|
|
$this->uri = "tcp://127.0.0.1:0";
|
2018-10-09 16:12:23 +02:00
|
|
|
} else {
|
|
|
|
$this->uri = "unix://" . \tempnam(\sys_get_temp_dir(), "amp-parallel-ipc-") . ".sock";
|
|
|
|
}
|
|
|
|
|
2018-10-06 17:05:49 +02:00
|
|
|
$this->server = \stream_socket_server($this->uri, $errno, $errstr, \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN);
|
|
|
|
|
2018-10-09 16:12:23 +02:00
|
|
|
if (!$this->server) {
|
|
|
|
throw new \RuntimeException(\sprintf("Could not create IPC server: (Errno: %d) %s", $errno, $errstr));
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($isWindows) {
|
|
|
|
$name = \stream_socket_get_name($this->server, false);
|
|
|
|
$port = \substr($name, \strrpos($name, ":") + 1);
|
2018-10-21 17:09:28 +02:00
|
|
|
$this->uri = "tcp://127.0.0.1:" . $port;
|
2018-10-09 16:12:23 +02:00
|
|
|
}
|
|
|
|
|
2018-10-11 01:31:34 +02:00
|
|
|
$keys = &$this->keys;
|
2018-10-06 17:05:49 +02:00
|
|
|
$acceptor = &$this->acceptor;
|
2018-10-11 01:31:34 +02:00
|
|
|
$this->watcher = Loop::onReadable($this->server, static function (string $watcher, $server) use (&$keys, &$acceptor) {
|
2018-10-06 17:05:49 +02:00
|
|
|
// Error reporting suppressed since stream_socket_accept() emits E_WARNING on client accept failure.
|
|
|
|
if (!$client = @\stream_socket_accept($server, 0)) { // Timeout of 0 to be non-blocking.
|
|
|
|
return; // Accepting client failed.
|
|
|
|
}
|
|
|
|
|
2018-10-11 01:31:34 +02:00
|
|
|
$channel = new ChannelledSocket($client, $client);
|
|
|
|
|
|
|
|
try {
|
|
|
|
$received = yield Promise\timeout($channel->receive(), self::KEY_RECEIVE_TIMEOUT);
|
2019-02-19 01:44:30 +01:00
|
|
|
} catch (\Throwable $exception) {
|
2018-10-24 05:14:51 +02:00
|
|
|
$channel->close();
|
2018-10-11 01:31:34 +02:00
|
|
|
return; // Ignore possible foreign connection attempt.
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!\is_string($received) || !isset($keys[$received])) {
|
2018-10-24 05:14:51 +02:00
|
|
|
$channel->close();
|
2018-10-11 01:31:34 +02:00
|
|
|
return; // Ignore possible foreign connection attempt.
|
|
|
|
}
|
|
|
|
|
|
|
|
$pid = $keys[$received];
|
|
|
|
|
|
|
|
$deferred = $acceptor[$pid];
|
|
|
|
unset($acceptor[$pid], $keys[$received]);
|
|
|
|
$deferred->resolve($channel);
|
2018-10-06 17:05:49 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
Loop::disable($this->watcher);
|
|
|
|
}
|
|
|
|
|
2018-10-07 16:50:45 +02:00
|
|
|
public function __destruct()
|
|
|
|
{
|
2018-10-06 17:05:49 +02:00
|
|
|
Loop::cancel($this->watcher);
|
|
|
|
\fclose($this->server);
|
|
|
|
}
|
|
|
|
|
2018-10-07 16:50:45 +02:00
|
|
|
public function getUri(): string
|
|
|
|
{
|
2018-10-06 17:05:49 +02:00
|
|
|
return $this->uri;
|
|
|
|
}
|
|
|
|
|
2018-10-11 01:31:34 +02:00
|
|
|
public function generateKey(int $pid, int $length): string
|
2018-10-07 16:50:45 +02:00
|
|
|
{
|
2018-10-11 01:31:34 +02:00
|
|
|
$key = \random_bytes($length);
|
|
|
|
$this->keys[$key] = $pid;
|
|
|
|
return $key;
|
|
|
|
}
|
2018-10-06 17:05:49 +02:00
|
|
|
|
2018-10-11 01:31:34 +02:00
|
|
|
public function accept(int $pid): Promise
|
|
|
|
{
|
|
|
|
return call(function () use ($pid) {
|
|
|
|
$this->acceptor[$pid] = new Deferred;
|
2018-10-06 17:05:49 +02:00
|
|
|
|
|
|
|
Loop::enable($this->watcher);
|
|
|
|
|
2018-10-09 16:12:23 +02:00
|
|
|
try {
|
2018-10-24 05:14:51 +02:00
|
|
|
$channel = yield Promise\timeout($this->acceptor[$pid]->promise(), self::PROCESS_START_TIMEOUT);
|
2018-10-09 16:12:23 +02:00
|
|
|
} catch (TimeoutException $exception) {
|
2018-10-11 18:14:14 +02:00
|
|
|
$key = \array_search($pid, $this->keys, true);
|
2018-10-24 05:14:51 +02:00
|
|
|
\assert(\is_string($key), "Key for {$pid} not found");
|
2018-10-11 18:14:14 +02:00
|
|
|
unset($this->acceptor[$pid], $this->keys[$key]);
|
2018-10-24 05:14:51 +02:00
|
|
|
throw new ContextException("Starting the process timed out", 0, $exception);
|
|
|
|
} finally {
|
2018-10-11 01:31:34 +02:00
|
|
|
if (empty($this->acceptor)) {
|
|
|
|
Loop::disable($this->watcher);
|
|
|
|
}
|
2018-10-09 16:12:23 +02:00
|
|
|
}
|
2018-10-24 05:14:51 +02:00
|
|
|
|
|
|
|
return $channel;
|
2018-10-06 17:05:49 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|