Unverified Commit 51c517c0 authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

[flutter_tools/dap] Add support for forwarding `flutter run --machine`...

[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
parent 3a656b16
......@@ -50,6 +50,24 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
@override
bool get supportsRestartRequest => true;
/// A list of reverse-requests from `flutter run --machine` that should be forwarded to the client.
final Set<String> _requestsToForwardToClient = <String>{
// The 'app.exposeUrl' request is sent by Flutter to request the client
// exposes a URL to the user and return the public version of that URL.
//
// This supports some web scenarios where the `flutter` tool may be running
// on a different machine to the user (for example a cloud IDE or in VS Code
// remote workspace) so we cannot just use the raw URL because the hostname
// and/or port might not be available to the machine the user is using.
// Instead, the IDE/infrastructure can set up port forwarding/proxying and
// return a user-facing URL that will map to the original (localhost) URL
// Flutter provided.
'app.exposeUrl',
};
/// Completers for reverse requests from Flutter that may need to be handled by the client.
final Map<Object, Completer<Object?>> _reverseRequestCompleters = <Object, Completer<Object?>>{};
/// Whether or not the user requested debugging be enabled.
///
/// For debugging to be enabled, the user must have chosen "Debug" (and not
......@@ -151,6 +169,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
sendResponse(null);
break;
// Handle requests (from the client) that provide responses to reverse-requests
// that we forwarded from `flutter run --machine`.
case 'flutter.sendForwardedRequestResponse':
_handleForwardedResponse(args);
sendResponse(null);
break;
default:
await super.customRequest(request, args, sendResponse);
}
......@@ -275,42 +300,41 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
sendResponse();
}
/// Sends a request to the Flutter daemon that is running/attaching to the app and waits for a response.
/// Sends a request to the Flutter run daemon that is running/attaching to the app and waits for a response.
///
/// If [failSilently] is `true` (the default) and there is no process, the
/// message will be silently ignored (this is common during the application
/// being stopped, where async messages may be processed). Setting it to
/// `false` will cause a [DebugAdapterException] to be thrown in that case.
/// If there is no process, the message will be silently ignored (this is
/// common during the application being stopped, where async messages may be
/// processed).
Future<Object?> sendFlutterRequest(
String method,
Map<String, Object?>? params, {
bool failSilently = true,
}) async {
final Process? process = this.process;
if (process == null) {
if (failSilently) {
return null;
} else {
throw DebugAdapterException(
'Unable to Restart because Flutter process is not available',
);
}
}
Map<String, Object?>? params,
) async {
final Completer<Object?> completer = Completer<Object?>();
final int id = _flutterRequestId++;
_flutterRequestCompleters[id] = completer;
sendFlutterMessage(<String, Object?>{
'id': id,
'method': method,
'params': params,
});
return completer.future;
}
/// Sends a message to the Flutter run daemon.
///
/// Throws `DebugAdapterException` if a Flutter process is not yet running.
void sendFlutterMessage(Map<String, Object?> message) {
final Process? process = this.process;
if (process == null) {
throw DebugAdapterException('Flutter process has not yet started');
}
final String messageString = jsonEncode(message);
// Flutter requests are always wrapped in brackets as an array.
final String messageString = jsonEncode(
<String, Object?>{'id': id, 'method': method, 'params': params},
);
final String payload = '[$messageString]\n';
process.stdin.writeln(payload);
return completer.future;
}
/// 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 {
}
}
/// Handles incoming reverse requests from `flutter run --machine`.
///
/// These requests are usually just forwarded to the client via an event
/// (`flutter.forwardedRequest`) and responses are provided by the client in a
/// custom event (`flutter.forwardedRequestResponse`).
void _handleJsonRequest(
Object id,
String method,
Map<String, Object?>? params,
) {
/// A helper to send a client response to Flutter.
void sendResponseToFlutter(Object? id, Object? value, { bool error = false }) {
sendFlutterMessage(<String, Object?>{
'id': id,
if (error)
'error': value
else
'result': value
});
}
// Set up a completer to forward the response back to `flutter` when it arrives.
final Completer<Object?> completer = Completer<Object?>();
_reverseRequestCompleters[id] = completer;
completer.future
.then((Object? value) => sendResponseToFlutter(id, value))
.catchError((Object? e) => sendResponseToFlutter(id, e.toString(), error: true));
if (_requestsToForwardToClient.contains(method)) {
// Forward the request to the client in an event.
sendEvent(
RawEventBody(<String, Object?>{
'id': id,
'method': method,
'params': params,
}),
eventType: 'flutter.forwardedRequest',
);
} else {
completer.completeError(ArgumentError.value(method, 'Unknown request method.'));
}
}
/// Handles client responses to reverse-requests that were forwarded from Flutter.
void _handleForwardedResponse(RawRequestArguments? args) {
final Object? id = args?.args['id'];
final Object? result = args?.args['result'];
final Object? error = args?.args['error'];
final Completer<Object?>? completer = _reverseRequestCompleters[id];
if (error != null) {
completer?.completeError(DebugAdapterException('Client reported an error handling reverse-request $error'));
} else {
completer?.complete(result);
}
}
/// Handles incoming JSON messages from `flutter run --machine` that are responses to requests that we sent.
void _handleJsonResponse(int id, Map<String, Object?> response) {
final Completer<Object?>? handler = _flutterRequestCompleters.remove(id);
......@@ -509,10 +589,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
}
final Object? event = payload['event'];
final Object? method = payload['method'];
final Object? params = payload['params'];
final Object? id = payload['id'];
if (event is String && params is Map<String, Object?>?) {
_handleJsonEvent(event, params);
} else if (id != null && method is String && params is Map<String, Object?>?) {
_handleJsonRequest(id, method, params);
} else if (id is int && _flutterRequestCompleters.containsKey(id)) {
_handleJsonResponse(id, payload);
} else {
......
......@@ -214,6 +214,35 @@ void main() {
});
});
group('handles reverse requests', () {
test('app.exposeUrl', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
fileSystem: MemoryFileSystem.test(style: fsStyle),
platform: platform,
);
// Pretend to be the client, handling any reverse-requests for exposeUrl
// and mapping the host to 'mapped-host'.
adapter.exposeUrlHandler = (String url) => Uri.parse(url).replace(host: 'mapped-host').toString();
// Simulate Flutter asking for a URL to be exposed.
const int requestId = 12345;
adapter.simulateStdoutMessage(<String, Object?>{
'id': requestId,
'method': 'app.exposeUrl',
'params': <String, Object?>{
'url': 'http://localhost:123/',
}
});
// Allow the handler to be processed.
await pumpEventQueue(times: 5000);
final Map<String, Object?> message = adapter.flutterMessages.singleWhere((Map<String, Object?> data) => data['id'] == requestId);
expect(message['result'], 'http://mapped-host:123/');
});
});
group('--start-paused', () {
test('is passed for debug mode', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:dds/dap.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
......@@ -21,11 +22,11 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
final StreamController<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null);
final ByteStreamServerChannel clientChannel = ByteStreamServerChannel(stdoutController.stream, stdinController.sink, null);
return MockFlutterDebugAdapter._(
stdinController.sink,
stdoutController.stream,
channel,
clientChannel: clientChannel,
fileSystem: fileSystem,
platform: platform,
simulateAppStarted: simulateAppStarted,
......@@ -33,22 +34,36 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
}
MockFlutterDebugAdapter._(
this.stdin,
this.stdout,
ByteStreamServerChannel channel, {
required FileSystem fileSystem,
required Platform platform,
super.channel, {
required this.clientChannel,
required super.fileSystem,
required super.platform,
this.simulateAppStarted = true,
}) : super(channel, fileSystem: fileSystem, platform: platform);
}) {
clientChannel.listen((ProtocolMessage message) {
_handleDapToClientMessage(message);
});
}
final StreamSink<List<int>> stdin;
final Stream<List<int>> stdout;
int _seq = 1;
final ByteStreamServerChannel clientChannel;
final bool simulateAppStarted;
late String executable;
late List<String> processArgs;
late Map<String, String>? env;
final List<String> flutterRequests = <String>[];
/// A list of all messages sent to the `flutter run` processes `stdin`.
final List<Map<String, Object?>> flutterMessages = <Map<String, Object?>>[];
/// The `method`s of all requests send to the `flutter run` processes `stdin`.
List<String> get flutterRequests => flutterMessages
.map((Map<String, Object?> message) => message['method'] as String?)
.whereNotNull()
.toList();
/// A handler for the 'app.exposeUrl' reverse-request.
String Function(String)? exposeUrlHandler;
@override
Future<void> launchAsProcess({
......@@ -75,6 +90,39 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
}
}
/// Handles messages sent from the debug adapter back to the client.
void _handleDapToClientMessage(ProtocolMessage message) {
// Pretend to be the client, delegating any reverse-requests to the relevant
// handler that is provided by the test.
if (message is Event && message.event == 'flutter.forwardedRequest') {
final Map<String, Object?> body = (message.body as Map<String, Object?>?)!;
final String method = (body['method'] as String?)!;
final Map<String, Object?>? params = body['params'] as Map<String, Object?>?;
final Object? result = _handleReverseRequest(method, params);
// Send the result back in the same way the client would.
clientChannel.sendRequest(Request(
seq: _seq++,
command: 'flutter.sendForwardedRequestResponse',
arguments: <String, Object?>{
'id': body['id'],
'result': result,
},
));
}
}
Object? _handleReverseRequest(String method, Map<String, Object?>? params) {
switch (method) {
case 'app.exposeUrl':
final String url = (params!['url'] as String?)!;
return exposeUrlHandler!(url);
default:
throw ArgumentError('Reverse-request $method is unknown');
}
}
/// Simulates a message emitted by the `flutter run` process by directly
/// calling the debug adapters [handleStdout] method.
void simulateStdoutMessage(Map<String, Object?> message) {
......@@ -84,13 +132,10 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
}
@override
Future<Object?> sendFlutterRequest(
String method,
Map<String, Object?>? params, {
bool failSilently = true,
}) {
flutterRequests.add(method);
return super.sendFlutterRequest(method, params, failSilently: failSilently);
void sendFlutterMessage(Map<String, Object?> message) {
flutterMessages.add(message);
// Don't call super because it will try to write to the process that we
// didn't actually spawn.
}
@override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment