Skip to content

Commit 8b42ee7

Browse files
prompt the user to restart the server if the executable or arguments change
1 parent 8d42288 commit 8b42ee7

File tree

5 files changed

+362
-136
lines changed

5 files changed

+362
-136
lines changed

lldb/tools/lldb-dap/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@
163163
"program"
164164
],
165165
"properties": {
166+
"debugAdapterHostname": {
167+
"type": "string",
168+
"markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
169+
},
170+
"debugAdapterPort": {
171+
"type": "number",
172+
"markdownDescription": "The port that an existing lldb-dap executable is listening on."
173+
},
166174
"debugAdapterExecutable": {
167175
"type": "string",
168176
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
@@ -360,6 +368,14 @@
360368
},
361369
"attach": {
362370
"properties": {
371+
"debugAdapterHostname": {
372+
"type": "string",
373+
"markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
374+
},
375+
"debugAdapterPort": {
376+
"type": "number",
377+
"markdownDescription": "The port that an existing lldb-dap executable is listening on."
378+
},
363379
"debugAdapterExecutable": {
364380
"type": "string",
365381
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."

lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts

Lines changed: 95 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as fs from "node:fs/promises";
66

77
const exec = util.promisify(child_process.execFile);
88

9-
export async function isExecutable(path: string): Promise<Boolean> {
9+
async function isExecutable(path: string): Promise<Boolean> {
1010
try {
1111
await fs.access(path, fs.constants.X_OK);
1212
} catch {
@@ -66,19 +66,17 @@ async function findDAPExecutable(): Promise<string | undefined> {
6666
}
6767

6868
async function getDAPExecutable(
69-
session: vscode.DebugSession,
69+
folder: vscode.WorkspaceFolder | undefined,
70+
configuration: vscode.DebugConfiguration,
7071
): Promise<string | undefined> {
7172
// Check if the executable was provided in the launch configuration.
72-
const launchConfigPath = session.configuration["debugAdapterExecutable"];
73+
const launchConfigPath = configuration["debugAdapterExecutable"];
7374
if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {
7475
return launchConfigPath;
7576
}
7677

7778
// Check if the executable was provided in the extension's configuration.
78-
const config = vscode.workspace.getConfiguration(
79-
"lldb-dap",
80-
session.workspaceFolder,
81-
);
79+
const config = vscode.workspace.getConfiguration("lldb-dap", folder);
8280
const configPath = config.get<string>("executable-path");
8381
if (configPath && configPath.length !== 0) {
8482
return configPath;
@@ -93,9 +91,12 @@ async function getDAPExecutable(
9391
return undefined;
9492
}
9593

96-
function getDAPArguments(session: vscode.DebugSession): string[] {
94+
function getDAPArguments(
95+
folder: vscode.WorkspaceFolder | undefined,
96+
configuration: vscode.DebugConfiguration,
97+
): string[] {
9798
// Check the debug configuration for arguments first
98-
const debugConfigArgs = session.configuration.debugAdapterArgs;
99+
const debugConfigArgs = configuration.debugAdapterArgs;
99100
if (
100101
Array.isArray(debugConfigArgs) &&
101102
debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
@@ -104,129 +105,110 @@ function getDAPArguments(session: vscode.DebugSession): string[] {
104105
}
105106
// Fall back on the workspace configuration
106107
return vscode.workspace
107-
.getConfiguration("lldb-dap")
108+
.getConfiguration("lldb-dap", folder)
108109
.get<string[]>("arguments", []);
109110
}
110111

111-
async function isServerModeSupported(exe: string): Promise<boolean> {
112-
const { stdout } = await exec(exe, ["--help"]);
113-
return /--connection/.test(stdout);
112+
/**
113+
* Shows a modal when the debug adapter's path is not found
114+
*/
115+
async function showLLDBDapNotFoundMessage(path?: string) {
116+
const message =
117+
path !== undefined
118+
? `Debug adapter path: ${path} is not a valid file`
119+
: "Unable to find the path to the LLDB debug adapter executable.";
120+
const openSettingsAction = "Open Settings";
121+
const callbackValue = await vscode.window.showErrorMessage(
122+
message,
123+
{ modal: true },
124+
openSettingsAction,
125+
);
126+
127+
if (openSettingsAction === callbackValue) {
128+
vscode.commands.executeCommand(
129+
"workbench.action.openSettings",
130+
"lldb-dap.executable-path",
131+
);
132+
}
114133
}
115134

116135
/**
117-
* This class defines a factory used to find the lldb-dap binary to use
118-
* depending on the session configuration.
136+
* Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
137+
* debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
138+
*
139+
* @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
140+
* @param configuration The {@link vscode.DebugConfiguration}
141+
* @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
142+
* @returns
119143
*/
120-
export class LLDBDapDescriptorFactory
121-
implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable
122-
{
123-
private server?: Promise<{
124-
process: child_process.ChildProcess;
125-
host: string;
126-
port: number;
127-
}>;
128-
129-
dispose() {
130-
this.server?.then(({ process }) => {
131-
process.kill();
132-
});
144+
export async function createDebugAdapterExecutable(
145+
folder: vscode.WorkspaceFolder | undefined,
146+
configuration: vscode.DebugConfiguration,
147+
userInteractive?: boolean,
148+
): Promise<vscode.DebugAdapterExecutable | undefined> {
149+
const config = vscode.workspace.getConfiguration("lldb-dap", folder);
150+
const log_path = config.get<string>("log-path");
151+
let env: { [key: string]: string } = {};
152+
if (log_path) {
153+
env["LLDBDAP_LOG"] = log_path;
133154
}
155+
const configEnvironment =
156+
config.get<{ [key: string]: string }>("environment") || {};
157+
const dapPath = await getDAPExecutable(folder, configuration);
134158

135-
async createDebugAdapterDescriptor(
136-
session: vscode.DebugSession,
137-
_executable: vscode.DebugAdapterExecutable | undefined,
138-
): Promise<vscode.DebugAdapterDescriptor | undefined> {
139-
const config = vscode.workspace.getConfiguration(
140-
"lldb-dap",
141-
session.workspaceFolder,
142-
);
143-
144-
const log_path = config.get<string>("log-path");
145-
let env: { [key: string]: string } = {};
146-
if (log_path) {
147-
env["LLDBDAP_LOG"] = log_path;
159+
if (!dapPath) {
160+
if (userInteractive) {
161+
showLLDBDapNotFoundMessage();
148162
}
149-
const configEnvironment =
150-
config.get<{ [key: string]: string }>("environment") || {};
151-
const dapPath = await getDAPExecutable(session);
163+
return undefined;
164+
}
152165

153-
if (!dapPath) {
154-
LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
155-
return undefined;
166+
if (!(await isExecutable(dapPath))) {
167+
if (userInteractive) {
168+
showLLDBDapNotFoundMessage(dapPath);
156169
}
170+
return undefined;
171+
}
157172

158-
if (!(await isExecutable(dapPath))) {
159-
LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
160-
return;
161-
}
173+
const dbgOptions = {
174+
env: {
175+
...configEnvironment,
176+
...env,
177+
},
178+
};
179+
const dbgArgs = getDAPArguments(folder, configuration);
180+
181+
return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
182+
}
162183

163-
const dbgOptions = {
164-
env: {
165-
...configEnvironment,
166-
...env,
167-
},
168-
};
169-
const dbgArgs = getDAPArguments(session);
170-
171-
const serverMode = config.get<boolean>("serverMode", false);
172-
if (serverMode && (await isServerModeSupported(dapPath))) {
173-
const { host, port } = await this.startServer(
174-
dapPath,
175-
dbgArgs,
176-
dbgOptions,
184+
/**
185+
* This class defines a factory used to find the lldb-dap binary to use
186+
* depending on the session configuration.
187+
*/
188+
export class LLDBDapDescriptorFactory
189+
implements vscode.DebugAdapterDescriptorFactory
190+
{
191+
async createDebugAdapterDescriptor(
192+
session: vscode.DebugSession,
193+
executable: vscode.DebugAdapterExecutable | undefined,
194+
): Promise<vscode.DebugAdapterDescriptor | undefined> {
195+
if (executable) {
196+
throw new Error(
197+
"Setting the debug adapter executable in the package.json is not supported.",
177198
);
178-
return new vscode.DebugAdapterServer(port, host);
179199
}
180200

181-
return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
182-
}
183-
184-
startServer(
185-
dapPath: string,
186-
args: string[],
187-
options: child_process.CommonSpawnOptions,
188-
): Promise<{ host: string; port: number }> {
189-
if (this.server) {
190-
return this.server;
201+
// Use a server connection if the debugAdapterPort is provided
202+
if (session.configuration.debugAdapterPort) {
203+
return new vscode.DebugAdapterServer(
204+
session.configuration.debugAdapterPort,
205+
session.configuration.debugAdapterHost,
206+
);
191207
}
192208

193-
this.server = new Promise((resolve) => {
194-
args.push("--connection", "connect://localhost:0");
195-
const server = child_process.spawn(dapPath, args, options);
196-
server.stdout!.setEncoding("utf8").once("data", (data: string) => {
197-
const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
198-
if (connection) {
199-
const host = connection[1];
200-
const port = Number(connection[2]);
201-
resolve({ process: server, host, port });
202-
}
203-
});
204-
server.on("exit", () => {
205-
this.server = undefined;
206-
});
207-
});
208-
return this.server;
209-
}
210-
211-
/**
212-
* Shows a message box when the debug adapter's path is not found
213-
*/
214-
static async showLLDBDapNotFoundMessage(path?: string | undefined) {
215-
const message =
216-
path !== undefined
217-
? `Debug adapter path: ${path} is not a valid file`
218-
: "Unable to find the path to the LLDB debug adapter executable.";
219-
const openSettingsAction = "Open Settings";
220-
const callbackValue = await vscode.window.showErrorMessage(
221-
message,
222-
openSettingsAction,
209+
return createDebugAdapterExecutable(
210+
session.workspaceFolder,
211+
session.configuration,
223212
);
224-
225-
if (openSettingsAction === callbackValue) {
226-
vscode.commands.executeCommand(
227-
"workbench.action.openSettings",
228-
"lldb-dap.executable-path",
229-
);
230-
}
231213
}
232214
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as vscode from "vscode";
2+
import * as child_process from "child_process";
3+
import * as util from "util";
4+
import { LLDBDapServer } from "./lldb-dap-server";
5+
import { createDebugAdapterExecutable } from "./debug-adapter-factory";
6+
7+
const exec = util.promisify(child_process.execFile);
8+
9+
/**
10+
* Shows an error message to the user that optionally allows them to open their
11+
* launch.json to configure it.
12+
*
13+
* @param message The error message to display to the user
14+
* @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
15+
*/
16+
async function showErrorWithConfigureButton(
17+
message: string,
18+
): Promise<null | undefined> {
19+
const userSelection = await vscode.window.showErrorMessage(
20+
message,
21+
{ modal: true },
22+
"Configure",
23+
);
24+
25+
if (userSelection === "Configure") {
26+
return null; // Stops the debug session and opens the launch.json for editing
27+
}
28+
29+
return undefined; // Only stops the debug session
30+
}
31+
32+
/**
33+
* Determines whether or not the given lldb-dap executable supports executing
34+
* in server mode.
35+
*
36+
* @param exe the path to the lldb-dap executable
37+
* @returns a boolean indicating whether or not lldb-dap supports server mode
38+
*/
39+
async function isServerModeSupported(exe: string): Promise<boolean> {
40+
const { stdout } = await exec(exe, ["--help"]);
41+
return /--connection/.test(stdout);
42+
}
43+
44+
export class LLDBDapConfigurationProvider
45+
implements vscode.DebugConfigurationProvider
46+
{
47+
constructor(private readonly server: LLDBDapServer) {}
48+
49+
async resolveDebugConfiguration(
50+
folder: vscode.WorkspaceFolder | undefined,
51+
debugConfiguration: vscode.DebugConfiguration,
52+
_token?: vscode.CancellationToken,
53+
): Promise<vscode.DebugConfiguration | null | undefined> {
54+
if (
55+
"debugAdapterHost" in debugConfiguration &&
56+
!("debugAdapterPort" in debugConfiguration)
57+
) {
58+
return showErrorWithConfigureButton(
59+
"A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
60+
);
61+
}
62+
63+
if (
64+
"debugAdapterPort" in debugConfiguration &&
65+
("debugAdapterExecutable" in debugConfiguration ||
66+
"debugAdapterArgs" in debugConfiguration)
67+
) {
68+
return showErrorWithConfigureButton(
69+
"The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
70+
);
71+
}
72+
73+
// Server mode needs to be handled here since DebugAdapterDescriptorFactory
74+
// will show an unhelpful error if it returns undefined. We'd rather show a
75+
// nicer error message here and allow stopping the debug session gracefully.
76+
const config = vscode.workspace.getConfiguration("lldb-dap", folder);
77+
if (config.get<boolean>("serverMode", false)) {
78+
const executable = await createDebugAdapterExecutable(
79+
folder,
80+
debugConfiguration,
81+
/* userInteractive */ true,
82+
);
83+
if (!executable) {
84+
return undefined;
85+
}
86+
if (await isServerModeSupported(executable.command)) {
87+
const serverInfo = await this.server.start(
88+
executable.command,
89+
executable.args,
90+
executable.options,
91+
);
92+
if (!serverInfo) {
93+
return undefined;
94+
}
95+
// Use a debug adapter host and port combination rather than an executable
96+
// and list of arguments.
97+
delete debugConfiguration.debugAdapterExecutable;
98+
delete debugConfiguration.debugAdapterArgs;
99+
debugConfiguration.debugAdapterHost = serverInfo.host;
100+
debugConfiguration.debugAdapterPort = serverInfo.port;
101+
}
102+
}
103+
104+
return debugConfiguration;
105+
}
106+
}

0 commit comments

Comments
 (0)