blob: f40dbf049a4bb20ccf61372b3117e1f7579c578a [file] [log] [blame]
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 }>;
/**
* 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 (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.serverProcess = undefined;
this.serverInfo = undefined;
}
}