import { DebugProtocol } from "@vscode/debugprotocol"; import * as vscode from "vscode"; export interface LLDBDapCapabilities extends DebugProtocol.Capabilities { /** The debug adapter supports the `moduleSymbols` request. */ supportsModuleSymbolsRequest?: boolean; } /** A helper type for mapping event types to their corresponding data type. */ // prettier-ignore interface EventMap { "module": DebugProtocol.ModuleEvent; "exited": DebugProtocol.ExitedEvent; "capabilities": DebugProtocol.CapabilitiesEvent; } /** A type assertion to check if a ProtocolMessage is an event or if it is a specific event. */ function isEvent( message: DebugProtocol.ProtocolMessage, ): message is DebugProtocol.Event; function isEvent( message: DebugProtocol.ProtocolMessage, event: K, ): message is EventMap[K]; function isEvent( message: DebugProtocol.ProtocolMessage, event?: string, ): boolean { return ( message.type === "event" && (!event || (message as DebugProtocol.Event).event === event) ); } /** Tracks lldb-dap sessions for data visualizers. */ export class DebugSessionTracker implements vscode.DebugAdapterTrackerFactory, vscode.Disposable { /** * Tracks active modules for each debug sessions. * * The modules are kept in an array to maintain the load order of the modules. */ private modules = new Map(); private modulesChanged = new vscode.EventEmitter< vscode.DebugSession | undefined >(); private sessionReceivedCapabilities = new vscode.EventEmitter<[ vscode.DebugSession, LLDBDapCapabilities ]>(); private sessionExited = new vscode.EventEmitter(); /** * Fired when modules are changed for any active debug session. * * Use `debugSessionModules` to retieve the active modules for a given debug session. */ onDidChangeModules: vscode.Event = this.modulesChanged.event; /** Fired when a debug session is initialized. */ onDidReceiveSessionCapabilities: vscode.Event<[ vscode.DebugSession, LLDBDapCapabilities ]> = this.sessionReceivedCapabilities.event; /** Fired when a debug session is exiting. */ onDidExitSession: vscode.Event = this.sessionExited.event; constructor(private logger: vscode.LogOutputChannel) { this.onDidChangeModules(this.moduleChangedListener, this); vscode.debug.onDidChangeActiveDebugSession((session) => this.modulesChanged.fire(session), ); } dispose() { this.modules.clear(); this.modulesChanged.dispose(); } createDebugAdapterTracker( session: vscode.DebugSession, ): vscode.ProviderResult { this.logger.info(`Starting debug session "${session.name}"`); let stopping = false; return { onError: (error) => !stopping && this.logger.error(error), // Can throw benign read errors when shutting down. onDidSendMessage: (message) => this.onDidSendMessage(session, message), onWillStopSession: () => (stopping = true), onExit: () => this.onExit(session), }; } /** * Retrieves the modules for the given debug session. * * Modules are returned in load order. */ debugSessionModules(session: vscode.DebugSession): DebugProtocol.Module[] { return this.modules.get(session) ?? []; } /** Clear information from the active session. */ private onExit(session: vscode.DebugSession) { this.modules.delete(session); this.modulesChanged.fire(undefined); } private showModulesTreeView(showModules: boolean) { vscode.commands.executeCommand( "setContext", "lldb-dap.showModules", showModules, ); } private moduleChangedListener(session: vscode.DebugSession | undefined) { if (!session) { this.showModulesTreeView(false); return; } if (session == vscode.debug.activeDebugSession) { const sessionHasModules = this.modules.get(session) != undefined; this.showModulesTreeView(sessionHasModules); } } private onDidSendMessage( session: vscode.DebugSession, message: DebugProtocol.ProtocolMessage, ) { if (isEvent(message, "module")) { const { module, reason } = message.body; const modules = this.modules.get(session) ?? []; switch (reason) { case "new": case "changed": { const index = modules.findIndex((m) => m.id === module.id); if (index !== -1) { modules[index] = module; } else { modules.push(module); } break; } case "removed": { const index = modules.findIndex((m) => m.id === module.id); if (index !== -1) { modules.splice(index, 1); } break; } default: console.error("unexpected module event reason"); break; } this.modules.set(session, modules); this.modulesChanged.fire(session); } else if (isEvent(message, "exited")) { // The vscode.DebugAdapterTracker#onExit event is sometimes called with // exitCode = undefined but the exit event from LLDB-DAP always has the "exitCode" const { exitCode } = message.body; this.logger.info( `Session "${session.name}" exited with code ${exitCode}`, ); this.sessionExited.fire(session); } else if (isEvent(message, "capabilities")) { this.sessionReceivedCapabilities.fire([ session, message.body.capabilities ]); } } }