aboutsummaryrefslogtreecommitdiff
path: root/editors/code/src/installation
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src/installation')
-rw-r--r--editors/code/src/installation/download_artifact.ts50
-rw-r--r--editors/code/src/installation/downloads.ts (renamed from editors/code/src/installation/download_file.ts)46
-rw-r--r--editors/code/src/installation/extension.ts144
-rw-r--r--editors/code/src/installation/fetch_artifact_release_info.ts3
-rw-r--r--editors/code/src/installation/interfaces.ts18
-rw-r--r--editors/code/src/installation/server.ts104
6 files changed, 250 insertions, 115 deletions
diff --git a/editors/code/src/installation/download_artifact.ts b/editors/code/src/installation/download_artifact.ts
deleted file mode 100644
index 97e4d67c2..000000000
--- a/editors/code/src/installation/download_artifact.ts
+++ /dev/null
@@ -1,50 +0,0 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from "fs";
4
5import { ArtifactReleaseInfo } from "./interfaces";
6import { downloadFile } from "./download_file";
7import { assert } from "../util";
8
9/**
10 * Downloads artifact from given `downloadUrl`.
11 * Creates `installationDir` if it is not yet created and put the artifact under
12 * `artifactFileName`.
13 * Displays info about the download progress in an info message printing the name
14 * of the artifact as `displayName`.
15 */
16export async function downloadArtifact(
17 { downloadUrl, releaseName }: ArtifactReleaseInfo,
18 artifactFileName: string,
19 installationDir: string,
20 displayName: string,
21) {
22 await fs.mkdir(installationDir).catch(err => assert(
23 err?.code === "EEXIST",
24 `Couldn't create directory "${installationDir}" to download ` +
25 `${artifactFileName} artifact: ${err?.message}`
26 ));
27
28 const installationPath = path.join(installationDir, artifactFileName);
29
30 await vscode.window.withProgress(
31 {
32 location: vscode.ProgressLocation.Notification,
33 cancellable: false, // FIXME: add support for canceling download?
34 title: `Downloading ${displayName} (${releaseName})`
35 },
36 async (progress, _cancellationToken) => {
37 let lastPrecentage = 0;
38 const filePermissions = 0o755; // (rwx, r_x, r_x)
39 await downloadFile(downloadUrl, installationPath, filePermissions, (readBytes, totalBytes) => {
40 const newPercentage = (readBytes / totalBytes) * 100;
41 progress.report({
42 message: newPercentage.toFixed(0) + "%",
43 increment: newPercentage - lastPrecentage
44 });
45
46 lastPrecentage = newPercentage;
47 });
48 }
49 );
50}
diff --git a/editors/code/src/installation/download_file.ts b/editors/code/src/installation/downloads.ts
index ee8949d61..7ce2e2960 100644
--- a/editors/code/src/installation/download_file.ts
+++ b/editors/code/src/installation/downloads.ts
@@ -1,8 +1,11 @@
1import fetch from "node-fetch"; 1import fetch from "node-fetch";
2import * as vscode from "vscode";
3import * as path from "path";
2import * as fs from "fs"; 4import * as fs from "fs";
3import * as stream from "stream"; 5import * as stream from "stream";
4import * as util from "util"; 6import * as util from "util";
5import { log, assert } from "../util"; 7import { log, assert } from "../util";
8import { ArtifactReleaseInfo } from "./interfaces";
6 9
7const pipeline = util.promisify(stream.pipeline); 10const pipeline = util.promisify(stream.pipeline);
8 11
@@ -49,3 +52,46 @@ export async function downloadFile(
49 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 52 // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776
50 }); 53 });
51} 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 */
63export 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
new file mode 100644
index 000000000..eea6fded2
--- /dev/null
+++ b/editors/code/src/installation/extension.ts
@@ -0,0 +1,144 @@
1import * as vscode from "vscode";
2import * as path from "path";
3import { promises as fs } from 'fs';
4
5import { vscodeReinstallExtension, vscodeReloadWindow, log, vscodeInstallExtensionFromVsix, assert, notReentrant } from "../util";
6import { Config, UpdatesChannel } from "../config";
7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
8import { downloadArtifactWithProgressUi } from "./downloads";
9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
10
11const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
12
13/**
14 * Installs `stable` or latest `nightly` version or does nothing if the current
15 * extension version is what's needed according to `desiredUpdateChannel`.
16 */
17export async function ensureProperExtensionVersion(config: Config): Promise<never | void> {
18 // User has built lsp server from sources, she should manage updates manually
19 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
20
21 const currentUpdChannel = config.installedExtensionUpdateChannel;
22 const desiredUpdChannel = config.updatesChannel;
23
24 if (currentUpdChannel === UpdatesChannel.Stable) {
25 // Release date is present only when we are on nightly
26 await config.installedNightlyExtensionReleaseDate.set(null);
27 }
28
29 if (desiredUpdChannel === UpdatesChannel.Stable) {
30 // VSCode should handle updates for stable channel
31 if (currentUpdChannel === UpdatesChannel.Stable) return;
32
33 if (!await askToDownloadProperExtensionVersion(config)) return;
34
35 await vscodeReinstallExtension(config.extensionId);
36 await vscodeReloadWindow(); // never returns
37 }
38
39 if (currentUpdChannel === UpdatesChannel.Stable) {
40 if (!await askToDownloadProperExtensionVersion(config)) return;
41
42 return await tryDownloadNightlyExtension(config);
43 }
44
45 const currentExtReleaseDate = config.installedNightlyExtensionReleaseDate.get();
46
47 if (currentExtReleaseDate === null) {
48 void vscode.window.showErrorMessage(
49 "Nightly release date must've been set during the installation. " +
50 "Did you download and install the nightly .vsix package manually?"
51 );
52 throw new Error("Nightly release date was not set in globalStorage");
53 }
54
55 const dateNow = new Date;
56 const hoursSinceLastUpdate = diffInHours(currentExtReleaseDate, dateNow);
57 log.debug(
58 "Current rust-analyzer nightly was downloaded", hoursSinceLastUpdate,
59 "hours ago, namely:", currentExtReleaseDate, "and now is", dateNow
60 );
61
62 if (hoursSinceLastUpdate < HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS) {
63 return;
64 }
65 if (!await askToDownloadProperExtensionVersion(config, "The installed nightly version is most likely outdated. ")) {
66 return;
67 }
68
69 await tryDownloadNightlyExtension(config, releaseInfo => {
70 assert(
71 currentExtReleaseDate.getTime() === config.installedNightlyExtensionReleaseDate.get()?.getTime(),
72 "Other active VSCode instance has reinstalled the extension"
73 );
74
75 if (releaseInfo.releaseDate.getTime() === currentExtReleaseDate.getTime()) {
76 vscode.window.showInformationMessage(
77 "Whoops, it appears that your nightly version is up-to-date. " +
78 "There might be some problems with the upcomming nightly release " +
79 "or you traveled too far into the future. Sorry for that 😅! "
80 );
81 return false;
82 }
83 return true;
84 });
85}
86
87async function askToDownloadProperExtensionVersion(config: Config, reason = "") {
88 if (!config.askBeforeDownload) return true;
89
90 const stableOrNightly = config.updatesChannel === UpdatesChannel.Stable ? "stable" : "latest nightly";
91
92 // In case of reentering this function and showing the same info message
93 // (e.g. after we had shown this message, the user changed the config)
94 // vscode will dismiss the already shown one (i.e. return undefined).
95 // This behaviour is what we want, but likely it is not documented
96
97 const userResponse = await vscode.window.showInformationMessage(
98 reason + `Do you want to download the ${stableOrNightly} rust-analyzer extension ` +
99 `version and reload the window now?`,
100 "Download now", "Cancel"
101 );
102 return userResponse === "Download now";
103}
104
105/**
106 * Shutdowns the process in case of success (i.e. reloads the window) or throws an error.
107 *
108 * ACHTUNG!: this function has a crazy amount of state transitions, handling errors during
109 * each of them would result in a ton of code (especially accounting for cross-process
110 * shared mutable `globalState` access). Enforcing no reentrancy for this is best-effort.
111 */
112const tryDownloadNightlyExtension = notReentrant(async (
113 config: Config,
114 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
115): Promise<never | void> => {
116 const vsixSource = config.nightlyVsixSource;
117 try {
118 const releaseInfo = await fetchArtifactReleaseInfo(vsixSource.repo, vsixSource.file, vsixSource.tag);
119
120 if (!shouldDownload(releaseInfo)) return;
121
122 await downloadArtifactWithProgressUi(releaseInfo, vsixSource.file, vsixSource.dir, "nightly extension");
123
124 const vsixPath = path.join(vsixSource.dir, vsixSource.file);
125
126 await vscodeInstallExtensionFromVsix(vsixPath);
127 await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
128 await fs.unlink(vsixPath);
129
130 await vscodeReloadWindow(); // never returns
131 } catch (err) {
132 log.downloadError(err, "nightly extension", vsixSource.repo.name);
133 }
134});
135
136function diffInHours(a: Date, b: Date): number {
137 // Discard the time and time-zone information (to abstract from daylight saving time bugs)
138 // https://stackoverflow.com/a/15289883/9259330
139
140 const utcA = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
141 const utcB = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
142
143 return (utcA - utcB) / (1000 * 60 * 60);
144}
diff --git a/editors/code/src/installation/fetch_artifact_release_info.ts b/editors/code/src/installation/fetch_artifact_release_info.ts
index b1b5a3485..1ad3b8338 100644
--- a/editors/code/src/installation/fetch_artifact_release_info.ts
+++ b/editors/code/src/installation/fetch_artifact_release_info.ts
@@ -59,12 +59,15 @@ export async function fetchArtifactReleaseInfo(
59 59
60 return { 60 return {
61 releaseName: release.name, 61 releaseName: release.name,
62 releaseDate: new Date(release.published_at),
62 downloadUrl: artifact.browser_download_url 63 downloadUrl: artifact.browser_download_url
63 }; 64 };
64 65
65 // We omit declaration of tremendous amount of fields that we are not using here 66 // We omit declaration of tremendous amount of fields that we are not using here
66 interface GithubRelease { 67 interface GithubRelease {
67 name: string; 68 name: string;
69 // eslint-disable-next-line camelcase
70 published_at: string;
68 assets: Array<{ 71 assets: Array<{
69 name: string; 72 name: string;
70 // eslint-disable-next-line camelcase 73 // eslint-disable-next-line camelcase
diff --git a/editors/code/src/installation/interfaces.ts b/editors/code/src/installation/interfaces.ts
index 50b635921..1a8ea0884 100644
--- a/editors/code/src/installation/interfaces.ts
+++ b/editors/code/src/installation/interfaces.ts
@@ -1,5 +1,3 @@
1import * as vscode from "vscode";
2
3export interface GithubRepo { 1export interface GithubRepo {
4 name: string; 2 name: string;
5 owner: string; 3 owner: string;
@@ -9,6 +7,7 @@ export interface GithubRepo {
9 * Metadata about particular artifact retrieved from GitHub releases. 7 * Metadata about particular artifact retrieved from GitHub releases.
10 */ 8 */
11export interface ArtifactReleaseInfo { 9export interface ArtifactReleaseInfo {
10 releaseDate: Date;
12 releaseName: string; 11 releaseName: string;
13 downloadUrl: string; 12 downloadUrl: string;
14} 13}
@@ -42,6 +41,9 @@ export namespace ArtifactSource {
42 */ 41 */
43 repo: GithubRepo; 42 repo: GithubRepo;
44 43
44
45 // FIXME: add installationPath: string;
46
45 /** 47 /**
46 * Directory on the filesystem where the bundled binary is stored. 48 * Directory on the filesystem where the bundled binary is stored.
47 */ 49 */
@@ -57,17 +59,5 @@ export namespace ArtifactSource {
57 * Tag of github release that denotes a version required by this extension. 59 * Tag of github release that denotes a version required by this extension.
58 */ 60 */
59 tag: string; 61 tag: string;
60
61 /**
62 * Object that provides `get()/update()` operations to store metadata
63 * about the actual binary, e.g. its actual version.
64 */
65 storage: vscode.Memento;
66
67 /**
68 * Ask for the user permission before downloading the artifact.
69 */
70 askBeforeDownload: boolean;
71 } 62 }
72
73} 63}
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
index ef1c45ff6..05730a778 100644
--- a/editors/code/src/installation/server.ts
+++ b/editors/code/src/installation/server.ts
@@ -1,14 +1,16 @@
1import * as vscode from "vscode"; 1import * as vscode from "vscode";
2import * as path from "path"; 2import * as path from "path";
3import { promises as dns } from "dns";
4import { spawnSync } from "child_process"; 3import { spawnSync } from "child_process";
5 4
6import { ArtifactSource } from "./interfaces"; 5import { ArtifactSource } from "./interfaces";
7import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; 6import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
8import { downloadArtifact } from "./download_artifact"; 7import { downloadArtifactWithProgressUi } from "./downloads";
9import { log, assert } from "../util"; 8import { log, assert, notReentrant } from "../util";
9import { Config, NIGHTLY_TAG } from "../config";
10
11export async function ensureServerBinary(config: Config): Promise<null | string> {
12 const source = config.serverSource;
10 13
11export async function ensureServerBinary(source: null | ArtifactSource): Promise<null | string> {
12 if (!source) { 14 if (!source) {
13 vscode.window.showErrorMessage( 15 vscode.window.showErrorMessage(
14 "Unfortunately we don't ship binaries for your platform yet. " + 16 "Unfortunately we don't ship binaries for your platform yet. " +
@@ -35,18 +37,11 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise
35 return null; 37 return null;
36 } 38 }
37 case ArtifactSource.Type.GithubRelease: { 39 case ArtifactSource.Type.GithubRelease: {
38 const prebuiltBinaryPath = path.join(source.dir, source.file); 40 if (!shouldDownloadServer(source, config)) {
39 41 return path.join(source.dir, source.file);
40 const installedVersion: null | string = getServerVersion(source.storage);
41 const requiredVersion: string = source.tag;
42
43 log.debug("Installed version:", installedVersion, "required:", requiredVersion);
44
45 if (isBinaryAvailable(prebuiltBinaryPath) && installedVersion === requiredVersion) {
46 return prebuiltBinaryPath;
47 } 42 }
48 43
49 if (source.askBeforeDownload) { 44 if (config.askBeforeDownload) {
50 const userResponse = await vscode.window.showInformationMessage( 45 const userResponse = await vscode.window.showInformationMessage(
51 `Language server version ${source.tag} for rust-analyzer is not installed. ` + 46 `Language server version ${source.tag} for rust-analyzer is not installed. ` +
52 "Do you want to download it now?", 47 "Do you want to download it now?",
@@ -55,38 +50,56 @@ export async function ensureServerBinary(source: null | ArtifactSource): Promise
55 if (userResponse !== "Download now") return null; 50 if (userResponse !== "Download now") return null;
56 } 51 }
57 52
58 if (!await downloadServer(source)) return null; 53 return await downloadServer(source, config);
59
60 return prebuiltBinaryPath;
61 } 54 }
62 } 55 }
63} 56}
64 57
65async function downloadServer(source: ArtifactSource.GithubRelease): Promise<boolean> { 58function shouldDownloadServer(
59 source: ArtifactSource.GithubRelease,
60 config: Config
61): boolean {
62 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
63
64 const installed = {
65 tag: config.serverReleaseTag.get(),
66 date: config.serverReleaseDate.get()
67 };
68 const required = {
69 tag: source.tag,
70 date: config.installedNightlyExtensionReleaseDate.get()
71 };
72
73 log.debug("Installed server:", installed, "required:", required);
74
75 if (required.tag !== NIGHTLY_TAG || installed.tag !== NIGHTLY_TAG) {
76 return required.tag !== installed.tag;
77 }
78
79 assert(required.date !== null, "Extension release date should have been saved during its installation");
80 assert(installed.date !== null, "Server release date should have been saved during its installation");
81
82 return installed.date.getTime() !== required.date.getTime();
83}
84
85/**
86 * Enforcing no reentrancy for this is best-effort.
87 */
88const downloadServer = notReentrant(async (
89 source: ArtifactSource.GithubRelease,
90 config: Config,
91): Promise<null | string> => {
66 try { 92 try {
67 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); 93 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
68 94
69 await downloadArtifact(releaseInfo, source.file, source.dir, "language server"); 95 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
70 await setServerVersion(source.storage, releaseInfo.releaseName); 96 await Promise.all([
97 config.serverReleaseTag.set(releaseInfo.releaseName),
98 config.serverReleaseDate.set(releaseInfo.releaseDate)
99 ]);
71 } catch (err) { 100 } catch (err) {
72 vscode.window.showErrorMessage( 101 log.downloadError(err, "language server", source.repo.name);
73 `Failed to download language server from ${source.repo.name} ` + 102 return null;
74 `GitHub repository: ${err.message}`
75 );
76
77 log.error(err);
78
79 dns.resolve('example.com').then(
80 addrs => log.debug("DNS resolution for example.com was successful", addrs),
81 err => {
82 log.error(
83 "DNS resolution for example.com failed, " +
84 "there might be an issue with Internet availability"
85 );
86 log.error(err);
87 }
88 );
89 return false;
90 } 103 }
91 104
92 const binaryPath = path.join(source.dir, source.file); 105 const binaryPath = path.join(source.dir, source.file);
@@ -101,8 +114,8 @@ async function downloadServer(source: ArtifactSource.GithubRelease): Promise<boo
101 "Rust analyzer language server was successfully installed 🦀" 114 "Rust analyzer language server was successfully installed 🦀"
102 ); 115 );
103 116
104 return true; 117 return binaryPath;
105} 118});
106 119
107function isBinaryAvailable(binaryPath: string): boolean { 120function isBinaryAvailable(binaryPath: string): boolean {
108 const res = spawnSync(binaryPath, ["--version"]); 121 const res = spawnSync(binaryPath, ["--version"]);
@@ -115,14 +128,3 @@ function isBinaryAvailable(binaryPath: string): boolean {
115 128
116 return res.status === 0; 129 return res.status === 0;
117} 130}
118
119function getServerVersion(storage: vscode.Memento): null | string {
120 const version = storage.get<null | string>("server-version", null);
121 log.debug("Get server-version:", version);
122 return version;
123}
124
125async function setServerVersion(storage: vscode.Memento, version: string): Promise<void> {
126 log.debug("Set server-version:", version);
127 await storage.update("server-version", version.toString());
128}