diff options
author | Aleksey Kladov <[email protected]> | 2020-03-17 11:44:31 +0000 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2020-03-19 08:04:59 +0000 |
commit | fb6e655de8a44c65275ad45a27bf5bd684670ba0 (patch) | |
tree | 9c307ac69c8fc59465ee2fb6f9a8a619fc064167 /editors | |
parent | f0a1b64d7ee3baa7ccf980b35b85f0a4a3b85b1a (diff) |
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).
Diffstat (limited to 'editors')
-rw-r--r-- | editors/code/package.json | 2 | ||||
-rw-r--r-- | editors/code/src/commands/server_version.ts | 16 | ||||
-rw-r--r-- | editors/code/src/config.ts | 103 | ||||
-rw-r--r-- | editors/code/src/ctx.ts | 9 | ||||
-rw-r--r-- | editors/code/src/installation/extension.ts | 146 | ||||
-rw-r--r-- | editors/code/src/installation/fetch_artifact_release_info.ts | 77 | ||||
-rw-r--r-- | editors/code/src/installation/interfaces.ts | 63 | ||||
-rw-r--r-- | editors/code/src/installation/server.ts | 131 | ||||
-rw-r--r-- | editors/code/src/main.ts | 158 | ||||
-rw-r--r-- | editors/code/src/net.ts (renamed from editors/code/src/installation/downloads.ts) | 132 | ||||
-rw-r--r-- | editors/code/src/persistent_state.ts | 64 | ||||
-rw-r--r-- | editors/code/src/util.ts | 54 |
12 files changed, 264 insertions, 691 deletions
diff --git a/editors/code/package.json b/editors/code/package.json index b9e0ffd2b..6528ff071 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -228,7 +228,7 @@ | |||
228 | "default": "stable", | 228 | "default": "stable", |
229 | "markdownEnumDescriptions": [ | 229 | "markdownEnumDescriptions": [ |
230 | "`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general", | 230 | "`\"stable\"` updates are shipped weekly, they don't contain cutting-edge features from VSCode proposed APIs but have less bugs in general", |
231 | "`\"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**" | 231 | "`\"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**" |
232 | ], | 232 | ], |
233 | "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" | 233 | "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" |
234 | }, | 234 | }, |
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts index 83b1acf67..03528b825 100644 --- a/editors/code/src/commands/server_version.ts +++ b/editors/code/src/commands/server_version.ts | |||
@@ -1,20 +1,10 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from "vscode"; |
2 | import { ensureServerBinary } from '../installation/server'; | 2 | import { spawnSync } from "child_process"; |
3 | import { Ctx, Cmd } from '../ctx'; | 3 | import { Ctx, Cmd } from '../ctx'; |
4 | import { spawnSync } from 'child_process'; | ||
5 | 4 | ||
6 | export function serverVersion(ctx: Ctx): Cmd { | 5 | export function serverVersion(ctx: Ctx): Cmd { |
7 | return async () => { | 6 | return async () => { |
8 | const binaryPath = await ensureServerBinary(ctx.config, ctx.state); | 7 | const version = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }).stdout; |
9 | |||
10 | if (binaryPath == null) { | ||
11 | throw new Error( | ||
12 | "Rust Analyzer Language Server is not available. " + | ||
13 | "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." | ||
14 | ); | ||
15 | } | ||
16 | |||
17 | const version = spawnSync(binaryPath, ["--version"], { encoding: "utf8" }).stdout; | ||
18 | vscode.window.showInformationMessage('rust-analyzer version : ' + version); | 8 | vscode.window.showInformationMessage('rust-analyzer version : ' + version); |
19 | }; | 9 | }; |
20 | } | 10 | } |
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index b45b14bef..28698ab8e 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts | |||
@@ -1,9 +1,5 @@ | |||
1 | import * as os from "os"; | ||
2 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
3 | import { ArtifactSource } from "./installation/interfaces"; | 2 | import { log } from "./util"; |
4 | import { log, vscodeReloadWindow } from "./util"; | ||
5 | |||
6 | const RA_LSP_DEBUG = process.env.__RA_LSP_SERVER_DEBUG; | ||
7 | 3 | ||
8 | export interface InlayHintOptions { | 4 | export interface InlayHintOptions { |
9 | typeHints: boolean; | 5 | typeHints: boolean; |
@@ -25,10 +21,7 @@ export interface CargoFeatures { | |||
25 | loadOutDirsFromCheck: boolean; | 21 | loadOutDirsFromCheck: boolean; |
26 | } | 22 | } |
27 | 23 | ||
28 | export const enum UpdatesChannel { | 24 | export type UpdatesChannel = "stable" | "nightly"; |
29 | Stable = "stable", | ||
30 | Nightly = "nightly" | ||
31 | } | ||
32 | 25 | ||
33 | export const NIGHTLY_TAG = "nightly"; | 26 | export const NIGHTLY_TAG = "nightly"; |
34 | export class Config { | 27 | export class Config { |
@@ -41,6 +34,7 @@ export class Config { | |||
41 | "cargo-watch", | 34 | "cargo-watch", |
42 | "highlighting.semanticTokens", | 35 | "highlighting.semanticTokens", |
43 | "inlayHints", | 36 | "inlayHints", |
37 | "updates.channel", | ||
44 | ] | 38 | ] |
45 | .map(opt => `${this.rootSection}.${opt}`); | 39 | .map(opt => `${this.rootSection}.${opt}`); |
46 | 40 | ||
@@ -94,100 +88,17 @@ export class Config { | |||
94 | ); | 88 | ); |
95 | 89 | ||
96 | if (userResponse === "Reload now") { | 90 | if (userResponse === "Reload now") { |
97 | await vscodeReloadWindow(); | 91 | await vscode.commands.executeCommand("workbench.action.reloadWindow"); |
98 | } | 92 | } |
99 | } | 93 | } |
100 | 94 | ||
101 | private static replaceTildeWithHomeDir(path: string) { | 95 | get globalStoragePath(): string { return this.ctx.globalStoragePath; } |
102 | if (path.startsWith("~/")) { | ||
103 | return os.homedir() + path.slice("~".length); | ||
104 | } | ||
105 | return path; | ||
106 | } | ||
107 | |||
108 | /** | ||
109 | * Name of the binary artifact for `rust-analyzer` that is published for | ||
110 | * `platform` on GitHub releases. (It is also stored under the same name when | ||
111 | * downloaded by the extension). | ||
112 | */ | ||
113 | get prebuiltServerFileName(): null | string { | ||
114 | // See possible `arch` values here: | ||
115 | // https://nodejs.org/api/process.html#process_process_arch | ||
116 | |||
117 | switch (process.platform) { | ||
118 | |||
119 | case "linux": { | ||
120 | switch (process.arch) { | ||
121 | case "arm": | ||
122 | case "arm64": return null; | ||
123 | |||
124 | default: return "rust-analyzer-linux"; | ||
125 | } | ||
126 | } | ||
127 | |||
128 | case "darwin": return "rust-analyzer-mac"; | ||
129 | case "win32": return "rust-analyzer-windows.exe"; | ||
130 | |||
131 | // Users on these platforms yet need to manually build from sources | ||
132 | case "aix": | ||
133 | case "android": | ||
134 | case "freebsd": | ||
135 | case "openbsd": | ||
136 | case "sunos": | ||
137 | case "cygwin": | ||
138 | case "netbsd": return null; | ||
139 | // The list of platforms is exhaustive (see `NodeJS.Platform` type definition) | ||
140 | } | ||
141 | } | ||
142 | |||
143 | get installedExtensionUpdateChannel(): UpdatesChannel { | ||
144 | return this.extensionReleaseTag === NIGHTLY_TAG | ||
145 | ? UpdatesChannel.Nightly | ||
146 | : UpdatesChannel.Stable; | ||
147 | } | ||
148 | |||
149 | get serverSource(): null | ArtifactSource { | ||
150 | const serverPath = RA_LSP_DEBUG ?? this.serverPath; | ||
151 | |||
152 | if (serverPath) { | ||
153 | return { | ||
154 | type: ArtifactSource.Type.ExplicitPath, | ||
155 | path: Config.replaceTildeWithHomeDir(serverPath) | ||
156 | }; | ||
157 | } | ||
158 | |||
159 | const prebuiltBinaryName = this.prebuiltServerFileName; | ||
160 | |||
161 | if (!prebuiltBinaryName) return null; | ||
162 | |||
163 | return this.createGithubReleaseSource( | ||
164 | prebuiltBinaryName, | ||
165 | this.extensionReleaseTag | ||
166 | ); | ||
167 | } | ||
168 | |||
169 | private createGithubReleaseSource(file: string, tag: string): ArtifactSource.GithubRelease { | ||
170 | return { | ||
171 | type: ArtifactSource.Type.GithubRelease, | ||
172 | file, | ||
173 | tag, | ||
174 | dir: this.ctx.globalStoragePath, | ||
175 | repo: { | ||
176 | name: "rust-analyzer", | ||
177 | owner: "rust-analyzer", | ||
178 | } | ||
179 | }; | ||
180 | } | ||
181 | |||
182 | get nightlyVsixSource(): ArtifactSource.GithubRelease { | ||
183 | return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG); | ||
184 | } | ||
185 | 96 | ||
186 | // We don't do runtime config validation here for simplicity. More on stackoverflow: | 97 | // We don't do runtime config validation here for simplicity. More on stackoverflow: |
187 | // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension | 98 | // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension |
188 | 99 | ||
189 | private get serverPath() { return this.cfg.get("serverPath") as null | string; } | 100 | get serverPath() { return this.cfg.get("serverPath") as null | string; } |
190 | get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } | 101 | get channel() { return this.cfg.get<"stable" | "nightly">("updates.channel")!; } |
191 | get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } | 102 | get askBeforeDownload() { return this.cfg.get("updates.askBeforeDownload") as boolean; } |
192 | get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } | 103 | get highlightingSemanticTokens() { return this.cfg.get("highlighting.semanticTokens") as boolean; } |
193 | get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } | 104 | get highlightingOn() { return this.cfg.get("highlightingOn") as boolean; } |
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index c929ab063..84c170ea8 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts | |||
@@ -4,21 +4,20 @@ import * as lc from 'vscode-languageclient'; | |||
4 | import { Config } from './config'; | 4 | import { Config } from './config'; |
5 | import { createClient } from './client'; | 5 | import { createClient } from './client'; |
6 | import { isRustEditor, RustEditor } from './util'; | 6 | import { isRustEditor, RustEditor } from './util'; |
7 | import { PersistentState } from './persistent_state'; | ||
8 | 7 | ||
9 | export class Ctx { | 8 | export class Ctx { |
10 | private constructor( | 9 | private constructor( |
11 | readonly config: Config, | 10 | readonly config: Config, |
12 | readonly state: PersistentState, | ||
13 | private readonly extCtx: vscode.ExtensionContext, | 11 | private readonly extCtx: vscode.ExtensionContext, |
14 | readonly client: lc.LanguageClient | 12 | readonly client: lc.LanguageClient, |
13 | readonly serverPath: string, | ||
15 | ) { | 14 | ) { |
16 | 15 | ||
17 | } | 16 | } |
18 | 17 | ||
19 | static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { | 18 | static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { |
20 | const client = await createClient(config, serverPath); | 19 | const client = await createClient(config, serverPath); |
21 | const res = new Ctx(config, state, extCtx, client); | 20 | const res = new Ctx(config, extCtx, client, serverPath); |
22 | res.pushCleanup(client.start()); | 21 | res.pushCleanup(client.start()); |
23 | await client.onReady(); | 22 | await client.onReady(); |
24 | return res; | 23 | return res; |
diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts deleted file mode 100644 index a1db96f05..000000000 --- a/editors/code/src/installation/extension.ts +++ /dev/null | |||
@@ -1,146 +0,0 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { promises as fs } from 'fs'; | ||
4 | |||
5 | import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util"; | ||
6 | import { Config, UpdatesChannel } from "../config"; | ||
7 | import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces"; | ||
8 | import { downloadArtifactWithProgressUi } from "./downloads"; | ||
9 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; | ||
10 | import { PersistentState } from "../persistent_state"; | ||
11 | |||
12 | const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; | ||
13 | |||
14 | /** | ||
15 | * Installs `stable` or latest `nightly` version or does nothing if the current | ||
16 | * extension version is what's needed according to `desiredUpdateChannel`. | ||
17 | */ | ||
18 | export async function ensureProperExtensionVersion(config: Config, state: PersistentState): Promise<never | void> { | ||
19 | // User has built lsp server from sources, she should manage updates manually | ||
20 | if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return; | ||
21 | |||
22 | const currentUpdChannel = config.installedExtensionUpdateChannel; | ||
23 | const desiredUpdChannel = config.updatesChannel; | ||
24 | |||
25 | if (currentUpdChannel === UpdatesChannel.Stable) { | ||
26 | // Release date is present only when we are on nightly | ||
27 | await state.installedNightlyExtensionReleaseDate.set(null); | ||
28 | } | ||
29 | |||
30 | if (desiredUpdChannel === UpdatesChannel.Stable) { | ||
31 | // VSCode should handle updates for stable channel | ||
32 | if (currentUpdChannel === UpdatesChannel.Stable) return; | ||
33 | |||
34 | if (!await askToDownloadProperExtensionVersion(config)) return; | ||
35 | |||
36 | await vscodeReinstallExtension(config.extensionId); | ||
37 | await vscodeReloadWindow(); // never returns | ||
38 | } | ||
39 | |||
40 | if (currentUpdChannel === UpdatesChannel.Stable) { | ||
41 | if (!await askToDownloadProperExtensionVersion(config)) return; | ||
42 | |||
43 | return await tryDownloadNightlyExtension(config, state); | ||
44 | } | ||
45 | |||
46 | const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get(); | ||
47 | |||
48 | if (currentExtReleaseDate === null) { | ||
49 | void vscode.window.showErrorMessage( | ||
50 | "Nightly release date must've been set during the installation. " + | ||
51 | "Did you download and install the nightly .vsix package manually?" | ||
52 | ); | ||
53 | throw new Error("Nightly release date was not set in globalStorage"); | ||
54 | } | ||
55 | |||
56 | const dateNow = new Date; | ||
57 | const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow); | ||
58 | log.debug( | ||
59 | "Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate, | ||
60 | "hours ago, namely:", currentExtReleaseDate, "and now is", dateNow | ||
61 | ); | ||
62 | |||
63 | if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) { | ||
64 | return; | ||
65 | } | ||
66 | if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) { | ||
67 | return; | ||
68 | } | ||
69 | |||
70 | await tryDownloadNightlyExtension(config, state, releaseInfo => { | ||
71 | assert( | ||
72 | currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(), | ||
73 | "Other active VSCode instance has reinstalled the extension" | ||
74 | ); | ||
75 | |||
76 | if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) { | ||
77 | vscode.window.showInformationMessage( | ||
78 | "Whoops, it appears that your nightly version is up-to-date. " + | ||
79 | "There might be some problems with the upcomming nightly release " + | ||
80 | "or you traveled too far into the future. Sorry for that 😅! " | ||
81 | ); | ||
82 | return false; | ||
83 | } | ||
84 | return true; | ||
85 | }); | ||
86 | } | ||
87 | |||
88 | async function askToDownloadProperExtensionVersion(config: Config, reason = "") { | ||
89 | if (!config.askBeforeDownload) return true; | ||
90 | |||
91 | const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly"; | ||
92 | |||
93 | // In case of reentering this function and showing the same info message | ||
94 | // (e.g. after we had shown this message, the user changed the config) | ||
95 | // vscode will dismiss the already shown one (i.e. return undefined). | ||
96 | // This behaviour is what we want, but likely it is not documented | ||
97 | |||
98 | const userResponse = await vscode.window.showInformationMessage( | ||
99 | reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` + | ||
100 | `version and reload the window now?`, | ||
101 | "Download now", "Cancel" | ||
102 | ); | ||
103 | return userResponse === "Download now"; | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Shutdowns the process in case of success (i.e. reloads the window) or throws an error. | ||
108 | * | ||
109 | * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during | ||
110 | * each of them would result in a ton of code (especially accounting for cross-process | ||
111 | * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort. | ||
112 | */ | ||
113 | const tryDownloadNightlyExtension = notReentrant(async ( | ||
114 | config: Config, | ||
115 | state: PersistentState, | ||
116 | shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true | ||
117 | ): Promise<never | void> => { | ||
118 | const vsixSource = config.nightlyVsixSource; | ||
119 | try { | ||
120 | const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag); | ||
121 | |||
122 | if (!shouldDownload(releaseInfo)) return; | ||
123 | |||
124 | await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension"); | ||
125 | |||
126 | const vsixPath = path.join(vsixSource.dir, vsixSource.file); | ||
127 | |||
128 | await vscodeInstallExtensionFromVsix(vsixPath); | ||
129 | await state.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate); | ||
130 | await fs.unlink(vsixPath); | ||
131 | |||
132 | await vscodeReloadWindow(); // never returns | ||
133 | } catch (err) { | ||
134 | log.downloadError(err, "nightly extension", vsixSource.repo.name); | ||
135 | } | ||
136 | }); | ||
137 | |||
138 | function diffInHours(a: Date, b: Date): number { | ||
139 | // Discard the time and time-zone information (to abstract from daylight saving time bugs) | ||
140 | // https://stackoverflow.com/a/15289883/9259330 | ||
141 | |||
142 | const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); | ||
143 | const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); | ||
144 | |||
145 | return (utcA - utcB) / (1000 * 60 * 60); | ||
146 | } | ||
diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts deleted file mode 100644 index 1ad3b8338..000000000 --- a/editors/code/src/installation/fetch_artifact_release_info.ts +++ /dev/null | |||
@@ -1,77 +0,0 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import { GithubRepo, ArtifactReleaseInfo } from "./interfaces"; | ||
3 | import { log } from "../util"; | ||
4 | |||
5 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | ||
6 | |||
7 | /** | ||
8 | * Fetches the release with `releaseTag` from GitHub `repo` and | ||
9 | * returns metadata about `artifactFileName` shipped with | ||
10 | * this release. | ||
11 | * | ||
12 | * @throws Error upon network failure or if no such repository, release, or artifact exists. | ||
13 | */ | ||
14 | export async function fetchArtifactReleaseInfo( | ||
15 | repo: GithubRepo, | ||
16 | artifactFileName: string, | ||
17 | releaseTag: string | ||
18 | ): Promise<ArtifactReleaseInfo> { | ||
19 | |||
20 | const repoOwner = encodeURIComponent(repo.owner); | ||
21 | const repoName = encodeURIComponent(repo.name); | ||
22 | |||
23 | const apiEndpointPath = `/repos/${repoOwner}/${repoName}/releases/tags/${releaseTag}`; | ||
24 | |||
25 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | ||
26 | |||
27 | log.debug("Issuing request for released artifacts metadata to", requestUrl); | ||
28 | |||
29 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); | ||
30 | |||
31 | if (!response.ok) { | ||
32 | log.error("Error fetching artifact release info", { | ||
33 | requestUrl, | ||
34 | releaseTag, | ||
35 | artifactFileName, | ||
36 | response: { | ||
37 | headers: response.headers, | ||
38 | status: response.status, | ||
39 | body: await response.text(), | ||
40 | } | ||
41 | }); | ||
42 | |||
43 | throw new Error( | ||
44 | `Got response ${response.status} when trying to fetch ` + | ||
45 | `"${artifactFileName}" artifact release info for ${releaseTag} release` | ||
46 | ); | ||
47 | } | ||
48 | |||
49 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | ||
50 | const release: GithubRelease = await response.json(); | ||
51 | |||
52 | const artifact = release.assets.find(artifact => artifact.name === artifactFileName); | ||
53 | |||
54 | if (!artifact) { | ||
55 | throw new Error( | ||
56 | `Artifact ${artifactFileName} was not found in ${release.name} release!` | ||
57 | ); | ||
58 | } | ||
59 | |||
60 | return { | ||
61 | releaseName: release.name, | ||
62 | releaseDate: new Date(release.published_at), | ||
63 | downloadUrl: artifact.browser_download_url | ||
64 | }; | ||
65 | |||
66 | // We omit declaration of tremendous amount of fields that we are not using here | ||
67 | interface GithubRelease { | ||
68 | name: string; | ||
69 | // eslint-disable-next-line camelcase | ||
70 | published_at: string; | ||
71 | assets: Array<{ | ||
72 | name: string; | ||
73 | // eslint-disable-next-line camelcase | ||
74 | browser_download_url: string; | ||
75 | }>; | ||
76 | } | ||
77 | } | ||
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts deleted file mode 100644 index 1a8ea0884..000000000 --- a/editors/code/src/installation/interfaces.ts +++ /dev/null | |||
@@ -1,63 +0,0 @@ | |||
1 | export interface GithubRepo { | ||
2 | name: string; | ||
3 | owner: string; | ||
4 | } | ||
5 | |||
6 | /** | ||
7 | * Metadata about particular artifact retrieved from GitHub releases. | ||
8 | */ | ||
9 | export interface ArtifactReleaseInfo { | ||
10 | releaseDate: Date; | ||
11 | releaseName: string; | ||
12 | downloadUrl: string; | ||
13 | } | ||
14 | |||
15 | /** | ||
16 | * Represents the source of a an artifact which is either specified by the user | ||
17 | * explicitly, or bundled by this extension from GitHub releases. | ||
18 | */ | ||
19 | export type ArtifactSource = ArtifactSource.ExplicitPath | ArtifactSource.GithubRelease; | ||
20 | |||
21 | export namespace ArtifactSource { | ||
22 | /** | ||
23 | * Type tag for `ArtifactSource` discriminated union. | ||
24 | */ | ||
25 | export const enum Type { ExplicitPath, GithubRelease } | ||
26 | |||
27 | export interface ExplicitPath { | ||
28 | type: Type.ExplicitPath; | ||
29 | |||
30 | /** | ||
31 | * Filesystem path to the binary specified by the user explicitly. | ||
32 | */ | ||
33 | path: string; | ||
34 | } | ||
35 | |||
36 | export interface GithubRelease { | ||
37 | type: Type.GithubRelease; | ||
38 | |||
39 | /** | ||
40 | * Repository where the binary is stored. | ||
41 | */ | ||
42 | repo: GithubRepo; | ||
43 | |||
44 | |||
45 | // FIXME: add installationPath: string; | ||
46 | |||
47 | /** | ||
48 | * Directory on the filesystem where the bundled binary is stored. | ||
49 | */ | ||
50 | dir: string; | ||
51 | |||
52 | /** | ||
53 | * Name of the binary file. It is stored under the same name on GitHub releases | ||
54 | * and in local `.dir`. | ||
55 | */ | ||
56 | file: string; | ||
57 | |||
58 | /** | ||
59 | * Tag of github release that denotes a version required by this extension. | ||
60 | */ | ||
61 | tag: string; | ||
62 | } | ||
63 | } | ||
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts deleted file mode 100644 index 05d326131..000000000 --- a/editors/code/src/installation/server.ts +++ /dev/null | |||
@@ -1,131 +0,0 @@ | |||
1 | import * as vscode from "vscode"; | ||
2 | import * as path from "path"; | ||
3 | import { spawnSync } from "child_process"; | ||
4 | |||
5 | import { ArtifactSource } from "./interfaces"; | ||
6 | import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; | ||
7 | import { downloadArtifactWithProgressUi } from "./downloads"; | ||
8 | import { log, assert, notReentrant } from "../util"; | ||
9 | import { Config, NIGHTLY_TAG } from "../config"; | ||
10 | import { PersistentState } from "../persistent_state"; | ||
11 | |||
12 | export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> { | ||
13 | const source = config.serverSource; | ||
14 | |||
15 | if (!source) { | ||
16 | vscode.window.showErrorMessage( | ||
17 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
18 | "You need to manually clone rust-analyzer repository and " + | ||
19 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
20 | "If you feel that your platform should be supported, please create an issue " + | ||
21 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
22 | "will consider it." | ||
23 | ); | ||
24 | return null; | ||
25 | } | ||
26 | |||
27 | switch (source.type) { | ||
28 | case ArtifactSource.Type.ExplicitPath: { | ||
29 | if (isBinaryAvailable(source.path)) { | ||
30 | return source.path; | ||
31 | } | ||
32 | |||
33 | vscode.window.showErrorMessage( | ||
34 | `Unable to run ${source.path} binary. ` + | ||
35 | `To use the pre-built language server, set "rust-analyzer.serverPath" ` + | ||
36 | "value to `null` or remove it from the settings to use it by default." | ||
37 | ); | ||
38 | return null; | ||
39 | } | ||
40 | case ArtifactSource.Type.GithubRelease: { | ||
41 | if (!shouldDownloadServer(state, source)) { | ||
42 | return path.join(source.dir, source.file); | ||
43 | } | ||
44 | |||
45 | if (config.askBeforeDownload) { | ||
46 | const userResponse = await vscode.window.showInformationMessage( | ||
47 | `Language server version ${source.tag} for rust-analyzer is not installed. ` + | ||
48 | "Do you want to download it now?", | ||
49 | "Download now", "Cancel" | ||
50 | ); | ||
51 | if (userResponse !== "Download now") return null; | ||
52 | } | ||
53 | |||
54 | return await downloadServer(state, source); | ||
55 | } | ||
56 | } | ||
57 | } | ||
58 | |||
59 | function shouldDownloadServer( | ||
60 | state: PersistentState, | ||
61 | source: ArtifactSource.GithubRelease, | ||
62 | ): boolean { | ||
63 | if (!isBinaryAvailable(path.join(source.dir, source.file))) return true; | ||
64 | |||
65 | const installed = { | ||
66 | tag: state.serverReleaseTag.get(), | ||
67 | date: state.serverReleaseDate.get() | ||
68 | }; | ||
69 | const required = { | ||
70 | tag: source.tag, | ||
71 | date: state.installedNightlyExtensionReleaseDate.get() | ||
72 | }; | ||
73 | |||
74 | log.debug("Installed server:", installed, "required:", required); | ||
75 | |||
76 | if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) { | ||
77 | return required.tag !== installed.tag; | ||
78 | } | ||
79 | |||
80 | assert(required.date !== null, "Extension release date should have been saved during its installation"); | ||
81 | assert(installed.date !== null, "Server release date should have been saved during its installation"); | ||
82 | |||
83 | return installed.date.getTime() !== required.date.getTime(); | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Enforcing no reentrancy for this is best-effort. | ||
88 | */ | ||
89 | const downloadServer = notReentrant(async ( | ||
90 | state: PersistentState, | ||
91 | source: ArtifactSource.GithubRelease, | ||
92 | ): Promise<null | string> => { | ||
93 | try { | ||
94 | const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); | ||
95 | |||
96 | await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server"); | ||
97 | await Promise.all([ | ||
98 | state.serverReleaseTag.set(releaseInfo.releaseName), | ||
99 | state.serverReleaseDate.set(releaseInfo.releaseDate) | ||
100 | ]); | ||
101 | } catch (err) { | ||
102 | log.downloadError(err, "language server", source.repo.name); | ||
103 | return null; | ||
104 | } | ||
105 | |||
106 | const binaryPath = path.join(source.dir, source.file); | ||
107 | |||
108 | assert(isBinaryAvailable(binaryPath), | ||
109 | `Downloaded language server binary is not functional.` + | ||
110 | `Downloaded from GitHub repo ${source.repo.owner}/${source.repo.name} ` + | ||
111 | `to ${binaryPath}` | ||
112 | ); | ||
113 | |||
114 | vscode.window.showInformationMessage( | ||
115 | "Rust analyzer language server was successfully installed 🦀" | ||
116 | ); | ||
117 | |||
118 | return binaryPath; | ||
119 | }); | ||
120 | |||
121 | function isBinaryAvailable(binaryPath: string): boolean { | ||
122 | const res = spawnSync(binaryPath, ["--version"]); | ||
123 | |||
124 | // ACHTUNG! `res` type declaration is inherently wrong, see | ||
125 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42221 | ||
126 | |||
127 | log.debug("Checked binary availablity via --version", res); | ||
128 | log.debug(binaryPath, "--version output:", res.output?.map(String)); | ||
129 | |||
130 | return res.status === 0; | ||
131 | } | ||
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 94ecd4dab..d907f3e6f 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts | |||
@@ -1,15 +1,18 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import * as path from "path"; | ||
3 | import * as os from "os"; | ||
4 | import { promises as fs } from "fs"; | ||
2 | 5 | ||
3 | import * as commands from './commands'; | 6 | import * as commands from './commands'; |
4 | import { activateInlayHints } from './inlay_hints'; | 7 | import { activateInlayHints } from './inlay_hints'; |
5 | import { activateStatusDisplay } from './status_display'; | 8 | import { activateStatusDisplay } from './status_display'; |
6 | import { Ctx } from './ctx'; | 9 | import { Ctx } from './ctx'; |
7 | import { activateHighlighting } from './highlighting'; | 10 | import { activateHighlighting } from './highlighting'; |
8 | import { ensureServerBinary } from './installation/server'; | 11 | import { Config, NIGHTLY_TAG } from './config'; |
9 | import { Config } from './config'; | 12 | import { log, assert } from './util'; |
10 | import { log } from './util'; | ||
11 | import { ensureProperExtensionVersion } from './installation/extension'; | ||
12 | import { PersistentState } from './persistent_state'; | 13 | import { PersistentState } from './persistent_state'; |
14 | import { fetchRelease, download } from './net'; | ||
15 | import { spawnSync } from 'child_process'; | ||
13 | 16 | ||
14 | let ctx: Ctx | undefined; | 17 | let ctx: Ctx | undefined; |
15 | 18 | ||
@@ -35,27 +38,14 @@ export async function activate(context: vscode.ExtensionContext) { | |||
35 | context.subscriptions.push(defaultOnEnter); | 38 | context.subscriptions.push(defaultOnEnter); |
36 | 39 | ||
37 | const config = new Config(context); | 40 | const config = new Config(context); |
38 | const state = new PersistentState(context); | 41 | const state = new PersistentState(context.globalState); |
39 | 42 | const serverPath = await bootstrap(config, state); | |
40 | vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error)); | ||
41 | |||
42 | // Don't await the user response here, otherwise we will block the lsp server bootstrap | ||
43 | void ensureProperExtensionVersion(config, state).catch(log.error); | ||
44 | |||
45 | const serverPath = await ensureServerBinary(config, state); | ||
46 | |||
47 | if (serverPath == null) { | ||
48 | throw new Error( | ||
49 | "Rust Analyzer Language Server is not available. " + | ||
50 | "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." | ||
51 | ); | ||
52 | } | ||
53 | 43 | ||
54 | // Note: we try to start the server before we activate type hints so that it | 44 | // Note: we try to start the server before we activate type hints so that it |
55 | // registers its `onDidChangeDocument` handler before us. | 45 | // registers its `onDidChangeDocument` handler before us. |
56 | // | 46 | // |
57 | // This a horribly, horribly wrong way to deal with this problem. | 47 | // This a horribly, horribly wrong way to deal with this problem. |
58 | ctx = await Ctx.create(config, state, context, serverPath); | 48 | ctx = await Ctx.create(config, context, serverPath); |
59 | 49 | ||
60 | // Commands which invokes manually via command palette, shortcut, etc. | 50 | // Commands which invokes manually via command palette, shortcut, etc. |
61 | ctx.registerCommand('reload', (ctx) => { | 51 | ctx.registerCommand('reload', (ctx) => { |
@@ -109,3 +99,131 @@ export async function deactivate() { | |||
109 | await ctx?.client?.stop(); | 99 | await ctx?.client?.stop(); |
110 | ctx = undefined; | 100 | ctx = undefined; |
111 | } | 101 | } |
102 | |||
103 | async function bootstrap(config: Config, state: PersistentState): Promise<string> { | ||
104 | await fs.mkdir(config.globalStoragePath, { recursive: true }); | ||
105 | |||
106 | await bootstrapExtension(config, state); | ||
107 | const path = await bootstrapServer(config, state); | ||
108 | |||
109 | return path; | ||
110 | } | ||
111 | |||
112 | async function bootstrapExtension(config: Config, state: PersistentState): Promise<void> { | ||
113 | if (config.channel === "stable") { | ||
114 | if (config.extensionReleaseTag === NIGHTLY_TAG) { | ||
115 | vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension. | ||
116 | To switch to stable, uninstall the extension and re-install it from the marketplace`); | ||
117 | } | ||
118 | return; | ||
119 | }; | ||
120 | |||
121 | const lastCheck = state.lastCheck; | ||
122 | const now = Date.now(); | ||
123 | |||
124 | const anHour = 60 * 60 * 1000; | ||
125 | const shouldDownloadNightly = state.releaseId === undefined || (now - (lastCheck ?? 0)) > anHour; | ||
126 | |||
127 | if (!shouldDownloadNightly) return; | ||
128 | |||
129 | const release = await fetchRelease("nightly").catch((e) => { | ||
130 | log.error(e); | ||
131 | if (state.releaseId === undefined) { // Show error only for the initial download | ||
132 | vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`); | ||
133 | } | ||
134 | return undefined; | ||
135 | }); | ||
136 | if (release === undefined || release.id === state.releaseId) return; | ||
137 | |||
138 | const userResponse = await vscode.window.showInformationMessage( | ||
139 | "New version of rust-analyzer (nightly) is available (requires reload).", | ||
140 | "Update" | ||
141 | ); | ||
142 | if (userResponse !== "Update") return; | ||
143 | |||
144 | const artifact = release.assets.find(artifact => artifact.name === "rust-analyzer.vsix"); | ||
145 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); | ||
146 | |||
147 | const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); | ||
148 | await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension"); | ||
149 | |||
150 | await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); | ||
151 | await fs.unlink(dest); | ||
152 | |||
153 | await state.updateReleaseId(release.id); | ||
154 | await state.updateLastCheck(now); | ||
155 | await vscode.commands.executeCommand("workbench.action.reloadWindow"); | ||
156 | } | ||
157 | |||
158 | async function bootstrapServer(config: Config, state: PersistentState): Promise<string> { | ||
159 | const path = await getServer(config, state); | ||
160 | if (!path) { | ||
161 | throw new Error( | ||
162 | "Rust Analyzer Language Server is not available. " + | ||
163 | "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." | ||
164 | ); | ||
165 | } | ||
166 | |||
167 | const res = spawnSync(path, ["--version"], { encoding: 'utf8' }); | ||
168 | log.debug("Checked binary availability via --version", res); | ||
169 | log.debug(res, "--version output:", res.output); | ||
170 | if (res.status !== 0) { | ||
171 | throw new Error( | ||
172 | `Failed to execute ${path} --version` | ||
173 | ); | ||
174 | } | ||
175 | |||
176 | return path; | ||
177 | } | ||
178 | |||
179 | async function getServer(config: Config, state: PersistentState): Promise<string | undefined> { | ||
180 | const explicitPath = process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; | ||
181 | if (explicitPath) { | ||
182 | if (explicitPath.startsWith("~/")) { | ||
183 | return os.homedir() + explicitPath.slice("~".length); | ||
184 | } | ||
185 | return explicitPath; | ||
186 | }; | ||
187 | |||
188 | let binaryName: string | undefined = undefined; | ||
189 | if (process.arch === "x64" || process.arch === "x32") { | ||
190 | if (process.platform === "linux") binaryName = "rust-analyzer-linux"; | ||
191 | if (process.platform === "darwin") binaryName = "rust-analyzer-mac"; | ||
192 | if (process.platform === "win32") binaryName = "rust-analyzer-windows.exe"; | ||
193 | } | ||
194 | if (binaryName === undefined) { | ||
195 | vscode.window.showErrorMessage( | ||
196 | "Unfortunately we don't ship binaries for your platform yet. " + | ||
197 | "You need to manually clone rust-analyzer repository and " + | ||
198 | "run `cargo xtask install --server` to build the language server from sources. " + | ||
199 | "If you feel that your platform should be supported, please create an issue " + | ||
200 | "about that [here](https://github.com/rust-analyzer/rust-analyzer/issues) and we " + | ||
201 | "will consider it." | ||
202 | ); | ||
203 | return undefined; | ||
204 | } | ||
205 | |||
206 | const dest = path.join(config.globalStoragePath, binaryName); | ||
207 | const exists = await fs.stat(dest).then(() => true, () => false); | ||
208 | if (!exists) { | ||
209 | await state.updateServerVersion(undefined); | ||
210 | } | ||
211 | |||
212 | if (state.serverVersion === config.packageJsonVersion) return dest; | ||
213 | |||
214 | if (config.askBeforeDownload) { | ||
215 | const userResponse = await vscode.window.showInformationMessage( | ||
216 | `Language server version ${config.packageJsonVersion} for rust-analyzer is not installed.`, | ||
217 | "Download now" | ||
218 | ); | ||
219 | if (userResponse !== "Download now") return dest; | ||
220 | } | ||
221 | |||
222 | const release = await fetchRelease(config.extensionReleaseTag); | ||
223 | const artifact = release.assets.find(artifact => artifact.name === binaryName); | ||
224 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); | ||
225 | |||
226 | await download(artifact.browser_download_url, dest, "Downloading rust-analyzer server", { mode: 0o755 }); | ||
227 | await state.updateServerVersion(config.packageJsonVersion); | ||
228 | return dest; | ||
229 | } | ||
diff --git a/editors/code/src/installation/downloads.ts b/editors/code/src/net.ts index 7ce2e2960..492213937 100644 --- a/editors/code/src/installation/downloads.ts +++ b/editors/code/src/net.ts | |||
@@ -1,24 +1,101 @@ | |||
1 | import fetch from "node-fetch"; | 1 | import fetch from "node-fetch"; |
2 | import * as vscode from "vscode"; | 2 | import * as vscode from "vscode"; |
3 | import * as path from "path"; | ||
4 | import * as fs from "fs"; | 3 | import * as fs from "fs"; |
5 | import * as stream from "stream"; | 4 | import * as stream from "stream"; |
6 | import * as util from "util"; | 5 | import * as util from "util"; |
7 | import { log, assert } from "../util"; | 6 | import { log, assert } from "./util"; |
8 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
9 | 7 | ||
10 | const pipeline = util.promisify(stream.pipeline); | 8 | const pipeline = util.promisify(stream.pipeline); |
11 | 9 | ||
10 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | ||
11 | const OWNER = "rust-analyzer"; | ||
12 | const REPO = "rust-analyzer"; | ||
13 | |||
14 | export async function fetchRelease( | ||
15 | releaseTag: string | ||
16 | ): Promise<GithubRelease> { | ||
17 | |||
18 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; | ||
19 | |||
20 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | ||
21 | |||
22 | log.debug("Issuing request for released artifacts metadata to", requestUrl); | ||
23 | |||
24 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); | ||
25 | |||
26 | if (!response.ok) { | ||
27 | log.error("Error fetching artifact release info", { | ||
28 | requestUrl, | ||
29 | releaseTag, | ||
30 | response: { | ||
31 | headers: response.headers, | ||
32 | status: response.status, | ||
33 | body: await response.text(), | ||
34 | } | ||
35 | }); | ||
36 | |||
37 | throw new Error( | ||
38 | `Got response ${response.status} when trying to fetch ` + | ||
39 | `release info for ${releaseTag} release` | ||
40 | ); | ||
41 | } | ||
42 | |||
43 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | ||
44 | const release: GithubRelease = await response.json(); | ||
45 | return release; | ||
46 | } | ||
47 | |||
48 | // We omit declaration of tremendous amount of fields that we are not using here | ||
49 | export interface GithubRelease { | ||
50 | name: string; | ||
51 | id: number; | ||
52 | // eslint-disable-next-line camelcase | ||
53 | published_at: string; | ||
54 | assets: Array<{ | ||
55 | name: string; | ||
56 | // eslint-disable-next-line camelcase | ||
57 | browser_download_url: string; | ||
58 | }>; | ||
59 | } | ||
60 | |||
61 | |||
62 | export async function download( | ||
63 | downloadUrl: string, | ||
64 | destinationPath: string, | ||
65 | progressTitle: string, | ||
66 | { mode }: { mode?: number } = {}, | ||
67 | ) { | ||
68 | await vscode.window.withProgress( | ||
69 | { | ||
70 | location: vscode.ProgressLocation.Notification, | ||
71 | cancellable: false, | ||
72 | title: progressTitle | ||
73 | }, | ||
74 | async (progress, _cancellationToken) => { | ||
75 | let lastPercentage = 0; | ||
76 | await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => { | ||
77 | const newPercentage = (readBytes / totalBytes) * 100; | ||
78 | progress.report({ | ||
79 | message: newPercentage.toFixed(0) + "%", | ||
80 | increment: newPercentage - lastPercentage | ||
81 | }); | ||
82 | |||
83 | lastPercentage = newPercentage; | ||
84 | }); | ||
85 | } | ||
86 | ); | ||
87 | } | ||
88 | |||
12 | /** | 89 | /** |
13 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. | 90 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. |
14 | * `onProgress` callback is called on recieveing each chunk of bytes | 91 | * `onProgress` callback is called on recieveing each chunk of bytes |
15 | * to track the progress of downloading, it gets the already read and total | 92 | * to track the progress of downloading, it gets the already read and total |
16 | * amount of bytes to read as its parameters. | 93 | * amount of bytes to read as its parameters. |
17 | */ | 94 | */ |
18 | export async function downloadFile( | 95 | async function downloadFile( |
19 | url: string, | 96 | url: string, |
20 | destFilePath: fs.PathLike, | 97 | destFilePath: fs.PathLike, |
21 | destFilePermissions: number, | 98 | mode: number | undefined, |
22 | onProgress: (readBytes: number, totalBytes: number) => void | 99 | onProgress: (readBytes: number, totalBytes: number) => void |
23 | ): Promise<void> { | 100 | ): Promise<void> { |
24 | const res = await fetch(url); | 101 | const res = await fetch(url); |
@@ -41,7 +118,7 @@ export async function downloadFile( | |||
41 | onProgress(readBytes, totalBytes); | 118 | onProgress(readBytes, totalBytes); |
42 | }); | 119 | }); |
43 | 120 | ||
44 | const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions }); | 121 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); |
45 | 122 | ||
46 | await pipeline(res.body, destFileStream); | 123 | await pipeline(res.body, destFileStream); |
47 | return new Promise<void>(resolve => { | 124 | return new Promise<void>(resolve => { |
@@ -52,46 +129,3 @@ export async function downloadFile( | |||
52 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 | 129 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 |
53 | }); | 130 | }); |
54 | } | 131 | } |
55 | |||
56 | /** | ||
57 | * Downloads artifact from given `downloadUrl`. | ||
58 | * Creates `installationDir` if it is not yet created and puts the artifact under | ||
59 | * `artifactFileName`. | ||
60 | * Displays info about the download progress in an info message printing the name | ||
61 | * of the artifact as `displayName`. | ||
62 | */ | ||
63 | export async function downloadArtifactWithProgressUi( | ||
64 | { downloadUrl, releaseName }: ArtifactReleaseInfo, | ||
65 | artifactFileName: string, | ||
66 | installationDir: string, | ||
67 | displayName: string, | ||
68 | ) { | ||
69 | await fs.promises.mkdir(installationDir).catch(err => assert( | ||
70 | err?.code === "EEXIST", | ||
71 | `Couldn't create directory "${installationDir}" to download ` + | ||
72 | `${artifactFileName} artifact: ${err?.message}` | ||
73 | )); | ||
74 | |||
75 | const installationPath = path.join(installationDir, artifactFileName); | ||
76 | |||
77 | await vscode.window.withProgress( | ||
78 | { | ||
79 | location: vscode.ProgressLocation.Notification, | ||
80 | cancellable: false, // FIXME: add support for canceling download? | ||
81 | title: `Downloading rust-analyzer ${displayName} (${releaseName})` | ||
82 | }, | ||
83 | async (progress, _cancellationToken) => { | ||
84 | let lastPrecentage = 0; | ||
85 | const filePermissions = 0o755; // (rwx, r_x, r_x) | ||
86 | await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => { | ||
87 | const newPercentage = (readBytes / totalBytes) * 100; | ||
88 | progress.report({ | ||
89 | message: newPercentage.toFixed(0) + "%", | ||
90 | increment: newPercentage - lastPrecentage | ||
91 | }); | ||
92 | |||
93 | lastPrecentage = newPercentage; | ||
94 | }); | ||
95 | } | ||
96 | ); | ||
97 | } | ||
diff --git a/editors/code/src/persistent_state.ts b/editors/code/src/persistent_state.ts index 13095b806..138d11b89 100644 --- a/editors/code/src/persistent_state.ts +++ b/editors/code/src/persistent_state.ts | |||
@@ -1,49 +1,41 @@ | |||
1 | import * as vscode from 'vscode'; | 1 | import * as vscode from 'vscode'; |
2 | import { log } from "./util"; | 2 | import { log } from './util'; |
3 | 3 | ||
4 | export class PersistentState { | 4 | export class PersistentState { |
5 | constructor(private readonly ctx: vscode.ExtensionContext) { | 5 | constructor(private readonly globalState: vscode.Memento) { |
6 | const { lastCheck, releaseId, serverVersion } = this; | ||
7 | log.debug("PersistentState: ", { lastCheck, releaseId, serverVersion }); | ||
6 | } | 8 | } |
7 | 9 | ||
8 | readonly installedNightlyExtensionReleaseDate = new DateStorage( | 10 | /** |
9 | "installed-nightly-extension-release-date", | 11 | * Used to check for *nightly* updates once an hour. |
10 | this.ctx.globalState | 12 | */ |
11 | ); | 13 | get lastCheck(): number | undefined { |
12 | readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState); | 14 | return this.globalState.get("lastCheck"); |
13 | readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null); | ||
14 | } | ||
15 | |||
16 | |||
17 | export class Storage<T> { | ||
18 | constructor( | ||
19 | private readonly key: string, | ||
20 | private readonly storage: vscode.Memento, | ||
21 | private readonly defaultVal: T | ||
22 | ) { } | ||
23 | |||
24 | get(): T { | ||
25 | const val = this.storage.get(this.key, this.defaultVal); | ||
26 | log.debug(this.key, "==", val); | ||
27 | return val; | ||
28 | } | 15 | } |
29 | async set(val: T) { | 16 | async updateLastCheck(value: number) { |
30 | log.debug(this.key, "=", val); | 17 | await this.globalState.update("lastCheck", value); |
31 | await this.storage.update(this.key, val); | ||
32 | } | 18 | } |
33 | } | ||
34 | export class DateStorage { | ||
35 | inner: Storage<null | string>; | ||
36 | 19 | ||
37 | constructor(key: string, storage: vscode.Memento) { | 20 | /** |
38 | this.inner = new Storage(key, storage, null); | 21 | * Release id of the *nightly* extension. |
22 | * Used to check if we should update. | ||
23 | */ | ||
24 | get releaseId(): number | undefined { | ||
25 | return this.globalState.get("releaseId"); | ||
39 | } | 26 | } |
40 | 27 | async updateReleaseId(value: number) { | |
41 | get(): null | Date { | 28 | await this.globalState.update("releaseId", value); |
42 | const dateStr = this.inner.get(); | ||
43 | return dateStr ? new Date(dateStr) : null; | ||
44 | } | 29 | } |
45 | 30 | ||
46 | async set(date: null | Date) { | 31 | /** |
47 | await this.inner.set(date ? date.toString() : null); | 32 | * Version of the extension that installed the server. |
33 | * Used to check if we need to update the server. | ||
34 | */ | ||
35 | get serverVersion(): string | undefined { | ||
36 | return this.globalState.get("serverVersion"); | ||
37 | } | ||
38 | async updateServerVersion(value: string | undefined) { | ||
39 | await this.globalState.update("serverVersion", value); | ||
48 | } | 40 | } |
49 | } | 41 | } |
diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index 2bfc145e6..978a31751 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import * as lc from "vscode-languageclient"; | 1 | import * as lc from "vscode-languageclient"; |
2 | import * as vscode from "vscode"; | 2 | import * as vscode from "vscode"; |
3 | import { promises as dns } from "dns"; | ||
4 | import { strict as nativeAssert } from "assert"; | 3 | import { strict as nativeAssert } from "assert"; |
5 | 4 | ||
6 | export function assert(condition: boolean, explanation: string): asserts condition { | 5 | export function assert(condition: boolean, explanation: string): asserts condition { |
@@ -31,22 +30,6 @@ export const log = new class { | |||
31 | // eslint-disable-next-line no-console | 30 | // eslint-disable-next-line no-console |
32 | console.error(message, ...optionalParams); | 31 | console.error(message, ...optionalParams); |
33 | } | 32 | } |
34 | |||
35 | downloadError(err: Error, artifactName: string, repoName: string) { | ||
36 | vscode.window.showErrorMessage( | ||
37 | `Failed to download the rust-analyzer ${artifactName} from ${repoName} ` + | ||
38 | `GitHub repository: ${err.message}` | ||
39 | ); | ||
40 | log.error(err); | ||
41 | dns.resolve('example.com').then( | ||
42 | addrs => log.debug("DNS resolution for example.com was successful", addrs), | ||
43 | err => log.error( | ||
44 | "DNS resolution for example.com failed, " + | ||
45 | "there might be an issue with Internet availability", | ||
46 | err | ||
47 | ) | ||
48 | ); | ||
49 | } | ||
50 | }; | 33 | }; |
51 | 34 | ||
52 | export async function sendRequestWithRetry<TParam, TRet>( | 35 | export async function sendRequestWithRetry<TParam, TRet>( |
@@ -86,17 +69,6 @@ function sleep(ms: number) { | |||
86 | return new Promise(resolve => setTimeout(resolve, ms)); | 69 | return new Promise(resolve => setTimeout(resolve, ms)); |
87 | } | 70 | } |
88 | 71 | ||
89 | export function notReentrant<TThis, TParams extends any[], TRet>( | ||
90 | fn: (this: TThis, ...params: TParams) => Promise<TRet> | ||
91 | ): typeof fn { | ||
92 | let entered = false; | ||
93 | return function(...params) { | ||
94 | assert(!entered, `Reentrancy invariant for ${fn.name} is violated`); | ||
95 | entered = true; | ||
96 | return fn.apply(this, params).finally(() => entered = false); | ||
97 | }; | ||
98 | } | ||
99 | |||
100 | export type RustDocument = vscode.TextDocument & { languageId: "rust" }; | 72 | export type RustDocument = vscode.TextDocument & { languageId: "rust" }; |
101 | export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; | 73 | export type RustEditor = vscode.TextEditor & { document: RustDocument; id: string }; |
102 | 74 | ||
@@ -110,29 +82,3 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD | |||
110 | export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { | 82 | export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { |
111 | return isRustDocument(editor.document); | 83 | return isRustDocument(editor.document); |
112 | } | 84 | } |
113 | |||
114 | /** | ||
115 | * @param extensionId The canonical extension identifier in the form of: `publisher.name` | ||
116 | */ | ||
117 | export async function vscodeReinstallExtension(extensionId: string) { | ||
118 | // Unfortunately there is no straightforward way as of now, these commands | ||
119 | // were found in vscode source code. | ||
120 | |||
121 | log.debug("Uninstalling extension", extensionId); | ||
122 | await vscode.commands.executeCommand("workbench.extensions.uninstallExtension", extensionId); | ||
123 | log.debug("Installing extension", extensionId); | ||
124 | await vscode.commands.executeCommand("workbench.extensions.installExtension", extensionId); | ||
125 | } | ||
126 | |||
127 | export async function vscodeReloadWindow(): Promise<never> { | ||
128 | await vscode.commands.executeCommand("workbench.action.reloadWindow"); | ||
129 | |||
130 | assert(false, "unreachable"); | ||
131 | } | ||
132 | |||
133 | export async function vscodeInstallExtensionFromVsix(vsixPath: string) { | ||
134 | await vscode.commands.executeCommand( | ||
135 | "workbench.extensions.installExtension", | ||
136 | vscode.Uri.file(vsixPath) | ||
137 | ); | ||
138 | } | ||