blob: 957bc5e1eb9569f2278ec86daf3a4ac45fdce0b0 [file] [log] [blame]
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<boolean> {
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<string, DefaultConfig> = {
// 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<vscode.DebugConfiguration> {
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 || 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<vscode.DebugConfiguration | null | undefined> {
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<boolean>("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,
});
}
}
}