Add a "flutter debug_adapter" command that runs a DAP server (#91802)
This commit is contained in:
parent
8b88645b84
commit
de966d8a49
@ -26,6 +26,7 @@ import 'src/commands/config.dart';
|
||||
import 'src/commands/create.dart';
|
||||
import 'src/commands/custom_devices.dart';
|
||||
import 'src/commands/daemon.dart';
|
||||
import 'src/commands/debug_adapter.dart';
|
||||
import 'src/commands/devices.dart';
|
||||
import 'src/commands/doctor.dart';
|
||||
import 'src/commands/downgrade.dart';
|
||||
@ -160,6 +161,7 @@ List<FlutterCommand> generateCommands({
|
||||
),
|
||||
CreateCommand(verboseHelp: verboseHelp),
|
||||
DaemonCommand(hidden: !verboseHelp),
|
||||
DebugAdapterCommand(verboseHelp: verboseHelp),
|
||||
DevicesCommand(verboseHelp: verboseHelp),
|
||||
DoctorCommand(verbose: verbose),
|
||||
DowngradeCommand(verboseHelp: verboseHelp),
|
||||
|
63
packages/flutter_tools/lib/src/commands/debug_adapter.dart
Normal file
63
packages/flutter_tools/lib/src/commands/debug_adapter.dart
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../debug_adapters/server.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../runner/flutter_command.dart';
|
||||
|
||||
/// This command will start up a Debug Adapter that communicates using the Debug Adapter Protocol (DAP).
|
||||
///
|
||||
/// This is for use by editors and IDEs that have DAP clients to launch and
|
||||
/// debug Flutter apps/tests. It extends the standard Dart DAP implementation
|
||||
/// from DDS with Flutter-specific functionality (such as Hot Restart).
|
||||
///
|
||||
/// The server is intended to be single-use. It should live only for the
|
||||
/// duration of a single debug session in the editor, and terminate when the
|
||||
/// user stops debugging. If a user starts multiple debug sessions
|
||||
/// simultaneously it is expected that the editor will start multiple debug
|
||||
/// adapters.
|
||||
///
|
||||
/// The DAP specification can be found at
|
||||
/// https://microsoft.github.io/debug-adapter-protocol/.
|
||||
class DebugAdapterCommand extends FlutterCommand {
|
||||
DebugAdapterCommand({ bool verboseHelp = false}) : hidden = !verboseHelp {
|
||||
usesIpv6Flag(verboseHelp: verboseHelp);
|
||||
addDdsOptions(verboseHelp: verboseHelp);
|
||||
}
|
||||
|
||||
@override
|
||||
final String name = 'debug-adapter';
|
||||
|
||||
@override
|
||||
List<String> get aliases => const <String>['debug_adapter'];
|
||||
|
||||
@override
|
||||
final String description = 'Run a Debug Adapter Protocol (DAP) server to communicate with the Flutter tool.';
|
||||
|
||||
@override
|
||||
final String category = FlutterCommandCategory.tools;
|
||||
|
||||
@override
|
||||
final bool hidden;
|
||||
|
||||
@override
|
||||
Future<FlutterCommandResult> runCommand() async {
|
||||
final DapServer server = DapServer(
|
||||
globals.stdio.stdin,
|
||||
globals.stdio.stdout.nonBlocking,
|
||||
fileSystem: globals.fs,
|
||||
platform: globals.platform,
|
||||
ipv6: ipv6,
|
||||
enableDds: enableDds,
|
||||
);
|
||||
|
||||
await server.channel.closed;
|
||||
|
||||
return FlutterCommandResult.success();
|
||||
}
|
||||
}
|
62
packages/flutter_tools/lib/src/debug_adapters/README.md
Normal file
62
packages/flutter_tools/lib/src/debug_adapters/README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Debug Adapter Protocol (DAP)
|
||||
|
||||
This document is Flutter-specific. For information on the standard Dart DAP implementation, [see this document](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md).
|
||||
|
||||
Flutter includes support for debugging using [the Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) as an alternative to using the [VM Service](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md) directly, simplying the integration for new editors.
|
||||
|
||||
The debug adapter is started with the `flutter debug-adapter` command and is intended to be consumed by DAP-compliant tools such as Flutter-specific extensions for editors, or configured by users whose editors include generic configurable DAP clients.
|
||||
|
||||
Because in the DAP protocol the client speaks first, running this command from the terminal will result in no output (nor will the process terminate). This is expected behaviour.
|
||||
|
||||
For details on the standard DAP functionality, see [the Debug Adapter Protocol Overview](https://microsoft.github.io/debug-adapter-protocol/) and [the Debug Adapter Protocol Specification](https://microsoft.github.io/debug-adapter-protocol/specification). Custom extensions are detailed below.
|
||||
|
||||
## Launch/Attach Arguments
|
||||
|
||||
Arguments common to both `launchRequest` and `attachRequest` are:
|
||||
|
||||
- `bool? debugExternalPackageLibraries` - whether to enable debugging for packages that are not inside the current workspace (if not supplied, defaults to `true`)
|
||||
- `bool? debugSdkLibraries` - whether to enable debugging for SDK libraries (if not supplied, defaults to `true`)
|
||||
- `bool? evaluateGettersInDebugViews` - whether to evaluate getters in expression evaluation requests (inc. hovers/watch windows) (if not supplied, defaults to `false`)
|
||||
- `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
|
||||
|
||||
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` tool
|
||||
- `String? console` - if set to `"terminal"` or `"externalTerminal"` will be run using the `runInTerminal` reverse-request; otherwise the debug adapter spawns the Dart process
|
||||
- `bool? enableAsserts` - whether to enable asserts (if not supplied, defaults to `true`)
|
||||
|
||||
`attachRequest` is not currently supported, but will be documented here when it is.
|
||||
|
||||
## Custom Requests
|
||||
|
||||
Some custom requests are available for clients to call. Below are the Flutter-specific custom requests, but the standard Dart DAP custom requests are also [documented here](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md#custom-requests).
|
||||
|
||||
### `hotReload`
|
||||
|
||||
`hotReload` injects updated source code files into the running VM and then rebuilds the widget tree. An optional `reason` can be provided and should usually be `"manual"` or `"save"` to indicate what how the reload was triggered (for example by the user clicking a button, versus a hot-reload-on-save feature).
|
||||
|
||||
```
|
||||
{
|
||||
"reason": "manual"
|
||||
}
|
||||
```
|
||||
|
||||
### `hotRestart`
|
||||
|
||||
`hotRestart` updates the code on the device and performs a full restart (which does not preserve state). An optional `reason` can be provided and should usually be `"manual"` or `"save"` to indicate what how the reload was triggered (for example by the user clicking a button, versus a hot-reload-on-save feature).
|
||||
|
||||
```
|
||||
{
|
||||
"reason": "manual"
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Events
|
||||
|
||||
The debug adapter may emit several custom events that are useful to clients. There are not currently any custom Flutter events, but the standard Dart DAP custom requests are [documented here](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md#custom-events).
|
@ -0,0 +1,450 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dds/dap.dart' hide PidTracker, PackageConfigUtils;
|
||||
import 'package:vm_service/vm_service.dart' as vm;
|
||||
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import 'flutter_adapter_args.dart';
|
||||
import 'mixins.dart';
|
||||
|
||||
/// A DAP Debug Adapter for running and debugging Flutter applications.
|
||||
class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments>
|
||||
with PidTracker, PackageConfigUtils {
|
||||
FlutterDebugAdapter(
|
||||
ByteStreamServerChannel channel, {
|
||||
required this.fileSystem,
|
||||
required this.platform,
|
||||
bool ipv6 = false,
|
||||
bool enableDds = true,
|
||||
bool enableAuthCodes = true,
|
||||
Logger? logger,
|
||||
}) : super(
|
||||
channel,
|
||||
ipv6: ipv6,
|
||||
enableDds: enableDds,
|
||||
enableAuthCodes: enableAuthCodes,
|
||||
logger: logger,
|
||||
);
|
||||
|
||||
@override
|
||||
FileSystem fileSystem;
|
||||
Platform platform;
|
||||
Process? _process;
|
||||
|
||||
@override
|
||||
final FlutterLaunchRequestArguments Function(Map<String, Object?> obj)
|
||||
parseLaunchArgs = FlutterLaunchRequestArguments.fromJson;
|
||||
|
||||
@override
|
||||
final FlutterAttachRequestArguments Function(Map<String, Object?> obj)
|
||||
parseAttachArgs = FlutterAttachRequestArguments.fromJson;
|
||||
|
||||
/// A completer that completes when the app.started event has been received.
|
||||
final Completer<void> _appStartedCompleter = Completer<void>();
|
||||
|
||||
/// Whether or not the app.started event has been received.
|
||||
bool get _receivedAppStarted => _appStartedCompleter.isCompleted;
|
||||
|
||||
/// The VM Service URI received from the app.debugPort event.
|
||||
Uri? _vmServiceUri;
|
||||
|
||||
/// The appId of the current running Flutter app.
|
||||
String? _appId;
|
||||
|
||||
/// The ID to use for the next request sent to the Flutter run daemon.
|
||||
int _flutterRequestId = 1;
|
||||
|
||||
/// Outstanding requests that have been sent to the Flutter run daemon and
|
||||
/// their handlers.
|
||||
final Map<int, Completer<Object?>> _flutterRequestCompleters = <int, Completer<Object?>>{};
|
||||
|
||||
/// Whether or not this adapter can handle the restartRequest.
|
||||
///
|
||||
/// For Flutter apps we can handle this with a Hot Restart rather than having
|
||||
/// the whole debug session stopped and restarted.
|
||||
@override
|
||||
bool get supportsRestartRequest => true;
|
||||
|
||||
/// Whether the VM Service closing should be used as a signal to terminate the debug session.
|
||||
///
|
||||
/// Since we always have a process for Flutter (whether run or attach) we'll
|
||||
/// always use its termination instead, so this is always false.
|
||||
@override
|
||||
bool get terminateOnVmServiceClose => false;
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// [customRequest] handles any messages that do not match standard messages in the spec.
|
||||
///
|
||||
/// This is used to allow a client/DA to have custom methods outside of the
|
||||
/// spec. It is up to the client/DA to negotiate which custom messages are
|
||||
/// allowed.
|
||||
///
|
||||
/// [sendResponse] must be called when handling a message, even if it is with
|
||||
/// a null response. Otherwise the client will never be informed that the
|
||||
/// request has completed.
|
||||
///
|
||||
/// Any requests not handled must call super which will respond with an error
|
||||
/// that the message was not supported.
|
||||
///
|
||||
/// Unless they start with _ to indicate they are private, custom messages
|
||||
/// should not change in breaking ways if client IDEs/editors may be calling
|
||||
/// them.
|
||||
@override
|
||||
Future<void> customRequest(
|
||||
Request request,
|
||||
RawRequestArguments? args,
|
||||
void Function(Object?) sendResponse,
|
||||
) async {
|
||||
switch (request.command) {
|
||||
case 'hotRestart':
|
||||
case 'hotReload':
|
||||
final bool isFullRestart = request.command == 'hotRestart';
|
||||
await _performRestart(isFullRestart, args?.args['reason'] as String?);
|
||||
sendResponse(null);
|
||||
break;
|
||||
|
||||
default:
|
||||
await super.customRequest(request, args, sendResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> debuggerConnected(vm.VM vmInfo) async {
|
||||
// Capture the PID from the VM Service so that we can terminate it when
|
||||
// cleaning up. Terminating the process might not be enough as it could be
|
||||
// just a shell script (e.g. flutter.bat on Windows) and may not pass the
|
||||
// signal on correctly.
|
||||
// See: https://github.com/Dart-Code/Dart-Code/issues/907
|
||||
final int? pid = vmInfo.pid;
|
||||
if (pid != null) {
|
||||
pidsToTerminate.add(pid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by [disconnectRequest] to request that we forcefully shut down the app being run (or in the case of an attach, disconnect).
|
||||
///
|
||||
/// Client IDEs/editors should send a terminateRequest before a
|
||||
/// disconnectRequest to allow a graceful shutdown. This method must terminate
|
||||
/// quickly and therefore may leave orphaned processes.
|
||||
@override
|
||||
Future<void> disconnectImpl() async {
|
||||
terminatePids(ProcessSignal.sigkill);
|
||||
}
|
||||
|
||||
/// Called by [launchRequest] to request that we actually start the app to be run/debugged.
|
||||
///
|
||||
/// For debugging, this should start paused, connect to the VM Service, set
|
||||
/// breakpoints, and resume.
|
||||
@override
|
||||
Future<void> launchImpl() async {
|
||||
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
|
||||
final String flutterToolPath = fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
|
||||
|
||||
// "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) ...<String>[
|
||||
'--start-paused',
|
||||
],
|
||||
];
|
||||
final List<String> processArgs = <String>[
|
||||
...toolArgs,
|
||||
...?args.toolArgs,
|
||||
if (program != null) ...<String>[
|
||||
'--target',
|
||||
program,
|
||||
],
|
||||
...?args.args,
|
||||
];
|
||||
|
||||
// Find the package_config file for this script. This is used by the
|
||||
// debugger to map package: URIs to file paths to check whether they're in
|
||||
// the editors workspace (args.cwd/args.additionalProjectPaths) so they can
|
||||
// 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
|
||||
? args.cwd
|
||||
: fileSystem.path.isAbsolute(program)
|
||||
? fileSystem.path.dirname(program)
|
||||
: fileSystem.path.dirname(
|
||||
fileSystem.path.normalize(fileSystem.path.join(args.cwd ?? '', args.program)));
|
||||
if (possibleRoot != null) {
|
||||
final File? packageConfig = findPackageConfigFile(possibleRoot);
|
||||
if (packageConfig != null) {
|
||||
usePackageConfigFile(packageConfig);
|
||||
}
|
||||
}
|
||||
|
||||
logger?.call('Spawning $flutterToolPath with $processArgs in ${args.cwd}');
|
||||
final Process process = await Process.start(
|
||||
flutterToolPath,
|
||||
processArgs,
|
||||
workingDirectory: args.cwd,
|
||||
);
|
||||
_process = process;
|
||||
pidsToTerminate.add(process.pid);
|
||||
|
||||
process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout);
|
||||
process.stderr.listen(_handleStderr);
|
||||
unawaited(process.exitCode.then(_handleExitCode));
|
||||
|
||||
// Delay responding until the app is launched and (optionally) the debugger
|
||||
// is connected.
|
||||
await _appStartedCompleter.future;
|
||||
if (debug) {
|
||||
await debuggerInitialized;
|
||||
}
|
||||
}
|
||||
|
||||
/// restart is called by the client when the user invokes a restart (for example with the button on the debug toolbar).
|
||||
///
|
||||
/// For Flutter, we handle this ourselves be sending a Hot Restart request
|
||||
/// to the running app.
|
||||
@override
|
||||
Future<void> restartRequest(
|
||||
Request request,
|
||||
RestartArguments? args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
await _performRestart(true);
|
||||
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Sends a request to the Flutter 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.
|
||||
Future<Object?> sendFlutterRequest(
|
||||
String method,
|
||||
Map<String, Object?>? params, {
|
||||
bool failSilently = true,
|
||||
}) async {
|
||||
final Process? process = _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 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);
|
||||
|
||||
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).
|
||||
@override
|
||||
Future<void> terminateImpl() async {
|
||||
terminatePids(ProcessSignal.sigterm);
|
||||
await _process?.exitCode;
|
||||
}
|
||||
|
||||
/// Connects to the VM Service if the app.started event has fired, and a VM Service URI is available.
|
||||
void _connectDebuggerIfReady() {
|
||||
final Uri? serviceUri = _vmServiceUri;
|
||||
if (_receivedAppStarted && serviceUri != null) {
|
||||
connectDebugger(serviceUri, resumeIfStarting: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the app.start event from Flutter.
|
||||
void _handleAppStart(Map<String, Object?> params) {
|
||||
_appId = params['appId'] as String?;
|
||||
assert(_appId != null);
|
||||
}
|
||||
|
||||
/// Handles the app.started event from Flutter.
|
||||
void _handleAppStarted() {
|
||||
_appStartedCompleter.complete();
|
||||
_connectDebuggerIfReady();
|
||||
}
|
||||
|
||||
/// Handles the app.debugPort event from Flutter, connecting to the VM Service if everything else is ready.
|
||||
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;
|
||||
}
|
||||
|
||||
// Capture the VM Service URL which we'll connect to when we get app.started.
|
||||
final String? wsUri = params['wsUri'] as String?;
|
||||
if (wsUri != null) {
|
||||
_vmServiceUri = Uri.parse(wsUri);
|
||||
}
|
||||
_connectDebuggerIfReady();
|
||||
}
|
||||
|
||||
/// Handles the Flutter process exiting, terminating the debug session if it has not already begun terminating.
|
||||
void _handleExitCode(int code) {
|
||||
final String codeSuffix = code == 0 ? '' : ' ($code)';
|
||||
logger?.call('Process exited ($code)');
|
||||
handleSessionTerminate(codeSuffix);
|
||||
}
|
||||
|
||||
/// Handles incoming JSON events from `flutter run --machine`.
|
||||
void _handleJsonEvent(String event, Map<String, Object?>? params) {
|
||||
params ??= <String, Object?>{};
|
||||
switch (event) {
|
||||
case 'app.debugPort':
|
||||
_handleDebugPort(params);
|
||||
break;
|
||||
case 'app.start':
|
||||
_handleAppStart(params);
|
||||
break;
|
||||
case 'app.started':
|
||||
_handleAppStarted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
if (handler == null) {
|
||||
logger?.call(
|
||||
'Received response from Flutter run daemon with ID $id '
|
||||
'but had not matching handler',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final Object? error = response['error'];
|
||||
final Object? result = response['result'];
|
||||
if (error != null) {
|
||||
handler.completeError(DebugAdapterException('$error'));
|
||||
} else {
|
||||
handler.complete(result);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStderr(List<int> data) {
|
||||
logger?.call('stderr: $data');
|
||||
sendOutput('stderr', utf8.decode(data));
|
||||
}
|
||||
|
||||
/// Handles stdout from the `flutter run --machine` process, decoding the JSON and calling the appropriate handlers.
|
||||
void _handleStdout(String data) {
|
||||
// Output intended for us to parse is JSON wrapped in brackets:
|
||||
// [{"event":"app.foo","params":{"bar":"baz"}}]
|
||||
// However, it's also possible a user printed things that look a little like
|
||||
// this so try to detect only things we're interested in:
|
||||
// - parses as JSON
|
||||
// - is a List of only a single item that is a Map<String, Object?>
|
||||
// - the item has an "event" field that is a String
|
||||
// - the item has a "params" field that is a Map<String, Object?>?
|
||||
|
||||
logger?.call('stdout: $data');
|
||||
|
||||
// Output is sent as console (eg. output from tooling) until the app has
|
||||
// started, then stdout (users output). This is so info like
|
||||
// "Launching lib/main.dart on Device foo" is formatted differently to
|
||||
// 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.
|
||||
Object? jsonData;
|
||||
try {
|
||||
jsonData = jsonDecode(data);
|
||||
} on FormatException {
|
||||
// If the output wasn't valid JSON, it was standard stdout that should
|
||||
// be passed through to the user.
|
||||
sendOutput(outputCategory, data);
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, Object?>? payload = jsonData is List &&
|
||||
jsonData.length == 1 &&
|
||||
jsonData.first is Map<String, Object?>
|
||||
? jsonData.first as Map<String, Object?>
|
||||
: null;
|
||||
|
||||
if (payload == null) {
|
||||
// JSON didn't match expected format for Flutter responses, so treat as
|
||||
// standard user output.
|
||||
sendOutput(outputCategory, data);
|
||||
return;
|
||||
}
|
||||
|
||||
final Object? event = payload['event'];
|
||||
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 is int && _flutterRequestCompleters.containsKey(id)) {
|
||||
_handleJsonResponse(id, payload);
|
||||
} else {
|
||||
// If it wasn't processed above,
|
||||
sendOutput(outputCategory, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a restart/reload by sending the `app.restart` message to the `flutter run --machine` process.
|
||||
Future<void> _performRestart(
|
||||
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,
|
||||
'fullRestart': fullRestart,
|
||||
'pause': debug,
|
||||
'reason': reason,
|
||||
'debounce': true,
|
||||
});
|
||||
} on DebugAdapterException catch (error) {
|
||||
final String action = fullRestart ? 'Hot Restart' : 'Hot Reload';
|
||||
sendOutput('console', 'Failed to $action: $error');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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
|
||||
/// application.
|
||||
class FlutterAttachRequestArguments
|
||||
extends DartCommonLaunchAttachRequestArguments
|
||||
implements AttachRequestArguments {
|
||||
FlutterAttachRequestArguments({
|
||||
Object? restart,
|
||||
String? name,
|
||||
String? cwd,
|
||||
List<String>? additionalProjectPaths,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
bool? sendLogsToClient,
|
||||
}) : super(
|
||||
name: name,
|
||||
cwd: cwd,
|
||||
restart: restart,
|
||||
additionalProjectPaths: additionalProjectPaths,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
sendLogsToClient: sendLogsToClient,
|
||||
);
|
||||
|
||||
FlutterAttachRequestArguments.fromMap(Map<String, Object?> obj):
|
||||
super.fromMap(obj);
|
||||
|
||||
static FlutterAttachRequestArguments fromJson(Map<String, Object?> obj) =>
|
||||
FlutterAttachRequestArguments.fromMap(obj);
|
||||
}
|
||||
|
||||
/// An implementation of [LaunchRequestArguments] 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 launchRequest, which is a request to start debugging an
|
||||
/// application.
|
||||
class FlutterLaunchRequestArguments
|
||||
extends DartCommonLaunchAttachRequestArguments
|
||||
implements LaunchRequestArguments {
|
||||
FlutterLaunchRequestArguments({
|
||||
this.noDebug,
|
||||
required this.program,
|
||||
this.args,
|
||||
this.toolArgs,
|
||||
Object? restart,
|
||||
String? name,
|
||||
String? cwd,
|
||||
List<String>? additionalProjectPaths,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
bool? sendLogsToClient,
|
||||
}) : super(
|
||||
restart: restart,
|
||||
name: name,
|
||||
cwd: cwd,
|
||||
additionalProjectPaths: additionalProjectPaths,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
sendLogsToClient: sendLogsToClient,
|
||||
);
|
||||
|
||||
FlutterLaunchRequestArguments.fromMap(Map<String, Object?> obj)
|
||||
: noDebug = obj['noDebug'] as bool?,
|
||||
program = obj['program'] as String?,
|
||||
args = (obj['args'] as List<Object?>?)?.cast<String>(),
|
||||
toolArgs = (obj['toolArgs'] as List<Object?>?)?.cast<String>(),
|
||||
super.fromMap(obj);
|
||||
|
||||
/// If noDebug is true the launch request should launch the program without enabling debugging.
|
||||
@override
|
||||
final bool? noDebug;
|
||||
|
||||
/// The program/Flutter app to be run.
|
||||
final String? program;
|
||||
|
||||
/// Arguments to be passed to [program].
|
||||
final List<String>? args;
|
||||
|
||||
/// Arguments to be passed to the tool that will run [program] (for example, the VM or Flutter tool).
|
||||
final List<String>? toolArgs;
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson() => <String, Object?>{
|
||||
...super.toJson(),
|
||||
if (noDebug != null) 'noDebug': noDebug,
|
||||
if (program != null) 'program': program,
|
||||
if (args != null) 'args': args,
|
||||
if (toolArgs != null) 'toolArgs': toolArgs,
|
||||
};
|
||||
|
||||
static FlutterLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
|
||||
FlutterLaunchRequestArguments.fromMap(obj);
|
||||
}
|
64
packages/flutter_tools/lib/src/debug_adapters/mixins.dart
Normal file
64
packages/flutter_tools/lib/src/debug_adapters/mixins.dart
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
|
||||
/// A mixin providing some utility functions for locating/working with
|
||||
/// package_config.json files.
|
||||
///
|
||||
/// Adapted from package:dds/src/dap/adapters/mixins.dart to use Flutter's
|
||||
/// dart:io wrappers.
|
||||
mixin PackageConfigUtils {
|
||||
abstract FileSystem fileSystem;
|
||||
|
||||
/// Find the `package_config.json` file for the program being launched.
|
||||
File? findPackageConfigFile(String possibleRoot) {
|
||||
// TODO(dantup): Remove this once
|
||||
// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
|
||||
// necessary.
|
||||
File? packageConfig;
|
||||
while (true) {
|
||||
packageConfig = fileSystem.file(
|
||||
fileSystem.path.join(possibleRoot, '.dart_tool', 'package_config.json'),
|
||||
);
|
||||
|
||||
// If this packageconfig exists, use it.
|
||||
if (packageConfig.existsSync()) {
|
||||
break;
|
||||
}
|
||||
|
||||
final String parent = fileSystem.path.dirname(possibleRoot);
|
||||
|
||||
// If we can't go up anymore, the search failed.
|
||||
if (parent == possibleRoot) {
|
||||
packageConfig = null;
|
||||
break;
|
||||
}
|
||||
|
||||
possibleRoot = parent;
|
||||
}
|
||||
|
||||
return packageConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/// A mixin for tracking additional PIDs that can be shut down at the end of a debug session.
|
||||
///
|
||||
/// Adapted from package:dds/src/dap/adapters/mixins.dart to use Flutter's
|
||||
/// dart:io wrappers.
|
||||
mixin PidTracker {
|
||||
/// Process IDs to terminate during shutdown.
|
||||
///
|
||||
/// This may be populated with pids from the VM Service to ensure we clean up
|
||||
/// properly where signals may not be passed through the shell to the
|
||||
/// underlying VM process.
|
||||
/// https://github.com/Dart-Code/Dart-Code/issues/907
|
||||
final Set<int> pidsToTerminate = <int>{};
|
||||
|
||||
/// Terminates all processes with the PIDs registered in [pidsToTerminate].
|
||||
void terminatePids(ProcessSignal signal) {
|
||||
pidsToTerminate.forEach(signal.send);
|
||||
}
|
||||
}
|
51
packages/flutter_tools/lib/src/debug_adapters/server.dart
Normal file
51
packages/flutter_tools/lib/src/debug_adapters/server.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dds/dap.dart' hide DapServer;
|
||||
|
||||
import '../base/file_system.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../debug_adapters/flutter_adapter.dart';
|
||||
import '../debug_adapters/flutter_adapter_args.dart';
|
||||
|
||||
/// A DAP server that communicates over a [ByteStreamServerChannel], usually constructed from the processes stdin/stdout streams.
|
||||
///
|
||||
/// The server is intended to be single-use. It should live only for the
|
||||
/// duration of a single debug session in the editor, and terminate when the
|
||||
/// user stops debugging. If a user starts multiple debug sessions
|
||||
/// simultaneously it is expected that the editor will start multiple debug
|
||||
/// adapters.
|
||||
class DapServer {
|
||||
DapServer(
|
||||
Stream<List<int>> _input,
|
||||
StreamSink<List<int>> _output, {
|
||||
required FileSystem fileSystem,
|
||||
required Platform platform,
|
||||
this.ipv6 = false,
|
||||
this.enableDds = true,
|
||||
this.enableAuthCodes = true,
|
||||
this.logger,
|
||||
}) : channel = ByteStreamServerChannel(_input, _output, logger) {
|
||||
adapter = FlutterDebugAdapter(channel,
|
||||
fileSystem: fileSystem,
|
||||
platform: platform,
|
||||
ipv6: ipv6,
|
||||
enableDds: enableDds,
|
||||
enableAuthCodes: enableAuthCodes,
|
||||
logger: logger);
|
||||
}
|
||||
|
||||
final ByteStreamServerChannel channel;
|
||||
late final DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments> adapter;
|
||||
final bool ipv6;
|
||||
final bool enableDds;
|
||||
final bool enableAuthCodes;
|
||||
final Logger? logger;
|
||||
|
||||
void stop() {
|
||||
channel.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dds/src/dap/protocol_generated.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../test_data/basic_project.dart';
|
||||
import '../test_data/compile_error_project.dart';
|
||||
import '../test_utils.dart';
|
||||
import 'test_client.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
void main() {
|
||||
Directory tempDir;
|
||||
/*late*/ DapTestSession dap;
|
||||
final String relativeMainPath = 'lib${fileSystem.path.separator}main.dart';
|
||||
|
||||
setUpAll(() {
|
||||
Cache.flutterRoot = getFlutterRoot();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
tempDir = createResolvedTempDirectorySync('debug_adapter_test.');
|
||||
dap = await DapTestSession.setUp();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await dap.tearDown();
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
testWithoutContext('can run and terminate a Flutter app in debug mode', () async {
|
||||
final BasicProject _project = BasicProject();
|
||||
await _project.setUpIn(tempDir);
|
||||
|
||||
// Once the "topLevelFunction" output arrives, we can terminate the app.
|
||||
unawaited(
|
||||
dap.client.outputEvents
|
||||
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
|
||||
.whenComplete(() => dap.client.terminate()),
|
||||
);
|
||||
|
||||
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
|
||||
launch: () => dap.client
|
||||
.launch(
|
||||
cwd: _project.dir.path,
|
||||
toolArgs: <String>['-d', 'flutter-tester'],
|
||||
),
|
||||
);
|
||||
|
||||
final String output = _uniqueOutputLines(outputEvents);
|
||||
|
||||
expectLines(output, <Object>[
|
||||
'Launching $relativeMainPath on Flutter test device in debug mode...',
|
||||
startsWith('Connecting to VM Service at'),
|
||||
'topLevelFunction',
|
||||
'',
|
||||
startsWith('Exited'),
|
||||
]);
|
||||
});
|
||||
|
||||
testWithoutContext('can run and terminate a Flutter app in noDebug mode', () async {
|
||||
final BasicProject _project = BasicProject();
|
||||
await _project.setUpIn(tempDir);
|
||||
|
||||
// Once the "topLevelFunction" output arrives, we can terminate the app.
|
||||
unawaited(
|
||||
dap.client.outputEvents
|
||||
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
|
||||
.whenComplete(() => dap.client.terminate()),
|
||||
);
|
||||
|
||||
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
|
||||
launch: () => dap.client
|
||||
.launch(
|
||||
cwd: _project.dir.path,
|
||||
noDebug: true,
|
||||
toolArgs: <String>['-d', 'flutter-tester'],
|
||||
),
|
||||
);
|
||||
|
||||
final String output = _uniqueOutputLines(outputEvents);
|
||||
|
||||
expectLines(output, <Object>[
|
||||
'Launching $relativeMainPath on Flutter test device in debug mode...',
|
||||
'topLevelFunction',
|
||||
'',
|
||||
startsWith('Exited'),
|
||||
]);
|
||||
});
|
||||
|
||||
testWithoutContext('correctly outputs launch errors and terminates', () async {
|
||||
final CompileErrorProject _project = CompileErrorProject();
|
||||
await _project.setUpIn(tempDir);
|
||||
|
||||
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
|
||||
launch: () => dap.client
|
||||
.launch(
|
||||
cwd: _project.dir.path,
|
||||
toolArgs: <String>['-d', 'flutter-tester'],
|
||||
),
|
||||
);
|
||||
|
||||
final String output = _uniqueOutputLines(outputEvents);
|
||||
expect(output, contains('this code does not compile'));
|
||||
expect(output, contains('Exception: Failed to build'));
|
||||
expect(output, contains('Exited (1)'));
|
||||
});
|
||||
|
||||
testWithoutContext('can hot reload', () async {
|
||||
final BasicProject _project = BasicProject();
|
||||
await _project.setUpIn(tempDir);
|
||||
|
||||
// Launch the app and wait for it to print "topLevelFunction".
|
||||
await Future.wait(<Future<Object>>[
|
||||
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
|
||||
dap.client.start(
|
||||
launch: () => dap.client.launch(
|
||||
cwd: _project.dir.path,
|
||||
noDebug: true,
|
||||
toolArgs: <String>['-d', 'flutter-tester'],
|
||||
),
|
||||
),
|
||||
], eagerError: true);
|
||||
|
||||
// Capture the next two output events that we expect to be the Reload
|
||||
// notification and then topLevelFunction being printed again.
|
||||
final Future<List<String>> outputEventsFuture = dap.client.output
|
||||
// But skip any topLevelFunctions that come before the reload.
|
||||
.skipWhile((String output) => output.startsWith('topLevelFunction'))
|
||||
.take(2)
|
||||
.toList();
|
||||
|
||||
await dap.client.hotReload();
|
||||
|
||||
expectLines(
|
||||
(await outputEventsFuture).join(),
|
||||
<Object>[
|
||||
startsWith('Reloaded'),
|
||||
'topLevelFunction',
|
||||
],
|
||||
);
|
||||
|
||||
await dap.client.terminate();
|
||||
});
|
||||
|
||||
testWithoutContext('can hot restart', () async {
|
||||
final BasicProject _project = BasicProject();
|
||||
await _project.setUpIn(tempDir);
|
||||
|
||||
// Launch the app and wait for it to print "topLevelFunction".
|
||||
await Future.wait(<Future<Object>>[
|
||||
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
|
||||
dap.client.start(
|
||||
launch: () => dap.client.launch(
|
||||
cwd: _project.dir.path,
|
||||
noDebug: true,
|
||||
toolArgs: <String>['-d', 'flutter-tester'],
|
||||
),
|
||||
),
|
||||
], eagerError: true);
|
||||
|
||||
// Capture the next two output events that we expect to be the Restart
|
||||
// notification and then topLevelFunction being printed again.
|
||||
final Future<List<String>> outputEventsFuture = dap.client.output
|
||||
// But skip any topLevelFunctions that come before the restart.
|
||||
.skipWhile((String output) => output.startsWith('topLevelFunction'))
|
||||
.take(2)
|
||||
.toList();
|
||||
|
||||
await dap.client.hotRestart();
|
||||
|
||||
expectLines(
|
||||
(await outputEventsFuture).join(),
|
||||
<Object>[
|
||||
startsWith('Restarted application'),
|
||||
'topLevelFunction',
|
||||
],
|
||||
);
|
||||
|
||||
await dap.client.terminate();
|
||||
});
|
||||
}
|
||||
|
||||
/// Extracts the output from a set of [OutputEventBody], removing any
|
||||
/// adjacent duplicates and combining into a single string.
|
||||
String _uniqueOutputLines(List<OutputEventBody> outputEvents) {
|
||||
String/*?*/ lastItem;
|
||||
return outputEvents
|
||||
.map((OutputEventBody e) => e.output)
|
||||
.where((String output) {
|
||||
// Skip the item if it's the same as the previous one.
|
||||
final bool isDupe = output == lastItem;
|
||||
lastItem = output;
|
||||
return !isDupe;
|
||||
})
|
||||
.join();
|
||||
}
|
@ -0,0 +1,276 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dds/src/dap/logging.dart';
|
||||
import 'package:dds/src/dap/protocol_generated.dart';
|
||||
import 'package:dds/src/dap/protocol_stream.dart';
|
||||
import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart';
|
||||
|
||||
import 'test_server.dart';
|
||||
|
||||
/// A helper class to simplify acting as a client for interacting with the
|
||||
/// [DapTestServer] in tests.
|
||||
///
|
||||
/// Methods on this class should map directly to protocol methods. Additional
|
||||
/// helpers are available in [DapTestClientExtension].
|
||||
class DapTestClient {
|
||||
DapTestClient._(
|
||||
this._channel,
|
||||
this._logger, {
|
||||
this.captureVmServiceTraffic = false,
|
||||
}) {
|
||||
// Set up a future that will complete when the 'dart.debuggerUris' event is
|
||||
// emitted by the debug adapter so tests have easy access to it.
|
||||
vmServiceUri = event('dart.debuggerUris').then<Uri?>((Event event) {
|
||||
final Map<String, Object?> body = event.body! as Map<String, Object?>;
|
||||
return Uri.parse(body['vmServiceUri']! as String);
|
||||
}).catchError((Object? e) => null);
|
||||
|
||||
_subscription = _channel.listen(
|
||||
_handleMessage,
|
||||
onDone: () {
|
||||
if (_pendingRequests.isNotEmpty) {
|
||||
_logger?.call(
|
||||
'Application terminated without a response to ${_pendingRequests.length} requests');
|
||||
}
|
||||
_pendingRequests.forEach((int id, _OutgoingRequest request) => request.completer.completeError(
|
||||
'Application terminated without a response to request $id (${request.name})'));
|
||||
_pendingRequests.clear();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ByteStreamServerChannel _channel;
|
||||
late final StreamSubscription<String> _subscription;
|
||||
final Logger? _logger;
|
||||
final bool captureVmServiceTraffic;
|
||||
final Map<int, _OutgoingRequest> _pendingRequests = <int, _OutgoingRequest>{};
|
||||
final StreamController<Event> _eventController = StreamController<Event>.broadcast();
|
||||
int _seq = 1;
|
||||
late final Future<Uri?> vmServiceUri;
|
||||
|
||||
/// Returns a stream of [OutputEventBody] events.
|
||||
Stream<OutputEventBody> get outputEvents => events('output')
|
||||
.map((Event e) => OutputEventBody.fromJson(e.body! as Map<String, Object?>));
|
||||
|
||||
/// Returns a stream of the string output from [OutputEventBody] events.
|
||||
Stream<String> get output => outputEvents.map((OutputEventBody output) => output.output);
|
||||
|
||||
/// Sends a custom request to the server and waits for a response.
|
||||
Future<Response> custom(String name, [Object? args]) async {
|
||||
return sendRequest(args, overrideCommand: name);
|
||||
}
|
||||
|
||||
/// Returns a Future that completes with the next [event] event.
|
||||
Future<Event> event(String event) => _eventController.stream.firstWhere(
|
||||
(Event e) => e.event == event,
|
||||
orElse: () => throw 'Did not recieve $event event before stream closed');
|
||||
|
||||
/// Returns a stream for [event] events.
|
||||
Stream<Event> events(String event) {
|
||||
return _eventController.stream.where((Event e) => e.event == event);
|
||||
}
|
||||
|
||||
/// Sends a custom request to the debug adapter to trigger a Hot Reload.
|
||||
Future<Response> hotReload() {
|
||||
return custom('hotReload');
|
||||
}
|
||||
|
||||
/// Sends a custom request to the debug adapter to trigger a Hot Restart.
|
||||
Future<Response> hotRestart() {
|
||||
return custom('hotRestart');
|
||||
}
|
||||
|
||||
/// Send an initialize request to the server.
|
||||
///
|
||||
/// This occurs before the request to start running/debugging a script and is
|
||||
/// used to exchange capabilities and send breakpoints and other settings.
|
||||
Future<Response> initialize({
|
||||
String exceptionPauseMode = 'None',
|
||||
bool? supportsRunInTerminalRequest,
|
||||
}) async {
|
||||
final List<ProtocolMessage> responses = await Future.wait(<Future<ProtocolMessage>>[
|
||||
event('initialized'),
|
||||
sendRequest(InitializeRequestArguments(
|
||||
adapterID: 'test',
|
||||
supportsRunInTerminalRequest: supportsRunInTerminalRequest,
|
||||
)),
|
||||
sendRequest(
|
||||
SetExceptionBreakpointsArguments(
|
||||
filters: <String>[exceptionPauseMode],
|
||||
),
|
||||
),
|
||||
]);
|
||||
await sendRequest(ConfigurationDoneArguments());
|
||||
return responses[1] as Response; // Return the initialize response.
|
||||
}
|
||||
|
||||
/// Send a launchRequest to the server, asking it to start a Dart program.
|
||||
Future<Response> launch({
|
||||
String? program,
|
||||
List<String>? args,
|
||||
List<String>? toolArgs,
|
||||
String? cwd,
|
||||
bool? noDebug,
|
||||
List<String>? additionalProjectPaths,
|
||||
String? console,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
}) {
|
||||
return sendRequest(
|
||||
FlutterLaunchRequestArguments(
|
||||
noDebug: noDebug,
|
||||
program: program,
|
||||
cwd: cwd,
|
||||
args: args,
|
||||
toolArgs: toolArgs,
|
||||
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
|
||||
// (DartLaunchRequestArguments).
|
||||
overrideCommand: 'launch',
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends an arbitrary request to the server.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> sendRequest(Object? arguments,
|
||||
{bool allowFailure = false, String? overrideCommand}) {
|
||||
final String command = overrideCommand ?? commandTypes[arguments.runtimeType]!;
|
||||
final Request request =
|
||||
Request(seq: _seq++, command: command, arguments: arguments);
|
||||
final Completer<Response> completer = Completer<Response>();
|
||||
_pendingRequests[request.seq] =
|
||||
_OutgoingRequest(completer, command, allowFailure);
|
||||
_channel.sendRequest(request);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Initializes the debug adapter and launches [program]/[cwd] or calls the
|
||||
/// custom [launch] method.
|
||||
Future<void> start({
|
||||
String? program,
|
||||
String? cwd,
|
||||
Future<Object?> Function()? launch,
|
||||
}) {
|
||||
return Future.wait(<Future<Object?>>[
|
||||
initialize(),
|
||||
launch?.call() ?? this.launch(program: program, cwd: cwd),
|
||||
], eagerError: true);
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_channel.close();
|
||||
await _subscription.cancel();
|
||||
}
|
||||
|
||||
Future<Response> terminate() => sendRequest(TerminateArguments());
|
||||
|
||||
/// Handles an incoming message from the server, completing the relevant request
|
||||
/// of raising the appropriate event.
|
||||
Future<void> _handleMessage(Object? message) async {
|
||||
if (message is Response) {
|
||||
final _OutgoingRequest? pendingRequest = _pendingRequests.remove(message.requestSeq);
|
||||
if (pendingRequest == null) {
|
||||
return;
|
||||
}
|
||||
final Completer<Response> completer = pendingRequest.completer;
|
||||
if (message.success || pendingRequest.allowFailure) {
|
||||
completer.complete(message);
|
||||
} else {
|
||||
completer.completeError(message);
|
||||
}
|
||||
} else if (message is Event) {
|
||||
_eventController.add(message);
|
||||
|
||||
// When we see a terminated event, close the event stream so if any
|
||||
// tests are waiting on something that will never come, they fail at
|
||||
// a useful location.
|
||||
if (message.event == 'terminated') {
|
||||
unawaited(_eventController.close());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [DapTestClient] that connects the server listening on
|
||||
/// [host]:[port].
|
||||
static Future<DapTestClient> connect(
|
||||
DapTestServer server, {
|
||||
bool captureVmServiceTraffic = false,
|
||||
Logger? logger,
|
||||
}) async {
|
||||
final ByteStreamServerChannel channel = ByteStreamServerChannel(server.stream, server.sink, logger);
|
||||
return DapTestClient._(channel, logger,
|
||||
captureVmServiceTraffic: captureVmServiceTraffic);
|
||||
}
|
||||
}
|
||||
|
||||
class _OutgoingRequest {
|
||||
_OutgoingRequest(this.completer, this.name, this.allowFailure);
|
||||
|
||||
final Completer<Response> completer;
|
||||
final String name;
|
||||
final bool allowFailure;
|
||||
}
|
||||
|
||||
/// Additional helper method for tests to simplify interaction with [DapTestClient].
|
||||
///
|
||||
/// Unlike the methods on [DapTestClient] these methods might not map directly
|
||||
/// onto protocol methods. They may call multiple protocol methods and/or
|
||||
/// simplify assertion specific conditions/results.
|
||||
extension DapTestClientExtension on DapTestClient {
|
||||
/// Collects all output events until the program terminates.
|
||||
///
|
||||
/// These results include all events in the order they are recieved, including
|
||||
/// console, stdout and stderr.
|
||||
///
|
||||
/// Only one of [start] or [launch] may be provided. Use [start] to customise
|
||||
/// the whole start of the session (including initialise) or [launch] to only
|
||||
/// customise the [launchRequest].
|
||||
Future<List<OutputEventBody>> collectAllOutput({
|
||||
String? program,
|
||||
String? cwd,
|
||||
Future<Response> Function()? start,
|
||||
Future<Response> Function()? launch,
|
||||
bool skipInitialPubGetOutput = true
|
||||
}) async {
|
||||
assert(
|
||||
start == null || launch == null,
|
||||
'Only one of "start" or "launch" may be provided',
|
||||
);
|
||||
final Future<List<OutputEventBody>> outputEventsFuture = outputEvents.toList();
|
||||
|
||||
// Don't await these, in case they don't complete (eg. an error prevents
|
||||
// the app from starting).
|
||||
if (start != null) {
|
||||
unawaited(start());
|
||||
} else {
|
||||
unawaited(this.start(program: program, cwd: cwd, launch: launch));
|
||||
}
|
||||
|
||||
final List<OutputEventBody> output = await outputEventsFuture;
|
||||
|
||||
// TODO(dantup): Integration tests currently trigger "flutter pub get" at
|
||||
// the start due to some timestamp manipulation writing the pubspec.
|
||||
// It may be possible to remove this if
|
||||
// https://github.com/flutter/flutter/pull/91300 lands.
|
||||
return skipInitialPubGetOutput
|
||||
? output.skipWhile((OutputEventBody output) => output.output.startsWith('Running "flutter pub get"')).toList()
|
||||
: output;
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dds/src/dap/logging.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/debug_adapters/server.dart';
|
||||
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
|
||||
|
||||
/// Enable to run from local source when running out-of-process (useful in
|
||||
/// development to avoid having to keep rebuilding the flutter tool).
|
||||
const bool _runFromSource = false;
|
||||
|
||||
abstract class DapTestServer {
|
||||
Future<void> stop();
|
||||
StreamSink<List<int>> get sink;
|
||||
Stream<List<int>> get stream;
|
||||
}
|
||||
|
||||
/// An instance of a DAP server running in-process (to aid debugging).
|
||||
///
|
||||
/// All communication still goes over the socket to ensure all messages are
|
||||
/// serialized and deserialized but it's not quite the same running out of
|
||||
/// process.
|
||||
class InProcessDapTestServer extends DapTestServer {
|
||||
InProcessDapTestServer._(List<String> args) {
|
||||
_server = DapServer(
|
||||
stdinController.stream,
|
||||
stdoutController.sink,
|
||||
fileSystem: globals.fs,
|
||||
platform: globals.platform,
|
||||
// Simulate flags based on the args to aid testing.
|
||||
enableDds: !args.contains('--no-dds'),
|
||||
ipv6: args.contains('--ipv6'),
|
||||
);
|
||||
}
|
||||
|
||||
late final DapServer _server;
|
||||
final StreamController<List<int>> stdinController = StreamController<List<int>>();
|
||||
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
|
||||
|
||||
@override
|
||||
StreamSink<List<int>> get sink => stdinController.sink;
|
||||
|
||||
@override
|
||||
Stream<List<int>> get stream => stdoutController.stream;
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_server.stop();
|
||||
}
|
||||
|
||||
static Future<InProcessDapTestServer> create({
|
||||
Logger? logger,
|
||||
List<String>? additionalArgs,
|
||||
}) async {
|
||||
return InProcessDapTestServer._(additionalArgs ?? <String>[]);
|
||||
}
|
||||
}
|
||||
|
||||
/// An instance of a DAP server running out-of-process.
|
||||
///
|
||||
/// This is how an editor will usually consume DAP so is a more accurate test
|
||||
/// but will be a little more difficult to debug tests as the debugger will not
|
||||
/// be attached to the process.
|
||||
class OutOfProcessDapTestServer extends DapTestServer {
|
||||
OutOfProcessDapTestServer._(
|
||||
this._process,
|
||||
Logger? logger,
|
||||
) {
|
||||
// Treat anything written to stderr as the DAP crashing and fail the test
|
||||
// unless it's "Waiting for another flutter command to release the startup lock".
|
||||
_process.stderr
|
||||
.transform(utf8.decoder)
|
||||
.where((String error) => !error.contains('Waiting for another flutter command to release the startup lock'))
|
||||
.listen((String error) {
|
||||
logger?.call(error);
|
||||
throw error;
|
||||
});
|
||||
unawaited(_process.exitCode.then((int code) {
|
||||
final String message = 'Out-of-process DAP server terminated with code $code';
|
||||
logger?.call(message);
|
||||
if (!_isShuttingDown && code != 0) {
|
||||
throw message;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
bool _isShuttingDown = false;
|
||||
final Process _process;
|
||||
|
||||
@override
|
||||
StreamSink<List<int>> get sink => _process.stdin;
|
||||
|
||||
@override
|
||||
Stream<List<int>> get stream => _process.stdout;
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_isShuttingDown = true;
|
||||
_process.kill();
|
||||
await _process.exitCode;
|
||||
}
|
||||
|
||||
static Future<OutOfProcessDapTestServer> create({
|
||||
Logger? logger,
|
||||
List<String>? additionalArgs,
|
||||
}) async {
|
||||
// runFromSource=true will run "dart bin/flutter_tools.dart ..." to avoid
|
||||
// having to rebuild the flutter_tools snapshot.
|
||||
// runFromSource=false will run "flutter ..."
|
||||
|
||||
final String flutterToolPath = globals.fs.path.join(Cache.flutterRoot!, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
|
||||
final String flutterToolsEntryScript = globals.fs.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools', 'bin', 'flutter_tools.dart');
|
||||
|
||||
// When running from source, run "dart bin/flutter_tools.dart debug_adapter"
|
||||
// instead of directly using "flutter debug_adapter".
|
||||
final String executable = _runFromSource
|
||||
? Platform.resolvedExecutable
|
||||
: flutterToolPath;
|
||||
final List<String> args = <String>[
|
||||
if (_runFromSource) flutterToolsEntryScript,
|
||||
'debug-adapter',
|
||||
...?additionalArgs,
|
||||
];
|
||||
|
||||
final Process _process = await Process.start(executable, args);
|
||||
|
||||
return OutOfProcessDapTestServer._(_process, logger);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dds/src/dap/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_client.dart';
|
||||
import 'test_server.dart';
|
||||
|
||||
/// Whether to run the DAP server in-process with the tests, or externally in
|
||||
/// another process.
|
||||
///
|
||||
/// By default tests will run the DAP server out-of-process to match the real
|
||||
/// use from editors, but this complicates debugging the adapter. Set this env
|
||||
/// variables to run the server in-process for easier debugging (this can be
|
||||
/// simplified in VS Code by using a launch config with custom CodeLens links).
|
||||
final bool useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true';
|
||||
|
||||
/// Whether to print all protocol traffic to stdout while running tests.
|
||||
///
|
||||
/// This is useful for debugging locally or on the bots and will include both
|
||||
/// DAP traffic (between the test DAP client and the DAP server) and the VM
|
||||
/// Service traffic (wrapped in a custom 'dart.log' event).
|
||||
final bool verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true';
|
||||
|
||||
/// Expects the lines in [actual] to match the relevant matcher in [expected],
|
||||
/// ignoring differences in line endings and trailing whitespace.
|
||||
void expectLines(String actual, List<Object> expected) {
|
||||
expect(
|
||||
actual.replaceAll('\r\n', '\n').trim().split('\n'),
|
||||
equals(expected),
|
||||
);
|
||||
}
|
||||
|
||||
/// A helper class containing the DAP server/client for DAP integration tests.
|
||||
class DapTestSession {
|
||||
DapTestSession._(this.server, this.client);
|
||||
|
||||
DapTestServer server;
|
||||
DapTestClient client;
|
||||
|
||||
Future<void> tearDown() async {
|
||||
await client.stop();
|
||||
await server.stop();
|
||||
}
|
||||
|
||||
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
|
||||
final DapTestServer server = await _startServer(additionalArgs: additionalArgs);
|
||||
final DapTestClient client = await DapTestClient.connect(
|
||||
server,
|
||||
captureVmServiceTraffic: verboseLogging,
|
||||
logger: verboseLogging ? print : null,
|
||||
);
|
||||
return DapTestSession._(server, client);
|
||||
}
|
||||
|
||||
/// Starts a DAP server that can be shared across tests.
|
||||
static Future<DapTestServer> _startServer({
|
||||
Logger? logger,
|
||||
List<String>? additionalArgs,
|
||||
}) async {
|
||||
return useInProcessDap
|
||||
? await InProcessDapTestServer.create(
|
||||
logger: logger,
|
||||
additionalArgs: additionalArgs,
|
||||
)
|
||||
: await OutOfProcessDapTestServer.create(
|
||||
logger: logger,
|
||||
additionalArgs: additionalArgs,
|
||||
);
|
||||
}
|
||||
}
|
@ -33,7 +33,6 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:process/process.dart';
|
||||
|
||||
import '../src/common.dart';
|
||||
|
@ -0,0 +1,32 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'project.dart';
|
||||
|
||||
class CompileErrorProject extends Project {
|
||||
|
||||
@override
|
||||
final String pubspec = '''
|
||||
name: test
|
||||
environment:
|
||||
sdk: ">=2.12.0-0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
''';
|
||||
|
||||
@override
|
||||
final String main = r'''
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
this code does not compile
|
||||
}
|
||||
''';
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user