Skip to content

Commit 08515a4

Browse files
authored
Ignore Path{NotFound,Access}Exception in more places. (#2183)
Directory.list() can fail with these errors due to concurrent file system modifications. Suppress these errors uniformly across the code base.
1 parent de93f01 commit 08515a4

File tree

7 files changed

+60
-31
lines changed

7 files changed

+60
-31
lines changed

pkgs/watcher/CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## 1.1.4-wip
22

3-
- Improve handling of subdirectories on Linux: ignore `PathNotFoundException`
4-
due to subdirectory deletion during watch setup, instead of raising it on the
5-
event stream.
3+
- Improve handling of subdirectories: ignore `PathNotFoundException` due to
4+
subdirectory deletion racing with watcher internals, instead of raising
5+
it on the event stream.
66

77
## 1.1.3
88

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class _LinuxDirectoryWatcher
9292
});
9393

9494
_listen(
95-
Directory(path).list(recursive: true),
95+
Directory(path).listRecursivelyIgnoringErrors(),
9696
(FileSystemEntity entity) {
9797
if (entity is Directory) {
9898
_watchSubdir(entity.path);
@@ -136,17 +136,10 @@ class _LinuxDirectoryWatcher
136136
// top-level clients such as barback as well, and could be implemented with
137137
// a wrapper similar to how listening/canceling works now.
138138

139-
var stream = Directory(path).watch().transform(
140-
StreamTransformer<FileSystemEvent, FileSystemEvent>.fromHandlers(
141-
handleError: (error, st, sink) {
142-
// Directory might no longer exist at the point where we try to
143-
// start the watcher. Simply ignore this error and let the stream
144-
// close.
145-
if (error is! PathNotFoundException) {
146-
sink.addError(error, st);
147-
}
148-
},
149-
));
139+
// Directory might no longer exist at the point where we try to
140+
// start the watcher. Simply ignore this error and let the stream
141+
// close.
142+
var stream = Directory(path).watch().ignoring<PathNotFoundException>();
150143
_subdirStreams[path] = stream;
151144
_nativeEvents.add(stream);
152145
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ class _MacOSDirectoryWatcher
148148

149149
if (_files.containsDir(path)) continue;
150150

151-
var stream = Directory(path).list(recursive: true);
151+
var stream = Directory(path)
152+
.list(recursive: true)
153+
.ignoring<PathNotFoundException>();
152154
var subscription = stream.listen((entity) {
153155
if (entity is Directory) return;
154156
if (_files.contains(path)) return;
@@ -373,7 +375,7 @@ class _MacOSDirectoryWatcher
373375

374376
_files.clear();
375377
var completer = Completer<void>();
376-
var stream = Directory(path).list(recursive: true);
378+
var stream = Directory(path).listRecursivelyIgnoringErrors();
377379
_initialListSubscription = stream.listen((entity) {
378380
if (entity is! Directory) _files.add(entity.path);
379381
}, onError: _emitError, onDone: completer.complete, cancelOnError: true);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class _PollingDirectoryWatcher
112112
_filesToProcess.add(null);
113113
}
114114

115-
var stream = Directory(path).list(recursive: true);
115+
var stream = Directory(path).listRecursivelyIgnoringErrors();
116116
_listSubscription = stream.listen((entity) {
117117
assert(!_events.isClosed);
118118

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,15 @@ class _WindowsDirectoryWatcher
123123
void _startParentWatcher() {
124124
var absoluteDir = p.absolute(path);
125125
var parent = p.dirname(absoluteDir);
126-
// Check if [path] is already the root directory.
127-
if (FileSystemEntity.identicalSync(parent, path)) return;
126+
try {
127+
// Check if [path] is already the root directory.
128+
if (FileSystemEntity.identicalSync(parent, path)) return;
129+
} on FileSystemException catch (_) {
130+
// Either parent or path or both might be gone due to concurrently
131+
// occurring changes. Just ignore and continue. If we fail to
132+
// watch path we will report an error from _startWatch.
133+
return;
134+
}
128135
var parentStream = Directory(parent).watch(recursive: false);
129136
_parentWatchSubscription = parentStream.listen(
130137
(event) {
@@ -185,7 +192,14 @@ class _WindowsDirectoryWatcher
185192

186193
if (_files.containsDir(path)) continue;
187194

188-
var stream = Directory(path).list(recursive: true);
195+
// "Path not found" can be caused by creating then quickly removing
196+
// a directory: continue without reporting an error. Nested files
197+
// that get removed during the `list` are already ignored by `list`
198+
// itself, so there are no other types of "path not found" that
199+
// might need different handling here.
200+
var stream = Directory(path)
201+
.list(recursive: true)
202+
.ignoring<PathNotFoundException>();
189203
var subscription = stream.listen((entity) {
190204
if (entity is Directory) return;
191205
if (_files.contains(entity.path)) return;
@@ -198,14 +212,7 @@ class _WindowsDirectoryWatcher
198212
});
199213
subscription.onError((Object e, StackTrace stackTrace) {
200214
_listSubscriptions.remove(subscription);
201-
// "Path not found" can be caused by creating then quickly removing
202-
// a directory: continue without reporting an error. Nested files
203-
// that get removed during the `list` are already ignored by `list`
204-
// itself, so there are no other types of "path not found" that
205-
// might need different handling here.
206-
if (e is! PathNotFoundException) {
207-
_emitError(e, stackTrace);
208-
}
215+
_emitError(e, stackTrace);
209216
});
210217
_listSubscriptions.add(subscription);
211218
} else if (event is FileSystemModifyEvent) {
@@ -435,7 +442,7 @@ class _WindowsDirectoryWatcher
435442

436443
_files.clear();
437444
var completer = Completer<void>();
438-
var stream = Directory(path).list(recursive: true);
445+
var stream = Directory(path).listRecursivelyIgnoringErrors();
439446
void handleEntity(FileSystemEntity entity) {
440447
if (entity is! Directory) _files.add(entity.path);
441448
}

pkgs/watcher/lib/src/utils.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,30 @@ extension BatchEvents<T> on Stream<T> {
5050
}).bind(this);
5151
}
5252
}
53+
54+
extension IgnoringError<T> on Stream<T> {
55+
/// Ignore all errors of type [E] emitted by the given stream.
56+
///
57+
/// Everything else gets forwarded through as-is.
58+
Stream<T> ignoring<E>() {
59+
return transform(StreamTransformer<T, T>.fromHandlers(
60+
handleError: (error, st, sink) {
61+
if (error is! E) {
62+
sink.addError(error, st);
63+
}
64+
},
65+
));
66+
}
67+
}
68+
69+
extension DirectoryRobustRecursiveListing on Directory {
70+
/// List the given directory recursively but ignore not-found or access
71+
/// errors.
72+
///
73+
/// Theses can arise from concurrent file-system modification.
74+
Stream<FileSystemEntity> listRecursivelyIgnoringErrors() {
75+
return list(recursive: true)
76+
.ignoring<PathNotFoundException>()
77+
.ignoring<PathAccessException>();
78+
}
79+
}

pkgs/watcher/test/directory_watcher/shared.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ void sharedTests() {
347347
test('subdirectory watching is robust against races', () async {
348348
// Make sandboxPath accessible to child isolates created by Isolate.run.
349349
final sandboxPath = d.sandbox;
350-
final dirNames = [for (var i = 0; i < 50; i++) 'dir$i'];
350+
final dirNames = [for (var i = 0; i < 500; i++) 'dir$i'];
351351
await startWatcher();
352352

353353
// Repeatedly create and delete subdirectories in attempt to trigger

0 commit comments

Comments
 (0)