Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pkgs/watcher/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## 1.1.4-wip

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

## 1.1.3

Expand Down
17 changes: 5 additions & 12 deletions pkgs/watcher/lib/src/directory_watcher/linux.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class _LinuxDirectoryWatcher
});

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

var stream = Directory(path).watch().transform(
StreamTransformer<FileSystemEvent, FileSystemEvent>.fromHandlers(
handleError: (error, st, sink) {
// Directory might no longer exist at the point where we try to
// start the watcher. Simply ignore this error and let the stream
// close.
if (error is! PathNotFoundException) {
sink.addError(error, st);
}
},
));
// Directory might no longer exist at the point where we try to
// start the watcher. Simply ignore this error and let the stream
// close.
var stream = Directory(path).watch().ignoring<PathNotFoundException>();
_subdirStreams[path] = stream;
_nativeEvents.add(stream);
}
Expand Down
6 changes: 4 additions & 2 deletions pkgs/watcher/lib/src/directory_watcher/mac_os.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ class _MacOSDirectoryWatcher

if (_files.containsDir(path)) continue;

var stream = Directory(path).list(recursive: true);
var stream = Directory(path)
.list(recursive: true)
.ignoring<PathNotFoundException>();
var subscription = stream.listen((entity) {
if (entity is Directory) return;
if (_files.contains(path)) return;
Expand Down Expand Up @@ -373,7 +375,7 @@ class _MacOSDirectoryWatcher

_files.clear();
var completer = Completer<void>();
var stream = Directory(path).list(recursive: true);
var stream = Directory(path).listRecursivelyIgnoringErrors();
_initialListSubscription = stream.listen((entity) {
if (entity is! Directory) _files.add(entity.path);
}, onError: _emitError, onDone: completer.complete, cancelOnError: true);
Expand Down
2 changes: 1 addition & 1 deletion pkgs/watcher/lib/src/directory_watcher/polling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class _PollingDirectoryWatcher
_filesToProcess.add(null);
}

var stream = Directory(path).list(recursive: true);
var stream = Directory(path).listRecursivelyIgnoringErrors();
_listSubscription = stream.listen((entity) {
assert(!_events.isClosed);

Expand Down
31 changes: 19 additions & 12 deletions pkgs/watcher/lib/src/directory_watcher/windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,15 @@ class _WindowsDirectoryWatcher
void _startParentWatcher() {
var absoluteDir = p.absolute(path);
var parent = p.dirname(absoluteDir);
// Check if [path] is already the root directory.
if (FileSystemEntity.identicalSync(parent, path)) return;
try {
// Check if [path] is already the root directory.
if (FileSystemEntity.identicalSync(parent, path)) return;
} on FileSystemException catch (_) {
// Either parent or path or both might be gone due to concurrently
// occurring changes. Just ignore and continue. If we fail to
// watch path we will report an error from _startWatch.
return;
}
var parentStream = Directory(parent).watch(recursive: false);
_parentWatchSubscription = parentStream.listen(
(event) {
Expand Down Expand Up @@ -185,7 +192,14 @@ class _WindowsDirectoryWatcher

if (_files.containsDir(path)) continue;

var stream = Directory(path).list(recursive: true);
// "Path not found" can be caused by creating then quickly removing
// a directory: continue without reporting an error. Nested files
// that get removed during the `list` are already ignored by `list`
// itself, so there are no other types of "path not found" that
// might need different handling here.
var stream = Directory(path)
.list(recursive: true)
.ignoring<PathNotFoundException>();
var subscription = stream.listen((entity) {
if (entity is Directory) return;
if (_files.contains(entity.path)) return;
Expand All @@ -198,14 +212,7 @@ class _WindowsDirectoryWatcher
});
subscription.onError((Object e, StackTrace stackTrace) {
_listSubscriptions.remove(subscription);
// "Path not found" can be caused by creating then quickly removing
// a directory: continue without reporting an error. Nested files
// that get removed during the `list` are already ignored by `list`
// itself, so there are no other types of "path not found" that
// might need different handling here.
if (e is! PathNotFoundException) {
_emitError(e, stackTrace);
}
_emitError(e, stackTrace);
});
_listSubscriptions.add(subscription);
} else if (event is FileSystemModifyEvent) {
Expand Down Expand Up @@ -435,7 +442,7 @@ class _WindowsDirectoryWatcher

_files.clear();
var completer = Completer<void>();
var stream = Directory(path).list(recursive: true);
var stream = Directory(path).listRecursivelyIgnoringErrors();
void handleEntity(FileSystemEntity entity) {
if (entity is! Directory) _files.add(entity.path);
}
Expand Down
27 changes: 27 additions & 0 deletions pkgs/watcher/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,30 @@ extension BatchEvents<T> on Stream<T> {
}).bind(this);
}
}

extension IgnoringError<T> on Stream<T> {
/// Ignore all errors of type [E] emitted by the given stream.
///
/// Everything else gets forwarded through as-is.
Stream<T> ignoring<E>() {
return transform(StreamTransformer<T, T>.fromHandlers(
handleError: (error, st, sink) {
if (error is! E) {
sink.addError(error, st);
}
},
));
}
}

extension DirectoryRobustRecursiveListing on Directory {
/// List the given directory recursively but ignore not-found or access
/// errors.
///
/// Theses can arise from concurrent file-system modification.
Stream<FileSystemEntity> listRecursivelyIgnoringErrors() {
return list(recursive: true)
.ignoring<PathNotFoundException>()
.ignoring<PathAccessException>();
}
}
2 changes: 1 addition & 1 deletion pkgs/watcher/test/directory_watcher/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ void sharedTests() {
test('subdirectory watching is robust against races', () async {
// Make sandboxPath accessible to child isolates created by Isolate.run.
final sandboxPath = d.sandbox;
final dirNames = [for (var i = 0; i < 50; i++) 'dir$i'];
final dirNames = [for (var i = 0; i < 500; i++) 'dir$i'];
await startWatcher();

// Repeatedly create and delete subdirectories in attempt to trigger
Expand Down