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/create.dart';
|
||||||
import 'src/commands/custom_devices.dart';
|
import 'src/commands/custom_devices.dart';
|
||||||
import 'src/commands/daemon.dart';
|
import 'src/commands/daemon.dart';
|
||||||
|
import 'src/commands/debug_adapter.dart';
|
||||||
import 'src/commands/devices.dart';
|
import 'src/commands/devices.dart';
|
||||||
import 'src/commands/doctor.dart';
|
import 'src/commands/doctor.dart';
|
||||||
import 'src/commands/downgrade.dart';
|
import 'src/commands/downgrade.dart';
|
||||||
@ -160,6 +161,7 @@ List<FlutterCommand> generateCommands({
|
|||||||
),
|
),
|
||||||
CreateCommand(verboseHelp: verboseHelp),
|
CreateCommand(verboseHelp: verboseHelp),
|
||||||
DaemonCommand(hidden: !verboseHelp),
|
DaemonCommand(hidden: !verboseHelp),
|
||||||
|
DebugAdapterCommand(verboseHelp: verboseHelp),
|
||||||
DevicesCommand(verboseHelp: verboseHelp),
|
DevicesCommand(verboseHelp: verboseHelp),
|
||||||
DoctorCommand(verbose: verbose),
|
DoctorCommand(verbose: verbose),
|
||||||
DowngradeCommand(verboseHelp: verboseHelp),
|
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 'dart:io';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
import 'package:process/process.dart';
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
import '../src/common.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