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 |