| // 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 '../file_watcher.dart'; |
| import '../polling.dart'; |
| import '../resubscribable.dart'; |
| import '../watch_event.dart'; |
| |
| /// Periodically polls a file for changes. |
| class PollingFileWatcher extends ResubscribableWatcher implements FileWatcher { |
| PollingFileWatcher(String path, {Duration? pollingDelay}) |
| : super(path, () { |
| return _PollingFileWatcher( |
| path, pollingDelay ?? const Duration(seconds: 1)); |
| }); |
| } |
| |
| class _PollingFileWatcher 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>(); |
| |
| /// The timer that controls polling. |
| late final Timer _timer; |
| |
| PollResult _previousPollResult = PollResult.notAFile(); |
| |
| _PollingFileWatcher(this.path, Duration pollingDelay) { |
| _timer = Timer.periodic(pollingDelay, (_) => _poll()); |
| _poll(); |
| } |
| |
| /// Checks the mtime of the file and whether it's been removed. |
| Future<void> _poll() async { |
| // We don't mark the file as removed if this is the first poll. Instead, |
| // below we forward the dart:io error that comes from trying to read the |
| // mtime below. |
| var pathExists = await File(path).exists(); |
| if (_eventsController.isClosed) return; |
| |
| if (_previousPollResult.fileExists && !pathExists) { |
| _flagReady(); |
| _eventsController.add(WatchEvent(ChangeType.REMOVE, path)); |
| unawaited(close()); |
| return; |
| } |
| |
| PollResult pollResult; |
| try { |
| pollResult = await PollResult.poll(path); |
| } on FileSystemException catch (error, stackTrace) { |
| if (!_eventsController.isClosed) { |
| _flagReady(); |
| _eventsController.addError(error, stackTrace); |
| await close(); |
| } |
| return; |
| } |
| if (_eventsController.isClosed) { |
| _flagReady(); |
| return; |
| } |
| |
| if (!isReady) { |
| // If this is the first poll, don't emit an event, just set the poll |
| // result and complete the completer. |
| _previousPollResult = pollResult; |
| _flagReady(); |
| return; |
| } |
| |
| if (_previousPollResult == pollResult) return; |
| |
| _previousPollResult = pollResult; |
| _eventsController.add(WatchEvent(ChangeType.MODIFY, path)); |
| } |
| |
| /// Flags this watcher as ready if it has not already been done. |
| void _flagReady() { |
| if (!isReady) { |
| _readyCompleter.complete(); |
| } |
| } |
| |
| @override |
| Future<void> close() async { |
| _timer.cancel(); |
| await _eventsController.close(); |
| } |
| } |