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