| // 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, (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(); | |
| } | |
| } |