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 |