Chromium Code Reviews| 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; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:io'; | |
| 9 | |
| 10 import 'package:crypto/crypto.dart'; | |
| 11 | |
| 12 import 'stat.dart'; | |
| 13 import 'watch_event.dart'; | |
| 14 | |
| 15 /// Watches the contents of a directory and emits [WatchEvent]s when something | |
| 16 /// in the directory has changed. | |
| 17 class DirectoryWatcher { | |
| 18 /// The directory whose contents are being monitored. | |
| 19 final String directory; | |
| 20 | |
| 21 /// The broadcast [Stream] of events that have occurred to files in | |
| 22 /// [directory]. | |
| 23 /// | |
| 24 /// Changes will only be monitored while this stream has subscribers. Any | |
| 25 /// file changes that occur during periods when there are no subscribers | |
| 26 /// will not be reported the next time a subscriber is added. | |
| 27 Stream<WatchEvent> get events => _events.stream; | |
| 28 StreamController<WatchEvent> _events; | |
| 29 | |
| 30 _WatchState _state = _WatchState.notWatching; | |
| 31 | |
| 32 /// A [Future] that completes when the watcher is initialized and watching | |
| 33 /// for file changes. | |
| 34 /// | |
| 35 /// If the watcher is not currently monitoring the directory (because there | |
| 36 /// are no subscribers to [events]), this returns a future that isn't | |
| 37 /// complete yet. It will complete when a subscriber starts listening and | |
| 38 /// the watcher finishes any initialization work it needs to do. | |
| 39 /// | |
| 40 /// If the watcher is already monitoring, this returns an already complete | |
| 41 /// future. | |
| 42 Future get ready => _ready.future; | |
| 43 Completer _ready = new Completer(); | |
| 44 | |
| 45 /// The previous status of the files in the directory. | |
| 46 /// | |
| 47 /// Used to tell which files have been modified. | |
| 48 final _statuses = new Map<String, _FileStatus>(); | |
| 49 | |
| 50 /// Creates a new [DirectoryWatcher] monitoring [directory]. | |
| 51 DirectoryWatcher(this.directory) { | |
| 52 _events = new StreamController<WatchEvent>.broadcast(onListen: () { | |
| 53 _state = _state.listen(this); | |
| 54 }, onCancel: () { | |
| 55 _state = _state.cancel(this); | |
| 56 }); | |
| 57 } | |
| 58 | |
| 59 /// Starts the asynchronous polling process. | |
| 60 /// | |
| 61 /// Scans the contents of the directory and compares the results to the | |
| 62 /// previous scan. Loops to continue monitoring as long as there are | |
| 63 /// subscribers to the [events] stream. | |
| 64 Future _watch() { | |
| 65 var files = new Set<String>(); | |
| 66 | |
| 67 var stream = new Directory(directory).list(recursive: true); | |
| 68 | |
| 69 return stream.map((entity) { | |
| 70 if (entity is! File) return new Future.value(); | |
| 71 files.add(entity.path); | |
| 72 // TODO(rnystrom): These all run as fast as possible and read the | |
| 73 // contents of the files. That means there's a pretty big IO hit all at | |
| 74 // once. Maybe these should be queued up and rate limited? | |
| 75 return _refreshFile(entity.path); | |
| 76 }).toList().then((futures) { | |
| 77 // Once the listing is done, make sure to wait until each file is also | |
| 78 // done. | |
| 79 return Future.wait(futures); | |
| 80 }).then((_) { | |
| 81 var removedFiles = _statuses.keys.toSet().difference(files); | |
| 82 for (var removed in removedFiles) { | |
| 83 if (_state.shouldNotify) { | |
| 84 _events.add(new WatchEvent(ChangeType.REMOVE, removed)); | |
| 85 } | |
| 86 _statuses.remove(removed); | |
| 87 } | |
| 88 | |
| 89 var previousState = _state; | |
| 90 _state = _state.finish(this); | |
| 91 | |
| 92 // If we were already sending notifications, add a bit of delay before | |
| 93 // restarting just so that we don't whale on the file system. | |
| 94 // TODO(rnystrom): Tune this and/or make it tunable? | |
| 95 if (_state.shouldNotify) { | |
| 96 return new Future.delayed(new Duration(seconds: 1)); | |
| 97 } | |
| 98 }).then((_) { | |
| 99 // Make sure we haven't transitioned to a non-watching state during the | |
| 100 // delay. | |
| 101 if (_state.shouldWatch) _watch(); | |
| 102 }); | |
| 103 } | |
| 104 | |
| 105 /// Compares the current state of the file at [path] to the state it was in | |
| 106 /// the last time it was scanned. | |
| 107 Future _refreshFile(String path) { | |
| 108 return getModificationTime(path).then((modified) { | |
| 109 var lastStatus = _statuses[path]; | |
| 110 | |
| 111 // If it's modification time hasn't changed, assume the file is unchanged. | |
| 112 if (lastStatus != null && lastStatus.modified == modified) return; | |
| 113 | |
| 114 return _hashFile(path).then((hash) { | |
| 115 var status = new _FileStatus(modified, hash); | |
| 116 _statuses[path] = status; | |
| 117 | |
| 118 // Only notify if the file contents changed. | |
| 119 if (_state.shouldNotify && | |
| 120 (lastStatus == null || !_sameHash(lastStatus.hash, hash))) { | |
| 121 var change = lastStatus == null ? ChangeType.ADD : ChangeType.MODIFY; | |
| 122 _events.add(new WatchEvent(change, path)); | |
| 123 } | |
| 124 }); | |
| 125 }); | |
| 126 } | |
| 127 | |
| 128 /// Calculates the SHA-1 hash of the file at [path]. | |
| 129 Future<List<int>> _hashFile(String path) { | |
| 130 return new File(path).readAsBytes().then((bytes) { | |
| 131 var sha1 = new SHA1(); | |
| 132 sha1.add(bytes); | |
| 133 return sha1.close(); | |
| 134 }); | |
| 135 } | |
| 136 | |
| 137 /// Returns `true` if [a] and [b] are the same hash value, i.e. the same | |
| 138 /// series of byte values. | |
| 139 bool _sameHash(List<int> a, List<int> b) { | |
| 140 // Hashes should always be the same size. | |
| 141 assert(a.length == b.length); | |
| 142 | |
| 143 for (var i = 0; i < a.length; i++) { | |
| 144 if (a[i] != b[i]) return false; | |
| 145 } | |
| 146 | |
| 147 return true; | |
| 148 } | |
| 149 } | |
| 150 | |
| 151 /// An "event" that is sent to the [_WatchState] FSM to trigger state | |
| 152 /// transitions. | |
| 153 typedef _WatchState _WatchStateEvent(DirectoryWatcher watcher); | |
| 154 | |
| 155 /// The different states that the watcher can be in and the transitions between | |
| 156 /// them. | |
| 157 /// | |
| 158 /// This class defines a finite state machine for keeping track of what the | |
| 159 /// asynchronous file polling is doing. Each instance of this is a state in the | |
| 160 /// machine and its [listen], [cancel], and [finish] fields define the state | |
| 161 /// transitions when those events occur. | |
| 162 class _WatchState { | |
| 163 /// The watcher has no subscribers. | |
| 164 static final notWatching = new _WatchState( | |
| 165 listen: (watcher) { | |
| 166 watcher._watch(); | |
| 167 return _WatchState.scanning; | |
| 168 }); | |
| 169 | |
| 170 /// The watcher has subscribers and is scanning for pre-existing files. | |
| 171 static final scanning = new _WatchState( | |
| 172 cancel: (watcher) { | |
| 173 // No longer watching, so create a new incomplete ready future. | |
| 174 watcher._ready = new Completer(); | |
| 175 return _WatchState.cancelling; | |
| 176 }, finish: (watcher) { | |
| 177 watcher._ready.complete(); | |
| 178 return _WatchState.watching; | |
| 179 }, shouldWatch: true); | |
| 180 | |
| 181 /// The watcher was unsubscribed while polling and we're waiting for the poll | |
| 182 /// to finish. | |
| 183 static final cancelling = new _WatchState( | |
| 184 listen: (_) => _WatchState.scanning, | |
| 185 finish: (watcher) { | |
| 186 return _WatchState.notWatching; | |
| 187 }); | |
|
nweiz
2013/07/12 01:04:29
Left-over formatting change?
Bob Nystrom
2013/07/12 17:46:16
Yup! Done.
| |
| 188 | |
| 189 /// The watcher has subscribers, we have scanned for pre-existing files and | |
| 190 /// now we're polling for changes. | |
| 191 static final watching = new _WatchState( | |
| 192 cancel: (watcher) { | |
| 193 // No longer watching, so create a new incomplete ready future. | |
| 194 watcher._ready = new Completer(); | |
| 195 return _WatchState.cancelling; | |
| 196 }, finish: (_) => _WatchState.watching, | |
| 197 shouldWatch: true, shouldNotify: true); | |
| 198 | |
| 199 /// Called when the first subscriber to the watcher has been added. | |
| 200 final _WatchStateEvent listen; | |
| 201 | |
| 202 /// Called when all subscriptions on the watcher have been cancelled. | |
| 203 final _WatchStateEvent cancel; | |
| 204 | |
| 205 /// Called when a poll loop has finished. | |
| 206 final _WatchStateEvent finish; | |
| 207 | |
| 208 /// If the directory watcher should be watching the file system while in | |
| 209 /// this state. | |
| 210 final bool shouldWatch; | |
| 211 | |
| 212 /// If a change event should be sent for a file modification while in this | |
| 213 /// state. | |
| 214 final bool shouldNotify; | |
| 215 | |
| 216 _WatchState({this.listen, this.cancel, this.finish, | |
| 217 this.shouldWatch: false, this.shouldNotify: false}); | |
| 218 } | |
| 219 | |
| 220 class _FileStatus { | |
| 221 /// The last time the file was modified. | |
| 222 DateTime modified; | |
| 223 | |
| 224 /// The SHA-1 hash of the contents of the file. | |
| 225 List<int> hash; | |
| 226 | |
| 227 _FileStatus(this.modified, this.hash); | |
| 228 } | |
| OLD | NEW |