diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-09-24 15:08:46 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-09-24 15:08:46 +0100 |
commit | de4fb138063c859f29b5a4cf6d382d94e38bb48c (patch) | |
tree | e5e605708e31a23983cbbe273bc8a6a72cb6d179 | |
parent | 9d3483a74dba6b0a338230fd003d91a0447c5398 (diff) | |
parent | 8eae893c767941bf02338cd74d7b103437783013 (diff) |
Merge #6061
6061: Allow to use a Github Auth token for fetching releases r=matklad a=Matthias247
This change allows to use a authorization token provided by Github in
order to fetch metadata for a RA release. Using an authorization token
prevents to get rate-limited in environments where lots of RA users use
a shared client IP (e.g. behind a company NAT).
The auth token is stored in `ExtensionContext.globalState`.
As far as I could observe through testing with a local WSL2 environment
that state is synced between an extension installed locally and a remote
version.
The change provides no explicit command to query for an auth token.
However in case a download fails it will provide a retry option as well
as an option to enter the auth token. This should be more discoverable
for most users.
Closes #3688
Co-authored-by: Matthias Einwag <[email protected]>
-rw-r--r-- | editors/code/package.json | 9 | ||||
-rw-r--r-- | editors/code/src/main.ts | 98 | ||||
-rw-r--r-- | editors/code/src/net.ts | 18 | ||||
-rw-r--r-- | editors/code/src/persistent_state.ts | 11 |
4 files changed, 117 insertions, 19 deletions
diff --git a/editors/code/package.json b/editors/code/package.json index c57fbdda2..132664926 100644 --- a/editors/code/package.json +++ b/editors/code/package.json | |||
@@ -159,6 +159,11 @@ | |||
159 | "category": "Rust Analyzer" | 159 | "category": "Rust Analyzer" |
160 | }, | 160 | }, |
161 | { | 161 | { |
162 | "command": "rust-analyzer.updateGithubToken", | ||
163 | "title": "Update Github API token", | ||
164 | "category": "Rust Analyzer" | ||
165 | }, | ||
166 | { | ||
162 | "command": "rust-analyzer.onEnter", | 167 | "command": "rust-analyzer.onEnter", |
163 | "title": "Enhanced enter key", | 168 | "title": "Enhanced enter key", |
164 | "category": "Rust Analyzer" | 169 | "category": "Rust Analyzer" |
@@ -985,6 +990,10 @@ | |||
985 | "when": "inRustProject" | 990 | "when": "inRustProject" |
986 | }, | 991 | }, |
987 | { | 992 | { |
993 | "command": "rust-analyzer.updateGithubToken", | ||
994 | "when": "inRustProject" | ||
995 | }, | ||
996 | { | ||
988 | "command": "rust-analyzer.onEnter", | 997 | "command": "rust-analyzer.onEnter", |
989 | "when": "inRustProject" | 998 | "when": "inRustProject" |
990 | }, | 999 | }, |
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index bd99d696a..2896d90ac 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts | |||
@@ -95,6 +95,10 @@ async function tryActivate(context: vscode.ExtensionContext) { | |||
95 | await activate(context).catch(log.error); | 95 | await activate(context).catch(log.error); |
96 | }); | 96 | }); |
97 | 97 | ||
98 | ctx.registerCommand('updateGithubToken', ctx => async () => { | ||
99 | await queryForGithubToken(new PersistentState(ctx.globalState)); | ||
100 | }); | ||
101 | |||
98 | ctx.registerCommand('analyzerStatus', commands.analyzerStatus); | 102 | ctx.registerCommand('analyzerStatus', commands.analyzerStatus); |
99 | ctx.registerCommand('memoryUsage', commands.memoryUsage); | 103 | ctx.registerCommand('memoryUsage', commands.memoryUsage); |
100 | ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace); | 104 | ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace); |
@@ -173,7 +177,9 @@ async function bootstrapExtension(config: Config, state: PersistentState): Promi | |||
173 | if (!shouldCheckForNewNightly) return; | 177 | if (!shouldCheckForNewNightly) return; |
174 | } | 178 | } |
175 | 179 | ||
176 | const release = await fetchRelease("nightly").catch((e) => { | 180 | const release = await downloadWithRetryDialog(state, async () => { |
181 | return await fetchRelease("nightly", state.githubToken); | ||
182 | }).catch((e) => { | ||
177 | log.error(e); | 183 | log.error(e); |
178 | if (state.releaseId === undefined) { // Show error only for the initial download | 184 | if (state.releaseId === undefined) { // Show error only for the initial download |
179 | vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`); | 185 | vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`); |
@@ -192,10 +198,14 @@ async function bootstrapExtension(config: Config, state: PersistentState): Promi | |||
192 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); | 198 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); |
193 | 199 | ||
194 | const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); | 200 | const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); |
195 | await download({ | 201 | |
196 | url: artifact.browser_download_url, | 202 | await downloadWithRetryDialog(state, async () => { |
197 | dest, | 203 | await download({ |
198 | progressTitle: "Downloading rust-analyzer extension", | 204 | url: artifact.browser_download_url, |
205 | dest, | ||
206 | progressTitle: "Downloading rust-analyzer extension", | ||
207 | overwrite: true, | ||
208 | }); | ||
199 | }); | 209 | }); |
200 | 210 | ||
201 | await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); | 211 | await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); |
@@ -308,21 +318,22 @@ async function getServer(config: Config, state: PersistentState): Promise<string | |||
308 | if (userResponse !== "Download now") return dest; | 318 | if (userResponse !== "Download now") return dest; |
309 | } | 319 | } |
310 | 320 | ||
311 | const release = await fetchRelease(config.package.releaseTag); | 321 | const releaseTag = config.package.releaseTag; |
322 | const release = await downloadWithRetryDialog(state, async () => { | ||
323 | return await fetchRelease(releaseTag, state.githubToken); | ||
324 | }); | ||
312 | const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`); | 325 | const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`); |
313 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); | 326 | assert(!!artifact, `Bad release: ${JSON.stringify(release)}`); |
314 | 327 | ||
315 | // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error. | 328 | await downloadWithRetryDialog(state, async () => { |
316 | await fs.unlink(dest).catch(err => { | 329 | await download({ |
317 | if (err.code !== "ENOENT") throw err; | 330 | url: artifact.browser_download_url, |
318 | }); | 331 | dest, |
319 | 332 | progressTitle: "Downloading rust-analyzer server", | |
320 | await download({ | 333 | gunzip: true, |
321 | url: artifact.browser_download_url, | 334 | mode: 0o755, |
322 | dest, | 335 | overwrite: true, |
323 | progressTitle: "Downloading rust-analyzer server", | 336 | }); |
324 | gunzip: true, | ||
325 | mode: 0o755 | ||
326 | }); | 337 | }); |
327 | 338 | ||
328 | // Patching executable if that's NixOS. | 339 | // Patching executable if that's NixOS. |
@@ -333,3 +344,56 @@ async function getServer(config: Config, state: PersistentState): Promise<string | |||
333 | await state.updateServerVersion(config.package.version); | 344 | await state.updateServerVersion(config.package.version); |
334 | return dest; | 345 | return dest; |
335 | } | 346 | } |
347 | |||
348 | async function downloadWithRetryDialog<T>(state: PersistentState, downloadFunc: () => Promise<T>): Promise<T> { | ||
349 | while (true) { | ||
350 | try { | ||
351 | return await downloadFunc(); | ||
352 | } catch (e) { | ||
353 | const selected = await vscode.window.showErrorMessage("Failed to download: " + e.message, {}, { | ||
354 | title: "Update Github Auth Token", | ||
355 | updateToken: true, | ||
356 | }, { | ||
357 | title: "Retry download", | ||
358 | retry: true, | ||
359 | }, { | ||
360 | title: "Dismiss", | ||
361 | }); | ||
362 | |||
363 | if (selected?.updateToken) { | ||
364 | await queryForGithubToken(state); | ||
365 | continue; | ||
366 | } else if (selected?.retry) { | ||
367 | continue; | ||
368 | } | ||
369 | throw e; | ||
370 | }; | ||
371 | } | ||
372 | } | ||
373 | |||
374 | async function queryForGithubToken(state: PersistentState): Promise<void> { | ||
375 | const githubTokenOptions: vscode.InputBoxOptions = { | ||
376 | value: state.githubToken, | ||
377 | password: true, | ||
378 | prompt: ` | ||
379 | This dialog allows to store a Github authorization token. | ||
380 | The usage of an authorization token will increase the rate | ||
381 | limit on the use of Github APIs and can thereby prevent getting | ||
382 | throttled. | ||
383 | Auth tokens can be created at https://github.com/settings/tokens`, | ||
384 | }; | ||
385 | |||
386 | const newToken = await vscode.window.showInputBox(githubTokenOptions); | ||
387 | if (newToken === undefined) { | ||
388 | // The user aborted the dialog => Do not update the stored token | ||
389 | return; | ||
390 | } | ||
391 | |||
392 | if (newToken === "") { | ||
393 | log.info("Clearing github token"); | ||
394 | await state.updateGithubToken(undefined); | ||
395 | } else { | ||
396 | log.info("Storing new github token"); | ||
397 | await state.updateGithubToken(newToken); | ||
398 | } | ||
399 | } | ||
diff --git a/editors/code/src/net.ts b/editors/code/src/net.ts index 5eba2728d..9ba17b7b5 100644 --- a/editors/code/src/net.ts +++ b/editors/code/src/net.ts | |||
@@ -18,7 +18,8 @@ const OWNER = "rust-analyzer"; | |||
18 | const REPO = "rust-analyzer"; | 18 | const REPO = "rust-analyzer"; |
19 | 19 | ||
20 | export async function fetchRelease( | 20 | export async function fetchRelease( |
21 | releaseTag: string | 21 | releaseTag: string, |
22 | githubToken: string | null | undefined, | ||
22 | ): Promise<GithubRelease> { | 23 | ): Promise<GithubRelease> { |
23 | 24 | ||
24 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; | 25 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; |
@@ -27,7 +28,12 @@ export async function fetchRelease( | |||
27 | 28 | ||
28 | log.debug("Issuing request for released artifacts metadata to", requestUrl); | 29 | log.debug("Issuing request for released artifacts metadata to", requestUrl); |
29 | 30 | ||
30 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); | 31 | const headers: Record<string, string> = { Accept: "application/vnd.github.v3+json" }; |
32 | if (githubToken != null) { | ||
33 | headers.Authorization = "token " + githubToken; | ||
34 | } | ||
35 | |||
36 | const response = await fetch(requestUrl, { headers: headers }); | ||
31 | 37 | ||
32 | if (!response.ok) { | 38 | if (!response.ok) { |
33 | log.error("Error fetching artifact release info", { | 39 | log.error("Error fetching artifact release info", { |
@@ -70,6 +76,7 @@ interface DownloadOpts { | |||
70 | dest: string; | 76 | dest: string; |
71 | mode?: number; | 77 | mode?: number; |
72 | gunzip?: boolean; | 78 | gunzip?: boolean; |
79 | overwrite?: boolean; | ||
73 | } | 80 | } |
74 | 81 | ||
75 | export async function download(opts: DownloadOpts) { | 82 | export async function download(opts: DownloadOpts) { |
@@ -79,6 +86,13 @@ export async function download(opts: DownloadOpts) { | |||
79 | const randomHex = crypto.randomBytes(5).toString("hex"); | 86 | const randomHex = crypto.randomBytes(5).toString("hex"); |
80 | const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); | 87 | const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`); |
81 | 88 | ||
89 | if (opts.overwrite) { | ||
90 | // Unlinking the exe file before moving new one on its place should prevent ETXTBSY error. | ||
91 | await fs.promises.unlink(opts.dest).catch(err => { | ||
92 | if (err.code !== "ENOENT") throw err; | ||
93 | }); | ||
94 | } | ||
95 | |||
82 | await vscode.window.withProgress( | 96 | await vscode.window.withProgress( |
83 | { | 97 | { |
84 | location: vscode.ProgressLocation.Notification, | 98 | location: vscode.ProgressLocation.Notification, |
diff --git a/editors/code/src/persistent_state.ts b/editors/code/src/persistent_state.ts index 5705eed81..afb652589 100644 --- a/editors/code/src/persistent_state.ts +++ b/editors/code/src/persistent_state.ts | |||
@@ -38,4 +38,15 @@ export class PersistentState { | |||
38 | async updateServerVersion(value: string | undefined) { | 38 | async updateServerVersion(value: string | undefined) { |
39 | await this.globalState.update("serverVersion", value); | 39 | await this.globalState.update("serverVersion", value); |
40 | } | 40 | } |
41 | |||
42 | /** | ||
43 | * Github authorization token. | ||
44 | * This is used for API requests against the Github API. | ||
45 | */ | ||
46 | get githubToken(): string | undefined { | ||
47 | return this.globalState.get("githubToken"); | ||
48 | } | ||
49 | async updateGithubToken(value: string | undefined) { | ||
50 | await this.globalState.update("githubToken", value); | ||
51 | } | ||
41 | } | 52 | } |