mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[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
This commit is contained in:
parent
3a656b16cb
commit
51c517c03c
@ -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,44 +300,43 @@ 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;
|
||||
|
||||
// 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);
|
||||
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 payload = '[$messageString]\n';
|
||||
process.stdin.writeln(payload);
|
||||
}
|
||||
|
||||
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
|
||||
@override
|
||||
Future<void> terminateImpl() async {
|
||||
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user