251 lines · typescript
1import * as vscode from "vscode";2import * as child_process from "child_process";3import * as util from "util";4import { LLDBDapServer } from "./lldb-dap-server";5import { createDebugAdapterExecutable } from "./debug-adapter-factory";6import { ConfigureButton, showErrorMessage } from "./ui/show-error-message";7import { ErrorWithNotification } from "./ui/error-with-notification";8import { LogFilePathProvider } from "./logging";9 10const exec = util.promisify(child_process.execFile);11 12/**13 * Determines whether or not the given lldb-dap executable supports executing14 * in server mode.15 *16 * @param exe the path to the lldb-dap executable17 * @returns a boolean indicating whether or not lldb-dap supports server mode18 */19async function isServerModeSupported(exe: string): Promise<boolean> {20 const { stdout } = await exec(exe, ["--help"]);21 return /--connection/.test(stdout);22}23 24interface BoolConfig {25 type: "boolean";26 default: boolean;27}28interface StringConfig {29 type: "string";30 default: string;31}32interface NumberConfig {33 type: "number";34 default: number;35}36interface StringArrayConfig {37 type: "stringArray";38 default: string[];39}40type DefaultConfig =41 | BoolConfig42 | NumberConfig43 | StringConfig44 | StringArrayConfig;45 46const configurations: Record<string, DefaultConfig> = {47 // Keys for debugger configurations.48 commandEscapePrefix: { type: "string", default: "`" },49 customFrameFormat: { type: "string", default: "" },50 customThreadFormat: { type: "string", default: "" },51 detachOnError: { type: "boolean", default: false },52 disableASLR: { type: "boolean", default: true },53 disableSTDIO: { type: "boolean", default: false },54 displayExtendedBacktrace: { type: "boolean", default: false },55 enableAutoVariableSummaries: { type: "boolean", default: false },56 enableSyntheticChildDebugging: { type: "boolean", default: false },57 timeout: { type: "number", default: 30 },58 59 // Keys for platform / target configuration.60 platformName: { type: "string", default: "" },61 targetTriple: { type: "string", default: "" },62 63 // Keys for debugger command hooks.64 initCommands: { type: "stringArray", default: [] },65 preRunCommands: { type: "stringArray", default: [] },66 postRunCommands: { type: "stringArray", default: [] },67 stopCommands: { type: "stringArray", default: [] },68 exitCommands: { type: "stringArray", default: [] },69 terminateCommands: { type: "stringArray", default: [] },70};71 72export function getDefaultConfigKey(key: string): string | number | boolean | string[] | undefined {73 return configurations[key]?.default;74}75 76export class LLDBDapConfigurationProvider77 implements vscode.DebugConfigurationProvider78{79 constructor(80 private readonly server: LLDBDapServer,81 private readonly logger: vscode.LogOutputChannel,82 private readonly logFilePath: LogFilePathProvider,83 ) {84 vscode.commands.registerCommand(85 "lldb-dap.resolveDebugConfiguration",86 (87 folder: vscode.WorkspaceFolder | undefined,88 debugConfiguration: vscode.DebugConfiguration,89 token?: vscode.CancellationToken,90 ) => this.resolveDebugConfiguration(folder, debugConfiguration, token),91 );92 vscode.commands.registerCommand(93 "lldb-dap.resolveDebugConfigurationWithSubstitutedVariables",94 (95 folder: vscode.WorkspaceFolder | undefined,96 debugConfiguration: vscode.DebugConfiguration,97 token?: vscode.CancellationToken,98 ) =>99 this.resolveDebugConfigurationWithSubstitutedVariables(100 folder,101 debugConfiguration,102 token,103 ),104 );105 }106 107 async resolveDebugConfiguration(108 folder: vscode.WorkspaceFolder | undefined,109 debugConfiguration: vscode.DebugConfiguration,110 token?: vscode.CancellationToken,111 ): Promise<vscode.DebugConfiguration> {112 this.logger.info(113 `Resolving debug configuration for "${debugConfiguration.name}"`,114 );115 this.logger.debug(116 "Initial debug configuration:\n" +117 JSON.stringify(debugConfiguration, undefined, 2),118 );119 let config = vscode.workspace.getConfiguration("lldb-dap");120 for (const [key, cfg] of Object.entries(configurations)) {121 if (Reflect.has(debugConfiguration, key)) {122 continue;123 }124 const value = config.get(key);125 if (value === undefined || value === cfg.default) {126 continue;127 }128 switch (cfg.type) {129 case "string":130 if (typeof value !== "string") {131 throw new Error(`Expected ${key} to be a string, got ${value}`);132 }133 break;134 case "number":135 if (typeof value !== "number") {136 throw new Error(`Expected ${key} to be a number, got ${value}`);137 }138 break;139 case "boolean":140 if (typeof value !== "boolean") {141 throw new Error(`Expected ${key} to be a boolean, got ${value}`);142 }143 break;144 case "stringArray":145 if (typeof value !== "object" && Array.isArray(value)) {146 throw new Error(147 `Expected ${key} to be a array of strings, got ${value}`,148 );149 }150 if ((value as string[]).length === 0) {151 continue;152 }153 break;154 }155 156 debugConfiguration[key] = value;157 }158 159 return debugConfiguration;160 }161 162 async resolveDebugConfigurationWithSubstitutedVariables(163 folder: vscode.WorkspaceFolder | undefined,164 debugConfiguration: vscode.DebugConfiguration,165 _token?: vscode.CancellationToken,166 ): Promise<vscode.DebugConfiguration | null | undefined> {167 try {168 if (169 "debugAdapterHostname" in debugConfiguration &&170 !("debugAdapterPort" in debugConfiguration)171 ) {172 throw new ErrorWithNotification(173 "A debugAdapterPort must be provided when debugAdapterHostname is set. Please update your launch configuration.",174 new ConfigureButton(),175 );176 }177 178 // Check if we're going to launch a debug session or use an existing process179 if ("debugAdapterPort" in debugConfiguration) {180 if (181 "debugAdapterExecutable" in debugConfiguration ||182 "debugAdapterArgs" in debugConfiguration183 ) {184 throw new ErrorWithNotification(185 "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",186 new ConfigureButton(),187 );188 }189 } else {190 // Always try to create the debug adapter executable as this will show the user errors191 // if there are any.192 const executable = await createDebugAdapterExecutable(193 this.logger,194 this.logFilePath,195 folder,196 debugConfiguration,197 );198 if (!executable) {199 return undefined;200 }201 202 // Server mode needs to be handled here since DebugAdapterDescriptorFactory203 // will show an unhelpful error if it returns undefined. We'd rather show a204 // nicer error message here and allow stopping the debug session gracefully.205 const config = vscode.workspace.getConfiguration("lldb-dap", folder);206 if (207 config.get<boolean>("serverMode", false) &&208 (await isServerModeSupported(executable.command))209 ) {210 const connectionTimeoutSeconds = config.get<number | undefined>(211 "connectionTimeout",212 undefined,213 );214 const serverInfo = await this.server.start(215 executable.command,216 executable.args,217 executable.options,218 connectionTimeoutSeconds,219 );220 if (!serverInfo) {221 return undefined;222 }223 // Use a debug adapter host and port combination rather than an executable224 // and list of arguments.225 delete debugConfiguration.debugAdapterExecutable;226 delete debugConfiguration.debugAdapterArgs;227 debugConfiguration.debugAdapterHostname = serverInfo.host;228 debugConfiguration.debugAdapterPort = serverInfo.port;229 }230 }231 232 this.logger.info(233 "Resolved debug configuration:\n" +234 JSON.stringify(debugConfiguration, undefined, 2),235 );236 237 return debugConfiguration;238 } catch (error) {239 this.logger.error(error as Error);240 // Show a better error message to the user if possible241 if (!(error instanceof ErrorWithNotification)) {242 throw error;243 }244 return await error.showNotification({245 modal: true,246 showConfigureButton: true,247 });248 }249 }250}251