-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Filesystem] Add watch method to watch filesystem for changes #31462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
0fdfa5a
to
7ef4b10
Compare
ref #4605, perhaps some relevant info is there still. |
src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php
Outdated
Show resolved
Hide resolved
@pierredup up to finish this one? |
7ef4b10
to
4d7dae5
Compare
cce66d2
to
1cbfd18
Compare
Appveyor failure is unrelated, this is ready for review |
1cbfd18
to
2a556d3
Compare
418d1d6
to
5d6ada4
Compare
It is possible to merge this PR in near feature ? |
* | ||
* @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 time in milliseconds to wait between checking for changes (defaults to 1000 when inotify is not available) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I wasn't clear enough in my suggestion here: I meant an idle timeout - ie if nothing happens for that duration, we should quit the while loop in implementations.
But as long as something happens during the interval, we should continue looping.
By default or null
should mean : forever (as is does now already - configuring the pause timeout doesn't provide any useful behavior from the outside)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify how is such timeout useful? As a user that looks highly undesired for watcher to terminate even after one hour of no changes. I wish watcher to run until I terminate it. More useful would be original interpretation of timeout.
4131719
to
73d84a2
Compare
If the feature is accepted and finished on time, it will be merged in 5.1, yes For what is worth, I'm 👍 for the feature. But I did not read the code in deep. I just saw you are using inotify, and this is the way to go BTW, did you see https://github.com/flint/Lurker ? may be there is some bits of code to grab |
I have just pushed the last changes, so this is ready for another round of review.
I did borrow some ideas from there yes. |
bdcd781
to
de386b2
Compare
Final comments have been addressed and I rebased from master. This is ready again for review. Travis failure is unrelated. |
868c3f5
to
fc8c244
Compare
fc8c244
to
c7036c9
Compare
Ping @nicolas-grekas, I would appreciate another review here and hope to get this merged into 5.1 |
c7036c9
to
23f09e6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. You always come up with most interesting features to contribute. I had this on my to-do list for a while and finally got around to properly review it (didn't test it though). Unfortunately, this doesn't look ready.
src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Filesystem/Tests/Watcher/FileSystemWatchTest.php
Outdated
Show resolved
Hide resolved
touch($file, time() + 1); | ||
|
||
$this->assertEquals([new FileChangeEvent($file, FileChangeEvent::FILE_CHANGED)], $resource->detectChanges()); | ||
$this->assertSame([], $resource->detectChanges()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are testing here behaviour of FileResource
, not ArrayResource
, which is confusing. Changing return values on subsequent executions of detectChanges
is FileResource specific defined behaviour. If these are unit tests, they should test only behaviour defined by ArrayResource itself, or be renamed to something else, since they aren't unit tests.
Same applies to all of the test classes in this namespace below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are testing here behaviour of FileResource, not ArrayResource
I don't understand this part. This is checking that ArrayResource
correctly returns the detected changes for all the resources defined. The only other option is to create mocks for the changed events here, but I prefer relying on concrete implementations rather than mocking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I explained it at
Changing return values on subsequent executions of detectChanges is FileResource specific defined behaviour.
This is not something ArrayResource does, so this test case shouldn't explicitly test it. If you want this to be unit test, then indeed you need to provide mock instead of FileResource and assert that ArrayResource calls it when expected, because that's all it does. It won't return different results on subsequent calls for different implementations of decorated resource instance.
Or we can have generic test case like I explain in some other comment, then this is no longer important because there will be no ArrayResource specific test code of these, it will just test these things for consistency sake.
|
||
touch($file, time() + 1); | ||
|
||
$this->assertEquals([new FileChangeEvent($file, FileChangeEvent::FILE_CHANGED)], $resource->detectChanges()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying same code over and over is a sign it needs extraction. Reducing PR size is also needed to incentive people to review it, larger PRs put people off reviewing it. I would suggest to have abstract test class where most of this code is defined once only and subclasses should define only target class and file location - in other words, only code that differs. Another side benefit is guarantee that all subclasses are tested for all operations. Currently it seems only DirectoryResource is fully tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reducing PR size is also needed to incentive people to review it, larger PRs put people off reviewing it
For some people maybe, but how do other big features than make its way into the codebase?
Abstracting a single line to be re-used does not seem to have a big benefit.
Currently it seems only DirectoryResource is fully tested.
How so? If you think there are test cases missing, let me know and I will gladly add more tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Abstracting a single line to be re-used does not seem to have a big benefit.
It's not a single line. It's everything except two lines of code like I mentioned at
subclasses should define only target class and file location
Pretty much everything else is currently copy pasted
How so? If you think there are test cases missing, let me know and I will gladly add more tests
testCreateFile, testDeleteFile are missing from FileResourceTest, ArrayResourceTest. I don't want more code to be copy pasted though.
if ($isDir) { | ||
$watchers[] = inotify_add_watch($inotifyInit, $path, IN_CREATE | IN_DELETE | IN_MODIFY); | ||
|
||
foreach ($this->scanPath("$path/*") as $path) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder how does this work in https://github.com/flint/Lurker, because upon quick inspection, not only I don't see this mechanism there, but also no inotify_add_watch
. I'm wondering this because recursively iterating over all folders and creating inotify watcher for each doesn't look efficient. I expect startup times in any moderately sized projects to be long doing it like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not only I don't see this mechanism there, but also no inotify_add_watch
inotify_add_watch
is used here
Checking recursively can maybe be turned off by default and enabled by either a flag or a new method watchRecursive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inotify_add_watch is used here
Thanks, GH didn't find it for me.
Checking recursively can maybe be turned off by default and enabled by either a flag or a new method watchRecursive
I don't mean to have ability to turn recursivity off, I am just trying to figure if there isn't smarter way to go around handling recursivity. First thing we need to clear up is: Is it absolute requirement for inotify PHP extension to create watcher for every subdirectory, instead of just parent directory?
src/Symfony/Component/Filesystem/Watcher/Resource/DirectoryResource.php
Outdated
Show resolved
Hide resolved
// Check if any files has been added | ||
foreach (array_keys($currentFiles) as $path) { | ||
if (!isset($this->files[$path])) { | ||
$this->files = $currentFiles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how is DirectoryResource designed to work. If there were multiple changes, e.g. file created and another file deleted, file creation will be detected by first foreach, then $this->files
will be overwritten and following loops will no longer detect other changes.
|
||
$paths = glob($path, \defined('GLOB_BRACE') ? GLOB_BRACE : 0); | ||
|
||
if (1 === \count($paths)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we care here if array has single path and not let ArrayResource do it's job? ArrayResource surely works with single value array being passed in too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small optimization. If we know there is only one file, then we don't need the loop in ArrayResource and can just check the file directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but I would say super premature optimization. This isn't really worth it for increase of cyclomatic complexity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Promising pull request! Can't wait to release this new feature 🚀 .
$events = array_merge($events, $event); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you have to move all $this->files = $currentFiles
here to avoid overwriting initial state and miss changes.
* | ||
* @throws \InvalidArgumentException|IOException | ||
*/ | ||
public function watch($path, callable $callback, float $timeout = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to define events we want to listen (by default creating, changing, and deleting).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The events are passed to the callback function, so you can skip certain events already by adding an if
statement in the callback
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some instructions take time to be executed and limit changes we want to watch can help to reduce useless time. Also, the change must in the watcher code to do it.
* | ||
* @internal | ||
*/ | ||
final class INotifyWatcher implements WatcherInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No tests for this watcher?
It took me some time to have a look at this PR. Sorry for that but I wasn't sure how I felt about it. On one hand, that would be great to have something like this in Symfony, but in the other hand, it's like opening a Pandora box. I've worked with similar systems in other languages, and it's really hard to get ti right and cross platform. The fallback in PHP is going to be very slow/CPU intensive and I would not recommend it except in very specific conditions. inotify is only for Linux, so Windows and MacOS will always use the fallback, which is a big problem for me. Adding support for Windows and MacOS is not trivial either as With all that said, and after thinking about it a lot, I think closing it is the best option for now as it's not ready for prime time yet. |
Add a watch method to the Filesystem component to monitor the filesystem for changes, and executing a callback function whenever a change is detected.
The watch method can take any parameter and uses a resource locator to transform a path to a resource. The resource can then be monitored for any changes. When a directory is monitored, you can get changed events when a file in the directory changes, when a new file is created or when an existing file is deleted. It also tries to make usage of Inotify is the extension is installed.
Example usage:
If you want to bail out of the watch events, the callback can return
false
to stop the process.