351 lines · typescript
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";6import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";7import { ErrorWithNotification } from "./ui/error-with-notification";8import { LogFilePathProvider, LogType } from "./logging";9import { expandUser } from "./utils";10 11const exec = util.promisify(child_process.execFile);12 13async function isExecutable(path: string): Promise<Boolean> {14 try {15 await fs.access(path, fs.constants.X_OK);16 } catch {17 return false;18 }19 return true;20}21 22async function findWithXcrun(executable: string): Promise<string | undefined> {23 if (process.platform === "darwin") {24 try {25 let { stdout, stderr } = await exec("/usr/bin/xcrun", [26 "-find",27 executable,28 ]);29 if (stdout) {30 return stdout.toString().trimEnd();31 }32 } catch (error) {}33 }34 return undefined;35}36 37async function findInPath(executable: string): Promise<string | undefined> {38 const env_path =39 process.platform === "win32" ? process.env["Path"] : process.env["PATH"];40 if (!env_path) {41 return undefined;42 }43 44 const paths = env_path.split(path.delimiter);45 for (const p of paths) {46 const exe_path = path.join(p, executable);47 if (await isExecutable(exe_path)) {48 return exe_path;49 }50 }51 return undefined;52}53 54async function findDAPExecutable(): Promise<string | undefined> {55 const executable = process.platform === "win32" ? "lldb-dap.exe" : "lldb-dap";56 57 // Prefer lldb-dap from Xcode on Darwin.58 const xcrun_dap = await findWithXcrun(executable);59 if (xcrun_dap) {60 return xcrun_dap;61 }62 63 // Find lldb-dap in the user's path.64 const path_dap = await findInPath(executable);65 if (path_dap) {66 return path_dap;67 }68 69 return undefined;70}71 72/**73 * Validates the DAP environment provided in the debug configuration.74 * It must be a dictionary of string keys and values OR an array of string values.75 *76 * @param debugConfigEnv The supposed DAP environment that will be validated77 * @returns Whether or not the DAP environment is valid78 */79function validateDAPEnv(debugConfigEnv: any): boolean {80 // If the env is an object, it should have string values.81 // The keys are guaranteed to be strings.82 if (83 typeof debugConfigEnv === "object" &&84 Object.values(debugConfigEnv).findIndex(85 (entry) => typeof entry !== "string",86 ) !== -187 ) {88 return false;89 }90 91 // If the env is an array, it should have string values which match the regex.92 if (93 Array.isArray(debugConfigEnv) &&94 debugConfigEnv.findIndex(95 (entry) =>96 typeof entry !== "string" || !/^\w+(=.*)?$/.test(entry),97 ) !== -198 ) {99 return false;100 }101 102 // The env is valid.103 return true;104}105 106/**107 * Retrieves the lldb-dap executable path either from settings or the provided108 * {@link vscode.DebugConfiguration}.109 *110 * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within111 * @param configuration The {@link vscode.DebugConfiguration} that will be launched112 * @throws An {@link ErrorWithNotification} if something went wrong113 * @returns The path to the lldb-dap executable114 */115async function getDAPExecutable(116 workspaceFolder: vscode.WorkspaceFolder | undefined,117 configuration: vscode.DebugConfiguration,118): Promise<string> {119 // Check if the executable was provided in the launch configuration.120 let launchConfigPath = configuration["debugAdapterExecutable"];121 if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {122 launchConfigPath = expandUser(launchConfigPath);123 if (!(await isExecutable(launchConfigPath))) {124 throw new ErrorWithNotification(125 `Debug adapter path "${launchConfigPath}" is not a valid file. The path comes from your launch configuration.`,126 new ConfigureButton(),127 );128 }129 return launchConfigPath;130 }131 132 // Check if the executable was provided in the extension's configuration.133 const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);134 const configPath = expandUser(config.get<string>("executable-path") ?? "");135 if (configPath && configPath.length !== 0) {136 if (!(await isExecutable(configPath))) {137 throw new ErrorWithNotification(138 `Debug adapter path "${configPath}" is not a valid file. The path comes from your settings.`,139 new OpenSettingsButton("lldb-dap.executable-path"),140 );141 }142 return configPath;143 }144 145 // Try finding the lldb-dap binary.146 const foundPath = await findDAPExecutable();147 if (foundPath) {148 if (!(await isExecutable(foundPath))) {149 throw new ErrorWithNotification(150 `Found a potential debug adapter on your system at "${configPath}", but it is not a valid file.`,151 new OpenSettingsButton("lldb-dap.executable-path"),152 );153 }154 return foundPath;155 }156 157 throw new ErrorWithNotification(158 "Unable to find the path to the LLDB debug adapter executable.",159 new OpenSettingsButton("lldb-dap.executable-path"),160 );161}162 163/**164 * Retrieves the arguments that will be provided to lldb-dap either from settings or the provided165 * {@link vscode.DebugConfiguration}.166 *167 * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within168 * @param configuration The {@link vscode.DebugConfiguration} that will be launched169 * @throws An {@link ErrorWithNotification} if something went wrong170 * @returns The arguments that will be provided to lldb-dap171 */172async function getDAPArguments(173 workspaceFolder: vscode.WorkspaceFolder | undefined,174 configuration: vscode.DebugConfiguration,175): Promise<string[]> {176 // Check the debug configuration for arguments first.177 const debugConfigArgs = configuration.debugAdapterArgs;178 if (debugConfigArgs) {179 if (180 !Array.isArray(debugConfigArgs) ||181 debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1182 ) {183 throw new ErrorWithNotification(184 "The debugAdapterArgs property must be an array of string values. Please update your launch configuration",185 new ConfigureButton(),186 );187 }188 return debugConfigArgs;189 }190 // Fall back on the workspace configuration.191 return vscode.workspace192 .getConfiguration("lldb-dap", workspaceFolder)193 .get<string[]>("arguments", []);194}195 196/**197 * Retrieves the environment that will be provided to lldb-dap either from settings or the provided198 * {@link vscode.DebugConfiguration}.199 *200 * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within201 * @param configuration The {@link vscode.DebugConfiguration} that will be launched202 * @throws An {@link ErrorWithNotification} if something went wrong203 * @returns The environment that will be provided to lldb-dap204 */205async function getDAPEnvironment(206 workspaceFolder: vscode.WorkspaceFolder | undefined,207 configuration: vscode.DebugConfiguration,208): Promise<{ [key: string]: string }> {209 const debugConfigEnv = configuration.debugAdapterEnv;210 if (debugConfigEnv) {211 if (validateDAPEnv(debugConfigEnv) === false) {212 throw new ErrorWithNotification(213 "The debugAdapterEnv property must be a dictionary of string keys and values OR an array of string values. Please update your launch configuration",214 new ConfigureButton(),215 );216 }217 218 // Transform, so that the returned value is always a dictionary.219 if (Array.isArray(debugConfigEnv)) {220 const ret: { [key: string]: string } = {};221 for (const envVar of debugConfigEnv as string[]) {222 const equalSignPos = envVar.search("=");223 if (equalSignPos >= 0) {224 ret[envVar.substr(0, equalSignPos)] = envVar.substr(equalSignPos + 1);225 } else {226 ret[envVar] = "";227 }228 }229 return ret;230 } else {231 return debugConfigEnv;232 }233 }234 235 const config = vscode.workspace.workspaceFile236 ? vscode.workspace.getConfiguration("lldb-dap")237 : vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);238 return config.get<{ [key: string]: string }>("environment") || {};239}240 241/**242 * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and243 * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.244 *245 * @param logger The {@link vscode.LogOutputChannel} to log setup diagnostics246 * @param logFilePath The {@link LogFilePathProvider} for determining where to put session logs247 * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within248 * @param configuration The {@link vscode.DebugConfiguration} that will be launched249 * @throws An {@link ErrorWithNotification} if something went wrong250 * @returns The {@link vscode.DebugAdapterExecutable} that can be used to launch lldb-dap251 */252export async function createDebugAdapterExecutable(253 logger: vscode.LogOutputChannel,254 logFilePath: LogFilePathProvider,255 workspaceFolder: vscode.WorkspaceFolder | undefined,256 configuration: vscode.DebugConfiguration,257): Promise<vscode.DebugAdapterExecutable> {258 const config = vscode.workspace.workspaceFile259 ? vscode.workspace.getConfiguration("lldb-dap")260 : vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);261 const log_path = config.get<string>("log-path");262 let env: { [key: string]: string } = {};263 if (log_path) {264 env["LLDBDAP_LOG"] = log_path;265 } else if (266 vscode.workspace267 .getConfiguration("lldb-dap")268 .get("captureSessionLogs", false)269 ) {270 env["LLDBDAP_LOG"] = logFilePath.get(LogType.DEBUG_SESSION);271 }272 const configEnvironment = await getDAPEnvironment(273 workspaceFolder,274 configuration,275 );276 const dapPath = await getDAPExecutable(workspaceFolder, configuration);277 278 const dbgOptions = {279 env: {280 ...configEnvironment,281 ...env,282 },283 cwd: configuration.cwd ?? workspaceFolder?.uri.fsPath,284 };285 const dbgArgs = await getDAPArguments(workspaceFolder, configuration);286 287 logger.info(`lldb-dap path: ${dapPath}`);288 logger.info(`lldb-dap args: ${dbgArgs}`);289 logger.info(`cwd: ${dbgOptions.cwd}`);290 logger.info(`env: ${JSON.stringify(dbgOptions.env)}`);291 292 return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);293}294 295/**296 * This class defines a factory used to find the lldb-dap binary to use297 * depending on the session configuration.298 */299export class LLDBDapDescriptorFactory300 implements vscode.DebugAdapterDescriptorFactory301{302 constructor(303 private readonly logger: vscode.LogOutputChannel,304 private logFilePath: LogFilePathProvider,305 ) {306 vscode.commands.registerCommand(307 "lldb-dap.createDebugAdapterDescriptor",308 (309 session: vscode.DebugSession,310 executable: vscode.DebugAdapterExecutable | undefined,311 ) => this.createDebugAdapterDescriptor(session, executable),312 );313 }314 315 async createDebugAdapterDescriptor(316 session: vscode.DebugSession,317 executable: vscode.DebugAdapterExecutable | undefined,318 ): Promise<vscode.DebugAdapterDescriptor | undefined> {319 this.logger.info(`Creating debug adapter for session "${session.name}"`);320 this.logger.info(321 `Session "${session.name}" debug configuration:\n` +322 JSON.stringify(session.configuration, undefined, 2),323 );324 if (executable) {325 const error = new Error(326 "Setting the debug adapter executable in the package.json is not supported.",327 );328 this.logger.error(error);329 throw error;330 }331 332 // Use a server connection if the debugAdapterPort is provided333 if (session.configuration.debugAdapterPort) {334 this.logger.info(335 `Spawning debug adapter server on port ${session.configuration.debugAdapterPort}`,336 );337 return new vscode.DebugAdapterServer(338 session.configuration.debugAdapterPort,339 session.configuration.debugAdapterHostname,340 );341 }342 343 return createDebugAdapterExecutable(344 this.logger,345 this.logFilePath,346 session.workspaceFolder,347 session.configuration,348 );349 }350}351