Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(19)

Side by Side Diff: pkg/watcher/lib/src/directory_watcher.dart

Issue 18612013: File watching package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Remove reference to old unittest config. Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/watcher/example/watch.dart ('k') | pkg/watcher/lib/src/stat.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « pkg/watcher/example/watch.dart ('k') | pkg/watcher/lib/src/stat.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698