llvm-project/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
royitaqi 0e0ea714f3
[vscode-lldb] Add VS Code commands for high level debug workflow (#151827)
This allows other debugger extensions to leverage the `lldb-dap`
extension's settings and logic (e.g. "Server Mode").

Other debugger extensions can invoke these commands to resolve
configuration, create adapter descriptor, and get the `lldb-dap` process
for state tracking, additional interaction, and telemetry.

VS Code commands added:
* `lldb-dap.resolveDebugConfiguration`
* `lldb-dap.resolveDebugConfigurationWithSubstitutedVariables`
* `lldb-dap.createDebugAdapterDescriptor`
* `lldb-dap.getServerProcess`
2025-08-06 11:42:21 -07:00

147 lines
4.5 KiB
TypeScript

import * as child_process from "node:child_process";
import { isDeepStrictEqual } from "util";
import * as vscode from "vscode";
/**
* 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 }>;
constructor() {
vscode.commands.registerCommand(
"lldb-dap.getServerProcess",
() => this.serverProcess,
);
}
/**
* 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", "listen://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.cleanUp(process);
});
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.cleanUp(process);
});
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 (isDeepStrictEqual(this.serverProcess.spawnargs, [dapPath, ...args])) {
return true;
}
const userInput = await vscode.window.showInformationMessage(
"The arguments to lldb-dap have changed. Would you like to restart the server?",
{
modal: true,
detail: `An existing lldb-dap server (${this.serverProcess.pid}) is running with different arguments.
The previous lldb-dap server was started with:
${this.serverProcess.spawnargs.join(" ")}
The new lldb-dap server will be started with:
${dapPath} ${args.join(" ")}
Restarting the server will interrupt any existing debug sessions and start a new server.`,
},
"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.cleanUp(this.serverProcess);
}
cleanUp(process: child_process.ChildProcessWithoutNullStreams) {
// If the following don't equal, then the fields have already been updated
// (either a new process has started, or the fields were already cleaned
// up), and so the cleanup should be skipped.
if (this.serverProcess === process) {
this.serverProcess = undefined;
this.serverInfo = undefined;
}
}
}