Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flutter_tools] Update DAP progress when waiting for Dart Debug extension connection #116892

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -123,12 +128,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
Future<void> 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<String> toolArgs = <String>[
Expand Down Expand Up @@ -230,12 +234,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
Future<void> 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<String> toolArgs = <String>[
'run',
Expand Down Expand Up @@ -398,6 +401,8 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {

/// Handles the app.started event from Flutter.
Future<void> _handleAppStarted() async {
launchProgress?.end();
launchProgress = null;
_appStartedCompleter.complete();

// Send a custom event so the editor knows the app has started.
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> responseCompleter = Completer<void>();

final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
);

// Begin listening for progress events up until `progressEnd` (but don't await yet).
final Future<List<List<Object?>>> progressEventsFuture =
adapter.dapToClientProgressEvents
.takeWhile((Map<String, Object?> message) => message['event'] != 'progressEnd')
.map((Map<String, Object?> message) => <Object?>[message['event'], (message['body']! as Map<String, Object?>)['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<List<Object?>> progressEvents = await progressEventsFuture;
expect(progressEvents, containsAllInOrder(<List<String>>[
<String>['progressStart', 'Launching…'],
<String>['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', () {
Expand Down Expand Up @@ -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<Map<String, Object?>> forwardedEvent = adapter.dapToClientMessages
.firstWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');

// Simulate Flutter asking for a URL to be launched.
adapter.simulateStdoutMessage(<String, Object?>{
'event': 'app.webLaunchUrl',
Expand All @@ -230,11 +276,8 @@ void main() {
}
});

// Allow the handler to be processed.
await pumpEventQueue(times: 5000);

// Find the forwarded event.
final Map<String, Object?> message = adapter.dapToClientMessages.singleWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');
// Wait for the forwarded event.
final Map<String, Object?> message = await forwardedEvent;
// Ensure the body of the event matches the original event sent by Flutter.
expect(message['body'], <String, Object?>{
'event': 'app.webLaunchUrl',
Expand Down
32 changes: 29 additions & 3 deletions packages/flutter_tools/test/general.shard/dap/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
required FileSystem fileSystem,
required Platform platform,
bool simulateAppStarted = true,
FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart,
}) {
final StreamController<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
Expand All @@ -30,6 +31,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
fileSystem: fileSystem,
platform: platform,
simulateAppStarted: simulateAppStarted,
preAppStart: preAppStart,
);
}

Expand All @@ -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);
Expand All @@ -48,13 +51,24 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
int _seq = 1;
final ByteStreamServerChannel clientChannel;
final bool simulateAppStarted;
final FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart;

late String executable;
late List<String> processArgs;
late Map<String, String>? env;

/// A list of all messages sent from the adapter back to the client.
final List<Map<String, Object?>> dapToClientMessages = <Map<String, Object?>>[];
final StreamController<Map<String, Object?>> _dapToClientMessagesController = StreamController<Map<String, Object?>>.broadcast();

/// A stream of all messages sent from the adapter back to the client.
Stream<Map<String, Object?>> get dapToClientMessages => _dapToClientMessagesController.stream;

/// A stream of all progress events sent from the adapter back to the client.
Stream<Map<String, Object?>> get dapToClientProgressEvents {
const List<String> progressEventTypes = <String>['progressStart', 'progressUpdate', 'progressEnd'];

return dapToClientMessages
.where((Map<String, Object?> message) => progressEventTypes.contains(message['event'] as String?));
}

/// A list of all messages sent from the adapter to the `flutter run` processes `stdin`.
final List<Map<String, Object?>> dapToFlutterMessages = <Map<String, Object?>>[];
Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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<String, Object?> message) {
// Messages are wrapped in a list because Flutter only processes messages
// wrapped in brackets.
handleStdout(jsonEncode(<Object?>[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<String, Object?> message) {
dapToFlutterMessages.add(message);
Expand Down