diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-19 08:06:48 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-19 08:06:48 +0000 |
commit | aca3c3086ee99f5770a60970e20af640c895d42a (patch) | |
tree | 2f0b3233cc4728436ba5e47a6e91e7df33585d43 /editors/code | |
parent | 55336722b3662cbdcc9e1b92a3e27ed0442d2452 (diff) | |
parent | fb6e655de8a44c65275ad45a27bf5bd684670ba0 (diff) |
Merge #3629
3629: Alternative aproach to plugin auto update r=matklad a=matklad
This is very much WIP (as in, I haven't run this once), but I like the result so far.
cc @Veetaha
The primary focus here on simplification:
* local simplification of data structures and control-flow: using union of strings instead of an enum, using unwrapped GitHub API responses
* global simplification of control flow: all logic is now in `main.ts`, implemented as linear functions without abstractions. This is stateful side-effective code, so arguments from [Carmack](http://number-none.com/blow/john_carmack_on_inlined_code.html) very much apply. We need all user interractions, all mutations, and all network requests to happen in a single file.
* as a side-effect of condensing everything to functions, we can get rid of various enums. The enums were basically a reified control flow:
```
enum E { A, B }
fn foo() -> E {
if cond { E::A } else { E::B }
}
fn bar(e: E) {
match e {
E::A => do_a(),
E::B => do_b(),
}
}
==>>
fn all() {
if cond { do_a() } else { do_b() }
}
```
* simplification of model: we don't need to reinstall on settings update, we can just ask the user to reload, we don't need to handle nightly=>stable fallback, we can ask the user to reinstall extension, (todo) we don't need to parse out the date from the version, we can use build id for nightly and for stable we can write the info directly into package.json.
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'editors/code')
-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 84642d11c..150c36845 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 | } | ||