diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md index fd65b4abd..01755b407 100644 --- a/pkgs/watcher/CHANGELOG.md +++ b/pkgs/watcher/CHANGELOG.md @@ -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 diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart index f696a89b5..99e2cf50e 100644 --- a/pkgs/watcher/lib/src/directory_watcher/linux.dart +++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart @@ -92,7 +92,7 @@ class _LinuxDirectoryWatcher }); _listen( - Directory(path).list(recursive: true), + Directory(path).listRecursivelyIgnoringErrors(), (FileSystemEntity entity) { if (entity is Directory) { _watchSubdir(entity.path); @@ -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.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(); _subdirStreams[path] = stream; _nativeEvents.add(stream); } diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart index b46138347..509cf6fe6 100644 --- a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart +++ b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart @@ -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(); var subscription = stream.listen((entity) { if (entity is Directory) return; if (_files.contains(path)) return; @@ -373,7 +375,7 @@ class _MacOSDirectoryWatcher _files.clear(); var completer = Completer(); - 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); diff --git a/pkgs/watcher/lib/src/directory_watcher/polling.dart b/pkgs/watcher/lib/src/directory_watcher/polling.dart index 207679b1a..d3fa6eb60 100644 --- a/pkgs/watcher/lib/src/directory_watcher/polling.dart +++ b/pkgs/watcher/lib/src/directory_watcher/polling.dart @@ -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); diff --git a/pkgs/watcher/lib/src/directory_watcher/windows.dart b/pkgs/watcher/lib/src/directory_watcher/windows.dart index 8f212684c..9b17f8db1 100644 --- a/pkgs/watcher/lib/src/directory_watcher/windows.dart +++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart @@ -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) { @@ -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(); var subscription = stream.listen((entity) { if (entity is Directory) return; if (_files.contains(entity.path)) return; @@ -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) { @@ -435,7 +442,7 @@ class _WindowsDirectoryWatcher _files.clear(); var completer = Completer(); - var stream = Directory(path).list(recursive: true); + var stream = Directory(path).listRecursivelyIgnoringErrors(); void handleEntity(FileSystemEntity entity) { if (entity is! Directory) _files.add(entity.path); } diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart index c2e71b3c1..e5ef54c66 100644 --- a/pkgs/watcher/lib/src/utils.dart +++ b/pkgs/watcher/lib/src/utils.dart @@ -50,3 +50,30 @@ extension BatchEvents on Stream { }).bind(this); } } + +extension IgnoringError on Stream { + /// Ignore all errors of type [E] emitted by the given stream. + /// + /// Everything else gets forwarded through as-is. + Stream ignoring() { + return transform(StreamTransformer.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 listRecursivelyIgnoringErrors() { + return list(recursive: true) + .ignoring() + .ignoring(); + } +} diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart index a1d2239c6..816d2a2fa 100644 --- a/pkgs/watcher/test/directory_watcher/shared.dart +++ b/pkgs/watcher/test/directory_watcher/shared.dart @@ -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