From 778354d4010d5a2dc05ade42c9b9dd3e0eab11cb Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Wed, 27 Dec 2023 15:32:31 -0600 Subject: [PATCH 01/16] Check if worker is in storage to avoid destruct issue --- src/Driver/ParallelFilesystemDriver.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Driver/ParallelFilesystemDriver.php b/src/Driver/ParallelFilesystemDriver.php index 4c978bf..490f3b2 100644 --- a/src/Driver/ParallelFilesystemDriver.php +++ b/src/Driver/ParallelFilesystemDriver.php @@ -22,7 +22,7 @@ final class ParallelFilesystemDriver implements FilesystemDriver /** @var int Maximum number of workers to use for open files. */ private int $workerLimit; - /** @var \SplObjectStorage Worker storage. */ + /** @var \SplObjectStorage Worker storage. */ private \SplObjectStorage $workerStorage; /** @var Future Pending worker request */ @@ -35,7 +35,7 @@ final class ParallelFilesystemDriver implements FilesystemDriver { $this->pool = $pool ?? workerPool(); $this->workerLimit = $workerLimit; - $this->workerStorage = new \SplObjectStorage; + $this->workerStorage = new \SplObjectStorage(); $this->pendingWorker = Future::complete(); } @@ -45,8 +45,11 @@ final class ParallelFilesystemDriver implements FilesystemDriver $workerStorage = $this->workerStorage; $worker = new Internal\FileWorker($worker, static function (Worker $worker) use ($workerStorage): void { - \assert($workerStorage->contains($worker)); - if (($workerStorage[$worker] -=1) === 0 || !$worker->isRunning()) { + if (!$workerStorage->contains($worker)) { + return; + } + + if (($workerStorage[$worker] -= 1) === 0 || !$worker->isRunning()) { $workerStorage->detach($worker); } }); From 0e9dd5e274f0ead144b1ee150cccc157b09bbe60 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Wed, 27 Dec 2023 15:45:45 -0600 Subject: [PATCH 02/16] Update CI extension installs --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d261c5b..e0d92f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,12 +11,16 @@ jobs: include: - operating-system: 'ubuntu-latest' php-version: '8.1' + extensions: uv, eio - operating-system: 'ubuntu-latest' php-version: '8.2' + extensions: uv, eio - operating-system: 'ubuntu-latest' php-version: '8.3' + extensions: uv + style-fix: none static-analysis: none - operating-system: 'windows-latest' @@ -26,7 +30,9 @@ jobs: - operating-system: 'macos-latest' php-version: '8.3' + extensions: uv job-description: 'on macOS' + style-fix: none static-analysis: none @@ -53,7 +59,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: eio-beta, uv-amphp/ext-uv@master + extensions: ${{ matrix.extensions }} - name: Get Composer cache directory id: composer-cache @@ -91,7 +97,7 @@ jobs: env: PHP_CS_FIXER_IGNORE_ENV: 1 run: vendor/bin/php-cs-fixer --diff --dry-run -v fix - if: runner.os != 'Windows' + if: runner.os != 'Windows' && matrix.style-fix != 'none' - name: Install composer-require-checker run: php -r 'file_put_contents("composer-require-checker.phar", file_get_contents("https://github.com/maglnet/ComposerRequireChecker/releases/download/3.7.0/composer-require-checker.phar"));' From 45d8d84c4496c695cf2ca2bf05f236a3e36c3558 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Wed, 27 Dec 2023 16:52:04 -0600 Subject: [PATCH 03/16] Fix detecting writable from mode Closes #77. --- src/Internal/QueuedWritesFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Internal/QueuedWritesFile.php b/src/Internal/QueuedWritesFile.php index ad91e2b..30b4d97 100644 --- a/src/Internal/QueuedWritesFile.php +++ b/src/Internal/QueuedWritesFile.php @@ -38,8 +38,8 @@ abstract class QueuedWritesFile implements File, \IteratorAggregate } $this->queue = new \SplQueue(); - $this->writable = $this->mode[0] !== 'r'; - $this->position = $this->mode[0] === 'a' ? $this->size : 0; + $this->writable = !\str_contains($this->mode, 'r') || \str_contains($this->mode, '+'); + $this->position = \str_contains($this->mode, 'a') ? $this->size : 0; } public function __destruct() From 1f1f8712da46d9b01b7648db59e746db93ce1beb Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 9 Mar 2024 09:18:09 -0600 Subject: [PATCH 04/16] Update Psalm --- composer.json | 2 +- psalm.xml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 80dea86..639fed4 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "require-dev": { "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4", + "psalm/phar": "5.22.2", "amphp/php-cs-fixer-config": "^2" }, "suggest": { diff --git a/psalm.xml b/psalm.xml index e774aea..cf504b6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -48,5 +48,12 @@ + + + + + + + From 1eca93a87ee28a6d15a2dae11d17a3017157292d Mon Sep 17 00:00:00 2001 From: Nadyita <43058421+Nadyita@users.noreply.github.com> Date: Sat, 9 Mar 2024 16:49:22 +0100 Subject: [PATCH 05/16] Add the promised FileCache class (#83) --- composer.json | 2 + src/FileCache.php | 181 +++++++++++++++++++++++++++++++++++++++++ test/FileCacheTest.php | 27 ++++++ 3 files changed, 210 insertions(+) create mode 100644 src/FileCache.php create mode 100644 test/FileCacheTest.php diff --git a/composer.json b/composer.json index 639fed4..6f46377 100644 --- a/composer.json +++ b/composer.json @@ -59,11 +59,13 @@ "autoload-dev": { "psr-4": { "Amp\\File\\Test\\": "test", + "Amp\\Cache\\Test\\": "vendor/amphp/cache/test", "Amp\\Sync\\": "vendor/amphp/sync/test" } }, "config": { "preferred-install": { + "amphp/cache": "source", "amphp/sync": "source" } }, diff --git a/src/FileCache.php b/src/FileCache.php new file mode 100644 index 0000000..66da94b --- /dev/null +++ b/src/FileCache.php @@ -0,0 +1,181 @@ +filesystem = $filesystem; + $this->directory = $directory = \rtrim($directory, "/\\"); + + $gcWatcher = static function () use ($directory, $mutex, $filesystem): void { + try { + $files = $filesystem->listFiles($directory); + + foreach ($files as $file) { + if (\strlen($file) !== 70 || !\str_ends_with($file, '.cache')) { + continue; + } + + try { + $lock = $mutex->acquire($file); + } catch (\Throwable) { + continue; + } + + try { + $handle = $filesystem->openFile($directory . '/' . $file, 'r'); + $ttl = $handle->read(length: 4); + + if ($ttl === null || \strlen($ttl) !== 4) { + $handle->close(); + continue; + } + + $ttl = \unpack('Nttl', $ttl)['ttl']; + if ($ttl < \time()) { + $filesystem->deleteFile($directory . '/' . $file); + } + } catch (\Throwable) { + // ignore + } finally { + $lock->release(); + } + } + } catch (\Throwable) { + // ignore + } + }; + + // trigger once, so short running scripts also GC and don't grow forever + EventLoop::defer($gcWatcher); + + $this->gcWatcher = EventLoop::repeat(300, $gcWatcher); + + EventLoop::unreference($this->gcWatcher); + } + + public function __destruct() + { + if ($this->gcWatcher !== null) { + EventLoop::cancel($this->gcWatcher); + } + } + + public function get(string $key): ?string + { + $filename = $this->getFilename($key); + + $lock = $this->lock($filename); + + try { + $cacheContent = $this->filesystem->read($this->directory . '/' . $filename); + + if (\strlen($cacheContent) < 4) { + return null; + } + + $ttl = \unpack('Nttl', \substr($cacheContent, 0, 4))['ttl']; + if ($ttl < \time()) { + $this->filesystem->deleteFile($this->directory . '/' . $filename); + + return null; + } + + $value = \substr($cacheContent, 4); + + \assert(\is_string($value)); + + return $value; + } catch (\Throwable) { + return null; + } finally { + $lock->release(); + } + } + + public function set(string $key, string $value, int $ttl = null): void + { + if ($ttl < 0) { + throw new \Error("Invalid cache TTL ({$ttl}); integer >= 0 or null required"); + } + + $filename = $this->getFilename($key); + + $lock = $this->lock($filename); + + if ($ttl === null) { + $ttl = \PHP_INT_MAX; + } else { + $ttl = \time() + $ttl; + } + + $encodedTtl = \pack('N', $ttl); + + try { + $this->filesystem->write($this->directory . '/' . $filename, $encodedTtl . $value); + } finally { + $lock->release(); + } + } + + public function delete(string $key): ?bool + { + $filename = $this->getFilename($key); + + $lock = $this->lock($filename); + + try { + $this->filesystem->deleteFile($this->directory . '/' . $filename); + } catch (FilesystemException) { + return false; + } finally { + $lock->release(); + } + + return true; + } + + private static function getFilename(string $key): string + { + return \hash('sha256', $key) . '.cache'; + } + + private function lock(string $key): Lock + { + try { + return $this->mutex->acquire($key); + } catch (\Throwable $exception) { + throw new CacheException( + \sprintf('Exception thrown when obtaining the lock for key "%s"', $key), + 0, + $exception + ); + } + } +} diff --git a/test/FileCacheTest.php b/test/FileCacheTest.php new file mode 100644 index 0000000..ed3daa6 --- /dev/null +++ b/test/FileCacheTest.php @@ -0,0 +1,27 @@ + Date: Sat, 9 Mar 2024 09:55:08 -0600 Subject: [PATCH 06/16] Spaces --- src/Whence.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Whence.php b/src/Whence.php index e21b8cb..21ef1d0 100644 --- a/src/Whence.php +++ b/src/Whence.php @@ -8,10 +8,12 @@ enum Whence * Set position equal to offset bytes. */ case Start; + /** * Set position to current location plus offset. */ case Current; + /** * Set position to end-of-file plus offset. */ From 0bc3e2d251627fbd90b6f07e16800947ea6b7c7f Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 9 Mar 2024 10:05:11 -0600 Subject: [PATCH 07/16] Add Filesystem arg to FileMutex --- src/FileMutex.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index e773ce1..d500559 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -11,11 +11,14 @@ final class FileMutex implements Mutex { private const LATENCY_TIMEOUT = 0.01; + private readonly Filesystem $filesystem; + /** * @param string $fileName Name of temporary file to use as a mutex. */ - public function __construct(private readonly string $fileName) + public function __construct(private readonly string $fileName, ?Filesystem $filesystem = null) { + $this->filesystem = $filesystem ?? filesystem(); } public function acquire(): Lock @@ -24,7 +27,7 @@ final class FileMutex implements Mutex // has the lock, so set an asynchronous timer and try again. while (true) { try { - $file = openFile($this->fileName, 'x'); + $file = $this->filesystem->openFile($this->fileName, 'x'); // Return a lock object that can be used to release the lock on the mutex. $lock = new Lock($this->release(...)); @@ -46,12 +49,11 @@ final class FileMutex implements Mutex private function release(): void { try { - deleteFile($this->fileName); + $this->filesystem->deleteFile($this->fileName); } catch (\Throwable $exception) { throw new SyncException( 'Failed to unlock the mutex file: ' . $this->fileName, - 0, - $exception + previous: $exception, ); } } From 503c1b5c0a2a2657bf52383c665b1c6b40a17181 Mon Sep 17 00:00:00 2001 From: Bob Weinand Date: Mon, 11 Mar 2024 05:06:03 +0100 Subject: [PATCH 08/16] Add KeyedFileMutex (#62) * Add KeyedFileMutex * Rebase and update * Style fix * Swap prefix for directory --------- Co-authored-by: Aaron Piotrowski --- src/KeyedFileMutex.php | 75 +++++++++++++++++++++++++++++++++++++ test/KeyedFileMutexTest.php | 27 +++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/KeyedFileMutex.php create mode 100644 test/KeyedFileMutexTest.php diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php new file mode 100644 index 0000000..da646f8 --- /dev/null +++ b/src/KeyedFileMutex.php @@ -0,0 +1,75 @@ +filesystem = $filesystem ?? filesystem(); + $this->directory = \rtrim($directory, "/\\"); + + if (!$this->filesystem->isDirectory($this->directory)) { + throw new ValueError(\sprintf('Directory "%s" does not exist', $this->directory)); + } + } + + public function acquire(string $key): Lock + { + $filename = $this->getFilename($key); + + // Try to create the lock file. If the file already exists, someone else + // has the lock, so set an asynchronous timer and try again. + while (true) { + try { + $file = $this->filesystem->openFile($filename, 'x'); + + // Return a lock object that can be used to release the lock on the mutex. + $lock = new Lock(fn () => $this->release($filename)); + + $file->close(); + + return $lock; + } catch (FilesystemException) { + delay(self::LATENCY_TIMEOUT); + } + } + } + + /** + * Releases the lock on the mutex. + * + * @throws SyncException + */ + private function release(string $filename): void + { + try { + $this->filesystem->deleteFile($filename); + } catch (\Throwable $exception) { + throw new SyncException( + 'Failed to unlock the mutex file: ' . $filename, + previous: $exception, + ); + } + } + + private function getFilename(string $key): string + { + return $this->directory . '/' . \hash('sha256', $key) . '.lock'; + } +} diff --git a/test/KeyedFileMutexTest.php b/test/KeyedFileMutexTest.php new file mode 100644 index 0000000..2852721 --- /dev/null +++ b/test/KeyedFileMutexTest.php @@ -0,0 +1,27 @@ + Date: Mon, 11 Mar 2024 19:20:54 -0500 Subject: [PATCH 09/16] Exponential backoff attempting to obtain file lock --- src/FileMutex.php | 5 +++-- src/KeyedFileMutex.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index d500559..99c1873 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -10,6 +10,7 @@ use function Amp\delay; final class FileMutex implements Mutex { private const LATENCY_TIMEOUT = 0.01; + private const DELAY_LIMIT = 1; private readonly Filesystem $filesystem; @@ -25,7 +26,7 @@ final class FileMutex implements Mutex { // Try to create the lock file. If the file already exists, someone else // has the lock, so set an asynchronous timer and try again. - while (true) { + for ($attempt = 0; true; ++$attempt) { try { $file = $this->filesystem->openFile($this->fileName, 'x'); @@ -36,7 +37,7 @@ final class FileMutex implements Mutex return $lock; } catch (FilesystemException) { - delay(self::LATENCY_TIMEOUT); + delay(min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); } } } diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index da646f8..f25fbba 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -11,6 +11,7 @@ use function Amp\delay; final class KeyedFileMutex implements KeyedMutex { private const LATENCY_TIMEOUT = 0.01; + private const DELAY_LIMIT = 1; private readonly Filesystem $filesystem; @@ -35,7 +36,7 @@ final class KeyedFileMutex implements KeyedMutex // Try to create the lock file. If the file already exists, someone else // has the lock, so set an asynchronous timer and try again. - while (true) { + for ($attempt = 0; true; ++$attempt) { try { $file = $this->filesystem->openFile($filename, 'x'); @@ -46,7 +47,7 @@ final class KeyedFileMutex implements KeyedMutex return $lock; } catch (FilesystemException) { - delay(self::LATENCY_TIMEOUT); + delay(min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); } } } From 22d64fb6dd76a323a0e067780a130627febaf62b Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Mon, 11 Mar 2024 19:23:18 -0500 Subject: [PATCH 10/16] Update readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d22bbe..b7f96c7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ This package can be installed as a [Composer](https://getcomposer.org/) dependen composer require amphp/file ``` +## Requirements + +- PHP 8.1+ + `amphp/file` works out of the box without any PHP extensions. It uses multiple processes by default, but also comes with a blocking driver that uses PHP's blocking functions in the current process. @@ -163,7 +167,7 @@ array(13) { ## Security -If you discover any security related issues, please email [`me@kelunik.com`](mailto:me@kelunik.com) instead of using the issue tracker. +If you discover any security related issues, please use the private security issue reporter instead of using the public issue tracker. ## License From d5f6c81e51336040af3dcb71ce519cd8788fcffc Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Mon, 11 Mar 2024 19:20:54 -0500 Subject: [PATCH 11/16] Style fix --- src/FileMutex.php | 2 +- src/KeyedFileMutex.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index 99c1873..5efd967 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -37,7 +37,7 @@ final class FileMutex implements Mutex return $lock; } catch (FilesystemException) { - delay(min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); + delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); } } } diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index f25fbba..88fe5c8 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -47,7 +47,7 @@ final class KeyedFileMutex implements KeyedMutex return $lock; } catch (FilesystemException) { - delay(min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); + delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); } } } From 6b82161f20ef539c8ea3b0ae2c03625511e23c09 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 16 Mar 2024 11:08:11 -0500 Subject: [PATCH 12/16] Add optional cancellation arg --- src/FileMutex.php | 12 ++++++++++-- src/KeyedFileMutex.php | 14 +++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/FileMutex.php b/src/FileMutex.php index 5efd967..b827f94 100644 --- a/src/FileMutex.php +++ b/src/FileMutex.php @@ -2,6 +2,7 @@ namespace Amp\File; +use Amp\Cancellation; use Amp\Sync\Lock; use Amp\Sync\Mutex; use Amp\Sync\SyncException; @@ -14,16 +15,23 @@ final class FileMutex implements Mutex private readonly Filesystem $filesystem; + private readonly string $directory; + /** * @param string $fileName Name of temporary file to use as a mutex. */ public function __construct(private readonly string $fileName, ?Filesystem $filesystem = null) { $this->filesystem = $filesystem ?? filesystem(); + $this->directory = \dirname($this->fileName); } - public function acquire(): Lock + public function acquire(?Cancellation $cancellation = null): Lock { + if (!$this->filesystem->isDirectory($this->directory)) { + throw new SyncException(\sprintf('Directory of "%s" does not exist or is not a directory', $this->fileName)); + } + // Try to create the lock file. If the file already exists, someone else // has the lock, so set an asynchronous timer and try again. for ($attempt = 0; true; ++$attempt) { @@ -37,7 +45,7 @@ final class FileMutex implements Mutex return $lock; } catch (FilesystemException) { - delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); + delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation); } } } diff --git a/src/KeyedFileMutex.php b/src/KeyedFileMutex.php index 88fe5c8..a6a340c 100644 --- a/src/KeyedFileMutex.php +++ b/src/KeyedFileMutex.php @@ -2,10 +2,10 @@ namespace Amp\File; +use Amp\Cancellation; use Amp\Sync\KeyedMutex; use Amp\Sync\Lock; use Amp\Sync\SyncException; -use ValueError; use function Amp\delay; final class KeyedFileMutex implements KeyedMutex @@ -24,14 +24,14 @@ final class KeyedFileMutex implements KeyedMutex { $this->filesystem = $filesystem ?? filesystem(); $this->directory = \rtrim($directory, "/\\"); - - if (!$this->filesystem->isDirectory($this->directory)) { - throw new ValueError(\sprintf('Directory "%s" does not exist', $this->directory)); - } } - public function acquire(string $key): Lock + public function acquire(string $key, ?Cancellation $cancellation = null): Lock { + if (!$this->filesystem->isDirectory($this->directory)) { + throw new SyncException(\sprintf('Directory "%s" does not exist or is not a directory', $this->directory)); + } + $filename = $this->getFilename($key); // Try to create the lock file. If the file already exists, someone else @@ -47,7 +47,7 @@ final class KeyedFileMutex implements KeyedMutex return $lock; } catch (FilesystemException) { - delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt))); + delay(\min(self::DELAY_LIMIT, self::LATENCY_TIMEOUT * (2 ** $attempt)), cancellation: $cancellation); } } } From 72f60716161813817c7ca0e722e4a2f66df15281 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 21 Apr 2024 09:47:27 -0500 Subject: [PATCH 13/16] Fix implicit nullable parameters --- src/Driver/ParallelFilesystemDriver.php | 2 +- src/FileCache.php | 2 +- src/FilesystemException.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Driver/ParallelFilesystemDriver.php b/src/Driver/ParallelFilesystemDriver.php index 490f3b2..f18a0a8 100644 --- a/src/Driver/ParallelFilesystemDriver.php +++ b/src/Driver/ParallelFilesystemDriver.php @@ -31,7 +31,7 @@ final class ParallelFilesystemDriver implements FilesystemDriver /** * @param int $workerLimit Maximum number of workers to use from the pool for open files. */ - public function __construct(WorkerPool $pool = null, int $workerLimit = self::DEFAULT_WORKER_LIMIT) + public function __construct(?WorkerPool $pool = null, int $workerLimit = self::DEFAULT_WORKER_LIMIT) { $this->pool = $pool ?? workerPool(); $this->workerLimit = $workerLimit; diff --git a/src/FileCache.php b/src/FileCache.php index 66da94b..f0971f5 100644 --- a/src/FileCache.php +++ b/src/FileCache.php @@ -119,7 +119,7 @@ final class FileCache implements StringCache } } - public function set(string $key, string $value, int $ttl = null): void + public function set(string $key, string $value, ?int $ttl = null): void { if ($ttl < 0) { throw new \Error("Invalid cache TTL ({$ttl}); integer >= 0 or null required"); diff --git a/src/FilesystemException.php b/src/FilesystemException.php index d2b2cdb..5e58ea4 100644 --- a/src/FilesystemException.php +++ b/src/FilesystemException.php @@ -4,7 +4,7 @@ namespace Amp\File; class FilesystemException extends \Exception { - public function __construct(string $message, \Throwable $previous = null) + public function __construct(string $message, ?\Throwable $previous = null) { parent::__construct($message, 0, $previous); } From 1d51235bd8cb4973c7908e645b62c638d8c081fe Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sun, 21 Apr 2024 09:49:46 -0500 Subject: [PATCH 14/16] Build on PHP 8.4 --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0d92f0..54f6615 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,12 @@ jobs: style-fix: none static-analysis: none + - operating-system: 'ubuntu-latest' + php-version: '8.4' + extensions: uv + style-fix: none + static-analysis: none + - operating-system: 'windows-latest' php-version: '8.3' job-description: 'on Windows' From 0453021eefa6b3921f6432ada2242528d7fcbd00 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Fri, 24 May 2024 18:33:40 -0500 Subject: [PATCH 15/16] Drop support for ext-uv 0.2.x Fixed detection of uv being used as the event loop driver when wrapped with a delegate driver (e.g., TracingDriver). --- src/Driver/UvFile.php | 23 +----- src/Driver/UvFilesystemDriver.php | 133 +++++++++--------------------- src/Internal/UvPoll.php | 4 +- src/functions.php | 1 - 4 files changed, 42 insertions(+), 119 deletions(-) diff --git a/src/Driver/UvFile.php b/src/Driver/UvFile.php index 23765bc..5b3f770 100644 --- a/src/Driver/UvFile.php +++ b/src/Driver/UvFile.php @@ -1,5 +1,4 @@ eventLoopHandle = $driver->getHandle(); $this->onClose = new DeferredFuture; - - $this->priorVersion = \version_compare(\phpversion('uv'), '0.3.0', '<'); } public function read(?Cancellation $cancellation = null, int $length = self::DEFAULT_READ_LENGTH): ?string @@ -86,16 +79,6 @@ final class UvFile extends Internal\QueuedWritesFile $deferred->complete($length ? $buffer : null); }; - if ($this->priorVersion) { - $onRead = static function ($fh, $result, $buffer) use ($onRead): void { - if ($result < 0) { - $buffer = $result; // php-uv v0.3.0 changed the callback to put an int in $buffer on error. - } - - $onRead($result, $buffer); - }; - } - \uv_fs_read($this->eventLoopHandle, $this->fh, $this->position, $length, $onRead); $id = $cancellation?->subscribe(function (\Throwable $exception) use ($deferred): void { diff --git a/src/Driver/UvFilesystemDriver.php b/src/Driver/UvFilesystemDriver.php index 695bdc3..f396c70 100644 --- a/src/Driver/UvFilesystemDriver.php +++ b/src/Driver/UvFilesystemDriver.php @@ -1,5 +1,4 @@ =') && $driver->getHandle() instanceof \UVLoop; } - /** @var \UVLoop|resource Loop resource of type uv_loop or instance of \UVLoop. */ - private $eventLoopHandle; + private readonly \UVLoop $eventLoopHandle; private readonly Internal\UvPoll $poll; - /** @var bool True if ext-uv version is < 0.3.0. */ - private readonly bool $priorVersion; - - public function __construct(private readonly UvLoopDriver $driver) + public function __construct(private readonly EventLoopDriver $driver) { + if (!self::isSupported($driver)) { + throw new \Error('Event loop did not return a compatible handle'); + } + /** @psalm-suppress PropertyTypeCoercion */ $this->eventLoopHandle = $driver->getHandle(); $this->poll = new Internal\UvPoll($driver); - $this->priorVersion = \version_compare(\phpversion('uv'), '0.3.0', '<'); } public function openFile(string $path, string $mode): UvFile @@ -83,16 +85,6 @@ final class UvFilesystemDriver implements FilesystemDriver $deferred->complete($stat); }; - if ($this->priorVersion) { - $callback = static function ($fh, $stat) use ($callback): void { - if (empty($fh)) { - $stat = 0; - } - - $callback($stat); - }; - } - \uv_fs_stat($this->eventLoopHandle, $path, $callback); try { @@ -107,17 +99,9 @@ final class UvFilesystemDriver implements FilesystemDriver $deferred = new DeferredFuture; $this->poll->listen(); - if ($this->priorVersion) { - $callback = static function ($fh, $stat) use ($deferred): void { - $deferred->complete(empty($fh) ? null : $stat); - }; - } else { - $callback = static function ($stat) use ($deferred): void { - $deferred->complete(\is_int($stat) ? null : $stat); - }; - } - - \uv_fs_lstat($this->eventLoopHandle, $path, $callback); + \uv_fs_lstat($this->eventLoopHandle, $path, static function ($stat) use ($deferred): void { + $deferred->complete(\is_int($stat) ? null : $stat); + }); try { return $deferred->getFuture()->await(); @@ -160,27 +144,14 @@ final class UvFilesystemDriver implements FilesystemDriver $deferred = new DeferredFuture; $this->poll->listen(); - if ($this->priorVersion) { - $callback = static function ($fh, $target) use ($deferred): void { - if (!(bool) $fh) { - $deferred->error(new FilesystemException("Could not read symbolic link")); - return; - } + \uv_fs_readlink($this->eventLoopHandle, $target, static function ($target) use ($deferred): void { + if (\is_int($target)) { + $deferred->error(new FilesystemException("Could not read symbolic link")); + return; + } - $deferred->complete($target); - }; - } else { - $callback = static function ($target) use ($deferred): void { - if (\is_int($target)) { - $deferred->error(new FilesystemException("Could not read symbolic link")); - return; - } - - $deferred->complete($target); - }; - } - - \uv_fs_readlink($this->eventLoopHandle, $target, $callback); + $deferred->complete($target); + }); try { return $deferred->getFuture()->await(); @@ -297,28 +268,16 @@ final class UvFilesystemDriver implements FilesystemDriver $deferred = new DeferredFuture; $this->poll->listen(); - if ($this->priorVersion) { - \uv_fs_readdir($this->eventLoopHandle, $path, 0, static function ($fh, $data) use ($deferred, $path): void { - if (empty($fh) && $data !== 0) { - $deferred->error(new FilesystemException("Failed reading contents from {$path}")); - } elseif ($data === 0) { - $deferred->complete([]); - } else { - $deferred->complete($data); - } - }); - } else { - /** @noinspection PhpUndefinedFunctionInspection */ - \uv_fs_scandir($this->eventLoopHandle, $path, static function ($data) use ($deferred, $path): void { - if (\is_int($data) && $data !== 0) { - $deferred->error(new FilesystemException("Failed reading contents from {$path}")); - } elseif ($data === 0) { - $deferred->complete([]); - } else { - $deferred->complete($data); - } - }); - } + /** @noinspection PhpUndefinedFunctionInspection */ + \uv_fs_scandir($this->eventLoopHandle, $path, static function ($data) use ($deferred, $path): void { + if (\is_int($data) && $data !== 0) { + $deferred->error(new FilesystemException("Failed reading contents from {$path}")); + } elseif ($data === 0) { + $deferred->complete([]); + } else { + $deferred->complete($data); + } + }); try { return $deferred->getFuture()->await(); @@ -528,42 +487,24 @@ final class UvFilesystemDriver implements FilesystemDriver { $deferred = new DeferredFuture; - if ($this->priorVersion) { - $callback = static function ($fileHandle, $readBytes, $buffer) use ($deferred): void { - $deferred->complete($readBytes < 0 ? null : $buffer); - }; - } else { - $callback = static function ($readBytes, $buffer) use ($deferred): void { - $deferred->complete($readBytes < 0 ? null : $buffer); - }; - } + $callback = static function ($readBytes, $buffer) use ($deferred): void { + $deferred->complete($readBytes < 0 ? null : $buffer); + }; \uv_fs_read($this->eventLoopHandle, $fileHandle, 0, $length, $callback); return $deferred->getFuture()->await(); } - private function doWrite(string $path, string $contents): void - { - } - private function createGenericCallback(DeferredFuture $deferred, string $error): \Closure { - $callback = static function (int $result) use ($deferred, $error): void { + return static function (int $result) use ($deferred, $error): void { if ($result !== 0) { $deferred->error(new FilesystemException($error)); return; } - $deferred->complete(null); + $deferred->complete(); }; - - if ($this->priorVersion) { - $callback = static function (bool $result) use ($callback): void { - $callback($result ? 0 : -1); - }; - } - - return $callback; } } diff --git a/src/Internal/UvPoll.php b/src/Internal/UvPoll.php index 5bccadf..fec197d 100644 --- a/src/Internal/UvPoll.php +++ b/src/Internal/UvPoll.php @@ -2,7 +2,7 @@ namespace Amp\File\Internal; -use Revolt\EventLoop\Driver\UvDriver as UvLoopDriver; +use Revolt\EventLoop\Driver as EventLoopDriver; /** @internal */ final class UvPoll @@ -11,7 +11,7 @@ final class UvPoll private int $requests = 0; - public function __construct(private readonly UvLoopDriver $driver) + public function __construct(private readonly EventLoopDriver $driver) { // Create dummy watcher to keep loop running while polling. diff --git a/src/functions.php b/src/functions.php index c81dd7e..80f8b3d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -55,7 +55,6 @@ function createDefaultDriver(): FilesystemDriver $driver = EventLoop::getDriver(); if (UvFilesystemDriver::isSupported($driver)) { - /** @var EventLoop\Driver\UvDriver $driver */ return new UvFilesystemDriver($driver); } From be1e2457409cc58877cbd27d3cb2246870000419 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Fri, 24 May 2024 18:40:25 -0500 Subject: [PATCH 16/16] Add UVLoop to whitelist --- composer-require-check.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer-require-check.json b/composer-require-check.json index 93a5db8..3067f87 100644 --- a/composer-require-check.json +++ b/composer-require-check.json @@ -62,6 +62,7 @@ "EIO_S_IWUSR", "EIO_S_IXUSR", "UV", + "UVLoop", "uv_fs_chmod", "uv_fs_chown", "uv_fs_fstat",