import * as vscode from "vscode"; import * as child_process from "child_process"; import * as util from "util"; import { LLDBDapServer } from "./lldb-dap-server"; import { createDebugAdapterExecutable } from "./debug-adapter-factory"; import { ConfigureButton, showErrorMessage } from "./ui/show-error-message"; import { ErrorWithNotification } from "./ui/error-with-notification"; const exec = util.promisify(child_process.execFile); /** * Determines whether or not the given lldb-dap executable supports executing * in server mode. * * @param exe the path to the lldb-dap executable * @returns a boolean indicating whether or not lldb-dap supports server mode */ async function isServerModeSupported(exe: string): Promise { const { stdout } = await exec(exe, ["--help"]); return /--connection/.test(stdout); } interface BoolConfig { type: "boolean"; default: boolean; } interface StringConfig { type: "string"; default: string; } interface NumberConfig { type: "number"; default: number; } interface StringArrayConfig { type: "stringArray"; default: string[]; } type DefaultConfig = | BoolConfig | NumberConfig | StringConfig | StringArrayConfig; const configurations: Record = { // Keys for debugger configurations. commandEscapePrefix: { type: "string", default: "`" }, customFrameFormat: { type: "string", default: "" }, customThreadFormat: { type: "string", default: "" }, detachOnError: { type: "boolean", default: false }, disableASLR: { type: "boolean", default: true }, disableSTDIO: { type: "boolean", default: false }, displayExtendedBacktrace: { type: "boolean", default: false }, enableAutoVariableSummaries: { type: "boolean", default: false }, enableSyntheticChildDebugging: { type: "boolean", default: false }, timeout: { type: "number", default: 30 }, // Keys for platform / target configuration. platformName: { type: "string", default: "" }, targetTriple: { type: "string", default: "" }, // Keys for debugger command hooks. initCommands: { type: "stringArray", default: [] }, preRunCommands: { type: "stringArray", default: [] }, postRunCommands: { type: "stringArray", default: [] }, stopCommands: { type: "stringArray", default: [] }, exitCommands: { type: "stringArray", default: [] }, terminateCommands: { type: "stringArray", default: [] }, }; export class LLDBDapConfigurationProvider implements vscode.DebugConfigurationProvider { constructor(private readonly server: LLDBDapServer) {} async resolveDebugConfiguration( folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, token?: vscode.CancellationToken, ): Promise { let config = vscode.workspace.getConfiguration("lldb-dap"); for (const [key, cfg] of Object.entries(configurations)) { if (Reflect.has(debugConfiguration, key)) { continue; } const value = config.get(key); if (value === undefined || value === cfg.default) { continue; } switch (cfg.type) { case "string": if (typeof value !== "string") { throw new Error(`Expected ${key} to be a string, got ${value}`); } break; case "number": if (typeof value !== "number") { throw new Error(`Expected ${key} to be a number, got ${value}`); } break; case "boolean": if (typeof value !== "boolean") { throw new Error(`Expected ${key} to be a boolean, got ${value}`); } break; case "stringArray": if (typeof value !== "object" && Array.isArray(value)) { throw new Error( `Expected ${key} to be a array of strings, got ${value}`, ); } if ((value as string[]).length === 0) { continue; } break; } debugConfiguration[key] = value; } return debugConfiguration; } async resolveDebugConfigurationWithSubstitutedVariables( folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, _token?: vscode.CancellationToken, ): Promise { try { if ( "debugAdapterHostname" in debugConfiguration && !("debugAdapterPort" in debugConfiguration) ) { throw new ErrorWithNotification( "A debugAdapterPort must be provided when debugAdapterHostname is set. Please update your launch configuration.", new ConfigureButton(), ); } // Check if we're going to launch a debug session or use an existing process if ("debugAdapterPort" in debugConfiguration) { if ( "debugAdapterExecutable" in debugConfiguration || "debugAdapterArgs" in debugConfiguration ) { throw new ErrorWithNotification( "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.", new ConfigureButton(), ); } } else { // Always try to create the debug adapter executable as this will show the user errors // if there are any. const executable = await createDebugAdapterExecutable( folder, debugConfiguration, ); if (!executable) { return undefined; } // Server mode needs to be handled here since DebugAdapterDescriptorFactory // will show an unhelpful error if it returns undefined. We'd rather show a // nicer error message here and allow stopping the debug session gracefully. const config = vscode.workspace.getConfiguration("lldb-dap", folder); if ( config.get("serverMode", false) && (await isServerModeSupported(executable.command)) ) { const serverInfo = await this.server.start( executable.command, executable.args, executable.options, ); if (!serverInfo) { return undefined; } // Use a debug adapter host and port combination rather than an executable // and list of arguments. delete debugConfiguration.debugAdapterExecutable; delete debugConfiguration.debugAdapterArgs; debugConfiguration.debugAdapterHostname = serverInfo.host; debugConfiguration.debugAdapterPort = serverInfo.port; } } return debugConfiguration; } catch (error) { // Show a better error message to the user if possible if (!(error instanceof ErrorWithNotification)) { throw error; } return await error.showNotification({ modal: true, showConfigureButton: true, }); } } }