Skip to content

Commit b48fbde

Browse files
committed
Report rate limiting errors more clearly
Also report problems updating the distribution on extension activation as warnings, and improve task names in error messages.
1 parent 36fedac commit b48fbde

File tree

2 files changed

+61
-34
lines changed

2 files changed

+61
-34
lines changed

extensions/ql-vscode/src/distribution.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,13 @@ export class ReleasesApiConsumer {
400400
Object.assign({}, this._defaultHeaders, additionalHeaders));
401401

402402
if (!response.ok) {
403+
// Check for rate limiting
404+
const rateLimitResetValue = response.headers.get("X-RateLimit-Reset");
405+
if (response.status === 403 && rateLimitResetValue) {
406+
const secondsToMillisecondsFactor = 1000;
407+
const rateLimitResetDate = new Date(parseInt(rateLimitResetValue, 10) * secondsToMillisecondsFactor);
408+
throw new GithubRateLimitedError(response.status, await response.text(), rateLimitResetDate);
409+
}
403410
throw new GithubApiError(response.status, await response.text());
404411
}
405412
return response;
@@ -673,3 +680,9 @@ export class GithubApiError extends Error {
673680
super(`API call failed with status code ${status}, body: ${body}`);
674681
}
675682
}
683+
684+
export class GithubRateLimitedError extends GithubApiError {
685+
constructor(public status: number, public body: string, public rateLimitResetDate: Date) {
686+
super(status, body);
687+
}
688+
}

extensions/ql-vscode/src/extension.ts

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import * as archiveFilesystemProvider from './archive-filesystem-provider';
44
import { DistributionConfigListener, QueryServerConfigListener } from './config';
55
import { DatabaseManager } from './databases';
66
import { DatabaseUI } from './databases-ui';
7-
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT } from './distribution';
7+
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError,
8+
DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT, GithubRateLimitedError } from './distribution';
89
import * as helpers from './helpers';
910
import { spawnIdeServer } from './ide-server';
1011
import { InterfaceManager, WebviewReveal } from './interface';
@@ -79,19 +80,24 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
7980
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
8081

8182
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
82-
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
83+
helpers.showAndLogErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
8384
});
8485

85-
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
86+
interface ReportingConfig {
87+
shouldDisplayMessageWhenNoUpdates: boolean;
88+
shouldErrorIfUpdateFails: boolean;
89+
}
90+
91+
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, reportingConfig: ReportingConfig): Promise<void> {
8692
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
8793
switch (result.kind) {
8894
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
89-
if (!isSilentIfCannotUpdate) {
95+
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
9096
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
9197
}
9298
break;
9399
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
94-
if (!isSilentIfCannotUpdate) {
100+
if (reportingConfig.shouldDisplayMessageWhenNoUpdates) {
95101
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
96102
}
97103
break;
@@ -121,34 +127,32 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
121127
}
122128
}
123129

124-
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
130+
async function installOrUpdateDistribution(reportingConfig: ReportingConfig): Promise<void> {
125131
if (isInstallingOrUpdatingDistribution) {
126132
throw new Error("Already installing or updating CodeQL CLI");
127133
}
128134
isInstallingOrUpdatingDistribution = true;
135+
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
136+
const willUpdateCodeQl = ctx.globalState.get(shouldUpdateOnNextActivationKey);
137+
const messageText = willUpdateCodeQl ? "Updating CodeQL CLI" :
138+
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
129139
try {
130-
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
131-
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
132-
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
133-
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
140+
await installOrUpdateDistributionWithProgressTitle(messageText, reportingConfig);
134141
} catch (e) {
135142
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
136143
// or updating the distribution.
137-
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
138-
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
139-
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
140-
"to obtain CodeQL CLI.", "Edit Settings");
141-
// We're deliberately not `await`ing this promise, just
142-
// asynchronously letting the user follow the convenience link
143-
// if they want to.
144-
errorMessageResponse.then(response => {
145-
if (response !== undefined) {
146-
commands.executeCommand('workbench.action.openSettingsJson');
147-
}
148-
});
149-
} else {
150-
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
144+
const alertFunction = (codeQlInstalled && !reportingConfig.shouldErrorIfUpdateFails) ?
145+
helpers.showAndLogWarningMessage : helpers.showAndLogErrorMessage;
146+
const taskDescription = (willUpdateCodeQl ? "update" :
147+
codeQlInstalled ? "check for updates to" : "install") + " CodeQL CLI";
148+
149+
if (e instanceof GithubRateLimitedError) {
150+
alertFunction(`Rate limited while trying to ${taskDescription}. Please try again after ` +
151+
`your rate limit window resets at ${e.rateLimitResetDate.toLocaleString()}.`);
152+
} else if (e instanceof GithubApiError) {
153+
alertFunction(`Encountered GitHub API error while trying to ${taskDescription}. ` + e);
151154
}
155+
alertFunction(`Unable to ${taskDescription}. ` + e);
152156
} finally {
153157
isInstallingOrUpdatingDistribution = false;
154158
}
@@ -176,10 +180,8 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
176180
return result;
177181
}
178182

179-
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
180-
if (!isInstallingOrUpdatingDistribution) {
181-
await installOrUpdateDistribution(isSilentIfCannotUpdate);
182-
}
183+
async function installOrUpdateThenTryActivate(reportingConfig: ReportingConfig): Promise<void> {
184+
await installOrUpdateDistribution(reportingConfig);
183185

184186
// Display the warnings even if the extension has already activated.
185187
const distributionResult = await getDistributionDisplayingDistributionWarnings();
@@ -189,18 +191,30 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
189191
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
190192
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
191193
const installActionName = "Install CodeQL CLI";
192-
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
194+
const chosenAction = await helpers.showAndLogErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
193195
if (chosenAction === installActionName) {
194-
installOrUpdateThenTryActivate(true);
196+
installOrUpdateThenTryActivate({
197+
shouldDisplayMessageWhenNoUpdates: false,
198+
shouldErrorIfUpdateFails: true
199+
});
195200
}
196201
});
197202
}
198203
}
199204

200-
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
201-
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
202-
203-
await installOrUpdateThenTryActivate(true);
205+
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate({
206+
shouldDisplayMessageWhenNoUpdates: false,
207+
shouldErrorIfUpdateFails: true
208+
})));
209+
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate({
210+
shouldDisplayMessageWhenNoUpdates: true,
211+
shouldErrorIfUpdateFails: true
212+
})));
213+
214+
await installOrUpdateThenTryActivate({
215+
shouldDisplayMessageWhenNoUpdates: false,
216+
shouldErrorIfUpdateFails: !!ctx.globalState.get(shouldUpdateOnNextActivationKey)
217+
});
204218
}
205219

206220
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {

0 commit comments

Comments
 (0)