mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-12 13:18:47 +00:00
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).
This commit is contained in:
parent
f0a1b64d7e
commit
fb6e655de8
13 changed files with 270 additions and 696 deletions
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
|
@ -16,6 +16,7 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": [
|
"args": [
|
||||||
|
// "--user-data-dir=${workspaceFolder}/target/code",
|
||||||
"--disable-extensions",
|
"--disable-extensions",
|
||||||
"--extensionDevelopmentPath=${workspaceFolder}/editors/code"
|
"--extensionDevelopmentPath=${workspaceFolder}/editors/code"
|
||||||
],
|
],
|
||||||
|
|
|
@ -228,7 +228,7 @@
|
||||||
"default": "stable",
|
"default": "stable",
|
||||||
"markdownEnumDescriptions": [
|
"markdownEnumDescriptions": [
|
||||||
"`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general",
|
"`\"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**"
|
"`\"nightly\"` updates are shipped daily (extension updates automatically by downloading artifacts directly from GitHub), 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"
|
"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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from "vscode";
|
||||||
import { ensureServerBinary } from '../installation/server';
|
import { spawnSync } from "child_process";
|
||||||
import { Ctx, Cmd } from '../ctx';
|
import { Ctx, Cmd } from '../ctx';
|
||||||
import { spawnSync } from 'child_process';
|
|
||||||
|
|
||||||
export function serverVersion(ctx: Ctx): Cmd {
|
export function serverVersion(ctx: Ctx): Cmd {
|
||||||
return async () => {
|
return async () => {
|
||||||
const binaryPath = await ensureServerBinary(ctx.config, ctx.state);
|
const version = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }).stdout;
|
||||||
|
|
||||||
if (binaryPath == 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 version = spawnSync(binaryPath, ["--version"], { encoding: "utf8" }).stdout;
|
|
||||||
vscode.window.showInformationMessage('rust-analyzer version : ' + version);
|
vscode.window.showInformationMessage('rust-analyzer version : ' + version);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
import * as os from "os";
|
|
||||||
import * as vscode from 'vscode';
|
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;
|
|
||||||
|
|
||||||
export interface InlayHintOptions {
|
export interface InlayHintOptions {
|
||||||
typeHints: boolean;
|
typeHints: boolean;
|
||||||
|
@ -25,10 +21,7 @@ export interface CargoFeatures {
|
||||||
loadOutDirsFromCheck: boolean;
|
loadOutDirsFromCheck: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum UpdatesChannel {
|
export type UpdatesChannel = "stable" | "nightly";
|
||||||
Stable = "stable",
|
|
||||||
Nightly = "nightly"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NIGHTLY_TAG = "nightly";
|
export const NIGHTLY_TAG = "nightly";
|
||||||
export class Config {
|
export class Config {
|
||||||
|
@ -41,6 +34,7 @@ export class Config {
|
||||||
"cargo-watch",
|
"cargo-watch",
|
||||||
"highlighting.semanticTokens",
|
"highlighting.semanticTokens",
|
||||||
"inlayHints",
|
"inlayHints",
|
||||||
|
"updates.channel",
|
||||||
]
|
]
|
||||||
.map(opt => `${this.rootSection}.${opt}`);
|
.map(opt => `${this.rootSection}.${opt}`);
|
||||||
|
|
||||||
|
@ -94,100 +88,17 @@ export class Config {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userResponse === "Reload now") {
|
if (userResponse === "Reload now") {
|
||||||
await vscodeReloadWindow();
|
await vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static replaceTildeWithHomeDir(path: string) {
|
get globalStoragePath(): string { return this.ctx.globalStoragePath; }
|
||||||
if (path.startsWith("~/")) {
|
|
||||||
return os.homedir() + path.slice("~".length);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the binary artifact for `rust-analyzer` that is published for
|
|
||||||
* `platform` on GitHub releases. (It is also stored under the same name when
|
|
||||||
* downloaded by the extension).
|
|
||||||
*/
|
|
||||||
get prebuiltServerFileName(): null | string {
|
|
||||||
// See possible `arch` values here:
|
|
||||||
// https://nodejs.org/api/process.html#process_process_arch
|
|
||||||
|
|
||||||
switch (process.platform) {
|
|
||||||
|
|
||||||
case "linux": {
|
|
||||||
switch (process.arch) {
|
|
||||||
case "arm":
|
|
||||||
case "arm64": return null;
|
|
||||||
|
|
||||||
default: return "rust-analyzer-linux";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "darwin": return "rust-analyzer-mac";
|
|
||||||
case "win32": return "rust-analyzer-windows.exe";
|
|
||||||
|
|
||||||
// Users on these platforms yet need to manually build from sources
|
|
||||||
case "aix":
|
|
||||||
case "android":
|
|
||||||
case "freebsd":
|
|
||||||
case "openbsd":
|
|
||||||
case "sunos":
|
|
||||||
case "cygwin":
|
|
||||||
case "netbsd": return null;
|
|
||||||
// The list of platforms is exhaustive (see `NodeJS.Platform` type definition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get installedExtensionUpdateChannel(): UpdatesChannel {
|
|
||||||
return this.extensionReleaseTag === NIGHTLY_TAG
|
|
||||||
? UpdatesChannel.Nightly
|
|
||||||
: UpdatesChannel.Stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
get serverSource(): null | ArtifactSource {
|
|
||||||
const serverPath = RA_LSP_DEBUG ?? this.serverPath;
|
|
||||||
|
|
||||||
if (serverPath) {
|
|
||||||
return {
|
|
||||||
type: ArtifactSource.Type.ExplicitPath,
|
|
||||||
path: Config.replaceTildeWithHomeDir(serverPath)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const prebuiltBinaryName = this.prebuiltServerFileName;
|
|
||||||
|
|
||||||
if (!prebuiltBinaryName) return null;
|
|
||||||
|
|
||||||
return this.createGithubReleaseSource(
|
|
||||||
prebuiltBinaryName,
|
|
||||||
this.extensionReleaseTag
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease {
|
|
||||||
return {
|
|
||||||
type: ArtifactSource.Type.GithubRelease,
|
|
||||||
file,
|
|
||||||
tag,
|
|
||||||
dir: this.ctx.globalStoragePath,
|
|
||||||
repo: {
|
|
||||||
name: "rust-analyzer",
|
|
||||||
owner: "rust-analyzer",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get nightlyVsixSource(): ArtifactSource.GithubRelease {
|
|
||||||
return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't do runtime config validation here for simplicity. More on stackoverflow:
|
// 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
|
// 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 channel() { return this.cfg.get<"stable" | "nightly">("updates.channel")!; }
|
||||||
get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
|
get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; }
|
||||||
get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
|
get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; }
|
||||||
get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
|
get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; }
|
||||||
|
|
|
@ -4,21 +4,20 @@ import * as lc from 'vscode-languageclient';
|
||||||
import { Config } from './config';
|
import { Config } from './config';
|
||||||
import { createClient } from './client';
|
import { createClient } from './client';
|
||||||
import { isRustEditor, RustEditor } from './util';
|
import { isRustEditor, RustEditor } from './util';
|
||||||
import { PersistentState } from './persistent_state';
|
|
||||||
|
|
||||||
export class Ctx {
|
export class Ctx {
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly config: Config,
|
readonly config: Config,
|
||||||
readonly state: PersistentState,
|
|
||||||
private readonly extCtx: vscode.ExtensionContext,
|
private readonly extCtx: vscode.ExtensionContext,
|
||||||
readonly client: lc.LanguageClient
|
readonly client: lc.LanguageClient,
|
||||||
|
readonly serverPath: string,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
|
static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
|
||||||
const client = await createClient(config, serverPath);
|
const client = await createClient(config, serverPath);
|
||||||
const res = new Ctx(config, state, extCtx, client);
|
const res = new Ctx(config, extCtx, client, serverPath);
|
||||||
res.pushCleanup(client.start());
|
res.pushCleanup(client.start());
|
||||||
await client.onReady();
|
await client.onReady();
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -1,146 +0,0 @@
|
||||||
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, ArtifactSource } from "./interfaces";
|
|
||||||
import { downloadArtifactWithProgressUi } from "./downloads";
|
|
||||||
import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
|
|
||||||
import { PersistentState } from "../persistent_state";
|
|
||||||
|
|
||||||
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, state: PersistentState): Promise<never | void> {
|
|
||||||
// User has built lsp server from sources, she should manage updates manually
|
|
||||||
if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
|
|
||||||
|
|
||||||
const currentUpdChannel = config.installedExtensionUpdateChannel;
|
|
||||||
const desiredUpdChannel = config.updatesChannel;
|
|
||||||
|
|
||||||
if (currentUpdChannel === UpdatesChannel.Stable) {
|
|
||||||
// Release date is present only when we are on nightly
|
|
||||||
await state.installedNightlyExtensionReleaseDate.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get();
|
|
||||||
|
|
||||||
if (currentExtReleaseDate === null) {
|
|
||||||
void vscode.window.showErrorMessage(
|
|
||||||
"Nightly release date must've been set during the installation. " +
|
|
||||||
"Did you download and install the nightly .vsix package manually?"
|
|
||||||
);
|
|
||||||
throw new Error("Nightly release date was not set in globalStorage");
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateNow = new Date;
|
|
||||||
const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow);
|
|
||||||
log.debug(
|
|
||||||
"Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate,
|
|
||||||
"hours ago, namely:", currentExtReleaseDate, "and now is", dateNow
|
|
||||||
);
|
|
||||||
|
|
||||||
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, state, releaseInfo => {
|
|
||||||
assert(
|
|
||||||
currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(),
|
|
||||||
"Other active VSCode instance has reinstalled the extension"
|
|
||||||
);
|
|
||||||
|
|
||||||
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 " +
|
|
||||||
"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"
|
|
||||||
);
|
|
||||||
return userResponse === "Download now";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 no reentrancy for this is best-effort.
|
|
||||||
*/
|
|
||||||
const tryDownloadNightlyExtension = notReentrant(async (
|
|
||||||
config: Config,
|
|
||||||
state: PersistentState,
|
|
||||||
shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
|
|
||||||
): Promise<never | void> => {
|
|
||||||
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 state.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);
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { GithubRepo, ArtifactReleaseInfo } from "./interfaces";
|
|
||||||
import { log } from "../util";
|
|
||||||
|
|
||||||
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the release with `releaseTag` from GitHub `repo` and
|
|
||||||
* returns metadata about `artifactFileName` shipped with
|
|
||||||
* this release.
|
|
||||||
*
|
|
||||||
* @throws Error upon network failure or if no such repository, release, or artifact exists.
|
|
||||||
*/
|
|
||||||
export async function fetchArtifactReleaseInfo(
|
|
||||||
repo: GithubRepo,
|
|
||||||
artifactFileName: string,
|
|
||||||
releaseTag: string
|
|
||||||
): Promise<ArtifactReleaseInfo> {
|
|
||||||
|
|
||||||
const repoOwner = encodeURIComponent(repo.owner);
|
|
||||||
const repoName = encodeURIComponent(repo.name);
|
|
||||||
|
|
||||||
const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}`;
|
|
||||||
|
|
||||||
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
|
||||||
|
|
||||||
log.debug("Issuing request for released artifacts metadata to", requestUrl);
|
|
||||||
|
|
||||||
const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
log.error("Error fetching artifact release info", {
|
|
||||||
requestUrl,
|
|
||||||
releaseTag,
|
|
||||||
artifactFileName,
|
|
||||||
response: {
|
|
||||||
headers: response.headers,
|
|
||||||
status: response.status,
|
|
||||||
body: await response.text(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Got response ${response.status} when trying to fetch ` +
|
|
||||||
`"${artifactFileName}" artifact release info for ${releaseTag} release`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
|
|
||||||
const release: GithubRelease = await response.json();
|
|
||||||
|
|
||||||
const artifact = release.assets.find(artifact => artifact.name === artifactFileName);
|
|
||||||
|
|
||||||
if (!artifact) {
|
|
||||||
throw new Error(
|
|
||||||
`Artifact ${artifactFileName} was not found in ${release.name} release!`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
browser_download_url: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
export interface GithubRepo {
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata about particular artifact retrieved from GitHub releases.
|
|
||||||
*/
|
|
||||||
export interface ArtifactReleaseInfo {
|
|
||||||
releaseDate: Date;
|
|
||||||
releaseName: string;
|
|
||||||
downloadUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents the source of a an artifact which is either specified by the user
|
|
||||||
* explicitly, or bundled by this extension from GitHub releases.
|
|
||||||
*/
|
|
||||||
export type ArtifactSource = ArtifactSource.ExplicitPath | ArtifactSource.GithubRelease;
|
|
||||||
|
|
||||||
export namespace ArtifactSource {
|
|
||||||
/**
|
|
||||||
* Type tag for `ArtifactSource` discriminated union.
|
|
||||||
*/
|
|
||||||
export const enum Type { ExplicitPath, GithubRelease }
|
|
||||||
|
|
||||||
export interface ExplicitPath {
|
|
||||||
type: Type.ExplicitPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filesystem path to the binary specified by the user explicitly.
|
|
||||||
*/
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GithubRelease {
|
|
||||||
type: Type.GithubRelease;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository where the binary is stored.
|
|
||||||
*/
|
|
||||||
repo: GithubRepo;
|
|
||||||
|
|
||||||
|
|
||||||
// FIXME: add installationPath: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Directory on the filesystem where the bundled binary is stored.
|
|
||||||
*/
|
|
||||||
dir: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the binary file. It is stored under the same name on GitHub releases
|
|
||||||
* and in local `.dir`.
|
|
||||||
*/
|
|
||||||
file: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tag of github release that denotes a version required by this extension.
|
|
||||||
*/
|
|
||||||
tag: string;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
import * as vscode from "vscode";
|
|
||||||
import * as path from "path";
|
|
||||||
import { spawnSync } from "child_process";
|
|
||||||
|
|
||||||
import { ArtifactSource } from "./interfaces";
|
|
||||||
import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
|
|
||||||
import { downloadArtifactWithProgressUi } from "./downloads";
|
|
||||||
import { log, assert, notReentrant } from "../util";
|
|
||||||
import { Config, NIGHTLY_TAG } from "../config";
|
|
||||||
import { PersistentState } from "../persistent_state";
|
|
||||||
|
|
||||||
export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
|
|
||||||
const source = config.serverSource;
|
|
||||||
|
|
||||||
if (!source) {
|
|
||||||
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 null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (source.type) {
|
|
||||||
case ArtifactSource.Type.ExplicitPath: {
|
|
||||||
if (isBinaryAvailable(source.path)) {
|
|
||||||
return source.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
vscode.window.showErrorMessage(
|
|
||||||
`Unable to run ${source.path} binary. ` +
|
|
||||||
`To use the pre-built language server, set "rust-analyzer.serverPath" ` +
|
|
||||||
"value to `null` or remove it from the settings to use it by default."
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
case ArtifactSource.Type.GithubRelease: {
|
|
||||||
if (!shouldDownloadServer(state, source)) {
|
|
||||||
return path.join(source.dir, source.file);
|
|
||||||
}
|
|
||||||
|
|
||||||
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?",
|
|
||||||
"Download now", "Cancel"
|
|
||||||
);
|
|
||||||
if (userResponse !== "Download now") return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await downloadServer(state, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldDownloadServer(
|
|
||||||
state: PersistentState,
|
|
||||||
source: ArtifactSource.GithubRelease,
|
|
||||||
): boolean {
|
|
||||||
if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
|
|
||||||
|
|
||||||
const installed = {
|
|
||||||
tag: state.serverReleaseTag.get(),
|
|
||||||
date: state.serverReleaseDate.get()
|
|
||||||
};
|
|
||||||
const required = {
|
|
||||||
tag: source.tag,
|
|
||||||
date: state.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforcing no reentrancy for this is best-effort.
|
|
||||||
*/
|
|
||||||
const downloadServer = notReentrant(async (
|
|
||||||
state: PersistentState,
|
|
||||||
source: ArtifactSource.GithubRelease,
|
|
||||||
): Promise<null | string> => {
|
|
||||||
try {
|
|
||||||
const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
|
|
||||||
|
|
||||||
await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
|
|
||||||
await Promise.all([
|
|
||||||
state.serverReleaseTag.set(releaseInfo.releaseName),
|
|
||||||
state.serverReleaseDate.set(releaseInfo.releaseDate)
|
|
||||||
]);
|
|
||||||
} catch (err) {
|
|
||||||
log.downloadError(err, "language server", source.repo.name);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const binaryPath = path.join(source.dir, source.file);
|
|
||||||
|
|
||||||
assert(isBinaryAvailable(binaryPath),
|
|
||||||
`Downloaded language server binary is not functional.` +
|
|
||||||
`Downloaded from GitHub repo ${source.repo.owner}/${source.repo.name} ` +
|
|
||||||
`to ${binaryPath}`
|
|
||||||
);
|
|
||||||
|
|
||||||
vscode.window.showInformationMessage(
|
|
||||||
"Rust analyzer language server was successfully installed 🦀"
|
|
||||||
);
|
|
||||||
|
|
||||||
return binaryPath;
|
|
||||||
});
|
|
||||||
|
|
||||||
function isBinaryAvailable(binaryPath: string): boolean {
|
|
||||||
const res = spawnSync(binaryPath, ["--version"]);
|
|
||||||
|
|
||||||
// ACHTUNG! `res` type declaration is inherently wrong, see
|
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221
|
|
||||||
|
|
||||||
log.debug("Checked binary availablity via --version", res);
|
|
||||||
log.debug(binaryPath, "--version output:", res.output?.map(String));
|
|
||||||
|
|
||||||
return res.status === 0;
|
|
||||||
}
|
|
|
@ -1,15 +1,18 @@
|
||||||
import * as vscode from 'vscode';
|
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 * as commands from './commands';
|
||||||
import { activateInlayHints } from './inlay_hints';
|
import { activateInlayHints } from './inlay_hints';
|
||||||
import { activateStatusDisplay } from './status_display';
|
import { activateStatusDisplay } from './status_display';
|
||||||
import { Ctx } from './ctx';
|
import { Ctx } from './ctx';
|
||||||
import { activateHighlighting } from './highlighting';
|
import { activateHighlighting } from './highlighting';
|
||||||
import { ensureServerBinary } from './installation/server';
|
import { Config, NIGHTLY_TAG } from './config';
|
||||||
import { Config } from './config';
|
import { log, assert } from './util';
|
||||||
import { log } from './util';
|
|
||||||
import { ensureProperExtensionVersion } from './installation/extension';
|
|
||||||
import { PersistentState } from './persistent_state';
|
import { PersistentState } from './persistent_state';
|
||||||
|
import { fetchRelease, download } from './net';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
let ctx: Ctx | undefined;
|
let ctx: Ctx | undefined;
|
||||||
|
|
||||||
|
@ -35,27 +38,14 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||||
context.subscriptions.push(defaultOnEnter);
|
context.subscriptions.push(defaultOnEnter);
|
||||||
|
|
||||||
const config = new Config(context);
|
const config = new Config(context);
|
||||||
const state = new PersistentState(context);
|
const state = new PersistentState(context.globalState);
|
||||||
|
const serverPath = await bootstrap(config, state);
|
||||||
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)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we try to start the server before we activate type hints so that it
|
// Note: we try to start the server before we activate type hints so that it
|
||||||
// registers its `onDidChangeDocument` handler before us.
|
// registers its `onDidChangeDocument` handler before us.
|
||||||
//
|
//
|
||||||
// This a horribly, horribly wrong way to deal with this problem.
|
// 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.
|
// Commands which invokes manually via command palette, shortcut, etc.
|
||||||
ctx.registerCommand('reload', (ctx) => {
|
ctx.registerCommand('reload', (ctx) => {
|
||||||
|
@ -109,3 +99,131 @@ export async function deactivate() {
|
||||||
await ctx?.client?.stop();
|
await ctx?.client?.stop();
|
||||||
ctx = undefined;
|
ctx = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bootstrap(config: Config, state: PersistentState): Promise<string> {
|
||||||
|
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<void> {
|
||||||
|
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<string> {
|
||||||
|
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<string | undefined> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,24 +1,101 @@
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import * as path from "path";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as stream from "stream";
|
import * as stream from "stream";
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import { log, assert } from "../util";
|
import { log, assert } from "./util";
|
||||||
import { ArtifactReleaseInfo } from "./interfaces";
|
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
|
|
||||||
|
const GITHUB_API_ENDPOINT_URL = "https://api.github.com";
|
||||||
|
const OWNER = "rust-analyzer";
|
||||||
|
const REPO = "rust-analyzer";
|
||||||
|
|
||||||
|
export async function fetchRelease(
|
||||||
|
releaseTag: string
|
||||||
|
): Promise<GithubRelease> {
|
||||||
|
|
||||||
|
const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`;
|
||||||
|
|
||||||
|
const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath;
|
||||||
|
|
||||||
|
log.debug("Issuing request for released artifacts metadata to", requestUrl);
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
log.error("Error fetching artifact release info", {
|
||||||
|
requestUrl,
|
||||||
|
releaseTag,
|
||||||
|
response: {
|
||||||
|
headers: response.headers,
|
||||||
|
status: response.status,
|
||||||
|
body: await response.text(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Got response ${response.status} when trying to fetch ` +
|
||||||
|
`release info for ${releaseTag} release`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`)
|
||||||
|
const release: GithubRelease = await response.json();
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We omit declaration of tremendous amount of fields that we are not using here
|
||||||
|
export interface GithubRelease {
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
published_at: string;
|
||||||
|
assets: Array<{
|
||||||
|
name: string;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
browser_download_url: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function download(
|
||||||
|
downloadUrl: string,
|
||||||
|
destinationPath: string,
|
||||||
|
progressTitle: string,
|
||||||
|
{ mode }: { mode?: number } = {},
|
||||||
|
) {
|
||||||
|
await vscode.window.withProgress(
|
||||||
|
{
|
||||||
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
cancellable: false,
|
||||||
|
title: progressTitle
|
||||||
|
},
|
||||||
|
async (progress, _cancellationToken) => {
|
||||||
|
let lastPercentage = 0;
|
||||||
|
await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => {
|
||||||
|
const newPercentage = (readBytes / totalBytes) * 100;
|
||||||
|
progress.report({
|
||||||
|
message: newPercentage.toFixed(0) + "%",
|
||||||
|
increment: newPercentage - lastPercentage
|
||||||
|
});
|
||||||
|
|
||||||
|
lastPercentage = newPercentage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
|
* Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`.
|
||||||
* `onProgress` callback is called on recieveing each chunk of bytes
|
* `onProgress` callback is called on recieveing each chunk of bytes
|
||||||
* to track the progress of downloading, it gets the already read and total
|
* to track the progress of downloading, it gets the already read and total
|
||||||
* amount of bytes to read as its parameters.
|
* amount of bytes to read as its parameters.
|
||||||
*/
|
*/
|
||||||
export async function downloadFile(
|
async function downloadFile(
|
||||||
url: string,
|
url: string,
|
||||||
destFilePath: fs.PathLike,
|
destFilePath: fs.PathLike,
|
||||||
destFilePermissions: number,
|
mode: number | undefined,
|
||||||
onProgress: (readBytes: number, totalBytes: number) => void
|
onProgress: (readBytes: number, totalBytes: number) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
@ -41,7 +118,7 @@ export async function downloadFile(
|
||||||
onProgress(readBytes, totalBytes);
|
onProgress(readBytes, totalBytes);
|
||||||
});
|
});
|
||||||
|
|
||||||
const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions });
|
const destFileStream = fs.createWriteStream(destFilePath, { mode });
|
||||||
|
|
||||||
await pipeline(res.body, destFileStream);
|
await pipeline(res.body, destFileStream);
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
|
@ -52,46 +129,3 @@ export async function downloadFile(
|
||||||
// Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
|
// 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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,49 +1,41 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { log } from "./util";
|
import { log } from './util';
|
||||||
|
|
||||||
export class PersistentState {
|
export class PersistentState {
|
||||||
constructor(private readonly ctx: vscode.ExtensionContext) {
|
constructor(private readonly globalState: vscode.Memento) {
|
||||||
|
const { lastCheck, releaseId, serverVersion } = this;
|
||||||
|
log.debug("PersistentState: ", { lastCheck, releaseId, serverVersion });
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly installedNightlyExtensionReleaseDate = new DateStorage(
|
/**
|
||||||
"installed-nightly-extension-release-date",
|
* Used to check for *nightly* updates once an hour.
|
||||||
this.ctx.globalState
|
*/
|
||||||
);
|
get lastCheck(): number | undefined {
|
||||||
readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
|
return this.globalState.get("lastCheck");
|
||||||
readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class Storage<T> {
|
|
||||||
constructor(
|
|
||||||
private readonly key: string,
|
|
||||||
private readonly storage: vscode.Memento,
|
|
||||||
private readonly defaultVal: T
|
|
||||||
) { }
|
|
||||||
|
|
||||||
get(): T {
|
|
||||||
const val = this.storage.get(this.key, this.defaultVal);
|
|
||||||
log.debug(this.key, "==", val);
|
|
||||||
return val;
|
|
||||||
}
|
}
|
||||||
async set(val: T) {
|
async updateLastCheck(value: number) {
|
||||||
log.debug(this.key, "=", val);
|
await this.globalState.update("lastCheck", value);
|
||||||
await this.storage.update(this.key, val);
|
}
|
||||||
}
|
|
||||||
}
|
/**
|
||||||
export class DateStorage {
|
* Release id of the *nightly* extension.
|
||||||
inner: Storage<null | string>;
|
* Used to check if we should update.
|
||||||
|
*/
|
||||||
constructor(key: string, storage: vscode.Memento) {
|
get releaseId(): number | undefined {
|
||||||
this.inner = new Storage(key, storage, null);
|
return this.globalState.get("releaseId");
|
||||||
}
|
}
|
||||||
|
async updateReleaseId(value: number) {
|
||||||
get(): null | Date {
|
await this.globalState.update("releaseId", value);
|
||||||
const dateStr = this.inner.get();
|
}
|
||||||
return dateStr ? new Date(dateStr) : null;
|
|
||||||
}
|
/**
|
||||||
|
* Version of the extension that installed the server.
|
||||||
async set(date: null | Date) {
|
* Used to check if we need to update the server.
|
||||||
await this.inner.set(date ? date.toString() : null);
|
*/
|
||||||
|
get serverVersion(): string | undefined {
|
||||||
|
return this.globalState.get("serverVersion");
|
||||||
|
}
|
||||||
|
async updateServerVersion(value: string | undefined) {
|
||||||
|
await this.globalState.update("serverVersion", value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as lc from "vscode-languageclient";
|
import * as lc from "vscode-languageclient";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { promises as dns } from "dns";
|
|
||||||
import { strict as nativeAssert } from "assert";
|
import { strict as nativeAssert } from "assert";
|
||||||
|
|
||||||
export function assert(condition: boolean, explanation: string): asserts condition {
|
export function assert(condition: boolean, explanation: string): asserts condition {
|
||||||
|
@ -31,22 +30,6 @@ export const log = new class {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message, ...optionalParams);
|
console.error(message, ...optionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendRequestWithRetry<TParam, TRet>(
|
export async function sendRequestWithRetry<TParam, TRet>(
|
||||||
|
@ -86,17 +69,6 @@ function sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notReentrant<TThis, TParams extends any[], TRet>(
|
|
||||||
fn: (this: TThis, ...params: TParams) => Promise<TRet>
|
|
||||||
): 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 RustDocument = vscode.TextDocument & { languageId: "rust" };
|
||||||
export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
|
export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string };
|
||||||
|
|
||||||
|
@ -110,29 +82,3 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
|
||||||
export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
|
export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
|
||||||
return isRustDocument(editor.document);
|
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<never> {
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue