import * as path from "path"; import * as util from "util"; import * as vscode from "vscode"; import * as child_process from "child_process"; import * as fs from "node:fs/promises"; import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message"; import { ErrorWithNotification } from "./ui/error-with-notification"; const exec = util.promisify(child_process.execFile); async function isExecutable(path: string): Promise { try { await fs.access(path, fs.constants.X_OK); } catch { return false; } return true; } async function findWithXcrun(executable: string): Promise { if (process.platform === "darwin") { try { let { stdout, stderr } = await exec("/usr/bin/xcrun", [ "-find", executable, ]); if (stdout) { return stdout.toString().trimEnd(); } } catch (error) {} } return undefined; } async function findInPath(executable: string): Promise { const env_path = process.platform === "win32" ? process.env["Path"] : process.env["PATH"]; if (!env_path) { return undefined; } const paths = env_path.split(path.delimiter); for (const p of paths) { const exe_path = path.join(p, executable); if (await isExecutable(exe_path)) { return exe_path; } } return undefined; } async function findDAPExecutable(): Promise { const executable = process.platform === "win32" ? "lldb-dap.exe" : "lldb-dap"; // Prefer lldb-dap from Xcode on Darwin. const xcrun_dap = await findWithXcrun(executable); if (xcrun_dap) { return xcrun_dap; } // Find lldb-dap in the user's path. const path_dap = await findInPath(executable); if (path_dap) { return path_dap; } return undefined; } /** * Retrieves the lldb-dap executable path either from settings or the provided * {@link vscode.DebugConfiguration}. * * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within * @param configuration The {@link vscode.DebugConfiguration} that will be launched * @throws An {@link ErrorWithNotification} if something went wrong * @returns The path to the lldb-dap executable */ async function getDAPExecutable( workspaceFolder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, ): Promise { // Check if the executable was provided in the launch configuration. const launchConfigPath = configuration["debugAdapterExecutable"]; if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) { if (!(await isExecutable(launchConfigPath))) { throw new ErrorWithNotification( `Debug adapter path "${launchConfigPath}" is not a valid file. The path comes from your launch configuration.`, new ConfigureButton(), ); } return launchConfigPath; } // Check if the executable was provided in the extension's configuration. const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder); const configPath = config.get("executable-path"); if (configPath && configPath.length !== 0) { if (!(await isExecutable(configPath))) { throw new ErrorWithNotification( `Debug adapter path "${configPath}" is not a valid file. The path comes from your settings.`, new OpenSettingsButton("lldb-dap.executable-path"), ); } return configPath; } // Try finding the lldb-dap binary. const foundPath = await findDAPExecutable(); if (foundPath) { if (!(await isExecutable(foundPath))) { throw new ErrorWithNotification( `Found a potential debug adapter on your system at "${configPath}", but it is not a valid file.`, new OpenSettingsButton("lldb-dap.executable-path"), ); } return foundPath; } throw new ErrorWithNotification( "Unable to find the path to the LLDB debug adapter executable.", new OpenSettingsButton("lldb-dap.executable-path"), ); } /** * Retrieves the arguments that will be provided to lldb-dap either from settings or the provided * {@link vscode.DebugConfiguration}. * * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within * @param configuration The {@link vscode.DebugConfiguration} that will be launched * @throws An {@link ErrorWithNotification} if something went wrong * @returns The arguments that will be provided to lldb-dap */ async function getDAPArguments( workspaceFolder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, ): Promise { // Check the debug configuration for arguments first. const debugConfigArgs = configuration.debugAdapterArgs; if (debugConfigArgs) { if ( !Array.isArray(debugConfigArgs) || debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1 ) { throw new ErrorWithNotification( "The debugAdapterArgs property must be an array of string values. Please update your launch configuration", new ConfigureButton(), ); } return debugConfigArgs; } // Fall back on the workspace configuration. return vscode.workspace .getConfiguration("lldb-dap", workspaceFolder) .get("arguments", []); } /** * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap. * * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within * @param configuration The {@link vscode.DebugConfiguration} that will be launched * @throws An {@link ErrorWithNotification} if something went wrong * @returns The {@link vscode.DebugAdapterExecutable} that can be used to launch lldb-dap */ export async function createDebugAdapterExecutable( workspaceFolder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, ): Promise { const config = vscode.workspace.workspaceFile ? vscode.workspace.getConfiguration("lldb-dap") : vscode.workspace.getConfiguration("lldb-dap", workspaceFolder); const log_path = config.get("log-path"); let env: { [key: string]: string } = {}; if (log_path) { env["LLDBDAP_LOG"] = log_path; } const configEnvironment = config.get<{ [key: string]: string }>("environment") || {}; const dapPath = await getDAPExecutable(workspaceFolder, configuration); const dbgOptions = { env: { ...configEnvironment, ...env, }, cwd: configuration.cwd ?? workspaceFolder?.uri.fsPath, }; const dbgArgs = await getDAPArguments(workspaceFolder, configuration); return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions); } /** * This class defines a factory used to find the lldb-dap binary to use * depending on the session configuration. */ export class LLDBDapDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { async createDebugAdapterDescriptor( session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined, ): Promise { if (executable) { throw new Error( "Setting the debug adapter executable in the package.json is not supported.", ); } // Use a server connection if the debugAdapterPort is provided if (session.configuration.debugAdapterPort) { return new vscode.DebugAdapterServer( session.configuration.debugAdapterPort, session.configuration.debugAdapterHostname, ); } return createDebugAdapterExecutable( session.workspaceFolder, session.configuration, ); } }