| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 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. | |
| 4 | |
| 5 library watcher.directory_watcher.linux; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:io'; | |
| 9 | |
| 10 import '../directory_watcher.dart'; | |
| 11 import '../resubscribable.dart'; | |
| 12 import '../utils.dart'; | |
| 13 import '../watch_event.dart'; | |
| 14 | |
| 15 /// Uses the inotify subsystem to watch for filesystem events. | |
| 16 /// | |
| 17 /// Inotify doesn't suport recursively watching subdirectories, nor does | |
| 18 /// [Directory.watch] polyfill that functionality. This class polyfills it | |
| 19 /// instead. | |
| 20 /// | |
| 21 /// This class also compensates for the non-inotify-specific issues of | |
| 22 /// [Directory.watch] producing multiple events for a single logical action | |
| 23 /// (issue 14372) and providing insufficient information about move events | |
| 24 /// (issue 14424). | |
| 25 class LinuxDirectoryWatcher extends ResubscribableWatcher | |
| 26 implements DirectoryWatcher { | |
| 27 String get directory => path; | |
| 28 | |
| 29 LinuxDirectoryWatcher(String directory) | |
| 30 : super(directory, () => new _LinuxDirectoryWatcher(directory)); | |
| 31 } | |
| 32 | |
| 33 class _LinuxDirectoryWatcher | |
| 34 implements DirectoryWatcher, ManuallyClosedWatcher { | |
| 35 String get directory => path; | |
| 36 final String path; | |
| 37 | |
| 38 Stream<WatchEvent> get events => _eventsController.stream; | |
| 39 final _eventsController = new StreamController<WatchEvent>.broadcast(); | |
| 40 | |
| 41 bool get isReady => _readyCompleter.isCompleted; | |
| 42 | |
| 43 Future get ready => _readyCompleter.future; | |
| 44 final _readyCompleter = new Completer(); | |
| 45 | |
| 46 /// The last known state for each entry in this directory. | |
| 47 /// | |
| 48 /// The keys in this map are the paths to the directory entries; the values | |
| 49 /// are [_EntryState]s indicating whether the entries are files or | |
| 50 /// directories. | |
| 51 final _entries = new Map<String, _EntryState>(); | |
| 52 | |
| 53 /// The watchers for subdirectories of [directory]. | |
| 54 final _subWatchers = new Map<String, _LinuxDirectoryWatcher>(); | |
| 55 | |
| 56 /// A set of all subscriptions that this watcher subscribes to. | |
| 57 /// | |
| 58 /// These are gathered together so that they may all be canceled when the | |
| 59 /// watcher is closed. | |
| 60 final _subscriptions = new Set<StreamSubscription>(); | |
| 61 | |
| 62 _LinuxDirectoryWatcher(this.path) { | |
| 63 // Batch the inotify changes together so that we can dedup events. | |
| 64 var innerStream = new Directory(path).watch() | |
| 65 .transform(new BatchedStreamTransformer<FileSystemEvent>()); | |
| 66 _listen(innerStream, _onBatch, | |
| 67 onError: _eventsController.addError, | |
| 68 onDone: _onDone); | |
| 69 | |
| 70 _listen(new Directory(path).list(), (entity) { | |
| 71 _entries[entity.path] = new _EntryState(entity is Directory); | |
| 72 if (entity is! Directory) return; | |
| 73 _watchSubdir(entity.path); | |
| 74 }, onError: (error, stackTrace) { | |
| 75 _eventsController.addError(error, stackTrace); | |
| 76 close(); | |
| 77 }, onDone: () { | |
| 78 _waitUntilReady().then((_) => _readyCompleter.complete()); | |
| 79 }, cancelOnError: true); | |
| 80 } | |
| 81 | |
| 82 /// Returns a [Future] that completes once all the subdirectory watchers are | |
| 83 /// fully initialized. | |
| 84 Future _waitUntilReady() { | |
| 85 return Future.wait(_subWatchers.values.map((watcher) => watcher.ready)) | |
| 86 .then((_) { | |
| 87 if (_subWatchers.values.every((watcher) => watcher.isReady)) return null; | |
| 88 return _waitUntilReady(); | |
| 89 }); | |
| 90 } | |
| 91 | |
| 92 void close() { | |
| 93 for (var subscription in _subscriptions) { | |
| 94 subscription.cancel(); | |
| 95 } | |
| 96 for (var watcher in _subWatchers.values) { | |
| 97 watcher.close(); | |
| 98 } | |
| 99 | |
| 100 _subWatchers.clear(); | |
| 101 _subscriptions.clear(); | |
| 102 _eventsController.close(); | |
| 103 } | |
| 104 | |
| 105 /// Returns all files (not directories) that this watcher knows of are | |
| 106 /// recursively in the watched directory. | |
| 107 Set<String> get _allFiles { | |
| 108 var files = new Set<String>(); | |
| 109 _getAllFiles(files); | |
| 110 return files; | |
| 111 } | |
| 112 | |
| 113 /// Helper function for [_allFiles]. | |
| 114 /// | |
| 115 /// Adds all files that this watcher knows of to [files]. | |
| 116 void _getAllFiles(Set<String> files) { | |
| 117 files.addAll(_entries.keys | |
| 118 .where((path) => _entries[path] == _EntryState.FILE).toSet()); | |
| 119 for (var watcher in _subWatchers.values) { | |
| 120 watcher._getAllFiles(files); | |
| 121 } | |
| 122 } | |
| 123 | |
| 124 /// Watch a subdirectory of [directory] for changes. | |
| 125 /// | |
| 126 /// If the subdirectory was added after [this] began emitting events, its | |
| 127 /// contents will be emitted as ADD events. | |
| 128 void _watchSubdir(String path) { | |
| 129 if (_subWatchers.containsKey(path)) return; | |
| 130 var watcher = new _LinuxDirectoryWatcher(path); | |
| 131 _subWatchers[path] = watcher; | |
| 132 | |
| 133 // TODO(nweiz): Catch any errors here that indicate that the directory in | |
| 134 // question doesn't exist and silently stop watching it instead of | |
| 135 // propagating the errors. | |
| 136 _listen(watcher.events, (event) { | |
| 137 if (isReady) _eventsController.add(event); | |
| 138 }, onError: (error, stackTrace) { | |
| 139 _eventsController.addError(error, stackTrace); | |
| 140 close(); | |
| 141 }, onDone: () { | |
| 142 if (_subWatchers[path] == watcher) _subWatchers.remove(path); | |
| 143 | |
| 144 // It's possible that a directory was removed and recreated very quickly. | |
| 145 // If so, make sure we're still watching it. | |
| 146 if (new Directory(path).existsSync()) _watchSubdir(path); | |
| 147 }); | |
| 148 | |
| 149 // TODO(nweiz): Right now it's possible for the watcher to emit an event for | |
| 150 // a file before the directory list is complete. This could lead to the user | |
| 151 // seeing a MODIFY or REMOVE event for a file before they see an ADD event, | |
| 152 // which is bad. We should handle that. | |
| 153 // | |
| 154 // One possibility is to provide a general means (e.g. | |
| 155 // `DirectoryWatcher.eventsAndExistingFiles`) to tell a watcher to emit | |
| 156 // events for all the files that already exist. This would be useful for | |
| 157 // top-level clients such as barback as well, and could be implemented with | |
| 158 // a wrapper similar to how listening/canceling works now. | |
| 159 | |
| 160 // If a directory is added after we're finished with the initial scan, emit | |
| 161 // an event for each entry in it. This gives the user consistently gets an | |
| 162 // event for every new file. | |
| 163 watcher.ready.then((_) { | |
| 164 if (!isReady || _eventsController.isClosed) return; | |
| 165 _listen(new Directory(path).list(recursive: true), (entry) { | |
| 166 if (entry is Directory) return; | |
| 167 _eventsController.add(new WatchEvent(ChangeType.ADD, entry.path)); | |
| 168 }, onError: (error, stackTrace) { | |
| 169 // Ignore an exception caused by the dir not existing. It's fine if it | |
| 170 // was added and then quickly removed. | |
| 171 if (error is FileSystemException) return; | |
| 172 | |
| 173 _eventsController.addError(error, stackTrace); | |
| 174 close(); | |
| 175 }, cancelOnError: true); | |
| 176 }); | |
| 177 } | |
| 178 | |
| 179 /// The callback that's run when a batch of changes comes in. | |
| 180 void _onBatch(List<FileSystemEvent> batch) { | |
| 181 var changedEntries = new Set<String>(); | |
| 182 var oldEntries = new Map.from(_entries); | |
| 183 | |
| 184 // inotify event batches are ordered by occurrence, so we treat them as a | |
| 185 // log of what happened to a file. | |
| 186 for (var event in batch) { | |
| 187 // If the watched directory is deleted or moved, we'll get a deletion | |
| 188 // event for it. Ignore it; we handle closing [this] when the underlying | |
| 189 // stream is closed. | |
| 190 if (event.path == path) continue; | |
| 191 | |
| 192 changedEntries.add(event.path); | |
| 193 | |
| 194 if (event is FileSystemMoveEvent) { | |
| 195 changedEntries.add(event.destination); | |
| 196 _changeEntryState(event.path, ChangeType.REMOVE, event.isDirectory); | |
| 197 _changeEntryState(event.destination, ChangeType.ADD, event.isDirectory); | |
| 198 } else { | |
| 199 _changeEntryState(event.path, _changeTypeFor(event), event.isDirectory); | |
| 200 } | |
| 201 } | |
| 202 | |
| 203 for (var path in changedEntries) { | |
| 204 emitEvent(ChangeType type) { | |
| 205 if (isReady) _eventsController.add(new WatchEvent(type, path)); | |
| 206 } | |
| 207 | |
| 208 var oldState = oldEntries[path]; | |
| 209 var newState = _entries[path]; | |
| 210 | |
| 211 if (oldState != _EntryState.FILE && newState == _EntryState.FILE) { | |
| 212 emitEvent(ChangeType.ADD); | |
| 213 } else if (oldState == _EntryState.FILE && newState == _EntryState.FILE) { | |
| 214 emitEvent(ChangeType.MODIFY); | |
| 215 } else if (oldState == _EntryState.FILE && newState != _EntryState.FILE) { | |
| 216 emitEvent(ChangeType.REMOVE); | |
| 217 } | |
| 218 | |
| 219 if (oldState == _EntryState.DIRECTORY) { | |
| 220 var watcher = _subWatchers.remove(path); | |
| 221 if (watcher == null) continue; | |
| 222 for (var path in watcher._allFiles) { | |
| 223 _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); | |
| 224 } | |
| 225 watcher.close(); | |
| 226 } | |
| 227 | |
| 228 if (newState == _EntryState.DIRECTORY) _watchSubdir(path); | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 /// Changes the known state of the entry at [path] based on [change] and | |
| 233 /// [isDir]. | |
| 234 void _changeEntryState(String path, ChangeType change, bool isDir) { | |
| 235 if (change == ChangeType.ADD || change == ChangeType.MODIFY) { | |
| 236 _entries[path] = new _EntryState(isDir); | |
| 237 } else { | |
| 238 assert(change == ChangeType.REMOVE); | |
| 239 _entries.remove(path); | |
| 240 } | |
| 241 } | |
| 242 | |
| 243 /// Determines the [ChangeType] associated with [event]. | |
| 244 ChangeType _changeTypeFor(FileSystemEvent event) { | |
| 245 if (event is FileSystemDeleteEvent) return ChangeType.REMOVE; | |
| 246 if (event is FileSystemCreateEvent) return ChangeType.ADD; | |
| 247 | |
| 248 assert(event is FileSystemModifyEvent); | |
| 249 return ChangeType.MODIFY; | |
| 250 } | |
| 251 | |
| 252 /// Handles the underlying event stream closing, indicating that the directory | |
| 253 /// being watched was removed. | |
| 254 void _onDone() { | |
| 255 // Most of the time when a directory is removed, its contents will get | |
| 256 // individual REMOVE events before the watch stream is closed -- in that | |
| 257 // case, [_entries] will be empty here. However, if the directory's removal | |
| 258 // is caused by a MOVE, we need to manually emit events. | |
| 259 if (isReady) { | |
| 260 _entries.forEach((path, state) { | |
| 261 if (state == _EntryState.DIRECTORY) return; | |
| 262 _eventsController.add(new WatchEvent(ChangeType.REMOVE, path)); | |
| 263 }); | |
| 264 } | |
| 265 | |
| 266 // The parent directory often gets a close event before the subdirectories | |
| 267 // are done emitting events. We wait for them to finish before we close | |
| 268 // [events] so that we can be sure to emit a remove event for every file | |
| 269 // that used to exist. | |
| 270 Future.wait(_subWatchers.values.map((watcher) { | |
| 271 try { | |
| 272 return watcher.events.toList(); | |
| 273 } on StateError catch (_) { | |
| 274 // It's possible that [watcher.events] is closed but the onDone event | |
| 275 // hasn't reached us yet. It's fine if so. | |
| 276 return new Future.value(); | |
| 277 } | |
| 278 })).then((_) => close()); | |
| 279 } | |
| 280 | |
| 281 /// Like [Stream.listen], but automatically adds the subscription to | |
| 282 /// [_subscriptions] so that it can be canceled when [close] is called. | |
| 283 void _listen(Stream stream, void onData(event), {Function onError, | |
| 284 void onDone(), bool cancelOnError}) { | |
| 285 var subscription; | |
| 286 subscription = stream.listen(onData, onError: onError, onDone: () { | |
| 287 _subscriptions.remove(subscription); | |
| 288 if (onDone != null) onDone(); | |
| 289 }, cancelOnError: cancelOnError); | |
| 290 _subscriptions.add(subscription); | |
| 291 } | |
| 292 } | |
| 293 | |
| 294 /// An enum for the possible states of entries in a watched directory. | |
| 295 class _EntryState { | |
| 296 final String _name; | |
| 297 | |
| 298 /// The entry is a file. | |
| 299 static const FILE = const _EntryState._("file"); | |
| 300 | |
| 301 /// The entry is a directory. | |
| 302 static const DIRECTORY = const _EntryState._("directory"); | |
| 303 | |
| 304 const _EntryState._(this._name); | |
| 305 | |
| 306 /// Returns [DIRECTORY] if [isDir] is true, and [FILE] otherwise. | |
| 307 factory _EntryState(bool isDir) => | |
| 308 isDir ? _EntryState.DIRECTORY : _EntryState.FILE; | |
| 309 | |
| 310 String toString() => _name; | |
| 311 } | |
| OLD | NEW |