From fb6e655de8a44c65275ad45a27bf5bd684670ba0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Tue, 17 Mar 2020 12:44:31 +0100 Subject: Rewrite auto-update Everything now happens in main.ts, in the bootstrap family of functions. The current flow is: * check everything only on extension installation. * if the user is on nightly channel, try to download the nightly extension and reload. * when we install nightly extension, we persist its release id, so that we can check if the current release is different. * if server binary was not downloaded by the current version of the extension, redownload it (we persist the version of ext that downloaded the server). --- editors/code/src/main.ts | 158 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 20 deletions(-) (limited to 'editors/code/src/main.ts') diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 94ecd4dab..d907f3e6f 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -1,15 +1,18 @@ import * as vscode from 'vscode'; +import * as path from "path"; +import * as os from "os"; +import { promises as fs } from "fs"; import * as commands from './commands'; import { activateInlayHints } from './inlay_hints'; import { activateStatusDisplay } from './status_display'; import { Ctx } from './ctx'; import { activateHighlighting } from './highlighting'; -import { ensureServerBinary } from './installation/server'; -import { Config } from './config'; -import { log } from './util'; -import { ensureProperExtensionVersion } from './installation/extension'; +import { Config, NIGHTLY_TAG } from './config'; +import { log, assert } from './util'; import { PersistentState } from './persistent_state'; +import { fetchRelease, download } from './net'; +import { spawnSync } from 'child_process'; let ctx: Ctx | undefined; @@ -35,27 +38,14 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(defaultOnEnter); const config = new Config(context); - const state = new PersistentState(context); - - vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error)); - - // Don't await the user response here, otherwise we will block the lsp server bootstrap - void ensureProperExtensionVersion(config, state).catch(log.error); - - const serverPath = await ensureServerBinary(config, state); - - if (serverPath == null) { - throw new Error( - "Rust Analyzer Language Server is not available. " + - "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." - ); - } + const state = new PersistentState(context.globalState); + const serverPath = await bootstrap(config, state); // Note: we try to start the server before we activate type hints so that it // registers its `onDidChangeDocument` handler before us. // // This a horribly, horribly wrong way to deal with this problem. - ctx = await Ctx.create(config, state, context, serverPath); + ctx = await Ctx.create(config, context, serverPath); // Commands which invokes manually via command palette, shortcut, etc. ctx.registerCommand('reload', (ctx) => { @@ -109,3 +99,131 @@ export async function deactivate() { await ctx?.client?.stop(); ctx = undefined; } + +async function bootstrap(config: Config, state: PersistentState): Promise { + await fs.mkdir(config.globalStoragePath, { recursive: true }); + + await bootstrapExtension(config, state); + const path = await bootstrapServer(config, state); + + return path; +} + +async function bootstrapExtension(config: Config, state: PersistentState): Promise { + if (config.channel === "stable") { + if (config.extensionReleaseTag === NIGHTLY_TAG) { + vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension. +To switch to stable, uninstall the extension and re-install it from the marketplace`); + } + return; + }; + + const lastCheck = state.lastCheck; + const now = Date.now(); + + const anHour = 60 * 60 * 1000; + const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour; + + if (!shouldDownloadNightly) return; + + const release = await fetchRelease("nightly").catch((e) => { + log.error(e); + if (state.releaseId === undefined) { // Show error only for the initial download + vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`); + } + return undefined; + }); + if (release === undefined || release.id === state.releaseId) return; + + const userResponse = await vscode.window.showInformationMessage( + "New version of rust-analyzer (nightly) is available (requires reload).", + "Update" + ); + if (userResponse !== "Update") return; + + const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix"); + assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); + + const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); + await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension"); + + await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); + await fs.unlink(dest); + + await state.updateReleaseId(release.id); + await state.updateLastCheck(now); + await vscode.commands.executeCommand("workbench.action.reloadWindow"); +} + +async function bootstrapServer(config: Config, state: PersistentState): Promise { + const path = await getServer(config, state); + if (!path) { + throw new Error( + "Rust Analyzer Language Server is not available. " + + "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." + ); + } + + const res = spawnSync(path, ["--version"], { encoding: 'utf8' }); + log.debug("Checked binary availability via --version", res); + log.debug(res, "--version output:", res.output); + if (res.status !== 0) { + throw new Error( + `Failed to execute ${path} --version` + ); + } + + return path; +} + +async function getServer(config: Config, state: PersistentState): Promise { + const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; + if (explicitPath) { + if (explicitPath.startsWith("~/")) { + return os.homedir() + explicitPath.slice("~".length); + } + return explicitPath; + }; + + let binaryName: string | undefined = undefined; + if (process.arch === "x64" || process.arch === "x32") { + if (process.platform === "linux") binaryName = "rust-analyzer-linux"; + if (process.platform === "darwin") binaryName = "rust-analyzer-mac"; + if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe"; + } + if (binaryName === undefined) { + vscode.window.showErrorMessage( + "Unfortunately we don't ship binaries for your platform yet. " + + "You need to manually clone rust-analyzer repository and " + + "run `cargo xtask install --server` to build the language server from sources. " + + "If you feel that your platform should be supported, please create an issue " + + "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + + "will consider it." + ); + return undefined; + } + + const dest = path.join(config.globalStoragePath, binaryName); + const exists = await fs.stat(dest).then(() => true, () => false); + if (!exists) { + await state.updateServerVersion(undefined); + } + + if (state.serverVersion === config.packageJsonVersion) return dest; + + if (config.askBeforeDownload) { + const userResponse = await vscode.window.showInformationMessage( + `Language server version ${config.packageJsonVersion} for rust-analyzer is not installed.`, + "Download now" + ); + if (userResponse !== "Download now") return dest; + } + + const release = await fetchRelease(config.extensionReleaseTag); + const artifact = release.assets.find(artifact => artifact.name === binaryName); + assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); + + await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 }); + await state.updateServerVersion(config.packageJsonVersion); + return dest; +} -- cgit v1.2.3