Skip to content

Commit 6e9c64d

Browse files
authored
Merge pull request #177 from henrymercer/improve-errors
Improve error handling for CLI installation and updates
2 parents 6f4211b + b48fbde commit 6e9c64d

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)