| 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 |