Skip to content

Commit 09a9bd3

Browse files
authored
Make link handling consistent across native watchers. (#2261)
1 parent 2fe6286 commit 09a9bd3

File tree

6 files changed

+69
-37
lines changed

6 files changed

+69
-37
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/end_to_end_tests.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import 'file_changer.dart';
2121
/// until a failure, it outputs a log which can be turned into a test case here.
2222
void endToEndTests() {
2323
// Random test to cover a wide range of cases.
24-
test('end to end test: random', timeout: const Timeout(Duration(minutes: 5)),
24+
test('end to end test: random', timeout: const Timeout(Duration(minutes: 10)),
2525
() async {
2626
await runTest(name: 'random', repeats: 100);
2727
});
@@ -30,7 +30,7 @@ void endToEndTests() {
3030
for (final testCase in testCases) {
3131
test('end to end test: ${testCase.name}',
3232
timeout: const Timeout(Duration(minutes: 5)), () async {
33-
await runTest(name: testCase.name, replayLog: testCase.log, repeats: 100);
33+
await runTest(name: testCase.name, replayLog: testCase.log, repeats: 50);
3434
}, skip: testCase.skipOnLinux && Platform.isLinux);
3535
}
3636
}

pkgs/watcher/test/directory_watcher/link_tests.dart

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'dart:io';
6-
75
import 'package:test/test.dart';
86

97
import '../utils.dart';
@@ -66,7 +64,7 @@ void _linkTests({required bool isNative}) {
6664
await startWatcher(path: 'links');
6765
writeFile('targets/a.target', contents: 'modified');
6866

69-
// TODO(davidmorgan): reconcile differences.
67+
// Native watchers treat links as files, polling watcher polls through them.
7068
if (isNative) {
7169
await expectNoEvents();
7270
} else {
@@ -83,7 +81,7 @@ void _linkTests({required bool isNative}) {
8381

8482
deleteFile('targets/a.target');
8583

86-
// TODO(davidmorgan): reconcile differences.
84+
// Native watchers treat links as files, polling watcher polls through them.
8785
if (isNative) {
8886
await expectNoEvents();
8987
} else {
@@ -115,7 +113,7 @@ void _linkTests({required bool isNative}) {
115113
target: 'targets/a.targetdir',
116114
unawaitedAsync: true);
117115

118-
// TODO(davidmorgan): reconcile differences.
116+
// Native watchers treat links as files, polling watcher polls through them.
119117
if (isNative) {
120118
await expectAddEvent('links/a.link');
121119
} else {
@@ -137,7 +135,7 @@ void _linkTests({required bool isNative}) {
137135
target: 'targets/a.targetdir',
138136
unawaitedAsync: true);
139137

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

155153
writeFile('targets/a.targetdir/a.txt');
156154

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

@@ -174,8 +190,8 @@ void _linkTests({required bool isNative}) {
174190

175191
renameDir('links', 'watched/links');
176192

177-
// TODO(davidmorgan): reconcile differences.
178-
if (isNative && Platform.isLinux) {
193+
// Native watchers treat links as files, polling watcher polls through them.
194+
if (isNative) {
179195
await expectAddEvent('watched/links/a.link');
180196
} else {
181197
await expectAddEvent('watched/links/a.link/a.txt');
@@ -197,8 +213,8 @@ void _linkTests({required bool isNative}) {
197213

198214
renameDir('links', 'watched/links');
199215

200-
// TODO(davidmorgan): reconcile differences.
201-
if (isNative && Platform.isLinux) {
216+
// Native watchers treat links as files, polling watcher polls through them.
217+
if (isNative) {
202218
await expectAddEvent('watched/links/a.link');
203219
} else {
204220
await expectAddEvent('watched/links/a.link/a.txt');
@@ -223,8 +239,8 @@ void _linkTests({required bool isNative}) {
223239

224240
renameDir('links', 'watched/links');
225241

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

0 commit comments

Comments
 (0)