Index: sdk/lib/io/file_system_watcher.dart |
diff --git a/sdk/lib/io/file_system_watcher.dart b/sdk/lib/io/file_system_watcher.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..5b9b3f7ebe17fc90db6e042317201789f37b65db |
--- /dev/null |
+++ b/sdk/lib/io/file_system_watcher.dart |
@@ -0,0 +1,281 @@ |
+// 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. |
+ |
+part of dart.io; |
+ |
+ |
+/** |
+ * Base event class emitted by FileSystemWatcher. |
+ */ |
+class FileSystemEvent { |
+ /** |
+ * The type of event. See [FileSystemEvent] for a list of events. |
+ */ |
+ final int type; |
+ |
+ /** |
+ * The path that triggered the event. |
+ */ |
+ final String path; |
+ |
+ FileSystemEvent._(this.type, this.path); |
+} |
+ |
+ |
+/** |
+ * File system event for newly created file system objects. |
+ */ |
+class FileSystemCreateEvent extends FileSystemEvent { |
+ FileSystemCreateEvent._(path) |
+ : super._(FileSystemWatcher.CREATE_EVENT, path); |
+ |
+ String toString() => "FileSystemCreateEvent('$path')"; |
+} |
+ |
+ |
+/** |
+ * File system event for modifications of file system objects. |
+ */ |
+class FileSystemModifyEvent extends FileSystemEvent { |
+ /** |
+ * If the content was changed and not only the attributes, [contentChanged] |
+ * is `true`. |
+ */ |
+ final bool contentChanged; |
+ |
+ FileSystemModifyEvent._(path, this.contentChanged) |
+ : super._(FileSystemWatcher.MODIFY_EVENT, path); |
+ |
+ String toString() => |
+ "FileSystemModifyEvent('$path', contentChanged=$contentChanged)"; |
+} |
+ |
+ |
+/** |
+ * File system event for deletion of file system objects. |
+ */ |
+class FileSystemDeleteEvent extends FileSystemEvent { |
+ FileSystemDeleteEvent._(path) |
+ : super._(FileSystemWatcher.DELETE_EVENT, path); |
+ |
+ String toString() => "FileSystemDeleteEvent('$path')"; |
+} |
+ |
+ |
+/** |
+ * File system event for moving of file system objects. |
+ */ |
+class FileSystemMoveEvent extends FileSystemEvent { |
+ /** |
+ * If the underlaying implementation is able to identify the destination of |
+ * the moved file, [destination] will be set. Otherwise, it will be `null`. |
+ */ |
+ final String destination; |
+ |
+ FileSystemMoveEvent._(path, this.destination) |
+ : super._(FileSystemWatcher.MOVE_EVENT, path); |
+ |
+ String toString() { |
+ var buffer = new StringBuffer(); |
+ buffer.write("FileSystemMoveEvent('$path'"); |
+ if (destination != null) buffer.write(", '$destination'"); |
+ buffer.write(')'); |
+ return buffer.toString(); |
+ } |
+} |
+ |
+ |
+/** |
Søren Gjesse
2013/08/23 07:47:45
I am not sure about this new public class. Shouldn
|
+ * Event-based watcher for the file system. The [FileSystemWatcher] uses |
+ * platform-specific APIs for receiving events, thus behvaiour depends on the |
+ * platform. |
+ * |
+ * * `Windows`: Uses `ReadDirectoryChangesW`. The implementation supports only |
+ * watching dirctories but supports recursive watching. |
+ * * `Linux`: Uses `inotify`. The implementation supports watching both files |
+ * and dirctories, but doesn't support recursive watching. |
+ * * `Mac OS`: Uses `FSEvents`. The implementation supports watching both |
+ * files and dirctories, and also recursive watching. Note that FSEvents |
+ * always use recursion, so when disabled, some events are ignore. |
Søren Gjesse
2013/08/23 07:47:45
some events are ignore -> the events are filtered
|
+ * |
+ * On some platforms, it can be benaficial to use the same FileSystemWatcher for |
Søren Gjesse
2013/08/23 07:47:45
I don't like this comment. We should handle figure
|
+ * multiple paths. |
+ */ |
+class FileSystemWatcher extends Stream<FileSystemEvent> { |
+ static const int CREATE_EVENT = 1 << 0; |
+ static const int MODIFY_EVENT = 1 << 1; |
+ static const int DELETE_EVENT = 1 << 2; |
+ static const int MOVE_EVENT = 1 << 3; |
+ static const int ALL_EVENTS = |
+ CREATE_EVENT | MODIFY_EVENT | DELETE_EVENT | MOVE_EVENT; |
+ |
+ static const int _MODIFY_ATTRIBUTES_EVENT = 1 << 4; |
+ |
+ int _id; |
+ StreamController _controller; |
+ Map _paths = {}; |
+ Map _sockets = {}; |
+ |
+ FileSystemWatcher() { |
+ _id = _initWatcher(); |
+ _controller = new StreamController(onCancel: close); |
+ |
+ _listenOn(-1); |
+ } |
+ |
+ StreamSubscription<FileSystemEvent> listen(void onData(FileSystemEvent event), |
+ {void onError(error), |
+ void onDone(), |
+ bool cancelOnError}) { |
+ return _controller.stream.listen(onData, |
+ onError: onError, |
+ onDone: onDone, |
+ cancelOnError: cancelOnError); |
+ } |
+ |
+ /** |
+ * Start watching [path] for file system changes events. See |
+ * [FileSystemWatcher] for behaviour depending on the platform. |
+ * |
+ * A single path can only be added once. |
+ */ |
+ void watchPath(String path, |
+ {int events: ALL_EVENTS, |
+ bool recursive: false}) { |
+ recursive = identical(recursive, true); |
+ if (Platform.isLinux && recursive) { |
+ throw new ArgumentError("'recursive' watching is not available on Linux"); |
+ } |
+ if (_controller.isClosed) { |
+ throw new StateError("FileSystemWatcher is already closed"); |
+ } |
+ for (var value in _paths.values) { |
+ if (FileSystemEntity.identicalSync(value, path)) { |
+ throw new ArgumentError("'$path' is already being watched"); |
+ } |
+ } |
+ int id = _addPath(_id, path, events, recursive); |
+ _paths[id] = path; |
+ |
+ _listenOn(id, events); |
+ } |
+ |
+ /** |
+ * Unwatch the path [path]. It's an error if the [path] is not being watched. |
+ */ |
+ void unwatchPath(String path) { |
+ for (var key in _paths.keys) { |
+ var value = _paths[key]; |
+ if (FileSystemEntity.identicalSync(value, path)) { |
+ _removePath(_id, key); |
+ _paths.remove(key); |
+ if (_sockets.containsKey(key)) { |
+ _sockets[key].close(); |
+ _sockets.remove(key); |
+ } |
+ return; |
+ } |
+ } |
+ throw new ArgumentError("'$path' is not being watched"); |
+ } |
+ |
+ /** |
+ * Close the [FileSystemWatcher]. Once closed the [FileSystemWatcher] is |
+ * rendered invalid and can no longer be used. |
+ */ |
+ void close() { |
+ if (_controller.isClosed) return; |
+ for (var id in _paths.keys) { |
+ _removePath(_id, id); |
+ } |
+ _paths.clear(); |
+ for (var socket in _sockets.values) { |
+ socket.close(); |
+ } |
+ _sockets.clear(); |
+ _controller.close(); |
+ |
+ _stopWatcher(_id); |
+ } |
+ |
+ /** |
+ * Get the list of paths currently being watched. |
+ */ |
+ List<String> get paths => _paths.values; |
+ |
+ void _listenOn(int pathId, [int events = ALL_EVENTS]) { |
Søren Gjesse
2013/08/23 07:47:45
No need for events to be optional.
|
+ int socketId = _getSocketId(_id, pathId); |
+ if (socketId == -1) return; |
+ var native = new _NativeSocket.normal(); |
Søren Gjesse
2013/08/23 07:47:45
Instead of adding setSocketId could we add a new c
|
+ native.isClosedWrite = true; |
+ native.setSocketId(socketId); |
+ var socket = new _RawSocket(native); |
+ _sockets[pathId] = socket; |
+ socket.expand((event) { |
Søren Gjesse
2013/08/23 07:47:45
How about moving the closure in expand into a loca
|
+ var events = []; |
+ var pair = {}; |
+ if (event == RawSocketEvent.READ) { |
+ String getPath(event) { |
+ var path = _paths[event[1]]; |
+ if (event[2] != null) { |
+ path += Platform.pathSeparator; |
+ path += event[2]; |
+ } |
+ return path; |
+ } |
+ while (socket.available() > 0) { |
+ for (var event in _readNextEvent(_id, pathId)) { |
+ var path = getPath(event); |
+ if ((event[0] & CREATE_EVENT) != 0) { |
+ events.add(new FileSystemCreateEvent._(path)); |
+ } |
+ if ((event[0] & MODIFY_EVENT) != 0) { |
+ events.add(new FileSystemModifyEvent._(path, true)); |
+ } |
+ if ((event[0] & _MODIFY_ATTRIBUTES_EVENT) != 0) { |
+ events.add(new FileSystemModifyEvent._(path, false)); |
+ } |
+ if ((event[0] & MOVE_EVENT) != 0) { |
+ int link = event[3]; |
+ if (link > 0) { |
+ if (pair.containsKey(link)) { |
+ events.add( |
+ new FileSystemMoveEvent._(getPath(pair[link]), path)); |
+ pair.remove(link); |
+ } else { |
+ pair[link] = event; |
+ } |
+ } else { |
+ events.add(new FileSystemMoveEvent._(path, null)); |
+ } |
+ } |
+ if ((event[0] & DELETE_EVENT) != 0) { |
+ events.add(new FileSystemDeleteEvent._(path)); |
+ } |
+ } |
+ } |
+ for (var event in pair.values) { |
+ events.add(new FileSystemMoveEvent._(getPath(event), null)); |
+ } |
+ } else if (event == RawSocketEvent.CLOSED) { |
+ if (_paths.containsKey(pathId)) { |
+ _removePath(_id, pathId); |
+ _paths.remove(pathId); |
+ _sockets.remove(pathId); |
+ } |
+ } else if (event == RawSocketEvent.READ_CLOSED) { |
+ } else { |
+ assert(false); |
+ } |
+ return events; |
+ }).where((event) => (event.type & events) != 0).listen(_controller.add); |
+ } |
+ |
+ external int _initWatcher(); |
+ external void _stopWatcher(int id); |
+ external int _getSocketId(int id, int pathId); |
+ external int _addPath(int id, String path, int events, bool recursive); |
+ external void _removePath(int id, int pathId); |
+ external List _readNextEvent(int id, int pathId); |
+} |