Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 51c517c

Browse files
authored
[flutter_tools/dap] Add support for forwarding flutter run --machine exposeUrl requests to the DAP client (#114539)
* [flutter_tools/dap] Add support for forwarding `flutter run --machine` requests to the DAP client Currently the only request that Flutter sends to the client is `app.exposeUrl` though most of this code is generic to support other requests that may be added in future. * Improve comment * Fix thrown strings * StateError -> DebugAdapterException * Add a non-null assertion and assert * Use DebugAdapterException to handle restartRequests sent before process starts * Fix typo + use local var * Don't try to actually send Flutter messages in tests because there's no process
1 parent 3a656b1 commit 51c517c

File tree

3 files changed

+201
-44
lines changed

3 files changed

+201
-44
lines changed

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

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
5050
@override
5151
bool get supportsRestartRequest => true;
5252

53+
/// A list of reverse-requests from `flutter run --machine` that should be forwarded to the client.
54+
final Set<String> _requestsToForwardToClient = <String>{
55+
// The 'app.exposeUrl' request is sent by Flutter to request the client
56+
// exposes a URL to the user and return the public version of that URL.
57+
//
58+
// This supports some web scenarios where the `flutter` tool may be running
59+
// on a different machine to the user (for example a cloud IDE or in VS Code
60+
// remote workspace) so we cannot just use the raw URL because the hostname
61+
// and/or port might not be available to the machine the user is using.
62+
// Instead, the IDE/infrastructure can set up port forwarding/proxying and
63+
// return a user-facing URL that will map to the original (localhost) URL
64+
// Flutter provided.
65+
'app.exposeUrl',
66+
};
67+
68+
/// Completers for reverse requests from Flutter that may need to be handled by the client.
69+
final Map<Object, Completer<Object?>> _reverseRequestCompleters = <Object, Completer<Object?>>{};
70+
5371
/// Whether or not the user requested debugging be enabled.
5472
///
5573
/// For debugging to be enabled, the user must have chosen "Debug" (and not
@@ -151,6 +169,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
151169
sendResponse(null);
152170
break;
153171

172+
// Handle requests (from the client) that provide responses to reverse-requests
173+
// that we forwarded from `flutter run --machine`.
174+
case 'flutter.sendForwardedRequestResponse':
175+
_handleForwardedResponse(args);
176+
sendResponse(null);
177+
break;
178+
154179
default:
155180
await super.customRequest(request, args, sendResponse);
156181
}
@@ -275,42 +300,41 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
275300
sendResponse();
276301
}
277302

278-
/// Sends a request to the Flutter daemon that is running/attaching to the app and waits for a response.
303+
/// Sends a request to the Flutter run daemon that is running/attaching to the app and waits for a response.
279304
///
280-
/// If [failSilently] is `true` (the default) and there is no process, the
281-
/// message will be silently ignored (this is common during the application
282-
/// being stopped, where async messages may be processed). Setting it to
283-
/// `false` will cause a [DebugAdapterException] to be thrown in that case.
305+
/// If there is no process, the message will be silently ignored (this is
306+
/// common during the application being stopped, where async messages may be
307+
/// processed).
284308
Future<Object?> sendFlutterRequest(
285309
String method,
286-
Map<String, Object?>? params, {
287-
bool failSilently = true,
288-
}) async {
289-
final Process? process = this.process;
290-
291-
if (process == null) {
292-
if (failSilently) {
293-
return null;
294-
} else {
295-
throw DebugAdapterException(
296-
'Unable to Restart because Flutter process is not available',
297-
);
298-
}
299-
}
300-
310+
Map<String, Object?>? params,
311+
) async {
301312
final Completer<Object?> completer = Completer<Object?>();
302313
final int id = _flutterRequestId++;
303314
_flutterRequestCompleters[id] = completer;
304315

316+
sendFlutterMessage(<String, Object?>{
317+
'id': id,
318+
'method': method,
319+
'params': params,
320+
});
321+
322+
return completer.future;
323+
}
324+
325+
/// Sends a message to the Flutter run daemon.
326+
///
327+
/// Throws `DebugAdapterException` if a Flutter process is not yet running.
328+
void sendFlutterMessage(Map<String, Object?> message) {
329+
final Process? process = this.process;
330+
if (process == null) {
331+
throw DebugAdapterException('Flutter process has not yet started');
332+
}
333+
334+
final String messageString = jsonEncode(message);
305335
// Flutter requests are always wrapped in brackets as an array.
306-
final String messageString = jsonEncode(
307-
<String, Object?>{'id': id, 'method': method, 'params': params},
308-
);
309336
final String payload = '[$messageString]\n';
310-
311337
process.stdin.writeln(payload);
312-
313-
return completer.future;
314338
}
315339

316340
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
@@ -432,6 +456,62 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
432456
}
433457
}
434458

459+
/// Handles incoming reverse requests from `flutter run --machine`.
460+
///
461+
/// These requests are usually just forwarded to the client via an event
462+
/// (`flutter.forwardedRequest`) and responses are provided by the client in a
463+
/// custom event (`flutter.forwardedRequestResponse`).
464+
void _handleJsonRequest(
465+
Object id,
466+
String method,
467+
Map<String, Object?>? params,
468+
) {
469+
/// A helper to send a client response to Flutter.
470+
void sendResponseToFlutter(Object? id, Object? value, { bool error = false }) {
471+
sendFlutterMessage(<String, Object?>{
472+
'id': id,
473+
if (error)
474+
'error': value
475+
else
476+
'result': value
477+
});
478+
}
479+
480+
// Set up a completer to forward the response back to `flutter` when it arrives.
481+
final Completer<Object?> completer = Completer<Object?>();
482+
_reverseRequestCompleters[id] = completer;
483+
completer.future
484+
.then((Object? value) => sendResponseToFlutter(id, value))
485+
.catchError((Object? e) => sendResponseToFlutter(id, e.toString(), error: true));
486+
487+
if (_requestsToForwardToClient.contains(method)) {
488+
// Forward the request to the client in an event.
489+
sendEvent(
490+
RawEventBody(<String, Object?>{
491+
'id': id,
492+
'method': method,
493+
'params': params,
494+
}),
495+
eventType: 'flutter.forwardedRequest',
496+
);
497+
} else {
498+
completer.completeError(ArgumentError.value(method, 'Unknown request method.'));
499+
}
500+
}
501+
502+
/// Handles client responses to reverse-requests that were forwarded from Flutter.
503+
void _handleForwardedResponse(RawRequestArguments? args) {
504+
final Object? id = args?.args['id'];
505+
final Object? result = args?.args['result'];
506+
final Object? error = args?.args['error'];
507+
final Completer<Object?>? completer = _reverseRequestCompleters[id];
508+
if (error != null) {
509+
completer?.completeError(DebugAdapterException('Client reported an error handling reverse-request $error'));
510+
} else {
511+
completer?.complete(result);
512+
}
513+
}
514+
435515
/// Handles incoming JSON messages from `flutter run --machine` that are responses to requests that we sent.
436516
void _handleJsonResponse(int id, Map<String, Object?> response) {
437517
final Completer<Object?>? handler = _flutterRequestCompleters.remove(id);
@@ -509,10 +589,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
509589
}
510590

511591
final Object? event = payload['event'];
592+
final Object? method = payload['method'];
512593
final Object? params = payload['params'];
513594
final Object? id = payload['id'];
514595
if (event is String && params is Map<String, Object?>?) {
515596
_handleJsonEvent(event, params);
597+
} else if (id != null && method is String && params is Map<String, Object?>?) {
598+
_handleJsonRequest(id, method, params);
516599
} else if (id is int && _flutterRequestCompleters.containsKey(id)) {
517600
_handleJsonResponse(id, payload);
518601
} else {

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,35 @@ void main() {
214214
});
215215
});
216216

217+
group('handles reverse requests', () {
218+
test('app.exposeUrl', () async {
219+
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
220+
fileSystem: MemoryFileSystem.test(style: fsStyle),
221+
platform: platform,
222+
);
223+
224+
// Pretend to be the client, handling any reverse-requests for exposeUrl
225+
// and mapping the host to 'mapped-host'.
226+
adapter.exposeUrlHandler = (String url) => Uri.parse(url).replace(host: 'mapped-host').toString();
227+
228+
// Simulate Flutter asking for a URL to be exposed.
229+
const int requestId = 12345;
230+
adapter.simulateStdoutMessage(<String, Object?>{
231+
'id': requestId,
232+
'method': 'app.exposeUrl',
233+
'params': <String, Object?>{
234+
'url': 'http://localhost:123/',
235+
}
236+
});
237+
238+
// Allow the handler to be processed.
239+
await pumpEventQueue(times: 5000);
240+
241+
final Map<String, Object?> message = adapter.flutterMessages.singleWhere((Map<String, Object?> data) => data['id'] == requestId);
242+
expect(message['result'], 'http://mapped-host:123/');
243+
});
244+
});
245+
217246
group('--start-paused', () {
218247
test('is passed for debug mode', () async {
219248
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(

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

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:collection/collection.dart';
78
import 'package:dds/dap.dart';
89
import 'package:flutter_tools/src/base/file_system.dart';
910
import 'package:flutter_tools/src/base/platform.dart';
@@ -21,34 +22,48 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
2122
final StreamController<List<int>> stdinController = StreamController<List<int>>();
2223
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
2324
final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null);
25+
final ByteStreamServerChannel clientChannel = ByteStreamServerChannel(stdoutController.stream, stdinController.sink, null);
2426

2527
return MockFlutterDebugAdapter._(
26-
stdinController.sink,
27-
stdoutController.stream,
2828
channel,
29+
clientChannel: clientChannel,
2930
fileSystem: fileSystem,
3031
platform: platform,
3132
simulateAppStarted: simulateAppStarted,
3233
);
3334
}
3435

3536
MockFlutterDebugAdapter._(
36-
this.stdin,
37-
this.stdout,
38-
ByteStreamServerChannel channel, {
39-
required FileSystem fileSystem,
40-
required Platform platform,
37+
super.channel, {
38+
required this.clientChannel,
39+
required super.fileSystem,
40+
required super.platform,
4141
this.simulateAppStarted = true,
42-
}) : super(channel, fileSystem: fileSystem, platform: platform);
42+
}) {
43+
clientChannel.listen((ProtocolMessage message) {
44+
_handleDapToClientMessage(message);
45+
});
46+
}
4347

44-
final StreamSink<List<int>> stdin;
45-
final Stream<List<int>> stdout;
48+
int _seq = 1;
49+
final ByteStreamServerChannel clientChannel;
4650
final bool simulateAppStarted;
4751

4852
late String executable;
4953
late List<String> processArgs;
5054
late Map<String, String>? env;
51-
final List<String> flutterRequests = <String>[];
55+
56+
/// A list of all messages sent to the `flutter run` processes `stdin`.
57+
final List<Map<String, Object?>> flutterMessages = <Map<String, Object?>>[];
58+
59+
/// The `method`s of all requests send to the `flutter run` processes `stdin`.
60+
List<String> get flutterRequests => flutterMessages
61+
.map((Map<String, Object?> message) => message['method'] as String?)
62+
.whereNotNull()
63+
.toList();
64+
65+
/// A handler for the 'app.exposeUrl' reverse-request.
66+
String Function(String)? exposeUrlHandler;
5267

5368
@override
5469
Future<void> launchAsProcess({
@@ -75,6 +90,39 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
7590
}
7691
}
7792

93+
/// Handles messages sent from the debug adapter back to the client.
94+
void _handleDapToClientMessage(ProtocolMessage message) {
95+
// Pretend to be the client, delegating any reverse-requests to the relevant
96+
// handler that is provided by the test.
97+
if (message is Event && message.event == 'flutter.forwardedRequest') {
98+
final Map<String, Object?> body = (message.body as Map<String, Object?>?)!;
99+
final String method = (body['method'] as String?)!;
100+
final Map<String, Object?>? params = body['params'] as Map<String, Object?>?;
101+
102+
final Object? result = _handleReverseRequest(method, params);
103+
104+
// Send the result back in the same way the client would.
105+
clientChannel.sendRequest(Request(
106+
seq: _seq++,
107+
command: 'flutter.sendForwardedRequestResponse',
108+
arguments: <String, Object?>{
109+
'id': body['id'],
110+
'result': result,
111+
},
112+
));
113+
}
114+
}
115+
116+
Object? _handleReverseRequest(String method, Map<String, Object?>? params) {
117+
switch (method) {
118+
case 'app.exposeUrl':
119+
final String url = (params!['url'] as String?)!;
120+
return exposeUrlHandler!(url);
121+
default:
122+
throw ArgumentError('Reverse-request $method is unknown');
123+
}
124+
}
125+
78126
/// Simulates a message emitted by the `flutter run` process by directly
79127
/// calling the debug adapters [handleStdout] method.
80128
void simulateStdoutMessage(Map<String, Object?> message) {
@@ -84,13 +132,10 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
84132
}
85133

86134
@override
87-
Future<Object?> sendFlutterRequest(
88-
String method,
89-
Map<String, Object?>? params, {
90-
bool failSilently = true,
91-
}) {
92-
flutterRequests.add(method);
93-
return super.sendFlutterRequest(method, params, failSilently: failSilently);
135+
void sendFlutterMessage(Map<String, Object?> message) {
136+
flutterMessages.add(message);
137+
// Don't call super because it will try to write to the process that we
138+
// didn't actually spawn.
94139
}
95140

96141
@override

0 commit comments

Comments
 (0)