xref: /llvm-project/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts (revision ff5953804ea5b430710b07f1dae395bfcf6d35d0)
1import * as path from "path";
2import * as util from "util";
3import * as vscode from "vscode";
4import * as child_process from "child_process";
5import * as fs from "node:fs/promises";
6
7export async function isExecutable(path: string): Promise<Boolean> {
8  try {
9    await fs.access(path, fs.constants.X_OK);
10  } catch {
11    return false;
12  }
13  return true;
14}
15
16async function findWithXcrun(executable: string): Promise<string | undefined> {
17  if (process.platform === "darwin") {
18    try {
19      const exec = util.promisify(child_process.execFile);
20      let { stdout, stderr } = await exec("/usr/bin/xcrun", [
21        "-find",
22        executable,
23      ]);
24      if (stdout) {
25        return stdout.toString().trimEnd();
26      }
27    } catch (error) {}
28  }
29  return undefined;
30}
31
32async function findInPath(executable: string): Promise<string | undefined> {
33  const env_path =
34    process.platform === "win32" ? process.env["Path"] : process.env["PATH"];
35  if (!env_path) {
36    return undefined;
37  }
38
39  const paths = env_path.split(path.delimiter);
40  for (const p of paths) {
41    const exe_path = path.join(p, executable);
42    if (await isExecutable(exe_path)) {
43      return exe_path;
44    }
45  }
46  return undefined;
47}
48
49async function findDAPExecutable(): Promise<string | undefined> {
50  const executable = process.platform === "win32" ? "lldb-dap.exe" : "lldb-dap";
51
52  // Prefer lldb-dap from Xcode on Darwin.
53  const xcrun_dap = findWithXcrun(executable);
54  if (xcrun_dap) {
55    return xcrun_dap;
56  }
57
58  // Find lldb-dap in the user's path.
59  const path_dap = findInPath(executable);
60  if (path_dap) {
61    return path_dap;
62  }
63
64  return undefined;
65}
66
67async function getDAPExecutable(
68  session: vscode.DebugSession,
69): Promise<string | undefined> {
70  const config = vscode.workspace.getConfiguration(
71    "lldb-dap",
72    session.workspaceFolder,
73  );
74
75  // Prefer the explicitly specified path in the extension's configuration.
76  const configPath = config.get<string>("executable-path");
77  if (configPath && configPath.length !== 0) {
78    return configPath;
79  }
80
81  // Try finding the lldb-dap binary.
82  const foundPath = await findDAPExecutable();
83  if (foundPath) {
84    return foundPath;
85  }
86
87  return undefined;
88}
89
90/**
91 * This class defines a factory used to find the lldb-dap binary to use
92 * depending on the session configuration.
93 */
94export class LLDBDapDescriptorFactory
95  implements vscode.DebugAdapterDescriptorFactory
96{
97  async createDebugAdapterDescriptor(
98    session: vscode.DebugSession,
99    executable: vscode.DebugAdapterExecutable | undefined,
100  ): Promise<vscode.DebugAdapterDescriptor | undefined> {
101    const config = vscode.workspace.getConfiguration(
102      "lldb-dap",
103      session.workspaceFolder,
104    );
105
106    const log_path = config.get<string>("log-path");
107    let env: { [key: string]: string } = {};
108    if (log_path) {
109      env["LLDBDAP_LOG"] = log_path;
110    }
111    const configEnvironment =
112      config.get<{ [key: string]: string }>("environment") || {};
113    const dapPath = await getDAPExecutable(session);
114    const dbgOptions = {
115      env: {
116        ...executable?.options?.env,
117        ...configEnvironment,
118        ...env,
119      },
120    };
121    if (dapPath) {
122      if (!(await isExecutable(dapPath))) {
123        LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
124        return undefined;
125      }
126      return new vscode.DebugAdapterExecutable(dapPath, [], dbgOptions);
127    } else if (executable) {
128      if (!(await isExecutable(executable.command))) {
129        LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(executable.command);
130        return undefined;
131      }
132      return new vscode.DebugAdapterExecutable(
133        executable.command,
134        executable.args,
135        dbgOptions,
136      );
137    }
138    return undefined;
139  }
140
141  /**
142   * Shows a message box when the debug adapter's path is not found
143   */
144  static async showLLDBDapNotFoundMessage(path: string) {
145    const openSettingsAction = "Open Settings";
146    const callbackValue = await vscode.window.showErrorMessage(
147      `Debug adapter path: ${path} is not a valid file`,
148      openSettingsAction,
149    );
150
151    if (openSettingsAction === callbackValue) {
152      vscode.commands.executeCommand(
153        "workbench.action.openSettings",
154        "lldb-dap.executable-path",
155      );
156    }
157  }
158}
159