From 9f9010f5e86efc76656188d14e821ad8084860e4 Mon Sep 17 00:00:00 2001 From: Danny Tuppeny Date: Mon, 19 Dec 2022 16:33:26 +0000 Subject: [PATCH] [flutter_tools] Update DAP progress when waiting for Dart Debug extension connection (#116892) Fixes https://github.com/Dart-Code/Dart-Code/issues/4293. --- .../src/debug_adapters/flutter_adapter.dart | 23 ++++++-- .../dap/flutter_adapter_test.dart | 53 +++++++++++++++++-- .../test/general.shard/dap/mocks.dart | 32 +++++++++-- 3 files changed, 96 insertions(+), 12 deletions(-) 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 3daffd0838e..edce7e97c2e 100644 --- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart +++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart @@ -36,6 +36,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { /// The appId of the current running Flutter app. String? _appId; + /// A progress reporter for the applications launch progress. + /// + /// `null` if a launch is not in progress (or has completed). + DapProgressReporter? launchProgress; + /// The ID to use for the next request sent to the Flutter run daemon. int _flutterRequestId = 1; @@ -123,12 +128,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { Future attachImpl() async { final FlutterAttachRequestArguments args = this.args as FlutterAttachRequestArguments; - final DapProgressReporter progress = startProgressNotification( + launchProgress = startProgressNotification( 'launch', 'Flutter', message: 'Attaching…', ); - unawaited(_appStartedCompleter.future.then((_) => progress.end())); final String? vmServiceUri = args.vmServiceUri; final List toolArgs = [ @@ -230,12 +234,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { Future launchImpl() async { final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments; - final DapProgressReporter progress = startProgressNotification( + launchProgress = startProgressNotification( 'launch', 'Flutter', message: 'Launching…', ); - unawaited(_appStartedCompleter.future.then((_) => progress.end())); final List toolArgs = [ 'run', @@ -398,6 +401,8 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { /// Handles the app.started event from Flutter. Future _handleAppStarted() async { + launchProgress?.end(); + launchProgress = null; _appStartedCompleter.complete(); // Send a custom event so the editor knows the app has started. @@ -591,6 +596,16 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter { // If the output wasn't valid JSON, it was standard stdout that should // be passed through to the user. sendOutput(outputCategory, data); + + // Detect if the output contains a prompt about using the Dart Debug + // extension and also update the progress notification to make it clearer + // we're waiting for the user to do something. + if (data.contains('Waiting for connection from Dart debug extension')) { + launchProgress?.update( + message: 'Please click the Dart Debug extension button in the spawned browser window', + ); + } + return; } diff --git a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart index 6d3e9e98a65..9f7fca60c2f 100644 --- a/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart +++ b/packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart @@ -147,6 +147,47 @@ void main() { expect(adapter.dapToFlutterRequests, isNot(contains('app.stop'))); }); + + test('includes Dart Debug extension progress update', () async { + final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter( + fileSystem: MemoryFileSystem.test(style: fsStyle), + platform: platform, + preAppStart: (MockFlutterDebugAdapter adapter) { + adapter.simulateRawStdout('Waiting for connection from Dart debug extension…'); + } + ); + final Completer responseCompleter = Completer(); + + final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments( + cwd: '/project', + program: 'foo.dart', + ); + + // Begin listening for progress events up until `progressEnd` (but don't await yet). + final Future>> progressEventsFuture = + adapter.dapToClientProgressEvents + .takeWhile((Map message) => message['event'] != 'progressEnd') + .map((Map message) => [message['event'], (message['body']! as Map)['message']]) + .toList(); + + // Initialize with progress support. + await adapter.initializeRequest( + MockRequest(), + InitializeRequestArguments(adapterID: 'test', supportsProgressReporting: true, ), + (_) {}, + ); + await adapter.configurationDoneRequest(MockRequest(), null, () {}); + await adapter.launchRequest(MockRequest(), args, responseCompleter.complete); + await responseCompleter.future; + + // Ensure we got the expected events prior to the + final List> progressEvents = await progressEventsFuture; + expect(progressEvents, containsAllInOrder(>[ + ['progressStart', 'Launching…'], + ['progressUpdate', 'Please click the Dart Debug extension button in the spawned browser window'], + // progressEnd isn't included because we used takeWhile to stop when it arrived above. + ])); + }); }); group('attachRequest', () { @@ -221,6 +262,11 @@ void main() { platform: platform, ); + // Start listening for the forwarded event (don't await it yet, it won't + // be triggered until the call below). + final Future> forwardedEvent = adapter.dapToClientMessages + .firstWhere((Map data) => data['event'] == 'flutter.forwardedEvent'); + // Simulate Flutter asking for a URL to be launched. adapter.simulateStdoutMessage({ 'event': 'app.webLaunchUrl', @@ -230,11 +276,8 @@ void main() { } }); - // Allow the handler to be processed. - await pumpEventQueue(times: 5000); - - // Find the forwarded event. - final Map message = adapter.dapToClientMessages.singleWhere((Map data) => data['event'] == 'flutter.forwardedEvent'); + // Wait for the forwarded event. + final Map message = await forwardedEvent; // Ensure the body of the event matches the original event sent by Flutter. expect(message['body'], { 'event': 'app.webLaunchUrl', diff --git a/packages/flutter_tools/test/general.shard/dap/mocks.dart b/packages/flutter_tools/test/general.shard/dap/mocks.dart index b8001783794..07618e93039 100644 --- a/packages/flutter_tools/test/general.shard/dap/mocks.dart +++ b/packages/flutter_tools/test/general.shard/dap/mocks.dart @@ -18,6 +18,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { required FileSystem fileSystem, required Platform platform, bool simulateAppStarted = true, + FutureOr Function(MockFlutterDebugAdapter adapter)? preAppStart, }) { final StreamController> stdinController = StreamController>(); final StreamController> stdoutController = StreamController>(); @@ -30,6 +31,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { fileSystem: fileSystem, platform: platform, simulateAppStarted: simulateAppStarted, + preAppStart: preAppStart, ); } @@ -39,6 +41,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { required super.fileSystem, required super.platform, this.simulateAppStarted = true, + this.preAppStart, }) { clientChannel.listen((ProtocolMessage message) { _handleDapToClientMessage(message); @@ -48,13 +51,24 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { int _seq = 1; final ByteStreamServerChannel clientChannel; final bool simulateAppStarted; + final FutureOr Function(MockFlutterDebugAdapter adapter)? preAppStart; late String executable; late List processArgs; late Map? env; - /// A list of all messages sent from the adapter back to the client. - final List> dapToClientMessages = >[]; + final StreamController> _dapToClientMessagesController = StreamController>.broadcast(); + + /// A stream of all messages sent from the adapter back to the client. + Stream> get dapToClientMessages => _dapToClientMessagesController.stream; + + /// A stream of all progress events sent from the adapter back to the client. + Stream> get dapToClientProgressEvents { + const List progressEventTypes = ['progressStart', 'progressUpdate', 'progressEnd']; + + return dapToClientMessages + .where((Map message) => progressEventTypes.contains(message['event'] as String?)); + } /// A list of all messages sent from the adapter to the `flutter run` processes `stdin`. final List> dapToFlutterMessages = >[]; @@ -79,6 +93,8 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { this.processArgs = processArgs; this.env = env; + await preAppStart?.call(this); + // Simulate the app starting by triggering handling of events that Flutter // would usually write to stdout. if (simulateAppStarted) { @@ -96,7 +112,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { /// Handles messages sent from the debug adapter back to the client. void _handleDapToClientMessage(ProtocolMessage message) { - dapToClientMessages.add(message.toJson()); + _dapToClientMessagesController.add(message.toJson()); // Pretend to be the client, delegating any reverse-requests to the relevant // handler that is provided by the test. @@ -131,12 +147,22 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter { /// Simulates a message emitted by the `flutter run` process by directly /// calling the debug adapters [handleStdout] method. + /// + /// Use [simulateRawStdout] to simulate non-daemon text output. void simulateStdoutMessage(Map message) { // Messages are wrapped in a list because Flutter only processes messages // wrapped in brackets. handleStdout(jsonEncode([message])); } + /// Simulates a string emitted by the `flutter run` process by directly + /// calling the debug adapters [handleStdout] method. + /// + /// Use [simulateStdoutMessage] to simulate a daemon JSON message. + void simulateRawStdout(String output) { + handleStdout(output); + } + @override void sendFlutterMessage(Map message) { dapToFlutterMessages.add(message);