diff --git a/composer.json b/composer.json index 6a304aaa..27987cc0 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,6 @@ "hhvm/hsl": "^4.15" }, "provide": { - "hhvm/hsl-io": "0.2.0" + "hhvm/hsl-io": "0.2.1" } } diff --git a/src/io/CloseableHandle.php b/src/io/CloseableHandle.php index 6bbae6ee..0fe4f1bb 100644 --- a/src/io/CloseableHandle.php +++ b/src/io/CloseableHandle.php @@ -19,4 +19,11 @@ interface CloseableHandle extends Handle { /** Close the handle */ public function close(): void; + + /** Close the handle when the returned disposable is disposed. + * + * Usage: `using $handle->closeWhenDisposed();` + */ + <<__ReturnDisposable>> + public function closeWhenDisposed(): \IDisposable; } diff --git a/src/io/MemoryHandle.php b/src/io/MemoryHandle.php index 9229ba5e..b355ebee 100644 --- a/src/io/MemoryHandle.php +++ b/src/io/MemoryHandle.php @@ -11,7 +11,7 @@ namespace HH\Lib\IO; use namespace HH\Lib\{Math, OS, Str}; -use namespace HH\Lib\_Private\_OS; +use namespace HH\Lib\_Private\{_IO, _OS}; enum MemoryHandleWriteMode: int { OVERWRITE = 0; @@ -42,6 +42,11 @@ public function close(): void { $this->offset = -1; } + <<__ReturnDisposable>> + public function closeWhenDisposed(): \IDisposable { + return new _IO\CloseWhenDisposed($this); + } + public async function readAsync( ?int $max_bytes = null, ?int $_timeout_nanos = null, diff --git a/src/io/_Private/CloseWhenDisposed.php b/src/io/_Private/CloseWhenDisposed.php new file mode 100644 index 00000000..09b71b2d --- /dev/null +++ b/src/io/_Private/CloseWhenDisposed.php @@ -0,0 +1,24 @@ +handle->close(); + } +} diff --git a/src/io/_Private/FileDescriptorHandle.php b/src/io/_Private/FileDescriptorHandle.php index 8947eae5..f47288b7 100644 --- a/src/io/_Private/FileDescriptorHandle.php +++ b/src/io/_Private/FileDescriptorHandle.php @@ -11,7 +11,7 @@ namespace HH\Lib\_Private\_IO; use namespace HH\Lib\{IO, OS, Str}; -use namespace HH\Lib\_Private\_OS; +use namespace HH\Lib\_Private\{_IO, _OS}; abstract class FileDescriptorHandle implements IO\CloseableHandle, IO\FDHandle { protected bool $isAwaitable = true; @@ -52,4 +52,9 @@ final public function getFileDescriptor(): OS\FileDescriptor { final public function close(): void { OS\close($this->impl); } + + <<__ReturnDisposable>> + final public function closeWhenDisposed(): \IDisposable { + return new _IO\CloseWhenDisposed($this); + } } diff --git a/tests/io/MemoryHandleTest.php b/tests/io/MemoryHandleTest.php index c94f98f6..c46a96f7 100644 --- a/tests/io/MemoryHandleTest.php +++ b/tests/io/MemoryHandleTest.php @@ -30,6 +30,15 @@ final class MemoryHandleTest extends HackTest { expect(await $h->readAllAsync())->toEqual('derp'); } + public function testCloseWhenDisposed(): void { + $h = new IO\MemoryHandle('foobar'); + using ($h->closeWhenDisposed()) { + expect($h->read(3))->toEqual('foo'); + } + $ex = expect(() ==> $h->read(3))->toThrow(OS\ErrnoException::class); + expect($ex->getErrno())->toEqual(OS\Errno::EBADF); + } + public async function testReadAtInvalidOffset(): Awaitable { $h = new IO\MemoryHandle('herpderp'); $h->seek(99999); diff --git a/tests/io/PipeTest.php b/tests/io/PipeTest.php index 8f513d64..5ccfa4b7 100644 --- a/tests/io/PipeTest.php +++ b/tests/io/PipeTest.php @@ -81,6 +81,20 @@ final class PipeTest extends HackTest { } } + public async function testCloseWhenDisposed(): Awaitable { + list($r, $w) = IO\pipe(); + using ($w->closeWhenDisposed()) { + $w->write("Hello, world\n"); + } + expect(await $r->readAsync())->toEqual("Hello, world\n"); + // - does not block forever + // - does not fail, just succeeds with no data + expect(await $r->readAsync())->toEqual(''); + + $ex = expect(() ==> $w->write('foo'))->toThrow(OS\ErrnoException::class); + expect($ex->getErrno())->toEqual(OS\Errno::EBADF); + } + public async function testReadFromClosedPipe(): Awaitable { // Intent is to: // - make sure we throw the expected errno