Chromium Code Reviews| 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); |
| +} |