From 3c9f3e7bd256fe914d1420d5f7ce7e6550598688 Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Fri, 9 Feb 2018 16:37:45 +0200 Subject: [PATCH 1/5] Add watch method to watch filesystem for changes --- src/Symfony/Component/Filesystem/CHANGELOG.md | 5 + .../Component/Filesystem/Filesystem.php | 22 +++++ .../Tests/Fixtures/ChangeFileResource.php | 33 +++++++ .../Tests/Watcher/FileSystemWatchTest.php | 80 ++++++++++++++++ .../Watcher/Resource/ArrayResourceTest.php | 35 +++++++ .../Resource/DirectoryResourceTest.php | 70 ++++++++++++++ .../Watcher/Resource/FileResourceTest.php | 34 +++++++ .../Locator/FileResourceLocatorTest.php | 96 +++++++++++++++++++ .../Filesystem/Watcher/FileChangeEvent.php | 42 ++++++++ .../Filesystem/Watcher/FileChangeWatcher.php | 57 +++++++++++ .../Filesystem/Watcher/INotifyWatcher.php | 84 ++++++++++++++++ .../Watcher/Resource/ArrayResource.php | 43 +++++++++ .../Watcher/Resource/DirectoryResource.php | 83 ++++++++++++++++ .../Watcher/Resource/FileResource.php | 55 +++++++++++ .../Resource/Locator/FileResourceLocator.php | 59 ++++++++++++ .../Watcher/Resource/ResourceInterface.php | 20 ++++ .../Filesystem/Watcher/WatcherInterface.php | 20 ++++ 17 files changed, 838 insertions(+) create mode 100644 src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php create mode 100644 src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php create mode 100644 src/Symfony/Component/Filesystem/Tests/Watcher/Resource/ArrayResourceTest.php create mode 100644 src/Symfony/Component/Filesystem/Tests/Watcher/Resource/DirectoryResourceTest.php create mode 100644 src/Symfony/Component/Filesystem/Tests/Watcher/Resource/FileResourceTest.php create mode 100644 src/Symfony/Component/Filesystem/Tests/Watcher/Resource/Locator/FileResourceLocatorTest.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/Resource/ResourceInterface.php create mode 100644 src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php diff --git a/src/Symfony/Component/Filesystem/CHANGELOG.md b/src/Symfony/Component/Filesystem/CHANGELOG.md index 4a0755bfe0a83..a1e219f58c704 100644 --- a/src/Symfony/Component/Filesystem/CHANGELOG.md +++ b/src/Symfony/Component/Filesystem/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + + * added `watch()` method to watch filesystem for changes + 5.0.0 ----- diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index a5539fa5eb26f..9f15a5298a192 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -14,6 +14,8 @@ use Symfony\Component\Filesystem\Exception\FileNotFoundException; use Symfony\Component\Filesystem\Exception\InvalidArgumentException; use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Watcher\FileChangeWatcher; +use Symfony\Component\Filesystem\Watcher\INotifyWatcher; /** * Provides basic utility to manipulate the file system. @@ -695,6 +697,26 @@ public function appendToFile(string $filename, $content) } } + /** + * Watches a file or directory for any changes, and calls $callback when any changes are detected. + * + * @param mixed $path The path to watch for changes. Can be a path to a file or directory, iterator or array with paths + * @param callable $callback The callback to execute when a change is detected + * @param float $timeout The idle timeout in milliseconds after which the process will be aborted if there are no changes detected + * + * @throws \InvalidArgumentException|IOException + */ + public function watch($path, callable $callback, float $timeout = null) + { + if (\extension_loaded('inotify')) { + $watcher = new INotifyWatcher(); + } else { + $watcher = new FileChangeWatcher(); + } + + $watcher->watch($path, $callback, $timeout); + } + private function toIterable($files): iterable { return \is_array($files) || $files instanceof \Traversable ? $files : [$files]; diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php new file mode 100644 index 0000000000000..7bdf80e77294e --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Fixtures; + +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; +use Symfony\Component\Filesystem\Watcher\Resource\ResourceInterface; + +/** + * @author Pierre du Plessis + */ +class ChangeFileResource implements ResourceInterface +{ + private $path; + + public function __construct(string $path) + { + $this->path = $path; + } + + public function detectChanges(): array + { + return [new FileChangeEvent($this->path, FileChangeEvent::FILE_CHANGED)]; + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php new file mode 100644 index 0000000000000..bb6a81685535e --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Watcher; + +use Symfony\Component\Filesystem\Tests\FilesystemTestCase; +use Symfony\Component\Filesystem\Tests\Fixtures\ChangeFileResource; +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; +use Symfony\Component\Filesystem\Watcher\FileChangeWatcher; +use Symfony\Component\Filesystem\Watcher\Resource\DirectoryResource; +use Symfony\Component\Filesystem\Watcher\Resource\ResourceInterface; + +class FileSystemWatchTest extends FilesystemTestCase +{ + public function testWatch() + { + $workspace = $this->workspace; + + $locator = new class($workspace) { + private $workspace; + + public function __construct($workspace) + { + $this->workspace = $workspace; + } + + public function locate($path): ?ResourceInterface + { + return new ChangeFileResource($this->workspace.'/foobar.txt'); + } + }; + + $watcher = new FileChangeWatcher(); + $ref = new \ReflectionProperty($watcher, 'locator'); + $ref->setAccessible(true); + $ref->setValue($watcher, $locator); + + $count = 0; + $watcher->watch($this->workspace, function ($file, $code) use (&$count) { + $this->assertSame($this->workspace.'/foobar.txt', $file); + $this->assertSame(FileChangeEvent::FILE_CHANGED, $code); + ++$count; + + if (2 === $count) { + return false; + } + }); + + $this->assertSame(2, $count); + } + + public function testWatchTimeout() + { + $locator = new class() { + public function locate($path): ?ResourceInterface + { + return new DirectoryResource($path); + } + }; + + $watcher = new FileChangeWatcher(); + $ref = new \ReflectionProperty($watcher, 'locator'); + $ref->setAccessible(true); + $ref->setValue($watcher, $locator); + + $start = microtime(true); + $watcher->watch($this->workspace, function ($file, $code) { + }, 500); + + $this->assertTrue(microtime(true) - $start > 0.5); + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/ArrayResourceTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/ArrayResourceTest.php new file mode 100644 index 0000000000000..436aa5c592d42 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/ArrayResourceTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Watcher\Resource; + +use Symfony\Component\Filesystem\Tests\FilesystemTestCase; +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; +use Symfony\Component\Filesystem\Watcher\Resource\ArrayResource; +use Symfony\Component\Filesystem\Watcher\Resource\FileResource; + +class ArrayResourceTest extends FilesystemTestCase +{ + public function testFileChange() + { + $file = $this->workspace.'/foo.txt'; + touch($file); + + $resource = new ArrayResource([new FileResource($file)]); + + $this->assertSame([], $resource->detectChanges()); + + touch($file, time() + 1); + + $this->assertEquals([new FileChangeEvent($file, FileChangeEvent::FILE_CHANGED)], $resource->detectChanges()); + $this->assertSame([], $resource->detectChanges()); + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/DirectoryResourceTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/DirectoryResourceTest.php new file mode 100644 index 0000000000000..a22b3d1849f4f --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/DirectoryResourceTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Watcher\Resource; + +use Symfony\Component\Filesystem\Tests\FilesystemTestCase; +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; +use Symfony\Component\Filesystem\Watcher\Resource\DirectoryResource; + +class DirectoryResourceTest extends FilesystemTestCase +{ + public function testCreateFile() + { + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'foo'; + mkdir($dir); + + $resource = new DirectoryResource($dir); + + $this->assertSame([], $resource->detectChanges()); + + touch($dir.'/foo.txt'); + + $this->assertEquals([new FileChangeEvent($dir.\DIRECTORY_SEPARATOR.'foo.txt', FileChangeEvent::FILE_CREATED)], $resource->detectChanges()); + $this->assertSame([], $resource->detectChanges()); + } + + public function testDeleteFile() + { + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'foo'; + mkdir($dir); + + touch($dir.'/foo.txt'); + touch($dir.'/bar.txt'); + + $resource = new DirectoryResource($dir); + + $this->assertSame([], $resource->detectChanges()); + + unlink($dir.'/foo.txt'); + + $this->assertEquals([new FileChangeEvent($dir.\DIRECTORY_SEPARATOR.'foo.txt', FileChangeEvent::FILE_DELETED)], $resource->detectChanges()); + $this->assertSame([], $resource->detectChanges()); + } + + public function testFileChanges() + { + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'foo'; + mkdir($dir); + + touch($dir.'/foo.txt'); + touch($dir.'/bar.txt'); + + $resource = new DirectoryResource($dir); + + $this->assertSame([], $resource->detectChanges()); + + touch($dir.'/foo.txt', time() + 1); + + $this->assertEquals([new FileChangeEvent($dir.\DIRECTORY_SEPARATOR.'foo.txt', FileChangeEvent::FILE_CHANGED)], $resource->detectChanges()); + $this->assertSame([], $resource->detectChanges()); + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/FileResourceTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/FileResourceTest.php new file mode 100644 index 0000000000000..c901145b2b11a --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/FileResourceTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Watcher\Resource; + +use Symfony\Component\Filesystem\Tests\FilesystemTestCase; +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; +use Symfony\Component\Filesystem\Watcher\Resource\FileResource; + +class FileResourceTest extends FilesystemTestCase +{ + public function testFileChanges() + { + $file = $this->workspace.'/foo.txt'; + touch($file); + + $resource = new FileResource($file); + + $this->assertSame([], $resource->detectChanges()); + + touch($file, time() + 1); + + $this->assertEquals([new FileChangeEvent($file, FileChangeEvent::FILE_CHANGED)], $resource->detectChanges()); + $this->assertSame([], $resource->detectChanges()); + } +} diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/Locator/FileResourceLocatorTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/Locator/FileResourceLocatorTest.php new file mode 100644 index 0000000000000..206ae7c0a05cf --- /dev/null +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/Resource/Locator/FileResourceLocatorTest.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Tests\Watcher\Resource\Locator; + +use Symfony\Component\Filesystem\Tests\FilesystemTestCase; +use Symfony\Component\Filesystem\Watcher\Resource\ArrayResource; +use Symfony\Component\Filesystem\Watcher\Resource\DirectoryResource; +use Symfony\Component\Filesystem\Watcher\Resource\FileResource; +use Symfony\Component\Filesystem\Watcher\Resource\Locator\FileResourceLocator; + +class FileResourceLocatorTest extends FilesystemTestCase +{ + public function testLocateIterator() + { + $locator = new FileResourceLocator(); + + $path = new \ArrayIterator([$this->createFile('foo.txt')]); + + $this->assertEquals(new ArrayResource([new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'foo.txt')]), $locator->locate($path)); + } + + public function testLocateSplFileInfo() + { + $locator = new FileResourceLocator(); + + $path = new \SplFileInfo($this->createFile('foo.txt')); + + $this->assertEquals(new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'foo.txt'), $locator->locate($path)); + } + + public function testFilePath() + { + $locator = new FileResourceLocator(); + + $path = $this->createFile('foo.txt'); + + $this->assertEquals(new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'foo.txt'), $locator->locate($path)); + } + + public function testGlob() + { + $locator = new FileResourceLocator(); + + $this->createFile('bar.txt'); + $this->createFile('foo.txt'); + + $this->assertEquals( + new ArrayResource([new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'bar.txt'), new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'foo.txt')]), + $locator->locate($this->workspace.\DIRECTORY_SEPARATOR.'*.txt') + ); + } + + public function testArray() + { + $locator = new FileResourceLocator(); + + $path = [$this->createFile('foo.txt')]; + + $this->assertEquals(new ArrayResource([new FileResource($this->workspace.\DIRECTORY_SEPARATOR.'foo.txt')]), $locator->locate($path)); + } + + public function testDirectory() + { + $locator = new FileResourceLocator(); + + $dir = $this->createDirecty('foobar'); + + $this->assertEquals(new DirectoryResource($this->workspace.\DIRECTORY_SEPARATOR.'foobar'), $locator->locate($dir)); + } + + private function createFile(string $file) + { + $fullPath = $this->workspace.\DIRECTORY_SEPARATOR.$file; + touch($fullPath); + + return $fullPath; + } + + private function createDirecty(string $dir) + { + $fullPath = $this->workspace.\DIRECTORY_SEPARATOR.$dir; + + mkdir($fullPath, 0777, true); + + return $fullPath; + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php new file mode 100644 index 0000000000000..d929c049f74a9 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher; + +/** + * @author Pierre du Plessis + */ +class FileChangeEvent +{ + public const FILE_CHANGED = 1; + public const FILE_DELETED = 2; + public const FILE_CREATED = 3; + + private $file; + + private $event; + + public function __construct(string $file, int $event) + { + $this->file = $file; + $this->event = $event; + } + + public function getFile(): string + { + return $this->file; + } + + public function getEvent(): int + { + return $this->event; + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php new file mode 100644 index 0000000000000..b13192255613c --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher; + +use Symfony\Component\Filesystem\Watcher\Resource\Locator\FileResourceLocator; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class FileChangeWatcher implements WatcherInterface +{ + private $locator; + + public function __construct() + { + $this->locator = new FileResourceLocator(); + } + + public function watch($path, callable $callback, float $timeout = null) + { + $resource = $this->locator->locate($path); + + if (!$resource) { + throw new \InvalidArgumentException(sprintf('%s is not a valid path to watch', \gettype($path))); + } + + $run = true; + $start = microtime(true); + + while ($run) { + if ($changes = $resource->detectChanges()) { + foreach ($changes as $change) { + $run = false !== $callback($change->getFile(), $change->getEvent()); + } + + $start = microtime(true); + } + + if (null !== $timeout && ($timeout / 1000) <= (microtime(true) - $start)) { + break; + } + + sleep(1); + } + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php new file mode 100644 index 0000000000000..22862a272bf83 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher; + +use Symfony\Component\Filesystem\Exception\IOException; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class INotifyWatcher implements WatcherInterface +{ + public function watch($path, callable $callback, float $timeout = null) + { + $inotifyInit = inotify_init(); + + if (false === $inotifyInit) { + throw new IOException('Unable initialize inotify', 0, null, $path); + } + + stream_set_blocking($inotifyInit, false); + + $isDir = is_dir($path); + + if ($isDir) { + $watchId = inotify_add_watch($inotifyInit, $path, IN_CREATE | IN_DELETE | IN_MODIFY); + } else { + $watchId = inotify_add_watch($inotifyInit, $path, IN_MODIFY); + } + + try { + $read = [$inotifyInit]; + $write = null; + $except = null; + $tvSec = null === $timeout ? null : 0; + $tvUsec = null === $timeout ? null : $timeout * 1000; + + while (true) { + if (0 === stream_select($read, $write, $except, $tvSec, $tvUsec)) { + $read = [$inotifyInit]; + break; + } + + $events = inotify_read($inotifyInit); + + if (false === $events) { + continue; + } + + foreach ($events as $event) { + $code = null; + switch ($event['mask']) { + case IN_CREATE: + $code = FileChangeEvent::FILE_CREATED; + break; + case IN_DELETE: + $code = FileChangeEvent::FILE_DELETED; + break; + case IN_MODIFY: + $code = FileChangeEvent::FILE_CHANGED; + break; + } + + if (false === $callback(($isDir ? $path : '').$event['name'], $code)) { + break; + } + } + } + } finally { + inotify_rm_watch($inotifyInit, $watchId); + fclose($inotifyInit); + } + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php new file mode 100644 index 0000000000000..ccbe16e4c437b --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher\Resource; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class ArrayResource implements ResourceInterface +{ + /** + * @var ResourceInterface[] + */ + private $resources; + + public function __construct(array $resources) + { + $this->resources = $resources; + } + + public function detectChanges(): array + { + $events = []; + + foreach ($this->resources as $resource) { + if ($changed = $resource->detectChanges()) { + $events = array_merge($events, $changed); + } + } + + return $events; + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php new file mode 100644 index 0000000000000..79138e6aefe21 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher\Resource; + +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class DirectoryResource implements ResourceInterface +{ + private $dir; + + /** + * @var FileResource[] + */ + private $files; + + public function __construct(string $dir) + { + $this->dir = $dir; + + $this->files = $this->getFiles(); + } + + public function detectChanges(): array + { + $events = []; + + $currentFiles = $this->getFiles(); + + // Check if any files has been added + foreach (array_keys($currentFiles) as $path) { + if (!isset($this->files[$path])) { + $this->files = $currentFiles; + + $events[] = new FileChangeEvent($path, FileChangeEvent::FILE_CREATED); + } + } + + // Check if any files has been deleted + foreach (array_keys($this->files) as $file) { + if (!isset($currentFiles[$file])) { + $this->files = $currentFiles; + + $events[] = new FileChangeEvent($file, FileChangeEvent::FILE_DELETED); + } + } + + // Check for any changes in files + foreach ($this->files as $file) { + if ($event = $file->detectChanges()) { + $events = array_merge($events, $event); + } + } + + return $events; + } + + private function getFiles(): array + { + $files = []; + + /** @var \SplFileInfo $file */ + foreach (new \RecursiveDirectoryIterator($this->dir, \RecursiveDirectoryIterator::SKIP_DOTS) as $file) { + $path = $file->getRealPath(); + $files[$path] = new FileResource($path); + } + + return $files; + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php new file mode 100644 index 0000000000000..17b28ef572433 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher\Resource; + +use Symfony\Component\Filesystem\Watcher\FileChangeEvent; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class FileResource implements ResourceInterface +{ + private $file; + + private $lastModified; + + public function __construct(string $file) + { + $this->file = $file; + $this->lastModified = filemtime($file); + } + + public function detectChanges(): array + { + if ($this->isModified()) { + $this->updateModifiedTime(); + + return [new FileChangeEvent($this->file, FileChangeEvent::FILE_CHANGED)]; + } + + return []; + } + + private function isModified(): bool + { + clearstatcache(false, $this->file); + + return $this->lastModified < filemtime($this->file); + } + + private function updateModifiedTime(): void + { + $this->lastModified = filemtime($this->file); + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php b/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php new file mode 100644 index 0000000000000..35f9204ee5482 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher\Resource\Locator; + +use Symfony\Component\Filesystem\Watcher\Resource\ArrayResource; +use Symfony\Component\Filesystem\Watcher\Resource\DirectoryResource; +use Symfony\Component\Filesystem\Watcher\Resource\FileResource; +use Symfony\Component\Filesystem\Watcher\Resource\ResourceInterface; +use Symfony\Component\Finder\Finder; + +/** + * @author Pierre du Plessis + * + * @internal + */ +class FileResourceLocator +{ + public function locate($path): ?ResourceInterface + { + if ($path instanceof Finder || $path instanceof \Iterator) { + $path = iterator_to_array($path); + } + + if ($path instanceof \SplFileInfo) { + $path = $path->getRealPath(); + } + + if (\is_array($path)) { + return new ArrayResource(array_map([$this, 'locate'], $path)); + } + + if (\is_string($path)) { + if (is_dir($path)) { + return new DirectoryResource($path); + } + + $paths = glob($path, \defined('GLOB_BRACE') ? GLOB_BRACE : 0); + + if (1 === \count($paths)) { + return new FileResource($paths[0]); + } + + return new ArrayResource(array_map(function ($path) { + return new FileResource($path); + }, $paths)); + } + + return null; + } +} diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/ResourceInterface.php b/src/Symfony/Component/Filesystem/Watcher/Resource/ResourceInterface.php new file mode 100644 index 0000000000000..ec5069d0bf731 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/ResourceInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher\Resource; + +/** + * @author Pierre du Plessis + */ +interface ResourceInterface +{ + public function detectChanges(): array; +} diff --git a/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php b/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php new file mode 100644 index 0000000000000..00007ee3be73a --- /dev/null +++ b/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Watcher; + +/** + * @author Pierre du Plessis + */ +interface WatcherInterface +{ + public function watch($path, callable $callback, float $timeout = null); +} From 78c9969bc949d6236df8691dae86b907b06eecd3 Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Tue, 26 Nov 2019 15:02:57 +0200 Subject: [PATCH 2/5] Add recursive directory handling to inotify watcher --- .../Filesystem/Watcher/INotifyWatcher.php | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php index 22862a272bf83..81c0aa87ae803 100644 --- a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php @@ -31,11 +31,16 @@ public function watch($path, callable $callback, float $timeout = null) stream_set_blocking($inotifyInit, false); $isDir = is_dir($path); + $watchers = []; if ($isDir) { - $watchId = inotify_add_watch($inotifyInit, $path, IN_CREATE | IN_DELETE | IN_MODIFY); + $watchers[] = inotify_add_watch($inotifyInit, $path, IN_CREATE | IN_DELETE | IN_MODIFY); + + foreach ($this->scanPath("$path/*") as $path) { + $watchers[] = inotify_add_watch($inotifyInit, $path, IN_CREATE | IN_DELETE | IN_MODIFY); + } } else { - $watchId = inotify_add_watch($inotifyInit, $path, IN_MODIFY); + $watchers[] = inotify_add_watch($inotifyInit, $path, IN_MODIFY); } try { @@ -77,8 +82,19 @@ public function watch($path, callable $callback, float $timeout = null) } } } finally { - inotify_rm_watch($inotifyInit, $watchId); + foreach ($watchers as $watchId) { + inotify_rm_watch($inotifyInit, $watchId); + } + fclose($inotifyInit); } } + + private function scanPath($path) + { + foreach (glob($path, GLOB_ONLYDIR) as $directory) { + yield $directory; + yield from $this->scanPath("$directory/*"); + } + } } From 86ae2519d4f9c1e93d6f358ae93b89c141a53977 Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Thu, 26 Mar 2020 08:58:06 +0200 Subject: [PATCH 3/5] Make internal classes final and additional return types --- .../Filesystem/Tests/Fixtures/ChangeFileResource.php | 2 +- .../Component/Filesystem/Watcher/FileChangeEvent.php | 2 +- .../Component/Filesystem/Watcher/FileChangeWatcher.php | 4 ++-- src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php | 6 +++--- .../Component/Filesystem/Watcher/Resource/ArrayResource.php | 2 +- .../Filesystem/Watcher/Resource/DirectoryResource.php | 2 +- .../Component/Filesystem/Watcher/Resource/FileResource.php | 2 +- .../Watcher/Resource/Locator/FileResourceLocator.php | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php b/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php index 7bdf80e77294e..4696d23bf01d7 100644 --- a/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php +++ b/src/Symfony/Component/Filesystem/Tests/Fixtures/ChangeFileResource.php @@ -17,7 +17,7 @@ /** * @author Pierre du Plessis */ -class ChangeFileResource implements ResourceInterface +final class ChangeFileResource implements ResourceInterface { private $path; diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php index d929c049f74a9..593fb3fb76857 100644 --- a/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeEvent.php @@ -14,7 +14,7 @@ /** * @author Pierre du Plessis */ -class FileChangeEvent +final class FileChangeEvent { public const FILE_CHANGED = 1; public const FILE_DELETED = 2; diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php index b13192255613c..7e8b13425c506 100644 --- a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php @@ -18,7 +18,7 @@ * * @internal */ -class FileChangeWatcher implements WatcherInterface +final class FileChangeWatcher implements WatcherInterface { private $locator; @@ -27,7 +27,7 @@ public function __construct() $this->locator = new FileResourceLocator(); } - public function watch($path, callable $callback, float $timeout = null) + public function watch($path, callable $callback, float $timeout = null): void { $resource = $this->locator->locate($path); diff --git a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php index 81c0aa87ae803..5c86c162a76ff 100644 --- a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php @@ -18,9 +18,9 @@ * * @internal */ -class INotifyWatcher implements WatcherInterface +final class INotifyWatcher implements WatcherInterface { - public function watch($path, callable $callback, float $timeout = null) + public function watch($path, callable $callback, float $timeout = null): void { $inotifyInit = inotify_init(); @@ -90,7 +90,7 @@ public function watch($path, callable $callback, float $timeout = null) } } - private function scanPath($path) + private function scanPath($path): iterable { foreach (glob($path, GLOB_ONLYDIR) as $directory) { yield $directory; diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php index ccbe16e4c437b..3091c34110855 100644 --- a/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/ArrayResource.php @@ -16,7 +16,7 @@ * * @internal */ -class ArrayResource implements ResourceInterface +final class ArrayResource implements ResourceInterface { /** * @var ResourceInterface[] diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php index 79138e6aefe21..0dd280c1e3df7 100644 --- a/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php @@ -18,7 +18,7 @@ * * @internal */ -class DirectoryResource implements ResourceInterface +final class DirectoryResource implements ResourceInterface { private $dir; diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php index 17b28ef572433..e463340206d3f 100644 --- a/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/FileResource.php @@ -18,7 +18,7 @@ * * @internal */ -class FileResource implements ResourceInterface +final class FileResource implements ResourceInterface { private $file; diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php b/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php index 35f9204ee5482..79ffb3c631e64 100644 --- a/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/Locator/FileResourceLocator.php @@ -22,7 +22,7 @@ * * @internal */ -class FileResourceLocator +final class FileResourceLocator { public function locate($path): ?ResourceInterface { From 23f09e6c379d429bd3d5e00f820932ee4971205c Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Thu, 26 Mar 2020 09:00:48 +0200 Subject: [PATCH 4/5] Fix CS --- src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php | 2 +- src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php index 7e8b13425c506..624acfd398f80 100644 --- a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php @@ -32,7 +32,7 @@ public function watch($path, callable $callback, float $timeout = null): void $resource = $this->locator->locate($path); if (!$resource) { - throw new \InvalidArgumentException(sprintf('%s is not a valid path to watch', \gettype($path))); + throw new \InvalidArgumentException(sprintf('"%s" is not a valid path to watch.', \gettype($path))); } $run = true; diff --git a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php index 5c86c162a76ff..0d413eb844e43 100644 --- a/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/INotifyWatcher.php @@ -25,7 +25,7 @@ public function watch($path, callable $callback, float $timeout = null): void $inotifyInit = inotify_init(); if (false === $inotifyInit) { - throw new IOException('Unable initialize inotify', 0, null, $path); + throw new IOException('Unable initialize inotify.', 0, null, $path); } stream_set_blocking($inotifyInit, false); From 5b3156881fdb91fc0d4a771f55e3e0d6dd4b0e81 Mon Sep 17 00:00:00 2001 From: Pierre du Plessis Date: Tue, 5 May 2020 17:22:54 +0200 Subject: [PATCH 5/5] Update some tests --- .../Filesystem/Tests/Watcher/FileSystemWatchTest.php | 8 +++----- .../Component/Filesystem/Watcher/FileChangeWatcher.php | 2 +- .../Filesystem/Watcher/Resource/DirectoryResource.php | 2 +- .../Component/Filesystem/Watcher/WatcherInterface.php | 9 ++++++++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php b/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php index bb6a81685535e..61b4d7bcee403 100644 --- a/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php +++ b/src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php @@ -39,9 +39,7 @@ public function locate($path): ?ResourceInterface }; $watcher = new FileChangeWatcher(); - $ref = new \ReflectionProperty($watcher, 'locator'); - $ref->setAccessible(true); - $ref->setValue($watcher, $locator); + $watcher->locator = $locator; $count = 0; $watcher->watch($this->workspace, function ($file, $code) use (&$count) { @@ -72,9 +70,9 @@ public function locate($path): ?ResourceInterface $ref->setValue($watcher, $locator); $start = microtime(true); - $watcher->watch($this->workspace, function ($file, $code) { + $watcher->watch($this->workspace, static function ($file, $code) { }, 500); - $this->assertTrue(microtime(true) - $start > 0.5); + $this->assertGreaterThan(0.5, microtime(true) - $start); } } diff --git a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php index 624acfd398f80..864681e91ff56 100644 --- a/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php +++ b/src/Symfony/Component/Filesystem/Watcher/FileChangeWatcher.php @@ -20,7 +20,7 @@ */ final class FileChangeWatcher implements WatcherInterface { - private $locator; + public $locator; public function __construct() { diff --git a/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php index 0dd280c1e3df7..e4c52649e1965 100644 --- a/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php @@ -73,7 +73,7 @@ private function getFiles(): array $files = []; /** @var \SplFileInfo $file */ - foreach (new \RecursiveDirectoryIterator($this->dir, \RecursiveDirectoryIterator::SKIP_DOTS) as $file) { + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->dir, \RecursiveDirectoryIterator::SKIP_DOTS)) as $file) { $path = $file->getRealPath(); $files[$path] = new FileResource($path); } diff --git a/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php b/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php index 00007ee3be73a..c58304252f2f2 100644 --- a/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php +++ b/src/Symfony/Component/Filesystem/Watcher/WatcherInterface.php @@ -11,10 +11,17 @@ namespace Symfony\Component\Filesystem\Watcher; +use Symfony\Component\Filesystem\Exception\IOException; + /** * @author Pierre du Plessis */ interface WatcherInterface { - public function watch($path, callable $callback, float $timeout = null); + /** + * @param mixed $path The path to watch for changes. Can be a path to a file or directory, iterator or array with paths + * @param callable $callback The callback to execute when a change is detected + * @param float $timeout The idle timeout in milliseconds after which the process will be aborted if there are no changes detected + */ + public function watch($path, callable $callback, float $timeout = null): void; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy