Listen to log reader before VM Service and make delay configurable (#187202)
- Listen to device log reader before VM Service connection in both
`flutter drive` and `flutter run` (Resident Runner) to ensure logs are
captured even if startup fails.
- Wrap VM Service connection in a try-catch and apply a configurable log
flush delay (default 500ms) to allow buffered logs to arrive before
rethrowing.
- Make the delay configurable (`logFlushDelay`) in
`FlutterDriverService` and `FlutterDevice`, defaulting to
`Duration.zero` in tests to keep them fast.
- Add tests in `resident_runner_test.dart` and update
`drive_service_test.dart` to verify the behavior.
Fixes: #162087
## Pre-launch Checklist
- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Ben Konyi <bkonyi@google.com>
diff --git a/packages/flutter_tools/lib/src/drive/drive_service.dart b/packages/flutter_tools/lib/src/drive/drive_service.dart
index b1d5461..8f519d5 100644
--- a/packages/flutter_tools/lib/src/drive/drive_service.dart
+++ b/packages/flutter_tools/lib/src/drive/drive_service.dart
@@ -124,13 +124,15 @@
required String dartSdkPath,
required DevtoolsLauncher devtoolsLauncher,
@visibleForTesting VMServiceConnector vmServiceConnector = connectToVmService,
+ @visibleForTesting Duration logFlushDelay = const Duration(milliseconds: 500),
}) : _applicationPackageFactory = applicationPackageFactory,
_logger = logger,
_platform = platform,
_processUtils = processUtils,
_dartSdkPath = dartSdkPath,
_vmServiceConnector = vmServiceConnector,
- _devtoolsLauncher = devtoolsLauncher;
+ _devtoolsLauncher = devtoolsLauncher,
+ _logFlushDelay = logFlushDelay;
static const _kLaunchAttempts = 3;
@@ -141,6 +143,7 @@
final String _dartSdkPath;
final VMServiceConnector _vmServiceConnector;
final DevtoolsLauncher _devtoolsLauncher;
+ final Duration _logFlushDelay;
Device? _device;
ApplicationPackage? _applicationPackage;
@@ -216,26 +219,34 @@
}
_vmServiceUri = uri.toString();
_device = device;
- if (debuggingOptions.enableDds) {
- try {
- await device.dds.startDartDevelopmentServiceFromDebuggingOptions(
- uri,
- appName:
- 'Kind: Flutter - Device: ${device.displayName} - '
- 'Package: ${_applicationPackage?.name}',
- debuggingOptions: debuggingOptions,
- );
- _vmServiceUri = device.dds.uri.toString();
- } on DartDevelopmentServiceException {
- // If there's another flutter_tools instance still connected to the target
- // application, DDS will already be running remotely and this call will fail.
- // This can be ignored to continue to use the existing remote DDS instance.
- }
- }
- _vmService = await _vmServiceConnector(uri, device: _device, logger: _logger);
+
final DeviceLogReader logReader = await device.getLogReader(app: _applicationPackage);
logReader.logLines.listen(_logger.printStatus);
- await logReader.provideVmService(_vmService);
+
+ try {
+ if (debuggingOptions.enableDds) {
+ try {
+ await device.dds.startDartDevelopmentServiceFromDebuggingOptions(
+ uri,
+ appName:
+ 'Kind: Flutter - Device: ${device.displayName} - '
+ 'Package: ${_applicationPackage?.name}',
+ debuggingOptions: debuggingOptions,
+ );
+ _vmServiceUri = device.dds.uri.toString();
+ } on DartDevelopmentServiceException {
+ // If there's another flutter_tools instance still connected to the target
+ // application, DDS will already be running remotely and this call will fail.
+ // This can be ignored to continue to use the existing remote DDS instance.
+ }
+ }
+ _vmService = await _vmServiceConnector(uri, device: _device, logger: _logger);
+ await logReader.provideVmService(_vmService);
+ } catch (error) {
+ // Allow time for buffered/async log messages (e.g. engine crash logs) to arrive and flush.
+ await Future<void>.delayed(_logFlushDelay);
+ rethrow;
+ }
}
@override
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index fb85b53..12d366f 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -48,8 +48,11 @@
required this.generator,
required this.developmentShaderCompiler,
this.userIdentifier,
+ @visibleForTesting this.logFlushDelay = const Duration(milliseconds: 500),
});
+ final Duration logFlushDelay;
+
/// Create a [FlutterDevice] with optional code generation enabled.
static Future<FlutterDevice> create(
Device device, {
@@ -1287,14 +1290,23 @@
_finished = Completer<int>();
// Listen for service protocol connection to close.
for (final FlutterDevice? device in flutterDevices) {
- await device!.connect(
- debuggingOptions: debuggingOptions,
- reloadSources: reloadSources,
- restart: restart,
- compileExpression: compileExpression,
- hostVmServicePort: debuggingOptions.hostVmServicePort,
- printStructuredErrorLogMethod: printStructuredErrorLog,
- );
+ if (device == null) {
+ continue;
+ }
+ try {
+ await device.connect(
+ debuggingOptions: debuggingOptions,
+ reloadSources: reloadSources,
+ restart: restart,
+ compileExpression: compileExpression,
+ hostVmServicePort: debuggingOptions.hostVmServicePort,
+ printStructuredErrorLogMethod: printStructuredErrorLog,
+ );
+ } catch (error) {
+ // Allow time for buffered/async log messages (e.g. engine crash logs) to arrive and flush.
+ await Future<void>.delayed(device.logFlushDelay);
+ rethrow;
+ }
await device.vmService!.getFlutterViews();
// This hooks up callbacks for when the connection stops in the future.
diff --git a/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart b/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart
index 11b393c..594773b 100644
--- a/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart
+++ b/packages/flutter_tools/test/general.shard/drive/drive_service_test.dart
@@ -404,6 +404,50 @@
);
await driverService.stop();
});
+
+ testWithoutContext(
+ 'Listens to device log reader even if connection to VM service fails',
+ () async {
+ final processManager = FakeProcessManager.empty();
+ final logReader = FakeDeviceLogReader();
+ final DriverService driverService = FlutterDriverService(
+ applicationPackageFactory: FakeApplicationPackageFactory(FakeApplicationPackage()),
+ logger: BufferLogger.test(),
+ platform: FakePlatform(),
+ processUtils: ProcessUtils(logger: BufferLogger.test(), processManager: processManager),
+ dartSdkPath: 'dart',
+ devtoolsLauncher: FakeDevtoolsLauncher(),
+ logFlushDelay: Duration.zero,
+ vmServiceConnector:
+ (
+ Uri httpUri, {
+ ReloadSources? reloadSources,
+ Restart? restart,
+ CompileExpression? compileExpression,
+ FlutterProject? flutterProject,
+ PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
+ io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
+ Device? device,
+ required Logger logger,
+ }) async {
+ throw Exception('Failed to connect to VM service');
+ },
+ );
+ final device = FakeDevice(LaunchResult.failed(), logReader: logReader);
+
+ try {
+ await driverService.reuseApplication(
+ Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
+ device,
+ DebuggingOptions.enabled(BuildInfo.debug),
+ );
+ fail('Expected reuseApplication to fail');
+ } on Exception catch (e) {
+ expect(e.toString(), contains('Failed to connect to VM service'));
+ }
+ expect(logReader.isListened, true);
+ },
+ );
}
FlutterDriverService setUpDriverService({
@@ -466,8 +510,10 @@
}
class FakeDevice extends Fake implements Device {
- FakeDevice(this.result, {this.supportsFlutterExit = true});
+ FakeDevice(this.result, {this.supportsFlutterExit = true, DeviceLogReader? logReader})
+ : _logReader = logReader ?? NoOpDeviceLogReader('test');
+ final DeviceLogReader _logReader;
LaunchResult result;
bool didStopApp = false;
bool didUninstallApp = false;
@@ -495,7 +541,7 @@
Future<DeviceLogReader> getLogReader({
ApplicationPackage? app,
bool includePastLogs = false,
- }) async => NoOpDeviceLogReader('test');
+ }) async => _logReader;
@override
Future<LaunchResult> startApp(
@@ -563,3 +609,25 @@
disposed = true;
}
}
+
+class FakeDeviceLogReader implements DeviceLogReader {
+ final StreamController<String> _logLinesController = StreamController<String>.broadcast();
+ bool isListened = false;
+
+ @override
+ String get name => 'fake_log_reader';
+
+ @override
+ Stream<String> get logLines {
+ isListened = true;
+ return _logLinesController.stream;
+ }
+
+ @override
+ void dispose() {
+ _logLinesController.close();
+ }
+
+ @override
+ Future<void> provideVmService(FlutterVmService connectedVmService) async {}
+}
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart
index 452311e..21aa7c7 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_helpers.dart
@@ -206,10 +206,14 @@
UpdateFSReport report = UpdateFSReport(success: true, invalidatedSourcesCount: 1);
Exception? reportError;
Exception? runColdError;
+ Exception? connectError;
int runHotCode = 0;
int runColdCode = 0;
@override
+ Duration logFlushDelay = Duration.zero;
+
+ @override
ResidentCompiler? generator;
@override
@@ -266,7 +270,11 @@
required DebuggingOptions debuggingOptions,
int? hostVmServicePort,
bool? ipv6 = false,
- }) async {}
+ }) async {
+ if (connectError != null) {
+ throw connectError!;
+ }
+ }
@override
Future<UpdateFSReport> updateDevFS({
diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
index 236ea20..1564f47 100644
--- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart
@@ -2275,6 +2275,20 @@
},
);
+ testUsingContext(
+ 'ResidentRunner delays on connection failure to allow logs to flush',
+ () => testbed.run(() async {
+ flutterDevice.connectError = Exception('Failed to connect');
+ flutterDevice.logFlushDelay = const Duration(milliseconds: 100);
+
+ final stopwatch = Stopwatch()..start();
+ final int result = await residentRunner.attach();
+ stopwatch.stop();
+
+ expect(result, 2);
+ expect(stopwatch.elapsedMilliseconds, greaterThanOrEqualTo(100));
+ }),
+ );
group('ResidentRunner cached Initial Dill Compilation', () {
late TestBed testbed;
late FakeFlutterDevice flutterDevice;