Skip to content

Commit de4fb13

Browse files
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]>
2 parents 9d3483a + 8eae893 commit de4fb13

File tree

4 files changed

+117
-19
lines changed

4 files changed

+117
-19
lines changed

editors/code/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@
158158
"title": "Restart server",
159159
"category": "Rust Analyzer"
160160
},
161+
{
162+
"command": "rust-analyzer.updateGithubToken",
163+
"title": "Update Github API token",
164+
"category": "Rust Analyzer"
165+
},
161166
{
162167
"command": "rust-analyzer.onEnter",
163168
"title": "Enhanced enter key",
@@ -984,6 +989,10 @@
984989
"command": "rust-analyzer.reload",
985990
"when": "inRustProject"
986991
},
992+
{
993+
"command": "rust-analyzer.updateGithubToken",
994+
"when": "inRustProject"
995+
},
987996
{
988997
"command": "rust-analyzer.onEnter",
989998
"when": "inRustProject"

editors/code/src/main.ts

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ async function tryActivate(context: vscode.ExtensionContext) {
9595
await activate(context).catch(log.error);
9696
});
9797

98+
ctx.registerCommand('updateGithubToken', ctx => async () => {
99+
await queryForGithubToken(new PersistentState(ctx.globalState));
100+
});
101+
98102
ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
99103
ctx.registerCommand('memoryUsage', commands.memoryUsage);
100104
ctx.registerCommand('reloadWorkspace', commands.reloadWorkspace);
@@ -173,7 +177,9 @@ async function bootstrapExtension(config: Config, state: PersistentState): Promi
173177
if (!shouldCheckForNewNightly) return;
174178
}
175179

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) => {
177183
log.error(e);
178184
if (state.releaseId === undefined) { // Show error only for the initial download
179185
vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`);
@@ -192,10 +198,14 @@ async function bootstrapExtension(config: Config, state: PersistentState): Promi
192198
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
193199

194200
const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix");
195-
await download({
196-
url: artifact.browser_download_url,
197-
dest,
198-
progressTitle: "Downloading rust-analyzer extension",
201+
202+
await downloadWithRetryDialog(state, async () => {
203+
await download({
204+
url: artifact.browser_download_url,
205+
dest,
206+
progressTitle: "Downloading rust-analyzer extension",
207+
overwrite: true,
208+
});
199209
});
200210

201211
await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest));
@@ -308,21 +318,22 @@ async function getServer(config: Config, state: PersistentState): Promise<string
308318
if (userResponse !== "Download now") return dest;
309319
}
310320

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+
});
312325
const artifact = release.assets.find(artifact => artifact.name === `rust-analyzer-${platform}.gz`);
313326
assert(!!artifact, `Bad release: ${JSON.stringify(release)}`);
314327

315-
// Unlinking the exe file before moving new one on its place should prevent ETXTBSY error.
316-
await fs.unlink(dest).catch(err => {
317-
if (err.code !== "ENOENT") throw err;
318-
});
319-
320-
await download({
321-
url: artifact.browser_download_url,
322-
dest,
323-
progressTitle: "Downloading rust-analyzer server",
324-
gunzip: true,
325-
mode: 0o755
328+
await downloadWithRetryDialog(state, async () => {
329+
await download({
330+
url: artifact.browser_download_url,
331+
dest,
332+
progressTitle: "Downloading rust-analyzer server",
333+
gunzip: true,
334+
mode: 0o755,
335+
overwrite: true,
336+
});
326337
});
327338

328339
// Patching executable if that's NixOS.
@@ -333,3 +344,56 @@ async function getServer(config: Config, state: PersistentState): Promise<string
333344
await state.updateServerVersion(config.package.version);
334345
return dest;
335346
}
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+
}

editors/code/src/net.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const OWNER = "rust-analyzer";
1818
const REPO = "rust-analyzer";
1919

2020
export async function fetchRelease(
21-
releaseTag: string
21+
releaseTag: string,
22+
githubToken: string | null | undefined,
2223
): Promise<GithubRelease> {
2324

2425
const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`;
@@ -27,7 +28,12 @@ export async function fetchRelease(
2728

2829
log.debug("Issuing request for released artifacts metadata to", requestUrl);
2930

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 });
3137

3238
if (!response.ok) {
3339
log.error("Error fetching artifact release info", {
@@ -70,6 +76,7 @@ interface DownloadOpts {
7076
dest: string;
7177
mode?: number;
7278
gunzip?: boolean;
79+
overwrite?: boolean;
7380
}
7481

7582
export async function download(opts: DownloadOpts) {
@@ -79,6 +86,13 @@ export async function download(opts: DownloadOpts) {
7986
const randomHex = crypto.randomBytes(5).toString("hex");
8087
const tempFile = path.join(dest.dir, `${dest.name}${randomHex}`);
8188

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+
8296
await vscode.window.withProgress(
8397
{
8498
location: vscode.ProgressLocation.Notification,

editors/code/src/persistent_state.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,15 @@ export class PersistentState {
3838
async updateServerVersion(value: string | undefined) {
3939
await this.globalState.update("serverVersion", value);
4040
}
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+
}
4152
}

0 commit comments

Comments
 (0)