From 8203828bb081faae4cd9d39c8abe6bc073138176 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:54:40 +0200 Subject: [PATCH 01/25] vscode-prerefactor: merge two files into downloads.ts --- .../src/installation/download_artifact.ts | 50 ------------------- .../{download_file.ts => downloads.ts} | 46 +++++++++++++++++ 2 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 editors/code/src/installation/download_artifact.ts rename editors/code/src/installation/{download_file.ts => downloads.ts} (51%) diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts deleted file mode 100644 index 97e4d67c21d..00000000000 --- a/editors/code/src/installation/download_artifact.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as vscode from "vscode"; -import * as path from "path"; -import { promises as fs } from "fs"; - -import { ArtifactReleaseInfo } from "./interfaces"; -import { downloadFile } from "./download_file"; -import { assert } from "../util"; - -/** - * Downloads artifact from given `downloadUrl`. - * Creates `installationDir` if it is not yet created and put the artifact under - * `artifactFileName`. - * Displays info about the download progress in an info message printing the name - * of the artifact as `displayName`. - */ -export async function downloadArtifact( - { downloadUrl, releaseName }: ArtifactReleaseInfo, - artifactFileName: string, - installationDir: string, - displayName: string, -) { - await fs.mkdir(installationDir).catch(err => assert( - err?.code === "EEXIST", - `Couldn't create directory "${installationDir}" to download ` + - `${artifactFileName} artifact: ${err?.message}` - )); - - const installationPath = path.join(installationDir, artifactFileName); - - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, // FIXME: add support for canceling download? - title: `Downloading ${displayName} (${releaseName})` - }, - async (progress, _cancellationToken) => { - let lastPrecentage = 0; - const filePermissions = 0o755; // (rwx, r_x, r_x) - await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => { - const newPercentage = (readBytes / totalBytes) * 100; - progress.report({ - message: newPercentage.toFixed(0) + "%", - increment: newPercentage - lastPrecentage - }); - - lastPrecentage = newPercentage; - }); - } - ); -} diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/downloads.ts similarity index 51% rename from editors/code/src/installation/download_file.ts rename to editors/code/src/installation/downloads.ts index ee8949d61c4..7ce2e2960f2 100644 --- a/editors/code/src/installation/download_file.ts +++ b/editors/code/src/installation/downloads.ts @@ -1,8 +1,11 @@ import fetch from "node-fetch"; +import * as vscode from "vscode"; +import * as path from "path"; import * as fs from "fs"; import * as stream from "stream"; import * as util from "util"; import { log, assert } from "../util"; +import { ArtifactReleaseInfo } from "./interfaces"; const pipeline = util.promisify(stream.pipeline); @@ -49,3 +52,46 @@ export async function downloadFile( // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 }); } + +/** + * Downloads artifact from given `downloadUrl`. + * Creates `installationDir` if it is not yet created and puts the artifact under + * `artifactFileName`. + * Displays info about the download progress in an info message printing the name + * of the artifact as `displayName`. + */ +export async function downloadArtifactWithProgressUi( + { downloadUrl, releaseName }: ArtifactReleaseInfo, + artifactFileName: string, + installationDir: string, + displayName: string, +) { + await fs.promises.mkdir(installationDir).catch(err => assert( + err?.code === "EEXIST", + `Couldn't create directory "${installationDir}" to download ` + + `${artifactFileName} artifact: ${err?.message}` + )); + + const installationPath = path.join(installationDir, artifactFileName); + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, // FIXME: add support for canceling download? + title: `Downloading rust-analyzer ${displayName} (${releaseName})` + }, + async (progress, _cancellationToken) => { + let lastPrecentage = 0; + const filePermissions = 0o755; // (rwx, r_x, r_x) + await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => { + const newPercentage = (readBytes / totalBytes) * 100; + progress.report({ + message: newPercentage.toFixed(0) + "%", + increment: newPercentage - lastPrecentage + }); + + lastPrecentage = newPercentage; + }); + } + ); +} From 98b2a942d1c67f80a67a5779ecaa482f84c3a30d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:54:54 +0200 Subject: [PATCH 02/25] vscode-prerefactor: add some utility functions --- editors/code/src/util.ts | 69 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 95a5f1227ca..2bfc145e6fe 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -1,5 +1,6 @@ import * as lc from "vscode-languageclient"; import * as vscode from "vscode"; +import { promises as dns } from "dns"; import { strict as nativeAssert } from "assert"; export function assert(condition: boolean, explanation: string): asserts condition { @@ -11,21 +12,40 @@ export function assert(condition: boolean, explanation: string): asserts conditi } } -export const log = { - enabled: true, +export const log = new class { + private enabled = true; + + setEnabled(yes: boolean): void { + log.enabled = yes; + } + debug(message?: any, ...optionalParams: any[]): void { if (!log.enabled) return; // eslint-disable-next-line no-console console.log(message, ...optionalParams); - }, + } + error(message?: any, ...optionalParams: any[]): void { if (!log.enabled) return; debugger; // eslint-disable-next-line no-console console.error(message, ...optionalParams); - }, - setEnabled(yes: boolean): void { - log.enabled = yes; + } + + downloadError(err: Error, artifactName: string, repoName: string) { + vscode.window.showErrorMessage( + `Failed to download the rust-analyzer ${artifactName} from ${repoName} ` + + `GitHub repository: ${err.message}` + ); + log.error(err); + dns.resolve('example.com').then( + addrs => log.debug("DNS resolution for example.com was successful", addrs), + err => log.error( + "DNS resolution for example.com failed, " + + "there might be an issue with Internet availability", + err + ) + ); } }; @@ -66,6 +86,17 @@ function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } +export function notReentrant( + fn: (this: TThis, ...params: TParams) => Promise +): typeof fn { + let entered = false; + return function(...params) { + assert(!entered, `Reentrancy invariant for ${fn.name} is violated`); + entered = true; + return fn.apply(this, params).finally(() => entered = false); + }; +} + export type RustDocument = vscode.TextDocument & { languageId: "rust" }; export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; @@ -79,3 +110,29 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { return isRustDocument(editor.document); } + +/** + * @param extensionId The canonical extension identifier in the form of: `publisher.name` + */ +export async function vscodeReinstallExtension(extensionId: string) { + // Unfortunately there is no straightforward way as of now, these commands + // were found in vscode source code. + + log.debug("Uninstalling extension", extensionId); + await vscode.commands.executeCommand("workbench.extensions.uninstallExtension", extensionId); + log.debug("Installing extension", extensionId); + await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId); +} + +export async function vscodeReloadWindow(): Promise { + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + + assert(false, "unreachable"); +} + +export async function vscodeInstallExtensionFromVsix(vsixPath: string) { + await vscode.commands.executeCommand( + "workbench.extensions.installExtension", + vscode.Uri.file(vsixPath) + ); +} From 0f826aec8280cf1593e1b1e265cced6f7e5d84d7 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:55:26 +0200 Subject: [PATCH 03/25] vscode: get release date from release info --- .../fetch_artifact_release_info.ts | 3 +++ editors/code/src/installation/interfaces.ts | 18 ++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts index b1b5a3485cc..1ad3b8338ba 100644 --- a/editors/code/src/installation/fetch_artifact_release_info.ts +++ b/editors/code/src/installation/fetch_artifact_release_info.ts @@ -59,12 +59,15 @@ export async function fetchArtifactReleaseInfo( return { releaseName: release.name, + releaseDate: new Date(release.published_at), downloadUrl: artifact.browser_download_url }; // We omit declaration of tremendous amount of fields that we are not using here interface GithubRelease { name: string; + // eslint-disable-next-line camelcase + published_at: string; assets: Array<{ name: string; // eslint-disable-next-line camelcase diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts index 50b63592186..1a8ea0884cb 100644 --- a/editors/code/src/installation/interfaces.ts +++ b/editors/code/src/installation/interfaces.ts @@ -1,5 +1,3 @@ -import * as vscode from "vscode"; - export interface GithubRepo { name: string; owner: string; @@ -9,6 +7,7 @@ export interface GithubRepo { * Metadata about particular artifact retrieved from GitHub releases. */ export interface ArtifactReleaseInfo { + releaseDate: Date; releaseName: string; downloadUrl: string; } @@ -42,6 +41,9 @@ export namespace ArtifactSource { */ repo: GithubRepo; + + // FIXME: add installationPath: string; + /** * Directory on the filesystem where the bundled binary is stored. */ @@ -57,17 +59,5 @@ export namespace ArtifactSource { * Tag of github release that denotes a version required by this extension. */ tag: string; - - /** - * Object that provides `get()/update()` operations to store metadata - * about the actual binary, e.g. its actual version. - */ - storage: vscode.Memento; - - /** - * Ask for the user permission before downloading the artifact. - */ - askBeforeDownload: boolean; } - } From bc98c02dd0cdc33e3b34c0054c1570702e198d9b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:56:51 +0200 Subject: [PATCH 04/25] vscode: prepare package.json for nightlies --- editors/code/package-lock.json | 2 +- editors/code/package.json | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index b0796454618..575dc7c4a1f 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -1,6 +1,6 @@ { "name": "rust-analyzer", - "version": "0.2.20200211-dev", + "version": "0.2.20200309-nightly", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/editors/code/package.json b/editors/code/package.json index 3aaae357a9b..faf10528d53 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -6,7 +6,7 @@ "private": true, "icon": "icon.png", "//": "The real version is in release.yaml, this one just needs to be bigger", - "version": "0.2.20200211-dev", + "version": "0.2.20200309-nightly", "publisher": "matklad", "repository": { "url": "https://github.com/rust-analyzer/rust-analyzer.git", @@ -219,6 +219,19 @@ } } }, + "rust-analyzer.updates.channel": { + "type": "string", + "enum": [ + "stable", + "nightly" + ], + "default": "stable", + "markdownEnumDescriptions": [ + "`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general", + "`\"nightly\"` updates are shipped daily, they contain cutting-edge features and latest bug fixes. These releases help us get your feedback very quickly and speed up rust-analyzer development **drastically**" + ], + "markdownDescription": "Choose `\"nightly\"` updates to get the latest features and bug fixes every day. While `\"stable\"` releases occur weekly and don't contain cutting-edge features from VSCode proposed APIs" + }, "rust-analyzer.updates.askBeforeDownload": { "type": "boolean", "default": true, @@ -235,7 +248,7 @@ "string" ], "default": null, - "description": "Path to rust-analyzer executable (points to bundled binary by default)" + "description": "Path to rust-analyzer executable (points to bundled binary by default). If this is set, then \"rust-analyzer.updates.channel\" setting is not used" }, "rust-analyzer.excludeGlobs": { "type": "array", From 6d2d75367763686286779dd4b595a575c6ea689e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:57:13 +0200 Subject: [PATCH 05/25] vscode: prepare config for nightlies --- editors/code/src/config.ts | 111 ++++++++++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 6db073bec02..e2b0f6f8470 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -23,22 +23,36 @@ export interface CargoFeatures { allFeatures: boolean; features: string[]; } + +export const enum UpdatesChannel { + Stable = "stable", + Nightly = "nightly" +} + +export const NIGHTLY_TAG = "nightly"; export class Config { - private static readonly rootSection = "rust-analyzer"; - private static readonly requiresReloadOpts = [ + readonly extensionId = "matklad.rust-analyzer"; + + private readonly rootSection = "rust-analyzer"; + private readonly requiresReloadOpts = [ "cargoFeatures", "cargo-watch", "highlighting.semanticTokens", "inlayHints", ] - .map(opt => `${Config.rootSection}.${opt}`); + .map(opt => `${this.rootSection}.${opt}`); - private static readonly extensionVersion: string = (() => { + /** + * Either `nightly` or `YYYY-MM-DD` (i.e. `stable` release) + */ + private readonly extensionVersion: string = (() => { const packageJsonVersion = vscode .extensions - .getExtension("matklad.rust-analyzer")! + .getExtension(this.extensionId)! .packageJSON - .version as string; // n.n.YYYYMMDD + .version as string; + + if (packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG; const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; @@ -54,7 +68,7 @@ export class Config { } private refreshConfig() { - this.cfg = vscode.workspace.getConfiguration(Config.rootSection); + this.cfg = vscode.workspace.getConfiguration(this.rootSection); const enableLogging = this.cfg.get("trace.extension") as boolean; log.setEnabled(enableLogging); log.debug("Using configuration:", this.cfg); @@ -63,7 +77,7 @@ export class Config { private async onConfigChange(event: vscode.ConfigurationChangeEvent) { this.refreshConfig(); - const requiresReloadOpt = Config.requiresReloadOpts.find( + const requiresReloadOpt = this.requiresReloadOpts.find( opt => event.affectsConfiguration(opt) ); @@ -121,8 +135,16 @@ export class Config { } } + get installedExtensionUpdateChannel() { + if (this.serverPath !== null) return null; + + return this.extensionVersion === NIGHTLY_TAG + ? UpdatesChannel.Nightly + : UpdatesChannel.Stable; + } + get serverSource(): null | ArtifactSource { - const serverPath = RA_LSP_DEBUG ?? this.cfg.get("serverPath"); + const serverPath = RA_LSP_DEBUG ?? this.serverPath; if (serverPath) { return { @@ -135,23 +157,47 @@ export class Config { if (!prebuiltBinaryName) return null; + return this.createGithubReleaseSource( + prebuiltBinaryName, + this.extensionVersion + ); + } + + private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease { return { type: ArtifactSource.Type.GithubRelease, + file, + tag, dir: this.ctx.globalStoragePath, - file: prebuiltBinaryName, - storage: this.ctx.globalState, - tag: Config.extensionVersion, - askBeforeDownload: this.cfg.get("updates.askBeforeDownload") as boolean, repo: { name: "rust-analyzer", - owner: "rust-analyzer", + owner: "rust-analyzer" } - }; + } } + get nightlyVsixSource(): ArtifactSource.GithubRelease { + return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG); + } + + readonly installedNightlyExtensionReleaseDate = new DateStorage( + "rust-analyzer-installed-nightly-extension-release-date", + this.ctx.globalState + ); + readonly serverReleaseDate = new DateStorage( + "rust-analyzer-server-release-date", + this.ctx.globalState + ); + readonly serverReleaseTag = new StringStorage( + "rust-analyzer-release-tag", this.ctx.globalState + ); + // We don't do runtime config validation here for simplicity. More on stackoverflow: // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension + private get serverPath() { return this.cfg.get("serverPath") as null | string; } + get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } + get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } get rainbowHighlightingOn() { return this.cfg.get("rainbowHighlightingOn") as boolean; } @@ -189,3 +235,38 @@ export class Config { // for internal use get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } } + +export class StringStorage { + constructor( + private readonly key: string, + private readonly storage: vscode.Memento + ) {} + + get(): null | string { + const tag = this.storage.get(this.key, null); + log.debug(this.key, "==", tag); + return tag; + } + async set(tag: string) { + log.debug(this.key, "=", tag); + await this.storage.update(this.key, tag); + } +} +export class DateStorage { + + constructor( + private readonly key: string, + private readonly storage: vscode.Memento + ) {} + + get(): null | Date { + const date = this.storage.get(this.key, null); + log.debug(this.key, "==", date); + return date ? new Date(date) : null; + } + + async set(date: null | Date) { + log.debug(this.key, "=", date); + await this.storage.update(this.key, date); + } +} From 601fc9d1abf52c16356d49b6c540b31718e62b88 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:57:34 +0200 Subject: [PATCH 06/25] vscode: add nightly extension installation logic --- editors/code/src/installation/extension.ts | 131 +++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 editors/code/src/installation/extension.ts diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts new file mode 100644 index 00000000000..7709cd3cd3e --- /dev/null +++ b/editors/code/src/installation/extension.ts @@ -0,0 +1,131 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { promises as fs } from 'fs'; + +import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util"; +import { Config, UpdatesChannel } from "../config"; +import { ArtifactReleaseInfo } from "./interfaces"; +import { downloadArtifactWithProgressUi } from "./downloads"; +import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; + +const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; + +/** + * Installs `stable` or latest `nightly` version or does nothing if the current + * extension version is what's needed according to `desiredUpdateChannel`. + */ +export async function ensureProperExtensionVersion(config: Config): Promise { + const currentUpdChannel = config.installedExtensionUpdateChannel; + const desiredUpdChannel = config.updatesChannel; + + if (currentUpdChannel === UpdatesChannel.Stable) { + // Release date is present only when we are on nightly + config.installedNightlyExtensionReleaseDate.set(null); + } + + // User has built lsp server from sources, she should manage updates manually + if (currentUpdChannel === null) return; + + if (desiredUpdChannel === UpdatesChannel.Stable) { + // VSCode should handle updates for stable channel + if (currentUpdChannel === UpdatesChannel.Stable) return; + + if (!await askToDownloadProperExtensionVersion(config)) return; + + await vscodeReinstallExtension(config.extensionId); + await vscodeReloadWindow(); // never returns + } + + if (currentUpdChannel === UpdatesChannel.Stable) { + if (!await askToDownloadProperExtensionVersion(config)) return; + + return await tryDownloadNightlyExtension(config); + } + + const currentExtReleaseDate = config.installedNightlyExtensionReleaseDate.get(); + + assert(currentExtReleaseDate !== null, "nightly release date must've been set during installation"); + + const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, new Date()); + log.debug(`Current rust-analyzer nightly was downloaded ${hoursSinceLastUpdate} hours ago`); + + if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) { + return; + } + if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) { + return; + } + + await tryDownloadNightlyExtension(config, releaseInfo => { + assert( + currentExtReleaseDate === config.installedNightlyExtensionReleaseDate.get(), + "Other active VSCode instance has reinstalled the extension" + ); + + if (releaseInfo.releaseDate === currentExtReleaseDate) { + vscode.window.showInformationMessage( + "Whoops, it appears that your nightly version is up-to-date. " + + "There might be some problems with the upcomming nightly release " + + "or you traveled too far into the future. Sorry for that 😅! " + ); + return false; + } + return true; + }); +} + +async function askToDownloadProperExtensionVersion(config: Config, reason = "") { + if (!config.askBeforeDownload) return true; + + const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly"; + + // In case of reentering this function and showing the same info message + // (e.g. after we had shown this message, the user changed the config) + // vscode will dismiss the already shown one (i.e. return undefined). + // This behaviour is what we want, but likely it is not documented + + const userResponse = await vscode.window.showInformationMessage( + reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` + + `version and reload the window now?`, + "Download now", "Cancel" + ); + log.debug("Response: ", userResponse); + return userResponse === "Download now"; +} + +/** + * Shutdowns the process in case of success (i.e. reloads the window) or throws an error. + */ +const tryDownloadNightlyExtension = notReentrant(async function tryDownloadNightlyExtension( + config: Config, + shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true +): Promise { + const vsixSource = config.nightlyVsixSource; + try { + const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag); + + if (!shouldDownload(releaseInfo)) return; + + await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension"); + + const vsixPath = path.join(vsixSource.dir, vsixSource.file); + + await vscodeInstallExtensionFromVsix(vsixPath) + await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate); + await fs.unlink(vsixPath); + + await vscodeReloadWindow(); // never returns + } catch (err) { + log.downloadError(err, "nightly extension", vsixSource.repo.name); + } +}); + +function diffInHours(a: Date, b: Date): number { + // Discard the time and time-zone information (to abstract from daylight saving time bugs) + // https://stackoverflow.com/a/15289883/9259330 + + const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return (utcA - utcB) / (1000 * 60 * 60); +} From 1e73811fbe634efec90a3e009a84fd8dda9f5697 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 19:57:55 +0200 Subject: [PATCH 07/25] vscode: amend server installation logic to account for nightlies --- editors/code/src/commands/server_version.ts | 3 +- editors/code/src/installation/server.ts | 97 ++++++++++----------- editors/code/src/main.ts | 9 +- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts index 421301b42cd..c4d84b4439e 100644 --- a/editors/code/src/commands/server_version.ts +++ b/editors/code/src/commands/server_version.ts @@ -5,7 +5,7 @@ import { spawnSync } from 'child_process'; export function serverVersion(ctx: Ctx): Cmd { return async () => { - const binaryPath = await ensureServerBinary(ctx.config.serverSource); + const binaryPath = await ensureServerBinary(ctx.config); if (binaryPath == null) { throw new Error( @@ -18,4 +18,3 @@ export function serverVersion(ctx: Ctx): Cmd { vscode.window.showInformationMessage('rust-analyzer version : ' + version); }; } - diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts index ef1c45ff659..345f30d4763 100644 --- a/editors/code/src/installation/server.ts +++ b/editors/code/src/installation/server.ts @@ -1,14 +1,16 @@ import * as vscode from "vscode"; import * as path from "path"; -import { promises as dns } from "dns"; import { spawnSync } from "child_process"; import { ArtifactSource } from "./interfaces"; import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; -import { downloadArtifact } from "./download_artifact"; +import { downloadArtifactWithProgressUi } from "./downloads"; import { log, assert } from "../util"; +import { Config, NIGHTLY_TAG } from "../config"; + +export async function ensureServerBinary(config: Config): Promise { + const source = config.serverSource; -export async function ensureServerBinary(source: null | ArtifactSource): Promise { if (!source) { vscode.window.showErrorMessage( "Unfortunately we don't ship binaries for your platform yet. " + @@ -35,18 +37,11 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise return null; } case ArtifactSource.Type.GithubRelease: { - const prebuiltBinaryPath = path.join(source.dir, source.file); - - const installedVersion: null | string = getServerVersion(source.storage); - const requiredVersion: string = source.tag; - - log.debug("Installed version:", installedVersion, "required:", requiredVersion); - - if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion === requiredVersion) { - return prebuiltBinaryPath; + if (!shouldDownloadServer(source, config)) { + return path.join(source.dir, source.file); } - if (source.askBeforeDownload) { + if (config.askBeforeDownload) { const userResponse = await vscode.window.showInformationMessage( `Language server version ${source.tag} for rust-analyzer is not installed. ` + "Do you want to download it now?", @@ -55,38 +50,53 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise if (userResponse !== "Download now") return null; } - if (!await downloadServer(source)) return null; - - return prebuiltBinaryPath; + return await downloadServer(source, config); } } } -async function downloadServer(source: ArtifactSource.GithubRelease): Promise { +function shouldDownloadServer( + source: ArtifactSource.GithubRelease, + config: Config +): boolean { + if (!isBinaryAvailable(path.join(source.dir, source.file))) return true; + + const installed = { + tag: config.serverReleaseTag.get(), + date: config.serverReleaseDate.get() + }; + const required = { + tag: source.tag, + date: config.installedNightlyExtensionReleaseDate.get() + }; + + log.debug("Installed server:", installed, "required:", required); + + if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) { + return required.tag !== installed.tag; + } + + assert(required.date !== null, "Extension release date should have been saved during its installation"); + assert(installed.date !== null, "Server release date should have been saved during its installation"); + + return installed.date.getTime() !== required.date.getTime(); +} + +async function downloadServer( + source: ArtifactSource.GithubRelease, + config: Config, +): Promise { try { const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); - await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); - await setServerVersion(source.storage, releaseInfo.releaseName); + await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server"); + await Promise.all([ + config.serverReleaseTag.set(releaseInfo.releaseName), + config.serverReleaseDate.set(releaseInfo.releaseDate) + ]); } catch (err) { - vscode.window.showErrorMessage( - `Failed to download language server from ${source.repo.name} ` + - `GitHub repository: ${err.message}` - ); - - log.error(err); - - dns.resolve('example.com').then( - addrs => log.debug("DNS resolution for example.com was successful", addrs), - err => { - log.error( - "DNS resolution for example.com failed, " + - "there might be an issue with Internet availability" - ); - log.error(err); - } - ); - return false; + log.downloadError(err, "language server", source.repo.name); + return null; } const binaryPath = path.join(source.dir, source.file); @@ -101,7 +111,7 @@ async function downloadServer(source: ArtifactSource.GithubRelease): Promise("server-version", null); - log.debug("Get server-version:", version); - return version; -} - -async function setServerVersion(storage: vscode.Memento, version: string): Promise { - log.debug("Set server-version:", version); - await storage.update("server-version", version.toString()); -} diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index ecf53cf775f..ee67c750c1b 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -8,6 +8,7 @@ import { activateHighlighting } from './highlighting'; import { ensureServerBinary } from './installation/server'; import { Config } from './config'; import { log } from './util'; +import { ensureProperExtensionVersion } from './installation/extension'; let ctx: Ctx | undefined; @@ -34,7 +35,13 @@ export async function activate(context: vscode.ExtensionContext) { const config = new Config(context); - const serverPath = await ensureServerBinary(config.serverSource); + vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config)); + + // Don't await the user response here, otherwise we will block the lsp server bootstrap + void ensureProperExtensionVersion(config); + + const serverPath = await ensureServerBinary(config); + if (serverPath == null) { throw new Error( "Rust Analyzer Language Server is not available. " + From 7e6b1a60c3d7b20e1b4cee2ab210b617e359a002 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:04:11 +0200 Subject: [PATCH 08/25] vscode: npm run fix --- editors/code/src/config.ts | 6 +++--- editors/code/src/installation/extension.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index e2b0f6f8470..b5c07876b51 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -173,7 +173,7 @@ export class Config { name: "rust-analyzer", owner: "rust-analyzer" } - } + }; } get nightlyVsixSource(): ArtifactSource.GithubRelease { @@ -240,7 +240,7 @@ export class StringStorage { constructor( private readonly key: string, private readonly storage: vscode.Memento - ) {} + ) { } get(): null | string { const tag = this.storage.get(this.key, null); @@ -257,7 +257,7 @@ export class DateStorage { constructor( private readonly key: string, private readonly storage: vscode.Memento - ) {} + ) { } get(): null | Date { const date = this.storage.get(this.key, null); diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 7709cd3cd3e..7eab68852b7 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -110,7 +110,7 @@ const tryDownloadNightlyExtension = notReentrant(async function tryDownloadNight const vsixPath = path.join(vsixSource.dir, vsixSource.file); - await vscodeInstallExtensionFromVsix(vsixPath) + await vscodeInstallExtensionFromVsix(vsixPath); await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate); await fs.unlink(vsixPath); From 4d17152b31b27a8c851b4b1abaff359044ee9d96 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:14:55 +0200 Subject: [PATCH 09/25] vscode: make bailing out on custom serverPath more evident --- editors/code/src/config.ts | 12 +++++------- editors/code/src/installation/extension.ts | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index b5c07876b51..345c9e21a3f 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -45,7 +45,7 @@ export class Config { /** * Either `nightly` or `YYYY-MM-DD` (i.e. `stable` release) */ - private readonly extensionVersion: string = (() => { + readonly extensionReleaseTag: string = (() => { const packageJsonVersion = vscode .extensions .getExtension(this.extensionId)! @@ -135,10 +135,8 @@ export class Config { } } - get installedExtensionUpdateChannel() { - if (this.serverPath !== null) return null; - - return this.extensionVersion === NIGHTLY_TAG + get installedExtensionUpdateChannel(): UpdatesChannel { + return this.extensionReleaseTag === NIGHTLY_TAG ? UpdatesChannel.Nightly : UpdatesChannel.Stable; } @@ -159,7 +157,7 @@ export class Config { return this.createGithubReleaseSource( prebuiltBinaryName, - this.extensionVersion + this.extensionReleaseTag ); } @@ -195,7 +193,7 @@ export class Config { // We don't do runtime config validation here for simplicity. More on stackoverflow: // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension - private get serverPath() { return this.cfg.get("serverPath") as null | string; } + get serverPath() { return this.cfg.get("serverPath") as null | string; } get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 7eab68852b7..a0925acaa5d 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -15,6 +15,9 @@ const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; * extension version is what's needed according to `desiredUpdateChannel`. */ export async function ensureProperExtensionVersion(config: Config): Promise { + // User has built lsp server from sources, she should manage updates manually + if (config.serverPath !== null) return; + const currentUpdChannel = config.installedExtensionUpdateChannel; const desiredUpdChannel = config.updatesChannel; @@ -23,9 +26,6 @@ export async function ensureProperExtensionVersion(config: Config): Promise Date: Mon, 9 Mar 2020 20:17:50 +0200 Subject: [PATCH 10/25] vscode: put comma back --- editors/code/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 345c9e21a3f..a05d8ac0679 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -169,7 +169,7 @@ export class Config { dir: this.ctx.globalStoragePath, repo: { name: "rust-analyzer", - owner: "rust-analyzer" + owner: "rust-analyzer", } }; } From bc87d6de86a2d67febe7e4e21347af9c92dc7552 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:28:12 +0200 Subject: [PATCH 11/25] vscode-postrefactor: global storages --- editors/code/src/config.ts | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index a05d8ac0679..5371384ba27 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -186,8 +186,8 @@ export class Config { "rust-analyzer-server-release-date", this.ctx.globalState ); - readonly serverReleaseTag = new StringStorage( - "rust-analyzer-release-tag", this.ctx.globalState + readonly serverReleaseTag = new Storage( + "rust-analyzer-release-tag", this.ctx.globalState, null ); // We don't do runtime config validation here for simplicity. More on stackoverflow: @@ -234,37 +234,36 @@ export class Config { get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } } -export class StringStorage { +export class Storage { constructor( private readonly key: string, - private readonly storage: vscode.Memento + private readonly storage: vscode.Memento, + private readonly defaultVal: T ) { } - get(): null | string { - const tag = this.storage.get(this.key, null); - log.debug(this.key, "==", tag); - return tag; + get(): T { + const val = this.storage.get(this.key, this.defaultVal); + log.debug(this.key, "==", val); + return val; } - async set(tag: string) { - log.debug(this.key, "=", tag); - await this.storage.update(this.key, tag); + async set(val: T) { + log.debug(this.key, "=", val); + await this.storage.update(this.key, val); } } export class DateStorage { + inner: Storage; - constructor( - private readonly key: string, - private readonly storage: vscode.Memento - ) { } + constructor(key: string, storage: vscode.Memento) { + this.inner = new Storage(key, storage, null); + } get(): null | Date { - const date = this.storage.get(this.key, null); - log.debug(this.key, "==", date); - return date ? new Date(date) : null; + const dateStr = this.inner.get(); + return dateStr ? new Date(dateStr) : null; } async set(date: null | Date) { - log.debug(this.key, "=", date); - await this.storage.update(this.key, date); + await this.inner.set(date ? date.toString() : null); } } From c3ee8b10b89e6e0b1d80fd419291e5d73ff49a41 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:35:21 +0200 Subject: [PATCH 12/25] vscode-postrefactor: eliminate my-mistake floating promise @matklad --- editors/code/src/installation/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index a0925acaa5d..d0782ad13d9 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -23,7 +23,7 @@ export async function ensureProperExtensionVersion(config: Config): Promise Date: Mon, 9 Mar 2020 20:41:23 +0200 Subject: [PATCH 13/25] vscode-postrefactor: compare dates by value, not by reference --- editors/code/src/installation/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index d0782ad13d9..3a1481a89b5 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -58,11 +58,11 @@ export async function ensureProperExtensionVersion(config: Config): Promise { assert( - currentExtReleaseDate === config.installedNightlyExtensionReleaseDate.get(), + currentExtReleaseDate.getTime() === config.installedNightlyExtensionReleaseDate.get()?.getTime(), "Other active VSCode instance has reinstalled the extension" ); - if (releaseInfo.releaseDate === currentExtReleaseDate) { + if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) { vscode.window.showInformationMessage( "Whoops, it appears that your nightly version is up-to-date. " + "There might be some problems with the upcomming nightly release " + From 028a4aa99f7f6829cab2d8968ef6b5976e9b3ee9 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:53:01 +0200 Subject: [PATCH 14/25] vscode-postrefactor: unhandled promise rejections shall not pass --- editors/code/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index ee67c750c1b..4f46345d6b6 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -35,7 +35,7 @@ export async function activate(context: vscode.ExtensionContext) { const config = new Config(context); - vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config)); + vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config).catch(log.error)); // Don't await the user response here, otherwise we will block the lsp server bootstrap void ensureProperExtensionVersion(config); From abee777b6ec842eef6609475b2bf623e9490fd7e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 20:57:20 +0200 Subject: [PATCH 15/25] vscode-postrefactor: remove remainders of debug logging --- editors/code/src/installation/extension.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 3a1481a89b5..eb6acc341a5 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -89,7 +89,6 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "") `version and reload the window now?`, "Download now", "Cancel" ); - log.debug("Response: ", userResponse); return userResponse === "Download now"; } From c2b0fffe3d08d4819ca073c5821869b38784328d Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 21:02:19 +0200 Subject: [PATCH 16/25] vscode-postrefactor: add achtung comment --- editors/code/src/installation/extension.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index eb6acc341a5..87a5874037b 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -94,6 +94,10 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "") /** * Shutdowns the process in case of success (i.e. reloads the window) or throws an error. + * + * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during + * each of them would result in a ton of code (especially accounting for cross-process + * shared mutable `globalState` access). Enforcing reentrancy for this is best-effort. */ const tryDownloadNightlyExtension = notReentrant(async function tryDownloadNightlyExtension( config: Config, From 607d017229ce398bd7fef43aa5f4ab35914e6f31 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 9 Mar 2020 21:15:07 +0200 Subject: [PATCH 17/25] vscode-postrefactor: unhandled promise rejections shall not pass 2 --- editors/code/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 4f46345d6b6..8a7c817276d 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -38,7 +38,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config).catch(log.error)); // Don't await the user response here, otherwise we will block the lsp server bootstrap - void ensureProperExtensionVersion(config); + void ensureProperExtensionVersion(config).catch(log.error); const serverPath = await ensureServerBinary(config); From 7f02d4657b796a438e441e107d4fb1906ec1ed7b Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 14 Mar 2020 02:00:34 +0200 Subject: [PATCH 18/25] vscode-postrefactor: minor config refactorings --- editors/code/src/config.ts | 5 +++-- editors/code/src/installation/extension.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 5371384ba27..93f72409ddf 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -35,6 +35,7 @@ export class Config { private readonly rootSection = "rust-analyzer"; private readonly requiresReloadOpts = [ + "serverPath", "cargoFeatures", "cargo-watch", "highlighting.semanticTokens", @@ -50,7 +51,7 @@ export class Config { .extensions .getExtension(this.extensionId)! .packageJSON - .version as string; + .version as string; // n.n.YYYYMMDD[-nightly] if (packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG; @@ -193,7 +194,7 @@ export class Config { // We don't do runtime config validation here for simplicity. More on stackoverflow: // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension - get serverPath() { return this.cfg.get("serverPath") as null | string; } + private get serverPath() { return this.cfg.get("serverPath") as null | string; } get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 87a5874037b..2022d090d3e 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -4,7 +4,7 @@ import { promises as fs } from 'fs'; import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util"; import { Config, UpdatesChannel } from "../config"; -import { ArtifactReleaseInfo } from "./interfaces"; +import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces"; import { downloadArtifactWithProgressUi } from "./downloads"; import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; @@ -16,7 +16,7 @@ const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; */ export async function ensureProperExtensionVersion(config: Config): Promise { // User has built lsp server from sources, she should manage updates manually - if (config.serverPath !== null) return; + if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return; const currentUpdChannel = config.installedExtensionUpdateChannel; const desiredUpdChannel = config.updatesChannel; From d7b46e0527cd7b52845f37cffc57cbae4ba0b945 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 14 Mar 2020 02:01:28 +0200 Subject: [PATCH 19/25] vscode-postrefactor: enforcing more reentrancy --- editors/code/src/installation/extension.ts | 2 +- editors/code/src/installation/server.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 2022d090d3e..f6dd20d82d1 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -97,7 +97,7 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "") * * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during * each of them would result in a ton of code (especially accounting for cross-process - * shared mutable `globalState` access). Enforcing reentrancy for this is best-effort. + * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort. */ const tryDownloadNightlyExtension = notReentrant(async function tryDownloadNightlyExtension( config: Config, diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts index 345f30d4763..f359584744b 100644 --- a/editors/code/src/installation/server.ts +++ b/editors/code/src/installation/server.ts @@ -5,7 +5,7 @@ import { spawnSync } from "child_process"; import { ArtifactSource } from "./interfaces"; import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; import { downloadArtifactWithProgressUi } from "./downloads"; -import { log, assert } from "../util"; +import { log, assert, notReentrant } from "../util"; import { Config, NIGHTLY_TAG } from "../config"; export async function ensureServerBinary(config: Config): Promise { @@ -82,7 +82,10 @@ function shouldDownloadServer( return installed.date.getTime() !== required.date.getTime(); } -async function downloadServer( +/** + * Enforcing no reentrancy for this is best-effort. + */ +const downloadServer = notReentrant(async function downloadServer( source: ArtifactSource.GithubRelease, config: Config, ): Promise { @@ -112,7 +115,7 @@ async function downloadServer( ); return binaryPath; -} +}); function isBinaryAvailable(binaryPath: string): boolean { const res = spawnSync(binaryPath, ["--version"]); From 5e32a67c83d46862db0166c263efc65b6ecd5b52 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 14 Mar 2020 03:01:14 +0200 Subject: [PATCH 20/25] vscode-postrefactor: more logging and better error handling --- editors/code/package-lock.json | 2 +- editors/code/src/config.ts | 36 ++++++++++------------ editors/code/src/installation/extension.ts | 16 ++++++++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 575dc7c4a1f..1b497edd700 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -1,6 +1,6 @@ { "name": "rust-analyzer", - "version": "0.2.20200309-nightly", + "version": "0.2.20200309", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 93f72409ddf..f63e1d20e85 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -1,7 +1,7 @@ import * as os from "os"; import * as vscode from 'vscode'; import { ArtifactSource } from "./installation/interfaces"; -import { log } from "./util"; +import { log, vscodeReloadWindow } from "./util"; const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; @@ -43,20 +43,20 @@ export class Config { ] .map(opt => `${this.rootSection}.${opt}`); + readonly packageJsonVersion = vscode + .extensions + .getExtension(this.extensionId)! + .packageJSON + .version as string; // n.n.YYYYMMDD[-nightly] + /** * Either `nightly` or `YYYY-MM-DD` (i.e. `stable` release) */ readonly extensionReleaseTag: string = (() => { - const packageJsonVersion = vscode - .extensions - .getExtension(this.extensionId)! - .packageJSON - .version as string; // n.n.YYYYMMDD[-nightly] - - if (packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG; + if (this.packageJsonVersion.endsWith(NIGHTLY_TAG)) return NIGHTLY_TAG; const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; - const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; + const [, yyyy, mm, dd] = this.packageJsonVersion.match(realVersionRegexp)!; return `${yyyy}-${mm}-${dd}`; })(); @@ -72,7 +72,10 @@ export class Config { this.cfg = vscode.workspace.getConfiguration(this.rootSection); const enableLogging = this.cfg.get("trace.extension") as boolean; log.setEnabled(enableLogging); - log.debug("Using configuration:", this.cfg); + log.debug( + "Extension version:", this.packageJsonVersion, + "using configuration:", this.cfg + ); } private async onConfigChange(event: vscode.ConfigurationChangeEvent) { @@ -90,7 +93,7 @@ export class Config { ); if (userResponse === "Reload now") { - vscode.commands.executeCommand("workbench.action.reloadWindow"); + await vscodeReloadWindow(); } } @@ -180,16 +183,11 @@ export class Config { } readonly installedNightlyExtensionReleaseDate = new DateStorage( - "rust-analyzer-installed-nightly-extension-release-date", + "installed-nightly-extension-release-date", this.ctx.globalState ); - readonly serverReleaseDate = new DateStorage( - "rust-analyzer-server-release-date", - this.ctx.globalState - ); - readonly serverReleaseTag = new Storage( - "rust-analyzer-release-tag", this.ctx.globalState, null - ); + readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState); + readonly serverReleaseTag = new Storage("server-release-tag", this.ctx.globalState, null); // We don't do runtime config validation here for simplicity. More on stackoverflow: // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index f6dd20d82d1..0d69b8d0cfe 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -44,10 +44,20 @@ export async function ensureProperExtensionVersion(config: Config): Promise Date: Sat, 14 Mar 2020 03:16:50 +0200 Subject: [PATCH 21/25] docs: add documentation for vscode nightlies --- docs/user/readme.adoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/user/readme.adoc b/docs/user/readme.adoc index 4e99dd0a662..2e6c6112fa2 100644 --- a/docs/user/readme.adoc +++ b/docs/user/readme.adoc @@ -65,6 +65,25 @@ Note that we only support the latest version of VS Code. The extension will be updated automatically as new versions become available. It will ask your permission to download the matching language server version binary if needed. +===== Nightly + +We ship nightly releases for VS Code. To help us out with testing the newest code and follow the bleeding edge of our `master`, please use the following config: + +[source,json] +---- +{ "rust-analyzer.updates.channel": "nightly" } +---- + +You will be prompted to install the `nightly` extension version. Just click `Download now` and from that moment you will get automatic updates each 24 hours. + +If you don't want to be asked for `Download now` every day when the new nightly version is released add the following to your `settings.json`: +[source,json] +---- +{ "rust-analyzer.updates.askBeforeDownload": false } +---- + +NOTE: Nightly extension should **only** be installed via the `Download now` action from VS Code. + ==== Building From Source Alternatively, both the server and the plugin can be installed from source: From 9789f984cbe20a7fca1354e542be7bb345d795eb Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sat, 14 Mar 2020 03:20:39 +0200 Subject: [PATCH 22/25] vscode: sync package-lock.json version with package.json --- editors/code/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/package-lock.json b/editors/code/package-lock.json index 1b497edd700..575dc7c4a1f 100644 --- a/editors/code/package-lock.json +++ b/editors/code/package-lock.json @@ -1,6 +1,6 @@ { "name": "rust-analyzer", - "version": "0.2.20200309", + "version": "0.2.20200309-nightly", "lockfileVersion": 1, "requires": true, "dependencies": { From d38d59fed810e702d7473cbb8485b26f921889c6 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 16 Mar 2020 12:17:36 +0200 Subject: [PATCH 23/25] vscode-postrefactor: prefer arrow functions --- editors/code/src/installation/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index 0d69b8d0cfe..e0aa5317d77 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -109,7 +109,7 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "") * each of them would result in a ton of code (especially accounting for cross-process * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort. */ -const tryDownloadNightlyExtension = notReentrant(async function tryDownloadNightlyExtension( +const tryDownloadNightlyExtension = notReentrant(async ( config: Config, shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true ): Promise { From fc47274541b5f4e9c8406c4dd401392129845396 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 16 Mar 2020 12:18:30 +0200 Subject: [PATCH 24/25] vscode-postrefactor: fix syntax error --- editors/code/src/installation/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts index e0aa5317d77..eea6fded237 100644 --- a/editors/code/src/installation/extension.ts +++ b/editors/code/src/installation/extension.ts @@ -112,7 +112,7 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "") const tryDownloadNightlyExtension = notReentrant(async ( config: Config, shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true -): Promise { +): Promise => { const vsixSource = config.nightlyVsixSource; try { const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag); From 5a0041c5aaeee49be84ce771fb0360ae55cbd8b2 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Mon, 16 Mar 2020 12:19:26 +0200 Subject: [PATCH 25/25] vscode-postrefactor: migrate to arrow functions --- editors/code/src/installation/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts index f359584744b..05730a77885 100644 --- a/editors/code/src/installation/server.ts +++ b/editors/code/src/installation/server.ts @@ -85,10 +85,10 @@ function shouldDownloadServer( /** * Enforcing no reentrancy for this is best-effort. */ -const downloadServer = notReentrant(async function downloadServer( +const downloadServer = notReentrant(async ( source: ArtifactSource.GithubRelease, config: Config, -): Promise { +): Promise => { try { const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);