Ignore PathNotFoundException when watching subdirs (#2181)
Watching subdirectories is inherently racy with file-system modifications
thus watcher must be prepared for `Directory.watch` to fail with
`PathNotFoundException`.
This is revealed when running tests against a [CL][1] which changes
timing of certain events slightly: `onDone` for `FileSystemEntity.watch()`
stream is now delayed by few turns of the event loop while underlying
watcher is being destroyed, previously it used to fire immediately which
hid away some of the inherent races in the code.
[1]: https://dart-review.googlesource.com/c/sdk/+/450921
diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
index 30a1975..fd65b4a 100644
--- a/pkgs/watcher/CHANGELOG.md
+++ b/pkgs/watcher/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 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.
+
## 1.1.3
- Improve handling of
@@ -6,7 +12,7 @@
events. But, the restart would sometimes silently fail. Now, it is more
reliable.
- Improving handling of directories that are created then immediately deleted on
- Windows. Previously, that could cause a `PathNotfoundException` to be thrown.
+ Windows. Previously, that could cause a `PathNotFoundException` to be thrown.
## 1.1.2
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart
index cb1d077..f696a89 100644
--- a/pkgs/watcher/lib/src/directory_watcher/linux.dart
+++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart
@@ -136,10 +136,17 @@
// top-level clients such as barback as well, and could be implemented with
// a wrapper similar to how listening/canceling works now.
- // TODO(nweiz): Catch any errors here that indicate that the directory in
- // question doesn't exist and silently stop watching it instead of
- // propagating the errors.
- var stream = Directory(path).watch();
+ 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);
+ }
+ },
+ ));
_subdirStreams[path] = stream;
_nativeEvents.add(stream);
}
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml
index 16af27b..80a8abb 100644
--- a/pkgs/watcher/pubspec.yaml
+++ b/pkgs/watcher/pubspec.yaml
@@ -1,5 +1,5 @@
name: watcher
-version: 1.1.3
+version: 1.1.4-wip
description: >-
A file system watcher. It monitors changes to contents of directories and
sends notifications when files have been added, removed, or modified.
diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart
index 1ebc78d..a1d2239 100644
--- a/pkgs/watcher/test/directory_watcher/shared.dart
+++ b/pkgs/watcher/test/directory_watcher/shared.dart
@@ -1,8 +1,11 @@
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:io' as io;
+import 'dart:isolate';
import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:watcher/src/utils.dart';
import '../utils.dart';
@@ -340,5 +343,31 @@
events.add(isRemoveEvent('dir/sub'));
await inAnyOrder(events);
});
+
+ 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'];
+ await startWatcher();
+
+ // Repeatedly create and delete subdirectories in attempt to trigger
+ // a race.
+ for (var i = 0; i < 10; i++) {
+ for (var dir in dirNames) {
+ createDir(dir);
+ }
+ await Isolate.run(() async {
+ await Future.wait([
+ for (var dir in dirNames)
+ io.Directory('$sandboxPath/$dir').delete(),
+ ]);
+ });
+ }
+
+ writeFile('a/b/c/d/file.txt');
+ await inAnyOrder([
+ isAddEvent('a/b/c/d/file.txt'),
+ ]);
+ });
});
}