| // 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); |
| } |