Skip to content

Commit 333b9ea

Browse files
committed
Make link handling consistent across native watchers.
1 parent 2fe6286 commit 333b9ea

File tree

5 files changed

+67
-33
lines changed

5 files changed

+67
-33
lines changed

pkgs/watcher/CHANGELOG.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010
exhaustion, "Directory watcher closed unexpectedly", much less likely. The old
1111
implementation which does not use a separate Isolate is available as
1212
`DirectoryWatcher(path, runInIsolateOnWindows: false)`.
13-
- Bug fix: new `DirectoryWatcher` implementation on Linux that fixes various
14-
issues: tracking failure following subdirectory move, incorrect events when
15-
there are changes in a recently-moved subdirectory, incorrect events due to
16-
various situations involving subdirectory moves.
17-
- Bug fix: in `DirectoryWatcher` while listing directories skip symlinks that
18-
lead to a directory that has already been listed. This prevents a severe
19-
performance regression on MacOS and Linux when there are more than a few symlink loops.
13+
- Document behavior on Linux if the system watcher limit is hit.
14+
- Bug fix: native `DirectoryWatcher` implementations now consistently handle
15+
links as files, instead of sometimes reading through them and sometimes
16+
reporting them as files. The polling `DirectoryWatcher` still reads through
17+
links.
18+
- Bug fix: with the polling `DirectoryWatcher`, fix spurious modify event
19+
emitted because of a file delete during polling.
20+
- Bug fix: due to the link handling change, native `DirectoryWatcher` on Linux
21+
and MacOS is no longer affected by a severe performance regression if there
22+
are symlink loops in the watched directory. The polling `DirectoryWatcher`
23+
is fixed to skip already-visited directories to prevent the performance issue
24+
while still reading through links.
2025
- Bug fix: with `DirectoryWatcher` on Windows, the last of a rapid sequence of
2126
modifications in a newly-created directory was sometimes dropped. Make it
2227
reliably report the last modification.
@@ -25,18 +30,19 @@
2530
moved onto `b`, it would be reported as three events: delete `a`, delete `b`,
2631
create `b`. Now it's reported as two events: delete `a`, modify `b`. This
2732
matches the behavior of the Linux and MacOS watchers.
28-
- Bug fix: with `DirectoryWatcher` on Windows, new links to direcories were
33+
- Bug fix: with `DirectoryWatcher` on Windows, new links to directories were
2934
sometimes incorrectly handled as actual directories. Now they are reported
3035
as files, matching the behavior of the Linux and MacOS watchers.
36+
- Bug fix: new `DirectoryWatcher` implementation on Linux that fixes various
37+
issues: tracking failure following subdirectory move, incorrect events when
38+
there are changes in a recently-moved subdirectory, incorrect events due to
39+
various situations involving subdirectory moves.
3140
- Bug fix: with `DirectoryWatcher` on MacOS, fix events for changes in new
3241
directories: don't emit duplicate ADD, don't emit MODIFY without ADD.
33-
- Bug fix: with `PollingDirectoryWatcher`, fix spurious modify event emitted
34-
because of a file delete during polling.
3542
- Bug fix: with `FileWatcher` on MacOS, a modify event was sometimes reported if
3643
the file was created immediately before the watcher was created. Now, if the
3744
file exists when the watcher is created then this modify event is not sent.
3845
This matches the Linux native and polling (Windows) watchers.
39-
- Document behavior on Linux if the system watcher limit is hit.
4046

4147
## 1.1.4
4248

pkgs/watcher/lib/src/directory_watcher/directory_list.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ extension DirectoryRobustRecursiveListing on Directory {
1515
/// These can arise from concurrent file-system modification.
1616
///
1717
/// See [listRecursively] for how symlinks are handled.
18-
Stream<FileSystemEntity> listRecursivelyIgnoringErrors() {
19-
return listRecursively()
18+
Stream<FileSystemEntity> listRecursivelyIgnoringErrors(
19+
{bool followLinks = true}) {
20+
return listRecursively(followLinks: followLinks)
2021
.ignoring<PathNotFoundException>()
2122
.ignoring<PathAccessException>();
2223
}
2324

2425
/// Lists the directory recursively.
2526
///
26-
/// This is like `Directory.list(recursive: true)`, but handles symlinks like
27-
/// `find -L` to avoid a performance issue with symbolic link cycles.
27+
/// If you pass `followLinks: false` then this exactly calls
28+
/// `Directory.list(recursive: true, followLinks: false)`. If not, it is like
29+
/// `Directory.list(recursive: true)`, but handles symlinks like `find -L` to
30+
/// avoid a performance issue with symbolic link cycles.
2831
///
2932
/// See: https://github.com/dart-lang/sdk/issues/61407.
3033
///
@@ -33,8 +36,11 @@ extension DirectoryRobustRecursiveListing on Directory {
3336
/// symlink-resolved paths.
3437
///
3538
/// Skipped links to directories are not mentioned in the directory listing.
36-
Stream<FileSystemEntity> listRecursively() =>
37-
_DirectoryTraversal(this).listRecursively();
39+
Stream<FileSystemEntity> listRecursively({bool followLinks = true}) {
40+
return followLinks
41+
? _DirectoryTraversal(this).listRecursively()
42+
: list(recursive: true, followLinks: false);
43+
}
3844
}
3945

4046
/// A recursive directory listing algorithm that follows symlinks carefully.

pkgs/watcher/lib/src/directory_watcher/mac_os.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ class _MacOSDirectoryWatcher
151151
case EventType.createDirectory:
152152
if (_files.containsDir(path)) continue;
153153

154-
var stream = Directory(path).listRecursivelyIgnoringErrors();
154+
var stream = Directory(path)
155+
.listRecursivelyIgnoringErrors(followLinks: false);
155156
var subscription = stream.listen((entity) {
156157
if (entity is Directory) return;
157158
if (_files.contains(entity.path)) return;
@@ -336,7 +337,8 @@ class _MacOSDirectoryWatcher
336337

337338
_files.clear();
338339
var completer = Completer<void>();
339-
var stream = Directory(path).listRecursivelyIgnoringErrors();
340+
var stream =
341+
Directory(path).listRecursivelyIgnoringErrors(followLinks: false);
340342
_initialListSubscription = stream.listen((entity) {
341343
if (entity is! Directory) _files.add(entity.path);
342344
}, onError: _emitError, onDone: completer.complete, cancelOnError: true);

pkgs/watcher/lib/src/directory_watcher/windows.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ class WindowsManuallyClosedDirectoryWatcher
229229
_files.add(path);
230230

231231
case EventType.createDirectory:
232-
final stream = Directory(path).listRecursivelyIgnoringErrors();
232+
final stream =
233+
Directory(path).listRecursivelyIgnoringErrors(followLinks: false);
233234
final subscription = stream.listen((entity) {
234235
if (entity is Directory) return;
235236
if (_files.contains(entity.path)) return;
@@ -375,7 +376,8 @@ class WindowsManuallyClosedDirectoryWatcher
375376

376377
_files.clear();
377378
var completer = Completer<void>();
378-
var stream = Directory(path).listRecursivelyIgnoringErrors();
379+
var stream =
380+
Directory(path).listRecursivelyIgnoringErrors(followLinks: false);
379381
void handleEntity(FileSystemEntity entity) {
380382
if (entity is! Directory) _files.add(entity.path);
381383
}

pkgs/watcher/test/directory_watcher/link_tests.dart

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ void _linkTests({required bool isNative}) {
6666
await startWatcher(path: 'links');
6767
writeFile('targets/a.target', contents: 'modified');
6868

69-
// TODO(davidmorgan): reconcile differences.
69+
// Native watchers treat links as files, polling watcher polls through them.
7070
if (isNative) {
7171
await expectNoEvents();
7272
} else {
@@ -83,7 +83,7 @@ void _linkTests({required bool isNative}) {
8383

8484
deleteFile('targets/a.target');
8585

86-
// TODO(davidmorgan): reconcile differences.
86+
// Native watchers treat links as files, polling watcher polls through them.
8787
if (isNative) {
8888
await expectNoEvents();
8989
} else {
@@ -115,7 +115,7 @@ void _linkTests({required bool isNative}) {
115115
target: 'targets/a.targetdir',
116116
unawaitedAsync: true);
117117

118-
// TODO(davidmorgan): reconcile differences.
118+
// Native watchers treat links as files, polling watcher polls through them.
119119
if (isNative) {
120120
await expectAddEvent('links/a.link');
121121
} else {
@@ -137,7 +137,7 @@ void _linkTests({required bool isNative}) {
137137
target: 'targets/a.targetdir',
138138
unawaitedAsync: true);
139139

140-
// TODO(davidmorgan): reconcile differences.
140+
// Native watchers treat links as files, polling watcher polls through them.
141141
if (isNative) {
142142
await expectAddEvent('links/a.link');
143143
} else {
@@ -154,10 +154,28 @@ void _linkTests({required bool isNative}) {
154154

155155
writeFile('targets/a.targetdir/a.txt');
156156

157-
if (!isNative) {
157+
// Native watchers treat links as files, polling watcher polls through them.
158+
if (isNative) {
159+
await expectNoEvents();
160+
} else {
158161
await expectAddEvent('links/a.link/a.txt');
162+
}
163+
});
164+
165+
test('notifies when a file is added to a newly linked directory', () async {
166+
createDir('targets');
167+
createDir('links');
168+
createDir('targets/a.targetdir');
169+
await startWatcher(path: 'links');
170+
171+
writeLink(link: 'links/a.link', target: 'targets/a.targetdir');
172+
writeFile('targets/a.targetdir/a.txt');
173+
174+
// Native watchers treat links as files, polling watcher polls through them.
175+
if (isNative) {
176+
await expectAddEvent('links/a.link');
159177
} else {
160-
await expectNoEvents();
178+
await expectAddEvent('links/a.link/a.txt');
161179
}
162180
});
163181

@@ -174,8 +192,8 @@ void _linkTests({required bool isNative}) {
174192

175193
renameDir('links', 'watched/links');
176194

177-
// TODO(davidmorgan): reconcile differences.
178-
if (isNative && Platform.isLinux) {
195+
// Native watchers treat links as files, polling watcher polls through them.
196+
if (isNative) {
179197
await expectAddEvent('watched/links/a.link');
180198
} else {
181199
await expectAddEvent('watched/links/a.link/a.txt');
@@ -197,8 +215,8 @@ void _linkTests({required bool isNative}) {
197215

198216
renameDir('links', 'watched/links');
199217

200-
// TODO(davidmorgan): reconcile differences.
201-
if (isNative && Platform.isLinux) {
218+
// Native watchers treat links as files, polling watcher polls through them.
219+
if (isNative) {
202220
await expectAddEvent('watched/links/a.link');
203221
} else {
204222
await expectAddEvent('watched/links/a.link/a.txt');
@@ -223,8 +241,8 @@ void _linkTests({required bool isNative}) {
223241

224242
renameDir('links', 'watched/links');
225243

226-
// TODO(davidmorgan): reconcile diffences.
227-
if (isNative && Platform.isLinux) {
244+
// Native watchers treat links as files, polling watcher polls through them.
245+
if (isNative) {
228246
await expectAddEvent('watched/links/a.link');
229247
} else {
230248
await expectAddEvent('watched/links/a.link/a.txt');

0 commit comments

Comments
 (0)