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.
diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
index fd65b4a..01755b4 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 f696a89..99e2cf5 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart
@@ -92,7 +92,7 @@
     });
 
     _listen(
-      Directory(path).list(recursive: true),
+      Directory(path).listRecursivelyIgnoringErrors(),
       (FileSystemEntity entity) {
         if (entity is Directory) {
           _watchSubdir(entity.path);
@@ -136,17 +136,10 @@
     // 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);
   }
diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
index b461383..509cf6f 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 @@
 
           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;
@@ -373,7 +375,7 @@
 
     _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);
diff --git a/pkgs/watcher/lib/src/directory_watcher/polling.dart b/pkgs/watcher/lib/src/directory_watcher/polling.dart
index 207679b..d3fa6eb 100644
--- a/pkgs/watcher/lib/src/directory_watcher/polling.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/polling.dart
@@ -112,7 +112,7 @@
       _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 8f21268..9b17f8d 100644
--- a/pkgs/watcher/lib/src/directory_watcher/windows.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart
@@ -123,8 +123,15 @@
   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 @@
 
           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;
@@ -198,14 +212,7 @@
           });
           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 @@
 
     _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);
     }
diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart
index c2e71b3..e5ef54c 100644
--- a/pkgs/watcher/lib/src/utils.dart
+++ b/pkgs/watcher/lib/src/utils.dart
@@ -50,3 +50,30 @@
     }).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>();
+  }
+}
diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart
index a1d2239..816d2a2 100644
--- a/pkgs/watcher/test/directory_watcher/shared.dart
+++ b/pkgs/watcher/test/directory_watcher/shared.dart
@@ -347,7 +347,7 @@
     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