diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fc36315..0c76fc6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -16,19 +16,34 @@ "script": "watch", "group": "build", "isBackground": true, - "problemMatcher": ["$esbuild-watch"] + "problemMatcher": [ + "$esbuild-watch" + ] }, { "type": "npm", "script": "compile-web", - "problemMatcher": ["$esbuild-watch"] + "problemMatcher": [ + "$esbuild-watch" + ] }, { "type": "npm", "script": "watch-web", "group": "build", "isBackground": true, - "problemMatcher": ["$ts-webpack-watch"] + "problemMatcher": [ + "$ts-webpack-watch" + ] + }, + { + "type": "deno", + "command": "", + "problemMatcher": [ + "$deno" + ], + "label": "deno task: build", + "detail": "esbuild ./src/extension.ts --bundle --tsconfig=./tsconfig.json --external:vscode --format=cjs --platform=node --outfile=dist/extension.js" } ] } diff --git a/package.json b/package.json index 30917ac..847612e 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ }, "main": "./dist/extension.js", "activationEvents": [ - "onDebug" + "onDebug", + "onStartupFinished" ], "workspaceTrust": { "request": "never" diff --git a/src/domain.ts b/src/domain.ts new file mode 100644 index 0000000..00eda40 --- /dev/null +++ b/src/domain.ts @@ -0,0 +1,32 @@ +const path = require('path'); +import {promises as fs} from 'fs'; + +/** + * @param {string} exe executable name (without extension if on Windows) + * @return {Promise} executable path if found + * */ +async function findExecutable(executableName: string): Promise { + const envPath = process.env.PATH || ''; + const envExt = process.env.PATHEXT || ''; + const pathDirs = envPath.replace(/["]+/g, '').split(path.delimiter).filter(Boolean); + const extensions = envExt.split(';'); + const candidates = pathDirs.flatMap((d) => + extensions.map((ext) => path.join(d, executableName + ext)), + ); + try { + return await Promise.any(candidates.map(checkFileExists)); + } catch (e) { + return null; + } +} + +async function checkFileExists(filePath): Promise { + if ((await fs.stat(filePath)).isFile()) { + return filePath; + } + return null; +} + +export async function probeRsInstalled() { + return await findExecutable('probe-rs'); +} diff --git a/src/extension.ts b/src/extension.ts index 0ea8c78..97266fd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { ProviderResult, WorkspaceFolder, } from 'vscode'; +import {probeRsInstalled} from './domain'; export async function activate(context: vscode.ExtensionContext) { const descriptorFactory = new ProbeRSDebugAdapterServerDescriptorFactory(); @@ -29,9 +30,21 @@ export async function activate(context: vscode.ExtensionContext) { vscode.debug.onDidReceiveDebugSessionCustomEvent( descriptorFactory.receivedCustomEvent.bind(descriptorFactory), ), - vscode.debug.onDidTerminateDebugSession(descriptorFactory.dispose.bind(descriptorFactory)), ); + (async () => { + if (await probeRsInstalled()) { + const resp = await vscode.window.showInformationMessage( + 'probe-rs seems to not be installed. Do you want to install it automatically now?', + 'Install', + ); + + if (resp === 'Install') { + await installProbeRs(); + } + } + })(); + // I cannot find a way to programmatically test for when VSCode is debugging the extension, versus when a user is using the extension to debug their own code, but the following code is useful in the former situation, so I will leave it here to be commented out by extension developers when needed. // const trackerFactory = new ProbeRsDebugAdapterTrackerFactory(); // context.subscriptions.push( @@ -536,6 +549,85 @@ function startDebugServer( }); } +/// Installs probe-rs if it is not present. +function installProbeRs() { + let windows = process.platform === 'win32'; + let done = false; + + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + cancellable: false, + title: 'Installing probe-rs ...', + }, + async (progress) => { + progress.report({increment: 0}); + + const launchedDebugAdapter = childProcess.exec( + windows + ? 'irm https://github.com/probe-rs/probe-rs/releases/download/latest/probe-rs-installer.ps1 | iex' + : "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-installer.sh | sh", + (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`); + done = true; + return; + } + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + }, + ); + + const errorListener = (err: Error) => { + progress.report({ + increment: 100, + message: 'Installation failed. Check the logs for more info.', + }); + done = true; + }; + + const exitListener = (code: number | null, signal: NodeJS.Signals | null) => { + if (code === 0) { + progress.report({ + increment: 100, + message: 'Installation successful.', + }); + done = true; + } else if (signal) { + progress.report({ + increment: 100, + message: 'Installation aborted.', + }); + done = true; + } else { + progress.report({ + increment: 100, + message: 'Installation failed. Check the logs for more info.', + }); + done = true; + } + }; + + launchedDebugAdapter.on('spawn', () => { + // The error listener here is only used for failed spawn, + // so has to be removed afterwards. + launchedDebugAdapter.removeListener('error', errorListener); + }); + + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + launchedDebugAdapter.on('error', errorListener); + launchedDebugAdapter.on('exit', exitListener); + + while (!done) { + await delay(100); + } + + progress.report({increment: 100}); + }, + ); +} + // Get the name of the debugger executable // // This takes the value from configuration, if set, or diff --git a/tsconfig.json b/tsconfig.json index 45ffa0c..fa87d19 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": ["es2021"], "sourceMap": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */,