mirror of
https://github.com/danog/parallel.git
synced 2024-11-26 20:34:40 +01:00
Remove dependency on amphp/socket
This commit is contained in:
parent
046f7defb8
commit
fa8985d7e3
@ -2,9 +2,8 @@
|
|||||||
<?php declare(strict_types = 1);
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
use Amp\Parallel\{ ChannelException, SerializationException} ;
|
use Amp\Parallel\{ ChannelException, SerializationException} ;
|
||||||
use Amp\Parallel\Sync\{ ChannelledStream, Internal\ExitFailure, Internal\ExitSuccess };
|
use Amp\Parallel\Sync\{ ChannelledSocket, Internal\ExitFailure, Internal\ExitSuccess };
|
||||||
use Amp\Parallel\Worker\{ BasicEnvironment, Internal\TaskRunner };
|
use Amp\Parallel\Worker\{ BasicEnvironment, Internal\TaskRunner };
|
||||||
use Amp\Socket\Socket;
|
|
||||||
|
|
||||||
@cli_set_process_title("amp-worker");
|
@cli_set_process_title("amp-worker");
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
@ -38,7 +37,7 @@ ob_start(function ($data) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
Amp\execute(function () {
|
Amp\execute(function () {
|
||||||
$channel = new ChannelledStream(new Socket(STDIN, false), new Socket(STDOUT, false));
|
$channel = new ChannelledSocket(STDIN, STDOUT, false);
|
||||||
$environment = new BasicEnvironment;
|
$environment = new BasicEnvironment;
|
||||||
$runner = new TaskRunner($channel, $environment);
|
$runner = new TaskRunner($channel, $environment);
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"amphp/amp": "^2.0",
|
"amphp/amp": "^2.0"
|
||||||
"amphp/socket": "dev-amp_v2 as 0.2"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"amphp/loop": "dev-master",
|
"amphp/loop": "dev-master",
|
||||||
|
"amphp/stream": "dev-master as 0.1",
|
||||||
"phpunit/phpunit": "^5.0"
|
"phpunit/phpunit": "^5.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
|
@ -12,21 +12,17 @@ use Amp\Parallel\{
|
|||||||
Strand,
|
Strand,
|
||||||
SynchronizationError
|
SynchronizationError
|
||||||
};
|
};
|
||||||
use Amp\Parallel\Sync\{ Channel, ChannelledStream };
|
use Amp\Parallel\Sync\{ Channel, ChannelledSocket };
|
||||||
use Amp\Parallel\Sync\Internal\{ ExitFailure, ExitStatus, ExitSuccess };
|
use Amp\Parallel\Sync\Internal\{ ExitFailure, ExitStatus, ExitSuccess };
|
||||||
use Amp\Socket\Socket;
|
|
||||||
use Interop\Async\Awaitable;
|
use Interop\Async\Awaitable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a UNIX-compatible context using forked processes.
|
* Implements a UNIX-compatible context using forked processes.
|
||||||
*/
|
*/
|
||||||
class Fork implements Process, Strand {
|
class Fork implements Process, Strand {
|
||||||
/** @var \Amp\Parallel\Sync\Channel A channel for communicating with the child. */
|
/** @var \Amp\Parallel\Sync\ChannelledSocket A channel for communicating with the child. */
|
||||||
private $channel;
|
private $channel;
|
||||||
|
|
||||||
/** @var \Amp\Socket\Socket */
|
|
||||||
private $pipe;
|
|
||||||
|
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private $pid = 0;
|
private $pid = 0;
|
||||||
|
|
||||||
@ -73,7 +69,6 @@ class Fork implements Process, Strand {
|
|||||||
public function __clone() {
|
public function __clone() {
|
||||||
$this->pid = 0;
|
$this->pid = 0;
|
||||||
$this->oid = 0;
|
$this->oid = 0;
|
||||||
$this->pipe = null;
|
|
||||||
$this->channel = null;
|
$this->channel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,14 +147,23 @@ class Fork implements Process, Strand {
|
|||||||
* Starts the context execution.
|
* Starts the context execution.
|
||||||
*
|
*
|
||||||
* @throws \Amp\Parallel\ContextException If forking fails.
|
* @throws \Amp\Parallel\ContextException If forking fails.
|
||||||
* @throws \Amp\Socket\SocketException If creating a socket pair fails.
|
|
||||||
*/
|
*/
|
||||||
public function start() {
|
public function start() {
|
||||||
if (0 !== $this->oid) {
|
if (0 !== $this->oid) {
|
||||||
throw new StatusError('The context has already been started.');
|
throw new StatusError('The context has already been started.');
|
||||||
}
|
}
|
||||||
|
|
||||||
list($parent, $child) = \Amp\Socket\pair();
|
$sockets = @\stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
||||||
|
|
||||||
|
if ($sockets === false) {
|
||||||
|
$message = "Failed to create socket pair";
|
||||||
|
if ($error = \error_get_last()) {
|
||||||
|
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||||
|
}
|
||||||
|
throw new ContextException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
list($parent, $child) = $sockets;
|
||||||
|
|
||||||
switch ($pid = \pcntl_fork()) {
|
switch ($pid = \pcntl_fork()) {
|
||||||
case -1: // Failure
|
case -1: // Failure
|
||||||
@ -171,7 +175,7 @@ class Fork implements Process, Strand {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
\Amp\execute(function () use ($parent) {
|
\Amp\execute(function () use ($parent) {
|
||||||
$channel = new ChannelledStream(new Socket($parent));
|
$channel = new ChannelledSocket($parent, $parent);
|
||||||
return $this->execute($channel);
|
return $this->execute($channel);
|
||||||
});
|
});
|
||||||
$code = 0;
|
$code = 0;
|
||||||
@ -184,7 +188,7 @@ class Fork implements Process, Strand {
|
|||||||
default: // Parent
|
default: // Parent
|
||||||
$this->pid = $pid;
|
$this->pid = $pid;
|
||||||
$this->oid = \posix_getpid();
|
$this->oid = \posix_getpid();
|
||||||
$this->channel = new ChannelledStream($this->pipe = new Socket($child));
|
$this->channel = new ChannelledSocket($child, $child);
|
||||||
\fclose($parent);
|
\fclose($parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,8 +252,8 @@ class Fork implements Process, Strand {
|
|||||||
\posix_kill($this->pid, SIGKILL);
|
\posix_kill($this->pid, SIGKILL);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $this->pipe && $this->pipe->isReadable()) {
|
if ($this->channel !== null) {
|
||||||
$this->pipe->close();
|
$this->channel->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Detach" from the process and let it die asynchronously.
|
// "Detach" from the process and let it die asynchronously.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
namespace Amp\Parallel\Process;
|
namespace Amp\Parallel\Process;
|
||||||
|
|
||||||
use Amp\Parallel\{ Process as ProcessContext, StatusError, Strand, SynchronizationError };
|
use Amp\Parallel\{ Process as ProcessContext, StatusError, Strand, SynchronizationError };
|
||||||
use Amp\Parallel\Sync\{ ChannelledStream, Internal\ExitStatus };
|
use Amp\Parallel\Sync\{ ChannelledSocket, Internal\ExitStatus };
|
||||||
use Interop\Async\Awaitable;
|
use Interop\Async\Awaitable;
|
||||||
|
|
||||||
class ChannelledProcess implements ProcessContext, Strand {
|
class ChannelledProcess implements ProcessContext, Strand {
|
||||||
@ -36,7 +36,7 @@ class ChannelledProcess implements ProcessContext, Strand {
|
|||||||
*/
|
*/
|
||||||
public function start() {
|
public function start() {
|
||||||
$this->process->start();
|
$this->process->start();
|
||||||
$this->channel = new ChannelledStream($this->process->getStdOut(), $this->process->getStdIn());
|
$this->channel = new ChannelledSocket($this->process->getStdOut(), $this->process->getStdIn(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,8 +4,6 @@ namespace Amp\Parallel\Process;
|
|||||||
|
|
||||||
use Amp\Deferred;
|
use Amp\Deferred;
|
||||||
use Amp\Parallel\{ ContextException, Process as ProcessContext, StatusError };
|
use Amp\Parallel\{ ContextException, Process as ProcessContext, StatusError };
|
||||||
use Amp\Socket\Socket;
|
|
||||||
use Amp\Stream\Stream;
|
|
||||||
use Interop\Async\{ Awaitable, Loop };
|
use Interop\Async\{ Awaitable, Loop };
|
||||||
|
|
||||||
class Process implements ProcessContext {
|
class Process implements ProcessContext {
|
||||||
@ -24,13 +22,13 @@ class Process implements ProcessContext {
|
|||||||
/** @var array */
|
/** @var array */
|
||||||
private $options;
|
private $options;
|
||||||
|
|
||||||
/** @var \Amp\Stream\Stream|null */
|
/** @var resource|null */
|
||||||
private $stdin;
|
private $stdin;
|
||||||
|
|
||||||
/** @var \Amp\Stream\Stream|null */
|
/** @var resource|null */
|
||||||
private $stdout;
|
private $stdout;
|
||||||
|
|
||||||
/** @var \Amp\Stream\Stream|null */
|
/** @var resource|null */
|
||||||
private $stderr;
|
private $stderr;
|
||||||
|
|
||||||
/** @var int */
|
/** @var int */
|
||||||
@ -75,16 +73,16 @@ class Process implements ProcessContext {
|
|||||||
if (\getmypid() === $this->oid) {
|
if (\getmypid() === $this->oid) {
|
||||||
$this->kill(); // Will only terminate if the process is still running.
|
$this->kill(); // Will only terminate if the process is still running.
|
||||||
|
|
||||||
if ($this->stdin !== null) {
|
if (\is_resource($this->stdin)) {
|
||||||
$this->stdin->close();
|
\fclose($this->stdin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->stdout !== null) {
|
if (\is_resource($this->stdout)) {
|
||||||
$this->stdout->close();
|
\fclose($this->stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->stderr !== null) {
|
if (\is_resource($this->stderr)) {
|
||||||
$this->stderr->close();
|
\fclose($this->stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,9 +144,9 @@ class Process implements ProcessContext {
|
|||||||
|
|
||||||
$this->pid = $status["pid"];
|
$this->pid = $status["pid"];
|
||||||
|
|
||||||
$this->stdin = $stdin = new Socket($pipes[0]);
|
$this->stdin = $stdin = $pipes[0];
|
||||||
$this->stdout = new Socket($pipes[1]);
|
$this->stdout = $pipes[1];
|
||||||
$this->stderr = new Socket($pipes[2]);
|
$this->stderr = $pipes[2];
|
||||||
|
|
||||||
$stream = $pipes[3];
|
$stream = $pipes[3];
|
||||||
\stream_set_blocking($stream, false);
|
\stream_set_blocking($stream, false);
|
||||||
@ -175,7 +173,9 @@ class Process implements ProcessContext {
|
|||||||
\proc_close($process);
|
\proc_close($process);
|
||||||
$process = null;
|
$process = null;
|
||||||
}
|
}
|
||||||
$stdin->close();
|
if (\is_resource($stdin)) {
|
||||||
|
\fclose($stdin);
|
||||||
|
}
|
||||||
Loop::cancel($watcher);
|
Loop::cancel($watcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,14 +200,10 @@ class Process implements ProcessContext {
|
|||||||
|
|
||||||
Loop::enable($this->watcher);
|
Loop::enable($this->watcher);
|
||||||
|
|
||||||
$awaitable = $this->deferred->getAwaitable();
|
\fclose($this->stdout);
|
||||||
|
\fclose($this->stderr);
|
||||||
|
|
||||||
$awaitable->when(function () {
|
return $this->deferred->getAwaitable();
|
||||||
$this->stdout->close();
|
|
||||||
$this->stderr->close();
|
|
||||||
});
|
|
||||||
|
|
||||||
return $awaitable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -304,11 +300,11 @@ class Process implements ProcessContext {
|
|||||||
/**
|
/**
|
||||||
* Gets the process input stream (STDIN).
|
* Gets the process input stream (STDIN).
|
||||||
*
|
*
|
||||||
* @return \Amp\Stream\Stream
|
* @return resource
|
||||||
*
|
*
|
||||||
* @throws \Amp\Parallel\StatusError If the process is not running.
|
* @throws \Amp\Parallel\StatusError If the process is not running.
|
||||||
*/
|
*/
|
||||||
public function getStdIn(): Stream {
|
public function getStdIn() {
|
||||||
if ($this->stdin === null) {
|
if ($this->stdin === null) {
|
||||||
throw new StatusError("The process has not been started");
|
throw new StatusError("The process has not been started");
|
||||||
}
|
}
|
||||||
@ -319,11 +315,11 @@ class Process implements ProcessContext {
|
|||||||
/**
|
/**
|
||||||
* Gets the process output stream (STDOUT).
|
* Gets the process output stream (STDOUT).
|
||||||
*
|
*
|
||||||
* @return \Amp\Stream\Stream
|
* @return resource
|
||||||
*
|
*
|
||||||
* @throws \Amp\Parallel\StatusError If the process is not running.
|
* @throws \Amp\Parallel\StatusError If the process is not running.
|
||||||
*/
|
*/
|
||||||
public function getStdOut(): Stream {
|
public function getStdOut() {
|
||||||
if ($this->stdout === null) {
|
if ($this->stdout === null) {
|
||||||
throw new StatusError("The process has not been started");
|
throw new StatusError("The process has not been started");
|
||||||
}
|
}
|
||||||
@ -334,11 +330,11 @@ class Process implements ProcessContext {
|
|||||||
/**
|
/**
|
||||||
* Gets the process error stream (STDERR).
|
* Gets the process error stream (STDERR).
|
||||||
*
|
*
|
||||||
* @return \Amp\Stream\Stream
|
* @return resource
|
||||||
*
|
*
|
||||||
* @throws \Amp\Parallel\StatusError If the process is not running.
|
* @throws \Amp\Parallel\StatusError If the process is not running.
|
||||||
*/
|
*/
|
||||||
public function getStdErr(): Stream {
|
public function getStdErr() {
|
||||||
if ($this->stderr === null) {
|
if ($this->stderr === null) {
|
||||||
throw new StatusError("The process has not been started");
|
throw new StatusError("The process has not been started");
|
||||||
}
|
}
|
||||||
|
309
lib/Sync/ChannelledSocket.php
Normal file
309
lib/Sync/ChannelledSocket.php
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Amp\Parallel\Sync;
|
||||||
|
|
||||||
|
use Amp\{ Coroutine, Deferred, Failure, Success };
|
||||||
|
use Amp\Parallel\{ ChannelException, SerializationException };
|
||||||
|
use Interop\Async\{ Awaitable, Loop };
|
||||||
|
|
||||||
|
class ChannelledSocket implements Channel {
|
||||||
|
const HEADER_LENGTH = 5;
|
||||||
|
|
||||||
|
/** @var resource Stream resource. */
|
||||||
|
private $readResource;
|
||||||
|
|
||||||
|
/** @var resource Stream resource. */
|
||||||
|
private $writeResource;
|
||||||
|
|
||||||
|
/** @var string onReadable loop watcher. */
|
||||||
|
private $readWatcher;
|
||||||
|
|
||||||
|
/** @var string onWritable loop watcher. */
|
||||||
|
private $writeWatcher;
|
||||||
|
|
||||||
|
/** @var \SplQueue Queue of pending reads. */
|
||||||
|
private $reads;
|
||||||
|
|
||||||
|
/** @var \SplQueue Queue of pending writes. */
|
||||||
|
private $writes;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $open = true;
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $autoClose = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $resource Stream resource.
|
||||||
|
* @param bool $autoClose True to close the stream resource when this object is destroyed, false to leave open.
|
||||||
|
*
|
||||||
|
* @throws \Error If a stream resource is not given for $resource.
|
||||||
|
*/
|
||||||
|
public function __construct($read, $write, bool $autoClose = true) {
|
||||||
|
if (!\is_resource($read) ||\get_resource_type($read) !== 'stream') {
|
||||||
|
throw new \Error('Invalid resource given to constructor!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_resource($write) ||\get_resource_type($write) !== 'stream') {
|
||||||
|
throw new \Error('Invalid resource given to constructor!');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->readResource = $read;
|
||||||
|
$this->writeResource = $write;
|
||||||
|
$this->autoClose = $autoClose;
|
||||||
|
|
||||||
|
\stream_set_blocking($this->readResource, false);
|
||||||
|
\stream_set_read_buffer($this->readResource, 0);
|
||||||
|
\stream_set_write_buffer($this->readResource, 0);
|
||||||
|
|
||||||
|
if ($this->readResource !== $this->writeResource) {
|
||||||
|
\stream_set_blocking($this->writeResource, false);
|
||||||
|
\stream_set_read_buffer($this->writeResource, 0);
|
||||||
|
\stream_set_write_buffer($this->writeResource, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reads = $reads = new \SplQueue;
|
||||||
|
$this->writes = $writes = new \SplQueue;
|
||||||
|
|
||||||
|
$errorHandler = static function ($errno, $errstr) {
|
||||||
|
throw new ChannelException(\sprintf('Received corrupted data. Errno: %d; %s', $errno, $errstr));
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->readWatcher = Loop::onReadable($this->readResource, static function ($watcher, $stream) use ($reads, $errorHandler) {
|
||||||
|
while (!$reads->isEmpty()) {
|
||||||
|
/** @var \Amp\Deferred $deferred */
|
||||||
|
list($buffer, $length, $deferred) = $reads->shift();
|
||||||
|
|
||||||
|
if ($length === 0) {
|
||||||
|
// Error reporting suppressed since fread() produces a warning if the stream unexpectedly closes.
|
||||||
|
$data = @\fread($stream, self::HEADER_LENGTH - \strlen($buffer));
|
||||||
|
|
||||||
|
if ($data === false || ($data === '' && (\feof($stream) || !\is_resource($stream)))) {
|
||||||
|
$deferred->fail(new ChannelException("The socket unexpectedly closed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer .= $data;
|
||||||
|
|
||||||
|
if (\strlen($buffer) !== self::HEADER_LENGTH) {
|
||||||
|
// Not enough data available.
|
||||||
|
$reads->unshift([$buffer, 0, $deferred]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = \unpack("Cprefix/Llength", $data);
|
||||||
|
|
||||||
|
if ($data["prefix"] !== 0) {
|
||||||
|
$deferred->fail(new ChannelException("Invalid header received"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = $data["length"];
|
||||||
|
$buffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error reporting suppressed since fread() produces a warning if the stream unexpectedly closes.
|
||||||
|
$data = @\fread($stream, $length - \strlen($buffer));
|
||||||
|
|
||||||
|
if ($data === false || ($data === '' && (\feof($stream) || !\is_resource($stream)))) {
|
||||||
|
$deferred->fail(new ChannelException("The socket unexpectedly closed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer .= $data;
|
||||||
|
|
||||||
|
if (\strlen($buffer) < $length) {
|
||||||
|
// Not enough data available.
|
||||||
|
$reads->unshift([$buffer, $length, $deferred]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
\set_error_handler($errorHandler);
|
||||||
|
|
||||||
|
// Attempt to unserialize the received data.
|
||||||
|
try {
|
||||||
|
$data = \unserialize($buffer);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$deferred->fail(new SerializationException("Exception thrown when unserializing data", $exception));
|
||||||
|
continue;
|
||||||
|
} finally {
|
||||||
|
\restore_error_handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deferred->resolve($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loop::disable($watcher);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->writeWatcher = Loop::onWritable($this->writeResource, static function ($watcher, $stream) use ($writes) {
|
||||||
|
while (!$writes->isEmpty()) {
|
||||||
|
/** @var \Amp\Deferred $deferred */
|
||||||
|
list($data, $previous, $deferred) = $writes->shift();
|
||||||
|
$length = \strlen($data);
|
||||||
|
|
||||||
|
if ($length === 0) {
|
||||||
|
$deferred->resolve(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
|
||||||
|
$written = @\fwrite($stream, $data);
|
||||||
|
|
||||||
|
if ($written === false || $written === 0) {
|
||||||
|
$message = "Failed to write to socket";
|
||||||
|
if ($error = \error_get_last()) {
|
||||||
|
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||||
|
}
|
||||||
|
$deferred->fail(new ChannelException($message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($length <= $written) {
|
||||||
|
$deferred->resolve($written + $previous);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = \substr($data, $written);
|
||||||
|
$writes->unshift([$data, $written + $previous, $deferred]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Loop::disable($this->readWatcher);
|
||||||
|
Loop::disable($this->writeWatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
if ($this->readResource !== null) {
|
||||||
|
$this->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function close() {
|
||||||
|
if (\is_resource($this->readResource)) {
|
||||||
|
if ($this->autoClose) {
|
||||||
|
@\fclose($this->readResource);
|
||||||
|
|
||||||
|
if ($this->readResource !== $this->writeResource) {
|
||||||
|
@\fclose($this->writeResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->readResource = null;
|
||||||
|
$this->writeResource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->open = false;
|
||||||
|
|
||||||
|
if (!$this->reads->isEmpty()) {
|
||||||
|
$exception = new ChannelException("The connection was unexpectedly closed before reading completed");
|
||||||
|
do {
|
||||||
|
/** @var \Amp\Deferred $deferred */
|
||||||
|
list( , , $deferred) = $this->reads->shift();
|
||||||
|
$deferred->fail($exception);
|
||||||
|
} while (!$this->reads->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->writes->isEmpty()) {
|
||||||
|
$exception = new ChannelException("The connection was unexpectedly writing completed");
|
||||||
|
do {
|
||||||
|
/** @var \Amp\Deferred $deferred */
|
||||||
|
list( , , $deferred) = $this->writes->shift();
|
||||||
|
$deferred->fail($exception);
|
||||||
|
} while (!$this->writes->isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// defer this, else the Loop::disable() may be invalid
|
||||||
|
$read = $this->readWatcher;
|
||||||
|
$write = $this->writeWatcher;
|
||||||
|
Loop::defer(static function () use ($read, $write) {
|
||||||
|
Loop::cancel($read);
|
||||||
|
Loop::cancel($write);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function receive(): Awaitable {
|
||||||
|
if (!$this->open) {
|
||||||
|
return new Failure(new ChannelException("The channel is has been closed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$deferred = new Deferred;
|
||||||
|
$this->reads->push(["", 0, $deferred]);
|
||||||
|
|
||||||
|
Loop::enable($this->readWatcher);
|
||||||
|
|
||||||
|
return $deferred->getAwaitable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $data
|
||||||
|
* @param bool $end
|
||||||
|
*
|
||||||
|
* @return \Interop\Async\Awaitable
|
||||||
|
*/
|
||||||
|
public function send($data): Awaitable {
|
||||||
|
if (!$this->open) {
|
||||||
|
return new Failure(new ChannelException("The channel is has been closed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the data to send into the channel.
|
||||||
|
try {
|
||||||
|
$data = \serialize($data);
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
throw new SerializationException(
|
||||||
|
"The given data cannot be sent because it is not serializable.", $exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = \strlen($data);
|
||||||
|
$data = \pack("CL", 0, $length) . $data;
|
||||||
|
$written = 0;
|
||||||
|
|
||||||
|
if ($this->writes->isEmpty()) {
|
||||||
|
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
|
||||||
|
$written = @\fwrite($this->writeResource, $data);
|
||||||
|
|
||||||
|
if ($written === false) {
|
||||||
|
$message = "Failed to write to stream";
|
||||||
|
if ($error = \error_get_last()) {
|
||||||
|
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||||
|
}
|
||||||
|
return new Failure(new ChannelException($message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($length <= $written) {
|
||||||
|
return new Success($written);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = \substr($data, $written);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Coroutine($this->doSend($data, $written));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doSend(string $data, int $written): \Generator {
|
||||||
|
$deferred = new Deferred;
|
||||||
|
$this->writes->push([$data, $written, $deferred]);
|
||||||
|
|
||||||
|
Loop::enable($this->writeWatcher);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$written = yield $deferred->getAwaitable();
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
$this->close();
|
||||||
|
throw $exception;
|
||||||
|
} finally {
|
||||||
|
if ($this->writes->isEmpty()) {
|
||||||
|
Loop::disable($this->writeWatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $written;
|
||||||
|
}
|
||||||
|
}
|
@ -4,8 +4,7 @@ namespace Amp\Parallel\Threading\Internal;
|
|||||||
|
|
||||||
use Amp\Coroutine;
|
use Amp\Coroutine;
|
||||||
use Amp\Parallel\{ ChannelException, SerializationException };
|
use Amp\Parallel\{ ChannelException, SerializationException };
|
||||||
use Amp\Parallel\Sync\{ Channel, ChannelledStream, Internal\ExitFailure, Internal\ExitSuccess };
|
use Amp\Parallel\Sync\{ Channel, ChannelledSocket, Internal\ExitFailure, Internal\ExitSuccess };
|
||||||
use Amp\Socket\Socket;
|
|
||||||
use Interop\Async\Awaitable;
|
use Interop\Async\Awaitable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,7 +75,7 @@ class Thread extends \Thread {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
\Amp\execute(function () {
|
\Amp\execute(function () {
|
||||||
$channel = new ChannelledStream(new Socket($this->socket, false));
|
$channel = new ChannelledSocket($this->socket, $this->socket, false);
|
||||||
|
|
||||||
$watcher = \Amp\repeat(self::KILL_CHECK_FREQUENCY, function () {
|
$watcher = \Amp\repeat(self::KILL_CHECK_FREQUENCY, function () {
|
||||||
if ($this->killed) {
|
if ($this->killed) {
|
||||||
|
@ -4,8 +4,7 @@ namespace Amp\Parallel\Threading;
|
|||||||
|
|
||||||
use Amp\Coroutine;
|
use Amp\Coroutine;
|
||||||
use Amp\Parallel\{ ContextException, StatusError, SynchronizationError, Strand };
|
use Amp\Parallel\{ ContextException, StatusError, SynchronizationError, Strand };
|
||||||
use Amp\Parallel\Sync\{ ChannelledStream, Internal\ExitStatus };
|
use Amp\Parallel\Sync\{ ChannelledSocket, Internal\ExitStatus };
|
||||||
use Amp\Socket\Socket;
|
|
||||||
use Interop\Async\Awaitable;
|
use Interop\Async\Awaitable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,12 +18,9 @@ class Thread implements Strand {
|
|||||||
/** @var Internal\Thread An internal thread instance. */
|
/** @var Internal\Thread An internal thread instance. */
|
||||||
private $thread;
|
private $thread;
|
||||||
|
|
||||||
/** @var \Amp\Parallel\Sync\Channel A channel for communicating with the thread. */
|
/** @var \Amp\Parallel\Sync\ChannelledSocket A channel for communicating with the thread. */
|
||||||
private $channel;
|
private $channel;
|
||||||
|
|
||||||
/** @var \Amp\Socket\Socket */
|
|
||||||
private $pipe;
|
|
||||||
|
|
||||||
/** @var resource */
|
/** @var resource */
|
||||||
private $socket;
|
private $socket;
|
||||||
|
|
||||||
@ -82,7 +78,6 @@ class Thread implements Strand {
|
|||||||
public function __clone() {
|
public function __clone() {
|
||||||
$this->thread = null;
|
$this->thread = null;
|
||||||
$this->socket = null;
|
$this->socket = null;
|
||||||
$this->pipe = null;
|
|
||||||
$this->channel = null;
|
$this->channel = null;
|
||||||
$this->oid = 0;
|
$this->oid = 0;
|
||||||
}
|
}
|
||||||
@ -104,7 +99,7 @@ class Thread implements Strand {
|
|||||||
* @return bool True if the context is running, otherwise false.
|
* @return bool True if the context is running, otherwise false.
|
||||||
*/
|
*/
|
||||||
public function isRunning(): bool {
|
public function isRunning(): bool {
|
||||||
return null !== $this->pipe && $this->pipe->isReadable();
|
return $this->channel !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,7 +115,21 @@ class Thread implements Strand {
|
|||||||
|
|
||||||
$this->oid = \getmypid();
|
$this->oid = \getmypid();
|
||||||
|
|
||||||
list($channel, $this->socket) = \Amp\Socket\pair();
|
$sockets = @\stream_socket_pair(
|
||||||
|
\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX,
|
||||||
|
STREAM_SOCK_STREAM,
|
||||||
|
STREAM_IPPROTO_IP
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($sockets === false) {
|
||||||
|
$message = "Failed to create socket pair";
|
||||||
|
if ($error = \error_get_last()) {
|
||||||
|
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||||
|
}
|
||||||
|
throw new ContextException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
list($channel, $this->socket) = $sockets;
|
||||||
|
|
||||||
$this->thread = new Internal\Thread($this->socket, $this->function, $this->args);
|
$this->thread = new Internal\Thread($this->socket, $this->function, $this->args);
|
||||||
|
|
||||||
@ -128,7 +137,7 @@ class Thread implements Strand {
|
|||||||
throw new ContextException('Failed to start the thread.');
|
throw new ContextException('Failed to start the thread.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->channel = new ChannelledStream($this->pipe = new Socket($channel));
|
$this->channel = new ChannelledSocket($channel, $channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,8 +161,8 @@ class Thread implements Strand {
|
|||||||
* Closes channel and socket if still open.
|
* Closes channel and socket if still open.
|
||||||
*/
|
*/
|
||||||
private function close() {
|
private function close() {
|
||||||
if ($this->pipe !== null && $this->pipe->isReadable()) {
|
if ($this->channel !== null) {
|
||||||
$this->pipe->close();
|
$this->channel->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\is_resource($this->socket)) {
|
if (\is_resource($this->socket)) {
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Amp\Parallel\Test\Sync;
|
namespace Amp\Parallel\Test\Sync;
|
||||||
|
|
||||||
use Amp\Parallel\Sync\Lock;
|
|
||||||
use Amp\Parallel\Test\TestCase;
|
use Amp\Parallel\Test\TestCase;
|
||||||
use Amp\Pause;
|
use Amp\Pause;
|
||||||
|
|
||||||
|
121
test/Sync/ChannelledSocketTest.php
Normal file
121
test/Sync/ChannelledSocketTest.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Amp\Parallel\Test\Sync;
|
||||||
|
|
||||||
|
use Amp\Parallel\Sync\ChannelledSocket;
|
||||||
|
use Amp\Parallel\Test\TestCase;
|
||||||
|
|
||||||
|
class ChannelledSocketTest extends TestCase {
|
||||||
|
/**
|
||||||
|
* @return resource[]
|
||||||
|
*/
|
||||||
|
protected function createSockets() {
|
||||||
|
if (($sockets = @\stream_socket_pair(\stripos(PHP_OS, "win") === 0 ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP)) === false) {
|
||||||
|
$message = "Failed to create socket pair";
|
||||||
|
if ($error = \error_get_last()) {
|
||||||
|
$message .= \sprintf(" Errno: %d; %s", $error["type"], $error["message"]);
|
||||||
|
}
|
||||||
|
$this->fail($message);
|
||||||
|
}
|
||||||
|
return $sockets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendReceive() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$b = new ChannelledSocket($right, $right);
|
||||||
|
|
||||||
|
$message = 'hello';
|
||||||
|
|
||||||
|
yield $a->send($message);
|
||||||
|
$data = yield $b->receive();
|
||||||
|
$this->assertSame($message, $data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testSendReceive
|
||||||
|
*/
|
||||||
|
public function testSendReceiveLongData() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$b = new ChannelledSocket($right, $right);
|
||||||
|
|
||||||
|
$length = 0xffff;
|
||||||
|
$message = '';
|
||||||
|
for ($i = 0; $i < $length; ++$i) {
|
||||||
|
$message .= chr(mt_rand(0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
$a->send($message);
|
||||||
|
$data = yield $b->receive();
|
||||||
|
$this->assertSame($message, $data);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testSendReceive
|
||||||
|
* @expectedException \Amp\Parallel\ChannelException
|
||||||
|
*/
|
||||||
|
public function testInvalidDataReceived() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$b = new ChannelledSocket($right, $right);
|
||||||
|
|
||||||
|
fwrite($left, pack('L', 10) . '1234567890');
|
||||||
|
$data = yield $b->receive();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testSendReceive
|
||||||
|
* @expectedException \Amp\Parallel\ChannelException
|
||||||
|
*/
|
||||||
|
public function testSendUnserializableData() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$b = new ChannelledSocket($right, $right);
|
||||||
|
|
||||||
|
// Close $a. $b should close on next read...
|
||||||
|
yield $a->send(function () {});
|
||||||
|
$data = yield $b->receive();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testSendReceive
|
||||||
|
* @expectedException \Amp\Parallel\ChannelException
|
||||||
|
*/
|
||||||
|
public function testSendAfterClose() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$a->close();
|
||||||
|
|
||||||
|
yield $a->send('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @depends testSendReceive
|
||||||
|
* @expectedException \Amp\Parallel\ChannelException
|
||||||
|
*/
|
||||||
|
public function testReceiveAfterClose() {
|
||||||
|
\Amp\execute(function () {
|
||||||
|
list($left, $right) = $this->createSockets();
|
||||||
|
$a = new ChannelledSocket($left, $left);
|
||||||
|
$a->close();
|
||||||
|
|
||||||
|
$data = yield $a->receive();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@
|
|||||||
namespace Amp\Parallel\Test\Sync;
|
namespace Amp\Parallel\Test\Sync;
|
||||||
|
|
||||||
use Amp\Parallel\Forking\Fork;
|
use Amp\Parallel\Forking\Fork;
|
||||||
use Amp\Parallel\Sync\{PosixSemaphore, Semaphore};
|
use Amp\Parallel\Sync\{ PosixSemaphore, Semaphore };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group posix
|
* @group posix
|
||||||
|
Loading…
Reference in New Issue
Block a user