174 lines · typescript
1import { DebugProtocol } from "@vscode/debugprotocol";2import * as vscode from "vscode";3 4export interface LLDBDapCapabilities extends DebugProtocol.Capabilities {5 /** The debug adapter supports the `moduleSymbols` request. */6 supportsModuleSymbolsRequest?: boolean;7}8 9/** A helper type for mapping event types to their corresponding data type. */10// prettier-ignore11interface EventMap {12 "module": DebugProtocol.ModuleEvent;13 "exited": DebugProtocol.ExitedEvent;14 "capabilities": DebugProtocol.CapabilitiesEvent;15}16 17/** A type assertion to check if a ProtocolMessage is an event or if it is a specific event. */18function isEvent(19 message: DebugProtocol.ProtocolMessage,20): message is DebugProtocol.Event;21function isEvent<K extends keyof EventMap>(22 message: DebugProtocol.ProtocolMessage,23 event: K,24): message is EventMap[K];25function isEvent(26 message: DebugProtocol.ProtocolMessage,27 event?: string,28): boolean {29 return (30 message.type === "event" &&31 (!event || (message as DebugProtocol.Event).event === event)32 );33}34 35/** Tracks lldb-dap sessions for data visualizers. */36export class DebugSessionTracker37 implements vscode.DebugAdapterTrackerFactory, vscode.Disposable38{39 /**40 * Tracks active modules for each debug sessions.41 *42 * The modules are kept in an array to maintain the load order of the modules.43 */44 private modules = new Map<vscode.DebugSession, DebugProtocol.Module[]>();45 private modulesChanged = new vscode.EventEmitter<46 vscode.DebugSession | undefined47 >();48 private sessionReceivedCapabilities =49 new vscode.EventEmitter<[ vscode.DebugSession, LLDBDapCapabilities ]>();50 private sessionExited = new vscode.EventEmitter<vscode.DebugSession>();51 52 /**53 * Fired when modules are changed for any active debug session.54 *55 * Use `debugSessionModules` to retieve the active modules for a given debug session.56 */57 onDidChangeModules: vscode.Event<vscode.DebugSession | undefined> =58 this.modulesChanged.event;59 60 /** Fired when a debug session is initialized. */61 onDidReceiveSessionCapabilities:62 vscode.Event<[ vscode.DebugSession, LLDBDapCapabilities ]> =63 this.sessionReceivedCapabilities.event;64 65 /** Fired when a debug session is exiting. */66 onDidExitSession: vscode.Event<vscode.DebugSession> =67 this.sessionExited.event;68 69 constructor(private logger: vscode.LogOutputChannel) {70 this.onDidChangeModules(this.moduleChangedListener, this);71 vscode.debug.onDidChangeActiveDebugSession((session) =>72 this.modulesChanged.fire(session),73 );74 }75 76 dispose() {77 this.modules.clear();78 this.modulesChanged.dispose();79 }80 81 createDebugAdapterTracker(82 session: vscode.DebugSession,83 ): vscode.ProviderResult<vscode.DebugAdapterTracker> {84 this.logger.info(`Starting debug session "${session.name}"`);85 let stopping = false;86 return {87 onError: (error) => !stopping && this.logger.error(error), // Can throw benign read errors when shutting down.88 onDidSendMessage: (message) => this.onDidSendMessage(session, message),89 onWillStopSession: () => (stopping = true),90 onExit: () => this.onExit(session),91 };92 }93 94 /**95 * Retrieves the modules for the given debug session.96 *97 * Modules are returned in load order.98 */99 debugSessionModules(session: vscode.DebugSession): DebugProtocol.Module[] {100 return this.modules.get(session) ?? [];101 }102 103 /** Clear information from the active session. */104 private onExit(session: vscode.DebugSession) {105 this.modules.delete(session);106 this.modulesChanged.fire(undefined);107 }108 109 private showModulesTreeView(showModules: boolean) {110 vscode.commands.executeCommand(111 "setContext",112 "lldb-dap.showModules",113 showModules,114 );115 }116 117 private moduleChangedListener(session: vscode.DebugSession | undefined) {118 if (!session) {119 this.showModulesTreeView(false);120 return;121 }122 123 if (session == vscode.debug.activeDebugSession) {124 const sessionHasModules = this.modules.get(session) != undefined;125 this.showModulesTreeView(sessionHasModules);126 }127 }128 129 private onDidSendMessage(130 session: vscode.DebugSession,131 message: DebugProtocol.ProtocolMessage,132 ) {133 if (isEvent(message, "module")) {134 const { module, reason } = message.body;135 const modules = this.modules.get(session) ?? [];136 switch (reason) {137 case "new":138 case "changed": {139 const index = modules.findIndex((m) => m.id === module.id);140 if (index !== -1) {141 modules[index] = module;142 } else {143 modules.push(module);144 }145 break;146 }147 case "removed": {148 const index = modules.findIndex((m) => m.id === module.id);149 if (index !== -1) {150 modules.splice(index, 1);151 }152 break;153 }154 default:155 console.error("unexpected module event reason");156 break;157 }158 this.modules.set(session, modules);159 this.modulesChanged.fire(session);160 } else if (isEvent(message, "exited")) {161 // The vscode.DebugAdapterTracker#onExit event is sometimes called with162 // exitCode = undefined but the exit event from LLDB-DAP always has the "exitCode"163 const { exitCode } = message.body;164 this.logger.info(165 `Session "${session.name}" exited with code ${exitCode}`,166 );167 168 this.sessionExited.fire(session);169 } else if (isEvent(message, "capabilities")) {170 this.sessionReceivedCapabilities.fire([ session, message.body.capabilities ]);171 }172 }173}174