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

Unified 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: 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 side-by-side diff with in-line comments
Download patch
Index: pkg/watcher/lib/src/directory_watcher.dart
diff --git a/pkg/watcher/lib/src/directory_watcher.dart b/pkg/watcher/lib/src/directory_watcher.dart
new file mode 100644
index 0000000000000000000000000000000000000000..2b75385f00b15e077d73cbafeb2ddd062446949e
--- /dev/null
+++ b/pkg/watcher/lib/src/directory_watcher.dart
@@ -0,0 +1,227 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library watcher.directory_watcher;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:crypto/crypto.dart';
+
+import 'change_type.dart';
+import 'stat.dart';
+import 'watch_event.dart';
+
+/// Watches the contents of a directory and emits [WatchEvents] when something
nweiz 2013/07/11 00:25:01 "[WatchEvent]s"
Bob Nystrom 2013/07/11 18:33:20 Done.
+/// in the directory has changed.
+class DirectoryWatcher {
+ /// The directory whose contents are being monitored.
+ final String directory;
+
+ /// The [Stream] of file modification events that have occurred to files in
nweiz 2013/07/11 00:25:01 "file modification events" -> "events" Also, ment
Bob Nystrom 2013/07/11 18:33:20 Done.
+ /// [directory].
+ ///
+ /// Changes will only be monitored while this stream has subscribers. Any
+ /// file changes that occur during periods when there are no subscribers
+ /// will not be reported the next time a subscriber is added.
+ Stream<WatchEvent> get events => _events.stream;
+
nweiz 2013/07/11 00:25:01 Style nit: I'd get rid of this newline to indicate
Bob Nystrom 2013/07/11 18:33:20 Done.
+ StreamController<WatchEvent> _events;
Siggi Cherem (dart-lang) 2013/07/10 22:53:53 change to final?
Bob Nystrom 2013/07/10 23:04:39 Wish I could. :( The only way to set onListen and
nweiz 2013/07/11 00:25:01 I'd call this _eventsController.
Bob Nystrom 2013/07/11 18:33:20 Since the stream itself isn't stored, I think that
+
+ _WatchState _state = _WatchState.notWatching;
+
+ /// The previous status of the files in the directory. Used to tell which
+ /// files have been modified.
nweiz 2013/07/11 00:25:01 Style nit: first sentence should be its own paragr
Bob Nystrom 2013/07/11 18:33:20 Done.
+ final _statuses = new Map<String, _FileStatus>();
+
+ /// Creates a new [DirectoryWatcher] monitoring [directory].
+ DirectoryWatcher(this.directory) {
+ _events = new StreamController<WatchEvent>.broadcast(onListen: () {
+ _state = _state.listen(this);
+ }, onCancel: () {
+ _state = _state.cancel(this);
+ });
+ }
+
+ /// Starts the asynchronous polling process.
+ ///
+ /// Scans the contents of the directory and compares the results to the
+ /// previous scan. Loops to continue monitoring as long as there are
+ /// subscribers to the [events] stream.
+ Future _watch() {
+ var files = new Set<String>();
+
+ var stream = new Directory(directory).list(
+ recursive: true, followLinks: true);
nweiz 2013/07/11 00:25:01 I'd much rather we use pub's listDirectory here, e
Bob Nystrom 2013/07/11 18:33:20 I think it's important for watcher to use a stream
nweiz 2013/07/11 22:29:00 They've marked an important bug as wontfix: https:
Bob Nystrom 2013/07/12 00:31:09 I think you marked that bug WontFix. I changed thi
nweiz 2013/07/12 01:04:28 Soren incorrectly marked it as fixed. I changed th
Bob Nystrom 2013/07/12 17:46:16 Yes, for now I'm OK with not supported symlinked d
+
+ var futures = [];
+ return stream.forEach((entity) {
+ if (entity is File) {
+ files.add(entity.path);
+ futures.add(_refreshFile(entity.path));
+ }
+ }).then((_) {
+ // Once the listing is done, make sure to wait until each file is also
+ // done.
+ return Future.wait(futures);
+ }).then((_) {
nweiz 2013/07/11 00:25:01 I think the following would be a little cleaner he
Bob Nystrom 2013/07/11 18:33:20 Had to stick a toList() in there, but this is a bi
+ var removedFiles = _statuses.keys.toSet().difference(files);
+ for (var removed in removedFiles) {
+ if (_state.shouldNotify) {
nweiz 2013/07/11 00:25:01 It's a little weird that this is a flag, but you j
Bob Nystrom 2013/07/11 18:33:20 Changed to check shouldNotify below.
+ _events.add(new WatchEvent(ChangeType.REMOVE, removed));
+ }
+ _statuses.remove(removed);
+ }
+
+ _state = _state.finish(this);
+
+ // If the new state isn't watching, just stop.
+ if (!_state.shouldWatch) return;
+
+ // If we're in the "watching" state, add a bit of delay before restarting
+ // just so that we don't whale on the file system.
+ // TODO(rnystrom): Tune this and/or make it tunable?
+ if (_state == _WatchState.watching) {
+ return new Future.delayed(new Duration(seconds: 1));
+ }
+
+ // Otherwise, loop.
+ return _watch();
nweiz 2013/07/11 00:25:01 Shouldn't this just be "return;"? Otherwise, would
Bob Nystrom 2013/07/11 18:33:20 Done.
+ }).then((_) {
+ // Make sure we haven't transitioned to a non-watching state during the
+ // delay.
+ if (_state.shouldWatch) _watch();
+ });
+ }
+
+ /// Compares the current state of the file at [path] to the state it was in
+ /// the last time it was scanned.
+ Future _refreshFile(String path) {
+ return getModificationTime(path).then((modified) {
+ var lastStatus = _statuses[path];
+
+ // If it's modification time hasn't changed, assume the file is unchanged.
+ if (lastStatus != null && lastStatus.modified == modified) {
+ return false;
nweiz 2013/07/11 00:25:01 Is this return value used anywhere?
Bob Nystrom 2013/07/11 18:33:20 Oops. Not anymore. Used to have the later future s
+ }
+
+ return _hashFile(path).then((hash) {
+ var status = new _FileStatus(modified, hash);
+ _statuses[path] = status;
+
+ if (!_state.shouldNotify) return;
nweiz 2013/07/11 00:25:01 Fold this into the following "if", or vice versa.
Bob Nystrom 2013/07/11 18:33:20 Done.
+
+ // Only notify if the file contents are changed.
+ if (lastStatus == null || !_sameHash(lastStatus.hash, hash)) {
Siggi Cherem (dart-lang) 2013/07/10 22:53:53 consider making this a configuration option - some
Bob Nystrom 2013/07/10 23:04:39 I expect this to get more configurable over time,
+ var change = lastStatus == null ? ChangeType.ADD : ChangeType.MODIFY;
+ _events.add(new WatchEvent(change, path));
+ }
+ });
+ });
+ }
+
+ /// Calculates the SHA-1 hash of the file at [path].
+ Future<List<int>> _hashFile(String path) {
+ return new File(path).readAsBytes().then((bytes) {
+ var sha1 = new SHA1();
+ sha1.add(bytes);
+ return sha1.close();
+ });
+ }
+
+ /// Returns `true` if [a] and [b] are the same hash value, i.e. the same
+ /// series of byte values.
+ bool _sameHash(List<int> a, List<int> b) {
Siggi Cherem (dart-lang) 2013/07/10 22:53:53 alternatively call CrytoUtils.bytesToHex and compa
Bob Nystrom 2013/07/10 23:04:39 Hmm, is that faster? Seems like a roundabout way t
+ // Hashes should always be the same size.
+ assert(a.length == b.length);
+
+ for (var i = 0; i < a.length; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+ }
+}
+
+/// An "event" that is sent to the [_WatchState] FSM to trigger state
+/// transitions.
+typedef _WatchState _WatchStateEvent(DirectoryWatcher watcher);
+
+/// The different states that the watcher can be in and the transitions between
+/// them.
+///
+/// This class defines a finite state machine for keeping track of what the
+/// asynchronous file polling is doing. Each instance of this is a state in the
+/// machine and its [listen], [cancel], and [finish] fields define the state
+/// transitions when those events occur.
+class _WatchState {
+ /// The watcher has no subscribers.
+ static final notWatching = new _WatchState(
+ listen: (watcher) {
+ watcher._watch();
nweiz 2013/07/11 00:25:01 If you move this into DirectoryWatcher, then _Watc
Bob Nystrom 2013/07/11 18:33:20 You mean move the state instances?
nweiz 2013/07/11 22:29:00 No, I mean the call to "watcher._watch" in particu
Bob Nystrom 2013/07/12 00:31:09 Agreed on both accounts. In this case, I think hav
nweiz 2013/07/12 01:04:28 Okay.
+ return _WatchState.scanning;
+ });
+
+ /// The watcher has subscribers and is scanning for pre-existing files.
+ static final scanning = new _WatchState(
+ cancel: (_) => _WatchState.cancelling,
+ finish: (_) => _WatchState.watching,
+ shouldWatch: true);
+
+ /// The watcher was unsubscribed while polling and we're waiting for the poll
+ /// to finish.
+ static final cancelling = new _WatchState(
+ listen: (_) => _WatchState.scanning,
+ finish: (_) => _WatchState.notWatching);
+
+ /// The watcher has subscribers, we have scanned for pre-existing files and
+ /// now we're waiting for changes to come in.
nweiz 2013/07/11 00:25:01 "waiting for changes to come in" -> "polling for c
Bob Nystrom 2013/07/11 18:33:20 Done.
+ static final watching = new _WatchState(
+ cancel: (_) => _WatchState.cancelling,
+ finish: (_) => _WatchState.watching,
+ shouldWatch: true, shouldNotify: true);
+
+ /// Asserts that an event is not expected for some state.
+ static _WatchState _badState(DirectoryWatcher watcher) {
+ // Should not receive this event in this state.
+ assert(false);
nweiz 2013/07/11 00:25:01 If it's possible, it would be nice to provide some
Bob Nystrom 2013/07/11 18:33:20 I just went ahead and deleted this. It only exist(
+ }
+
+ /// Called when the first subscriber to the watcher has been added.
+ final _WatchStateEvent listen;
+
+ /// Called when all subscriptions on the watcher have been cancelled.
+ final _WatchStateEvent cancel;
+
+ /// Called when a poll loop has finished.
+ final _WatchStateEvent finish;
+
+ /// If the directory watcher should be watching the file system while in
+ /// this state.
+ final bool shouldWatch;
+
+ /// `true` if a change event should be sent for a file modification while
+ /// in this state.
+ final bool shouldNotify;
+
+ _WatchState({
+ _WatchStateEvent listen,
+ _WatchStateEvent cancel,
+ _WatchStateEvent finish,
+ this.shouldWatch: false,
+ this.shouldNotify: false})
+ : listen = listen != null ? listen : _badState,
+ cancel = cancel != null ? cancel : _badState,
+ finish = finish != null ? finish : _badState;
+}
+
+class _FileStatus {
+ /// The last time the file was modified.
+ DateTime modified;
+
+ /// The SHA-1 hash of the contents of the file.
+ List<int> hash;
+
+ _FileStatus(this.modified, this.hash);
+}

Powered by Google App Engine
This is Rietveld 408576698