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 |