OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library watcher.directory_watcher.polling; | 5 library watcher.directory_watcher.polling; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:io'; | 8 import 'dart:io'; |
9 | 9 |
10 import 'package:crypto/crypto.dart'; | |
11 import 'package:stack_trace/stack_trace.dart'; | 10 import 'package:stack_trace/stack_trace.dart'; |
12 | 11 |
13 import '../async_queue.dart'; | 12 import '../async_queue.dart'; |
14 import '../stat.dart'; | 13 import '../stat.dart'; |
15 import '../utils.dart'; | 14 import '../utils.dart'; |
16 import '../watch_event.dart'; | 15 import '../watch_event.dart'; |
17 import 'resubscribable.dart'; | 16 import 'resubscribable.dart'; |
18 | 17 |
19 /// Periodically polls a directory for changes. | 18 /// Periodically polls a directory for changes. |
20 class PollingDirectoryWatcher extends ResubscribableDirectoryWatcher { | 19 class PollingDirectoryWatcher extends ResubscribableDirectoryWatcher { |
(...skipping 18 matching lines...) Expand all Loading... |
39 | 38 |
40 bool get isReady => _ready.isCompleted; | 39 bool get isReady => _ready.isCompleted; |
41 | 40 |
42 Future get ready => _ready.future; | 41 Future get ready => _ready.future; |
43 final _ready = new Completer(); | 42 final _ready = new Completer(); |
44 | 43 |
45 /// The amount of time the watcher pauses between successive polls of the | 44 /// The amount of time the watcher pauses between successive polls of the |
46 /// directory contents. | 45 /// directory contents. |
47 final Duration _pollingDelay; | 46 final Duration _pollingDelay; |
48 | 47 |
49 /// The previous status of the files in the directory. | 48 /// The previous modification times of the files in the directory. |
50 /// | 49 /// |
51 /// Used to tell which files have been modified. | 50 /// Used to tell which files have been modified. |
52 final _statuses = new Map<String, _FileStatus>(); | 51 final _lastModifieds = new Map<String, DateTime>(); |
53 | 52 |
54 /// The subscription used while [directory] is being listed. | 53 /// The subscription used while [directory] is being listed. |
55 /// | 54 /// |
56 /// Will be `null` if a list is not currently happening. | 55 /// Will be `null` if a list is not currently happening. |
57 StreamSubscription<FileSystemEntity> _listSubscription; | 56 StreamSubscription<FileSystemEntity> _listSubscription; |
58 | 57 |
59 /// The queue of files waiting to be processed to see if they have been | 58 /// The queue of files waiting to be processed to see if they have been |
60 /// modified. | 59 /// modified. |
61 /// | 60 /// |
62 /// Processing a file is asynchronous, as is listing the directory, so the | 61 /// Processing a file is asynchronous, as is listing the directory, so the |
(...skipping 19 matching lines...) Expand all Loading... |
82 | 81 |
83 void close() { | 82 void close() { |
84 _events.close(); | 83 _events.close(); |
85 | 84 |
86 // If we're in the middle of listing the directory, stop. | 85 // If we're in the middle of listing the directory, stop. |
87 if (_listSubscription != null) _listSubscription.cancel(); | 86 if (_listSubscription != null) _listSubscription.cancel(); |
88 | 87 |
89 // Don't process any remaining files. | 88 // Don't process any remaining files. |
90 _filesToProcess.clear(); | 89 _filesToProcess.clear(); |
91 _polledFiles.clear(); | 90 _polledFiles.clear(); |
92 _statuses.clear(); | 91 _lastModifieds.clear(); |
93 } | 92 } |
94 | 93 |
95 /// Scans the contents of the directory once to see which files have been | 94 /// Scans the contents of the directory once to see which files have been |
96 /// added, removed, and modified. | 95 /// added, removed, and modified. |
97 void _poll() { | 96 void _poll() { |
98 _filesToProcess.clear(); | 97 _filesToProcess.clear(); |
99 _polledFiles.clear(); | 98 _polledFiles.clear(); |
100 | 99 |
101 endListing() { | 100 endListing() { |
102 assert(!_events.isClosed); | 101 assert(!_events.isClosed); |
(...skipping 25 matching lines...) Expand all Loading... |
128 | 127 |
129 /// Processes [file] to determine if it has been modified since the last | 128 /// Processes [file] to determine if it has been modified since the last |
130 /// time it was scanned. | 129 /// time it was scanned. |
131 Future _processFile(String file) { | 130 Future _processFile(String file) { |
132 // `null` is the sentinel which means the directory listing is complete. | 131 // `null` is the sentinel which means the directory listing is complete. |
133 if (file == null) return _completePoll(); | 132 if (file == null) return _completePoll(); |
134 | 133 |
135 return getModificationTime(file).then((modified) { | 134 return getModificationTime(file).then((modified) { |
136 if (_events.isClosed) return null; | 135 if (_events.isClosed) return null; |
137 | 136 |
138 var lastStatus = _statuses[file]; | 137 var lastModified = _lastModifieds[file]; |
139 | 138 |
140 // If its modification time hasn't changed, assume the file is unchanged. | 139 // If its modification time hasn't changed, assume the file is unchanged. |
141 if (lastStatus != null && lastStatus.modified == modified) { | 140 if (lastModified != null && lastModified == modified) { |
142 // The file is still here. | 141 // The file is still here. |
143 _polledFiles.add(file); | 142 _polledFiles.add(file); |
144 return null; | 143 return null; |
145 } | 144 } |
146 | 145 |
147 return _hashFile(file).then((hash) { | 146 if (_events.isClosed) return null; |
148 if (_events.isClosed) return; | |
149 | 147 |
150 var status = new _FileStatus(modified, hash); | 148 _lastModifieds[file] = modified; |
151 _statuses[file] = status; | 149 _polledFiles.add(file); |
152 _polledFiles.add(file); | |
153 | 150 |
154 // Only notify if we're ready to emit events. | 151 // Only notify if we're ready to emit events. |
155 if (!isReady) return; | 152 if (!isReady) return null; |
156 | 153 |
157 // And the file is different. | 154 var type = lastModified == null ? ChangeType.ADD : ChangeType.MODIFY; |
158 var changed = lastStatus == null || !_sameHash(lastStatus.hash, hash); | 155 _events.add(new WatchEvent(type, file)); |
159 if (!changed) return; | |
160 | |
161 var type = lastStatus == null ? ChangeType.ADD : ChangeType.MODIFY; | |
162 _events.add(new WatchEvent(type, file)); | |
163 }); | |
164 }); | 156 }); |
165 } | 157 } |
166 | 158 |
167 /// After the directory listing is complete, this determines which files were | 159 /// After the directory listing is complete, this determines which files were |
168 /// removed and then restarts the next poll. | 160 /// removed and then restarts the next poll. |
169 Future _completePoll() { | 161 Future _completePoll() { |
170 // Any files that were not seen in the last poll but that we have a | 162 // Any files that were not seen in the last poll but that we have a |
171 // status for must have been removed. | 163 // status for must have been removed. |
172 var removedFiles = _statuses.keys.toSet().difference(_polledFiles); | 164 var removedFiles = _lastModifieds.keys.toSet().difference(_polledFiles); |
173 for (var removed in removedFiles) { | 165 for (var removed in removedFiles) { |
174 if (isReady) _events.add(new WatchEvent(ChangeType.REMOVE, removed)); | 166 if (isReady) _events.add(new WatchEvent(ChangeType.REMOVE, removed)); |
175 _statuses.remove(removed); | 167 _lastModifieds.remove(removed); |
176 } | 168 } |
177 | 169 |
178 if (!isReady) _ready.complete(); | 170 if (!isReady) _ready.complete(); |
179 | 171 |
180 // Wait and then poll again. | 172 // Wait and then poll again. |
181 return new Future.delayed(_pollingDelay).then((_) { | 173 return new Future.delayed(_pollingDelay).then((_) { |
182 if (_events.isClosed) return; | 174 if (_events.isClosed) return; |
183 _poll(); | 175 _poll(); |
184 }); | 176 }); |
185 } | 177 } |
186 | |
187 /// Calculates the SHA-1 hash of the file at [path]. | |
188 Future<List<int>> _hashFile(String path) { | |
189 return Chain.track(new File(path).readAsBytes()).then((bytes) { | |
190 var sha1 = new SHA1(); | |
191 sha1.add(bytes); | |
192 return sha1.close(); | |
193 }); | |
194 } | |
195 | |
196 /// Returns `true` if [a] and [b] are the same hash value, i.e. the same | |
197 /// series of byte values. | |
198 bool _sameHash(List<int> a, List<int> b) { | |
199 // Hashes should always be the same size. | |
200 assert(a.length == b.length); | |
201 | |
202 for (var i = 0; i < a.length; i++) { | |
203 if (a[i] != b[i]) return false; | |
204 } | |
205 | |
206 return true; | |
207 } | |
208 } | 178 } |
209 | |
210 class _FileStatus { | |
211 /// The last time the file was modified. | |
212 DateTime modified; | |
213 | |
214 /// The SHA-1 hash of the contents of the file. | |
215 List<int> hash; | |
216 | |
217 _FileStatus(this.modified, this.hash); | |
218 } | |
219 | |
OLD | NEW |