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;