Skip to content

Commit

Permalink
feat: check if latest runner is installed (#1097)
Browse files Browse the repository at this point in the history
Closes #1096

### Summary of Changes

Check whether the latest matching runner version is installed. If it's
not, offer to update it.
  • Loading branch information
lars-reimann authored Apr 24, 2024
1 parent 39d9e5a commit 93432bb
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
UpdateRunnerNotification,
} from '../communication/rpc.js';

const LOWEST_SUPPORTED_RUNNER_VERSION = '0.12.0';
const LOWEST_UNSUPPORTED_RUNNER_VERSION = '0.13.0';
const LOWEST_SUPPORTED_RUNNER_VERSION = '0.13.0';
const LOWEST_UNSUPPORTED_RUNNER_VERSION = '0.14.0';
const npmVersionRange = `>=${LOWEST_SUPPORTED_RUNNER_VERSION} <${LOWEST_UNSUPPORTED_RUNNER_VERSION}`;
export const pipVersionRange = `>=${LOWEST_SUPPORTED_RUNNER_VERSION},<${LOWEST_UNSUPPORTED_RUNNER_VERSION}`;

Expand Down Expand Up @@ -152,21 +152,30 @@ export class SafeDsPythonServer {
this.logger.debug(`Using runner command "${command}".`);

// Check whether the runner command is set properly and get the runner version
let version: string;
let installedVersion: string;
try {
version = await this.getRunnerVersion(command);
this.logger.debug(`Found safe-ds-runner with version "${version}".`);
installedVersion = await this.getInstalledRunnerVersion(command);
this.logger.debug(`Found safe-ds-runner with version "${installedVersion}".`);
} catch (error) {
await this.reportBadRunnerCommand(command, error);
return undefined;
}

// Check whether the runner version is supported
if (!this.isValidVersion(version)) {
await this.reportOutdatedRunner(version);
if (!this.isValidVersion(installedVersion)) {
await this.reportInvalidRunnerVersion(installedVersion);
return undefined;
}

// Check whether a new version of the runner is available
const latestVersion = await this.getLatestMatchingRunnerVersion();
if (latestVersion && semver.gt(latestVersion, installedVersion)) {
if (await this.reportOutdatedRunner(installedVersion, latestVersion)) {
// Abort the start process if the user wants to update the runner
return undefined;
}
}

return command;
}

Expand All @@ -176,7 +185,7 @@ export class SafeDsPythonServer {
* @returns A promise that resolves to the version of the runner if it could be determined, otherwise the promise is
* rejected.
*/
private async getRunnerVersion(command: string): Promise<string> {
private async getInstalledRunnerVersion(command: string): Promise<string> {
const versionProcess = child_process.spawn(command, ['-V']);

return new Promise((resolve, reject) => {
Expand All @@ -195,6 +204,30 @@ export class SafeDsPythonServer {
});
}

/**
* Get the latest version of the runner in the required version range.
*/
private async getLatestMatchingRunnerVersion(): Promise<string | undefined> {
// Get information about `safe-ds-runner` from Pypi
const response = await fetch('https://pypi.org/pypi/safe-ds-runner/json', {
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
this.logger.error(`Could not fetch the latest version of safe-ds-runner: ${response.statusText}`);
return undefined;
}

// Parse the response
try {
const jsonData = await response.json();
const allReleases = Object.keys(jsonData.releases);
return semver.maxSatisfying(allReleases, `>=0.13.0 <0.14.0`) ?? undefined;
} catch (error) {
this.logger.error(`Could not parse the response from PyPI: ${error}`);
return undefined;
}
}

/**
* Check whether the available runner is supported.
*/
Expand Down Expand Up @@ -401,6 +434,9 @@ export class SafeDsPythonServer {

// User interaction ------------------------------------------------------------------------------------------------

/**
* Report to the user that the runner cannot be started with the configured command.
*/
private async reportBadRunnerCommand(command: string, error: unknown): Promise<void> {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Could not start runner with command "${command}": ${message}`);
Expand All @@ -414,7 +450,10 @@ export class SafeDsPythonServer {
}
}

private async reportOutdatedRunner(version: string): Promise<void> {
/**
* Report to the user that the runner version does not match the required version range.
*/
private async reportInvalidRunnerVersion(version: string): Promise<void> {
this.logger.error(`Installed runner version ${version} is not in range "${pipVersionRange}".`);

// Show an error message to the user and offer to update the runner
Expand All @@ -427,6 +466,28 @@ export class SafeDsPythonServer {
}
}

/**
* Report to the user that the installed runner is outdated.
*
* @returns Whether the user decided to update the runner. Returning `true` aborts the start process.
*/
private async reportOutdatedRunner(installedVersion: string, availableVersion: string): Promise<boolean> {
this.logger.info(
`Installed runner version ${installedVersion} is outdated. Latest version is ${availableVersion}.`,
);

// Show an error message to the user and offer to update the runner
const action = await this.messaging.showInformationMessage(`A new version of the runner is available.`, {
title: 'Update runner',
});
if (action?.title === 'Update runner') {
await this.messaging.sendNotification(UpdateRunnerNotification.type);
return true;
} else {
return false;
}
}

// TODO ------------------------------------------------------------------------------------------------------------

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const createRunnerVirtualEnvironment = async (context: ExtensionContext, pythonC

export const installRunnerInVirtualEnvironment = async (pipCommand: string): Promise<void> => {
return new Promise((resolve, reject) => {
const installCommand = `${pipCommand} install "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`;
const installCommand = `${pipCommand} install --upgrade "safe-ds-runner${dependencies['safe-ds-runner'].pipVersionRange}"`;
const process = child_process.spawn(installCommand, { shell: true });

process.stdout.on('data', (data: Buffer) => {
Expand Down

0 comments on commit 93432bb

Please sign in to comment.