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"; import { LogFilePathProvider, LogType } from "./logging"; import { expandUser } from "./utils"; 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; } /** * Validates the DAP environment provided in the debug configuration. * It must be a dictionary of string keys and values OR an array of string values. * * @param debugConfigEnv The supposed DAP environment that will be validated * @returns Whether or not the DAP environment is valid */ function validateDAPEnv(debugConfigEnv: any): boolean { // If the env is an object, it should have string values. // The keys are guaranteed to be strings. if ( typeof debugConfigEnv === "object" && Object.values(debugConfigEnv).findIndex( (entry) => typeof entry !== "string", ) !== -1 ) { return false; } // If the env is an array, it should have string values which match the regex. if ( Array.isArray(debugConfigEnv) && debugConfigEnv.findIndex( (entry) => typeof entry !== "string" || !/^\w+(=.*)?$/.test(entry), ) !== -1 ) { return false; } // The env is valid. return true; } /** * 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. let launchConfigPath = configuration["debugAdapterExecutable"]; if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) { launchConfigPath = expandUser(launchConfigPath); 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 = expandUser(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", []); } /** * Retrieves the environment 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 environment that will be provided to lldb-dap */ async function getDAPEnvironment( workspaceFolder: vscode.WorkspaceFolder | undefined, configuration: vscode.DebugConfiguration, ): Promise<{ [key: string]: string }> { const debugConfigEnv = configuration.debugAdapterEnv; if (debugConfigEnv) { if (validateDAPEnv(debugConfigEnv) === false) { throw new ErrorWithNotification( "The debugAdapterEnv property must be a dictionary of string keys and values OR an array of string values. Please update your launch configuration", new ConfigureButton(), ); } // Transform, so that the returned value is always a dictionary. if (Array.isArray(debugConfigEnv)) { const ret: { [key: string]: string } = {}; for (const envVar of debugConfigEnv as string[]) { const equalSignPos = envVar.search("="); if (equalSignPos >= 0) { ret[envVar.substr(0, equalSignPos)] = envVar.substr(equalSignPos + 1); } else { ret[envVar] = ""; } } return ret; } else { return debugConfigEnv; } } const config = vscode.workspace.workspaceFile ? vscode.workspace.getConfiguration("lldb-dap") : vscode.workspace.getConfiguration("lldb-dap", workspaceFolder); return config.get<{ [key: string]: string }>("environment") || {}; } /** * 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 logger The {@link vscode.LogOutputChannel} to log setup diagnostics * @param logFilePath The {@link LogFilePathProvider} for determining where to put session logs * @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( logger: vscode.LogOutputChannel, logFilePath: LogFilePathProvider, 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; } else if ( vscode.workspace .getConfiguration("lldb-dap") .get("captureSessionLogs", false) ) { env["LLDBDAP_LOG"] = logFilePath.get(LogType.DEBUG_SESSION); } const configEnvironment = await getDAPEnvironment( workspaceFolder, configuration, ); const dapPath = await getDAPExecutable(workspaceFolder, configuration); const dbgOptions = { env: { ...configEnvironment, ...env, }, cwd: configuration.cwd ?? workspaceFolder?.uri.fsPath, }; const dbgArgs = await getDAPArguments(workspaceFolder, configuration); logger.info(`lldb-dap path: ${dapPath}`); logger.info(`lldb-dap args: ${dbgArgs}`); logger.info(`cwd: ${dbgOptions.cwd}`); logger.info(`env: ${JSON.stringify(dbgOptions.env)}`); 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 { constructor( private readonly logger: vscode.LogOutputChannel, private logFilePath: LogFilePathProvider, ) { vscode.commands.registerCommand( "lldb-dap.createDebugAdapterDescriptor", ( session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined, ) => this.createDebugAdapterDescriptor(session, executable), ); } async createDebugAdapterDescriptor( session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined, ): Promise { this.logger.info(`Creating debug adapter for session "${session.name}"`); this.logger.info( `Session "${session.name}" debug configuration:\n` + JSON.stringify(session.configuration, undefined, 2), ); if (executable) { const error = new Error( "Setting the debug adapter executable in the package.json is not supported.", ); this.logger.error(error); throw error; } // Use a server connection if the debugAdapterPort is provided if (session.configuration.debugAdapterPort) { this.logger.info( `Spawning debug adapter server on port ${session.configuration.debugAdapterPort}`, ); return new vscode.DebugAdapterServer( session.configuration.debugAdapterPort, session.configuration.debugAdapterHostname, ); } return createDebugAdapterExecutable( this.logger, this.logFilePath, session.workspaceFolder, session.configuration, ); } }