Skip to content

Commit

Permalink
Enhance dynamic resolution of debugger configuration (#1)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mverzilli authored Jan 23, 2024
1 parent 476ca90 commit d88bc0b
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 80 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
],
"activationEvents": [
"onLanguage:noir",
"onStartupFinished"
"onStartupFinished",
"onDebug"
],
"main": "./out/extension",
"contributes": {
Expand Down
160 changes: 160 additions & 0 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -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<DebugAdapterDescriptor> {
const config = workspace.getConfiguration('noir');

const configuredNargoPath = config.get<string | undefined>('nargoPath');
const nargoPath = configuredNargoPath || findNargo();

return new DebugAdapterExecutable(nargoPath, ['dap']);
}
}

class NoirDebugConfigurationProvider implements DebugConfigurationProvider {
async resolveDebugConfiguration(
_folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
_token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
if (window.activeTextEditor?.document.languageId != 'noir')
return window.showInformationMessage(`Select a Noir file to debug`);

const workspaceConfig = workspace.getConfiguration('noir');
const nargoPath = workspaceConfig.get<string | undefined>('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);
}
58 changes: 3 additions & 55 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
window,
workspace,
commands,
debug,
ExtensionContext,
Disposable,
TextDocument,
Expand All @@ -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<string, Disposable> = new Map();

class NoirDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(
_session: DebugSession,
_executable: DebugAdapterExecutable,
): ProviderResult<DebugAdapterDescriptor> {
const config = workspace.getConfiguration('noir');

const configuredNargoPath = config.get<string | undefined>('nargoPath');
const nargoPath = configuredNargoPath || findNargo();

return new DebugAdapterExecutable(nargoPath, ['dap']);
}
}

const activeMutex: Set<string> = new Set();

function mutex(key: string, fn: (...args: unknown[]) => Promise<void>) {
Expand Down Expand Up @@ -359,40 +336,11 @@ export async function activate(context: ExtensionContext): Promise<void> {
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<void> {
for (const client of lspClients.values()) {
await client.stop();
}
}

class NoirDebugConfigurationProvider implements DebugConfigurationProvider {
resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult<DebugConfiguration> {
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,
}
}
}
45 changes: 21 additions & 24 deletions src/find-nearest-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
}

0 comments on commit d88bc0b

Please sign in to comment.