Skip to content

Commit 9f9010f

Browse files
authored
[flutter_tools] Update DAP progress when waiting for Dart Debug extension connection (#116892)
Fixes Dart-Code/Dart-Code#4293.
1 parent 3eefb7a commit 9f9010f

File tree

3 files changed

+96
-12
lines changed

3 files changed

+96
-12
lines changed

packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart

+19-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
3636
/// The appId of the current running Flutter app.
3737
String? _appId;
3838

39+
/// A progress reporter for the applications launch progress.
40+
///
41+
/// `null` if a launch is not in progress (or has completed).
42+
DapProgressReporter? launchProgress;
43+
3944
/// The ID to use for the next request sent to the Flutter run daemon.
4045
int _flutterRequestId = 1;
4146

@@ -123,12 +128,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
123128
Future<void> attachImpl() async {
124129
final FlutterAttachRequestArguments args = this.args as FlutterAttachRequestArguments;
125130

126-
final DapProgressReporter progress = startProgressNotification(
131+
launchProgress = startProgressNotification(
127132
'launch',
128133
'Flutter',
129134
message: 'Attaching…',
130135
);
131-
unawaited(_appStartedCompleter.future.then((_) => progress.end()));
132136

133137
final String? vmServiceUri = args.vmServiceUri;
134138
final List<String> toolArgs = <String>[
@@ -230,12 +234,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
230234
Future<void> launchImpl() async {
231235
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
232236

233-
final DapProgressReporter progress = startProgressNotification(
237+
launchProgress = startProgressNotification(
234238
'launch',
235239
'Flutter',
236240
message: 'Launching…',
237241
);
238-
unawaited(_appStartedCompleter.future.then((_) => progress.end()));
239242

240243
final List<String> toolArgs = <String>[
241244
'run',
@@ -398,6 +401,8 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
398401

399402
/// Handles the app.started event from Flutter.
400403
Future<void> _handleAppStarted() async {
404+
launchProgress?.end();
405+
launchProgress = null;
401406
_appStartedCompleter.complete();
402407

403408
// Send a custom event so the editor knows the app has started.
@@ -591,6 +596,16 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
591596
// If the output wasn't valid JSON, it was standard stdout that should
592597
// be passed through to the user.
593598
sendOutput(outputCategory, data);
599+
600+
// Detect if the output contains a prompt about using the Dart Debug
601+
// extension and also update the progress notification to make it clearer
602+
// we're waiting for the user to do something.
603+
if (data.contains('Waiting for connection from Dart debug extension')) {
604+
launchProgress?.update(
605+
message: 'Please click the Dart Debug extension button in the spawned browser window',
606+
);
607+
}
608+
594609
return;
595610
}
596611

packages/flutter_tools/test/general.shard/dap/flutter_adapter_test.dart

+48-5
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,47 @@ void main() {
147147

148148
expect(adapter.dapToFlutterRequests, isNot(contains('app.stop')));
149149
});
150+
151+
test('includes Dart Debug extension progress update', () async {
152+
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
153+
fileSystem: MemoryFileSystem.test(style: fsStyle),
154+
platform: platform,
155+
preAppStart: (MockFlutterDebugAdapter adapter) {
156+
adapter.simulateRawStdout('Waiting for connection from Dart debug extension…');
157+
}
158+
);
159+
final Completer<void> responseCompleter = Completer<void>();
160+
161+
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
162+
cwd: '/project',
163+
program: 'foo.dart',
164+
);
165+
166+
// Begin listening for progress events up until `progressEnd` (but don't await yet).
167+
final Future<List<List<Object?>>> progressEventsFuture =
168+
adapter.dapToClientProgressEvents
169+
.takeWhile((Map<String, Object?> message) => message['event'] != 'progressEnd')
170+
.map((Map<String, Object?> message) => <Object?>[message['event'], (message['body']! as Map<String, Object?>)['message']])
171+
.toList();
172+
173+
// Initialize with progress support.
174+
await adapter.initializeRequest(
175+
MockRequest(),
176+
InitializeRequestArguments(adapterID: 'test', supportsProgressReporting: true, ),
177+
(_) {},
178+
);
179+
await adapter.configurationDoneRequest(MockRequest(), null, () {});
180+
await adapter.launchRequest(MockRequest(), args, responseCompleter.complete);
181+
await responseCompleter.future;
182+
183+
// Ensure we got the expected events prior to the
184+
final List<List<Object?>> progressEvents = await progressEventsFuture;
185+
expect(progressEvents, containsAllInOrder(<List<String>>[
186+
<String>['progressStart', 'Launching…'],
187+
<String>['progressUpdate', 'Please click the Dart Debug extension button in the spawned browser window'],
188+
// progressEnd isn't included because we used takeWhile to stop when it arrived above.
189+
]));
190+
});
150191
});
151192

152193
group('attachRequest', () {
@@ -221,6 +262,11 @@ void main() {
221262
platform: platform,
222263
);
223264

265+
// Start listening for the forwarded event (don't await it yet, it won't
266+
// be triggered until the call below).
267+
final Future<Map<String, Object?>> forwardedEvent = adapter.dapToClientMessages
268+
.firstWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');
269+
224270
// Simulate Flutter asking for a URL to be launched.
225271
adapter.simulateStdoutMessage(<String, Object?>{
226272
'event': 'app.webLaunchUrl',
@@ -230,11 +276,8 @@ void main() {
230276
}
231277
});
232278

233-
// Allow the handler to be processed.
234-
await pumpEventQueue(times: 5000);
235-
236-
// Find the forwarded event.
237-
final Map<String, Object?> message = adapter.dapToClientMessages.singleWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');
279+
// Wait for the forwarded event.
280+
final Map<String, Object?> message = await forwardedEvent;
238281
// Ensure the body of the event matches the original event sent by Flutter.
239282
expect(message['body'], <String, Object?>{
240283
'event': 'app.webLaunchUrl',

packages/flutter_tools/test/general.shard/dap/mocks.dart

+29-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
1818
required FileSystem fileSystem,
1919
required Platform platform,
2020
bool simulateAppStarted = true,
21+
FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart,
2122
}) {
2223
final StreamController<List<int>> stdinController = StreamController<List<int>>();
2324
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
@@ -30,6 +31,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
3031
fileSystem: fileSystem,
3132
platform: platform,
3233
simulateAppStarted: simulateAppStarted,
34+
preAppStart: preAppStart,
3335
);
3436
}
3537

@@ -39,6 +41,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
3941
required super.fileSystem,
4042
required super.platform,
4143
this.simulateAppStarted = true,
44+
this.preAppStart,
4245
}) {
4346
clientChannel.listen((ProtocolMessage message) {
4447
_handleDapToClientMessage(message);
@@ -48,13 +51,24 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
4851
int _seq = 1;
4952
final ByteStreamServerChannel clientChannel;
5053
final bool simulateAppStarted;
54+
final FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart;
5155

5256
late String executable;
5357
late List<String> processArgs;
5458
late Map<String, String>? env;
5559

56-
/// A list of all messages sent from the adapter back to the client.
57-
final List<Map<String, Object?>> dapToClientMessages = <Map<String, Object?>>[];
60+
final StreamController<Map<String, Object?>> _dapToClientMessagesController = StreamController<Map<String, Object?>>.broadcast();
61+
62+
/// A stream of all messages sent from the adapter back to the client.
63+
Stream<Map<String, Object?>> get dapToClientMessages => _dapToClientMessagesController.stream;
64+
65+
/// A stream of all progress events sent from the adapter back to the client.
66+
Stream<Map<String, Object?>> get dapToClientProgressEvents {
67+
const List<String> progressEventTypes = <String>['progressStart', 'progressUpdate', 'progressEnd'];
68+
69+
return dapToClientMessages
70+
.where((Map<String, Object?> message) => progressEventTypes.contains(message['event'] as String?));
71+
}
5872

5973
/// A list of all messages sent from the adapter to the `flutter run` processes `stdin`.
6074
final List<Map<String, Object?>> dapToFlutterMessages = <Map<String, Object?>>[];
@@ -79,6 +93,8 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
7993
this.processArgs = processArgs;
8094
this.env = env;
8195

96+
await preAppStart?.call(this);
97+
8298
// Simulate the app starting by triggering handling of events that Flutter
8399
// would usually write to stdout.
84100
if (simulateAppStarted) {
@@ -96,7 +112,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
96112

97113
/// Handles messages sent from the debug adapter back to the client.
98114
void _handleDapToClientMessage(ProtocolMessage message) {
99-
dapToClientMessages.add(message.toJson());
115+
_dapToClientMessagesController.add(message.toJson());
100116

101117
// Pretend to be the client, delegating any reverse-requests to the relevant
102118
// handler that is provided by the test.
@@ -131,12 +147,22 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
131147

132148
/// Simulates a message emitted by the `flutter run` process by directly
133149
/// calling the debug adapters [handleStdout] method.
150+
///
151+
/// Use [simulateRawStdout] to simulate non-daemon text output.
134152
void simulateStdoutMessage(Map<String, Object?> message) {
135153
// Messages are wrapped in a list because Flutter only processes messages
136154
// wrapped in brackets.
137155
handleStdout(jsonEncode(<Object?>[message]));
138156
}
139157

158+
/// Simulates a string emitted by the `flutter run` process by directly
159+
/// calling the debug adapters [handleStdout] method.
160+
///
161+
/// Use [simulateStdoutMessage] to simulate a daemon JSON message.
162+
void simulateRawStdout(String output) {
163+
handleStdout(output);
164+
}
165+
140166
@override
141167
void sendFlutterMessage(Map<String, Object?> message) {
142168
dapToFlutterMessages.add(message);

0 commit comments

Comments
 (0)