blob: 3bbc226580d08c7045a34da5f276f8c5216d26c4 [file] [log] [blame]
// 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 'package:vm_service/vm_service.dart' as vm;
import 'adapters/dart.dart';
import 'protocol_generated.dart';
/// Manages state of Isolates (called Threads by the DAP protocol).
///
/// Handles incoming Isolate and Debug events to track the lifetime of isolates
/// and updating breakpoints for each isolate as necessary.
class IsolateManager {
// TODO(dantup): This class has a lot of overlap with the same-named class
// in DDS. Review what can be shared.
final DartDebugAdapter _adapter;
final Map<String, Completer<void>> _isolateRegistrations = {};
final Map<String, ThreadInfo> _threadsByIsolateId = {};
final Map<int, ThreadInfo> _threadsByThreadId = {};
int _nextThreadNumber = 1;
IsolateManager(this._adapter);
/// A list of all current active isolates.
///
/// When isolates exit, they will no longer be returned in this list, although
/// due to the async nature, it's not guaranteed that threads in this list have
/// not exited between accessing this list and trying to use the results.
List<ThreadInfo> get threads => _threadsByIsolateId.values.toList();
Future<T> getObject<T extends vm.Response>(
vm.IsolateRef isolate, vm.ObjRef object) async {
final res = await _adapter.vmService?.getObject(isolate.id!, object.id!);
return res as T;
}
/// Handles Isolate and Debug events
FutureOr<void> handleEvent(vm.Event event) async {
final isolateId = event.isolate?.id;
if (isolateId == null) {
return;
}
// Delay processing any events until the debugger initialization has
// finished running, as events may arrive (for ex. IsolateRunnable) while
// it's doing is own initialization that this may interfere with.
await _adapter.debuggerInitialized;
final eventKind = event.kind;
if (eventKind == vm.EventKind.kIsolateStart ||
eventKind == vm.EventKind.kIsolateRunnable) {
await registerIsolate(event.isolate!, eventKind!);
}
// Additionally, ensure the thread registration has completed before trying
// to process any other events. This is to cover the case where we are
// processing the above registerIsolate call in the handler for one isolate
// event but another one arrives and gets us here before the registration
// above (in the other event handler) has finished.
await _isolateRegistrations[isolateId]?.future;
if (eventKind == vm.EventKind.kIsolateExit) {
await _handleExit(event);
} else if (eventKind?.startsWith('Pause') ?? false) {
await _handlePause(event);
} else if (eventKind == vm.EventKind.kResume) {
await _handleResumed(event);
}
}
/// Registers a new isolate that exists at startup, or has subsequently been
/// created.
///
/// 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(
vm.IsolateRef isolate,
String eventKind,
) async {
// Ensure the completer is set up before doing any async work, so future
// events can wait on it.
final registrationCompleter =
_isolateRegistrations.putIfAbsent(isolate.id!, () => Completer<void>());
final info = _threadsByIsolateId.putIfAbsent(
isolate.id!,
() {
// The first time we see an isolate, start tracking it.
final info = ThreadInfo(_nextThreadNumber++, isolate);
_threadsByThreadId[info.threadId] = info;
// And notify the client about it.
_adapter.sendEvent(
ThreadEventBody(reason: 'started', threadId: info.threadId),
);
return info;
},
);
// If it's just become runnable (IsolateRunnable), configure the isolate
// by sending breakpoints etc.
if (eventKind == vm.EventKind.kIsolateRunnable && !info.runnable) {
info.runnable = true;
await _configureIsolate(isolate);
registrationCompleter.complete();
}
}
FutureOr<void> resumeIsolate(vm.IsolateRef isolateRef,
[String? resumeType]) async {
final isolateId = isolateRef.id;
if (isolateId == null) {
return;
}
final thread = _threadsByIsolateId[isolateId];
if (thread == null) {
return;
}
await resumeThread(thread.threadId);
}
/// Resumes (or steps) an isolate using its client [threadId].
///
/// If the isolate is not paused, or already has a pending resume request
/// in-flight, a request will not be sent.
///
/// If the isolate is paused at an async suspension and the [resumeType] is
/// [vm.StepOption.kOver], a [StepOption.kOverAsyncSuspension] step will be
/// sent instead.
Future<void> resumeThread(int threadId, [String? resumeType]) async {
final thread = _threadsByThreadId[threadId];
if (thread == null) {
throw 'Thread $threadId was not found';
}
// 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) {
return;
}
// We always assume that a step when at an async suspension is intended to
// be an async step.
if (resumeType == vm.StepOption.kOver && thread.atAsyncSuspension) {
resumeType = vm.StepOption.kOverAsyncSuspension;
}
thread.hasPendingResume = true;
try {
await _adapter.vmService?.resume(thread.isolate.id!, step: resumeType);
} finally {
thread.hasPendingResume = false;
}
}
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
}
FutureOr<void> _handleExit(vm.Event event) {
final isolate = event.isolate!;
final isolateId = isolate.id!;
final thread = _threadsByIsolateId[isolateId];
if (thread != null) {
// Notify the client.
_adapter.sendEvent(
ThreadEventBody(reason: 'exited', threadId: thread.threadId),
);
_threadsByIsolateId.remove(isolateId);
_threadsByThreadId.remove(thread.threadId);
}
}
/// Handles a pause event.
///
/// For [vm.EventKind.kPausePostRequest] which occurs after a restart, the isolate
/// will be re-configured (pause-exception behaviour, debuggable libraries,
/// breakpoints) and then resumed.
///
/// For [vm.EventKind.kPauseStart], the isolate will be resumed.
///
/// For breakpoints with conditions that are not met and for logpoints, the
/// isolate will be automatically resumed.
///
/// 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 {
final eventKind = event.kind;
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
if (thread == null) {
return;
}
thread.atAsyncSuspension = event.atAsyncSuspension ?? false;
thread.paused = true;
thread.pauseEvent = event;
// For PausePostRequest we need to re-send all breakpoints; this happens
// after a hot restart.
if (eventKind == vm.EventKind.kPausePostRequest) {
_configureIsolate(isolate);
await resumeThread(thread.threadId);
} else if (eventKind == vm.EventKind.kPauseStart) {
await resumeThread(thread.threadId);
} else {
// PauseExit, PauseBreakpoint, PauseInterrupted, PauseException
var reason = 'pause';
if (eventKind == vm.EventKind.kPauseBreakpoint &&
(event.pauseBreakpoints?.isNotEmpty ?? false)) {
reason = 'breakpoint';
} else if (eventKind == vm.EventKind.kPauseBreakpoint) {
reason = 'step';
} else if (eventKind == vm.EventKind.kPauseException) {
reason = 'exception';
}
// TODO(dantup): Store exception.
// Notify the client.
_adapter.sendEvent(
StoppedEventBody(reason: reason, threadId: thread.threadId),
);
}
}
/// Handles a resume event from the VM, updating our local state.
FutureOr<void> _handleResumed(vm.Event event) {
final isolate = event.isolate!;
final thread = _threadsByIsolateId[isolate.id!];
if (thread != null) {
thread.paused = false;
thread.pauseEvent = null;
thread.exceptionReference = null;
}
}
}
/// Holds state for a single Isolate/Thread.
class ThreadInfo {
final vm.IsolateRef isolate;
final int threadId;
var runnable = false;
var atAsyncSuspension = false;
int? exceptionReference;
var paused = false;
// The most recent pauseEvent for this isolate.
vm.Event? pauseEvent;
/// Whether this isolate has an in-flight resume request that has not yet
/// been responded to.
var hasPendingResume = false;
ThreadInfo(this.threadId, this.isolate);
}