[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
|
@override
|
||||||
bool get supportsRestartRequest => true;
|
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.
|
/// Whether or not the user requested debugging be enabled.
|
||||||
///
|
///
|
||||||
/// For debugging to be enabled, the user must have chosen "Debug" (and not
|
/// For debugging to be enabled, the user must have chosen "Debug" (and not
|
||||||
@ -151,6 +169,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
|
|||||||
sendResponse(null);
|
sendResponse(null);
|
||||||
break;
|
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:
|
default:
|
||||||
await super.customRequest(request, args, sendResponse);
|
await super.customRequest(request, args, sendResponse);
|
||||||
}
|
}
|
||||||
@ -275,44 +300,43 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
|
|||||||
sendResponse();
|
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
|
/// If there is no process, the message will be silently ignored (this is
|
||||||
/// message will be silently ignored (this is common during the application
|
/// common during the application being stopped, where async messages may be
|
||||||
/// being stopped, where async messages may be processed). Setting it to
|
/// processed).
|
||||||
/// `false` will cause a [DebugAdapterException] to be thrown in that case.
|
|
||||||
Future<Object?> sendFlutterRequest(
|
Future<Object?> sendFlutterRequest(
|
||||||
String method,
|
String method,
|
||||||
Map<String, Object?>? params, {
|
Map<String, Object?>? params,
|
||||||
bool failSilently = true,
|
) async {
|
||||||
}) 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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final Completer<Object?> completer = Completer<Object?>();
|
final Completer<Object?> completer = Completer<Object?>();
|
||||||
final int id = _flutterRequestId++;
|
final int id = _flutterRequestId++;
|
||||||
_flutterRequestCompleters[id] = completer;
|
_flutterRequestCompleters[id] = completer;
|
||||||
|
|
||||||
// Flutter requests are always wrapped in brackets as an array.
|
sendFlutterMessage(<String, Object?>{
|
||||||
final String messageString = jsonEncode(
|
'id': id,
|
||||||
<String, Object?>{'id': id, 'method': method, 'params': params},
|
'method': method,
|
||||||
);
|
'params': params,
|
||||||
final String payload = '[$messageString]\n';
|
});
|
||||||
|
|
||||||
process.stdin.writeln(payload);
|
|
||||||
|
|
||||||
return completer.future;
|
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).
|
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
|
||||||
@override
|
@override
|
||||||
Future<void> terminateImpl() async {
|
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.
|
/// Handles incoming JSON messages from `flutter run --machine` that are responses to requests that we sent.
|
||||||
void _handleJsonResponse(int id, Map<String, Object?> response) {
|
void _handleJsonResponse(int id, Map<String, Object?> response) {
|
||||||
final Completer<Object?>? handler = _flutterRequestCompleters.remove(id);
|
final Completer<Object?>? handler = _flutterRequestCompleters.remove(id);
|
||||||
@ -509,10 +589,13 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Object? event = payload['event'];
|
final Object? event = payload['event'];
|
||||||
|
final Object? method = payload['method'];
|
||||||
final Object? params = payload['params'];
|
final Object? params = payload['params'];
|
||||||
final Object? id = payload['id'];
|
final Object? id = payload['id'];
|
||||||
if (event is String && params is Map<String, Object?>?) {
|
if (event is String && params is Map<String, Object?>?) {
|
||||||
_handleJsonEvent(event, params);
|
_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)) {
|
} else if (id is int && _flutterRequestCompleters.containsKey(id)) {
|
||||||
_handleJsonResponse(id, payload);
|
_handleJsonResponse(id, payload);
|
||||||
} else {
|
} 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', () {
|
group('--start-paused', () {
|
||||||
test('is passed for debug mode', () async {
|
test('is passed for debug mode', () async {
|
||||||
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
|
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dds/dap.dart';
|
import 'package:dds/dap.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.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>> stdinController = StreamController<List<int>>();
|
||||||
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
|
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
|
||||||
final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null);
|
final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null);
|
||||||
|
final ByteStreamServerChannel clientChannel = ByteStreamServerChannel(stdoutController.stream, stdinController.sink, null);
|
||||||
|
|
||||||
return MockFlutterDebugAdapter._(
|
return MockFlutterDebugAdapter._(
|
||||||
stdinController.sink,
|
|
||||||
stdoutController.stream,
|
|
||||||
channel,
|
channel,
|
||||||
|
clientChannel: clientChannel,
|
||||||
fileSystem: fileSystem,
|
fileSystem: fileSystem,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
simulateAppStarted: simulateAppStarted,
|
simulateAppStarted: simulateAppStarted,
|
||||||
@ -33,22 +34,36 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MockFlutterDebugAdapter._(
|
MockFlutterDebugAdapter._(
|
||||||
this.stdin,
|
super.channel, {
|
||||||
this.stdout,
|
required this.clientChannel,
|
||||||
ByteStreamServerChannel channel, {
|
required super.fileSystem,
|
||||||
required FileSystem fileSystem,
|
required super.platform,
|
||||||
required Platform platform,
|
|
||||||
this.simulateAppStarted = true,
|
this.simulateAppStarted = true,
|
||||||
}) : super(channel, fileSystem: fileSystem, platform: platform);
|
}) {
|
||||||
|
clientChannel.listen((ProtocolMessage message) {
|
||||||
|
_handleDapToClientMessage(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final StreamSink<List<int>> stdin;
|
int _seq = 1;
|
||||||
final Stream<List<int>> stdout;
|
final ByteStreamServerChannel clientChannel;
|
||||||
final bool simulateAppStarted;
|
final bool simulateAppStarted;
|
||||||
|
|
||||||
late String executable;
|
late String executable;
|
||||||
late List<String> processArgs;
|
late List<String> processArgs;
|
||||||
late Map<String, String>? env;
|
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
|
@override
|
||||||
Future<void> launchAsProcess({
|
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
|
/// Simulates a message emitted by the `flutter run` process by directly
|
||||||
/// calling the debug adapters [handleStdout] method.
|
/// calling the debug adapters [handleStdout] method.
|
||||||
void simulateStdoutMessage(Map<String, Object?> message) {
|
void simulateStdoutMessage(Map<String, Object?> message) {
|
||||||
@ -84,13 +132,10 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Object?> sendFlutterRequest(
|
void sendFlutterMessage(Map<String, Object?> message) {
|
||||||
String method,
|
flutterMessages.add(message);
|
||||||
Map<String, Object?>? params, {
|
// Don't call super because it will try to write to the process that we
|
||||||
bool failSilently = true,
|
// didn't actually spawn.
|
||||||
}) {
|
|
||||||
flutterRequests.add(method);
|
|
||||||
return super.sendFlutterRequest(method, params, failSilently: failSilently);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user