diff --git a/build_runner/lib/src/daemon/daemon_builder.dart b/build_runner/lib/src/daemon/daemon_builder.dart index ce11ec0464..bbef732cd9 100644 --- a/build_runner/lib/src/daemon/daemon_builder.dart +++ b/build_runner/lib/src/daemon/daemon_builder.dart @@ -277,6 +277,9 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder { Stream> graphEvents() => PackageGraphWatcher(packageGraph, watch: PackageNodeWatcher.new) .watch() + // TODO(davidmorgan): do something with the nulls, which indicate + // "watcher dropped events and restarted". + .whereNotNull() .asyncWhere( (change) => shouldProcess( change, diff --git a/build_runner/lib/src/entrypoint/watch.dart b/build_runner/lib/src/entrypoint/watch.dart index e6cc096c1c..367fd6d169 100644 --- a/build_runner/lib/src/entrypoint/watch.dart +++ b/build_runner/lib/src/entrypoint/watch.dart @@ -49,25 +49,30 @@ class WatchCommand extends BuildRunnerCommand { } Future _run(WatchOptions options) async { - var handler = await watch( - builderApplications, - enableLowResourcesMode: options.enableLowResourcesMode, - configKey: options.configKey, - buildDirs: options.buildDirs, - outputSymlinksOnly: options.outputSymlinksOnly, - packageGraph: packageGraph, - trackPerformance: options.trackPerformance, - skipBuildScriptCheck: options.skipBuildScriptCheck, - verbose: options.verbose, - builderConfigOverrides: options.builderConfigOverrides, - isReleaseBuild: options.isReleaseBuild, - logPerformanceDir: options.logPerformanceDir, - buildFilters: options.buildFilters, - ); + while (true) { + final handler = await watch( + builderApplications, + enableLowResourcesMode: options.enableLowResourcesMode, + configKey: options.configKey, + buildDirs: options.buildDirs, + outputSymlinksOnly: options.outputSymlinksOnly, + packageGraph: packageGraph, + trackPerformance: options.trackPerformance, + skipBuildScriptCheck: options.skipBuildScriptCheck, + verbose: options.verbose, + builderConfigOverrides: options.builderConfigOverrides, + isReleaseBuild: options.isReleaseBuild, + logPerformanceDir: options.logPerformanceDir, + buildFilters: options.buildFilters, + ); - final completer = Completer(); - handleBuildResultsStream(handler.buildResults, completer); - return completer.future; + final completer = Completer(); + handleBuildResultsStream(handler.buildResults, completer); + final result = await completer.future; + if (result != ExitCode.tempFail.code) { + return result; + } + } } /// Listens to [buildResults], handling certain types of errors and completing @@ -83,6 +88,9 @@ class WatchCommand extends BuildRunnerCommand { completer.completeError(const BuildScriptChangedException()); } else if (result.failureType == FailureType.buildConfigChanged) { completer.completeError(const BuildConfigChangedException()); + } else if (result.failureType == FailureType.watcherRestarted) { + // TODO(davidmorgan): don't communicate using errors. + completer.complete(ExitCode.tempFail.code); } } }); diff --git a/build_runner/lib/src/generate/watch_impl.dart b/build_runner/lib/src/generate/watch_impl.dart index 98e9977650..32b4d97eb3 100644 --- a/build_runner/lib/src/generate/watch_impl.dart +++ b/build_runner/lib/src/generate/watch_impl.dart @@ -291,6 +291,22 @@ class WatchImpl implements BuildState { ); graphWatcher .watch() + .where((change) { + if (change == null) { + controller.add( + BuildResult( + BuildStatus.failure, + [], + failureType: FailureType.watcherRestarted, + ), + ); + _terminateCompleter.complete(); + return false; + } else { + return true; + } + }) + .whereNotNull() .asyncMap((change) { // Delay any events until the first build is completed. if (firstBuildCompleter.isCompleted) return change; diff --git a/build_runner/lib/src/watcher/graph_watcher.dart b/build_runner/lib/src/watcher/graph_watcher.dart index e60ec37fd6..1b35ec80c6 100644 --- a/build_runner/lib/src/watcher/graph_watcher.dart +++ b/build_runner/lib/src/watcher/graph_watcher.dart @@ -33,18 +33,22 @@ class PackageGraphWatcher { }) : _strategy = watch ?? _default; /// Returns a stream of records for assets that changed in the package graph. - Stream watch() { + /// + /// A `null` event indicates that the watcher failed: watching will continue + /// but some events are missing. + Stream watch() { assert(!_isWatching); _isWatching = true; return LazyStream(_watch); } - Stream _watch() { + Stream _watch() { final allWatchers = _graph.allPackages.values .where((node) => node.dependencyType == DependencyType.path) .map(_strategy) .toList(); + final restartsController = StreamController(); final filteredEvents = allWatchers .map( @@ -52,12 +56,19 @@ class PackageGraphWatcher { Object e, StackTrace s, ) { - buildLog.error( - buildLog.renderThrowable( - 'Failed to watch files in package:${w.node.name}.', - e, - ), - ); + if (e is FileSystemException && + e.message.startsWith( + 'Directory watcher closed unexpectedly', + )) { + restartsController.add(null); + } else { + buildLog.error( + buildLog.renderThrowable( + 'Failed to watch files in package:${w.node.name}.', + e, + ), + ); + } }), ) .toList(); @@ -69,12 +80,13 @@ class PackageGraphWatcher { ); _readyCompleter.complete(); }(); - return StreamGroup.merge(filteredEvents); + return StreamGroup.merge([...filteredEvents, restartsController.stream]); } - bool Function(AssetChange) _nestedPathFilter(PackageNode rootNode) { + bool Function(AssetChange?) _nestedPathFilter(PackageNode rootNode) { final ignorePaths = _nestedPaths(rootNode); - return (change) => !ignorePaths.any(change.id.path.startsWith); + return (change) => + change == null || !ignorePaths.any(change.id.path.startsWith); } // Returns a set of all package paths that are "nested" within a node. diff --git a/build_runner/lib/src/watcher/node_watcher.dart b/build_runner/lib/src/watcher/node_watcher.dart index 5bf7ebe99b..b35a8dbebe 100644 --- a/build_runner/lib/src/watcher/node_watcher.dart +++ b/build_runner/lib/src/watcher/node_watcher.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:build_runner_core/build_runner_core.dart'; import 'package:watcher/watcher.dart'; @@ -29,9 +30,22 @@ class PackageNodeWatcher { : _strategy = watch ?? _default; /// Returns a stream of records for assets that change recursively. - Stream watch() { + /*Stream watch() { _watcher = _strategy(node.path); final events = _watcher.events; return events.map((e) => AssetChange.fromEvent(node, e)); + }*/ + + Stream watch() { + _watcher = _strategy(node.path); + final events = _watcher.events; + final result = StreamController(); + events.listen((e) => result.add(AssetChange.fromEvent(node, e))); + Timer.periodic(const Duration(seconds: 20), (_) { + result.addError( + const FileSystemException('Directory watcher closed unexpectedly'), + ); + }); + return result.stream; } } diff --git a/build_runner/test/watcher/graph_watcher_test.dart b/build_runner/test/watcher/graph_watcher_test.dart index caf0b28b28..3e2599d6a5 100644 --- a/build_runner/test/watcher/graph_watcher_test.dart +++ b/build_runner/test/watcher/graph_watcher_test.dart @@ -60,7 +60,7 @@ void main() { }, ); - final events = []; + final events = []; unawaited(watcher.watch().forEach(events.add)); await watcher.ready; diff --git a/build_runner_core/lib/src/generate/build_result.dart b/build_runner_core/lib/src/generate/build_result.dart index 3b6b0842af..18dc3c0687 100644 --- a/build_runner_core/lib/src/generate/build_result.dart +++ b/build_runner_core/lib/src/generate/build_result.dart @@ -59,6 +59,7 @@ class FailureType { static final cantCreate = FailureType._(73); static final buildConfigChanged = FailureType._(75); static final buildScriptChanged = FailureType._(75); + static final watcherRestarted = FailureType._(75); final int exitCode; FailureType._(this.exitCode); }