mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 00:39:49 +00:00
[dds] Add support for breakpoints and stepping to DAP
Change-Id: Iebd2c5630f826effac56ece3b4ffb3252b9ac556 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/201834 Reviewed-by: Ben Konyi <bkonyi@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
d5d1d6e6f6
commit
6ebcabaaf2
|
@ -9,8 +9,10 @@ import 'package:collection/collection.dart';
|
|||
import 'package:vm_service/vm_service.dart' as vm;
|
||||
|
||||
import '../base_debug_adapter.dart';
|
||||
import '../exceptions.dart';
|
||||
import '../isolate_manager.dart';
|
||||
import '../logging.dart';
|
||||
import '../protocol_converter.dart';
|
||||
import '../protocol_generated.dart';
|
||||
import '../protocol_stream.dart';
|
||||
|
||||
|
@ -56,10 +58,13 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
final _debuggerInitializedCompleter = Completer<void>();
|
||||
final _configurationDoneCompleter = Completer<void>();
|
||||
|
||||
/// Managers VM Isolates and their events, including fanning out any requests
|
||||
/// Manages VM Isolates and their events, including fanning out any requests
|
||||
/// to set breakpoints etc. from the client to all Isolates.
|
||||
late IsolateManager _isolateManager;
|
||||
|
||||
/// A helper that handlers converting to/from DAP and VM Service types.
|
||||
late ProtocolConverter _converter;
|
||||
|
||||
/// All active VM Service subscriptions.
|
||||
///
|
||||
/// TODO(dantup): This may be changed to use StreamManager as part of using
|
||||
|
@ -80,6 +85,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
DartDebugAdapter(ByteStreamServerChannel channel, Logger? logger)
|
||||
: super(channel, logger) {
|
||||
_isolateManager = IsolateManager(this);
|
||||
_converter = ProtocolConverter(this);
|
||||
}
|
||||
|
||||
/// Completes when the debugger initialization has completed. Used to delay
|
||||
|
@ -92,19 +98,16 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// an existing app. This will only be called once (and only one of this or
|
||||
/// launchRequest will be called).
|
||||
@override
|
||||
FutureOr<void> attachRequest(
|
||||
Future<void> attachRequest(
|
||||
Request request,
|
||||
T args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
this.args = args;
|
||||
isAttach = true;
|
||||
|
||||
// Don't start launching until configurationDone.
|
||||
if (!_configurationDoneCompleter.isCompleted) {
|
||||
logger?.call('Waiting for configurationDone request...');
|
||||
await _configurationDoneCompleter.future;
|
||||
}
|
||||
// Common setup.
|
||||
await _prepareForLaunchOrAttach();
|
||||
|
||||
// TODO(dantup): Implement attach support.
|
||||
throw UnimplementedError();
|
||||
|
@ -112,7 +115,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
// Delegate to the sub-class to attach to the process.
|
||||
// await attachImpl();
|
||||
//
|
||||
// sendResponse(null);
|
||||
// sendResponse();
|
||||
}
|
||||
|
||||
/// configurationDone is called by the client when it has finished sending
|
||||
|
@ -123,13 +126,13 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// been sent to ensure we're not still getting breakpoints (which are sent
|
||||
/// per-file) while we're launching and initializing over the VM Service.
|
||||
@override
|
||||
FutureOr<void> configurationDoneRequest(
|
||||
Future<void> configurationDoneRequest(
|
||||
Request request,
|
||||
ConfigurationDoneArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
_configurationDoneCompleter.complete();
|
||||
sendResponse(null);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Connects to the VM Service at [uri] and initializes debugging.
|
||||
|
@ -219,13 +222,25 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
_debuggerInitializedCompleter.complete();
|
||||
}
|
||||
|
||||
/// Handles the clients "continue" ("resume") request for the thread in
|
||||
/// [args.threadId].
|
||||
@override
|
||||
Future<void> continueRequest(
|
||||
Request request,
|
||||
ContinueArguments args,
|
||||
void Function(ContinueResponseBody) sendResponse,
|
||||
) async {
|
||||
await _isolateManager.resumeThread(args.threadId);
|
||||
sendResponse(ContinueResponseBody(allThreadsContinued: false));
|
||||
}
|
||||
|
||||
/// Overridden by sub-classes to perform any additional setup after the VM
|
||||
/// Service is connected.
|
||||
FutureOr<void> debuggerConnected(vm.VM vmInfo);
|
||||
Future<void> debuggerConnected(vm.VM vmInfo);
|
||||
|
||||
/// Overridden by sub-classes to handle when the client sends a
|
||||
/// `disconnectRequest` (a forceful request to shut down).
|
||||
FutureOr<void> disconnectImpl();
|
||||
Future<void> disconnectImpl();
|
||||
|
||||
/// disconnectRequest is called by the client when it wants to forcefully shut
|
||||
/// us down quickly. This comes after the `terminateRequest` which is intended
|
||||
|
@ -237,13 +252,13 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
///
|
||||
/// https://microsoft.github.io/debug-adapter-protocol/overview#debug-session-end
|
||||
@override
|
||||
FutureOr<void> disconnectRequest(
|
||||
Future<void> disconnectRequest(
|
||||
Request request,
|
||||
DisconnectArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
await disconnectImpl();
|
||||
sendResponse(null);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// initializeRequest is the first request send by the client during
|
||||
|
@ -254,7 +269,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// https://microsoft.github.io/debug-adapter-protocol/overview#initialization
|
||||
/// with a summary in this classes description.
|
||||
@override
|
||||
FutureOr<void> initializeRequest(
|
||||
Future<void> initializeRequest(
|
||||
Request request,
|
||||
InitializeRequestArguments? args,
|
||||
void Function(Capabilities) sendResponse,
|
||||
|
@ -293,30 +308,39 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
///
|
||||
/// Sub-classes can use the [args] field to access the arguments provided
|
||||
/// to this request.
|
||||
FutureOr<void> launchImpl();
|
||||
Future<void> launchImpl();
|
||||
|
||||
/// launchRequest is called by the client when it wants us to to start the app
|
||||
/// to be run/debug. This will only be called once (and only one of this or
|
||||
/// attachRequest will be called).
|
||||
@override
|
||||
FutureOr<void> launchRequest(
|
||||
Future<void> launchRequest(
|
||||
Request request,
|
||||
T args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
this.args = args;
|
||||
isAttach = false;
|
||||
|
||||
// Don't start launching until configurationDone.
|
||||
if (!_configurationDoneCompleter.isCompleted) {
|
||||
logger?.call('Waiting for configurationDone request...');
|
||||
await _configurationDoneCompleter.future;
|
||||
}
|
||||
// Common setup.
|
||||
await _prepareForLaunchOrAttach();
|
||||
|
||||
// Delegate to the sub-class to launch the process.
|
||||
await launchImpl();
|
||||
|
||||
sendResponse(null);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Handles the clients "next" ("step over") request for the thread in
|
||||
/// [args.threadId].
|
||||
@override
|
||||
Future<void> nextRequest(
|
||||
Request request,
|
||||
NextArguments args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
await _isolateManager.resumeThread(args.threadId, vm.StepOption.kOver);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Sends an OutputEvent (without a newline, since calls to this method
|
||||
|
@ -337,9 +361,160 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
sendOutput(category, '$prefix$indentedMessage\n');
|
||||
}
|
||||
|
||||
/// Handles a request from the client to set breakpoints.
|
||||
///
|
||||
/// This method can be called at any time (before the app is launched or while
|
||||
/// the app is running) and will include the new full set of breakpoints for
|
||||
/// the file URI in [args.source.path].
|
||||
///
|
||||
/// The VM requires breakpoints to be set per-isolate so these will be passed
|
||||
/// to [_isolateManager] that will fan them out to each isolate.
|
||||
///
|
||||
/// When new isolates are registered, it is [isolateManager]s responsibility
|
||||
/// to ensure all breakpoints are given to them (and like at startup, this
|
||||
/// must happen before they are resumed).
|
||||
@override
|
||||
Future<void> setBreakpointsRequest(
|
||||
Request request,
|
||||
SetBreakpointsArguments args,
|
||||
void Function(SetBreakpointsResponseBody) sendResponse,
|
||||
) async {
|
||||
final breakpoints = args.breakpoints ?? [];
|
||||
|
||||
final path = args.source.path;
|
||||
final name = args.source.name;
|
||||
final uri = path != null ? Uri.file(path).toString() : name!;
|
||||
|
||||
await _isolateManager.setBreakpoints(uri, breakpoints);
|
||||
|
||||
// TODO(dantup): Handle breakpoint resolution rather than pretending all
|
||||
// breakpoints are verified immediately.
|
||||
sendResponse(SetBreakpointsResponseBody(
|
||||
breakpoints: breakpoints.map((e) => Breakpoint(verified: true)).toList(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Handles a request from the client for the call stack for [args.threadId].
|
||||
///
|
||||
/// This is usually called after we sent a [StoppedEvent] to the client
|
||||
/// notifying it that execution of an isolate has paused and it wants to
|
||||
/// populate the call stack view.
|
||||
///
|
||||
/// Clients may fetch the frames in batches and VS Code in particular will
|
||||
/// send two requests initially - one for the top frame only, and then one for
|
||||
/// the next 19 frames. For better performance, the first request is satisfied
|
||||
/// entirely from the threads pauseEvent.topFrame so we do not need to
|
||||
/// round-trip to the VM Service.
|
||||
@override
|
||||
Future<void> stackTraceRequest(
|
||||
Request request,
|
||||
StackTraceArguments args,
|
||||
void Function(StackTraceResponseBody) sendResponse,
|
||||
) async {
|
||||
// We prefer to provide frames in small batches. Rather than tell the client
|
||||
// how many frames there really are (which can be expensive to compute -
|
||||
// especially for web) we just add 20 on to the last frame we actually send,
|
||||
// as described in the spec:
|
||||
//
|
||||
// "Returning monotonically increasing totalFrames values for subsequent
|
||||
// requests can be used to enforce paging in the client."
|
||||
const stackFrameBatchSize = 20;
|
||||
|
||||
final threadId = args.threadId;
|
||||
final thread = _isolateManager.getThread(threadId);
|
||||
final topFrame = thread?.pauseEvent?.topFrame;
|
||||
final startFrame = args.startFrame ?? 0;
|
||||
final numFrames = args.levels ?? 0;
|
||||
var totalFrames = 1;
|
||||
|
||||
if (thread == null) {
|
||||
throw DebugAdapterException('No thread with threadId $threadId');
|
||||
}
|
||||
|
||||
if (!thread.paused) {
|
||||
throw DebugAdapterException('Thread $threadId is not paused');
|
||||
}
|
||||
|
||||
final stackFrames = <StackFrame>[];
|
||||
// If the request is only for the top frame, we may be able to satisfy it
|
||||
// from the threads `pauseEvent.topFrame`.
|
||||
if (startFrame == 0 && numFrames == 1 && topFrame != null) {
|
||||
totalFrames = 1 + stackFrameBatchSize;
|
||||
final dapTopFrame = await _converter.convertVmToDapStackFrame(
|
||||
thread,
|
||||
topFrame,
|
||||
isTopFrame: true,
|
||||
);
|
||||
stackFrames.add(dapTopFrame);
|
||||
} else {
|
||||
// Otherwise, send the request on to the VM.
|
||||
// The VM doesn't support fetching an arbitrary slice of frames, only a
|
||||
// maximum limit, so if the client asks for frames 20-30 we must send a
|
||||
// request for the first 30 and trim them ourselves.
|
||||
final limit = startFrame + numFrames;
|
||||
final stack = await vmService?.getStack(thread.isolate.id!, limit: limit);
|
||||
final frames = stack?.frames;
|
||||
|
||||
if (stack != null && frames != null) {
|
||||
// When the call stack is truncated, we always add [stackFrameBatchSize]
|
||||
// to the count, indicating to the client there are more frames and
|
||||
// the size of the batch they should request when "loading more".
|
||||
//
|
||||
// It's ok to send a number that runs past the actual end of the call
|
||||
// stack and the client should handle this gracefully:
|
||||
//
|
||||
// "a client should be prepared to receive less frames than requested,
|
||||
// which is an indication that the end of the stack has been reached."
|
||||
totalFrames = (stack.truncated ?? false)
|
||||
? frames.length + stackFrameBatchSize
|
||||
: frames.length;
|
||||
|
||||
Future<StackFrame> convert(int index, vm.Frame frame) async {
|
||||
return _converter.convertVmToDapStackFrame(
|
||||
thread,
|
||||
frame,
|
||||
isTopFrame: startFrame == 0 && index == 0,
|
||||
);
|
||||
}
|
||||
|
||||
final frameSubset = frames.sublist(startFrame);
|
||||
stackFrames.addAll(await Future.wait(frameSubset.mapIndexed(convert)));
|
||||
}
|
||||
}
|
||||
|
||||
sendResponse(
|
||||
StackTraceResponseBody(
|
||||
stackFrames: stackFrames,
|
||||
totalFrames: totalFrames,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles the clients "step in" request for the thread in [args.threadId].
|
||||
@override
|
||||
Future<void> stepInRequest(
|
||||
Request request,
|
||||
StepInArguments args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
await _isolateManager.resumeThread(args.threadId, vm.StepOption.kInto);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Handles the clients "step out" request for the thread in [args.threadId].
|
||||
@override
|
||||
Future<void> stepOutRequest(
|
||||
Request request,
|
||||
StepOutArguments args,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
await _isolateManager.resumeThread(args.threadId, vm.StepOption.kOut);
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
/// Overridden by sub-classes to handle when the client sends a
|
||||
/// `terminateRequest` (a request for a graceful shut down).
|
||||
FutureOr<void> terminateImpl();
|
||||
Future<void> terminateImpl();
|
||||
|
||||
/// terminateRequest is called by the client when it wants us to gracefully
|
||||
/// shut down.
|
||||
|
@ -350,13 +525,13 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
///
|
||||
/// https://microsoft.github.io/debug-adapter-protocol/overview#debug-session-end
|
||||
@override
|
||||
FutureOr<void> terminateRequest(
|
||||
Future<void> terminateRequest(
|
||||
Request request,
|
||||
TerminateArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
) async {
|
||||
terminateImpl();
|
||||
sendResponse(null);
|
||||
await terminateImpl();
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
void _handleDebugEvent(vm.Event event) {
|
||||
|
@ -378,7 +553,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
/// Helper to convert to InstanceRef to a String, taking into account
|
||||
/// [vm.InstanceKind.kNull] which is the type for the unused fields of a
|
||||
/// log event.
|
||||
FutureOr<String?> asString(vm.InstanceRef? ref) {
|
||||
Future<String?> asString(vm.InstanceRef? ref) async {
|
||||
if (ref == null || ref.kind == vm.InstanceKind.kNull) {
|
||||
return null;
|
||||
}
|
||||
|
@ -407,6 +582,21 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
}
|
||||
}
|
||||
|
||||
/// Performs some setup that is common to both [launchRequest] and
|
||||
/// [attachRequest].
|
||||
Future<void> _prepareForLaunchOrAttach() async {
|
||||
// Don't start launching until configurationDone.
|
||||
if (!_configurationDoneCompleter.isCompleted) {
|
||||
logger?.call('Waiting for configurationDone request...');
|
||||
await _configurationDoneCompleter.future;
|
||||
}
|
||||
|
||||
// Notify IsolateManager if we'll be debugging so it knows whether to set
|
||||
// up breakpoints etc. when isolates are registered.
|
||||
final debug = !(args.noDebug ?? false);
|
||||
_isolateManager.setDebugEnabled(debug);
|
||||
}
|
||||
|
||||
/// A wrapper around the same name function from package:vm_service that
|
||||
/// allows logging all traffic over the VM Service.
|
||||
Future<vm.VmService> _vmServiceConnectUri(
|
||||
|
@ -455,8 +645,37 @@ class DartLaunchRequestArguments extends LaunchRequestArguments {
|
|||
final int? vmServicePort;
|
||||
final List<String>? vmAdditionalArgs;
|
||||
final bool? enableAsserts;
|
||||
|
||||
/// Whether SDK libraries should be marked as debuggable.
|
||||
///
|
||||
/// Treated as `false` if null, which means "step in" will not step into SDK
|
||||
/// libraries.
|
||||
final bool? debugSdkLibraries;
|
||||
|
||||
/// Whether external package libraries should be marked as debuggable.
|
||||
///
|
||||
/// Treated as `false` if null, which means "step in" will not step into
|
||||
/// libraries in packages that are not either the local package or a path
|
||||
/// dependency. This allows users to debug "just their code" and treat Pub
|
||||
/// packages as block boxes.
|
||||
final bool? debugExternalPackageLibraries;
|
||||
|
||||
/// Whether to evaluate getters in debug views like hovers and the variables
|
||||
/// list.
|
||||
///
|
||||
/// Invoking getters has a performance cost and may introduce side-effects,
|
||||
/// although users may expected this functionality. null is treated like false
|
||||
/// although clients may have their own defaults (for example Dart-Code sends
|
||||
/// true by default at the time of writing).
|
||||
final bool? evaluateGettersInDebugViews;
|
||||
|
||||
/// Whether to call toString() on objects in debug views like hovers and the
|
||||
/// variables list.
|
||||
///
|
||||
/// Invoking toString() has a performance cost and may introduce side-effects,
|
||||
/// although users may expected this functionality. null is treated like false
|
||||
/// although clients may have their own defaults (for example Dart-Code sends
|
||||
/// true by default at the time of writing).
|
||||
final bool? evaluateToStringInDebugViews;
|
||||
|
||||
/// Whether to send debug logging to clients in a custom `dart.log` event. This
|
||||
|
@ -476,6 +695,7 @@ class DartLaunchRequestArguments extends LaunchRequestArguments {
|
|||
this.vmAdditionalArgs,
|
||||
this.enableAsserts,
|
||||
this.debugSdkLibraries,
|
||||
this.debugExternalPackageLibraries,
|
||||
this.evaluateGettersInDebugViews,
|
||||
this.evaluateToStringInDebugViews,
|
||||
this.sendLogsToClient,
|
||||
|
@ -490,6 +710,8 @@ class DartLaunchRequestArguments extends LaunchRequestArguments {
|
|||
vmAdditionalArgs = (obj['vmAdditionalArgs'] as List?)?.cast<String>(),
|
||||
enableAsserts = obj['enableAsserts'] as bool?,
|
||||
debugSdkLibraries = obj['debugSdkLibraries'] as bool?,
|
||||
debugExternalPackageLibraries =
|
||||
obj['debugExternalPackageLibraries'] as bool?,
|
||||
evaluateGettersInDebugViews =
|
||||
obj['evaluateGettersInDebugViews'] as bool?,
|
||||
evaluateToStringInDebugViews =
|
||||
|
@ -508,6 +730,8 @@ class DartLaunchRequestArguments extends LaunchRequestArguments {
|
|||
if (vmAdditionalArgs != null) 'vmAdditionalArgs': vmAdditionalArgs,
|
||||
if (enableAsserts != null) 'enableAsserts': enableAsserts,
|
||||
if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries,
|
||||
if (debugExternalPackageLibraries != null)
|
||||
'debugExternalPackageLibraries': debugExternalPackageLibraries,
|
||||
if (evaluateGettersInDebugViews != null)
|
||||
'evaluateGettersInDebugViews': evaluateGettersInDebugViews,
|
||||
if (evaluateToStringInDebugViews != null)
|
||||
|
|
|
@ -44,7 +44,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
DartCliDebugAdapter(ByteStreamServerChannel channel, [Logger? logger])
|
||||
: super(channel, logger);
|
||||
|
||||
FutureOr<void> debuggerConnected(vm.VM vmInfo) {
|
||||
Future<void> debuggerConnected(vm.VM vmInfo) async {
|
||||
if (!isAttach) {
|
||||
// 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
|
||||
|
@ -60,7 +60,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
|
||||
/// Called by [disconnectRequest] to request that we forcefully shut down the
|
||||
/// app being run (or in the case of an attach, disconnect).
|
||||
FutureOr<void> disconnectImpl() {
|
||||
Future<void> disconnectImpl() async {
|
||||
// TODO(dantup): In Dart-Code DAP, we first try again with sigint and wait
|
||||
// for a few seconds before sending sigkill.
|
||||
pidsToTerminate.forEach(
|
||||
|
@ -137,7 +137,7 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
|
|||
|
||||
/// Called by [terminateRequest] to request that we gracefully shut down the
|
||||
/// app being run (or in the case of an attach, disconnect).
|
||||
FutureOr<void> terminateImpl() async {
|
||||
Future<void> terminateImpl() async {
|
||||
pidsToTerminate.forEach(
|
||||
(pid) => Process.killPid(pid, ProcessSignal.sigint),
|
||||
);
|
||||
|
|
|
@ -11,6 +11,12 @@ import 'protocol_stream.dart';
|
|||
|
||||
typedef _FromJsonHandler<T> = T Function(Map<String, Object?>);
|
||||
typedef _NullableFromJsonHandler<T> = T? Function(Map<String, Object?>?);
|
||||
typedef _RequestHandler<TArg, TResp> = Future<void> Function(
|
||||
Request, TArg, void Function(TResp));
|
||||
typedef _VoidArgRequestHandler<TArg> = Future<void> Function(
|
||||
Request, TArg, void Function(void));
|
||||
typedef _VoidNoArgRequestHandler<TArg> = Future<void> Function(
|
||||
Request, TArg, void Function());
|
||||
|
||||
/// A base class for debug adapters.
|
||||
///
|
||||
|
@ -34,22 +40,28 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
/// Dart CLI, Dart tests, Flutter, Flutter tests).
|
||||
TLaunchArgs Function(Map<String, Object?>) get parseLaunchArgs;
|
||||
|
||||
FutureOr<void> attachRequest(
|
||||
Future<void> attachRequest(
|
||||
Request request,
|
||||
TLaunchArgs args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
FutureOr<void> configurationDoneRequest(
|
||||
Future<void> configurationDoneRequest(
|
||||
Request request,
|
||||
ConfigurationDoneArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
FutureOr<void> disconnectRequest(
|
||||
Future<void> continueRequest(
|
||||
Request request,
|
||||
ContinueArguments args,
|
||||
void Function(ContinueResponseBody) sendResponse,
|
||||
);
|
||||
|
||||
Future<void> disconnectRequest(
|
||||
Request request,
|
||||
DisconnectArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
/// Calls [handler] for an incoming request, using [fromJson] to parse its
|
||||
|
@ -63,9 +75,9 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
/// require a body.
|
||||
///
|
||||
/// If [handler] throws, its exception will be sent as an error response.
|
||||
FutureOr<void> handle<TArg, TResp>(
|
||||
Future<void> handle<TArg, TResp>(
|
||||
Request request,
|
||||
FutureOr<void> Function(Request, TArg, void Function(TResp)) handler,
|
||||
_RequestHandler<TArg, TResp> handler,
|
||||
TArg Function(Map<String, Object?>) fromJson,
|
||||
) async {
|
||||
final args = request.arguments != null
|
||||
|
@ -109,14 +121,23 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
}
|
||||
}
|
||||
|
||||
FutureOr<void> initializeRequest(
|
||||
Future<void> initializeRequest(
|
||||
Request request,
|
||||
InitializeRequestArguments args,
|
||||
void Function(Capabilities) sendResponse,
|
||||
);
|
||||
|
||||
FutureOr<void> launchRequest(
|
||||
Request request, TLaunchArgs args, void Function(void) sendResponse);
|
||||
Future<void> launchRequest(
|
||||
Request request,
|
||||
TLaunchArgs args,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
Future<void> nextRequest(
|
||||
Request request,
|
||||
NextArguments args,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
/// Sends an event, lookup up the event type based on the runtimeType of
|
||||
/// [body].
|
||||
|
@ -140,10 +161,33 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
_channel.sendRequest(request);
|
||||
}
|
||||
|
||||
FutureOr<void> terminateRequest(
|
||||
Future<void> setBreakpointsRequest(
|
||||
Request request,
|
||||
SetBreakpointsArguments args,
|
||||
void Function(SetBreakpointsResponseBody) sendResponse);
|
||||
|
||||
Future<void> stackTraceRequest(
|
||||
Request request,
|
||||
StackTraceArguments args,
|
||||
void Function(StackTraceResponseBody) sendResponse,
|
||||
);
|
||||
|
||||
Future<void> stepInRequest(
|
||||
Request request,
|
||||
StepInArguments args,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
Future<void> stepOutRequest(
|
||||
Request request,
|
||||
StepOutArguments args,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
Future<void> terminateRequest(
|
||||
Request request,
|
||||
TerminateArguments? args,
|
||||
void Function(void) sendResponse,
|
||||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
/// Wraps a fromJson handler for requests that allow null arguments.
|
||||
|
@ -169,27 +213,44 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
if (request.command == 'initialize') {
|
||||
handle(request, initializeRequest, InitializeRequestArguments.fromJson);
|
||||
} else if (request.command == 'launch') {
|
||||
handle(request, launchRequest, parseLaunchArgs);
|
||||
handle(request, _withVoidResponse(launchRequest), parseLaunchArgs);
|
||||
} else if (request.command == 'attach') {
|
||||
handle(request, attachRequest, parseLaunchArgs);
|
||||
handle(request, _withVoidResponse(attachRequest), parseLaunchArgs);
|
||||
} else if (request.command == 'terminate') {
|
||||
handle(
|
||||
request,
|
||||
terminateRequest,
|
||||
_withVoidResponse(terminateRequest),
|
||||
_allowNullArg(TerminateArguments.fromJson),
|
||||
);
|
||||
} else if (request.command == 'disconnect') {
|
||||
handle(
|
||||
request,
|
||||
disconnectRequest,
|
||||
_withVoidResponse(disconnectRequest),
|
||||
_allowNullArg(DisconnectArguments.fromJson),
|
||||
);
|
||||
} else if (request.command == 'configurationDone') {
|
||||
handle(
|
||||
request,
|
||||
configurationDoneRequest,
|
||||
_withVoidResponse(configurationDoneRequest),
|
||||
_allowNullArg(ConfigurationDoneArguments.fromJson),
|
||||
);
|
||||
} else if (request.command == 'setBreakpoints') {
|
||||
handle(request, setBreakpointsRequest, SetBreakpointsArguments.fromJson);
|
||||
} else if (request.command == 'continue') {
|
||||
handle(request, continueRequest, ContinueArguments.fromJson);
|
||||
} else if (request.command == 'next') {
|
||||
handle(request, _withVoidResponse(nextRequest), NextArguments.fromJson);
|
||||
} else if (request.command == 'stepIn') {
|
||||
handle(
|
||||
request,
|
||||
_withVoidResponse(stepInRequest),
|
||||
StepInArguments.fromJson,
|
||||
);
|
||||
} else if (request.command == 'stepOut') {
|
||||
handle(request, _withVoidResponse(stepOutRequest),
|
||||
StepOutArguments.fromJson);
|
||||
} else if (request.command == 'stackTrace') {
|
||||
handle(request, stackTraceRequest, StackTraceArguments.fromJson);
|
||||
} else {
|
||||
final response = Response(
|
||||
success: false,
|
||||
|
@ -206,4 +267,20 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
// TODO(dantup): Implement this when the server sends requests to the client
|
||||
// (for example runInTerminalRequest).
|
||||
}
|
||||
|
||||
/// Helper that converts a handler with no response value to one that has
|
||||
/// passes an unused arg so that `Function()` can be passed to a function
|
||||
/// accepting `Function<T>(T x)` where `T` happens to be `void`.
|
||||
///
|
||||
/// This allows handlers to simple call sendResponse() where they have no
|
||||
/// return value but need to send a valid response.
|
||||
_VoidArgRequestHandler<TArg> _withVoidResponse<TArg>(
|
||||
_VoidNoArgRequestHandler<TArg> handler,
|
||||
) {
|
||||
return (request, arg, sendResponse) => handler(
|
||||
request,
|
||||
arg,
|
||||
() => sendResponse(null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
14
pkg/dds/lib/src/dap/exceptions.dart
Normal file
14
pkg/dds/lib/src/dap/exceptions.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// Exception thrown by a debug adapter when a request is not valid, either
|
||||
/// because the inputs are not correct or the adapter is not in the correct
|
||||
/// state.
|
||||
class DebugAdapterException implements Exception {
|
||||
final String message;
|
||||
|
||||
DebugAdapterException(this.message);
|
||||
|
||||
String toString() => 'DebugAdapterException: $message';
|
||||
}
|
|
@ -7,6 +7,7 @@ import 'dart:async';
|
|||
import 'package:vm_service/vm_service.dart' as vm;
|
||||
|
||||
import 'adapters/dart.dart';
|
||||
import 'exceptions.dart';
|
||||
import 'protocol_generated.dart';
|
||||
|
||||
/// Manages state of Isolates (called Threads by the DAP protocol).
|
||||
|
@ -22,6 +23,40 @@ class IsolateManager {
|
|||
final Map<int, ThreadInfo> _threadsByThreadId = {};
|
||||
int _nextThreadNumber = 1;
|
||||
|
||||
/// Whether debugging is enabled.
|
||||
///
|
||||
/// This must be set before any isolates are spawned and controls whether
|
||||
/// breakpoints or exception pause modes are sent to the VM.
|
||||
///
|
||||
/// This is used to support debug sessions that have VM Service connections
|
||||
/// but were run with noDebug: true (for example we may need a VM Service
|
||||
/// connection for a noDebug flutter app in order to support hot reload).
|
||||
bool _debug = false;
|
||||
|
||||
/// Tracks breakpoints last provided by the client so they can be sent to new
|
||||
/// isolates that appear after initial breakpoints were sent.
|
||||
final Map<String, List<SourceBreakpoint>> _clientBreakpointsByUri = {};
|
||||
|
||||
/// Tracks breakpoints created in the VM so they can be removed when the
|
||||
/// editor sends new breakpoints (currently the editor just sends a new list
|
||||
/// and not requests to add/remove).
|
||||
final Map<String, Map<String, List<vm.Breakpoint>>>
|
||||
_vmBreakpointsByIsolateIdAndUri = {};
|
||||
|
||||
/// An incrementing number used as the reference for [_storedData].
|
||||
var _nextStoredDataId = 1;
|
||||
|
||||
/// A store of data indexed by a number that is used for round tripping
|
||||
/// references to the client (which only accepts ints).
|
||||
///
|
||||
/// For example, when we send a stack frame back to the client we provide only
|
||||
/// a "sourceReference" integer and the client may later ask us for the source
|
||||
/// using that number (via sourceRequest).
|
||||
///
|
||||
/// Stored data is thread-scoped but the client will not provide the thread
|
||||
/// when asking for data so it's all stored together here.
|
||||
final _storedData = <int, _StoredData>{};
|
||||
|
||||
IsolateManager(this._adapter);
|
||||
|
||||
/// A list of all current active isolates.
|
||||
|
@ -37,8 +72,10 @@ class IsolateManager {
|
|||
return res as T;
|
||||
}
|
||||
|
||||
ThreadInfo? getThread(int threadId) => _threadsByThreadId[threadId];
|
||||
|
||||
/// Handles Isolate and Debug events
|
||||
FutureOr<void> handleEvent(vm.Event event) async {
|
||||
Future<void> handleEvent(vm.Event event) async {
|
||||
final isolateId = event.isolate?.id;
|
||||
if (isolateId == null) {
|
||||
return;
|
||||
|
@ -63,11 +100,11 @@ class IsolateManager {
|
|||
await _isolateRegistrations[isolateId]?.future;
|
||||
|
||||
if (eventKind == vm.EventKind.kIsolateExit) {
|
||||
await _handleExit(event);
|
||||
_handleExit(event);
|
||||
} else if (eventKind?.startsWith('Pause') ?? false) {
|
||||
await _handlePause(event);
|
||||
} else if (eventKind == vm.EventKind.kResume) {
|
||||
await _handleResumed(event);
|
||||
_handleResumed(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +114,7 @@ class IsolateManager {
|
|||
/// New isolates will be configured with the correct pause-exception behaviour,
|
||||
/// libraries will be marked as debuggable if appropriate, and breakpoints
|
||||
/// sent.
|
||||
FutureOr<void> registerIsolate(
|
||||
Future<void> registerIsolate(
|
||||
vm.IsolateRef isolate,
|
||||
String eventKind,
|
||||
) async {
|
||||
|
@ -90,7 +127,7 @@ class IsolateManager {
|
|||
isolate.id!,
|
||||
() {
|
||||
// The first time we see an isolate, start tracking it.
|
||||
final info = ThreadInfo(_nextThreadNumber++, isolate);
|
||||
final info = ThreadInfo(this, _nextThreadNumber++, isolate);
|
||||
_threadsByThreadId[info.threadId] = info;
|
||||
// And notify the client about it.
|
||||
_adapter.sendEvent(
|
||||
|
@ -109,7 +146,7 @@ class IsolateManager {
|
|||
}
|
||||
}
|
||||
|
||||
FutureOr<void> resumeIsolate(vm.IsolateRef isolateRef,
|
||||
Future<void> resumeIsolate(vm.IsolateRef isolateRef,
|
||||
[String? resumeType]) async {
|
||||
final isolateId = isolateRef.id;
|
||||
if (isolateId == null) {
|
||||
|
@ -135,7 +172,7 @@ class IsolateManager {
|
|||
Future<void> resumeThread(int threadId, [String? resumeType]) async {
|
||||
final thread = _threadsByThreadId[threadId];
|
||||
if (thread == null) {
|
||||
throw 'Thread $threadId was not found';
|
||||
throw DebugAdapterException('Thread $threadId was not found');
|
||||
}
|
||||
|
||||
// Check this thread hasn't already been resumed by another handler in the
|
||||
|
@ -159,16 +196,58 @@ class IsolateManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Records breakpoints for [uri].
|
||||
///
|
||||
/// [breakpoints] represents the new set and entirely replaces anything given
|
||||
/// before.
|
||||
Future<void> setBreakpoints(
|
||||
String uri,
|
||||
List<SourceBreakpoint> breakpoints,
|
||||
) async {
|
||||
// Track the breakpoints to get sent to any new isolates that start.
|
||||
_clientBreakpointsByUri[uri] = breakpoints;
|
||||
|
||||
// Send the breakpoints to all existing threads.
|
||||
await Future.wait(_threadsByThreadId.values
|
||||
.map((isolate) => _sendBreakpoints(isolate.isolate, uri: uri)));
|
||||
}
|
||||
|
||||
/// Sets whether debugging is enabled for this session.
|
||||
///
|
||||
/// If not, requests to send breakpoints or exception pause mode will be
|
||||
/// dropped. Other functionality (handling pause events, resuming, etc.) will
|
||||
/// all still function.
|
||||
///
|
||||
/// This is used to support debug sessions that have VM Service connections
|
||||
/// but were run with noDebug: true (for example we may need a VM Service
|
||||
/// connection for a noDebug flutter app in order to support hot reload).
|
||||
void setDebugEnabled(bool debug) {
|
||||
_debug = debug;
|
||||
}
|
||||
|
||||
/// Stores some basic data indexed by an integer for use in "reference" fields
|
||||
/// that are round-tripped to the client.
|
||||
int storeData(ThreadInfo thread, Object data) {
|
||||
final id = _nextStoredDataId++;
|
||||
_storedData[id] = _StoredData(thread, data);
|
||||
return id;
|
||||
}
|
||||
|
||||
ThreadInfo? threadForIsolate(vm.IsolateRef? isolate) =>
|
||||
isolate?.id != null ? _threadsByIsolateId[isolate!.id!] : null;
|
||||
|
||||
/// Configures a new isolate, setting it's exception-pause mode, which
|
||||
/// libraries are debuggable, and sending all breakpoints.
|
||||
FutureOr<void> _configureIsolate(vm.IsolateRef isolate) async {
|
||||
// TODO(dantup): set library debuggable, exception pause mode, breakpoints
|
||||
Future<void> _configureIsolate(vm.IsolateRef isolate) async {
|
||||
await Future.wait([
|
||||
_sendLibraryDebuggables(isolate),
|
||||
// TODO(dantup): Implement this...
|
||||
// _sendExceptionPauseMode(isolate),
|
||||
_sendBreakpoints(isolate),
|
||||
], eagerError: true);
|
||||
}
|
||||
|
||||
FutureOr<void> _handleExit(vm.Event event) {
|
||||
void _handleExit(vm.Event event) {
|
||||
final isolate = event.isolate!;
|
||||
final isolateId = isolate.id!;
|
||||
final thread = _threadsByIsolateId[isolateId];
|
||||
|
@ -195,7 +274,7 @@ class IsolateManager {
|
|||
///
|
||||
/// For all other pause types, the isolate will remain paused and a
|
||||
/// corresponding "Stopped" event sent to the editor.
|
||||
FutureOr<void> _handlePause(vm.Event event) async {
|
||||
Future<void> _handlePause(vm.Event event) async {
|
||||
final eventKind = event.kind;
|
||||
final isolate = event.isolate!;
|
||||
final thread = _threadsByIsolateId[isolate.id!];
|
||||
|
@ -211,7 +290,7 @@ class IsolateManager {
|
|||
// For PausePostRequest we need to re-send all breakpoints; this happens
|
||||
// after a hot restart.
|
||||
if (eventKind == vm.EventKind.kPausePostRequest) {
|
||||
_configureIsolate(isolate);
|
||||
await _configureIsolate(isolate);
|
||||
await resumeThread(thread.threadId);
|
||||
} else if (eventKind == vm.EventKind.kPauseStart) {
|
||||
await resumeThread(thread.threadId);
|
||||
|
@ -238,7 +317,7 @@ class IsolateManager {
|
|||
}
|
||||
|
||||
/// Handles a resume event from the VM, updating our local state.
|
||||
FutureOr<void> _handleResumed(vm.Event event) {
|
||||
void _handleResumed(vm.Event event) {
|
||||
final isolate = event.isolate!;
|
||||
final thread = _threadsByIsolateId[isolate.id!];
|
||||
if (thread != null) {
|
||||
|
@ -247,10 +326,103 @@ class IsolateManager {
|
|||
thread.exceptionReference = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isExternalPackageLibrary(vm.LibraryRef library) =>
|
||||
// TODO(dantup): This needs to check if it's _external_, eg.
|
||||
//
|
||||
// - is from the flutter SDK (flutter, flutter_test, ...)
|
||||
// - is from pub/pubcache
|
||||
//
|
||||
// This is intended to match the users idea of "my code". For example
|
||||
// they may wish to debug the current app being run, as well as any other
|
||||
// projects that are references with path: dependencies (which are likely
|
||||
// their own supporting projects).
|
||||
false /*library.uri?.startsWith('package:') ?? false*/;
|
||||
|
||||
bool _isSdkLibrary(vm.LibraryRef library) =>
|
||||
library.uri?.startsWith('dart:') ?? false;
|
||||
|
||||
/// Checks whether a library should be considered debuggable.
|
||||
///
|
||||
/// This usesthe settings from the launch arguments (debugSdkLibraries
|
||||
/// and debugExternalPackageLibraries) against the type of library given.
|
||||
bool _libaryIsDebuggable(vm.LibraryRef library) {
|
||||
if (_isSdkLibrary(library)) {
|
||||
return _adapter.args.debugSdkLibraries ?? false;
|
||||
} else if (_isExternalPackageLibrary(library)) {
|
||||
return _adapter.args.debugExternalPackageLibraries ?? false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets breakpoints for an individual isolate.
|
||||
///
|
||||
/// If [uri] is provided, only breakpoints for that URI will be sent (used
|
||||
/// when breakpoints are modified for a single file in the editor). Otherwise
|
||||
/// breakpoints for all previously set URIs will be sent (used for
|
||||
/// newly-created isolates).
|
||||
Future<void> _sendBreakpoints(vm.IsolateRef isolate, {String? uri}) async {
|
||||
final service = _adapter.vmService;
|
||||
if (!_debug || service == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isolateId = isolate.id!;
|
||||
|
||||
// If we were passed a single URI, we should send breakpoints only for that
|
||||
// (this means the request came from the client), otherwise we should send
|
||||
// all of them (because this is a new/restarting isolate).
|
||||
final uris = uri != null ? [uri] : _clientBreakpointsByUri.keys;
|
||||
|
||||
for (final uri in uris) {
|
||||
// Clear existing breakpoints.
|
||||
final existingBreakpointsForIsolate =
|
||||
_vmBreakpointsByIsolateIdAndUri.putIfAbsent(isolateId, () => {});
|
||||
final existingBreakpointsForIsolateAndUri =
|
||||
existingBreakpointsForIsolate.putIfAbsent(uri, () => []);
|
||||
await Future.forEach<vm.Breakpoint>(existingBreakpointsForIsolateAndUri,
|
||||
(bp) => service.removeBreakpoint(isolateId, bp.id!));
|
||||
|
||||
// Set new breakpoints.
|
||||
final newBreakpoints = _clientBreakpointsByUri[uri] ?? const [];
|
||||
await Future.forEach<SourceBreakpoint>(newBreakpoints, (bp) async {
|
||||
final vmBp = await service.addBreakpointWithScriptUri(
|
||||
isolateId, uri, bp.line,
|
||||
column: bp.column);
|
||||
existingBreakpointsForIsolateAndUri.add(vmBp);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls setLibraryDebuggable for all libraries in the given isolate based
|
||||
/// on the debug settings.
|
||||
Future<void> _sendLibraryDebuggables(vm.IsolateRef isolateRef) async {
|
||||
final service = _adapter.vmService;
|
||||
if (!_debug || service == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isolateId = isolateRef.id;
|
||||
if (isolateId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isolate = await service.getIsolate(isolateId);
|
||||
final libraries = isolate.libraries;
|
||||
if (libraries == null) {
|
||||
return;
|
||||
}
|
||||
await Future.wait(libraries.map((library) async {
|
||||
final isDebuggable = _libaryIsDebuggable(library);
|
||||
await service.setLibraryDebuggable(isolateId, library.id!, isDebuggable);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds state for a single Isolate/Thread.
|
||||
class ThreadInfo {
|
||||
final IsolateManager _manager;
|
||||
final vm.IsolateRef isolate;
|
||||
final int threadId;
|
||||
var runnable = false;
|
||||
|
@ -261,9 +433,37 @@ class ThreadInfo {
|
|||
// The most recent pauseEvent for this isolate.
|
||||
vm.Event? pauseEvent;
|
||||
|
||||
// A cache of requests (Futures) to fetch scripts, so that multiple requests
|
||||
// that require scripts (for example looking up locations for stack frames from
|
||||
// tokenPos) can share the same response.
|
||||
final _scripts = <String, Future<vm.Script>>{};
|
||||
|
||||
/// Whether this isolate has an in-flight resume request that has not yet
|
||||
/// been responded to.
|
||||
var hasPendingResume = false;
|
||||
|
||||
ThreadInfo(this.threadId, this.isolate);
|
||||
ThreadInfo(this._manager, this.threadId, this.isolate);
|
||||
|
||||
Future<T> getObject<T extends vm.Response>(vm.ObjRef ref) =>
|
||||
_manager.getObject<T>(isolate, ref);
|
||||
|
||||
/// Fetches a script for a given isolate.
|
||||
///
|
||||
/// Results from this method are cached so that if there are multiple
|
||||
/// concurrent calls (such as when converting multiple stack frames) they will
|
||||
/// all use the same script.
|
||||
Future<vm.Script> getScript(vm.ScriptRef script) {
|
||||
return _scripts.putIfAbsent(script.id!, () => getObject<vm.Script>(script));
|
||||
}
|
||||
|
||||
/// Stores some basic data indexed by an integer for use in "reference" fields
|
||||
/// that are round-tripped to the client.
|
||||
int storeData(Object data) => _manager.storeData(this, data);
|
||||
}
|
||||
|
||||
class _StoredData {
|
||||
final ThreadInfo thread;
|
||||
final Object data;
|
||||
|
||||
_StoredData(this.thread, this.data);
|
||||
}
|
||||
|
|
152
pkg/dds/lib/src/dap/protocol_converter.dart
Normal file
152
pkg/dds/lib/src/dap/protocol_converter.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:path/path.dart' as path;
|
||||
import 'package:vm_service/vm_service.dart' as vm;
|
||||
|
||||
import 'adapters/dart.dart';
|
||||
import 'isolate_manager.dart';
|
||||
import 'protocol_generated.dart' as dap;
|
||||
|
||||
/// A helper that handlers converting to/from DAP and VM Service types and to
|
||||
/// user-friendly display strings.
|
||||
///
|
||||
/// This class may call back to the VM Service to fetch additional information
|
||||
/// when converting classes - for example when converting a stack frame it may
|
||||
/// fetch scripts from the VM Service in order to map token positions back to
|
||||
/// line/columns as required by DAP.
|
||||
class ProtocolConverter {
|
||||
/// The parent debug adapter, used to access arguments and the VM Service for
|
||||
/// the debug session.
|
||||
final DartDebugAdapter _adapter;
|
||||
|
||||
ProtocolConverter(this._adapter);
|
||||
|
||||
/// Converts an absolute path to one relative to the cwd used to launch the
|
||||
/// application.
|
||||
///
|
||||
/// If [sourcePath] is outside of the cwd used for launching the application
|
||||
/// then the full absolute path will be returned.
|
||||
String convertToRelativePath(String sourcePath) {
|
||||
final cwd = _adapter.args.cwd;
|
||||
if (cwd == null) {
|
||||
return sourcePath;
|
||||
}
|
||||
final rel = path.relative(sourcePath, from: cwd);
|
||||
return !rel.startsWith('..') ? rel : sourcePath;
|
||||
}
|
||||
|
||||
/// Converts a VM Service stack frame to a DAP stack frame.
|
||||
Future<dap.StackFrame> convertVmToDapStackFrame(
|
||||
ThreadInfo thread,
|
||||
vm.Frame frame, {
|
||||
required bool isTopFrame,
|
||||
int? firstAsyncMarkerIndex,
|
||||
}) async {
|
||||
final frameId = thread.storeData(frame);
|
||||
|
||||
if (frame.kind == vm.FrameKind.kAsyncSuspensionMarker) {
|
||||
return dap.StackFrame(
|
||||
id: frameId,
|
||||
name: '<asynchronous gap>',
|
||||
presentationHint: 'label',
|
||||
line: 0,
|
||||
column: 0,
|
||||
);
|
||||
}
|
||||
|
||||
// The VM may supply frames with a prefix that we don't want to include in
|
||||
// the frame for the user.
|
||||
const unoptimizedPrefix = '[Unoptimized] ';
|
||||
final codeName = frame.code?.name;
|
||||
final frameName = codeName != null
|
||||
? (codeName.startsWith(unoptimizedPrefix)
|
||||
? codeName.substring(unoptimizedPrefix.length)
|
||||
: codeName)
|
||||
: '<unknown>';
|
||||
|
||||
// If there's no location, this isn't source a user can debug so use a
|
||||
// subtle hint (which the editor may use to render the frame faded).
|
||||
final location = frame.location;
|
||||
if (location == null) {
|
||||
return dap.StackFrame(
|
||||
id: frameId,
|
||||
name: frameName,
|
||||
presentationHint: 'subtle',
|
||||
line: 0,
|
||||
column: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final scriptRef = location.script;
|
||||
final tokenPos = location.tokenPos;
|
||||
final uri = scriptRef?.uri;
|
||||
final sourcePath = uri != null ? await convertVmUriToSourcePath(uri) : null;
|
||||
var canShowSource = sourcePath != null && File(sourcePath).existsSync();
|
||||
|
||||
// Download the source if from a "dart:" uri.
|
||||
int? sourceReference;
|
||||
if (uri != null &&
|
||||
(uri.startsWith('dart:') || uri.startsWith('org-dartlang-app:')) &&
|
||||
scriptRef != null) {
|
||||
sourceReference = thread.storeData(scriptRef);
|
||||
canShowSource = true;
|
||||
}
|
||||
|
||||
var line = 0, col = 0;
|
||||
if (scriptRef != null && tokenPos != null) {
|
||||
try {
|
||||
final script = await thread.getScript(scriptRef);
|
||||
line = script.getLineNumberFromTokenPos(tokenPos) ?? 0;
|
||||
col = script.getColumnNumberFromTokenPos(tokenPos) ?? 0;
|
||||
} catch (e) {
|
||||
_adapter.logger?.call('Failed to map frame location to line/col: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final source = canShowSource
|
||||
? dap.Source(
|
||||
name: sourcePath != null ? convertToRelativePath(sourcePath) : uri,
|
||||
path: sourcePath,
|
||||
sourceReference: sourceReference,
|
||||
origin: null,
|
||||
adapterData: location.script)
|
||||
: null;
|
||||
|
||||
// The VM only allows us to restart from frames that are not the top frame,
|
||||
// but since we're also showing asyncCausalFrames any indexes past the first
|
||||
// async boundary will not line up so we cap it there.
|
||||
final canRestart = !isTopFrame &&
|
||||
(firstAsyncMarkerIndex == null || frame.index! < firstAsyncMarkerIndex);
|
||||
|
||||
return dap.StackFrame(
|
||||
id: frameId,
|
||||
name: frameName,
|
||||
source: source,
|
||||
line: line,
|
||||
column: col,
|
||||
canRestart: canRestart,
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts the source path from the VM to a file path.
|
||||
///
|
||||
/// This is required so that when the user stops (or navigates via a stack
|
||||
/// frame) we open the same file on their local disk. If we downloaded the
|
||||
/// source from the VM, they would end up seeing two copies of files (and they
|
||||
/// would each have their own breakpoints) which can be confusing.
|
||||
Future<String?> convertVmUriToSourcePath(String uri) async {
|
||||
if (uri.startsWith('file://')) {
|
||||
return Uri.parse(uri).toFilePath();
|
||||
} else if (uri.startsWith('package:')) {
|
||||
// TODO(dantup): Handle mapping package: uris ?
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'exceptions.dart';
|
||||
import 'logging.dart';
|
||||
import 'protocol_generated.dart';
|
||||
import 'protocol_stream_transformers.dart';
|
||||
|
@ -113,7 +114,7 @@ class ByteStreamServerChannel {
|
|||
|
||||
void _sendParseError(String data) {
|
||||
// TODO(dantup): Review LSP implementation of this when consolidating classes.
|
||||
throw 'Message does not confirm to DAP spec: $data';
|
||||
throw DebugAdapterException('Message does not confirm to DAP spec: $data');
|
||||
}
|
||||
|
||||
/// Send [bytes] to [_output].
|
||||
|
|
|
@ -29,15 +29,16 @@ class DapServer {
|
|||
String get host => _socket.address.host;
|
||||
int get port => _socket.port;
|
||||
|
||||
FutureOr<void> stop() async {
|
||||
Future<void> stop() async {
|
||||
_channels.forEach((client) => client.close());
|
||||
await _socket.close();
|
||||
}
|
||||
|
||||
void _acceptConnection(Socket client) {
|
||||
_logger?.call('Accepted connection from ${client.remoteAddress}');
|
||||
final address = client.remoteAddress;
|
||||
_logger?.call('Accepted connection from $address');
|
||||
client.done.then((_) {
|
||||
_logger?.call('Connection from ${client.remoteAddress} closed');
|
||||
_logger?.call('Connection from $address closed');
|
||||
});
|
||||
_createAdapter(client.transform(Uint8ListTransformer()), client, _logger);
|
||||
}
|
||||
|
@ -48,7 +49,7 @@ class DapServer {
|
|||
// ultimately need to support having a factory passed in to support
|
||||
// tests and/or being used in flutter_tools.
|
||||
final channel = ByteStreamServerChannel(_input, _output, logger);
|
||||
final adapter = DartCliDebugAdapter(channel);
|
||||
final adapter = DartCliDebugAdapter(channel, logger);
|
||||
_channels.add(channel);
|
||||
_adapters.add(adapter);
|
||||
unawaited(channel.closed.then((_) {
|
||||
|
|
218
pkg/dds/test/dap/integration/debug_breakpoints_test.dart
Normal file
218
pkg/dds/test/dap/integration/debug_breakpoints_test.dart
Normal file
|
@ -0,0 +1,218 @@
|
|||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:test/test.dart';
|
||||
|
||||
import 'test_client.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
main() {
|
||||
testDap((dap) async {
|
||||
group('debug mode breakpoints', () {
|
||||
test('stops at a line breakpoint', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
await client.hitBreakpoint(testFile, breakpointLine);
|
||||
});
|
||||
|
||||
test('stops at a line breakpoint and can be resumed', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// Resume and expect termination (as the script will get to the end).
|
||||
await Future.wait([
|
||||
client.event('terminated'),
|
||||
client.continue_(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test('stops at a line breakpoint and can step over (next)', () async {
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
print('Hello!'); // STEP
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
final stepLine = lineWith(testFile, '// STEP');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// Step and expect stopping on the next line with a 'step' stop type.
|
||||
await Future.wait([
|
||||
dap.client.expectStop('step', file: testFile, line: stepLine),
|
||||
dap.client.next(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test(
|
||||
'stops at a line breakpoint and can step over (next) an async boundary',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
Future<void> main(List<String> args) async {
|
||||
await asyncPrint('Hello!'); // BREAKPOINT
|
||||
await asyncPrint('Hello!'); // STEP
|
||||
}
|
||||
|
||||
Future<void> asyncPrint(String message) async {
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
final stepLine = lineWith(testFile, '// STEP');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// The first step will move from `asyncPrint` to the `await`.
|
||||
await Future.wait([
|
||||
client.expectStop('step', file: testFile, line: breakpointLine),
|
||||
client.next(stop.threadId!),
|
||||
], eagerError: true);
|
||||
|
||||
// The next step should go over the async boundary and to stepLine (if
|
||||
// we did not correctly send kOverAsyncSuspension we would end up in
|
||||
// the asyncPrint method).
|
||||
await Future.wait([
|
||||
client.expectStop('step', file: testFile, line: stepLine),
|
||||
client.next(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test('stops at a line breakpoint and can step in', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
log('Hello!'); // BREAKPOINT
|
||||
}
|
||||
|
||||
void log(String message) { // STEP
|
||||
print(message);
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
final stepLine = lineWith(testFile, '// STEP');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// Step and expect stopping in the inner function with a 'step' stop type.
|
||||
await Future.wait([
|
||||
client.expectStop('step', file: testFile, line: stepLine),
|
||||
client.stepIn(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test('stops at a line breakpoint and can step out', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
log('Hello!');
|
||||
log('Hello!'); // STEP
|
||||
}
|
||||
|
||||
void log(String message) {
|
||||
print(message); // BREAKPOINT
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
final stepLine = lineWith(testFile, '// STEP');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// Step and expect stopping in the inner function with a 'step' stop type.
|
||||
await Future.wait([
|
||||
client.expectStop('step', file: testFile, line: stepLine),
|
||||
client.stepOut(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test('does not step into SDK code with debugSdkLibraries=false',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
print('Hello!'); // STEP
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
final stepLine = lineWith(testFile, '// STEP');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
|
||||
// Step in and expect stopping on the next line (don't go into print).
|
||||
await Future.wait([
|
||||
client.expectStop('step', file: testFile, line: stepLine),
|
||||
client.stepIn(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test('steps into SDK code with debugSdkLibraries=true', () async {
|
||||
final client = dap.client;
|
||||
final testFile = dap.createTestFile(r'''
|
||||
void main(List<String> args) async {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
print('Hello!');
|
||||
}
|
||||
''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
// Hit the initial breakpoint.
|
||||
final stop = await dap.client.hitBreakpoint(
|
||||
testFile,
|
||||
breakpointLine,
|
||||
launch: () => client.launch(
|
||||
testFile.path,
|
||||
debugSdkLibraries: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Step in and expect to go into print.
|
||||
await Future.wait([
|
||||
client.expectStop('step', sourceName: 'dart:core/print.dart'),
|
||||
client.stepIn(stop.threadId!),
|
||||
], eagerError: true);
|
||||
});
|
||||
|
||||
test(
|
||||
'does not step into external package code with debugExternalPackageLibraries=false',
|
||||
() {
|
||||
// TODO(dantup): Support for debugExternalPackageLibraries
|
||||
}, skip: true);
|
||||
|
||||
test(
|
||||
'steps into external package code with debugExternalPackageLibraries=true',
|
||||
() {
|
||||
// TODO(dantup): Support for debugExternalPackageLibraries
|
||||
}, skip: true);
|
||||
|
||||
test('allows changing debug settings during session', () {
|
||||
// TODO(dantup): !
|
||||
// Dart-Code's DAP has a custom method that allows an editor to change
|
||||
// the debug settings (debugSdkLibraries/debugExternalPackageLibraries)
|
||||
// during a debug session.
|
||||
}, skip: true);
|
||||
// These tests can be slow due to starting up the external server process.
|
||||
}, timeout: Timeout.none);
|
||||
});
|
||||
}
|
|
@ -10,11 +10,15 @@ 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:dds/src/dap/protocol_stream_transformers.dart';
|
||||
import 'package:test/test.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 {
|
||||
final Socket _socket;
|
||||
final ByteStreamServerChannel _channel;
|
||||
|
@ -66,6 +70,13 @@ class DapTestClient {
|
|||
return outputEventsFuture;
|
||||
}
|
||||
|
||||
/// Sends a continue request for the given thread.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> continue_(int threadId) =>
|
||||
sendRequest(ContinueArguments(threadId: threadId));
|
||||
|
||||
Future<Response> disconnect() => sendRequest(DisconnectArguments());
|
||||
|
||||
/// Returns a Future that completes with the next [event] event.
|
||||
|
@ -103,6 +114,7 @@ class DapTestClient {
|
|||
String? cwd,
|
||||
bool? noDebug,
|
||||
bool? debugSdkLibraries,
|
||||
bool? debugExternalPackageLibraries,
|
||||
bool? evaluateGettersInDebugViews,
|
||||
bool? evaluateToStringInDebugViews,
|
||||
}) {
|
||||
|
@ -113,6 +125,7 @@ class DapTestClient {
|
|||
cwd: cwd,
|
||||
args: args,
|
||||
debugSdkLibraries: debugSdkLibraries,
|
||||
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
||||
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
||||
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
||||
// When running out of process, VM Service traffic won't be available
|
||||
|
@ -126,6 +139,13 @@ class DapTestClient {
|
|||
);
|
||||
}
|
||||
|
||||
/// Sends a next (step over) request for the given thread.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> next(int threadId) =>
|
||||
sendRequest(NextArguments(threadId: threadId));
|
||||
|
||||
/// Sends an arbitrary request to the server.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
|
@ -142,7 +162,34 @@ class DapTestClient {
|
|||
return _logIfSlow('Request "$command"', completer.future);
|
||||
}
|
||||
|
||||
FutureOr<void> stop() async {
|
||||
/// Sends a stackTrace request to the server to request the call stack for a
|
||||
/// given thread.
|
||||
///
|
||||
/// If [startFrame] and/or [numFrames] are supplied, only a slice of the
|
||||
/// frames will be returned.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> stackTrace(int threadId,
|
||||
{int? startFrame, int? numFrames}) =>
|
||||
sendRequest(StackTraceArguments(
|
||||
threadId: threadId, startFrame: startFrame, levels: numFrames));
|
||||
|
||||
/// Sends a stepIn request for the given thread.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> stepIn(int threadId) =>
|
||||
sendRequest(StepInArguments(threadId: threadId));
|
||||
|
||||
/// Sends a stepOut request for the given thread.
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> stepOut(int threadId) =>
|
||||
sendRequest(StepOutArguments(threadId: threadId));
|
||||
|
||||
Future<void> stop() async {
|
||||
_channel.close();
|
||||
await _socket.close();
|
||||
await _subscription.cancel();
|
||||
|
@ -194,7 +241,7 @@ class DapTestClient {
|
|||
|
||||
/// Creates a [DapTestClient] that connects the server listening on
|
||||
/// [host]:[port].
|
||||
static FutureOr<DapTestClient> connect(
|
||||
static Future<DapTestClient> connect(
|
||||
int port, {
|
||||
String host = 'localhost',
|
||||
bool captureVmServiceTraffic = false,
|
||||
|
@ -216,3 +263,71 @@ class _OutgoingRequest {
|
|||
|
||||
_OutgoingRequest(this.completer, this.name, this.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 {
|
||||
/// Sets a breakpoint at [line] in [file] and expects to hit it after running
|
||||
/// the script.
|
||||
///
|
||||
/// Launch options can be customised by passing a custom [launch] function that
|
||||
/// will be used instead of calling `launch(file.path)`.
|
||||
Future<StoppedEventBody> hitBreakpoint(File file, int line,
|
||||
{Future<Response> Function()? launch}) async {
|
||||
final stop = expectStop('breakpoint', file: file, line: line);
|
||||
|
||||
await Future.wait([
|
||||
initialize(),
|
||||
sendRequest(
|
||||
SetBreakpointsArguments(
|
||||
source: Source(path: file.path),
|
||||
breakpoints: [SourceBreakpoint(line: line)]),
|
||||
),
|
||||
launch?.call() ?? this.launch(file.path),
|
||||
], eagerError: true);
|
||||
|
||||
return stop;
|
||||
}
|
||||
|
||||
/// Expects a 'stopped' event for [reason].
|
||||
///
|
||||
/// If [file] or [line] are provided, they will be checked against the stop
|
||||
/// location for the top stack frame.
|
||||
Future<StoppedEventBody> expectStop(String reason,
|
||||
{File? file, int? line, String? sourceName}) async {
|
||||
final e = await event('stopped');
|
||||
final stop = StoppedEventBody.fromJson(e.body as Map<String, Object?>);
|
||||
expect(stop.reason, equals(reason));
|
||||
|
||||
final result =
|
||||
await getValidStack(stop.threadId!, startFrame: 0, numFrames: 1);
|
||||
expect(result.stackFrames, hasLength(1));
|
||||
final frame = result.stackFrames[0];
|
||||
|
||||
if (file != null) {
|
||||
expect(frame.source!.path, equals(file.path));
|
||||
}
|
||||
if (sourceName != null) {
|
||||
expect(frame.source!.name, equals(sourceName));
|
||||
}
|
||||
if (line != null) {
|
||||
expect(frame.line, equals(line));
|
||||
}
|
||||
|
||||
return stop;
|
||||
}
|
||||
|
||||
/// Fetches a stack trace and asserts it was a valid response.
|
||||
Future<StackTraceResponseBody> getValidStack(int threadId,
|
||||
{required int startFrame, required int numFrames}) async {
|
||||
final response = await stackTrace(threadId,
|
||||
startFrame: startFrame, numFrames: numFrames);
|
||||
expect(response.success, isTrue);
|
||||
expect(response.command, equals('stackTrace'));
|
||||
return StackTraceResponseBody.fromJson(
|
||||
response.body as Map<String, Object?>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:dds/src/dap/logging.dart';
|
||||
import 'package:dds/src/dap/server.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
|
@ -14,7 +15,7 @@ import 'package:pedantic/pedantic.dart';
|
|||
abstract class DapTestServer {
|
||||
String get host;
|
||||
int get port;
|
||||
FutureOr<void> stop();
|
||||
Future<void> stop();
|
||||
List<String> get errorLogs;
|
||||
}
|
||||
|
||||
|
@ -33,12 +34,12 @@ class InProcessDapTestServer extends DapTestServer {
|
|||
List<String> get errorLogs => const []; // In-proc errors just throw in-line.
|
||||
|
||||
@override
|
||||
FutureOr<void> stop() async {
|
||||
Future<void> stop() async {
|
||||
await _server.stop();
|
||||
}
|
||||
|
||||
static Future<InProcessDapTestServer> create() async {
|
||||
final DapServer server = await DapServer.create();
|
||||
static Future<InProcessDapTestServer> create({Logger? logger}) async {
|
||||
final DapServer server = await DapServer.create(logger: logger);
|
||||
return InProcessDapTestServer._(server);
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +82,7 @@ class OutOfProcessDapTestServer extends DapTestServer {
|
|||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> stop() async {
|
||||
Future<void> stop() async {
|
||||
_isShuttingDown = true;
|
||||
await _process.kill();
|
||||
await _process.exitCode;
|
||||
|
|
|
@ -26,10 +26,14 @@ void expectLines(String actual, List<String> expected) {
|
|||
expect(actual.replaceAll('\r\n', '\n'), equals(expected.join('\n')));
|
||||
}
|
||||
|
||||
/// Returns the 1-base line in [file] that contains [searchText].
|
||||
int lineWith(File file, String searchText) =>
|
||||
file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
|
||||
|
||||
/// A helper function to wrap all tests in a library with setup/teardown functions
|
||||
/// to start a shared server for all tests in the library and an individual
|
||||
/// client for each test.
|
||||
testDap(FutureOr<void> Function(DapTestSession session) tests) {
|
||||
testDap(Future<void> Function(DapTestSession session) tests) {
|
||||
final session = DapTestSession();
|
||||
|
||||
setUpAll(session.setUpAll);
|
||||
|
@ -59,17 +63,17 @@ class DapTestSession {
|
|||
return testFile;
|
||||
}
|
||||
|
||||
FutureOr<void> setUp() async {
|
||||
Future<void> setUp() async {
|
||||
client = await _startClient(server);
|
||||
}
|
||||
|
||||
FutureOr<void> setUpAll() async {
|
||||
Future<void> setUpAll() async {
|
||||
server = await _startServer();
|
||||
}
|
||||
|
||||
FutureOr<void> tearDown() => client.stop();
|
||||
Future<void> tearDown() => client.stop();
|
||||
|
||||
FutureOr<void> tearDownAll() async {
|
||||
Future<void> tearDownAll() async {
|
||||
await server.stop();
|
||||
|
||||
// Clean up any temp folders created during the test runs.
|
||||
|
@ -77,7 +81,7 @@ class DapTestSession {
|
|||
}
|
||||
|
||||
/// Creates and connects a new [DapTestClient] to [server].
|
||||
FutureOr<DapTestClient> _startClient(DapTestServer server) async {
|
||||
Future<DapTestClient> _startClient(DapTestServer server) async {
|
||||
// Since we don't get a signal from the DAP server when it's ready and we
|
||||
// just started it, add a short retry to connections.
|
||||
// Since the bots can be quite slow, it may take 6-7 seconds for the server
|
||||
|
@ -107,7 +111,7 @@ class DapTestSession {
|
|||
}
|
||||
|
||||
/// Starts a DAP server that can be shared across tests.
|
||||
FutureOr<DapTestServer> _startServer() async {
|
||||
Future<DapTestServer> _startServer() async {
|
||||
return useInProcessDap
|
||||
? await InProcessDapTestServer.create()
|
||||
: await OutOfProcessDapTestServer.create();
|
||||
|
|
Loading…
Reference in a new issue