2017-04-30 08:31:53 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Amp\ByteStream;
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
use Amp\Deferred;
|
|
|
|
use Amp\Failure;
|
2017-04-30 08:31:53 +02:00
|
|
|
use Amp\Loop;
|
|
|
|
use Amp\Promise;
|
|
|
|
|
|
|
|
class ResourceInputStream implements InputStream {
|
2017-05-04 23:30:40 +02:00
|
|
|
const DEFAULT_CHUNK_SIZE = 8192;
|
|
|
|
|
2017-04-30 08:31:53 +02:00
|
|
|
/** @var resource */
|
|
|
|
private $resource;
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $watcher;
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
/** @var \Amp\Deferred|null */
|
|
|
|
private $deferred;
|
2017-04-30 08:31:53 +02:00
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
/** @var bool */
|
|
|
|
private $readable = true;
|
2017-04-30 08:31:53 +02:00
|
|
|
|
|
|
|
/** @var bool */
|
|
|
|
private $autoClose = true;
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
public function __construct($stream, int $chunkSize = self::DEFAULT_CHUNK_SIZE, $autoClose = true) {
|
2017-04-30 08:31:53 +02:00
|
|
|
if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
|
|
|
|
throw new \Error("Expected a valid stream");
|
|
|
|
}
|
|
|
|
|
|
|
|
$meta = \stream_get_meta_data($stream);
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
if (isset($meta["mode"]) && $meta["mode"] !== ""
|
|
|
|
&& strpos($meta["mode"], "r") === false
|
|
|
|
&& strpos($meta["mode"], "+") === false
|
|
|
|
) {
|
2017-04-30 08:31:53 +02:00
|
|
|
throw new \Error("Expected a readable stream");
|
|
|
|
}
|
|
|
|
|
|
|
|
\stream_set_blocking($stream, false);
|
|
|
|
\stream_set_read_buffer($stream, 0);
|
|
|
|
|
|
|
|
$this->resource = $stream;
|
|
|
|
$this->autoClose = $autoClose;
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
$deferred = &$this->deferred;
|
|
|
|
$readable = &$this->readable;
|
|
|
|
|
|
|
|
$this->watcher = Loop::onReadable($this->resource, static function ($watcher, $stream) use (
|
|
|
|
&$deferred, &$readable, $chunkSize
|
|
|
|
) {
|
|
|
|
if ($deferred === null) {
|
|
|
|
return;
|
|
|
|
}
|
2017-04-30 08:31:53 +02:00
|
|
|
|
|
|
|
// Error reporting suppressed since fread() produces a warning if the stream unexpectedly closes.
|
|
|
|
$data = @\fread($stream, $chunkSize);
|
|
|
|
|
|
|
|
if ($data === false || ($data === '' && (\feof($stream) || !\is_resource($stream)))) {
|
2017-05-04 23:30:40 +02:00
|
|
|
$readable = false;
|
2017-04-30 08:31:53 +02:00
|
|
|
Loop::cancel($watcher);
|
2017-05-04 23:30:40 +02:00
|
|
|
$data = null; // Stream closed, resolve read with null.
|
2017-04-30 08:31:53 +02:00
|
|
|
}
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
$temp = $deferred;
|
|
|
|
$deferred = null;
|
|
|
|
$temp->resolve($data);
|
2017-04-30 08:31:53 +02:00
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
if ($deferred === null) { // Only disable watcher if no further read was requested.
|
|
|
|
Loop::disable($watcher);
|
|
|
|
}
|
2017-04-30 08:31:53 +02:00
|
|
|
});
|
2017-05-04 23:30:40 +02:00
|
|
|
|
|
|
|
Loop::disable($this->watcher);
|
2017-04-30 08:31:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reads data from the stream.
|
|
|
|
*
|
|
|
|
* @return Promise Resolves with a string when new data is available or `null` if the stream has closed.
|
|
|
|
*
|
|
|
|
* @throws PendingReadException Thrown if another read operation is still pending.
|
|
|
|
*/
|
|
|
|
public function read(): Promise {
|
2017-05-04 23:30:40 +02:00
|
|
|
if ($this->deferred !== null) {
|
2017-04-30 08:31:53 +02:00
|
|
|
throw new PendingReadException;
|
|
|
|
}
|
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
if (!$this->readable) {
|
|
|
|
return new Failure(new ClosedException("The stream has been closed"));
|
|
|
|
}
|
2017-04-30 08:31:53 +02:00
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
$this->deferred = new Deferred;
|
|
|
|
Loop::enable($this->watcher);
|
2017-04-30 08:31:53 +02:00
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
return $this->deferred->promise();
|
2017-04-30 08:31:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the stream forcefully. Multiple `close()` calls are ignored.
|
|
|
|
*
|
|
|
|
* Note: If a class implements `InputStream` and `OutputStream`, `close()` will close both streams at once. If you
|
|
|
|
* want to allow half-closed duplex streams, you must use different objects for input and output.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function close() {
|
|
|
|
if ($this->resource === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this->autoClose && \is_resource($this->resource)) {
|
|
|
|
@\fclose($this->resource);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->resource = null;
|
2017-05-04 23:30:40 +02:00
|
|
|
$this->readable = false;
|
2017-04-30 08:31:53 +02:00
|
|
|
|
2017-05-04 23:30:40 +02:00
|
|
|
if ($this->deferred !== null) {
|
|
|
|
$deferred = $this->deferred;
|
|
|
|
$this->deferred = null;
|
|
|
|
$deferred->resolve(null);
|
2017-04-30 08:31:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Loop::cancel($this->watcher);
|
|
|
|
}
|
|
|
|
|
2017-05-05 16:45:53 +02:00
|
|
|
public function getResource() {
|
|
|
|
return $this->resource;
|
|
|
|
}
|
|
|
|
|
2017-04-30 08:31:53 +02:00
|
|
|
public function __destruct() {
|
|
|
|
if ($this->autoClose) {
|
|
|
|
$this->close();
|
|
|
|
}
|
|
|
|
}
|
2017-05-07 22:14:45 +02:00
|
|
|
}
|