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.polling; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import '../async_queue.dart'; | |
11 import '../directory_watcher.dart'; | |
12 import '../resubscribable.dart'; | |
13 import '../stat.dart'; | |
14 import '../utils.dart'; | |
15 import '../watch_event.dart'; | |
16 | |
17 /// Periodically polls a directory for changes. | |
18 class PollingDirectoryWatcher extends ResubscribableWatcher | |
19 implements DirectoryWatcher { | |
20 String get directory => path; | |
21 | |
22 /// Creates a new polling watcher monitoring [directory]. | |
23 /// | |
24 /// If [_pollingDelay] is passed, it specifies the amount of time the watcher | |
25 /// will pause between successive polls of the directory contents. Making this | |
26 /// shorter will give more immediate feedback at the expense of doing more IO | |
27 /// and higher CPU usage. Defaults to one second. | |
28 PollingDirectoryWatcher(String directory, {Duration pollingDelay}) | |
29 : super(directory, () { | |
30 return new _PollingDirectoryWatcher(directory, | |
31 pollingDelay != null ? pollingDelay : new Duration(seconds: 1)); | |
32 }); | |
33 } | |
34 | |
35 class _PollingDirectoryWatcher | |
36 implements DirectoryWatcher, ManuallyClosedWatcher { | |
37 String get directory => path; | |
38 final String path; | |
39 | |
40 Stream<WatchEvent> get events => _events.stream; | |
41 final _events = new StreamController<WatchEvent>.broadcast(); | |
42 | |
43 bool get isReady => _ready.isCompleted; | |
44 | |
45 Future get ready => _ready.future; | |
46 final _ready = new Completer(); | |
47 | |
48 /// The amount of time the watcher pauses between successive polls of the | |
49 /// directory contents. | |
50 final Duration _pollingDelay; | |
51 | |
52 /// The previous modification times of the files in the directory. | |
53 /// | |
54 /// Used to tell which files have been modified. | |
55 final _lastModifieds = new Map<String, DateTime>(); | |
56 | |
57 /// The subscription used while [directory] is being listed. | |
58 /// | |
59 /// Will be `null` if a list is not currently happening. | |
60 StreamSubscription<FileSystemEntity> _listSubscription; | |
61 | |
62 /// The queue of files waiting to be processed to see if they have been | |
63 /// modified. | |
64 /// | |
65 /// Processing a file is asynchronous, as is listing the directory, so the | |
66 /// queue exists to let each of those proceed at their own rate. The lister | |
67 /// will enqueue files as quickly as it can. Meanwhile, files are dequeued | |
68 /// and processed sequentially. | |
69 AsyncQueue<String> _filesToProcess; | |
70 | |
71 /// The set of files that have been seen in the current directory listing. | |
72 /// | |
73 /// Used to tell which files have been removed: files that are in [_statuses] | |
74 /// but not in here when a poll completes have been removed. | |
75 final _polledFiles = new Set<String>(); | |
76 | |
77 _PollingDirectoryWatcher(this.path, this._pollingDelay) { | |
78 _filesToProcess = new AsyncQueue<String>(_processFile, | |
79 onError: (e, stackTrace) { | |
80 if (!_events.isClosed) _events.addError(e, stackTrace); | |
81 }); | |
82 | |
83 _poll(); | |
84 } | |
85 | |
86 void close() { | |
87 _events.close(); | |
88 | |
89 // If we're in the middle of listing the directory, stop. | |
90 if (_listSubscription != null) _listSubscription.cancel(); | |
91 | |
92 // Don't process any remaining files. | |
93 _filesToProcess.clear(); | |
94 _polledFiles.clear(); | |
95 _lastModifieds.clear(); | |
96 } | |
97 | |
98 /// Scans the contents of the directory once to see which files have been | |
99 /// added, removed, and modified. | |
100 void _poll() { | |
101 _filesToProcess.clear(); | |
102 _polledFiles.clear(); | |
103 | |
104 endListing() { | |
105 assert(!_events.isClosed); | |
106 _listSubscription = null; | |
107 | |
108 // Null tells the queue consumer that we're done listing. | |
109 _filesToProcess.add(null); | |
110 } | |
111 | |
112 var stream = new Directory(path).list(recursive: true); | |
113 _listSubscription = stream.listen((entity) { | |
114 assert(!_events.isClosed); | |
115 | |
116 if (entity is! File) return; | |
117 _filesToProcess.add(entity.path); | |
118 }, onError: (error, stackTrace) { | |
119 if (!isDirectoryNotFoundException(error)) { | |
120 // It's some unknown error. Pipe it over to the event stream so the | |
121 // user can see it. | |
122 _events.addError(error, stackTrace); | |
123 } | |
124 | |
125 // When an error occurs, we end the listing normally, which has the | |
126 // desired effect of marking all files that were in the directory as | |
127 // being removed. | |
128 endListing(); | |
129 }, onDone: endListing, cancelOnError: true); | |
130 } | |
131 | |
132 /// Processes [file] to determine if it has been modified since the last | |
133 /// time it was scanned. | |
134 Future _processFile(String file) { | |
135 // `null` is the sentinel which means the directory listing is complete. | |
136 if (file == null) return _completePoll(); | |
137 | |
138 return getModificationTime(file).then((modified) { | |
139 if (_events.isClosed) return null; | |
140 | |
141 var lastModified = _lastModifieds[file]; | |
142 | |
143 // If its modification time hasn't changed, assume the file is unchanged. | |
144 if (lastModified != null && lastModified == modified) { | |
145 // The file is still here. | |
146 _polledFiles.add(file); | |
147 return null; | |
148 } | |
149 | |
150 if (_events.isClosed) return null; | |
151 | |
152 _lastModifieds[file] = modified; | |
153 _polledFiles.add(file); | |
154 | |
155 // Only notify if we're ready to emit events. | |
156 if (!isReady) return null; | |
157 | |
158 var type = lastModified == null ? ChangeType.ADD : ChangeType.MODIFY; | |
159 _events.add(new WatchEvent(type, file)); | |
160 }); | |
161 } | |
162 | |
163 /// After the directory listing is complete, this determines which files were | |
164 /// removed and then restarts the next poll. | |
165 Future _completePoll() { | |
166 // Any files that were not seen in the last poll but that we have a | |
167 // status for must have been removed. | |
168 var removedFiles = _lastModifieds.keys.toSet().difference(_polledFiles); | |
169 for (var removed in removedFiles) { | |
170 if (isReady) _events.add(new WatchEvent(ChangeType.REMOVE, removed)); | |
171 _lastModifieds.remove(removed); | |
172 } | |
173 | |
174 if (!isReady) _ready.complete(); | |
175 | |
176 // Wait and then poll again. | |
177 return new Future.delayed(_pollingDelay).then((_) { | |
178 if (_events.isClosed) return; | |
179 _poll(); | |
180 }); | |
181 } | |
182 } | |
OLD | NEW |