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/code/src/installation | |
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/code/src/installation')
-rw-r--r-- | editors/code/src/installation/downloads.ts | 97 | ||||
-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 |
5 files changed, 0 insertions, 514 deletions
diff --git a/editors/code/src/installation/downloads.ts b/editors/code/src/installation/downloads.ts deleted file mode 100644 index 7ce2e2960..000000000 --- a/editors/code/src/installation/downloads.ts +++ /dev/null | |||
@@ -1,97 +0,0 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import * as vscode from "vscode"; | ||
3 | import * as path from "path"; | ||
4 | import * as fs from "fs"; | ||
5 | import * as stream from "stream"; | ||
6 | import * as util from "util"; | ||
7 | import { log, assert } from "../util"; | ||
8 | import { ArtifactReleaseInfo } from "./interfaces"; | ||
9 | |||
10 | const pipeline = util.promisify(stream.pipeline); | ||
11 | |||
12 | /** | ||
13 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. | ||
14 | * `onProgress` callback is called on recieveing each chunk of bytes | ||
15 | * to track the progress of downloading, it gets the already read and total | ||
16 | * amount of bytes to read as its parameters. | ||
17 | */ | ||
18 | export async function downloadFile( | ||
19 | url: string, | ||
20 | destFilePath: fs.PathLike, | ||
21 | destFilePermissions: number, | ||
22 | onProgress: (readBytes: number, totalBytes: number) => void | ||
23 | ): Promise<void> { | ||
24 | const res = await fetch(url); | ||
25 | |||
26 | if (!res.ok) { | ||
27 | log.error("Error", res.status, "while downloading file from", url); | ||
28 | log.error({ body: await res.text(), headers: res.headers }); | ||
29 | |||
30 | throw new Error(`Got response ${res.status} when trying to download a file.`); | ||
31 | } | ||
32 | |||
33 | const totalBytes = Number(res.headers.get('content-length')); | ||
34 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); | ||
35 | |||
36 | log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | ||
37 | |||
38 | let readBytes = 0; | ||
39 | res.body.on("data", (chunk: Buffer) => { | ||
40 | readBytes += chunk.length; | ||
41 | onProgress(readBytes, totalBytes); | ||
42 | }); | ||
43 | |||
44 | const destFileStream = fs.createWriteStream(destFilePath, { mode: destFilePermissions }); | ||
45 | |||
46 | await pipeline(res.body, destFileStream); | ||
47 | return new Promise<void>(resolve => { | ||
48 | destFileStream.on("close", resolve); | ||
49 | destFileStream.destroy(); | ||
50 | |||
51 | // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 | ||
52 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 | ||
53 | }); | ||
54 | } | ||
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/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 | } | ||