diff --git a/docs/component/async.md b/docs/component/async.md index d15feff5..41ba2dc6 100644 --- a/docs/component/async.md +++ b/docs/component/async.md @@ -15,13 +15,13 @@ - [all](./../../src/Psl/Async/all.php#L22) - [any](./../../src/Psl/Async/any.php#L25) - [await](./../../src/Psl/Async/await.php#L18) -- [await_readable](./../../src/Psl/Async/await_readable.php#L18) +- [await_readable](./../../src/Psl/Async/await_readable.php#L23) - [await_signal](./../../src/Psl/Async/await_signal.php#L18) -- [await_writable](./../../src/Psl/Async/await_writable.php#L18) +- [await_writable](./../../src/Psl/Async/await_writable.php#L21) - [concurrent](./../../src/Psl/Async/concurrent.php#L21) - [first](./../../src/Psl/Async/first.php#L24) - [later](./../../src/Psl/Async/later.php#L14) -- [main](./../../src/Psl/Async/main.php#L18) +- [main](./../../src/Psl/Async/main.php#L16) - [run](./../../src/Psl/Async/run.php#L20) - [sleep](./../../src/Psl/Async/sleep.php#L10) diff --git a/src/Psl/Async/Exception/ResourceClosedException.php b/src/Psl/Async/Exception/ResourceClosedException.php new file mode 100644 index 00000000..5a178802 --- /dev/null +++ b/src/Psl/Async/Exception/ResourceClosedException.php @@ -0,0 +1,9 @@ +suspend(); + } catch (Throwable $e) { + if (!is_resource($resource)) { + throw new ResourceClosedException('Resource was closed before it became readable.'); + } + + /** + * @psalm-suppress MissingThrowsDocblock + * @psalm-suppress PossiblyUndefinedVariable + */ + throw $e; } finally { Scheduler::cancel($watcher); diff --git a/src/Psl/Async/await_writable.php b/src/Psl/Async/await_writable.php index 1c991848..112e1bdf 100644 --- a/src/Psl/Async/await_writable.php +++ b/src/Psl/Async/await_writable.php @@ -4,7 +4,9 @@ namespace Psl\Async; +use Psl\Async\Exception\ResourceClosedException; use Revolt\EventLoop; +use Throwable; /** * Wait for the given resource to become writable in a non-blocking way. @@ -12,6 +14,7 @@ * @param resource|object $resource * * @throws Exception\TimeoutException If $timeout is non-null, and the operation timed-out. + * @throws Exception\ResourceClosedException If $resource was closed before it became writable. * * @codeCoverageIgnore */ @@ -36,6 +39,16 @@ function await_writable(mixed $resource, bool $reference = true, ?float $timeout try { $suspension->suspend(); + } catch (Throwable $e) { + if (!is_resource($resource)) { + throw new ResourceClosedException('Resource was closed before it became writable.'); + } + + /** + * @psalm-suppress MissingThrowsDocblock + * @psalm-suppress PossiblyUndefinedVariable + */ + throw $e; } finally { Scheduler::cancel($watcher); diff --git a/src/Psl/Async/main.php b/src/Psl/Async/main.php index 5c46d3c8..c99d9490 100644 --- a/src/Psl/Async/main.php +++ b/src/Psl/Async/main.php @@ -4,35 +4,23 @@ namespace Psl\Async; -use Throwable; - /** * Execute the given callable in an async context, then exit with returned exit code. * * If the callable returns an awaitable, the awaitable *MUST* resolve with an exit code. * - * @param (callable(): int)|(callable(): Awaitable) $callable + * @param (callable(): int)|(callable(): Awaitable)|(callable(): never)|(callable(): Awaitable) $callable * * @codeCoverageIgnore */ function main(callable $callable): never { - $main = Scheduler::createSuspension(); - - Scheduler::defer(static function () use ($callable, $main): void { - try { - $exit_code = $callable(); - $main->resume($exit_code); - } catch (Throwable $throwable) { - $main->throw($throwable); - } - }); + later(); - /** @var int|Awaitable $return */ - $return = $main->suspend(); - if ($return instanceof Awaitable) { - $return = $return->await(); + $result = $callable(); + if ($result instanceof Awaitable) { + $result = $result->await(); } - exit($return); + exit($result); } diff --git a/src/Psl/IO/Internal/ResourceHandle.php b/src/Psl/IO/Internal/ResourceHandle.php index 68e41d7a..21d89ac5 100644 --- a/src/Psl/IO/Internal/ResourceHandle.php +++ b/src/Psl/IO/Internal/ResourceHandle.php @@ -124,6 +124,10 @@ public function write(string $bytes, ?float $timeout = null): int Async\await_writable($this->resource, timeout: $timeout); } catch (Async\Exception\TimeoutException) { throw new Exception\TimeoutException('reached timeout while the handle is still not writable.'); + } catch (Async\Exception\ResourceClosedException) { + $this->resource = null; + + throw new Exception\AlreadyClosedException('Handle has already been closed.'); } return $written + $this->writeImmediately($bytes); @@ -215,6 +219,10 @@ public function read(?int $max_bytes = null, ?float $timeout = null): string Async\await_readable($this->resource, timeout: $timeout); } catch (Async\Exception\TimeoutException) { throw new Exception\TimeoutException('reached timeout while the handle is still not readable.'); + } catch (Async\Exception\ResourceClosedException) { + $this->resource = null; + + throw new Exception\AlreadyClosedException('Handle has already been closed.'); } return $this->readImmediately($max_bytes);