[DAP] Set requirePermissionToResume and requireUserPermissionToResume for onPauseStart and onPauseExit so that DDS waits for DAP's permission before resuming the isolate.

Bug: https://github.com/dart-lang/sdk/issues/54843
Change-Id: I12686d6c683983b7af5a3148681410ff9c684343
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/350649
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Elliott Brooks <elliottbrooks@google.com>
This commit is contained in:
Elliott Brooks 2024-05-06 17:36:23 +00:00 committed by Commit Queue
parent 1afb12feb8
commit 93556aeee3
13 changed files with 404 additions and 99 deletions

View file

@ -2,6 +2,7 @@
- [DAP]: Fixed an issue where breakpoint `changed` events might contain incorrect location information when new isolates are created, causing breakpoints to appear to move in the editor.
- [DAP]: For consistency with other values, automatic `toString()` invocations for debugger views no longer expand long strings and instead show truncated values. Full values continue to be returned for evaluation (`context=="repl"`) and when copying to the clipboard (`context=="clipboard"`).
- [DAP]: Improved handling of sentinel responses when building `variables` responses. This prevents entire map/list requests from failing when only some values inside are sentinels.
- [DAP] Set `requirePermissionToResume` and `requireUserPermissionToResume` for `onPauseStart` and `onPauseExit` so that DDS waits for DAP's permission before resuming the isolate.
# 4.2.0
- [DAP] All `OutputEvent`s are now scanned for stack frames to attach `source` metadata to. The [parseStackFrames] parameter for `sendOutput` is ignored and deprecated.

View file

@ -8,6 +8,7 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dap/dap.dart';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:json_rpc_2/error_code.dart' as json_rpc_errors;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
@ -434,12 +435,19 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
/// vs 'detached').
bool isDetaching = false;
/// Whether isolates that pause in the PauseExit state should be automatically
/// resumed after any in-process log events have completed.
/// Whether this adapter set the --pause-isolates-on-start flag, specifying
/// that isolates should pause on starting.
///
/// Normally this will be true, but it may be set to false if the user
/// also manually passes pause-isolates-on-exit.
bool resumeIsolatesAfterPauseExit = true;
/// also manually passed the --pause-isolates-on-start flag.
bool pauseIsolatesOnStartSetByDap = true;
/// Whether this adapter set the --pause-isolates-on-exit flag, specifying
/// that isolates should pause on exiting.
///
/// Normally this will be true, but it may be set to false if the user
/// also manually passed the --pause-isolates-on-exit flag.
bool pauseIsolatesOnExitSetByDap = true;
/// A [Future] that completes when the last queued OutputEvent has been sent.
///
@ -559,10 +567,6 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
isAttach = true;
_subscribeToOutputStreams = true;
// When attaching to a process, suppress auto-resuming isolates until the
// first time the user resumes anything.
isolateManager.autoResumeStartingIsolates = false;
// Common setup.
await _prepareForLaunchOrAttach(null);
@ -702,6 +706,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
// Let the subclass do any existing setup once we have a connection.
await debuggerConnected(vmInfo);
await _configureIsolateSettings(vmService);
await _withErrorHandling(
() => _configureExistingIsolates(vmService, vmInfo),
);
@ -756,6 +761,68 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
return DapProgressReporter.start(this, id, title, message: message);
}
Future<void> _configureIsolateSettings(
vm.VmService vmService,
) async {
// If this is an attach workflow, check whether pause_isolates_on_start or
// pause_isolates_on_exit were already set, and if not set them (note: this
// is already done as part of the launch workflow):
if (isAttach) {
const pauseIsolatesOnStart = 'pause_isolates_on_start';
const pauseIsolatesOnExit = 'pause_isolates_on_exit';
final flags = (await vmService.getFlagList()).flags ?? <vm.Flag>[];
for (final flag in flags) {
final flagName = flag.name;
final isPauseIsolatesFlag =
flagName == pauseIsolatesOnStart || flagName == pauseIsolatesOnExit;
if (flagName == null || !isPauseIsolatesFlag) continue;
if (flag.valueAsString == 'true') {
if (flagName == pauseIsolatesOnStart) {
pauseIsolatesOnStartSetByDap = false;
}
if (flagName == pauseIsolatesOnExit) {
pauseIsolatesOnExitSetByDap = false;
}
} else {
_setVmFlagTo(vmService, flagName: flagName, valueAsString: 'true');
}
}
}
try {
// Make sure DDS waits for DAP to be ready to resume before forwarding
// resume requests to the VM Service:
await vmService.requirePermissionToResume(
onPauseStart: true,
onPauseExit: true,
);
// Specify whether DDS should wait for a user-initiated resume as well as a
// DAP-initiated resume:
await vmService.requireUserPermissionToResume(
onPauseStart: !pauseIsolatesOnStartSetByDap,
onPauseExit: !pauseIsolatesOnExitSetByDap,
);
} catch (e) {
// If DDS is not enabled, calling these DDS service extensions will fail.
// Therefore catch and log any errors.
logger?.call('Failure configuring isolate settings: $e');
}
}
Future<void> _setVmFlagTo(
vm.VmService vmService, {
required String flagName,
required String valueAsString,
}) async {
try {
await vmService.setFlag(flagName, valueAsString);
} catch (e) {
logger?.call('Failed to to set VM flag $flagName to $valueAsString: $e');
}
}
/// Process any existing isolates that may have been created before the
/// streams above were set up.
Future<void> _configureExistingIsolates(
@ -775,8 +842,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
final pauseEventKind = isolate.runnable ?? false
? vm.EventKind.kIsolateRunnable
: vm.EventKind.kIsolateStart;
final thread =
await isolateManager.registerIsolate(isolate, pauseEventKind);
await isolateManager.registerIsolate(isolate, pauseEventKind);
// If the Isolate already has a Pause event we can give it to the
// IsolateManager to handle (if it's PausePostStart it will re-configure
@ -788,13 +854,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
isolate.pauseEvent!,
);
} else if (isolate.runnable == true) {
// If requested, automatically resume. Otherwise send a Stopped event to
// inform the client UI the thread is paused.
if (isolateManager.autoResumeStartingIsolates) {
await isolateManager.resumeIsolate(isolate);
} else {
isolateManager.sendStoppedOnEntryEvent(thread.threadId);
}
await isolateManager.readyToResumeIsolate(isolate);
}
}));
}
@ -2352,11 +2412,9 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
// We pause isolates on exit to allow requests for resolving URIs in
// stderr call stacks, so when we see an isolate pause, wait for any
// pending logs and then resume it (so it exits).
if (resumeIsolatesAfterPauseExit &&
eventKind == vm.EventKind.kPauseExit &&
isolate != null) {
if (eventKind == vm.EventKind.kPauseExit && isolate != null) {
await _waitForPendingOutputEvents();
await isolateManager.resumeIsolate(isolate);
await isolateManager.readyToResumeIsolate(isolate);
}
}

View file

@ -11,6 +11,7 @@ import 'package:dap/dap.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service/vm_service.dart' as vm;
import '../utils.dart';
import 'dart.dart';
import 'mixins.dart';
@ -68,14 +69,6 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
terminatePids(ProcessSignal.sigkill);
}
/// Checks whether [flag] is in [args], allowing for both underscore and
/// dash format.
bool _containsVmFlag(List<String> args, String flag) {
final flagUnderscores = flag.replaceAll('-', '_');
final flagDashes = flag.replaceAll('_', '-');
return args.contains(flagUnderscores) || args.contains(flagDashes);
}
@override
Future<void> launchImpl() {
throw UnsupportedError(
@ -115,7 +108,6 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
...?args.vmAdditionalArgs,
if (debug) ...[
'--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}',
'--pause_isolates_on_start',
if (!enableAuthCodes) '--disable-service-auth-codes'
],
'--disable-dart-dev',
@ -128,12 +120,20 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
final toolArgs = args.toolArgs ?? [];
if (debug) {
// If the user has explicitly set pause-isolates-on-exit we need to
// not add it ourselves, and disable auto-resuming.
if (_containsVmFlag(toolArgs, '--pause_isolates_on_exit')) {
resumeIsolatesAfterPauseExit = false;
// not add it ourselves, and specify that we didn't set it.
if (containsVmFlag(toolArgs, '--pause_isolates_on_exit')) {
pauseIsolatesOnExitSetByDap = false;
} else {
vmArgs.add('--pause_isolates_on_exit');
}
// If the user has explicitly set pause-isolates-on-start we need to
// not add it ourselves, and specify that we didn't set it.
if (containsVmFlag(toolArgs, '--pause_isolates_on_start')) {
pauseIsolatesOnStartSetByDap = false;
} else {
vmArgs.add('--pause_isolates_on_start');
}
}
// Handle customTool and deletion of any arguments for it.

View file

@ -10,6 +10,7 @@ import 'dart:math' as math;
import 'package:vm_service/vm_service.dart' as vm;
import '../stream_transformers.dart';
import '../utils.dart';
import 'dart.dart';
import 'mixins.dart';
@ -78,17 +79,32 @@ class DartTestDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments,
.then((uri) => connectDebugger(uri)));
}
final vmArgs = <String>[
var vmArgs = <String>[
...?args.vmAdditionalArgs,
if (debug) ...[
'--enable-vm-service=${args.vmServicePort ?? 0}${ipv6 ? '/::1' : ''}',
'--pause_isolates_on_start',
if (!enableAuthCodes) '--disable-service-auth-codes'
],
if (debug && vmServiceInfoFile != null) ...[
'-DSILENT_VM_SERVICE=true',
'--write-service-info=${Uri.file(vmServiceInfoFile.path)}'
],
];
final toolArgs = args.toolArgs ?? [];
if (debug) {
// If the user has explicitly set pause-isolates-on-start we need to
// not add it ourselves, and specify that we didn't set it.
if (containsVmFlag(toolArgs, '--pause_isolates_on_start') ||
containsVmFlag(vmArgs, '--pause_isolates_on_start')) {
pauseIsolatesOnStartSetByDap = false;
} else {
vmArgs.add('--pause_isolates_on_start');
}
}
vmArgs = [
...vmArgs,
// TODO(dantup): This should be changed from "dart run test:test" to
// "dart test" once the start-paused flags are working correctly.
// Currently they start paused but do not write the vm-service-info file

View file

@ -7,6 +7,7 @@ import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:dap/dap.dart';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../rpc_error_codes.dart';
@ -62,16 +63,6 @@ class IsolateManager {
/// [debugExternalPackageLibraries] in one step.
bool debugExternalPackageLibraries = true;
/// Whether to automatically resume new isolates after configuring them.
///
/// This setting is almost always `true` because isolates are paused only so
/// we can configure them (send breakpoints, pause-on-exceptions,
/// setLibraryDebuggables) without races. It is set to `false` during the
/// initial connection of an `attachRequest` to allow paused isolates to
/// remain paused. In this case, it will be automatically re-set to `true` the
/// first time the user resumes.
bool autoResumeStartingIsolates = true;
/// 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<ClientBreakpoint>> _clientBreakpointsByUri = {};
@ -277,6 +268,17 @@ class IsolateManager {
await resumeThread(thread.threadId);
}
Future<void> readyToResumeIsolate(vm.IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
return;
}
await readyToResumeThread(thread.threadId);
}
/// Resumes (or steps) an isolate using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
@ -289,6 +291,16 @@ class IsolateManager {
await _resume(threadId, resumeType: resumeType);
}
/// Resumes an isolate using its client [threadId].
///
/// CAUTION: This should only be used for a tool-initiated resume, not a user-
/// initiated resume.
///
/// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
Future<void> readyToResumeThread(int threadId) async {
await _readyToResume(threadId);
}
/// Rewinds an isolate to an earlier frame using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
@ -301,7 +313,8 @@ class IsolateManager {
);
}
/// Resumes (or steps) an isolate using its client [threadId].
/// Resumes (or steps) an isolate using its client [threadId] on behalf
/// of the user.
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
@ -316,11 +329,6 @@ class IsolateManager {
String? resumeType,
int? frameIndex,
}) async {
// The first time a user resumes a thread is our signal that the app is now
// "running" and future isolates can be auto-resumed. This only affects
// attach, as it's already `true` for launch requests.
autoResumeStartingIsolates = true;
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
@ -336,7 +344,7 @@ class IsolateManager {
// Check this thread hasn't already been resumed by another handler in the
// meantime (for example if the user performs a hot restart or something
// while we processing some previous events).
if (!thread.paused || thread.hasPendingResume) {
if (!thread.paused || thread.hasPendingUserResume) {
return;
}
@ -350,7 +358,7 @@ class IsolateManager {
// we can drop them to save memory.
thread.clearStoredData();
thread.hasPendingResume = true;
thread.hasPendingUserResume = true;
try {
await _adapter.vmService?.resume(
thread.isolate.id!,
@ -369,7 +377,57 @@ class IsolateManager {
rethrow;
}
} finally {
thread.hasPendingResume = false;
thread.hasPendingUserResume = false;
}
}
/// Resumes an isolate using its client [threadId].
///
/// CAUTION: This should only be used for a tool-initiated resume, not a user-
/// initiated resume.
///
/// See: https://pub.dev/documentation/dds_service_extensions/latest/dds_service_extensions/DdsExtension/readyToResume.html
Future<void> _readyToResume(int threadId) async {
final thread = _threadsByThreadId[threadId];
if (thread == null) {
if (isInvalidThreadId(threadId)) {
throw DebugAdapterException('Thread $threadId was not found');
} else {
// Otherwise, this thread has exited and we don't need to do anything.
// It's possible another debugger unpaused or we're shutting down and
// the VM has terminated it.
return;
}
}
// When we're resuming, all stored objects become invalid and we can drop
// to save memory.
thread.clearStoredData();
try {
thread.hasPendingDapResume = true;
await _adapter.vmService?.readyToResume(thread.isolate.id!);
} on UnimplementedError {
// Fallback to a regular resume if the DDS version doesn't support
// `readyToResume`:
return _resume(threadId);
} on vm.SentinelException {
// It's possible during these async requests that the isolate went away
// (for example a shutdown/restart) and we no longer care about
// resuming it.
} on vm.RPCError catch (e) {
if (e.code == RpcErrorCodes.kIsolateMustBePaused) {
// It's possible something else resumed the thread (such as if another
// debugger is attached), we can just continue.
} else if (e.code == RpcErrorCodes.kMethodNotFound) {
// Fallback to a regular resume if the DDS service extension isn't
// available:
return _resume(threadId);
} else {
rethrow;
}
} finally {
thread.hasPendingDapResume = false;
}
}
@ -406,8 +464,9 @@ class IsolateManager {
bool isInvalidThreadId(int threadId) => threadId >= _nextThreadNumber;
/// Sends an event informing the client that a thread is stopped at entry.
void sendStoppedOnEntryEvent(int threadId) {
_adapter.sendEvent(StoppedEventBody(reason: 'entry', threadId: threadId));
void sendStoppedOnEntryEvent(ThreadInfo thread) {
_adapter.sendEvent(
StoppedEventBody(reason: 'entry', threadId: thread.threadId));
}
/// Records breakpoints for [uri].
@ -557,11 +616,9 @@ class IsolateManager {
///
/// For [vm.EventKind.kPausePostRequest] which occurs after a restart, the
/// isolate will be re-configured (pause-exception behaviour, debuggable
/// libraries, breakpoints) and then (if [autoResumeStartingIsolates] is
/// `true`) resumed.
/// libraries, breakpoints) and we'll declare we are ready to resume.
///
/// For [vm.EventKind.kPauseStart] and [autoResumeStartingIsolates] is `true`,
/// the isolate will be resumed.
/// For [vm.EventKind.kPauseStart] we'll declare we are ready to resume.
///
/// For breakpoints with conditions that are not met and for logpoints, the
/// isolate will be automatically resumed.
@ -586,21 +643,17 @@ class IsolateManager {
// after a hot restart.
if (eventKind == vm.EventKind.kPausePostRequest) {
await _configureIsolate(thread);
if (autoResumeStartingIsolates) {
await resumeThread(thread.threadId);
}
await readyToResumeThread(thread.threadId);
} else if (eventKind == vm.EventKind.kPauseStart) {
// Don't resume from a PauseStart if this has already happened (see
// comments on [thread.hasBeenStarted]).
if (!thread.startupHandled) {
thread.startupHandled = true;
// If requested, automatically resume. Otherwise send a Stopped event to
// inform the client UI the thread is paused.
if (autoResumeStartingIsolates) {
await resumeThread(thread.threadId);
} else {
sendStoppedOnEntryEvent(thread.threadId);
}
// Send a Stopped event to inform the client UI the thread is paused and
// declare that we are ready to resume (which might result in an
// immediate resume).
sendStoppedOnEntryEvent(thread);
await readyToResumeThread(thread.threadId);
}
} else {
// PauseExit, PauseBreakpoint, PauseInterrupted, PauseException
@ -643,6 +696,8 @@ class IsolateManager {
reason = 'step';
} else if (eventKind == vm.EventKind.kPauseException) {
reason = 'exception';
} else if (eventKind == vm.EventKind.kPauseExit) {
reason = 'exit';
}
// If we stopped at an exception, capture the exception instance so we
@ -671,6 +726,11 @@ class IsolateManager {
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
if (thread != null) {
// When a thread is resumed, we must inform the client. This is not
// necessary when the user has clicked Continue because it is implied.
// However, resume events can now be triggered by other things (eg. other
// in other IDEs or DevTools) so we must notify the client.
_adapter.sendEvent(ContinuedEventBody(threadId: thread.threadId));
thread.paused = false;
thread.pauseEvent = null;
thread.exceptionReference = null;
@ -1057,6 +1117,11 @@ class ThreadInfo with FileUtils {
var runnable = false;
var atAsyncSuspension = false;
int? exceptionReference;
/// Whether this thread is currently known to be paused in the VM.
///
/// Because requests are async, this is not guaranteed to be always correct
/// but should represent the state based on the latest VM events.
var paused = false;
/// Tracks whether an isolates startup routine has been handled.
@ -1096,9 +1161,13 @@ class ThreadInfo with FileUtils {
/// Values are file-like URIs (file: or similar, such as dart-macro+file:).
final _resolvedPaths = <String, Future<Uri?>>{};
/// Whether this isolate has an in-flight resume request that has not yet
/// been responded to.
var hasPendingResume = false;
/// Whether this isolate has an in-flight user-initiated resume request that
/// has not yet been responded to.
var hasPendingUserResume = false;
/// Whether this isolate has an in-flight DAP (readyToResume) resume request
/// that has not yet been responded to.
var hasPendingDapResume = false;
ThreadInfo(this._manager, this.threadId, this.isolate);

View file

@ -98,3 +98,11 @@ stack.Frame? _parseStackFrame(String line) {
tryParseFrame((line) => stack.Frame.parseIE(line)) ??
tryParseFrame((line) => stack.Frame.parseFriendly(line));
}
/// Checks whether [flag] is in [args], allowing for both underscore and
/// dash format.
bool containsVmFlag(List<String> args, String flag) {
final flagUnderscores = flag.replaceAll('-', '_');
final flagDashes = flag.replaceAll('_', '-');
return args.contains(flagUnderscores) || args.contains(flagDashes);
}

View file

@ -298,7 +298,8 @@ main() {
final outputEvents = await client.collectTestOutput(
launch: () => client.attach(
vmServiceUri: 'ws://bogus.local/',
autoResume: false,
autoResumeOnEntry: false,
autoResumeOnExit: false,
),
);

View file

@ -25,13 +25,15 @@ main() {
testFile.path,
args,
cwd: dap.testAppDir.path,
pauseOnExit: true, // To ensure we capture all output
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceUri: vmServiceUri.toString(),
autoResume: true,
autoResumeOnEntry: true,
autoResumeOnExit: true,
cwd: dap.testAppDir.path,
),
);
@ -78,11 +80,13 @@ main() {
['one', 'two'],
cwd: dap.testAppDir.path,
vmArgs: ['--write-service-info=${Uri.file(vmServiceInfoFilePath)}'],
pauseOnExit: true, // To ensure we capture all output
);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceInfoFile: vmServiceInfoFilePath,
autoResume: true,
autoResumeOnEntry: true,
autoResumeOnExit: true,
cwd: dap.testAppDir.path,
),
);
@ -125,6 +129,11 @@ main() {
testFile.path,
[],
cwd: dap.testAppDir.path,
// Disable user-pause-on-exit because we're checking DAP resumes and
// if the VM waits for user-resume, we won't complete. We don't want to
// send an explicit user-resume because that would force resume,
// invalidating this test that we did a DAP resume.
pauseOnExit: false,
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
@ -135,7 +144,8 @@ main() {
client.start(
launch: () => client.attach(
vmServiceUri: vmServiceUri.toString(),
autoResume: false,
autoResumeOnEntry: false,
autoResumeOnExit: false,
cwd: dap.testAppDir.path,
),
),

View file

@ -646,7 +646,8 @@ void main(List<String> args) async {
client2.start(
launch: () => client2.attach(
vmServiceUri: vmServiceUri.toString(),
autoResume: false,
autoResumeOnEntry: false,
autoResumeOnExit: false,
cwd: dap.testAppDir.path,
),
),

View file

@ -161,7 +161,7 @@ main() {
final testFile = dap.createTestFile(simpleArgPrintingProgram);
// Run the script, expecting a Stopped event.
final stop = dap.client.expectStop('pause');
final stop = dap.client.expectStop('exit');
await Future.wait([
stop,
dap.client.initialize(),
@ -176,6 +176,97 @@ main() {
], eagerError: true);
});
test('does not resume isolates if user passes --pause-isolates-on-start',
() async {
// Internally we always pass --pause-isolates-on-start and resume the
// isolates after setting any breakpoints.
//
// However if a user passes this flag explicitly, we should not
// auto-resume because they might be trying to debug something.
final testFile = dap.createTestFile(simpleArgPrintingProgram);
// Run the script, expecting a Stopped event.
final stop = dap.client.expectStop('entry');
await Future.wait([
stop,
dap.client.initialize(),
dap.client.launch(
testFile.path,
toolArgs: ["--pause-isolates-on-start"],
),
], eagerError: true);
// Resume and expect termination.
await Future.wait([
dap.client.event('terminated'),
dap.client.continue_((await stop).threadId!),
], eagerError: true);
});
test('receives thread, stopped, continued events during pause/resume',
() async {
final client = dap.client;
final testFile = dap.createTestFile(debuggerPauseAndPrintManyProgram);
// Collect interesting events that we want to verify exist and in the
// right order.
final interestingEvents = const {
'thread',
'stopped',
'continued',
'terminated'
};
final eventsFuture = client.allEvents
.where((e) => interestingEvents.contains(e.event))
.map((e) {
// Map onto a descriptive string for verifying later.
final reason = (e.body as Map<String, Object?>)['reason'] as String?;
return reason != null ? '${e.event} ($reason)' : e.event;
}).toList();
// Start the program and wait to pause on `debugger()`.
final stoppedFuture = client.expectStop('step');
await client.start(file: testFile);
final threadId = (await stoppedFuture).threadId!;
// Step 3 times and wait for the corresponding stop.
for (var i = 0; i < 3; i++) {
client.next(threadId);
await client.stoppedEvents.first;
}
// Resume to run to end.
client.continue_(threadId);
// Verify we had the expected events.
expect(
await eventsFuture,
[
'thread (started)',
'stopped (entry)',
'continued',
// stop on debugger()
'stopped (step)',
// step 1
'continued',
'stopped (step)',
// step 2
'continued',
'stopped (step)',
// step 3
'continued',
'stopped (step)',
// continue
'continued',
// pause-on-exit to drain stdout and handle looking up URIs
'stopped (exit)',
// finished
'thread (exited)',
'terminated',
],
);
});
for (final outputKind in ['stdout', 'stderr']) {
test('sends $outputKind output events in the correct order', () async {
// Output events that have their URIs mapped will be processed slowly due
@ -760,18 +851,21 @@ main() {
}, timeout: Timeout.none);
group('debug mode', () {
test('can run without DDS', () async {
final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
addTearDown(dap.tearDown);
test(
'can run without DDS',
() async {
final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
addTearDown(dap.tearDown);
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
await client.hitBreakpoint(testFile, breakpointLine);
expect(await client.ddsAvailable, isFalse);
});
expect(await client.ddsAvailable, isFalse);
},
);
test('can run without auth codes', () async {
final dap =

View file

@ -135,7 +135,8 @@ class DapTestClient {
/// Send an attachRequest to the server, asking it to attach to an existing
/// Dart program.
Future<Response> attach({
required bool autoResume,
required bool autoResumeOnEntry,
required bool autoResumeOnExit,
String? vmServiceUri,
String? vmServiceInfoFile,
String? cwd,
@ -154,10 +155,19 @@ class DapTestClient {
// When attaching, the paused VM will not be automatically unpaused, but
// instead send a Stopped(reason: 'entry') event. Respond to this by
// resuming (if requested).
final resumeFuture = autoResume
final resumeFuture = autoResumeOnEntry
? expectStop('entry').then((event) => continue_(event.threadId!))
: null;
// Also handle resuming on exit. This should be done if the test has
// started the app with a user-provided pause-on-exit but wants to
// simulate the user resuming after the exit pause.
if (autoResumeOnExit) {
stoppedEvents
.firstWhere((e) => e.reason == 'exit')
.then((event) => continue_(event.threadId!));
}
cwd ??= defaultCwd;
final attachResponse = sendRequest(
DartAttachRequestArguments(
@ -227,13 +237,22 @@ class DapTestClient {
/// Returns a Future that completes with the next [event] event.
Future<Event> event(String event) => _logIfSlow(
'Event "$event"',
_eventController.stream.firstWhere((e) => e.event == event,
allEvents.firstWhere((e) => e.event == event,
orElse: () =>
throw 'Did not receive $event event before stream closed'));
/// Returns a stream for [event] events.
Stream<Event> events(String event) {
return _eventController.stream.where((e) => e.event == event);
return allEvents.where((e) => e.event == event);
}
Stream<Event> get allEvents => _eventController.stream;
/// Returns a stream for 'stopped' events.
Stream<StoppedEventBody> get stoppedEvents {
return allEvents
.where((e) => e.event == 'stopped')
.map((e) => StoppedEventBody.fromJson(e.body as Map<String, Object?>));
}
/// Returns a stream for standard progress events.
@ -243,8 +262,7 @@ class DapTestClient {
'progressUpdate',
'progressEnd'
};
return _eventController.stream
.where((e) => standardProgressEvents.contains(e.event));
return allEvents.where((e) => standardProgressEvents.contains(e.event));
}
/// Returns a stream for custom Dart progress events.
@ -254,8 +272,7 @@ class DapTestClient {
'dart.progressUpdate',
'dart.progressEnd'
};
return _eventController.stream
.where((e) => customProgressEvents.contains(e.event));
return allEvents.where((e) => customProgressEvents.contains(e.event));
}
/// Records a handler for when the server sends a [request] request.
@ -902,14 +919,26 @@ extension DapTestClientExtension on DapTestClient {
///
/// If [file] or [line] are provided, they will be checked against the stop
/// location for the top stack frame.
///
/// Stopped-on-entry events will be automatically skipped unless
/// [skipFirstStopOnEntry] is `false` or [reason] is `"entry"`.
Future<StoppedEventBody> expectStop(
String reason, {
File? file,
int? line,
String? sourceName,
bool? skipFirstStopOnEntry,
}) async {
final e = await event('stopped');
final stop = StoppedEventBody.fromJson(e.body as Map<String, Object?>);
skipFirstStopOnEntry ??= reason != 'entry';
assert(skipFirstStopOnEntry != (reason == 'entry'));
// Unless we're specifically waiting for stop-on-entry, skip over those
// events because they can be emitted at during startup because now we use
// readyToResume we don't know if the isolate will be immediately unpaused
// and the client needs to have a consistent view of threads.
final stop = skipFirstStopOnEntry
? await stoppedEvents.skipWhile((e) => e.reason == 'entry').first
: await stoppedEvents.first;
expect(stop.reason, equals(reason));
final result =

View file

@ -227,6 +227,23 @@ const simpleMultiBreakpointProgram = '''
}
''';
/// A program that immediately pauses with debugger() and then has 5 print
/// statements.
///
/// Used for verifying breaking and stepping behaviour.
const debuggerPauseAndPrintManyProgram = '''
import 'dart:developer';
void main(List<String> args) async {
debugger();
print('1');
print('2');
print('3');
print('4');
print('5');
}
''';
final simpleBreakpointProgramWith50ExtraLines = '''
void main(List<String> args) async {
print('Hello!'); $breakpointMarker

View file

@ -88,6 +88,7 @@ Future<Process> startDartProcessPaused(
List<String> args, {
required String cwd,
List<String>? vmArgs,
required bool pauseOnExit,
}) async {
final vmPath = Platform.resolvedExecutable;
vmArgs ??= [];
@ -95,7 +96,7 @@ Future<Process> startDartProcessPaused(
'--enable-vm-service=0',
'--pause_isolates_on_start',
// Use pause-on-exit so we don't lose async output events in attach tests.
'--pause_isolates_on_exit',
if (pauseOnExit) '--pause_isolates_on_exit',
]);
final processArgs = [
...vmArgs,