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/src/installation/extension.ts | |
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/src/installation/extension.ts')
-rw-r--r-- | editors/code/src/installation/extension.ts | 146 |
1 files changed, 0 insertions, 146 deletions
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 | } | ||