https://github.com/matthewbastien updated https://github.com/llvm/llvm-project/pull/129262
>From b40c3e7e4ebb154c5f231676451acbd17e1f39f7 Mon Sep 17 00:00:00 2001 From: Matthew Bastien <matthew_bast...@apple.com> Date: Fri, 28 Feb 2025 11:08:25 -0500 Subject: [PATCH 1/5] allow providing debug adapter arguments --- lldb/tools/lldb-dap/package.json | 23 ++++++ .../lldb-dap/src-ts/debug-adapter-factory.ts | 72 +++++++++++++------ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index cd450a614b3f7..aa11d8bcaa66e 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -75,6 +75,15 @@ "type": "string", "description": "The path to the lldb-dap binary." }, + "lldb-dap.arguments": { + "scope": "resource", + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "The arguments provided to the lldb-dap process." + }, "lldb-dap.log-path": { "scope": "resource", "type": "string", @@ -162,6 +171,13 @@ "type": "string", "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." }, + "debugAdapterArgs": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The list of arguments used to launch the debug adapter executable." + }, "program": { "type": "string", "description": "Path to the program to debug." @@ -352,6 +368,13 @@ "type": "string", "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." }, + "debugAdapterArgs": { + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "The list of arguments used to launch the debug adapter executable." + }, "program": { "type": "string", "description": "Path to the program to attach to." diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts index 1f76fe31b00ad..51f45f87d660a 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts @@ -25,7 +25,7 @@ async function findWithXcrun(executable: string): Promise<string | undefined> { if (stdout) { return stdout.toString().trimEnd(); } - } catch (error) { } + } catch (error) {} } return undefined; } @@ -93,13 +93,33 @@ async function getDAPExecutable( return undefined; } +function getDAPArguments(session: vscode.DebugSession): string[] { + // Check the debug configuration for arguments first + const debugConfigArgs = session.configuration.debugAdapterArgs; + if ( + Array.isArray(debugConfigArgs) && + debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1 + ) { + return debugConfigArgs; + } + // Fall back on the workspace configuration + return vscode.workspace + .getConfiguration("lldb-dap") + .get<string[]>("arguments", []); +} + /** * This class defines a factory used to find the lldb-dap binary to use * depending on the session configuration. */ export class LLDBDapDescriptorFactory - implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable { - private server?: Promise<{ process: child_process.ChildProcess, host: string, port: number }>; + implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable +{ + private server?: Promise<{ + process: child_process.ChildProcess; + host: string; + port: number; + }>; dispose() { this.server?.then(({ process }) => { @@ -109,7 +129,7 @@ export class LLDBDapDescriptorFactory async createDebugAdapterDescriptor( session: vscode.DebugSession, - executable: vscode.DebugAdapterExecutable | undefined, + _executable: vscode.DebugAdapterExecutable | undefined, ): Promise<vscode.DebugAdapterDescriptor | undefined> { const config = vscode.workspace.getConfiguration( "lldb-dap", @@ -123,7 +143,7 @@ export class LLDBDapDescriptorFactory } const configEnvironment = config.get<{ [key: string]: string }>("environment") || {}; - const dapPath = (await getDAPExecutable(session)) ?? executable?.command; + const dapPath = await getDAPExecutable(session); if (!dapPath) { LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(); @@ -137,32 +157,38 @@ export class LLDBDapDescriptorFactory const dbgOptions = { env: { - ...executable?.options?.env, ...configEnvironment, ...env, }, }; - const dbgArgs = executable?.args ?? []; + const dbgArgs = getDAPArguments(session); - const serverMode = config.get<boolean>('serverMode', false); + const serverMode = config.get<boolean>("serverMode", false); if (serverMode) { - const { host, port } = await this.startServer(dapPath, dbgArgs, dbgOptions); + const { host, port } = await this.startServer( + dapPath, + dbgArgs, + dbgOptions, + ); return new vscode.DebugAdapterServer(port, host); } return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions); } - startServer(dapPath: string, args: string[], options: child_process.CommonSpawnOptions): Promise<{ host: string, port: number }> { - if (this.server) return this.server; + startServer( + dapPath: string, + args: string[], + options: child_process.CommonSpawnOptions, + ): Promise<{ host: string; port: number }> { + if (this.server) { + return this.server; + } - this.server = new Promise(resolve => { - args.push( - '--connection', - 'connect://localhost:0' - ); + this.server = new Promise((resolve) => { + args.push("--connection", "connect://localhost:0"); const server = child_process.spawn(dapPath, args, options); - server.stdout!.setEncoding('utf8').once('data', (data: string) => { + server.stdout!.setEncoding("utf8").once("data", (data: string) => { const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data); if (connection) { const host = connection[1]; @@ -170,9 +196,9 @@ export class LLDBDapDescriptorFactory resolve({ process: server, host, port }); } }); - server.on('exit', () => { + server.on("exit", () => { this.server = undefined; - }) + }); }); return this.server; } @@ -180,11 +206,11 @@ export class LLDBDapDescriptorFactory /** * Shows a message box when the debug adapter's path is not found */ - static async showLLDBDapNotFoundMessage(path?: string) { + static async showLLDBDapNotFoundMessage(path?: string | undefined) { const message = - path - ? `Debug adapter path: ${path} is not a valid file.` - : "Unable to find the path to the LLDB debug adapter executable."; + path !== undefined + ? `Debug adapter path: ${path} is not a valid file` + : "Unable to find the LLDB debug adapter executable."; const openSettingsAction = "Open Settings"; const callbackValue = await vscode.window.showErrorMessage( message, >From 057ff4c9eed5c2344f5377e9199814c55f6748b1 Mon Sep 17 00:00:00 2001 From: Matthew Bastien <matthew_bast...@apple.com> Date: Fri, 28 Feb 2025 11:22:41 -0500 Subject: [PATCH 2/5] update wording --- lldb/tools/lldb-dap/package.json | 6 +++--- lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index aa11d8bcaa66e..75d52786b01e8 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -82,7 +82,7 @@ "items": { "type": "string" }, - "description": "The arguments provided to the lldb-dap process." + "description": "The list of additional arguments used to launch the debug adapter executable." }, "lldb-dap.log-path": { "scope": "resource", @@ -176,7 +176,7 @@ "items": { "type": "string" }, - "markdownDescription": "The list of arguments used to launch the debug adapter executable." + "markdownDescription": "The list of additional arguments used to launch the debug adapter executable." }, "program": { "type": "string", @@ -373,7 +373,7 @@ "items": { "type": "string" }, - "markdownDescription": "The list of arguments used to launch the debug adapter executable." + "markdownDescription": "The list of additional arguments used to launch the debug adapter executable." }, "program": { "type": "string", diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts index 51f45f87d660a..8f1e8ad6b019a 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts @@ -210,7 +210,7 @@ export class LLDBDapDescriptorFactory const message = path !== undefined ? `Debug adapter path: ${path} is not a valid file` - : "Unable to find the LLDB debug adapter executable."; + : "Unable to find the path to the LLDB debug adapter executable."; const openSettingsAction = "Open Settings"; const callbackValue = await vscode.window.showErrorMessage( message, >From 14866434337e00a077945f63ebf3005fd3f8f61e Mon Sep 17 00:00:00 2001 From: Matthew Bastien <matthew_bast...@apple.com> Date: Fri, 7 Mar 2025 17:45:19 -0500 Subject: [PATCH 3/5] prompt the user to restart the server if the executable or arguments change --- lldb/tools/lldb-dap/package.json | 18 +- .../lldb-dap/src-ts/debug-adapter-factory.ts | 205 ++++++++---------- .../src-ts/debug-configuration-provider.ts | 88 ++++++++ lldb/tools/lldb-dap/src-ts/extension.ts | 38 ++-- lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts | 130 +++++++++++ 5 files changed, 346 insertions(+), 133 deletions(-) create mode 100644 lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts create mode 100644 lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index 75d52786b01e8..c3361ed3d0bf8 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -167,6 +167,14 @@ "program" ], "properties": { + "debugAdapterHostname": { + "type": "string", + "markdownDescription": "The hostname that an existing lldb-dap executable is listening on." + }, + "debugAdapterPort": { + "type": "number", + "markdownDescription": "The port that an existing lldb-dap executable is listening on." + }, "debugAdapterExecutable": { "type": "string", "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." @@ -364,6 +372,14 @@ }, "attach": { "properties": { + "debugAdapterHostname": { + "type": "string", + "markdownDescription": "The hostname that an existing lldb-dap executable is listening on." + }, + "debugAdapterPort": { + "type": "number", + "markdownDescription": "The port that an existing lldb-dap executable is listening on." + }, "debugAdapterExecutable": { "type": "string", "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." @@ -572,4 +588,4 @@ } ] } -} \ No newline at end of file +} diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts index 8f1e8ad6b019a..61c4b95efb8a7 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts @@ -6,7 +6,7 @@ import * as fs from "node:fs/promises"; const exec = util.promisify(child_process.execFile); -export async function isExecutable(path: string): Promise<Boolean> { +async function isExecutable(path: string): Promise<Boolean> { try { await fs.access(path, fs.constants.X_OK); } catch { @@ -66,19 +66,17 @@ async function findDAPExecutable(): Promise<string | undefined> { } async function getDAPExecutable( - session: vscode.DebugSession, + folder: vscode.WorkspaceFolder | undefined, + configuration: vscode.DebugConfiguration, ): Promise<string | undefined> { // Check if the executable was provided in the launch configuration. - const launchConfigPath = session.configuration["debugAdapterExecutable"]; + const launchConfigPath = configuration["debugAdapterExecutable"]; if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) { return launchConfigPath; } // Check if the executable was provided in the extension's configuration. - const config = vscode.workspace.getConfiguration( - "lldb-dap", - session.workspaceFolder, - ); + const config = vscode.workspace.getConfiguration("lldb-dap", folder); const configPath = config.get<string>("executable-path"); if (configPath && configPath.length !== 0) { return configPath; @@ -93,9 +91,12 @@ async function getDAPExecutable( return undefined; } -function getDAPArguments(session: vscode.DebugSession): string[] { +function getDAPArguments( + folder: vscode.WorkspaceFolder | undefined, + configuration: vscode.DebugConfiguration, +): string[] { // Check the debug configuration for arguments first - const debugConfigArgs = session.configuration.debugAdapterArgs; + const debugConfigArgs = configuration.debugAdapterArgs; if ( Array.isArray(debugConfigArgs) && debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1 @@ -104,124 +105,110 @@ function getDAPArguments(session: vscode.DebugSession): string[] { } // Fall back on the workspace configuration return vscode.workspace - .getConfiguration("lldb-dap") + .getConfiguration("lldb-dap", folder) .get<string[]>("arguments", []); } /** - * This class defines a factory used to find the lldb-dap binary to use - * depending on the session configuration. + * Shows a modal when the debug adapter's path is not found */ -export class LLDBDapDescriptorFactory - implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable -{ - private server?: Promise<{ - process: child_process.ChildProcess; - host: string; - port: number; - }>; - - dispose() { - this.server?.then(({ process }) => { - process.kill(); - }); - } +async function showLLDBDapNotFoundMessage(path?: string) { + const message = + path !== undefined + ? `Debug adapter path: ${path} is not a valid file` + : "Unable to find the path to the LLDB debug adapter executable."; + const openSettingsAction = "Open Settings"; + const callbackValue = await vscode.window.showErrorMessage( + message, + { modal: true }, + openSettingsAction, + ); - async createDebugAdapterDescriptor( - session: vscode.DebugSession, - _executable: vscode.DebugAdapterExecutable | undefined, - ): Promise<vscode.DebugAdapterDescriptor | undefined> { - const config = vscode.workspace.getConfiguration( - "lldb-dap", - session.workspaceFolder, + if (openSettingsAction === callbackValue) { + vscode.commands.executeCommand( + "workbench.action.openSettings", + "lldb-dap.executable-path", ); + } +} - const log_path = config.get<string>("log-path"); - let env: { [key: string]: string } = {}; - if (log_path) { - env["LLDBDAP_LOG"] = log_path; - } - const configEnvironment = - config.get<{ [key: string]: string }>("environment") || {}; - const dapPath = await getDAPExecutable(session); - - if (!dapPath) { - LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(); - return undefined; - } +/** + * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and + * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap. + * + * @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within + * @param configuration The {@link vscode.DebugConfiguration} + * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown) + * @returns + */ +export async function createDebugAdapterExecutable( + folder: vscode.WorkspaceFolder | undefined, + configuration: vscode.DebugConfiguration, + userInteractive?: boolean, +): Promise<vscode.DebugAdapterExecutable | undefined> { + const config = vscode.workspace.getConfiguration("lldb-dap", folder); + const log_path = config.get<string>("log-path"); + let env: { [key: string]: string } = {}; + if (log_path) { + env["LLDBDAP_LOG"] = log_path; + } + const configEnvironment = + config.get<{ [key: string]: string }>("environment") || {}; + const dapPath = await getDAPExecutable(folder, configuration); - if (!(await isExecutable(dapPath))) { - LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath); - return; + if (!dapPath) { + if (userInteractive) { + showLLDBDapNotFoundMessage(); } + return undefined; + } - const dbgOptions = { - env: { - ...configEnvironment, - ...env, - }, - }; - const dbgArgs = getDAPArguments(session); - - const serverMode = config.get<boolean>("serverMode", false); - if (serverMode) { - const { host, port } = await this.startServer( - dapPath, - dbgArgs, - dbgOptions, - ); - return new vscode.DebugAdapterServer(port, host); + if (!(await isExecutable(dapPath))) { + if (userInteractive) { + showLLDBDapNotFoundMessage(dapPath); } - - return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions); + return undefined; } - startServer( - dapPath: string, - args: string[], - options: child_process.CommonSpawnOptions, - ): Promise<{ host: string; port: number }> { - if (this.server) { - return this.server; - } + const dbgOptions = { + env: { + ...configEnvironment, + ...env, + }, + }; + const dbgArgs = getDAPArguments(folder, configuration); - this.server = new Promise((resolve) => { - args.push("--connection", "connect://localhost:0"); - const server = child_process.spawn(dapPath, args, options); - server.stdout!.setEncoding("utf8").once("data", (data: string) => { - const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data); - if (connection) { - const host = connection[1]; - const port = Number(connection[2]); - resolve({ process: server, host, port }); - } - }); - server.on("exit", () => { - this.server = undefined; - }); - }); - return this.server; - } + return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions); +} - /** - * Shows a message box when the debug adapter's path is not found - */ - static async showLLDBDapNotFoundMessage(path?: string | undefined) { - const message = - path !== undefined - ? `Debug adapter path: ${path} is not a valid file` - : "Unable to find the path to the LLDB debug adapter executable."; - const openSettingsAction = "Open Settings"; - const callbackValue = await vscode.window.showErrorMessage( - message, - openSettingsAction, - ); +/** + * This class defines a factory used to find the lldb-dap binary to use + * depending on the session configuration. + */ +export class LLDBDapDescriptorFactory + implements vscode.DebugAdapterDescriptorFactory +{ + async createDebugAdapterDescriptor( + session: vscode.DebugSession, + executable: vscode.DebugAdapterExecutable | undefined, + ): Promise<vscode.DebugAdapterDescriptor | undefined> { + if (executable) { + throw new Error( + "Setting the debug adapter executable in the package.json is not supported.", + ); + } - if (openSettingsAction === callbackValue) { - vscode.commands.executeCommand( - "workbench.action.openSettings", - "lldb-dap.executable-path", + // Use a server connection if the debugAdapterPort is provided + if (session.configuration.debugAdapterPort) { + return new vscode.DebugAdapterServer( + session.configuration.debugAdapterPort, + session.configuration.debugAdapterHost, ); } + + return createDebugAdapterExecutable( + session.workspaceFolder, + session.configuration, + ); } } diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts new file mode 100644 index 0000000000000..d14393afe6658 --- /dev/null +++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts @@ -0,0 +1,88 @@ +import * as vscode from "vscode"; +import { LLDBDapServer } from "./lldb-dap-server"; +import { createDebugAdapterExecutable } from "./debug-adapter-factory"; + +/** + * Shows an error message to the user that optionally allows them to open their + * launch.json to configure it. + * + * @param message The error message to display to the user + * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened + */ +async function showErrorWithConfigureButton( + message: string, +): Promise<null | undefined> { + const userSelection = await vscode.window.showErrorMessage( + message, + { modal: true }, + "Configure", + ); + + if (userSelection === "Configure") { + return null; // Stops the debug session and opens the launch.json for editing + } + + return undefined; // Only stops the debug session +} + +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 | null | undefined> { + if ( + "debugAdapterHost" in debugConfiguration && + !("debugAdapterPort" in debugConfiguration) + ) { + return showErrorWithConfigureButton( + "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.", + ); + } + + if ( + "debugAdapterPort" in debugConfiguration && + ("debugAdapterExecutable" in debugConfiguration || + "debugAdapterArgs" in debugConfiguration) + ) { + return showErrorWithConfigureButton( + "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.", + ); + } + + // 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)) { + const executable = await createDebugAdapterExecutable( + folder, + debugConfiguration, + /* userInteractive */ true, + ); + if (!executable) { + return undefined; + } + 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.debugAdapterHost = serverInfo.host; + debugConfiguration.debugAdapterPort = serverInfo.port; + } + + return debugConfiguration; + } +} diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts index a07bcdebcb68b..e29e2bee2f1fa 100644 --- a/lldb/tools/lldb-dap/src-ts/extension.ts +++ b/lldb/tools/lldb-dap/src-ts/extension.ts @@ -1,10 +1,9 @@ import * as vscode from "vscode"; -import { - LLDBDapDescriptorFactory, - isExecutable, -} from "./debug-adapter-factory"; +import { LLDBDapDescriptorFactory } from "./debug-adapter-factory"; import { DisposableContext } from "./disposable-context"; +import { LLDBDapConfigurationProvider } from "./debug-configuration-provider"; +import { LLDBDapServer } from "./lldb-dap-server"; /** * This class represents the extension and manages its life cycle. Other extensions @@ -13,29 +12,22 @@ import { DisposableContext } from "./disposable-context"; export class LLDBDapExtension extends DisposableContext { constructor() { super(); - const factory = new LLDBDapDescriptorFactory(); - this.pushSubscription(factory); + + const lldbDapServer = new LLDBDapServer(); + this.pushSubscription(lldbDapServer); + this.pushSubscription( - vscode.debug.registerDebugAdapterDescriptorFactory( + vscode.debug.registerDebugConfigurationProvider( "lldb-dap", - factory, - ) + new LLDBDapConfigurationProvider(lldbDapServer), + ), ); - this.pushSubscription( - vscode.workspace.onDidChangeConfiguration(async (event) => { - if (event.affectsConfiguration("lldb-dap.executable-path")) { - const dapPath = vscode.workspace - .getConfiguration("lldb-dap") - .get<string>("executable-path"); - if (dapPath) { - if (await isExecutable(dapPath)) { - return; - } - } - LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath || ""); - } - }), + this.pushSubscription( + vscode.debug.registerDebugAdapterDescriptorFactory( + "lldb-dap", + new LLDBDapDescriptorFactory(), + ), ); } } diff --git a/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts new file mode 100644 index 0000000000000..2241a8676e46f --- /dev/null +++ b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts @@ -0,0 +1,130 @@ +import * as child_process from "node:child_process"; +import * as vscode from "vscode"; + +function areArraysEqual<T>(lhs: T[], rhs: T[]): boolean { + if (lhs.length !== rhs.length) { + return false; + } + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false; + } + } + return true; +} + +/** + * Represents a running lldb-dap process that is accepting connections (i.e. in "server mode"). + * + * Handles startup of the process if it isn't running already as well as prompting the user + * to restart when arguments have changed. + */ +export class LLDBDapServer implements vscode.Disposable { + private serverProcess?: child_process.ChildProcessWithoutNullStreams; + private serverInfo?: Promise<{ host: string; port: number }>; + + /** + * Starts the server with the provided options. The server will be restarted or reused as + * necessary. + * + * @param dapPath the path to the debug adapter executable + * @param args the list of arguments to provide to the debug adapter + * @param options the options to provide to the debug adapter process + * @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server. + */ + async start( + dapPath: string, + args: string[], + options?: child_process.SpawnOptionsWithoutStdio, + ): Promise<{ host: string; port: number } | undefined> { + const dapArgs = [...args, "--connection", "connect://localhost:0"]; + if (!(await this.shouldContinueStartup(dapPath, dapArgs))) { + return undefined; + } + + if (this.serverInfo) { + return this.serverInfo; + } + + this.serverInfo = new Promise((resolve, reject) => { + const process = child_process.spawn(dapPath, dapArgs, options); + process.on("error", (error) => { + reject(error); + this.serverProcess = undefined; + this.serverInfo = undefined; + }); + process.on("exit", (code, signal) => { + let errorMessage = "Server process exited early"; + if (code !== undefined) { + errorMessage += ` with code ${code}`; + } else if (signal !== undefined) { + errorMessage += ` due to signal ${signal}`; + } + reject(new Error(errorMessage)); + this.serverProcess = undefined; + this.serverInfo = undefined; + }); + process.stdout.setEncoding("utf8").on("data", (data) => { + const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec( + data.toString(), + ); + if (connection) { + const host = connection[1]; + const port = Number(connection[2]); + resolve({ host, port }); + process.stdout.removeAllListeners(); + } + }); + this.serverProcess = process; + }); + return this.serverInfo; + } + + /** + * Checks to see if the server needs to be restarted. If so, it will prompt the user + * to ask if they wish to restart. + * + * @param dapPath the path to the debug adapter + * @param args the arguments for the debug adapter + * @returns whether or not startup should continue depending on user input + */ + private async shouldContinueStartup( + dapPath: string, + args: string[], + ): Promise<boolean> { + if (!this.serverProcess || !this.serverInfo) { + return true; + } + + if (areArraysEqual(this.serverProcess.spawnargs, [dapPath, ...args])) { + return true; + } + + const userInput = await vscode.window.showInformationMessage( + "A server mode instance of lldb-dap is already running, but the arguments are different from what is requested in your debug configuration or settings. Would you like to restart the server?", + { modal: true }, + "Restart", + "Use Existing", + ); + switch (userInput) { + case "Restart": + this.serverProcess.kill(); + this.serverProcess = undefined; + this.serverInfo = undefined; + return true; + case "Use Existing": + return true; + case undefined: + return false; + } + } + + dispose() { + if (!this.serverProcess) { + return; + } + this.serverProcess.kill(); + this.serverProcess = undefined; + this.serverInfo = undefined; + } +} >From 4c639360892f974c1e93f37425e477387a57123a Mon Sep 17 00:00:00 2001 From: Matthew Bastien <matthew_bast...@apple.com> Date: Fri, 7 Mar 2025 18:00:03 -0500 Subject: [PATCH 4/5] add more checks and error messages --- .../lldb-dap/src-ts/debug-adapter-factory.ts | 51 +++++++++---------- .../src-ts/debug-configuration-provider.ts | 24 +-------- .../lldb-dap/src-ts/ui/error-messages.ts | 49 ++++++++++++++++++ 3 files changed, 75 insertions(+), 49 deletions(-) create mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-messages.ts diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts index 61c4b95efb8a7..11a1cb776b0a3 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts @@ -3,6 +3,10 @@ import * as util from "util"; import * as vscode from "vscode"; import * as child_process from "child_process"; import * as fs from "node:fs/promises"; +import { + showErrorWithConfigureButton, + showLLDBDapNotFoundMessage, +} from "./ui/error-messages"; const exec = util.promisify(child_process.execFile); @@ -91,12 +95,27 @@ async function getDAPExecutable( return undefined; } -function getDAPArguments( +async function getDAPArguments( folder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, -): string[] { + userInteractive?: boolean, +): Promise<string[] | null | undefined> { // Check the debug configuration for arguments first const debugConfigArgs = configuration.debugAdapterArgs; + if (debugConfigArgs) { + if ( + !Array.isArray(debugConfigArgs) || + debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1 + ) { + if (!userInteractive) { + return undefined; + } + return showErrorWithConfigureButton( + "The debugAdapterArgs property must be an array of string values.", + ); + } + return debugConfigArgs; + } if ( Array.isArray(debugConfigArgs) && debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1 @@ -109,29 +128,6 @@ function getDAPArguments( .get<string[]>("arguments", []); } -/** - * Shows a modal when the debug adapter's path is not found - */ -async function showLLDBDapNotFoundMessage(path?: string) { - const message = - path !== undefined - ? `Debug adapter path: ${path} is not a valid file` - : "Unable to find the path to the LLDB debug adapter executable."; - const openSettingsAction = "Open Settings"; - const callbackValue = await vscode.window.showErrorMessage( - message, - { modal: true }, - openSettingsAction, - ); - - if (openSettingsAction === callbackValue) { - vscode.commands.executeCommand( - "workbench.action.openSettings", - "lldb-dap.executable-path", - ); - } -} - /** * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap. @@ -176,7 +172,10 @@ export async function createDebugAdapterExecutable( ...env, }, }; - const dbgArgs = getDAPArguments(folder, configuration); + const dbgArgs = await getDAPArguments(folder, configuration, userInteractive); + if (!dbgArgs) { + return undefined; + } return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions); } diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts index d14393afe6658..06517f05629aa 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts @@ -1,29 +1,7 @@ import * as vscode from "vscode"; import { LLDBDapServer } from "./lldb-dap-server"; import { createDebugAdapterExecutable } from "./debug-adapter-factory"; - -/** - * Shows an error message to the user that optionally allows them to open their - * launch.json to configure it. - * - * @param message The error message to display to the user - * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened - */ -async function showErrorWithConfigureButton( - message: string, -): Promise<null | undefined> { - const userSelection = await vscode.window.showErrorMessage( - message, - { modal: true }, - "Configure", - ); - - if (userSelection === "Configure") { - return null; // Stops the debug session and opens the launch.json for editing - } - - return undefined; // Only stops the debug session -} +import { showErrorWithConfigureButton } from "./ui/error-messages"; export class LLDBDapConfigurationProvider implements vscode.DebugConfigurationProvider diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts new file mode 100644 index 0000000000000..0127ca5e288cc --- /dev/null +++ b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts @@ -0,0 +1,49 @@ +import * as vscode from "vscode"; + +/** + * Shows a modal when the debug adapter's path is not found + */ +export async function showLLDBDapNotFoundMessage(path?: string) { + const message = + path !== undefined + ? `Debug adapter path: ${path} is not a valid file` + : "Unable to find the path to the LLDB debug adapter executable."; + const openSettingsAction = "Open Settings"; + const callbackValue = await vscode.window.showErrorMessage( + message, + { modal: true }, + openSettingsAction, + ); + + if (openSettingsAction === callbackValue) { + vscode.commands.executeCommand( + "workbench.action.openSettings", + "lldb-dap.executable-path", + ); + } +} + +/** + * Shows an error message to the user that optionally allows them to open their + * launch.json to configure it. + * + * Expected to be used in the context of a {@link vscode.DebugConfigurationProvider}. + * + * @param message The error message to display to the user + * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened + */ +export async function showErrorWithConfigureButton( + message: string, +): Promise<null | undefined> { + const userSelection = await vscode.window.showErrorMessage( + message, + { modal: true }, + "Configure", + ); + + if (userSelection === "Configure") { + return null; // Stops the debug session and opens the launch.json for editing + } + + return undefined; // Only stops the debug session +} >From 34f3875ddb00d1c963154c47a5696cc1d0e761f9 Mon Sep 17 00:00:00 2001 From: Matthew Bastien <matthew_bast...@apple.com> Date: Fri, 7 Mar 2025 18:09:15 -0500 Subject: [PATCH 5/5] mention that debug configuration properties override VS Code settings --- lldb/tools/lldb-dap/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index c3361ed3d0bf8..c2d1bceac1a07 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -177,14 +177,14 @@ }, "debugAdapterExecutable": { "type": "string", - "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." + "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings." }, "debugAdapterArgs": { "type": "array", "items": { "type": "string" }, - "markdownDescription": "The list of additional arguments used to launch the debug adapter executable." + "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings." }, "program": { "type": "string", @@ -382,14 +382,14 @@ }, "debugAdapterExecutable": { "type": "string", - "markdownDescription": "The absolute path to the LLDB debug adapter executable to use." + "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings." }, "debugAdapterArgs": { "type": "array", "items": { "type": "string" }, - "markdownDescription": "The list of additional arguments used to launch the debug adapter executable." + "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings." }, "program": { "type": "string", _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits