-
Notifications
You must be signed in to change notification settings - Fork 71
Don't follow symlinks when creating a DirectoryWatcher. #2167
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
base: main
Are you sure you want to change the base?
Conversation
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.
Code Review
This pull request introduces a breaking change to stop DirectoryWatcher
from following symbolic links when scanning directories. This is achieved by setting followLinks: false
in all Directory.list
calls across the different platform implementations. The change is accompanied by a major version bump to 2.0.0, an updated changelog, and a new test to verify the behavior. The changes look correct and well-implemented. I have one critical comment regarding the new test code which appears to have a compilation error.
test('are not watched when not enabled', () async { | ||
writeFile('dir/sub/a.txt', contents: 'a'); | ||
writeFile('sibling/sub/b.txt', contents: '1'); | ||
writeSymlink('dir/linked', target: 'sibling'); | ||
await startWatcher(path: 'dir', followLinks: false); |
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 call to startWatcher
on line 349 includes a followLinks
parameter, but the startWatcher
function (defined in test/utils.dart
) does not declare this parameter, which will cause a compilation error. Since not following symlinks is now the default and only behavior, this parameter is unnecessary.
Additionally, the test name on line 345, "are not watched when not enabled", could be updated to more accurately reflect the new default behavior. A name like "does not follow directory symlinks" would be clearer.
I've provided a suggestion that addresses both points.
test('are not watched when not enabled', () async { | |
writeFile('dir/sub/a.txt', contents: 'a'); | |
writeFile('sibling/sub/b.txt', contents: '1'); | |
writeSymlink('dir/linked', target: 'sibling'); | |
await startWatcher(path: 'dir', followLinks: false); | |
test('does not follow directory symlinks', () async { | |
writeFile('dir/sub/a.txt', contents: 'a'); | |
writeFile('sibling/sub/b.txt', contents: '1'); | |
writeSymlink('dir/linked', target: 'sibling'); | |
await startWatcher(path: 'dir'); |
PR HealthChangelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with
Coverage
|
File | Coverage |
---|---|
pkgs/watcher/lib/src/directory_watcher/linux.dart | 💔 0 % ⬇️ 100 % |
pkgs/watcher/lib/src/directory_watcher/mac_os.dart | 💔 0 % ⬇️ NaN % |
pkgs/watcher/lib/src/directory_watcher/polling.dart | 💔 0 % ⬇️ 100 % |
pkgs/watcher/lib/src/directory_watcher/windows.dart | 💔 0 % ⬇️ NaN % |
This check for test coverage is informational (issues shown here will not fail the PR).
This check can be disabled by tagging the PR with skip-coverage-check
.
License Headers ✔️
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files |
---|
no missing headers |
All source files should start with a license header.
Unrelated files missing license headers
Files |
---|
pkgs/bazel_worker/benchmark/benchmark.dart |
pkgs/bazel_worker/example/client.dart |
pkgs/bazel_worker/example/worker.dart |
pkgs/benchmark_harness/integration_test/perf_benchmark_test.dart |
pkgs/boolean_selector/example/example.dart |
pkgs/clock/lib/clock.dart |
pkgs/clock/lib/src/clock.dart |
pkgs/clock/lib/src/default.dart |
pkgs/clock/lib/src/stopwatch.dart |
pkgs/clock/lib/src/utils.dart |
pkgs/clock/test/clock_test.dart |
pkgs/clock/test/default_test.dart |
pkgs/clock/test/stopwatch_test.dart |
pkgs/clock/test/utils.dart |
pkgs/coverage/lib/src/coverage_options.dart |
pkgs/html/example/main.dart |
pkgs/html/lib/dom.dart |
pkgs/html/lib/dom_parsing.dart |
pkgs/html/lib/html_escape.dart |
pkgs/html/lib/parser.dart |
pkgs/html/lib/src/constants.dart |
pkgs/html/lib/src/encoding_parser.dart |
pkgs/html/lib/src/html_input_stream.dart |
pkgs/html/lib/src/list_proxy.dart |
pkgs/html/lib/src/query_selector.dart |
pkgs/html/lib/src/token.dart |
pkgs/html/lib/src/tokenizer.dart |
pkgs/html/lib/src/treebuilder.dart |
pkgs/html/lib/src/utils.dart |
pkgs/html/test/dom_test.dart |
pkgs/html/test/parser_feature_test.dart |
pkgs/html/test/parser_test.dart |
pkgs/html/test/query_selector_test.dart |
pkgs/html/test/selectors/level1_baseline_test.dart |
pkgs/html/test/selectors/level1_lib.dart |
pkgs/html/test/selectors/selectors.dart |
pkgs/html/test/support.dart |
pkgs/html/test/tokenizer_test.dart |
pkgs/html/test/trie_test.dart |
pkgs/html/tool/generate_trie.dart |
pkgs/pubspec_parse/test/git_uri_test.dart |
pkgs/stack_trace/example/example.dart |
pkgs/watcher/test/custom_watcher_factory_test.dart |
pkgs/yaml_edit/example/example.dart |
This check can be disabled by tagging the PR with skip-license-check
.
There appears to be a problem with bumping the version number, and I don't know how to fix it. Any advice? |
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.
This looks great, thank you!
writeFile('sibling/sub/b.txt', contents: '2'); | ||
// Ensure that any events for the first modification arrive before the | ||
// events for the second modification. | ||
await pumpEventQueue(); |
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.
Maybe add a comment that there is expected to be no modification event for dir/linked/b.txt
? Does the test API have a way to test that explicitly?
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.
Comment added.
I don't know whether the test actually confirms that. It kind of looks like it's just expecting that there's a modify event for a.txt
somewhere in the stream, but I can't find any way to ensure that there's only one event, or even that there's no event of a given kind.
await pumpEventQueue(); | ||
writeFile('dir/sub/a.txt', contents: 'a'); | ||
await expectModifyEvent('dir/sub/a.txt'); | ||
}); |
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 also be interesting to test whether the process of creating or deleting a symlink inside a watched directory fires a notification. I would expect it to, and it might be worth pinning that down. But it's not essential for this PR.
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 that should be tested and described with this release, since if there are any inconsistencies we might need another breaking release to fix them.
One possible behaviour is that within a watched directory, changes to symlinks to files/directories are reported exactly as changes to files would be.
Another possible behaviour is that they are always ignored.
I think either one is fine, as long as it is consistent across all three platforms + documented.
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 will add a test.
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.
Thanks! This should be a nice improvement to the package.
Made some suggestions.
void writeSymlink(String path, {required String target}) { | ||
var fullPath = p.join(d.sandbox, path); | ||
|
||
// Create any needed subdirectories. |
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 this can be just
createSync(target, recursive: true)
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're right. I didn't do that because I was copying the pattern in writeFile
, trying to keep a consistent style.
Directory(p.join(d.sandbox, path)).deleteSync(recursive: true); | ||
} | ||
|
||
/// Schedules writing a symlink in the sandbox at [path] that links to [target]. |
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 "schedules" refers to tracking of modification times which is not done here.
Possibly it should be if we also test behaviour of symlinking files.
If not then "Schedules writing" can be replaced with just "Writes".
@@ -1,3 +1,12 @@ | |||
## 2.0.0 | |||
|
|||
- Changes the behavior of the `DirectoryWatcher` so that it no longer follows |
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 make sense to also mention the bug fix aspect of the change
Bug fix: fix DirectoryWatcher
bad performance when there are cyclic symlinks in the directory being watched.
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.
Done
- Changes the behavior of the `DirectoryWatcher` so that it no longer follows | ||
symlinks. On Linux, but not on MacOS or Windows, it used to watch directories | ||
to which there was a symlink within the directory being watched. It no longer | ||
does that. Code that depends on that behavior will need to be updated so that |
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 would prefer people let us know if that is what they wanted, since we already have most of the implementation and could add it back.
So maybe
If you were relying on that behavior, please file an issue.
?
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.
Done
await pumpEventQueue(); | ||
writeFile('dir/sub/a.txt', contents: 'a'); | ||
await expectModifyEvent('dir/sub/a.txt'); | ||
}); |
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 that should be tested and described with this release, since if there are any inconsistencies we might need another breaking release to fix them.
One possible behaviour is that within a watched directory, changes to symlinks to files/directories are reported exactly as changes to files would be.
Another possible behaviour is that they are always ignored.
I think either one is fine, as long as it is consistent across all three platforms + documented.
}); | ||
}); | ||
group('symlinks', () { | ||
test('are not watched when not enabled', () async { |
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.
Update the name? There is no longer a choice whether to enable watching symlinks.
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.
Done
@@ -1,3 +1,12 @@ | |||
## 2.0.0 |
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.
Please also describe how symlinks are handled in the DirectoryWatcher
dartdoc.
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.
Done
Thanks for the great feedback! I'm not sure how useful it would be for me to address the comments before we address the bigger issue, which is that it doesn't appear to be possible to create a breaking change version of this package. See https://github.com/dart-lang/tools/actions/runs/17842069555/job/50733710413?pr=2167 for details. I don't know how to proceed, so advice is more than welcome. |
Sorry, I missed that bit. I think a dependency override forcing the |
Where would this dependency override be added? |
Re: the CI failures - Create a new
If that doesn't work, perhaps:
|
I didn't know that there was support for such a file. How widely used / publicized is this file? I ask because neither the analyzer nor the server has any knowledge of this file either, so none of the support for the Is it safe to publish a version of the package while this file exists? That is, will
I'm not sure how that will help given that it exactly matches the dependency in the |
Not sure about You don't need to publish with a dependency override. Once the version is committed, we can hop over to |
You can publish with a pubspec_overrides.yaml, however I think the near term goals are more getting the CI green with this PR, then testing the changes in the sdk and google3 (and w/
👍 |
Unfortunately, none of those suggestions worked. When I added dependency_overrides:
test: ^1.16.6 to a When I replaced the content of the dependency_overrides:
watcher:
path: . I got the message "A package may not list itself as a dependency." The same thing happened when I added the Any other ideas of how to proceed? |
Would it work to revert the version to |
Or, can we just widen the constraints on the |
Looks like the analyzer needs an override too:
|
That appears to have solved the problem. Thanks! |
I don't see your changes, I think you need to push them? |
Yes, I failed to push them because of technical issues. I'll have to figure that out first.
Ok, I've looked into this, and it's a mess, so I'd like some input. The watcher code assumes that every file system entity is either a My assumption was that it would therefore treat symlinks as if they were just files (which they mostly are), but that turns out to not be the case. I think that's because the underlying OS's aren't treating them like files in all cases. I don't know anything about the native file watching APIs, so I asked Gemini and it summarized the behavior with the following table:
In other words, it appears to be the case that the operating systems don't always do the same thing, possibly based on the version of the OS or possibly based on other factors (kind of file system, etc.). I tried writing a test, and on macOS it appears that changing the symlink's target produces a 'remove' event, but no 'add' event.
If we're happy with that (and I think that will be fine for the analysis server), then I can probably test for If we think they need to be reported, then this might be more than I currently know how to do. |
It looks to me like the test expectations are by default applied in order. So if the test causes an event and expects it, it implicitly asserts that there were no unexpected events before that. I think what we need is
|
I forgot to explicitly state this, but if we do nothing then the behavior for symlink files will be unchanged. That is, before, if there was a directory D1 that contained a symlink to a directory D2, and the symlink was changed to link to D3, then we'd still get an OS level event for the change and we'd sometimes convert that to a So unless we explicitly block events for symlinks (that is, if we land what's there today) we wouldn't be changing that behavior at all. |
In any case the first step is to cover any undefined behavior that we know about with tests, so it becomes defined, then we can go from there to deciding what it should be for the release, and documenting and dealing with any changes. |
Respectfully, I'd like to challenge that. If I were taking ownership of this package (which I am absolutely not doing), then yes, it would be appropriate to write tests for previously undefined and untested behavior. But it isn't my responsibility to clean up the package. My responsibility is to solve the known performance problem in the analysis server and then look for other performance issues that I can solve. I think it's fine if, as a result of that work, the package isn't in worse shape than when I started fixing the real issue. In other words, I don't think it's a problem for the package's behavior around symlinks to remain undefined and untested as long as it's unchanged (which it is). On the contrary, I think that given the relative priorities of improving the performance of the server and improving the handling of symlinks in this package, it would be irresponsible for me to spend more time on this PR than is absolutely necessary. |
I think this code is very high risk: it's genuinely cross platform, and by the nature of the package bugs can cause worse user experiences without ever failing in a way that would lead to a bug report being filed. Given this, and how critical it is to I chatted with Johnni and he's up for the Model team owning the package, and I'll be happy to take on the work here. I am assuming there will be no objection to someone taking ownership :) but I will kick off that discussion separately to confirm. For this change, I agree the current state of the tests is insufficient, and can take over adding them so it's possible to make progress. |
I agree that it would be good for the watcher package to be owned, and I'd be delighted for the dart model team to own it. I also have no objections to you taking over this PR (which is what I think you're suggesting). I'll send you a |
Sounds good to me--thanks! |
Thanks for putting all the work into it so far @bwilkerson, and thanks @davidmorgan for taking ownership. |
No description provided.