aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-09-24 15:08:46 +0100
committerGitHub <[email protected]>2020-09-24 15:08:46 +0100
commitde4fb138063c859f29b5a4cf6d382d94e38bb48c (patch)
treee5e605708e31a23983cbbe273bc8a6a72cb6d179
parent9d3483a74dba6b0a338230fd003d91a0447c5398 (diff)
parent8eae893c767941bf02338cd74d7b103437783013 (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.json9
-rw-r--r--editors/code/src/main.ts98
-rw-r--r--editors/code/src/net.ts18
-rw-r--r--editors/code/src/persistent_state.ts11
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
348async 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
374async 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";
18const REPO = "rust-analyzer"; 18const REPO = "rust-analyzer";
19 19
20export async function fetchRelease( 20export 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
75export async function download(opts: DownloadOpts) { 82export 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}