diff --git a/packages/safe-ds-lang/src/language/index.ts b/packages/safe-ds-lang/src/language/index.ts index c93a14e87..f684bf919 100644 --- a/packages/safe-ds-lang/src/language/index.ts +++ b/packages/safe-ds-lang/src/language/index.ts @@ -1,4 +1,10 @@ -import { pipVersionRange, RPC_RUNNER_INSTALL, RPC_RUNNER_START, RPC_RUNNER_STARTED } from './runner/safe-ds-runner.js'; +import { + pipVersionRange, + RPC_RUNNER_INSTALL, + RPC_RUNNER_START, + RPC_RUNNER_STARTED, + RPC_RUNNER_UPDATE, +} from './runner/safe-ds-runner.js'; // Services export type { SafeDsServices } from './safe-ds-module.js'; @@ -25,6 +31,7 @@ export const rpc = { runnerInstall: RPC_RUNNER_INSTALL, runnerStart: RPC_RUNNER_START, runnerStarted: RPC_RUNNER_STARTED, + runnerUpdate: RPC_RUNNER_UPDATE, }; // Dependencies diff --git a/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts b/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts index 5c2ee5748..1ea0d2705 100644 --- a/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts +++ b/packages/safe-ds-lang/src/language/runner/safe-ds-runner.ts @@ -25,6 +25,7 @@ import { SafeDsMessagingProvider } from '../../communication/safe-ds-messaging-p export const RPC_RUNNER_INSTALL = 'runner/install'; export const RPC_RUNNER_START = 'runner/start'; export const RPC_RUNNER_STARTED = 'runner/started'; +export const RPC_RUNNER_UPDATE = 'runner/update'; const LOWEST_SUPPORTED_RUNNER_VERSION = '0.10.0'; const LOWEST_UNSUPPORTED_RUNNER_VERSION = '0.11.0'; @@ -174,9 +175,13 @@ export class SafeDsRunner { const versionString = await this.getPythonServerVersion(pythonServerTest); if (!semver.satisfies(versionString, npmVersionRange)) { this.error(`Installed runner version ${versionString} does not meet requirements: ${pipVersionRange}`); - this.messaging.showErrorMessage( + const action = await this.messaging.showErrorMessage( `The installed runner version ${versionString} is not compatible with this version of the extension. The installed version should match these requirements: ${pipVersionRange}. Please update to a matching version.`, + { title: 'Update runner' }, ); + if (action?.title === 'Update runner') { + await this.messaging.sendNotification(RPC_RUNNER_UPDATE); + } return; } else { this.info(`Using safe-ds-runner version: ${versionString}`); diff --git a/packages/safe-ds-vscode/package.json b/packages/safe-ds-vscode/package.json index a0f86d6f6..40ac1f56a 100644 --- a/packages/safe-ds-vscode/package.json +++ b/packages/safe-ds-vscode/package.json @@ -198,6 +198,11 @@ "title": "Run Pipeline", "category": "Safe-DS", "icon": "$(play)" + }, + { + "command": "safe-ds.updateRunner", + "title": "Update the Safe-DS Runner", + "category": "Safe-DS" } ], "snippets": [ diff --git a/packages/safe-ds-vscode/src/extension/commands/installRunner.ts b/packages/safe-ds-vscode/src/extension/commands/installRunner.ts index 621fcc1c9..6cc8a4563 100644 --- a/packages/safe-ds-vscode/src/extension/commands/installRunner.ts +++ b/packages/safe-ds-vscode/src/extension/commands/installRunner.ts @@ -3,7 +3,6 @@ import child_process from 'node:child_process'; import semver from 'semver'; import { dependencies, rpc, SafeDsServices } from '@safe-ds/lang'; import { logError, printOutputMessage } from '../output.js'; -import fs from 'node:fs'; import { LanguageClient } from 'vscode-languageclient/node.js'; const pythonCommandCandidates = ['python3', 'python', 'py']; @@ -21,11 +20,9 @@ export const installRunner = (context: ExtensionContext, client: LanguageClient, } // Install the runner if it is not already installed - if (!fs.existsSync(getRunnerCommand(context))) { - const success = await doInstallRunner(context); - if (!success) { - return; - } + const success = await doInstallRunner(context); + if (!success) { + return; } // Set the runner command in the configuration @@ -82,7 +79,7 @@ const doInstallRunner = async (context: ExtensionContext): Promise => { }, async () => { try { - await installRunnerInVirtualEnvironment(context); + await installRunnerInVirtualEnvironment(getPipCommand(context)); return true; } catch (error) { vscode.window.showErrorMessage('Failed to install the runner.'); @@ -131,9 +128,9 @@ const createRunnerVirtualEnvironment = async (context: ExtensionContext, pythonC }); }; -const installRunnerInVirtualEnvironment = async (context: ExtensionContext): Promise => { +export const installRunnerInVirtualEnvironment = async (pipCommand: string): Promise => { return new Promise((resolve, reject) => { - const installCommand = `${getPipCommand(context)} install "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`; + const installCommand = `${pipCommand} install "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`; const process = child_process.spawn(installCommand, { shell: true }); process.stdout.on('data', (data: Buffer) => { diff --git a/packages/safe-ds-vscode/src/extension/commands/updateRunner.ts b/packages/safe-ds-vscode/src/extension/commands/updateRunner.ts new file mode 100644 index 000000000..0bc44b388 --- /dev/null +++ b/packages/safe-ds-vscode/src/extension/commands/updateRunner.ts @@ -0,0 +1,82 @@ +import vscode, { ExtensionContext } from 'vscode'; +import { LanguageClient } from 'vscode-languageclient/node.js'; +import { rpc, SafeDsServices } from '@safe-ds/lang'; +import fs from 'node:fs'; +import path from 'node:path'; +import { installRunner, installRunnerInVirtualEnvironment } from './installRunner.js'; +import { platform } from 'node:os'; +import { logError } from '../output.js'; + +export const updateRunner = (context: ExtensionContext, client: LanguageClient, services: SafeDsServices) => { + return async () => { + // If the runner is already started, do nothing + if (services.runtime.Runner.isPythonServerAvailable()) { + vscode.window.showInformationMessage('The runner is already installed and running.'); + return; + } + + // If the runner executable cannot be found at all, install it from scratch + if (!fs.existsSync(await getRunnerCommand())) { + await installRunner(context, client, services)(); + return; + } + + // Update the runner if it is already installed + const success = await doUpdateRunner(); + if (!success) { + return; + } + + // Start the runner (needed if the configuration did not change, so no event is fired) + await client.sendNotification(rpc.runnerStart); + + // Inform the user + vscode.window.showInformationMessage('The runner has been updated successfully.'); + }; +}; + +const doUpdateRunner = async (): Promise => { + // Check if pip is available + const pipCommand = await getPipCommand(); + if (!pipCommand) { + vscode.window.showErrorMessage('Failed to find pip.'); + logError('Failed to find pip.'); + return false; + } + + // Install the runner in the virtual environment + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'Installing the runner (this may take a few minutes)...', + }, + async () => { + try { + await installRunnerInVirtualEnvironment(pipCommand); + return true; + } catch (error) { + vscode.window.showErrorMessage('Failed to install the runner.'); + logError(String(error)); + return false; + } + }, + ); +}; + +const getRunnerCommand = async (): Promise => { + return vscode.workspace.getConfiguration('safe-ds.runner').get('command') ?? ''; +}; + +const getPipCommand = async (): Promise => { + const runnerCommand = await getRunnerCommand(); + if (!runnerCommand) { + return; + } + + const runnerDir = path.dirname(runnerCommand); + if (platform() === 'win32') { + return path.join(runnerDir, 'pip.exe'); + } else { + return path.join(runnerDir, 'pip'); + } +}; diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index 63a942667..6e43de7e6 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -13,6 +13,7 @@ import { dumpDiagnostics } from './commands/dumpDiagnostics.js'; import { openDiagnosticsDumps } from './commands/openDiagnosticsDumps.js'; import { isSdsPlaceholder, SdsPipeline } from '../../../safe-ds-lang/src/language/generated/ast.js'; import { installRunner } from './commands/installRunner.js'; +import { updateRunner } from './commands/updateRunner.js'; let client: LanguageClient; let services: SafeDsServices; @@ -51,6 +52,9 @@ const registerNotificationListeners = function (context: vscode.ExtensionContext client.onNotification(rpc.runnerStarted, async (port: number) => { await services.runtime.Runner.connectToPort(port); }); + client.onNotification(rpc.runnerUpdate, async () => { + await updateRunner(context, client, services)(); + }); }; // This function is called when the extension is deactivated. @@ -106,6 +110,9 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('safe-ds.openDiagnosticsDumps', openDiagnosticsDumps(context)), ); + context.subscriptions.push( + vscode.commands.registerCommand('safe-ds.updateRunner', updateRunner(context, client, services)), + ); context.subscriptions.push(vscode.commands.registerCommand('safe-ds.runPipelineFile', commandRunPipelineFile)); context.subscriptions.push(