Fix a race condition in file watcher. It was possible for events to be added to the events controller after it had been closed if the call to close() came in while a method was waiting on an asynchronous callback. R=cbracken@google.com Review URL: https://codereview.chromium.org//1228703007 .
diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md index 4ff6cfa..30762a0 100644 --- a/pkgs/watcher/CHANGELOG.md +++ b/pkgs/watcher/CHANGELOG.md
@@ -1,3 +1,8 @@ +# 0.9.7 + +* Fix a bug in `FileWatcher` where events could be added after watchers were + closed. + # 0.9.6 * Add a `Watcher` interface that encompasses watching both files and
diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart index f5699bb..1862e7b 100644 --- a/pkgs/watcher/lib/src/file_watcher/native.dart +++ b/pkgs/watcher/lib/src/file_watcher/native.dart
@@ -60,10 +60,17 @@ } _onDone() async { - // If the file exists now, it was probably removed and quickly replaced; - // this can happen for example when another file is moved on top of it. - // Re-subscribe and report a modify event. - if (await new File(path).exists()) { + var fileExists = await new File(path).exists(); + + // Check for this after checking whether the file exists because it's + // possible that [close] was called between [File.exists] being called and + // it completing. + if (_eventsController.isClosed) return; + + if (fileExists) { + // If the file exists now, it was probably removed and quickly replaced; + // this can happen for example when another file is moved on top of it. + // Re-subscribe and report a modify event. _eventsController.add(new WatchEvent(ChangeType.MODIFY, path)); _listen(); } else {
diff --git a/pkgs/watcher/lib/src/file_watcher/polling.dart b/pkgs/watcher/lib/src/file_watcher/polling.dart index a44f80c..3480ae2 100644 --- a/pkgs/watcher/lib/src/file_watcher/polling.dart +++ b/pkgs/watcher/lib/src/file_watcher/polling.dart
@@ -51,7 +51,10 @@ // We don't mark the file as removed if this is the first poll (indicated by // [_lastModified] being null). Instead, below we forward the dart:io error // that comes from trying to read the mtime below. - if (_lastModified != null && !await new File(path).exists()) { + var pathExists = await new File(path).exists(); + if (_eventsController.isClosed) return; + + if (_lastModified != null && !pathExists) { _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); close(); return; @@ -59,14 +62,17 @@ var modified; try { - modified = await getModificationTime(path); + try { + modified = await getModificationTime(path); + } finally { + if (_eventsController.isClosed) return; + } } on FileSystemException catch (error, stackTrace) { _eventsController.addError(error, stackTrace); close(); return; } - if (_eventsController.isClosed) return; if (_lastModified == modified) return; if (_lastModified == null) {
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml index f62248d..2e033f1 100644 --- a/pkgs/watcher/pubspec.yaml +++ b/pkgs/watcher/pubspec.yaml
@@ -1,5 +1,5 @@ name: watcher -version: 0.9.6 +version: 0.9.7 author: Dart Team <misc@dartlang.org> homepage: http://github.com/dart-lang/watcher description: >
diff --git a/pkgs/watcher/test/file_watcher/shared.dart b/pkgs/watcher/test/file_watcher/shared.dart index 2931d80..9a4965c 100644 --- a/pkgs/watcher/test/file_watcher/shared.dart +++ b/pkgs/watcher/test/file_watcher/shared.dart
@@ -2,6 +2,8 @@ // 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:async'; + import 'package:scheduled_test/scheduled_test.dart'; import 'package:watcher/src/utils.dart'; @@ -55,4 +57,19 @@ renameFile("old.txt", "file.txt"); expectModifyEvent("file.txt"); }); + + // Regression test for a race condition. + test("closes the watcher immediately after deleting the file", () { + writeFile("old.txt"); + var watcher = createWatcher(path: "file.txt", waitForReady: false); + var sub = schedule(() => watcher.events.listen(null)); + + deleteFile("file.txt"); + schedule(() async { + // Reproducing the race condition will always be flaky, but this sleep + // helped it reproduce more consistently on my machine. + await new Future.delayed(new Duration(milliseconds: 10)); + (await sub).cancel(); + }); + }); }