From d88bc0b3d9ba9df072f149b02eef2a171113a2bc Mon Sep 17 00:00:00 2001 From: Martin Verzilli Date: Tue, 23 Jan 2024 17:41:56 +0100 Subject: [PATCH] Enhance dynamic resolution of debugger configuration (#1) * Gracefully handle debugger loading/init errors * Refactor: extract debugger functionality to own file * Properly handle launch.json if exists * Add a bit more context on preflight check --- package.json | 3 +- src/debugger.ts | 160 ++++++++++++++++++++++++++++++++++++ src/extension.ts | 58 +------------ src/find-nearest-package.ts | 45 +++++----- 4 files changed, 186 insertions(+), 80 deletions(-) create mode 100644 src/debugger.ts diff --git a/package.json b/package.json index 5108a91..f72833d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ ], "activationEvents": [ "onLanguage:noir", - "onStartupFinished" + "onStartupFinished", + "onDebug" ], "main": "./out/extension", "contributes": { diff --git a/src/debugger.ts b/src/debugger.ts new file mode 100644 index 0000000..6636ce8 --- /dev/null +++ b/src/debugger.ts @@ -0,0 +1,160 @@ +import { + debug, + window, + workspace, + DebugAdapterDescriptorFactory, + DebugSession, + DebugAdapterExecutable, + DebugAdapterDescriptor, + ExtensionContext, + OutputChannel, + DebugConfigurationProvider, + CancellationToken, + DebugConfiguration, + ProviderResult, +} from 'vscode'; + +import { spawn } from 'child_process'; +import findNargo from './find-nargo'; +import findNearestPackageFrom from './find-nearest-package'; + +let outputChannel: OutputChannel; + +export function activateDebugger(context: ExtensionContext) { + outputChannel = window.createOutputChannel('NoirDebugger'); + + context.subscriptions.push( + debug.registerDebugAdapterDescriptorFactory('noir', new NoirDebugAdapterDescriptorFactory()), + debug.registerDebugConfigurationProvider('noir', new NoirDebugConfigurationProvider()), + debug.onDidTerminateDebugSession(() => { + outputChannel.appendLine(`Debug session ended.`); + }), + ); +} + +export class NoirDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory { + async createDebugAdapterDescriptor( + _session: DebugSession, + _executable: DebugAdapterExecutable, + ): ProviderResult { + const config = workspace.getConfiguration('noir'); + + const configuredNargoPath = config.get('nargoPath'); + const nargoPath = configuredNargoPath || findNargo(); + + return new DebugAdapterExecutable(nargoPath, ['dap']); + } +} + +class NoirDebugConfigurationProvider implements DebugConfigurationProvider { + async resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + config: DebugConfiguration, + _token?: CancellationToken, + ): ProviderResult { + if (window.activeTextEditor?.document.languageId != 'noir') + return window.showInformationMessage(`Select a Noir file to debug`); + + const workspaceConfig = workspace.getConfiguration('noir'); + const nargoPath = workspaceConfig.get('nargoPath') || findNargo(); + + const currentFilePath = window.activeTextEditor.document.uri.fsPath; + const projectFolder = + config.projectFolder && config.projectFolder !== `` + ? config.projectFolder + : findNearestPackageFrom(currentFilePath); + + const resolvedConfig = { + type: config.type || 'noir', + name: config.name || 'Noir binary package', + request: 'launch', + program: currentFilePath, + projectFolder, + package: config.package || ``, + proverName: config.proverName || `Prover`, + generateAcir: config.generateAcir || false, + skipInstrumentation: config.skipInstrumentation || false, + }; + + outputChannel.clear(); + + outputChannel.appendLine(`Using nargo at ${nargoPath}`); + outputChannel.appendLine(`Compiling Noir project...`); + outputChannel.appendLine(``); + + // Run Nargo's DAP in "pre-flight mode", which test runs + // the DAP initialization code without actually starting the DAP server. + // This lets us gracefully handle errors that happen *before* + // the DAP loop is established, which otherwise are considered + // "out of band". + // This was necessary due to the VS Code project being reluctant to let extension authors capture + // stderr output generated by a DAP server wrapped in DebugAdapterExecutable. + // + // More details here: https://github.com/microsoft/vscode/issues/108138 + const preflightArgs = [ + 'dap', + '--preflight-check', + '--preflight-project-folder', + resolvedConfig.projectFolder, + '--preflight-prover-name', + resolvedConfig.proverName, + ]; + + if (resolvedConfig.package !== ``) { + preflightArgs.push(`--preflight-package`); + preflightArgs.push(config.package); + } + + if (resolvedConfig.generateAcir) { + preflightArgs.push(`--preflight-generate-acir`); + } + + if (resolvedConfig.skipInstrumentation) { + preflightArgs.push(`--preflight-skip-instrumentation`); + } + + const preflightCheck = spawn(nargoPath, preflightArgs); + + // Create a promise to block until the preflight check child process + // ends. + let ready: (r: boolean) => void; + const preflightCheckMonitor = new Promise((resolve) => (ready = resolve)); + + preflightCheck.stderr.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel)); + preflightCheck.stdout.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel)); + preflightCheck.on('data', (ev_buffer) => preflightCheckPrinter(ev_buffer, outputChannel)); + preflightCheck.on('exit', async (code) => { + if (code !== 0) { + outputChannel.appendLine(`Exited with code ${code}`); + } + ready(code == 0); + }); + + if (!(await preflightCheckMonitor)) { + outputChannel.show(); + throw new Error(`Error launching debugger. Please inspect the Output pane for more details.`); + } else { + outputChannel.appendLine(`Starting debugger session...`); + } + + return resolvedConfig; + } +} + +/** + * Takes stderr or stdout output from the Nargo's DAP + * preflight check and formats it in an Output pane friendly way, + * by removing all special characters. + * + * Note: VS Code's output panes only support plain text. + * + */ +function preflightCheckPrinter(buffer: Buffer, output: OutputChannel) { + const formattedOutput = buffer + .toString() + // eslint-disable-next-line no-control-regex + .replace(/\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '') + .replace(/[^ -~\n\t]/g, ''); + + output.appendLine(formattedOutput); +} diff --git a/src/extension.ts b/src/extension.ts index c035472..79c5f72 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,7 +20,6 @@ import { window, workspace, commands, - debug, ExtensionContext, Disposable, TextDocument, @@ -35,40 +34,18 @@ import { TaskPanelKind, TaskGroup, ProcessExecution, - window, ProgressLocation, - DebugAdapterDescriptorFactory, - DebugConfigurationProvider, - CancellationToken, - DebugConfiguration, - DebugAdapterDescriptor, - DebugAdapterExecutable, - DebugSession, - ProviderResult, } from 'vscode'; +import { activateDebugger } from './debugger'; + import { languageId } from './constants'; import Client from './client'; import findNargo from './find-nargo'; -import findNearestPackageFrom from './find-nearest-package'; import { lspClients, editorLineDecorationManager } from './noir'; const activeCommands: Map = new Map(); -class NoirDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory { - createDebugAdapterDescriptor( - _session: DebugSession, - _executable: DebugAdapterExecutable, - ): ProviderResult { - const config = workspace.getConfiguration('noir'); - - const configuredNargoPath = config.get('nargoPath'); - const nargoPath = configuredNargoPath || findNargo(); - - return new DebugAdapterExecutable(nargoPath, ['dap']); - } -} - const activeMutex: Set = new Set(); function mutex(key: string, fn: (...args: unknown[]) => Promise) { @@ -359,13 +336,7 @@ export async function activate(context: ExtensionContext): Promise { context.subscriptions.push(disposable); } - context.subscriptions.push( - debug.registerDebugAdapterDescriptorFactory('noir', new NoirDebugAdapterDescriptorFactory()), - ); - - context.subscriptions.push( - debug.registerDebugConfigurationProvider('noir', new NoirDebugConfigurationProvider()), - ); + activateDebugger(context); } export async function deactivate(): Promise { @@ -373,26 +344,3 @@ export async function deactivate(): Promise { await client.stop(); } } - -class NoirDebugConfigurationProvider implements DebugConfigurationProvider { - resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult { - if (config.program || config.request == 'attach') - return config; - - if (window.activeTextEditor?.document.languageId != 'noir') - return window.showInformationMessage("Select a Noir file to debug").then(_ => { - return null; - }); - - const currentFilePath = window.activeTextEditor.document.uri.fsPath; - let currentProject = findNearestPackageFrom(currentFilePath); - - return { - type: 'noir', - name: 'Noir binary package', - request: 'launch', - program: currentFilePath, - projectFolder: currentProject, - } - } -} diff --git a/src/find-nearest-package.ts b/src/find-nearest-package.ts index 05b3774..ce5fba0 100644 --- a/src/find-nearest-package.ts +++ b/src/find-nearest-package.ts @@ -5,37 +5,34 @@ import * as fs from 'fs'; /** * Given a program file path, walk up the file system until * finding the nearest a Nargo.toml in a directory that contains - * the program. - * + * the program. + * * To reduce the odds of accidentally choosing the wrong Nargo package, - * end the walk at the root of the current VS Code open files. + * end the walk at the root of the current VS Code open files. */ export default function findNearestPackageFolder(program: string): string { - const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { - throw new Error("No workspace is currently open in VS Code."); - } + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + throw new Error(`No workspace is currently open in VS Code.`); + } - const workspaceRoots = workspaceFolders.map(wf => wf.uri.fsPath); + const workspaceRoots = workspaceFolders.map((wf) => wf.uri.fsPath); - let currentFolder = path.dirname(program); + let currentFolder = path.dirname(program); - try { - while ( - currentFolder !== path.dirname(currentFolder) && - !workspaceRoots.includes(currentFolder) - ) { - const maybeNargoProject = path.join(currentFolder, 'Nargo.toml'); + try { + while (currentFolder !== path.dirname(currentFolder) && !workspaceRoots.includes(currentFolder)) { + const maybeNargoProject = path.join(currentFolder, 'Nargo.toml'); - if (fs.existsSync(maybeNargoProject)) { - return currentFolder; - } + if (fs.existsSync(maybeNargoProject)) { + return currentFolder; + } - currentFolder = path.dirname(currentFolder); - } - } catch (error) { - throw new Error("Could not find a Nargo package associated to this file."); - } + currentFolder = path.dirname(currentFolder); + } + } catch (error) { + throw new Error(`Could not find a Nargo package associated to this file.`); + } - throw new Error("Could not find a Nargo package associated to this file."); + throw new Error(`Could not find a Nargo package associated to this file.`); }