Skip to content

Commit 25eeda7

Browse files
committed
feat(io): introduce ReadHandle::reachedEndOfDataSource
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent b66e921 commit 25eeda7

19 files changed

+258
-118
lines changed

examples/channel/bounded.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@
3030

3131
for ($i = 0; $i < 10; $i++) {
3232
$file = File\open_read_only(__FILE__);
33-
$reader = new IO\Reader($file);
34-
while (!$reader->isEndOfFile()) {
35-
$byte = $reader->readByte();
36-
33+
while ($byte = $file->readAll(1)) {
3734
$sender->send($byte);
3835
}
36+
37+
$file->close();
3938
}
4039

4140
IO\write_error_line("[ sender ]: completed.");

src/Psl/File/ReadHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ public function __construct(string $file)
3939
parent::__construct($this->readHandle);
4040
}
4141

42+
/**
43+
* {@inheritDoc}
44+
*/
45+
public function reachedEndOfDataSource(): bool
46+
{
47+
return $this->readHandle->reachedEndOfDataSource();
48+
}
49+
4250
/**
4351
* {@inheritDoc}
4452
*/

src/Psl/File/ReadWriteHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public function __construct(string $file, WriteMode $write_mode = WriteMode::Ope
6767
parent::__construct($this->readWriteHandle);
6868
}
6969

70+
/**
71+
* {@inheritDoc}
72+
*/
73+
public function reachedEndOfDataSource(): bool
74+
{
75+
return $this->readWriteHandle->reachedEndOfDataSource();
76+
}
77+
7078
/**
7179
* {@inheritDoc}
7280
*/

src/Psl/IO/CloseReadStreamHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public function __construct(mixed $stream)
2323
$this->handle = new Internal\ResourceHandle($stream, read: true, write: false, seek: false, close: true);
2424
}
2525

26+
/**
27+
* {@inheritDoc}
28+
*/
29+
public function reachedEndOfDataSource(): bool
30+
{
31+
return $this->handle->reachedEndOfDataSource();
32+
}
33+
2634
/**
2735
* {@inheritDoc}
2836
*/

src/Psl/IO/CloseReadWriteStreamHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public function __construct(mixed $stream)
2424
$this->handle = new Internal\ResourceHandle($stream, read: true, write: true, seek: false, close: true);
2525
}
2626

27+
/**
28+
* {@inheritDoc}
29+
*/
30+
public function reachedEndOfDataSource(): bool
31+
{
32+
return $this->handle->reachedEndOfDataSource();
33+
}
34+
2735
/**
2836
* {@inheritDoc}
2937
*/

src/Psl/IO/CloseSeekReadStreamHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ public function __construct(mixed $stream)
2323
$this->handle = new Internal\ResourceHandle($stream, read: true, write: false, seek: true, close: true);
2424
}
2525

26+
/**
27+
* {@inheritDoc}
28+
*/
29+
public function reachedEndOfDataSource(): bool
30+
{
31+
return $this->handle->reachedEndOfDataSource();
32+
}
33+
2634
/**
2735
* {@inheritDoc}
2836
*/

src/Psl/IO/CloseSeekReadWriteStreamHandle.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public function __construct(mixed $stream)
2424
$this->handle = new Internal\ResourceHandle($stream, read: true, write: true, seek: true, close: true);
2525
}
2626

27+
/**
28+
* {@inheritDoc}
29+
*/
30+
public function reachedEndOfDataSource(): bool
31+
{
32+
return $this->handle->reachedEndOfDataSource();
33+
}
34+
2735
/**
2836
* {@inheritDoc}
2937
*/

src/Psl/IO/Internal/ResourceHandle.php

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414

1515
use function error_get_last;
1616
use function fclose;
17+
use function feof;
18+
use function fread;
1719
use function fseek;
1820
use function ftell;
1921
use function fwrite;
2022
use function is_resource;
2123
use function max;
2224
use function str_contains;
25+
use function stream_get_contents;
2326
use function stream_get_meta_data;
2427
use function stream_set_blocking;
2528
use function stream_set_read_buffer;
29+
use function stream_set_write_buffer;
2630
use function substr;
2731

2832
/**
@@ -46,54 +50,57 @@ class ResourceHandle implements IO\CloseSeekReadWriteStreamHandleInterface
4650
*/
4751
protected mixed $stream;
4852

49-
/**
50-
* @var null|Async\Sequence<array{null|int<1, max>, null|float}, string>
51-
*/
52-
private ?Async\Sequence $readSequence = null;
53-
54-
private ?Suspension $readSuspension = null;
55-
56-
/**
57-
* @var string
58-
*/
59-
private string $readWatcher = 'invalid';
60-
6153
/**
6254
* @var null|Async\Sequence<array{string, null|float}, int<0, max>>
6355
*/
6456
private ?Async\Sequence $writeSequence = null;
65-
6657
private ?Suspension $writeSuspension = null;
58+
private string $writeWatcher = 'invalid';
6759

6860
/**
69-
* @var string
61+
* @var null|Async\Sequence<array{null|int<1, max>, null|float}, string>
7062
*/
71-
private string $writeWatcher = 'invalid';
63+
private ?Async\Sequence $readSequence = null;
64+
private ?Suspension $readSuspension = null;
65+
private string $readWatcher = 'invalid';
66+
67+
private bool $useSingleRead = false;
68+
private bool $reachedEof = false;
7269

7370
/**
7471
* @param resource $stream
7572
*/
76-
public function __construct(mixed $stream, bool $read, bool $write, bool $seek, private bool $close)
73+
public function __construct(mixed $stream, bool $read, bool $write, bool $seek, private readonly bool $close)
7774
{
7875
/** @psalm-suppress RedundantConditionGivenDocblockType - The stream is always a resource, but we want to make sure it is a stream resource. */
7976
$this->stream = Type\resource('stream')->assert($stream);
8077

81-
/** @psalm-suppress UnusedFunctionCall */
82-
stream_set_read_buffer($stream, 0);
8378
stream_set_blocking($stream, false);
8479

8580
$meta = stream_get_meta_data($stream);
81+
if ($read) {
82+
$this->useSingleRead = ($meta["stream_type"] === "udp_socket" || $meta["stream_type"] === "STDIO");
83+
}
84+
8685
$blocks = $meta['blocked'] || ($meta['wrapper_type'] ?? '') === 'plainfile';
8786
if ($seek) {
88-
Psl\invariant($meta['seekable'], 'Handle is not seekable.');
87+
$seekable = $meta['seekable'];
88+
89+
Psl\invariant($seekable, 'Handle is not seekable.');
8990
}
9091

9192
if ($read) {
92-
Psl\invariant(str_contains($meta['mode'], 'r') || str_contains($meta['mode'], '+'), 'Handle is not readable.');
93+
$readable = str_contains($meta['mode'], 'r') || str_contains($meta['mode'], '+');
94+
95+
Psl\invariant($readable, 'Handle is not readable.');
96+
97+
/** @psalm-suppress UnusedFunctionCall */
98+
stream_set_read_buffer($stream, 0);
9399

94100
$this->readWatcher = EventLoop::onReadable($this->stream, function () {
95-
$this->readSuspension?->resume(null);
101+
$this->readSuspension?->resume();
96102
});
103+
97104
$this->readSequence = new Async\Sequence(
98105
/**
99106
* @param array{null|int<1, max>, null|float} $input
@@ -142,11 +149,14 @@ function (array $input) use ($blocks): string {
142149
|| str_contains($meta['mode'], 'a')
143150
|| str_contains($meta['mode'], '+');
144151

145-
Psl\invariant($writable, 'Handle is not writeable.');
152+
Psl\invariant($writable, 'Handle is not writeable.');
153+
154+
stream_set_write_buffer($stream, 0);
146155

147-
$this->writeWatcher = EventLoop::onReadable($this->stream, function () {
148-
$this->writeSuspension?->resume(null);
156+
$this->writeWatcher = EventLoop::onWritable($this->stream, function () {
157+
$this->writeSuspension?->resume();
149158
});
159+
150160
$this->writeSequence = new Async\Sequence(
151161
/**
152162
* @param array{string, null|float} $input
@@ -254,6 +264,22 @@ public function tell(): int
254264
return max($result, 0);
255265
}
256266

267+
/**
268+
* {@inheritDoc}
269+
*/
270+
public function reachedEndOfDataSource(): bool
271+
{
272+
if (!is_resource($this->stream)) {
273+
throw new Exception\AlreadyClosedException('Handle has already been closed.');
274+
}
275+
276+
if ($this->reachedEof) {
277+
return true;
278+
}
279+
280+
return $this->reachedEof = feof($this->stream);
281+
}
282+
257283
/**
258284
* {@inheritDoc}
259285
*/
@@ -279,14 +305,23 @@ public function tryRead(?int $max_bytes = null): string
279305
$max_bytes = self::MAXIMUM_READ_BUFFER_SIZE;
280306
}
281307

282-
$result = fread($this->stream, $max_bytes);
308+
if ($this->useSingleRead) {
309+
$result = fread($this->stream, $max_bytes);
310+
} else {
311+
$result = stream_get_contents($this->stream, $max_bytes);
312+
}
313+
283314
if ($result === false) {
284315
/** @var array{message: string} $error */
285316
$error = error_get_last();
286317

287318
throw new Exception\RuntimeException($error['message'] ?? 'unknown error.');
288319
}
289320

321+
if ($result === '' && feof($this->stream)) {
322+
$this->reachedEof = true;
323+
}
324+
290325
return $result;
291326
}
292327

src/Psl/IO/MemoryHandle.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,25 @@ final class MemoryHandle implements CloseSeekReadWriteHandleInterface
2121
private int $offset = 0;
2222
private string $buffer;
2323
private bool $closed = false;
24+
private bool $reachedEof = false;
2425

2526
public function __construct(string $buffer = '')
2627
{
2728
$this->buffer = $buffer;
2829
}
2930

3031
/**
31-
* Read from the handle.
32-
*
33-
* @param positive-int|null $max_bytes the maximum number of bytes to read.
34-
*
35-
* @throws Exception\AlreadyClosedException If the handle has been already closed.
36-
*
37-
* @return string the read data on success, or an empty string if the end of file is reached.
32+
* {@inheritDoc}
33+
*/
34+
public function reachedEndOfDataSource(): bool
35+
{
36+
$this->assertHandleIsOpen();
37+
38+
return $this->reachedEof;
39+
}
40+
41+
/**
42+
* {@inheritDoc}
3843
*/
3944
public function tryRead(?int $max_bytes = null): string
4045
{
@@ -46,6 +51,8 @@ public function tryRead(?int $max_bytes = null): string
4651

4752
$length = strlen($this->buffer);
4853
if ($this->offset >= $length) {
54+
$this->reachedEof = true;
55+
4956
return '';
5057
}
5158

@@ -58,13 +65,7 @@ public function tryRead(?int $max_bytes = null): string
5865
}
5966

6067
/**
61-
* Read from the handle.
62-
*
63-
* @param positive-int|null $max_bytes the maximum number of bytes to read.
64-
*
65-
* @throws Exception\AlreadyClosedException If the handle has been already closed.
66-
*
67-
* @return string the read data on success, or an empty string if the end of file is reached.
68+
* {@inheritDoc}
6869
*/
6970
public function read(?int $max_bytes = null, ?float $timeout = null): string
7071
{

src/Psl/IO/ReadHandleConvenienceMethodsTrait.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ trait ReadHandleConvenienceMethodsTrait
1717
/**
1818
* Read until there is no more data to read.
1919
*
20-
* It is possible for this to never return, e.g. if called on a pipe or
20+
* It is possible for this to never return, e.g. if called on a pipe
2121
* or socket which the other end keeps open forever. Set a timeout if you
2222
* do not want this to happen.
2323
*
@@ -60,15 +60,15 @@ static function () use ($data): void {
6060
if ($to_read !== null) {
6161
$to_read -= strlen($chunk);
6262
}
63-
} while (($to_read === null || $to_read > 0) && $chunk !== '');
63+
} while (($to_read === null || $to_read > 0) && !$this->reachedEndOfDataSource());
6464

6565
return $data->value;
6666
}
6767

6868
/**
6969
* Read a fixed amount of data.
7070
*
71-
* It is possible for this to never return, e.g. if called on a pipe or
71+
* It is possible for this to never return, e.g. if called on a pipe
7272
* or socket which the other end keeps open forever. Set a timeout if you
7373
* do not want this to happen.
7474
*

0 commit comments

Comments
 (0)