From c1e9d6dc6cb320eaf259953eaef29b052e4bf74d Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Wed, 13 Nov 2024 14:24:53 -0800 Subject: [PATCH] New function to expose more info about an installed package Why? * Sometimes you want to get info about a package version without necessarily challenging the user to install/upgrade. * If you do need to nudge the user, this extra info allows us to build a better message. I also removed the caching, because the extra info makes that increasingly awkward. --- extensions/positron-r/src/session.ts | 152 +++++++++++++++++---------- 1 file changed, 97 insertions(+), 55 deletions(-) diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index 5833ebb90ca..732a6e9ea08 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -22,7 +22,9 @@ import { getPandocPath } from './pandoc'; interface RPackageInstallation { packageName: string; - packageVersion?: string; + packageVersion: string; + checkAgainst: string; + checkResult: boolean; } interface EnvVar { @@ -33,6 +35,7 @@ interface EnvVar { // locale to also be present here, such as LC_CTYPE or LC_TIME. These can vary by OS, so this // interface doesn't attempt to enumerate them. interface Locale { + // eslint-disable-next-line @typescript-eslint/naming-convention LANG: string; [key: string]: string; } @@ -76,9 +79,6 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa /** A timestamp assigned when the session was created. */ private _created: number; - /** Cache for which packages we know are installed in this runtime **/ - private _packageCache = new Array(); - /** The current dynamic runtime state */ public dynState: positron.LanguageRuntimeDynState; @@ -385,71 +385,113 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa } /** - * Checks whether a package is installed in the runtime. - * @param pkgName The name of the package to check - * @param pkgVersion Optionally, the version of the package needed - * @returns true if the package is installed, false otherwise + * Gets information from the runtime about a specific installed package (or maybe not + * installed). + * @param pkgName The name of the package to check. + * @param checkAgainst Optionally, a minimum version to check for. This may seem weird, but we + * need R to compare versions for us. We can't easily do it over here. + * @returns An instance of RPackageInstallation if the package is installed, `null` otherwise. */ - - async checkInstalled(pkgName: string, pkgVersion?: string): Promise { - let isInstalled: boolean; - // Check the cache first - if (this._packageCache.includes({ packageName: pkgName, packageVersion: pkgVersion }) || - (pkgVersion === undefined && this._packageCache.some(p => p.packageName === pkgName))) { - return true; - } + async packageVersion(pkgName: string, checkAgainst?: string): Promise { + let pkg: any; try { - if (pkgVersion) { - isInstalled = await this.callMethod('is_installed', pkgName, pkgVersion); + if (checkAgainst) { + pkg = await this.callMethod('packageVersion', pkgName, checkAgainst); } else { - isInstalled = await this.callMethod('is_installed', pkgName); + pkg = await this.callMethod('packageVersion', pkgName); } } catch (err) { const runtimeError = err as positron.RuntimeMethodError; - throw new Error(`Error checking for package ${pkgName}: ${runtimeError.message} ` + + throw new Error(`Error getting version of package ${pkgName}: ${runtimeError.message} ` + `(${runtimeError.code})`); } - if (!isInstalled) { - const message = pkgVersion ? vscode.l10n.t('Package `{0}` version `{1}` required but not installed.', pkgName, pkgVersion) - : vscode.l10n.t('Package `{0}` required but not installed.', pkgName); - const install = await positron.window.showSimpleModalDialogPrompt( - vscode.l10n.t('Missing R package'), - message, - vscode.l10n.t('Install now') - ); - if (install) { - const id = randomUUID(); - - // A promise that resolves when the runtime is idle: - const promise = new Promise(resolve => { - const disp = this.onDidReceiveRuntimeMessage(runtimeMessage => { - if (runtimeMessage.parent_id === id && - runtimeMessage.type === positron.LanguageRuntimeMessageType.State) { - const runtimeMessageState = runtimeMessage as positron.LanguageRuntimeState; - if (runtimeMessageState.state === positron.RuntimeOnlineState.Idle) { - resolve(); - disp.dispose(); - } - } - }); - }); + if (pkg.version === null) { + return null; + } - this.execute(`install.packages("${pkgName}")`, - id, - positron.RuntimeCodeExecutionMode.Interactive, - positron.RuntimeErrorBehavior.Continue); + const pkgInst: RPackageInstallation = { + packageName: pkgName, + packageVersion: pkg.version, + checkAgainst: pkg.checkAgainst, + checkResult: pkg.checkResult + }; - // Wait for the the runtime to be idle, or for the timeout: - await Promise.race([promise, timeout(2e4, 'waiting for package installation')]); + return pkgInst; + } - return true; - } else { - return false; - } + /** + * Checks whether a package is installed in the runtime, possibly at a minimum version. If not, + * prompts the user to install the package. + * @param pkgName The name of the package to check. + * @param pkgVersion Optionally, the version of the package needed. + * @returns true if the package is installed, false otherwise + */ + + async checkInstalled(pkgName: string, pkgVersion?: string): Promise { + let pkgInst: RPackageInstallation | null = null; + if (pkgVersion) { + pkgInst = await this.packageVersion(pkgName, pkgVersion); + } else { + pkgInst = await this.packageVersion(pkgName); + } + + const isInstalled = pkgInst !== null; + + if (isInstalled && pkgVersion === undefined) { + return true; + } + + const isSufficient = pkgInst?.checkResult ?? false; + + if (isSufficient) { + return true; } - this._packageCache.push({ packageName: pkgName, packageVersion: pkgVersion }); + // either the package is not installed or its version is insufficient + + const title = isInstalled ? vscode.l10n.t('Insufficient package version') : vscode.l10n.t('Missing R package'); + const message = isInstalled ? + vscode.l10n.t( + 'The {0} package is installed at version {1}, but version {2} is required.', + pkgName, pkgInst!.packageVersion, pkgVersion as string + ) : vscode.l10n.t('The {0} package is required, but not installed.', pkgName); + const okButtonTitle = isInstalled ? vscode.l10n.t('Update now') : vscode.l10n.t('Install now'); + + const install = await positron.window.showSimpleModalDialogPrompt( + title, + message, + okButtonTitle + ); + if (!install) { + return false; + } + + const id = randomUUID(); + + // A promise that resolves when the runtime is idle: + const promise = new Promise(resolve => { + const disp = this.onDidReceiveRuntimeMessage(runtimeMessage => { + if (runtimeMessage.parent_id === id && + runtimeMessage.type === positron.LanguageRuntimeMessageType.State) { + const runtimeMessageState = runtimeMessage as positron.LanguageRuntimeState; + if (runtimeMessageState.state === positron.RuntimeOnlineState.Idle) { + resolve(); + disp.dispose(); + } + } + }); + }); + + this.execute(`install.packages("${pkgName}")`, + id, + positron.RuntimeCodeExecutionMode.Interactive, + positron.RuntimeErrorBehavior.Continue); + + // Wait for the the runtime to be idle, or for the timeout: + await Promise.race([promise, timeout(2e4, 'waiting for package installation')]); + return true; + } async isPackageAttached(packageName: string): Promise {