blob: a891bb2755891f9903aa878825ecc3b07bbc0023 [file] [log] [blame]
// Copyright (c) 2015, 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.
import 'dart:async';
import 'dart:io';
import '../event.dart';
import '../event_batching.dart';
import '../file_watcher.dart';
import '../resubscribable.dart';
import '../watch_event.dart';
/// Uses the native file system notifications to watch for filesystem events.
///
/// Single-file notifications are much simpler than those for multiple files, so
/// this doesn't need to be split out into multiple OS-specific classes.
class NativeFileWatcher extends ResubscribableWatcher implements FileWatcher {
NativeFileWatcher(String path) : super(path, _NativeFileWatcher.new);
}
class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
@override
final String path;
@override
Stream<WatchEvent> get events => _eventsController.stream;
final _eventsController = StreamController<WatchEvent>.broadcast();
@override
bool get isReady => _readyCompleter.isCompleted;
@override
Future<void> get ready => _readyCompleter.future;
final _readyCompleter = Completer<void>();
StreamSubscription<List<Event>>? _subscription;
/// On MacOS only, whether the file existed on startup.
bool? _existedAtStartup;
_NativeFileWatcher(this.path) {
_listen();
// We don't need to do any initial set-up, so we're ready immediately after
// being listened to.
_readyCompleter.complete();
}
void _listen() async {
var file = File(path);
// Batch the events together so that we can dedupe them.
var stream = file.watch().batchNearbyMicrotasksAndConvertEvents();
if (Platform.isMacOS) {
var existedAtStartupFuture = file.exists();
// Delay processing watch events until the existence check finishes.
stream = stream.asyncMap((event) async {
_existedAtStartup ??= await existedAtStartupFuture;
return event;
});
}
_subscription = stream.listen(
_onBatch,
onError: _eventsController.addError,
onDone: _onDone,
);
}
void _onBatch(List<Event> batch) {
if (batch.any((event) => event.type == EventType.delete)) {
// If the file is deleted, the underlying stream will close. We handle
// emitting our own REMOVE event in [_onDone].
return;
}
if (Platform.isMacOS) {
// On MacOS, a spurious `create` event can be received for a file that is
// created just before the `watch`. If the file existed at startup then it
// should be ignored.
if (_existedAtStartup! &&
batch.every((event) => event.type == EventType.createFile)) {
return;
}
}
_eventsController.add(WatchEvent(ChangeType.MODIFY, path));
}
void _onDone() async {
var fileExists = await File(path).exists();
// Check for this after checking whether the file exists because it's
// possible that [close] was called between [File.exists] being called and
// it completing.
if (_eventsController.isClosed) return;
if (fileExists) {
// If the file exists now, it was probably removed and quickly replaced;
// this can happen for example when another file is moved on top of it.
// Re-subscribe and report a modify event.
_eventsController.add(WatchEvent(ChangeType.MODIFY, path));
_listen();
} else {
_eventsController.add(WatchEvent(ChangeType.REMOVE, path));
close();
}
}
@override
void close() {
_subscription?.cancel();
_subscription = null;
_eventsController.close();
}
}