Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(288)

Side by Side Diff: pkg/watcher/lib/src/directory_watcher/windows.dart

Issue 312743002: Use 'Directory.watch' on Windows in pkg/watcher, instead of pooling. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Fixes Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4 // TODO(rnystrom): Merge with mac_os version.
5 library watcher.directory_watcher.mac_os; 5
6 library watcher.directory_watcher.windows;
6 7
7 import 'dart:async'; 8 import 'dart:async';
9 import 'dart:collection';
8 import 'dart:io'; 10 import 'dart:io';
9 11
10 import 'package:path/path.dart' as p; 12 import 'package:path/path.dart' as p;
11 import 'package:stack_trace/stack_trace.dart'; 13 import 'package:stack_trace/stack_trace.dart';
12 14
13 import '../constructable_file_system_event.dart'; 15 import '../constructable_file_system_event.dart';
14 import '../path_set.dart'; 16 import '../path_set.dart';
15 import '../utils.dart'; 17 import '../utils.dart';
16 import '../watch_event.dart'; 18 import '../watch_event.dart';
17 import 'resubscribable.dart'; 19 import 'resubscribable.dart';
18 20
19 /// Uses the FSEvents subsystem to watch for filesystem events. 21 class WindowsDirectoryWatcher extends ResubscribableDirectoryWatcher {
20 /// 22 WindowsDirectoryWatcher(String directory)
21 /// FSEvents has two main idiosyncrasies that this class works around. First, it 23 : super(directory, () => new _WindowsDirectoryWatcher(directory));
22 /// will occasionally report events that occurred before the filesystem watch
23 /// was initiated. Second, if multiple events happen to the same file in close
24 /// succession, it won't report them in the order they occurred. See issue
25 /// 14373.
26 ///
27 /// This also works around issues 16003 and 14849 in the implementation of
28 /// [Directory.watch].
29 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
30 // TODO(nweiz): remove these when issue 15042 is fixed.
31 static var logDebugInfo = false;
32 static var _count = 0;
33
34 MacOSDirectoryWatcher(String directory)
35 : super(directory, () => new _MacOSDirectoryWatcher(directory, _count++));
36 } 24 }
37 25
38 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { 26 class _EventBatcher {
39 // TODO(nweiz): remove these when issue 15042 is fixed. 27 static const Duration _BATCH_DELAY = const Duration(milliseconds: 100);
40 static var _count = 0; 28 final List<FileSystemEvent> events = [];
41 final String _id; 29 Timer timer;
42 30
31 void addEvent(FileSystemEvent event) {
32 events.add(event);
33 }
34
35 void startTimer(void callback()) {
36 if (timer != null) {
37 timer.cancel();
38 }
39 timer = new Timer(_BATCH_DELAY, callback);
40 }
Bob Nystrom 2014/06/04 15:48:29 Let's combine these two methods. You never want to
Anders Johnsen 2014/06/10 06:47:14 Done.
41
42 void cancelTimer() {
43 timer.cancel();
44 }
45 }
46
47 class _WindowsDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
43 final String directory; 48 final String directory;
44 49
45 Stream<WatchEvent> get events => _eventsController.stream; 50 Stream<WatchEvent> get events => _eventsController.stream;
46 final _eventsController = new StreamController<WatchEvent>.broadcast(); 51 final _eventsController = new StreamController<WatchEvent>.broadcast();
47 52
48 bool get isReady => _readyCompleter.isCompleted; 53 bool get isReady => _readyCompleter.isCompleted;
49 54
50 Future get ready => _readyCompleter.future; 55 Future get ready => _readyCompleter.future;
51 final _readyCompleter = new Completer(); 56 final _readyCompleter = new Completer();
52 57
58 final Map<String, _EventBatcher> _eventBatchers =
59 new HashMap<String, _EventBatcher>();
60
53 /// The set of files that are known to exist recursively within the watched 61 /// The set of files that are known to exist recursively within the watched
54 /// directory. 62 /// directory.
55 /// 63 ///
56 /// The state of files on the filesystem is compared against this to determine 64 /// The state of files on the filesystem is compared against this to determine
57 /// the real change that occurred when working around issue 14373. This is 65 /// the real change that occurred. This is also used to emit REMOVE events
58 /// also used to emit REMOVE events when subdirectories are moved out of the 66 /// when subdirectories are moved out of the watched directory.
59 /// watched directory.
60 final PathSet _files; 67 final PathSet _files;
61 68
62 /// The subscription to the stream returned by [Directory.watch]. 69 /// The subscription to the stream returned by [Directory.watch].
63 ///
64 /// This is separate from [_subscriptions] because this stream occasionally
65 /// needs to be resubscribed in order to work around issue 14849.
66 StreamSubscription<FileSystemEvent> _watchSubscription; 70 StreamSubscription<FileSystemEvent> _watchSubscription;
67 71
72 /// The subscription to the stream returned by [Directory.watch] of the
73 /// parent directory to [directory]. This is needed to detect changes to
74 /// [directory], as they are not included on Windows.
75 StreamSubscription<FileSystemEvent> _parentWatchSubscription;
76
68 /// The subscription to the [Directory.list] call for the initial listing of 77 /// The subscription to the [Directory.list] call for the initial listing of
69 /// the directory to determine its initial state. 78 /// the directory to determine its initial state.
70 StreamSubscription<FileSystemEntity> _initialListSubscription; 79 StreamSubscription<FileSystemEntity> _initialListSubscription;
71 80
72 /// The subscription to the [Directory.list] call for listing the contents of 81 /// The subscriptions to the [Directory.list] call for listing the contents of
Bob Nystrom 2014/06/04 15:48:29 "call" -> "calls".
Anders Johnsen 2014/06/10 06:47:15 Done.
73 /// a subdirectory that was moved into the watched directory. 82 /// subdirectories that was moved into the watched directory.
Bob Nystrom 2014/06/04 15:48:29 "was" -> "were".
Anders Johnsen 2014/06/10 06:47:15 Done.
74 StreamSubscription<FileSystemEntity> _listSubscription; 83 final Set<StreamSubscription<FileSystemEntity>> _listSubscriptions
75 84 = new HashSet<StreamSubscription<FileSystemEntity>>();
76 /// The timer for tracking how long we wait for an initial batch of bogus 85
77 /// events (see issue 14373). 86 _WindowsDirectoryWatcher(String directory)
78 Timer _bogusEventTimer; 87 : directory = directory, _files = new PathSet(directory) {
79
80 _MacOSDirectoryWatcher(String directory, int parentId)
81 : directory = directory,
82 _files = new PathSet(directory),
83 _id = "$parentId/${_count++}" {
84 _startWatch(); 88 _startWatch();
85 89 _startParentWatcher();
86 // Before we're ready to emit events, wait for [_listDir] to complete and 90
87 // for enough time to elapse that if bogus events (issue 14373) would be 91 // Before we're ready to emit events, wait for [_listDir] to complete.
88 // emitted, they will be. 92 _listDir().then(_readyCompleter.complete);
89 // 93 }
90 // If we do receive a batch of events, [_onBatch] will ensure that these 94
91 // futures don't fire and that the directory is re-listed. 95 void close() {
92 Future.wait([ 96 if (_watchSubscription != null) _watchSubscription.cancel();
97 if (_parentWatchSubscription != null) _parentWatchSubscription.cancel();
98 if (_initialListSubscription != null) _initialListSubscription.cancel();
99 for (var sub in _listSubscriptions) {
100 sub.cancel();
101 }
102 _listSubscriptions.clear();
103 for (var batcher in _eventBatchers.values) {
104 batcher.cancelTimer();
105 }
106 _eventBatchers.clear();
107 _watchSubscription = null;
108 _parentWatchSubscription = null;
109 _initialListSubscription = null;
110 _eventsController.close();
111 }
112
113 /// On Windows, if [directory] is deleted, we will not receive any event.
Bob Nystrom 2014/06/04 15:48:29 Blank "///" line after this.
Anders Johnsen 2014/06/10 06:47:15 Done.
114 /// Instead, we add a watcher on the parent folder (if any), that can notify
115 /// us about [directory].
116 /// This also includes events such as moves.
Bob Nystrom 2014/06/04 15:48:29 This can be part of the previous paragraph.
Anders Johnsen 2014/06/10 06:47:15 Done.
117 void _startParentWatcher() {
118 var absoluteDir = p.absolute(directory);
119 var parent = p.dirname(absoluteDir);
120 // Check if we [directory] is already the root directory.
Bob Nystrom 2014/06/04 15:48:29 Remove "we".
Anders Johnsen 2014/06/10 06:47:14 Done.
121 if (FileSystemEntity.identicalSync(parent, directory)) return;
122 var parentStream = Chain.track(
123 new Directory(parent).watch(recursive: false));
124 _parentWatchSubscription = parentStream.listen((event) {
125 // Only look at events for 'directory'.
126 if (p.basename(event.path) != p.basename(absoluteDir)) return;
127 // Test if the directory is removed. FileSystemEntity.typeSync will
128 // return NOT_FOUND if it's unable to decide upon the type, including
129 // access denied issues, which may happen when the directory is deleted.
130 // FileSystemMoveEvent and FileSystemDeleteEvent events will always mean
131 // the directory is now gone.
132 if (event is FileSystemMoveEvent ||
133 event is FileSystemDeleteEvent ||
134 (FileSystemEntity.typeSync(directory) ==
135 FileSystemEntityType.NOT_FOUND)) {
136 for (var path in _files.toSet()) {
Bob Nystrom 2014/06/04 15:48:29 The .toSet() call here could be expensive. Is it n
Anders Johnsen 2014/06/10 06:47:15 This is needed for some reason.
137 _emitEvent(ChangeType.REMOVE, path);
138 }
139 _files.clear();
140 close();
141 }
142 }, onError: (error) {
143 // Ignore errors, simply close the stream.
Bob Nystrom 2014/06/04 15:48:29 You answered in the code review, but can you leave
Anders Johnsen 2014/06/10 06:47:15 Done.
144 _parentWatchSubscription.cancel();
145 _parentWatchSubscription = null;
146 });
147 }
148
149 void _onEvent(FileSystemEvent event) {
150 // If we get a event before we're ready to begin emitting events,
151 // ignore those events and re-list the directory.
Bob Nystrom 2014/06/04 15:48:29 Why not just defer calling _startWatch() until aft
Anders Johnsen 2014/06/10 06:47:15 Done.
152 if (!isReady) {
93 _listDir().then((_) { 153 _listDir().then((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) { 154 _readyCompleter.complete();
95 print("[$_id] finished initial directory list"); 155 });
96 } 156 return;
97 }), 157 }
98 _waitForBogusEvents() 158
99 ]).then((_) { 159 _EventBatcher batcher = _eventBatchers.putIfAbsent(
Bob Nystrom 2014/06/04 15:48:29 Use var here.
Anders Johnsen 2014/06/10 06:47:15 Done.
100 if (MacOSDirectoryWatcher.logDebugInfo) { 160 event.path, () => new _EventBatcher());
101 print("[$_id] watcher is ready, known files:"); 161 batcher.addEvent(event);
102 for (var file in _files.toSet()) { 162 batcher.startTimer(() {
103 print("[$_id] ${p.relative(file, from: directory)}"); 163 _eventBatchers.remove(event.path);
104 } 164 _onBatch(batcher.events);
105 }
106 _readyCompleter.complete();
107 }); 165 });
108 } 166 }
109 167
110 void close() {
111 if (MacOSDirectoryWatcher.logDebugInfo) {
112 print("[$_id] watcher is closed\n${new Chain.current().terse}");
113 }
114 if (_watchSubscription != null) _watchSubscription.cancel();
115 if (_initialListSubscription != null) _initialListSubscription.cancel();
116 if (_listSubscription != null) _listSubscription.cancel();
117 _watchSubscription = null;
118 _initialListSubscription = null;
119 _listSubscription = null;
120 _eventsController.close();
121 }
122
123 /// The callback that's run when [Directory.watch] emits a batch of events. 168 /// The callback that's run when [Directory.watch] emits a batch of events.
124 void _onBatch(List<FileSystemEvent> batch) { 169 void _onBatch(List<FileSystemEvent> batch) {
125 if (MacOSDirectoryWatcher.logDebugInfo) {
126 print("[$_id] ======== batch:");
127 for (var event in batch) {
128 print("[$_id] ${_formatEvent(event)}");
129 }
130
131 print("[$_id] known files:");
132 for (var file in _files.toSet()) {
133 print("[$_id] ${p.relative(file, from: directory)}");
134 }
135 }
136
137 // If we get a batch of events before we're ready to begin emitting events,
138 // it's probable that it's a batch of pre-watcher events (see issue 14373).
139 // Ignore those events and re-list the directory.
140 if (!isReady) {
141 if (MacOSDirectoryWatcher.logDebugInfo) {
142 print("[$_id] not ready to emit events, re-listing directory");
143 }
144
145 // Cancel the timer because bogus events only occur in the first batch, so
146 // we can fire [ready] as soon as we're done listing the directory.
147 _bogusEventTimer.cancel();
148 _listDir().then((_) {
149 if (MacOSDirectoryWatcher.logDebugInfo) {
150 print("[$_id] watcher is ready, known files:");
151 for (var file in _files.toSet()) {
152 print("[$_id] ${p.relative(file, from: directory)}");
153 }
154 }
155 _readyCompleter.complete();
156 });
157 return;
158 }
159
160 _sortEvents(batch).forEach((path, events) { 170 _sortEvents(batch).forEach((path, events) {
161 var relativePath = p.relative(path, from: directory); 171 var relativePath = p.relative(path, from: directory);
162 if (MacOSDirectoryWatcher.logDebugInfo) {
163 print("[$_id] events for $relativePath:");
164 for (var event in events) {
165 print("[$_id] ${_formatEvent(event)}");
166 }
167 }
168 172
169 var canonicalEvent = _canonicalEvent(events); 173 var canonicalEvent = _canonicalEvent(events);
170 events = canonicalEvent == null ? 174 events = canonicalEvent == null ?
171 _eventsBasedOnFileSystem(path) : [canonicalEvent]; 175 _eventsBasedOnFileSystem(path) : [canonicalEvent];
172 if (MacOSDirectoryWatcher.logDebugInfo) {
173 print("[$_id] canonical event for $relativePath: "
174 "${_formatEvent(canonicalEvent)}");
175 print("[$_id] actionable events for $relativePath: "
176 "${events.map(_formatEvent)}");
177 }
178 176
179 for (var event in events) { 177 for (var event in events) {
180 if (event is FileSystemCreateEvent) { 178 if (event is FileSystemCreateEvent) {
181 if (!event.isDirectory) { 179 if (!event.isDirectory) {
182 // Don't emit ADD events for files or directories that we already
183 // know about. Such an event comes from FSEvents reporting an add
184 // that happened prior to the watch beginning.
185 if (_files.contains(path)) continue; 180 if (_files.contains(path)) continue;
186 181
187 _emitEvent(ChangeType.ADD, path); 182 _emitEvent(ChangeType.ADD, path);
188 _files.add(path); 183 _files.add(path);
189 continue; 184 continue;
190 } 185 }
191 186
192 if (_files.containsDir(path)) continue; 187 if (_files.containsDir(path)) continue;
193 188
194 var stream = Chain.track(new Directory(path).list(recursive: true)); 189 var stream = Chain.track(new Directory(path).list(recursive: true));
195 _listSubscription = stream.listen((entity) { 190 var sub;
191 sub = stream.listen((entity) {
196 if (entity is Directory) return; 192 if (entity is Directory) return;
197 if (_files.contains(path)) return; 193 if (_files.contains(path)) return;
198 194
199 _emitEvent(ChangeType.ADD, entity.path); 195 _emitEvent(ChangeType.ADD, entity.path);
200 _files.add(entity.path); 196 _files.add(entity.path);
197 }, onDone: () {
198 _listSubscriptions.remove(sub);
201 }, onError: (e, stackTrace) { 199 }, onError: (e, stackTrace) {
202 if (MacOSDirectoryWatcher.logDebugInfo) { 200 _listSubscriptions.remove(sub);
203 print("[$_id] got error listing $relativePath: $e");
204 }
205 _emitError(e, stackTrace); 201 _emitError(e, stackTrace);
206 }, cancelOnError: true); 202 }, cancelOnError: true);
203 _listSubscriptions.add(sub);
207 } else if (event is FileSystemModifyEvent) { 204 } else if (event is FileSystemModifyEvent) {
208 assert(!event.isDirectory); 205 if (!event.isDirectory) {
209 _emitEvent(ChangeType.MODIFY, path); 206 _emitEvent(ChangeType.MODIFY, path);
207 }
210 } else { 208 } else {
211 assert(event is FileSystemDeleteEvent); 209 assert(event is FileSystemDeleteEvent);
212 for (var removedPath in _files.remove(path)) { 210 for (var removedPath in _files.remove(path)) {
213 _emitEvent(ChangeType.REMOVE, removedPath); 211 _emitEvent(ChangeType.REMOVE, removedPath);
214 } 212 }
215 } 213 }
216 } 214 }
217 }); 215 });
218
219 if (MacOSDirectoryWatcher.logDebugInfo) {
220 print("[$_id] ======== batch complete");
221 }
222 } 216 }
223 217
224 /// Sort all the events in a batch into sets based on their path. 218 /// Sort all the events in a batch into sets based on their path.
225 /// 219 ///
226 /// A single input event may result in multiple events in the returned map; 220 /// A single input event may result in multiple events in the returned map;
227 /// for example, a MOVE event becomes a DELETE event for the source and a 221 /// for example, a MOVE event becomes a DELETE event for the source and a
228 /// CREATE event for the destination. 222 /// CREATE event for the destination.
229 /// 223 ///
230 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it 224 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
231 /// contain any events relating to [directory]. 225 /// contain any events relating to [directory].
232 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { 226 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
233 var eventsForPaths = {}; 227 var eventsForPaths = {};
234 228
235 // FSEvents can report past events, including events on the root directory
236 // such as it being created. We want to ignore these. If the directory is
237 // really deleted, that's handled by [_onDone].
238 batch = batch.where((event) => event.path != directory).toList();
239
240 // Events within directories that already have events are superfluous; the 229 // Events within directories that already have events are superfluous; the
241 // directory's full contents will be examined anyway, so we ignore such 230 // directory's full contents will be examined anyway, so we ignore such
242 // events. Emitting them could cause useless or out-of-order events. 231 // events. Emitting them could cause useless or out-of-order events.
243 var directories = unionAll(batch.map((event) { 232 var directories = unionAll(batch.map((event) {
244 if (!event.isDirectory) return new Set(); 233 if (!event.isDirectory) return new Set();
245 if (event is! FileSystemMoveEvent) return new Set.from([event.path]); 234 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
246 return new Set.from([event.path, event.destination]); 235 return new Set.from([event.path, event.destination]);
247 })); 236 }));
248 237
249 isInModifiedDirectory(path) => 238 isInModifiedDirectory(path) =>
250 directories.any((dir) => path != dir && path.startsWith(dir)); 239 directories.any((dir) => path != dir && path.startsWith(dir));
251 240
252 addEvent(path, event) { 241 addEvent(path, event) {
253 if (isInModifiedDirectory(path)) return; 242 if (isInModifiedDirectory(path)) return;
254 var set = eventsForPaths.putIfAbsent(path, () => new Set()); 243 var set = eventsForPaths.putIfAbsent(path, () => new Set());
255 set.add(event); 244 set.add(event);
256 } 245 }
257 246
258 for (var event in batch) { 247 for (var event in batch) {
259 // The Mac OS watcher doesn't emit move events. See issue 14806. 248 if (event is FileSystemMoveEvent) {
260 assert(event is! FileSystemMoveEvent); 249 FileSystemMoveEvent moveEvent = event;
Bob Nystrom 2014/06/04 15:48:29 Ditch this line.
Anders Johnsen 2014/06/10 06:47:15 Done.
250 addEvent(moveEvent.destination, event);
251 }
261 addEvent(event.path, event); 252 addEvent(event.path, event);
262 } 253 }
263 254
264 return eventsForPaths; 255 return eventsForPaths;
265 } 256 }
266 257
267 /// Returns the canonical event from a batch of events on the same path, if 258 /// Returns the canonical event from a batch of events on the same path, if
268 /// one exists. 259 /// one exists.
269 /// 260 ///
270 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and 261 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
(...skipping 18 matching lines...) Expand all
289 if (isDir != event.isDirectory) return null; 280 if (isDir != event.isDirectory) return null;
290 281
291 // Modify events don't contradict either CREATE or REMOVE events. We can 282 // Modify events don't contradict either CREATE or REMOVE events. We can
292 // safely assume the file was modified after a CREATE or before the 283 // safely assume the file was modified after a CREATE or before the
293 // REMOVE; otherwise there will also be a REMOVE or CREATE event 284 // REMOVE; otherwise there will also be a REMOVE or CREATE event
294 // (respectively) that will be contradictory. 285 // (respectively) that will be contradictory.
295 if (event is FileSystemModifyEvent) { 286 if (event is FileSystemModifyEvent) {
296 hadModifyEvent = true; 287 hadModifyEvent = true;
297 continue; 288 continue;
298 } 289 }
299 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); 290 assert(event is FileSystemCreateEvent ||
291 event is FileSystemDeleteEvent ||
292 event is FileSystemMoveEvent);
300 293
301 // If we previously thought this was a MODIFY, we now consider it to be a 294 // If we previously thought this was a MODIFY, we now consider it to be a
302 // CREATE or REMOVE event. This is safe for the same reason as above. 295 // CREATE or REMOVE event. This is safe for the same reason as above.
303 if (type == FileSystemEvent.MODIFY) { 296 if (type == FileSystemEvent.MODIFY) {
304 type = event.type; 297 type = event.type;
305 continue; 298 continue;
306 } 299 }
307 300
308 // A CREATE event contradicts a REMOVE event and vice versa. 301 // A CREATE event contradicts a REMOVE event and vice versa.
309 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE); 302 assert(type == FileSystemEvent.CREATE ||
303 type == FileSystemEvent.DELETE ||
304 type == FileSystemEvent.MOVE);
310 if (type != event.type) return null; 305 if (type != event.type) return null;
311 } 306 }
312 307
313 // If we got a CREATE event for a file we already knew about, that comes
314 // from FSEvents reporting an add that happened prior to the watch
315 // beginning. If we also received a MODIFY event, we want to report that,
316 // but not the CREATE.
317 if (type == FileSystemEvent.CREATE && hadModifyEvent &&
318 _files.contains(batch.first.path)) {
319 type = FileSystemEvent.MODIFY;
320 }
321
322 switch (type) { 308 switch (type) {
323 case FileSystemEvent.CREATE: 309 case FileSystemEvent.CREATE:
324 // Issue 16003 means that a CREATE event for a directory can indicate 310 return new ConstructableFileSystemCreateEvent(batch.first.path, isDir);
325 // that the directory was moved and then re-created.
326 // [_eventsBasedOnFileSystem] will handle this correctly by producing a
327 // DELETE event followed by a CREATE event if the directory exists.
328 if (isDir) return null;
329 return new ConstructableFileSystemCreateEvent(batch.first.path, false);
330 case FileSystemEvent.DELETE: 311 case FileSystemEvent.DELETE:
331 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir); 312 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
332 case FileSystemEvent.MODIFY: 313 case FileSystemEvent.MODIFY:
333 return new ConstructableFileSystemModifyEvent( 314 return new ConstructableFileSystemModifyEvent(
334 batch.first.path, isDir, false); 315 batch.first.path, isDir, false);
316 case FileSystemEvent.MOVE:
317 return null;
335 default: assert(false); 318 default: assert(false);
336 } 319 }
337 } 320 }
338 321
339 /// Returns one or more events that describe the change between the last known 322 /// Returns one or more events that describe the change between the last known
340 /// state of [path] and its current state on the filesystem. 323 /// state of [path] and its current state on the filesystem.
341 /// 324 ///
342 /// This returns a list whose order should be reflected in the events emitted 325 /// This returns a list whose order should be reflected in the events emitted
343 /// to the user, unlike the batched events from [Directory.watch]. The 326 /// to the user, unlike the batched events from [Directory.watch]. The
344 /// returned list may be empty, indicating that no changes occurred to [path] 327 /// returned list may be empty, indicating that no changes occurred to [path]
345 /// (probably indicating that it was created and then immediately deleted). 328 /// (probably indicating that it was created and then immediately deleted).
346 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { 329 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
347 var fileExisted = _files.contains(path); 330 var fileExisted = _files.contains(path);
348 var dirExisted = _files.containsDir(path); 331 var dirExisted = _files.containsDir(path);
349 var fileExists = new File(path).existsSync(); 332 var fileExists = new File(path).existsSync();
350 var dirExists = new Directory(path).existsSync(); 333 var dirExists = new Directory(path).existsSync();
351 334
352 if (MacOSDirectoryWatcher.logDebugInfo) {
353 print("[$_id] checking file system for "
354 "${p.relative(path, from: directory)}");
355 print("[$_id] file existed: $fileExisted");
356 print("[$_id] dir existed: $dirExisted");
357 print("[$_id] file exists: $fileExists");
358 print("[$_id] dir exists: $dirExists");
359 }
360
361 var events = []; 335 var events = [];
362 if (fileExisted) { 336 if (fileExisted) {
363 if (fileExists) { 337 if (fileExists) {
364 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); 338 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
365 } else { 339 } else {
366 events.add(new ConstructableFileSystemDeleteEvent(path, false)); 340 events.add(new ConstructableFileSystemDeleteEvent(path, false));
367 } 341 }
368 } else if (dirExisted) { 342 } else if (dirExisted) {
369 if (dirExists) { 343 if (dirExists) {
370 // If we got contradictory events for a directory that used to exist and 344 // If we got contradictory events for a directory that used to exist and
371 // still exists, we need to rescan the whole thing in case it was 345 // still exists, we need to rescan the whole thing in case it was
372 // replaced with a different directory. 346 // replaced with a different directory.
373 events.add(new ConstructableFileSystemDeleteEvent(path, true)); 347 events.add(new ConstructableFileSystemDeleteEvent(path, true));
374 events.add(new ConstructableFileSystemCreateEvent(path, true)); 348 events.add(new ConstructableFileSystemCreateEvent(path, true));
375 } else { 349 } else {
376 events.add(new ConstructableFileSystemDeleteEvent(path, true)); 350 events.add(new ConstructableFileSystemDeleteEvent(path, true));
377 } 351 }
378 } 352 }
379 353
380 if (!fileExisted && fileExists) { 354 if (!fileExisted && fileExists) {
381 events.add(new ConstructableFileSystemCreateEvent(path, false)); 355 events.add(new ConstructableFileSystemCreateEvent(path, false));
382 } else if (!dirExisted && dirExists) { 356 } else if (!dirExisted && dirExists) {
383 events.add(new ConstructableFileSystemCreateEvent(path, true)); 357 events.add(new ConstructableFileSystemCreateEvent(path, true));
384 } 358 }
385 359
386 return events; 360 return events;
387 } 361 }
388 362
389 /// The callback that's run when the [Directory.watch] stream is closed. 363 /// The callback that's run when the [Directory.watch] stream is closed.
364 /// Note that this is unlikely to happen on Windows, unless the system itself
365 /// closes the handle.
390 void _onDone() { 366 void _onDone() {
391 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed");
392
393 _watchSubscription = null; 367 _watchSubscription = null;
394 368
395 // If the directory still exists and we're still expecting bogus events, 369 // Emit remove-events for any remaining files.
Bob Nystrom 2014/06/04 15:48:29 Remove the "-".
Anders Johnsen 2014/06/10 06:47:15 Done.
396 // this is probably issue 14849 rather than a real close event. We should
397 // just restart the watcher.
398 if (!isReady && new Directory(directory).existsSync()) {
399 if (MacOSDirectoryWatcher.logDebugInfo) {
400 print("[$_id] fake closure (issue 14849), re-opening stream");
401 }
402 _startWatch();
403 return;
404 }
405
406 // FSEvents can fail to report the contents of the directory being removed
407 // when the directory itself is removed, so we need to manually mark the
408 // files as removed.
409 for (var file in _files.toSet()) { 370 for (var file in _files.toSet()) {
410 _emitEvent(ChangeType.REMOVE, file); 371 _emitEvent(ChangeType.REMOVE, file);
411 } 372 }
412 _files.clear(); 373 _files.clear();
413 close(); 374 close();
414 } 375 }
415 376
416 /// Start or restart the underlying [Directory.watch] stream. 377 /// Start or restart the underlying [Directory.watch] stream.
417 void _startWatch() { 378 void _startWatch() {
418 // Batch the FSEvent changes together so that we can dedup events. 379 // Batch the events changes together so that we can dedup events.
Bob Nystrom 2014/06/04 15:48:29 Remove "changes".
Anders Johnsen 2014/06/10 06:47:15 Done.
419 var innerStream = 380 var innerStream =
420 Chain.track(new Directory(directory).watch(recursive: true)) 381 Chain.track(new Directory(directory).watch(recursive: true));
421 .transform(new BatchedStreamTransformer<FileSystemEvent>()); 382 _watchSubscription = innerStream.listen(_onEvent,
422 _watchSubscription = innerStream.listen(_onBatch,
423 onError: _eventsController.addError, 383 onError: _eventsController.addError,
424 onDone: _onDone); 384 onDone: _onDone);
425 } 385 }
426 386
427 /// Starts or restarts listing the watched directory to get an initial picture 387 /// Starts or restarts listing the watched directory to get an initial picture
428 /// of its state. 388 /// of its state.
429 Future _listDir() { 389 Future _listDir() {
430 assert(!isReady); 390 assert(!isReady);
431 if (_initialListSubscription != null) _initialListSubscription.cancel(); 391 if (_initialListSubscription != null) _initialListSubscription.cancel();
432 392
433 _files.clear(); 393 _files.clear();
434 var completer = new Completer(); 394 var completer = new Completer();
435 var stream = Chain.track(new Directory(directory).list(recursive: true)); 395 var stream = Chain.track(new Directory(directory).list(recursive: true));
436 _initialListSubscription = stream.listen((entity) { 396 void handleEntity(entity) {
437 if (entity is! Directory) _files.add(entity.path); 397 if (entity is! Directory) _files.add(entity.path);
438 }, 398 }
399 _initialListSubscription = stream.listen(
400 handleEntity,
439 onError: _emitError, 401 onError: _emitError,
440 onDone: completer.complete, 402 onDone: completer.complete,
441 cancelOnError: true); 403 cancelOnError: true);
442 return completer.future; 404 return completer.future;
443 } 405 }
444 406
445 /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
446 ///
447 /// 200ms is short in terms of human interaction, but longer than any Mac OS
448 /// watcher tests take on the bots, so it should be safe to assume that any
449 /// bogus events will be signaled in that time frame.
450 Future _waitForBogusEvents() {
451 var completer = new Completer();
452 _bogusEventTimer = new Timer(
453 new Duration(milliseconds: 200),
454 completer.complete);
455 return completer.future;
456 }
457
458 /// Emit an event with the given [type] and [path]. 407 /// Emit an event with the given [type] and [path].
459 void _emitEvent(ChangeType type, String path) { 408 void _emitEvent(ChangeType type, String path) {
460 if (!isReady) return; 409 if (!isReady) return;
461 410
462 if (MacOSDirectoryWatcher.logDebugInfo) {
463 print("[$_id] emitting $type ${p.relative(path, from: directory)}");
464 }
465
466 _eventsController.add(new WatchEvent(type, path)); 411 _eventsController.add(new WatchEvent(type, path));
467 } 412 }
468 413
469 /// Emit an error, then close the watcher. 414 /// Emit an error, then close the watcher.
470 void _emitError(error, StackTrace stackTrace) { 415 void _emitError(error, StackTrace stackTrace) {
471 if (MacOSDirectoryWatcher.logDebugInfo) {
472 print("[$_id] emitting error: $error\n" +
473 "${new Chain.forTrace(stackTrace).terse}");
474 }
475 _eventsController.addError(error, stackTrace); 416 _eventsController.addError(error, stackTrace);
476 close(); 417 close();
477 } 418 }
478
479 // TODO(nweiz): remove this when issue 15042 is fixed.
480 /// Return a human-friendly string representation of [event].
481 String _formatEvent(FileSystemEvent event) {
482 if (event == null) return 'null';
483
484 var path = p.relative(event.path, from: directory);
485 var type = event.isDirectory ? 'directory' : 'file';
486 if (event is FileSystemCreateEvent) {
487 return "create $type $path";
488 } else if (event is FileSystemDeleteEvent) {
489 return "delete $type $path";
490 } else if (event is FileSystemModifyEvent) {
491 return "modify $type $path";
492 } else if (event is FileSystemMoveEvent) {
493 return "move $type $path to "
494 "${p.relative(event.destination, from: directory)}";
495 }
496 }
497 } 419 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698