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 'change_type.dart'; | |
| 13 import 'stat.dart'; | |
| 14 import 'watch_event.dart'; | |
| 15 | |
| 16 /// Watches the contents of a directory and emits [WatchEvents] when something | |
|
nweiz
2013/07/11 00:25:01
"[WatchEvent]s"
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 17 /// in the directory has changed. | |
| 18 class DirectoryWatcher { | |
| 19 /// The directory whose contents are being monitored. | |
| 20 final String directory; | |
| 21 | |
| 22 /// The [Stream] of file modification events that have occurred to files in | |
|
nweiz
2013/07/11 00:25:01
"file modification events" -> "events"
Also, ment
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 23 /// [directory]. | |
| 24 /// | |
| 25 /// Changes will only be monitored while this stream has subscribers. Any | |
| 26 /// file changes that occur during periods when there are no subscribers | |
| 27 /// will not be reported the next time a subscriber is added. | |
| 28 Stream<WatchEvent> get events => _events.stream; | |
| 29 | |
|
nweiz
2013/07/11 00:25:01
Style nit: I'd get rid of this newline to indicate
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 30 StreamController<WatchEvent> _events; | |
|
Siggi Cherem (dart-lang)
2013/07/10 22:53:53
change to final?
Bob Nystrom
2013/07/10 23:04:39
Wish I could. :(
The only way to set onListen and
nweiz
2013/07/11 00:25:01
I'd call this _eventsController.
Bob Nystrom
2013/07/11 18:33:20
Since the stream itself isn't stored, I think that
| |
| 31 | |
| 32 _WatchState _state = _WatchState.notWatching; | |
| 33 | |
| 34 /// The previous status of the files in the directory. Used to tell which | |
| 35 /// files have been modified. | |
|
nweiz
2013/07/11 00:25:01
Style nit: first sentence should be its own paragr
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 36 final _statuses = new Map<String, _FileStatus>(); | |
| 37 | |
| 38 /// Creates a new [DirectoryWatcher] monitoring [directory]. | |
| 39 DirectoryWatcher(this.directory) { | |
| 40 _events = new StreamController<WatchEvent>.broadcast(onListen: () { | |
| 41 _state = _state.listen(this); | |
| 42 }, onCancel: () { | |
| 43 _state = _state.cancel(this); | |
| 44 }); | |
| 45 } | |
| 46 | |
| 47 /// Starts the asynchronous polling process. | |
| 48 /// | |
| 49 /// Scans the contents of the directory and compares the results to the | |
| 50 /// previous scan. Loops to continue monitoring as long as there are | |
| 51 /// subscribers to the [events] stream. | |
| 52 Future _watch() { | |
| 53 var files = new Set<String>(); | |
| 54 | |
| 55 var stream = new Directory(directory).list( | |
| 56 recursive: true, followLinks: true); | |
|
nweiz
2013/07/11 00:25:01
I'd much rather we use pub's listDirectory here, e
Bob Nystrom
2013/07/11 18:33:20
I think it's important for watcher to use a stream
nweiz
2013/07/11 22:29:00
They've marked an important bug as wontfix: https:
Bob Nystrom
2013/07/12 00:31:09
I think you marked that bug WontFix. I changed thi
nweiz
2013/07/12 01:04:28
Soren incorrectly marked it as fixed. I changed th
Bob Nystrom
2013/07/12 17:46:16
Yes, for now I'm OK with not supported symlinked d
| |
| 57 | |
| 58 var futures = []; | |
| 59 return stream.forEach((entity) { | |
| 60 if (entity is File) { | |
| 61 files.add(entity.path); | |
| 62 futures.add(_refreshFile(entity.path)); | |
| 63 } | |
| 64 }).then((_) { | |
| 65 // Once the listing is done, make sure to wait until each file is also | |
| 66 // done. | |
| 67 return Future.wait(futures); | |
| 68 }).then((_) { | |
|
nweiz
2013/07/11 00:25:01
I think the following would be a little cleaner he
Bob Nystrom
2013/07/11 18:33:20
Had to stick a toList() in there, but this is a bi
| |
| 69 var removedFiles = _statuses.keys.toSet().difference(files); | |
| 70 for (var removed in removedFiles) { | |
| 71 if (_state.shouldNotify) { | |
|
nweiz
2013/07/11 00:25:01
It's a little weird that this is a flag, but you j
Bob Nystrom
2013/07/11 18:33:20
Changed to check shouldNotify below.
| |
| 72 _events.add(new WatchEvent(ChangeType.REMOVE, removed)); | |
| 73 } | |
| 74 _statuses.remove(removed); | |
| 75 } | |
| 76 | |
| 77 _state = _state.finish(this); | |
| 78 | |
| 79 // If the new state isn't watching, just stop. | |
| 80 if (!_state.shouldWatch) return; | |
| 81 | |
| 82 // If we're in the "watching" state, add a bit of delay before restarting | |
| 83 // just so that we don't whale on the file system. | |
| 84 // TODO(rnystrom): Tune this and/or make it tunable? | |
| 85 if (_state == _WatchState.watching) { | |
| 86 return new Future.delayed(new Duration(seconds: 1)); | |
| 87 } | |
| 88 | |
| 89 // Otherwise, loop. | |
| 90 return _watch(); | |
|
nweiz
2013/07/11 00:25:01
Shouldn't this just be "return;"? Otherwise, would
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 91 }).then((_) { | |
| 92 // Make sure we haven't transitioned to a non-watching state during the | |
| 93 // delay. | |
| 94 if (_state.shouldWatch) _watch(); | |
| 95 }); | |
| 96 } | |
| 97 | |
| 98 /// Compares the current state of the file at [path] to the state it was in | |
| 99 /// the last time it was scanned. | |
| 100 Future _refreshFile(String path) { | |
| 101 return getModificationTime(path).then((modified) { | |
| 102 var lastStatus = _statuses[path]; | |
| 103 | |
| 104 // If it's modification time hasn't changed, assume the file is unchanged. | |
| 105 if (lastStatus != null && lastStatus.modified == modified) { | |
| 106 return false; | |
|
nweiz
2013/07/11 00:25:01
Is this return value used anywhere?
Bob Nystrom
2013/07/11 18:33:20
Oops. Not anymore. Used to have the later future s
| |
| 107 } | |
| 108 | |
| 109 return _hashFile(path).then((hash) { | |
| 110 var status = new _FileStatus(modified, hash); | |
| 111 _statuses[path] = status; | |
| 112 | |
| 113 if (!_state.shouldNotify) return; | |
|
nweiz
2013/07/11 00:25:01
Fold this into the following "if", or vice versa.
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 114 | |
| 115 // Only notify if the file contents are changed. | |
| 116 if (lastStatus == null || !_sameHash(lastStatus.hash, hash)) { | |
|
Siggi Cherem (dart-lang)
2013/07/10 22:53:53
consider making this a configuration option - some
Bob Nystrom
2013/07/10 23:04:39
I expect this to get more configurable over time,
| |
| 117 var change = lastStatus == null ? ChangeType.ADD : ChangeType.MODIFY; | |
| 118 _events.add(new WatchEvent(change, path)); | |
| 119 } | |
| 120 }); | |
| 121 }); | |
| 122 } | |
| 123 | |
| 124 /// Calculates the SHA-1 hash of the file at [path]. | |
| 125 Future<List<int>> _hashFile(String path) { | |
| 126 return new File(path).readAsBytes().then((bytes) { | |
| 127 var sha1 = new SHA1(); | |
| 128 sha1.add(bytes); | |
| 129 return sha1.close(); | |
| 130 }); | |
| 131 } | |
| 132 | |
| 133 /// Returns `true` if [a] and [b] are the same hash value, i.e. the same | |
| 134 /// series of byte values. | |
| 135 bool _sameHash(List<int> a, List<int> b) { | |
|
Siggi Cherem (dart-lang)
2013/07/10 22:53:53
alternatively call CrytoUtils.bytesToHex and compa
Bob Nystrom
2013/07/10 23:04:39
Hmm, is that faster? Seems like a roundabout way t
| |
| 136 // Hashes should always be the same size. | |
| 137 assert(a.length == b.length); | |
| 138 | |
| 139 for (var i = 0; i < a.length; i++) { | |
| 140 if (a[i] != b[i]) return false; | |
| 141 } | |
| 142 | |
| 143 return true; | |
| 144 } | |
| 145 } | |
| 146 | |
| 147 /// An "event" that is sent to the [_WatchState] FSM to trigger state | |
| 148 /// transitions. | |
| 149 typedef _WatchState _WatchStateEvent(DirectoryWatcher watcher); | |
| 150 | |
| 151 /// The different states that the watcher can be in and the transitions between | |
| 152 /// them. | |
| 153 /// | |
| 154 /// This class defines a finite state machine for keeping track of what the | |
| 155 /// asynchronous file polling is doing. Each instance of this is a state in the | |
| 156 /// machine and its [listen], [cancel], and [finish] fields define the state | |
| 157 /// transitions when those events occur. | |
| 158 class _WatchState { | |
| 159 /// The watcher has no subscribers. | |
| 160 static final notWatching = new _WatchState( | |
| 161 listen: (watcher) { | |
| 162 watcher._watch(); | |
|
nweiz
2013/07/11 00:25:01
If you move this into DirectoryWatcher, then _Watc
Bob Nystrom
2013/07/11 18:33:20
You mean move the state instances?
nweiz
2013/07/11 22:29:00
No, I mean the call to "watcher._watch" in particu
Bob Nystrom
2013/07/12 00:31:09
Agreed on both accounts. In this case, I think hav
nweiz
2013/07/12 01:04:28
Okay.
| |
| 163 return _WatchState.scanning; | |
| 164 }); | |
| 165 | |
| 166 /// The watcher has subscribers and is scanning for pre-existing files. | |
| 167 static final scanning = new _WatchState( | |
| 168 cancel: (_) => _WatchState.cancelling, | |
| 169 finish: (_) => _WatchState.watching, | |
| 170 shouldWatch: true); | |
| 171 | |
| 172 /// The watcher was unsubscribed while polling and we're waiting for the poll | |
| 173 /// to finish. | |
| 174 static final cancelling = new _WatchState( | |
| 175 listen: (_) => _WatchState.scanning, | |
| 176 finish: (_) => _WatchState.notWatching); | |
| 177 | |
| 178 /// The watcher has subscribers, we have scanned for pre-existing files and | |
| 179 /// now we're waiting for changes to come in. | |
|
nweiz
2013/07/11 00:25:01
"waiting for changes to come in" -> "polling for c
Bob Nystrom
2013/07/11 18:33:20
Done.
| |
| 180 static final watching = new _WatchState( | |
| 181 cancel: (_) => _WatchState.cancelling, | |
| 182 finish: (_) => _WatchState.watching, | |
| 183 shouldWatch: true, shouldNotify: true); | |
| 184 | |
| 185 /// Asserts that an event is not expected for some state. | |
| 186 static _WatchState _badState(DirectoryWatcher watcher) { | |
| 187 // Should not receive this event in this state. | |
| 188 assert(false); | |
|
nweiz
2013/07/11 00:25:01
If it's possible, it would be nice to provide some
Bob Nystrom
2013/07/11 18:33:20
I just went ahead and deleted this. It only exist(
| |
| 189 } | |
| 190 | |
| 191 /// Called when the first subscriber to the watcher has been added. | |
| 192 final _WatchStateEvent listen; | |
| 193 | |
| 194 /// Called when all subscriptions on the watcher have been cancelled. | |
| 195 final _WatchStateEvent cancel; | |
| 196 | |
| 197 /// Called when a poll loop has finished. | |
| 198 final _WatchStateEvent finish; | |
| 199 | |
| 200 /// If the directory watcher should be watching the file system while in | |
| 201 /// this state. | |
| 202 final bool shouldWatch; | |
| 203 | |
| 204 /// `true` if a change event should be sent for a file modification while | |
| 205 /// in this state. | |
| 206 final bool shouldNotify; | |
| 207 | |
| 208 _WatchState({ | |
| 209 _WatchStateEvent listen, | |
| 210 _WatchStateEvent cancel, | |
| 211 _WatchStateEvent finish, | |
| 212 this.shouldWatch: false, | |
| 213 this.shouldNotify: false}) | |
| 214 : listen = listen != null ? listen : _badState, | |
| 215 cancel = cancel != null ? cancel : _badState, | |
| 216 finish = finish != null ? finish : _badState; | |
| 217 } | |
| 218 | |
| 219 class _FileStatus { | |
| 220 /// The last time the file was modified. | |
| 221 DateTime modified; | |
| 222 | |
| 223 /// The SHA-1 hash of the contents of the file. | |
| 224 List<int> hash; | |
| 225 | |
| 226 _FileStatus(this.modified, this.hash); | |
| 227 } | |
| OLD | NEW |