blob: 2df5886baedb84c0d0cd6b2477a4e21787cd3657 [file] [log] [blame] [edit]
// Copyright (c) 2012, 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 "common_patch.dart";
@patch
class _File {
@patch
@pragma("vm:external-name", "File_Exists")
external static _exists(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_Create")
external static _create(
_Namespace namespace,
Uint8List rawPath,
bool exclusive,
);
@patch
@pragma("vm:external-name", "File_CreateLink")
external static _createLink(
_Namespace namespace,
Uint8List rawPath,
String target,
);
@patch
@pragma("vm:external-name", "File_CreatePipe")
external static List<dynamic> _createPipe(_Namespace namespace);
@patch
@pragma("vm:external-name", "File_LinkTarget")
external static _linkTarget(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_Delete")
external static _deleteNative(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_DeleteLink")
external static _deleteLinkNative(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_Rename")
external static _rename(
_Namespace namespace,
Uint8List oldPath,
String newPath,
);
@patch
@pragma("vm:external-name", "File_RenameLink")
external static _renameLink(
_Namespace namespace,
Uint8List oldPath,
String newPath,
);
@patch
@pragma("vm:external-name", "File_Copy")
external static _copy(
_Namespace namespace,
Uint8List oldPath,
String newPath,
);
@patch
@pragma("vm:external-name", "File_LengthFromPath")
external static _lengthFromPath(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_LastModified")
external static _lastModified(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_SetLastModified")
external static _setLastModified(
_Namespace namespace,
Uint8List rawPath,
int millis,
);
@patch
@pragma("vm:external-name", "File_LastAccessed")
external static _lastAccessed(_Namespace namespace, Uint8List rawPath);
@patch
@pragma("vm:external-name", "File_SetLastAccessed")
external static _setLastAccessed(
_Namespace namespace,
Uint8List rawPath,
int millis,
);
@patch
@pragma("vm:external-name", "File_Open")
external static _open(_Namespace namespace, Uint8List rawPath, int mode);
@patch
@pragma("vm:external-name", "File_OpenStdio")
external static int _openStdio(int fd);
}
@patch
class _RandomAccessFileOps {
@patch
factory _RandomAccessFileOps._(int pointer) =>
_RandomAccessFileOpsImpl._().._setPointer(pointer);
}
@pragma("vm:entry-point")
base class _RandomAccessFileOpsImpl extends NativeFieldWrapperClass1
implements _RandomAccessFileOps {
_RandomAccessFileOpsImpl._();
@pragma("vm:external-name", "File_SetPointer")
external void _setPointer(int pointer);
@pragma("vm:external-name", "File_GetPointer")
external int _getPointer();
@pragma("vm:external-name", "File_GetFD")
external int get fd;
@pragma("vm:external-name", "File_Close")
external int close();
@pragma("vm:external-name", "File_ReadByte")
external readByte();
@pragma("vm:external-name", "File_Read")
external read(int bytes);
@pragma("vm:external-name", "File_ReadInto")
external readInto(List<int> buffer, int start, int? end);
@pragma("vm:external-name", "File_WriteByte")
external writeByte(int value);
@pragma("vm:external-name", "File_WriteFrom")
external writeFrom(List<int> buffer, int start, int? end);
@pragma("vm:external-name", "File_Position")
external position();
@pragma("vm:external-name", "File_SetPosition")
external setPosition(int position);
@pragma("vm:external-name", "File_Truncate")
external truncate(int length);
@pragma("vm:external-name", "File_Length")
external length();
@pragma("vm:external-name", "File_Flush")
external flush();
@pragma("vm:external-name", "File_Lock")
external lock(int lock, int start, int end);
}
class _WatchedPath implements ffi.Finalizable {
/// Path ID returned by [_FileSystemWatcher._watchPath].
///
/// Will remain valid until either [_unwatchPath] is called.
final int pathId;
final String path;
final int events;
/// Listeners subscribed to [FileSystemEvent] occuring at [path].
///
/// Once the last listener is unsubscribed the underlying watcher will
/// stop monitoring changes at [path].
final List<MultiStreamController<FileSystemEvent>> listeners =
<MultiStreamController<FileSystemEvent>>[];
bool isClosed = false;
/// Source of [_NativeFSEvent] for this path.
///
/// This subscription will be cancelled once the last listener is gone.
///
/// Might be `null` if events for this path are delivered over multiplexed
/// stream (see [_InotifyFileSystemWatcher]).
StreamSubscription<List<_NativeFSEvent>>? source;
/// Finalizer associated with [_WatchedPath] instances.
///
/// [_FileSystemWatcher._watchPathImpl] returns `pathId` values which behave
/// slightly differently on different OSes.
///
/// * On Mac OS X `pathId` values which are actually pointers to `Node`
/// objects. These objects need to be released by calling
/// [_FileSystemWatcher._unwatchPath] - otherwise they will leak.
/// Attaching a [NativeFinalizer] ensures that even if Isolate exits
/// abruptly via [Isolate.exit] we will still free these objects.
/// * On Linux `pathId` is a _watch descriptor_ (returned by
/// `inotify_add_watch`) associated with a specific inotify instance.
/// Inotify instances are created by [_FileSystemWatcher._initWatcher] (see
/// `inotify_init`) which returns a file descriptor. This file descriptor is
/// wrapped in a [_NativeSocket] by
/// [_FileSystemWatcher._eventsStreamFromSocket]. Socket takes ownership of
/// the file descriptor and has a finalizer attached to itself. This
/// finalizer will close the descriptor when socket is garbage collected.
/// Thus there is no need to associate a separate finalizer with
/// [_WatchedPath].
/// * Windows is a mixture of Linux and Mac OS X: `pathId` itself is a
/// pointer to a socket-like `DirectoryWatchHandle`. It is wrapped in a
/// socket by [_FileSystemWatcher._eventsStreamFromSocket] by we do not
/// allow the socket to take full ownership of the `pathId` handle, because
/// we want to guarantee that invariant that `pathId` remains valid
/// until we explicitly call `_FileSystemWatcher._unwatchPath`. To ensure
/// this we explicitly retain `DirectoryWatchHandle` before returning it
/// from [_FileSystemWatcher._initWatcher]. This will keep the handle
/// alive even after event handler is done with it. This however means that
/// [_FileSystemWatcher._unwatchPath] must be called to release the handle.
/// Thus we need to attach a finalizer to [_WatchedPath] to guarantee that.
static final ffi.NativeFinalizer? finalizer =
Platform.isMacOS || Platform.isWindows
? ffi.NativeFinalizer(
ffi.Native.addressOf(_FileSystemWatcher._destroyWatch),
)
: null;
_WatchedPath(this.pathId, this.path, this.events) {
finalizer?.attach(this, .fromAddress(pathId), detach: this);
}
FutureOr<void> dispose() {
assert(listeners.isEmpty);
isClosed = true;
finalizer?.detach(this);
return source?.cancel();
}
void add(FileSystemEvent event) {
if (isClosed) {
return;
}
if ((event.type & events) == 0) {
return;
}
for (var listener in listeners) {
listener.add(event);
}
}
void close({Object? error}) {
if (isClosed) {
return;
}
isClosed = true;
for (var listener in listeners) {
if (error != null) {
listener.addError(error);
}
listener.close();
}
}
/// Emit the given [_NativeFSEvent] to listeners.
///
/// A single [_NativeFSEvent] is expanded into a sequence of appropriate
/// [FileSystemEvent].
///
/// Note: this might modify [unmatchedMoves] - the caller is responsible for
/// calling [flushUnmatchedMoves] once it emitted all events from a chunk
/// of events.
void addEvent(_NativeFSEvent event) {
if (isClosed) {
return;
}
final flags = event.flags;
final fullPath = fullPathOf(event);
final isDir = _NativeFSEvent.isDirectory(event, fullPath);
if ((flags & FileSystemEvent.create) != 0) {
add(FileSystemCreateEvent(fullPath, isDir));
}
if ((flags & FileSystemEvent.modify) != 0) {
add(FileSystemModifyEvent(fullPath, isDir, true));
}
if ((flags & FileSystemEvent._modifyAttributes) != 0) {
add(FileSystemModifyEvent(fullPath, isDir, false));
}
if ((flags & FileSystemEvent.move) != 0) {
// Use cookie to merge pairs of move from and move to events.
final int cookie = event.cookie;
if (cookie > 0) {
if (unmatchedMoves.remove(cookie) case final linkedEvent?) {
add(FileSystemMoveEvent(fullPathOf(linkedEvent), isDir, fullPath));
} else {
unmatchedMoves[cookie] = event;
}
} else {
addMove(event, fullPath, isDir);
}
}
if ((flags & FileSystemEvent.delete) != 0) {
add(FileSystemDeleteEvent(fullPath, false));
}
if ((flags & FileSystemEvent._deleteSelf) != 0) {
add(FileSystemDeleteEvent(fullPath, false));
// Emit all unmatched moves before emitting the stop event to avoid
// loosing these events.
flushUnmatchedMoves();
close();
}
}
/// Flush unmatched move events accumulated in [unmatchedMoves].
void flushUnmatchedMoves() {
for (var move in unmatchedMoves.values) {
final fullPathOfMove = fullPathOf(move);
addMove(
move,
fullPathOfMove,
_NativeFSEvent.isDirectory(move, fullPathOf(move)),
);
}
unmatchedMoves.clear();
}
/// Most recently encountered move events by their [_NativeFSEvent.cookie].
///
/// When converting a chunk of _NativeFSEvent to corresponding FileSystemEvents
/// we try to match and merge pairs of events which correspond to a single
/// move operation (e.g. FILE_ACTION_RENAMED_{OLD|NEW}_NAME on Windows and
/// IN_MOVED_{FROM|TO} on Linux). We use this as a temporary storage for
/// matching. The caller feeding native events must call [flushUnmatchedMoves]
/// at the end of the chunk to flush all unmatched moves.
final Map<int, _NativeFSEvent> unmatchedMoves = {};
String fullPathOf(_NativeFSEvent event) {
assert(event.pathId == pathId);
if (event.relativePath case final eventPath? when eventPath.isNotEmpty) {
return '${path}${Platform.pathSeparator}${eventPath}';
} else {
return path;
}
}
void addMove(_NativeFSEvent event, String fullPath, bool isDir) {
if ((event.flags & FileSystemEvent._movedTo) != 0) {
add(FileSystemCreateEvent(fullPath, isDir));
} else {
add(FileSystemDeleteEvent(fullPath, false));
}
}
}
@patch
abstract class _FileSystemWatcher {
@patch
static Stream<FileSystemEvent> _watch(
String path,
int events,
bool recursive,
) {
return _watcher._watchImpl(path, events, recursive);
}
Stream<FileSystemEvent> _watchImpl(String path, int events, bool recursive) {
_WatchedPath? watchedPath;
final stream = Stream<FileSystemEvent>.multi((controller) {
_WatchedPath wp;
try {
wp = watchedPath ??= _watcher._startWatching(path, events, recursive);
} on FileSystemException catch (e, st) {
controller.addError(e, st);
controller.close();
return;
}
if (wp.isClosed) {
controller.addError(
FileSystemException('Directory watcher is already closed', path),
);
controller.close();
return;
}
wp.listeners.add(controller);
controller.onCancel = () {
wp.listeners.remove(controller);
if (wp.listeners.isEmpty) {
watchedPath = null;
return _stopWatching(wp);
}
};
});
return stream;
}
Stream<List<_NativeFSEvent>> _eventsStreamFromSocket(
int socketId,
int pathId,
) {
final nativeSocket = _NativeSocket._watch(socketId);
return _RawSocket(nativeSocket).map((event) {
if (event == RawSocketEvent.read) {
final result = <_NativeFSEvent>[];
int totalEvents;
do {
totalEvents = result.length;
for (_NativeFSEvent? e in _readEvents(_watcherId, pathId)) {
if (e == null) {
break;
}
result.add(e);
}
} while (result.length > totalEvents);
// Be sure to clear this manually, as the sockets are not read through
// the _NativeSocket interface.
nativeSocket.available = 0;
return result;
}
return [];
});
}
/// Native ID associated with the watcher.
///
/// Passed as a parameter to [_watchPathImpl] and other native functions.
int get _watcherId => 0;
/// Start watching the given path for the specified events.
///
/// Returns [_WatchedPath] instances representing the watch.
_WatchedPath _startWatching(String path, int events, bool recursive);
/// Stop watching the given path.
///
/// If this causes the watcher to free some native resources (e.g. because
/// this was the last active filesystem watch) this function will return
/// an instance of [Future] which will complete after native cleanup is
/// complete.
FutureOr<void> _stopWatching(_WatchedPath wp) {
_unwatchPath(_watcherId, wp.pathId);
return wp.dispose();
}
/// Wrapper over [_watchPathImpl] which takes care of converting
/// [OSError] into [FileSystemException].
int _watchPath(String path, int events, bool recursive) {
try {
return _watchPathImpl(
_watcherId,
_Namespace._namespace,
path,
events,
recursive,
);
} on OSError catch (e) {
throw FileSystemException._fromOSError(e, "Failed to watch path", path);
}
}
/// Singleton [_FileSystemWatcher] which takes care of watching file system.
static _FileSystemWatcher _watcher = () {
if (isSupported) {
if (Platform.isLinux || Platform.isAndroid) {
return _InotifyFileSystemWatcher();
}
if (Platform.isWindows) {
return _Win32FileSystemWatcher();
}
if (Platform.isMacOS) {
return _FSEventStreamFileSystemWatcher();
}
}
throw FileSystemException(
"File system watching is not supported on this platform",
);
}();
@patch
@pragma("vm:external-name", "FileSystemWatcher_IsSupported")
external static bool get isSupported;
@pragma("vm:external-name", "FileSystemWatcher_InitWatcher")
external static int _initWatcher();
@pragma("vm:external-name", "FileSystemWatcher_WatchPath")
external static int _watchPathImpl(
int watcherId,
_Namespace namespace,
String path,
int events,
bool recursive,
);
@pragma("vm:external-name", "FileSystemWatcher_UnwatchPath")
external static void _unwatchPath(int watcherId, int pathId);
/// Returns a list each element of which is [_NativeFSEvents] or `null`.
///
/// After the first `null` only `null` entries will follow, in other words
/// all non-`null` entries form the prefix of the list.
@pragma("vm:external-name", "FileSystemWatcher_ReadEvents")
external static List _readEvents(int watcherId, int pathId);
@pragma("vm:external-name", "FileSystemWatcher_GetSocketId")
external static int _getSocketId(int watcherId, int pathId);
@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
symbol: "FileSystemWatcher::DestroyWatch",
)
external static void _destroyWatch(ffi.Pointer<ffi.Void> pathId);
}
/// A watcher that receives events for multiple `pathId` on a single channel.
abstract class _MultiplexingFileSystemWatcher extends _FileSystemWatcher {
/// Map of [_watchedPath] indexed by `pathId` values.
final Map<int, _WatchedPath> _watchedPaths = <int, _WatchedPath>{};
/// Perform necessary initialization of the native state for the watcher.
void _ensureWatcherIsRunning();
/// Shutdown the watcher when there is no actively watched paths.
///
/// If shutdown requires asynchronous actions returns [Future] which will
/// complete when shutdown is finished.
FutureOr<void> _stopWatcher();
@override
_WatchedPath _startWatching(String path, int events, bool recursive) {
_ensureWatcherIsRunning();
final pathId = super._watchPath(path, events, recursive);
// On Linux inotify_add_watch will return an existing watch descriptor
// for the inode if there is already one associated with it. Thus we
// need accept the possibility that calling _watchPath twice will
// return the same pathId. Other OSes do not reuse pathId values.
assert(Platform.isLinux || !_watchedPaths.containsKey(pathId));
return _watchedPaths[pathId] ??= _WatchedPath(pathId, path, events);
}
@override
Future<void> _stopWatching(_WatchedPath wp) async {
assert(_watchedPaths[wp.pathId] == wp);
_watchedPaths.remove(wp.pathId);
await super._stopWatching(wp);
// If there are no more active watcher close inotify descriptor.
if (_watchedPaths.isEmpty) {
await _stopWatcher();
}
}
}
class _InotifyFileSystemWatcher extends _MultiplexingFileSystemWatcher {
int? _inotifyFd;
StreamSubscription<List<_NativeFSEvent>>? _inotifySubscription;
@override
int get _watcherId => _inotifyFd!;
@override
void _ensureWatcherIsRunning() {
if (_inotifyFd != null) {
return;
}
final inotifyFd = _inotifyFd = _FileSystemWatcher._initWatcher();
_inotifySubscription = _eventsStreamFromSocket(
inotifyFd,
0,
).listen(_handleEvents);
}
void _handleEvents(List<_NativeFSEvent> events) {
Set<_WatchedPath>? dirty;
// Distribute events to corresponding _WatchedPath objects based on
// pathId.
for (_NativeFSEvent event in events) {
if (_watchedPaths[event.pathId] case final watchedPath?) {
watchedPath.addEvent(event);
if (watchedPath.unmatchedMoves.isNotEmpty) {
(dirty ??= {}).add(watchedPath);
}
}
}
if (dirty != null) {
for (var watchedPath in dirty) {
watchedPath.flushUnmatchedMoves();
}
}
}
@override
FutureOr<void> _stopWatcher() {
final subscription = _inotifySubscription;
_inotifyFd = null;
_inotifySubscription = null;
return subscription?.cancel();
}
}
class _FSEventStreamFileSystemWatcher extends _MultiplexingFileSystemWatcher {
final _port = RawReceivePort()..keepIsolateAlive = false;
@override
late final _watcherId = ffi.NativePort(_port.sendPort).nativePort;
@override
void _ensureWatcherIsRunning() {
_port.keepIsolateAlive = true;
_port.handler = _handleEvents;
}
@override
FutureOr<void> _stopWatcher() {
_port.keepIsolateAlive = false;
_port.handler = null;
}
void _handleEvents(List events) {
// All events in a bundle have the same pathId, and we never get an empty
// bundle.
final pathId = (events[0] as _NativeFSEvent).pathId;
if (_watchedPaths[pathId] case final watchedPath?) {
for (_NativeFSEvent event in events) {
watchedPath.addEvent(event);
}
watchedPath.flushUnmatchedMoves();
}
}
}
class _Win32FileSystemWatcher extends _FileSystemWatcher {
@override
_WatchedPath _startWatching(String path, int events, bool recursive) {
final watchedPath = _WatchedPath(
_watchPath(path, events, recursive),
path,
events,
);
watchedPath.source =
_eventsStreamFromSocket(
_FileSystemWatcher._getSocketId(0, watchedPath.pathId),
watchedPath.pathId,
).listen(
(events) {
for (var e in events) {
watchedPath.addEvent(e);
}
watchedPath.flushUnmatchedMoves();
},
onError: (error) {
if (watchedPath.listeners.isNotEmpty) {
watchedPath.close(
error: FileSystemException(
'Directory watcher failed due to: $error',
watchedPath.path,
),
);
}
},
onDone: () {
if (watchedPath.listeners.isNotEmpty) {
watchedPath.close(
error: FileSystemException(
'Directory watcher closed unexpectedly',
watchedPath.path,
),
);
}
},
cancelOnError: true,
);
return watchedPath;
}
}
extension type _NativeFSEvent(List<dynamic> _) {
// See FileSystemWatcher::kEvent*Index constants.
static const int flagsIndex = 0;
static const int cookieIndex = 1;
static const int pathIndex = 2;
static const int pathIdIndex = 3;
int get flags => this._[flagsIndex];
/// A unique identifier (32-bit unsigned integer) for matching related events.
///
/// On Linux (inotify) associates unique cookie values with pairs of
/// `IN_MOVED_FROM` and `IN_MOVED_TO` events.
///
/// On Windows we set cookie to `1` on pairs of `FILE_ACTION_RENAMED_OLD_NAME`
/// and `FILE_ACTION_RENAMED_NEW_NAME`.
///
/// Not used on Mac OS X (because `FSEventStream` does not generate move
/// events).
int get cookie => this._[cookieIndex];
String? get relativePath => this._[pathIndex];
int get pathId => this._[pathIdIndex];
static bool isDirectory(_NativeFSEvent event, String fullPath) {
if (Platform.isWindows) {
// Windows does not get FileSystemEvent._isDir bit as part of the event
// so we need to compute it by checking the file-system. We ignore links
// when computing isDirectory.
return FileSystemEntity._isDirectoryIgnoringLinksSync(fullPath);
} else {
return (event.flags & FileSystemEvent._isDir) != 0;
}
}
}
@pragma("vm:entry-point", "call")
Uint8List _makeUint8ListView(Uint8List source, int offsetInBytes, int length) {
return Uint8List.view(source.buffer, offsetInBytes, length);
}