diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart index 8ebdcf7e7cd6..e89f39dcff0f 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart @@ -346,6 +346,7 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { final String messageString = jsonEncode(message); // Flutter requests are always wrapped in brackets as an array. final String payload = '[$messageString]\n'; + _logTraffic('==> [Flutter] $payload'); process.stdin.writeln(payload); } @@ -447,7 +448,7 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { @override void handleExitCode(int code) { final String codeSuffix = code == 0 ? '' : ' ($code)'; - logger?.call('Process exited ($code)'); + _logTraffic('<== [Flutter] Process exited ($code)'); handleSessionTerminate(codeSuffix); } @@ -559,7 +560,7 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { @override void handleStderr(List data) { - logger?.call('stderr: $data'); + _logTraffic('<== [Flutter] [stderr] $data'); sendOutput('stderr', utf8.decode(data)); } @@ -575,7 +576,7 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { // - the item has an "event" field that is a String // - the item has a "params" field that is a Map? - logger?.call('stdout: $data'); + _logTraffic('<== [Flutter] $data'); // Output is sent as console (eg. output from tooling) until the app has // started, then stdout (users output). This is so info like @@ -639,6 +640,22 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { } } + /// Logs JSON traffic to aid debugging. + /// + /// If `sendLogsToClient` was `true` in the launch/attach config, logs will + /// also be sent back to the client in a "dart.log" event to simplify + /// capturing logs from the IDE (such as using the **Dart: Capture Logs** + /// command in VS Code). + void _logTraffic(String message) { + logger?.call(message); + if (sendLogsToClient) { + sendEvent( + RawEventBody({'message': message}), + eventType: 'dart.log', + ); + } + } + /// Performs a restart/reload by sending the `app.restart` message to the `flutter run --machine` process. Future _performRestart( bool fullRestart, [ diff --git a/packages/flutter_tools/test/general.shard/dap/mocks.dart b/packages/flutter_tools/test/general.shard/dap/mocks.dart index fba7f22cf6e3..31022580ae7d 100644 --- a/packages/flutter_tools/test/general.shard/dap/mocks.dart +++ b/packages/flutter_tools/test/general.shard/dap/mocks.dart @@ -57,6 +57,11 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { late List processArgs; late Map? env; + /// Overrides base implementation of [sendLogsToClient] which requires valid + /// `args` to have been set which may not be the case for mocks. + @override + bool get sendLogsToClient => false; + final StreamController> _dapToClientMessagesController = StreamController>.broadcast(); /// A stream of all messages sent from the adapter back to the client. diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart index f92ee3ad09f7..9384e0e7aebd 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart @@ -70,6 +70,39 @@ void main() { ]); }); + testWithoutContext('logs to client when sendLogsToClient=true', () async { + final BasicProject project = BasicProject(); + await project.setUpIn(tempDir); + + // Launch the app and wait for it to print "topLevelFunction". + await Future.wait(>[ + dap.client.stdoutOutput.firstWhere((String output) => output.startsWith('topLevelFunction')), + dap.client.start( + launch: () => dap.client.launch( + cwd: project.dir.path, + noDebug: true, + toolArgs: ['-d', 'flutter-tester'], + sendLogsToClient: true, + ), + ), + ], eagerError: true); + + // Capture events while terminating. + final Future> logEventsFuture = dap.client.events('dart.log').toList(); + await dap.client.terminate(); + + // Ensure logs contain both the app.stop request and the result. + final List logEvents = await logEventsFuture; + final List logMessages = logEvents.map((Event l) => (l.body! as Map)['message']! as String).toList(); + expect( + logMessages, + containsAll([ + startsWith('==> [Flutter] [{"id":1,"method":"app.stop"'), + startsWith('<== [Flutter] [{"id":1,"result":true}]'), + ]), + ); + }); + testWithoutContext('can run and terminate a Flutter app in noDebug mode', () async { final BasicProject project = BasicProject(); await project.setUpIn(tempDir); diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart index 1bb15b3c97db..47a0df502c8f 100644 --- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart +++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart @@ -153,6 +153,7 @@ class DapTestClient { bool? debugExternalPackageLibraries, bool? evaluateGettersInDebugViews, bool? evaluateToStringInDebugViews, + bool sendLogsToClient = false, }) { return sendRequest( FlutterLaunchRequestArguments( @@ -167,9 +168,9 @@ class DapTestClient { evaluateGettersInDebugViews: evaluateGettersInDebugViews, evaluateToStringInDebugViews: evaluateToStringInDebugViews, // When running out of process, VM Service traffic won't be available - // to the client-side logger, so force logging on which sends VM Service - // traffic in a custom event. - sendLogsToClient: captureVmServiceTraffic, + // to the client-side logger, so force logging regardless of + // `sendLogsToClient` which sends VM Service traffic in a custom event. + sendLogsToClient: sendLogsToClient || captureVmServiceTraffic, ), // We can't automatically pick the command when using a custom type // (FlutterLaunchRequestArguments).