From 8533fc437b7619e1c289fa7913fdafda533903b8 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sun, 16 Feb 2020 03:08:36 +0200 Subject: vscode: add version and storage parameters to github binary source --- editors/code/package.json | 2 +- editors/code/src/client.ts | 2 +- editors/code/src/config.ts | 17 ++++++++++++++++- editors/code/src/ctx.ts | 4 ++++ editors/code/src/installation/interfaces.ts | 13 +++++++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) (limited to 'editors/code') diff --git a/editors/code/package.json b/editors/code/package.json index a607c2148..96b8e9eb0 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.0-dev", + "version": "0.2.20200211-dev", "publisher": "matklad", "repository": { "url": "https://github.com/rust-analyzer/rust-analyzer.git", diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts index 12c97be2f..efef820ab 100644 --- a/editors/code/src/client.ts +++ b/editors/code/src/client.ts @@ -11,7 +11,7 @@ export async function createClient(config: Config): Promise `${Config.rootSection}.${opt}`); + private static readonly extensionVersion: string = (() => { + const packageJsonVersion = vscode + .extensions + .getExtension("matklad.rust-analyzer")! + .packageJSON + .version as string; // n.n.YYYYMMDD + + const realVersionRegexp = /^\d+\.\d+\.(\d{4})(\d{2})(\d{2})/; + const [, yyyy, mm, dd] = packageJsonVersion.match(realVersionRegexp)!; + + return `${yyyy}-${mm}-${dd}`; + })(); + private cfg!: vscode.WorkspaceConfiguration; constructor(private readonly ctx: vscode.ExtensionContext) { @@ -98,7 +111,7 @@ export class Config { } } - get serverBinarySource(): null | BinarySource { + get serverSource(): null | BinarySource { const serverPath = RA_LSP_DEBUG ?? this.cfg.get("raLspServerPath"); if (serverPath) { @@ -116,6 +129,8 @@ export class Config { type: BinarySource.Type.GithubRelease, dir: this.ctx.globalStoragePath, file: prebuiltBinaryName, + storage: this.ctx.globalState, + version: Config.extensionVersion, repo: { name: "rust-analyzer", owner: "rust-analyzer", diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 70042a479..9fcf2ec38 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -60,6 +60,10 @@ export class Ctx { this.pushCleanup(d); } + get globalState(): vscode.Memento { + return this.extCtx.globalState; + } + get subscriptions(): Disposable[] { return this.extCtx.subscriptions; } diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts index 93ea577d4..e40839e4b 100644 --- a/editors/code/src/installation/interfaces.ts +++ b/editors/code/src/installation/interfaces.ts @@ -1,3 +1,5 @@ +import * as vscode from "vscode"; + export interface GithubRepo { name: string; owner: string; @@ -50,6 +52,17 @@ export namespace BinarySource { * and in local `.dir`. */ file: string; + + /** + * Tag of github release that denotes a version required by this extension. + */ + version: string; + + /** + * Object that provides `get()/update()` operations to store metadata + * about the actual binary, e.g. its actual version. + */ + storage: vscode.Memento; } } -- cgit v1.2.3 From 0f7abeb03599964e58d979820134c9e7a61a690e Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sun, 16 Feb 2020 03:12:25 +0200 Subject: vscode: add release tag option to fetchArtifactReleaseInfo() --- .../installation/fetch_artifact_release_info.ts | 52 ++++++++++++++++++++++ .../fetch_latest_artifact_release_info.ts | 46 ------------------- 2 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 editors/code/src/installation/fetch_artifact_release_info.ts delete mode 100644 editors/code/src/installation/fetch_latest_artifact_release_info.ts (limited to 'editors/code') diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts new file mode 100644 index 000000000..7d497057a --- /dev/null +++ b/editors/code/src/installation/fetch_artifact_release_info.ts @@ -0,0 +1,52 @@ +import fetch from "node-fetch"; +import { GithubRepo, ArtifactReleaseInfo } from "./interfaces"; + +const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; + + +/** + * Fetches the release with `releaseTag` (or just latest release when not specified) + * from GitHub `repo` and returns metadata about `artifactFileName` shipped with + * this release or `null` if no such artifact was published. + */ +export async function fetchArtifactReleaseInfo( + repo: GithubRepo, artifactFileName: string, releaseTag?: string +): Promise { + + const repoOwner = encodeURIComponent(repo.owner); + const repoName = encodeURIComponent(repo.name); + + const apiEndpointPath = releaseTag + ? `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}` + : `/repos/${repoOwner}/${repoName}/releases/latest`; + + const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; + + // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) + + console.log("Issuing request for released artifacts metadata to", requestUrl); + + // FIXME: handle non-ok response + const response: GithubRelease = await fetch(requestUrl, { + headers: { Accept: "application/vnd.github.v3+json" } + }) + .then(res => res.json()); + + const artifact = response.assets.find(artifact => artifact.name === artifactFileName); + + if (!artifact) return null; + + return { + releaseName: response.name, + downloadUrl: artifact.browser_download_url + }; + + // We omit declaration of tremendous amount of fields that we are not using here + interface GithubRelease { + name: string; + assets: Array<{ + name: string; + browser_download_url: string; + }>; + } +} diff --git a/editors/code/src/installation/fetch_latest_artifact_release_info.ts b/editors/code/src/installation/fetch_latest_artifact_release_info.ts deleted file mode 100644 index 29ee029a7..000000000 --- a/editors/code/src/installation/fetch_latest_artifact_release_info.ts +++ /dev/null @@ -1,46 +0,0 @@ -import fetch from "node-fetch"; -import { GithubRepo, ArtifactReleaseInfo } from "./interfaces"; - -const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; - -/** - * Fetches the latest release from GitHub `repo` and returns metadata about - * `artifactFileName` shipped with this release or `null` if no such artifact was published. - */ -export async function fetchLatestArtifactReleaseInfo( - repo: GithubRepo, artifactFileName: string -): Promise { - - const repoOwner = encodeURIComponent(repo.owner); - const repoName = encodeURIComponent(repo.name); - - const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/latest`; - const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; - - // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) - - console.log("Issuing request for released artifacts metadata to", requestUrl); - - const response: GithubRelease = await fetch(requestUrl, { - headers: { Accept: "application/vnd.github.v3+json" } - }) - .then(res => res.json()); - - const artifact = response.assets.find(artifact => artifact.name === artifactFileName); - - if (!artifact) return null; - - return { - releaseName: response.name, - downloadUrl: artifact.browser_download_url - }; - - // We omit declaration of tremendous amount of fields that we are not using here - interface GithubRelease { - name: string; - assets: Array<{ - name: string; - browser_download_url: string; - }>; - } -} -- cgit v1.2.3 From b9188226fabb00de3c5f3706186e96b01c223566 Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sun, 16 Feb 2020 03:13:06 +0200 Subject: vscode: extract downloadArtifact() function --- editors/code/src/installation/download_artifact.ts | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 editors/code/src/installation/download_artifact.ts (limited to 'editors/code') diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts new file mode 100644 index 000000000..de655f8f4 --- /dev/null +++ b/editors/code/src/installation/download_artifact.ts @@ -0,0 +1,58 @@ +import * as vscode from "vscode"; +import * as path from "path"; +import { promises as fs } from "fs"; +import { strict as assert } from "assert"; + +import { ArtifactReleaseInfo } from "./interfaces"; +import { downloadFile } from "./download_file"; +import { throttle } from "throttle-debounce"; + +/** + * 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.strictEqual( + err?.code, + "EEXIST", + `Couldn't create directory "${installationDir}" to download `+ + `${artifactFileName} artifact: ${err.message}` + )); + + const installationPath = path.join(installationDir, artifactFileName); + + console.time(`Downloading ${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, throttle( + 200, + /* noTrailing: */ true, + (readBytes, totalBytes) => { + const newPercentage = (readBytes / totalBytes) * 100; + progress.report({ + message: newPercentage.toFixed(0) + "%", + increment: newPercentage - lastPrecentage + }); + + lastPrecentage = newPercentage; + }) + ); + } + ); + console.timeEnd(`Downloading ${artifactFileName}`); +} -- cgit v1.2.3 From 467b925b53edfd77981344346c3ae2200acdaabe Mon Sep 17 00:00:00 2001 From: Veetaha Date: Sun, 16 Feb 2020 03:14:20 +0200 Subject: vscode: save binary version when downloading and download only version that matches TypeScript extension version --- editors/code/src/installation/server.ts | 168 ++++++++++++++------------------ 1 file changed, 72 insertions(+), 96 deletions(-) (limited to 'editors/code') diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts index 406e2299c..80cb719e3 100644 --- a/editors/code/src/installation/server.ts +++ b/editors/code/src/installation/server.ts @@ -1,63 +1,15 @@ import * as vscode from "vscode"; import * as path from "path"; import { strict as assert } from "assert"; -import { promises as fs } from "fs"; import { promises as dns } from "dns"; import { spawnSync } from "child_process"; -import { throttle } from "throttle-debounce"; import { BinarySource } from "./interfaces"; -import { fetchLatestArtifactReleaseInfo } from "./fetch_latest_artifact_release_info"; -import { downloadFile } from "./download_file"; - -export async function downloadLatestServer( - {file: artifactFileName, dir: installationDir, repo}: BinarySource.GithubRelease -) { - const { releaseName, downloadUrl } = (await fetchLatestArtifactReleaseInfo( - repo, artifactFileName - ))!; - - await fs.mkdir(installationDir).catch(err => assert.strictEqual( - err?.code, - "EEXIST", - `Couldn't create directory "${installationDir}" to download `+ - `language server binary: ${err.message}` - )); - - const installationPath = path.join(installationDir, artifactFileName); - - console.time("Downloading ra_lsp_server"); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, // FIXME: add support for canceling download? - title: `Downloading language server (${releaseName})` - }, - async (progress, _cancellationToken) => { - let lastPrecentage = 0; - const filePermissions = 0o755; // (rwx, r_x, r_x) - await downloadFile(downloadUrl, installationPath, filePermissions, throttle( - 200, - /* noTrailing: */ true, - (readBytes, totalBytes) => { - const newPercentage = (readBytes / totalBytes) * 100; - progress.report({ - message: newPercentage.toFixed(0) + "%", - increment: newPercentage - lastPrecentage - }); - - lastPrecentage = newPercentage; - }) - ); - } - ); - console.timeEnd("Downloading ra_lsp_server"); -} -export async function ensureServerBinary( - serverSource: null | BinarySource -): Promise { +import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; +import { downloadArtifact } from "./download_artifact"; - if (!serverSource) { +export async function ensureServerBinary(source: null | BinarySource): Promise { + if (!source) { vscode.window.showErrorMessage( "Unfortunately we don't ship binaries for your platform yet. " + "You need to manually clone rust-analyzer repository and " + @@ -69,80 +21,104 @@ export async function ensureServerBinary( return null; } - switch (serverSource.type) { + switch (source.type) { case BinarySource.Type.ExplicitPath: { - if (isBinaryAvailable(serverSource.path)) { - return serverSource.path; + if (isBinaryAvailable(source.path)) { + return source.path; } vscode.window.showErrorMessage( - `Unable to run ${serverSource.path} binary. ` + + `Unable to run ${source.path} binary. ` + `To use the pre-built language server, set "rust-analyzer.raLspServerPath" ` + "value to `null` or remove it from the settings to use it by default." ); return null; } case BinarySource.Type.GithubRelease: { - const prebuiltBinaryPath = path.join(serverSource.dir, serverSource.file); + const prebuiltBinaryPath = path.join(source.dir, source.file); + + const installedVersion: null | string = getServerVersion(source.storage); + const requiredVersion: string = source.version; - if (isBinaryAvailable(prebuiltBinaryPath)) { + console.log("Installed version:", installedVersion, "required:", requiredVersion); + + if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion == requiredVersion) { + // FIXME: check for new releases and notify the user to update if possible return prebuiltBinaryPath; } const userResponse = await vscode.window.showInformationMessage( - "Language server binary for rust-analyzer was not found. " + + `Language server version ${source.version} for rust-analyzer is not installed. ` + "Do you want to download it now?", "Download now", "Cancel" ); if (userResponse !== "Download now") return null; - try { - await downloadLatestServer(serverSource); - } catch (err) { - vscode.window.showErrorMessage( - `Failed to download language server from ${serverSource.repo.name} ` + - `GitHub repository: ${err.message}` - ); + if (!await downloadServer(source)) return null; - console.error(err); + return prebuiltBinaryPath; + } + } +} - dns.resolve('example.com').then( - addrs => console.log("DNS resolution for example.com was successful", addrs), - err => { - console.error( - "DNS resolution for example.com failed, " + - "there might be an issue with Internet availability" - ); - console.error(err); - } - ); +async function downloadServer(source: BinarySource.GithubRelease): Promise { + try { + const releaseInfo = (await fetchArtifactReleaseInfo(source.repo, source.file, source.version))!; + + await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); + await setServerVersion(source.storage, releaseInfo.releaseName); + } catch (err) { + vscode.window.showErrorMessage( + `Failed to download language server from ${source.repo.name} ` + + `GitHub repository: ${err.message}` + ); + + console.error(err); - return null; + dns.resolve('example.com').then( + addrs => console.log("DNS resolution for example.com was successful", addrs), + err => { + console.error( + "DNS resolution for example.com failed, " + + "there might be an issue with Internet availability" + ); + console.error(err); } + ); + return false; + } - if (!isBinaryAvailable(prebuiltBinaryPath)) assert(false, - `Downloaded language server binary is not functional.` + - `Downloaded from: ${JSON.stringify(serverSource)}` - ); + if (!isBinaryAvailable(path.join(source.dir, source.file))) assert(false, + `Downloaded language server binary is not functional.` + + `Downloaded from: ${JSON.stringify(source, null, 4)}` + ); + vscode.window.showInformationMessage( + "Rust analyzer language server was successfully installed 🦀" + ); - vscode.window.showInformationMessage( - "Rust analyzer language server was successfully installed 🦀" - ); + return true; +} - return prebuiltBinaryPath; - } - } +function isBinaryAvailable(binaryPath: string): boolean { + const res = spawnSync(binaryPath, ["--version"]); - function isBinaryAvailable(binaryPath: string) { - const res = spawnSync(binaryPath, ["--version"]); + // ACHTUNG! `res` type declaration is inherently wrong, see + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 - // ACHTUNG! `res` type declaration is inherently wrong, see - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 + console.log("Checked binary availablity via --version", res); + console.log(binaryPath, "--version output:", res.output?.map(String)); - console.log("Checked binary availablity via --version", res); - console.log(binaryPath, "--version output:", res.output?.map(String)); + return res.status === 0; +} - return res.status === 0; - } +function getServerVersion(storage: vscode.Memento): null | string { + const version = storage.get("server-version", null); + console.log("Get server-version:", version); + return version; +} + +async function setServerVersion(storage: vscode.Memento, version: string): Promise { + console.log("Set server-version:", version); + await storage.update("server-version", version.toString()); } -- cgit v1.2.3