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/CHANGELOG.md b/CHANGELOG.md
index 4ff6cfa..30762a0 100644
--- a/CHANGELOG.md
+++ b/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/lib/src/file_watcher/native.dart b/lib/src/file_watcher/native.dart
index f5699bb..1862e7b 100644
--- a/lib/src/file_watcher/native.dart
+++ b/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/lib/src/file_watcher/polling.dart b/lib/src/file_watcher/polling.dart
index a44f80c..3480ae2 100644
--- a/lib/src/file_watcher/polling.dart
+++ b/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/pubspec.yaml b/pubspec.yaml
index f62248d..2e033f1 100644
--- a/pubspec.yaml
+++ b/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/test/file_watcher/shared.dart b/test/file_watcher/shared.dart
index 2931d80..9a4965c 100644
--- a/test/file_watcher/shared.dart
+++ b/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();
+    });
+  });
 }