Unverified Commit c659ad6a authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

Add support for attachRequest in DAP, running "flutter attach" (#97652)

* Add support for attachRequest in DAP, which runs "flutter attach"

* Update DAP docs for attachRequest

* Improve doc comments

* Fix comments

* Remove noDebug from attach + create a getter for `debug`

* Fix indent
parent ba01ec8f
......@@ -27,18 +27,20 @@ Arguments common to both `launchRequest` and `attachRequest` are:
- `bool? evaluateToStringInDebugViews` - whether to invoke `toString()` in expression evaluation requests (inc. hovers/watch windows) (if not supplied, defaults to `false`)
- `bool? sendLogsToClient` - used to proxy all VM Service traffic back to the client in custom `dart.log` events (has performance implications, intended for troubleshooting) (if not supplied, defaults to `false`)
- `List<String>? additionalProjectPaths` - paths of any projects (outside of `cwd`) that are open in the users workspace
- `String? cwd` - the working directory for the Dart process to be spawned in
- `String? cwd` - the working directory for the Flutter process to be spawned in
- `List<String>? toolArgs` - arguments for the `flutter run`, `flutter attach` or `flutter test` commands
- `String? customTool` - an optional tool to run instead of `flutter` - the custom tool must be completely compatible with the tool/command it is replacing
- `int? customToolReplacesArgs` - the number of arguments to delete from the beginning of the argument list when invoking `customTool` - e.g. setting `customTool` to `flutter_test_wrapper` and `customToolReplacesArgs` to `1` for a test run would invoke `flutter_test_wrapper foo_test.dart` instead of `flutter test foo_test.dart` (if larger than the number of computed arguments all arguments will be removed, if not supplied will default to `0`)
Arguments specific to `launchRequest` are:
- `bool? noDebug` - whether to run in debug or noDebug mode (if not supplied, defaults to debug)
- `String program` - the path of the Flutter application to run
- `List<String>? args` - arguments to be passed to the Flutter program
- `List<String>? toolArgs` - arguments for the `flutter run` or `flutter test` commands
- `String? customTool` - an optional tool to run instead of `flutter` - the custom tool must be completely compatible with the tool/command it is replacing
- `int? customToolReplacesArgs` - the number of arguments to delete from the beginning of the argument list when invoking `customTool` - e.g. setting `customTool` to `flutter_test_wrapper` and `customToolReplacesArgs` to `1` for a test run would invoke `flutter_test_wrapper foo_test.dart` instead of `flutter test foo_test.dart` (if larger than the number of computed arguments all arguments will be removed, if not supplied will default to `0`)
`attachRequest` is not currently supported, but will be documented here when it is.
Arguments specific to `attachRequest` are:
- `String? vmServiceUri` - the VM Service URI to attach to (if not supplied, Flutter will try to discover it from the device)
## Custom Requests
......
......@@ -83,11 +83,51 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
@override
bool get terminateOnVmServiceClose => false;
/// Whether or not the user requested debugging be enabled.
///
/// debug/noDebug here refers to the DAP "debug" mode and not the Flutter
/// debug mode (vs Profile/Release). It is provided by the client editor based
/// on whether a user chooses to "Run" or "Debug" their app.
///
/// This is always enabled for attach requests, but can be disabled for launch
/// requests via DAP's `noDebug` flag. If `noDebug` is not provided, will
/// default to debugging.
///
/// When not debugging, we will not connect to the VM Service so some
/// functionality (breakpoints, evaluation, etc.) will not be available.
/// Functionality provided via the daemon (hot reload/restart) will still be
/// available.
bool get debug {
final DartCommonLaunchAttachRequestArguments args = this.args;
if (args is FlutterLaunchRequestArguments) {
// Invert DAP's noDebug flag, treating it as false (so _do_ debug) if not
// provided.
return !(args.noDebug ?? false);
}
// Otherwise (attach), always debug.
return true;
}
/// Called by [attachRequest] to request that we actually connect to the app to be debugged.
@override
Future<void> attachImpl() async {
sendOutput('console', '\nAttach is not currently supported');
handleSessionTerminate();
final FlutterAttachRequestArguments args = this.args as FlutterAttachRequestArguments;
final String? vmServiceUri = args.vmServiceUri;
final List<String> toolArgs = <String>[
'attach',
'--machine',
if (vmServiceUri != null)
...<String>['--debug-uri', vmServiceUri],
];
await _startProcess(
toolArgs: toolArgs,
customTool: args.customTool,
customToolReplacesArgs: args.customToolReplacesArgs,
userToolArgs: args.toolArgs,
);
}
/// [customRequest] handles any messages that do not match standard messages in the spec.
......@@ -171,34 +211,46 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
Future<void> launchImpl() async {
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
// "debug"/"noDebug" refers to the DAP "debug" mode and not the Flutter
// debug mode (vs Profile/Release). It is possible for the user to "Run"
// from VS Code (eg. not want to hit breakpoints/etc.) but still be running
// a debug build.
final bool debug = !(args.noDebug ?? false);
final String? program = args.program;
final List<String> toolArgs = <String>[
'run',
'--machine',
if (debug) '--start-paused',
];
await _startProcess(
toolArgs: toolArgs,
customTool: args.customTool,
customToolReplacesArgs: args.customToolReplacesArgs,
targetProgram: args.program,
userToolArgs: args.toolArgs,
userArgs: args.args,
);
}
/// Starts the `flutter` process to run/attach to the required app.
Future<void> _startProcess({
required String? customTool,
required int? customToolReplacesArgs,
required List<String> toolArgs,
required List<String>? userToolArgs,
String? targetProgram,
List<String>? userArgs,
}) async {
// Handle customTool and deletion of any arguments for it.
final String executable = args.customTool ?? fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final int? removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) {
final String executable = customTool ?? fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final int? removeArgs = customToolReplacesArgs;
if (customTool != null && removeArgs != null) {
toolArgs.removeRange(0, math.min(removeArgs, toolArgs.length));
}
final List<String> processArgs = <String>[
...toolArgs,
...?args.toolArgs,
if (program != null) ...<String>[
...?userToolArgs,
if (targetProgram != null) ...<String>[
'--target',
program,
targetProgram,
],
...?args.args,
...?userArgs,
];
// Find the package_config file for this script. This is used by the
......@@ -207,12 +259,12 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
// be correctly classes as "my code", "sdk" or "external packages".
// TODO(dantup): Remove this once https://github.com/dart-lang/sdk/issues/45530
// is done as it will not be necessary.
final String? possibleRoot = program == null
final String? possibleRoot = targetProgram == null
? args.cwd
: fileSystem.path.isAbsolute(program)
? fileSystem.path.dirname(program)
: fileSystem.path.isAbsolute(targetProgram)
? fileSystem.path.dirname(targetProgram)
: fileSystem.path.dirname(
fileSystem.path.normalize(fileSystem.path.join(args.cwd ?? '', args.program)));
fileSystem.path.normalize(fileSystem.path.join(args.cwd ?? '', targetProgram)));
if (possibleRoot != null) {
final File? packageConfig = findPackageConfigFile(possibleRoot);
if (packageConfig != null) {
......@@ -330,8 +382,6 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
void _handleDebugPort(Map<String, Object?> params) {
// When running in noDebug mode, Flutter may still provide us a VM Service
// URI, but we will not connect it because we don't want to do any debugging.
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
final bool debug = !(args.noDebug ?? false);
if (!debug) {
return;
}
......@@ -411,13 +461,13 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
// general output printed by the user.
final String outputCategory = _receivedAppStarted ? 'stdout' : 'console';
// Output in stdout can include both user output (eg. print) and Flutter
// daemon output. Since it's not uncommon for users to print JSON while
// debugging, we must try to detect which messages are likely Flutter
// messages as reliably as possible, as trying to process users output
// as a Flutter message may result in an unhandled error that will
// terminate the debug adater in a way that does not provide feedback
// because the standard crash violates the DAP protocol.
// Output in stdout can include both user output (eg. print) and Flutter
// daemon output. Since it's not uncommon for users to print JSON while
// debugging, we must try to detect which messages are likely Flutter
// messages as reliably as possible, as trying to process users output
// as a Flutter message may result in an unhandled error that will
// terminate the debug adater in a way that does not provide feedback
// because the standard crash violates the DAP protocol.
Object? jsonData;
try {
jsonData = jsonDecode(data);
......@@ -459,9 +509,6 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
bool fullRestart, [
String? reason,
]) async {
final DartCommonLaunchAttachRequestArguments args = this.args;
final bool debug =
args is! FlutterLaunchRequestArguments || args.noDebug != true;
try {
await sendFlutterRequest('app.restart', <String, Object?>{
'appId': _appId,
......
......@@ -7,12 +7,16 @@ import 'package:dds/dap.dart';
/// An implementation of [AttachRequestArguments] that includes all fields used by the Flutter debug adapter.
///
/// This class represents the data passed from the client editor to the debug
/// adapter in attachRequest, which is a request to start debugging an
/// adapter in attachRequest, which is a request to attach to/debug a running
/// application.
class FlutterAttachRequestArguments
extends DartCommonLaunchAttachRequestArguments
implements AttachRequestArguments {
FlutterAttachRequestArguments({
this.toolArgs,
this.customTool,
this.customToolReplacesArgs,
this.vmServiceUri,
Object? restart,
String? name,
String? cwd,
......@@ -34,11 +38,49 @@ class FlutterAttachRequestArguments
sendLogsToClient: sendLogsToClient,
);
FlutterAttachRequestArguments.fromMap(Map<String, Object?> obj):
FlutterAttachRequestArguments.fromMap(Map<String, Object?> obj)
: toolArgs = (obj['toolArgs'] as List<Object?>?)?.cast<String>(),
customTool = obj['customTool'] as String?,
customToolReplacesArgs = obj['customToolReplacesArgs'] as int?,
vmServiceUri = obj['vmServiceUri'] as String?,
super.fromMap(obj);
static FlutterAttachRequestArguments fromJson(Map<String, Object?> obj) =>
FlutterAttachRequestArguments.fromMap(obj);
/// Arguments to be passed to the tool that will run [program] (for example, the VM or Flutter tool).
final List<String>? toolArgs;
/// An optional tool to run instead of "flutter".
///
/// In combination with [customToolReplacesArgs] allows invoking a custom
/// tool instead of "flutter" to launch scripts/tests. The custom tool must be
/// completely compatible with the tool/command it is replacing.
///
/// This field should be a full absolute path if the tool may not be available
/// in `PATH`.
final String? customTool;
/// The number of arguments to delete from the beginning of the argument list
/// when invoking [customTool].
///
/// For example, setting [customTool] to `flutter_test_wrapper` and
/// `customToolReplacesArgs` to `1` for a test run would invoke
/// `flutter_test_wrapper foo_test.dart` instead of `flutter test foo_test.dart`.
final int? customToolReplacesArgs;
/// The VM Service URI of the running Flutter app to connect to.
final String? vmServiceUri;
@override
Map<String, Object?> toJson() => <String, Object?>{
...super.toJson(),
if (toolArgs != null) 'toolArgs': toolArgs,
if (customTool != null) 'customTool': customTool,
if (customToolReplacesArgs != null)
'customToolReplacesArgs': customToolReplacesArgs,
if (vmServiceUri != null) 'vmServiceUri': vmServiceUri,
};
}
/// An implementation of [LaunchRequestArguments] that includes all fields used by the Flutter debug adapter.
......
......@@ -133,7 +133,7 @@ class DapTestClient {
return responses[1] as Response; // Return the initialize response.
}
/// Send a launchRequest to the server, asking it to start a Dart program.
/// Send a launchRequest to the server, asking it to start a Flutter app.
Future<Response> launch({
String? program,
List<String>? args,
......@@ -141,7 +141,6 @@ class DapTestClient {
String? cwd,
bool? noDebug,
List<String>? additionalProjectPaths,
String? console,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
......@@ -165,11 +164,43 @@ class DapTestClient {
sendLogsToClient: captureVmServiceTraffic,
),
// We can't automatically pick the command when using a custom type
// (DartLaunchRequestArguments).
// (FlutterLaunchRequestArguments).
overrideCommand: 'launch',
);
}
/// Send an attachRequest to the server, asking it to attach to an already-running Flutter app.
Future<Response> attach({
List<String>? toolArgs,
String? vmServiceUri,
String? cwd,
List<String>? additionalProjectPaths,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
bool? evaluateToStringInDebugViews,
}) {
return sendRequest(
FlutterAttachRequestArguments(
cwd: cwd,
toolArgs: toolArgs,
vmServiceUri: vmServiceUri,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
// When running out of process, VM Service traffic won't be available
// to the client-side logger, so force logging on which sends VM Service
// traffic in a custom event.
sendLogsToClient: captureVmServiceTraffic,
),
// We can't automatically pick the command when using a custom type
// (FlutterAttachRequestArguments).
overrideCommand: 'attach',
);
}
/// Sends an arbitrary request to the server.
///
/// Returns a Future that completes when the server returns a corresponding
......@@ -357,4 +388,33 @@ extension DapTestClientExtension on DapTestClient {
);
}
/// Sets a breakpoint at [line] in [file].
Future<void> setBreakpoint(String filePath, int line) async {
await sendRequest(
SetBreakpointsArguments(
source: Source(path: filePath),
breakpoints: <SourceBreakpoint>[
SourceBreakpoint(line: line),
],
),
);
}
/// Sends a continue request for the given thread.
///
/// Returns a Future that completes when the server returns a corresponding
/// response.
Future<Response> continue_(int threadId) =>
sendRequest(ContinueArguments(threadId: threadId));
/// Clears breakpoints in [file].
Future<void> clearBreakpoints(String filePath) async {
await sendRequest(
SetBreakpointsArguments(
source: Source(path: filePath),
breakpoints: <SourceBreakpoint>[],
),
);
}
}
......@@ -5,7 +5,13 @@
import 'dart:async';
import 'dart:io';
import 'package:dds/dap.dart';
import 'package:dds/src/dap/logging.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:test/test.dart';
import 'test_client.dart';
......@@ -47,6 +53,72 @@ void expectLines(
}
}
/// Manages running a simple Flutter app to be used in tests that need to attach
/// to an existing process.
class SimpleFlutterRunner {
SimpleFlutterRunner(this.process) {
process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout);
process.stderr.transform(utf8.decoder).listen(_handleStderr);
unawaited(process.exitCode.then(_handleExitCode));
}
void _handleExitCode(int code) {
if (!_vmServiceUriCompleter.isCompleted) {
_vmServiceUriCompleter.completeError('Flutter process ended without producing a VM Service URI');
}
}
void _handleStderr(String err) {
if (!_vmServiceUriCompleter.isCompleted) {
_vmServiceUriCompleter.completeError(err);
}
}
void _handleStdout(String outputLine) {
try {
final Object? json = jsonDecode(outputLine);
// Flutter --machine output is wrapped in [brackets] so will deserialize
// as a list with one item.
if (json is List && json.length == 1) {
final Object? message = json.single;
// Parse the add.debugPort event which contains our VM Service URI.
if (message is Map<String, Object?> && message['event'] == 'app.debugPort') {
final String vmServiceUri = (message['params']! as Map<String, Object?>)['wsUri']! as String;
if (!_vmServiceUriCompleter.isCompleted) {
_vmServiceUriCompleter.complete(Uri.parse(vmServiceUri));
}
}
}
} on FormatException {
// `flutter run` writes a lot of text to stdout so just ignore anything
// that's not valid JSON.
}
}
final Process process;
final Completer<Uri> _vmServiceUriCompleter = Completer<Uri>();
Future<Uri> get vmServiceUri => _vmServiceUriCompleter.future;
static Future<SimpleFlutterRunner> start(Directory projectDirectory) async {
final String flutterToolPath = globals.fs.path.join(Cache.flutterRoot!, 'bin', globals.platform.isWindows ? 'flutter.bat' : 'flutter');
final List<String> args = <String>[
'run',
'--machine',
'-d',
'flutter-tester',
];
final Process process = await Process.start(
flutterToolPath,
args,
workingDirectory: projectDirectory.path,
);
return SimpleFlutterRunner(process);
}
}
/// A helper class containing the DAP server/client for DAP integration tests.
class DapTestSession {
DapTestSession._(this.server, this.client);
......
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