Skip to content

Commit 5401244

Browse files
committed
Refactor: Move commandRunner to its own module
Also, extract related functions and types. There are no behavioral changes in this commit. Only refactorings.
1 parent 6074a1a commit 5401244

20 files changed

Lines changed: 332 additions & 298 deletions

.vscode/launch.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"request": "launch",
99
"runtimeExecutable": "${execPath}",
1010
"args": [
11-
"--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode"
11+
"--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode",
12+
// Add a reference to a workspace to open. Eg-
13+
// "${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace"
1214
],
1315
"stopOnEntry": false,
1416
"sourceMaps": true,

extensions/ql-vscode/src/astViewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DatabaseItem } from './databases';
1818
import { UrlValue, BqrsId } from './pure/bqrs-cli-types';
1919
import { showLocation } from './interface-utils';
2020
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';
21-
import { commandRunner } from './helpers';
21+
import { commandRunner } from './commandRunner';
2222
import { DisposableObject } from './vscode-utils/disposable-object';
2323

2424
export interface AstItem {
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import {
2+
CancellationToken,
3+
ProgressOptions,
4+
window as Window,
5+
commands,
6+
Disposable,
7+
ProgressLocation
8+
} from 'vscode';
9+
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
10+
import { logger } from './logging';
11+
12+
export class UserCancellationException extends Error {
13+
/**
14+
* @param message The error message
15+
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
16+
*/
17+
constructor(message?: string, public readonly silent = false) {
18+
super(message);
19+
}
20+
}
21+
22+
export interface ProgressUpdate {
23+
/**
24+
* The current step
25+
*/
26+
step: number;
27+
/**
28+
* The maximum step. This *should* be constant for a single job.
29+
*/
30+
maxStep: number;
31+
/**
32+
* The current progress message
33+
*/
34+
message: string;
35+
}
36+
37+
export type ProgressCallback = (p: ProgressUpdate) => void;
38+
39+
/**
40+
* A task that handles command invocations from `commandRunner`
41+
* and includes a progress monitor.
42+
*
43+
*
44+
* Arguments passed to the command handler are passed along,
45+
* untouched to this `ProgressTask` instance.
46+
*
47+
* @param progress a progress handler function. Call this
48+
* function with a `ProgressUpdate` instance in order to
49+
* denote some progress being achieved on this task.
50+
* @param token a cencellation token
51+
* @param args arguments passed to this task passed on from
52+
* `commands.registerCommand`.
53+
*/
54+
export type ProgressTask<R> = (
55+
progress: ProgressCallback,
56+
token: CancellationToken,
57+
...args: any[]
58+
) => Thenable<R>;
59+
60+
/**
61+
* A task that handles command invocations from `commandRunner`.
62+
* Arguments passed to the command handler are passed along,
63+
* untouched to this `NoProgressTask` instance.
64+
*
65+
* @param args arguments passed to this task passed on from
66+
* `commands.registerCommand`.
67+
*/
68+
type NoProgressTask = ((...args: any[]) => Promise<any>);
69+
70+
/**
71+
* This mediates between the kind of progress callbacks we want to
72+
* write (where we *set* current progress position and give
73+
* `maxSteps`) and the kind vscode progress api expects us to write
74+
* (which increment progress by a certain amount out of 100%).
75+
*
76+
* Where possible, the `commandRunner` function below should be used
77+
* instead of this function. The commandRunner is meant for wrapping
78+
* top-level commands and provides error handling and other support
79+
* automatically.
80+
*
81+
* Only use this function if you need a progress monitor and the
82+
* control flow does not always come from a command (eg- during
83+
* extension activation, or from an internal language server
84+
* request).
85+
*/
86+
export function withProgress<R>(
87+
options: ProgressOptions,
88+
task: ProgressTask<R>,
89+
...args: any[]
90+
): Thenable<R> {
91+
let progressAchieved = 0;
92+
return Window.withProgress(options,
93+
(progress, token) => {
94+
return task(p => {
95+
const { message, step, maxStep } = p;
96+
const increment = 100 * (step - progressAchieved) / maxStep;
97+
progressAchieved = step;
98+
progress.report({ message, increment });
99+
}, token, ...args);
100+
});
101+
}
102+
103+
/**
104+
* A generic wrapper for command registration. This wrapper adds uniform error handling for commands.
105+
*
106+
* In this variant of the command runner, no progress monitor is used.
107+
*
108+
* @param commandId The ID of the command to register.
109+
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
110+
* arguments to the command handler are passed on to the task.
111+
*/
112+
export function commandRunner(
113+
commandId: string,
114+
task: NoProgressTask,
115+
): Disposable {
116+
return commands.registerCommand(commandId, async (...args: any[]) => {
117+
try {
118+
return await task(...args);
119+
} catch (e) {
120+
const errorMessage = `${e.message || e} (${commandId})`;
121+
if (e instanceof UserCancellationException) {
122+
// User has cancelled this action manually
123+
if (e.silent) {
124+
logger.log(errorMessage);
125+
} else {
126+
showAndLogWarningMessage(errorMessage);
127+
}
128+
} else {
129+
showAndLogErrorMessage(errorMessage);
130+
}
131+
return undefined;
132+
}
133+
});
134+
}
135+
136+
/**
137+
* A generic wrapper for command registration. This wrapper adds uniform error handling,
138+
* progress monitoring, and cancellation for commands.
139+
*
140+
* @param commandId The ID of the command to register.
141+
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
142+
* arguments to the command handler are passed on to the task after the progress callback
143+
* and cancellation token.
144+
* @param progressOptions Progress options to be sent to the progress monitor.
145+
*/
146+
export function commandRunnerWithProgress<R>(
147+
commandId: string,
148+
task: ProgressTask<R>,
149+
progressOptions: Partial<ProgressOptions>
150+
): Disposable {
151+
return commands.registerCommand(commandId, async (...args: any[]) => {
152+
const progressOptionsWithDefaults = {
153+
location: ProgressLocation.Notification,
154+
...progressOptions
155+
};
156+
try {
157+
return await withProgress(progressOptionsWithDefaults, task, ...args);
158+
} catch (e) {
159+
const errorMessage = `${e.message || e} (${commandId})`;
160+
if (e instanceof UserCancellationException) {
161+
// User has cancelled this action manually
162+
if (e.silent) {
163+
logger.log(errorMessage);
164+
} else {
165+
showAndLogWarningMessage(errorMessage);
166+
}
167+
} else {
168+
showAndLogErrorMessage(errorMessage);
169+
}
170+
return undefined;
171+
}
172+
});
173+
}
174+
175+
/**
176+
* Displays a progress monitor that indicates how much progess has been made
177+
* reading from a stream.
178+
*
179+
* @param readable The stream to read progress from
180+
* @param messagePrefix A prefix for displaying the message
181+
* @param totalNumBytes Total number of bytes in this stream
182+
* @param progress The progress callback used to set messages
183+
*/
184+
export function reportStreamProgress(
185+
readable: NodeJS.ReadableStream,
186+
messagePrefix: string,
187+
totalNumBytes?: number,
188+
progress?: ProgressCallback
189+
) {
190+
if (progress && totalNumBytes) {
191+
let numBytesDownloaded = 0;
192+
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
193+
const updateProgress = () => {
194+
progress({
195+
step: numBytesDownloaded,
196+
maxStep: totalNumBytes,
197+
message: `${messagePrefix} [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`,
198+
});
199+
};
200+
201+
// Display the progress straight away rather than waiting for the first chunk.
202+
updateProgress();
203+
204+
readable.on('data', data => {
205+
numBytesDownloaded += data.length;
206+
updateProgress();
207+
});
208+
} else if (progress) {
209+
progress({
210+
step: 1,
211+
maxStep: 2,
212+
message: `${messagePrefix} (Size unknown)`,
213+
});
214+
}
215+
}

extensions/ql-vscode/src/contextual/locationFinder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import fileRangeFromURI from './fileRangeFromURI';
88
import * as messages from '../pure/messages';
99
import { QueryServerClient } from '../queryserver-client';
1010
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
11-
import { ProgressCallback } from '../helpers';
11+
import { ProgressCallback } from '../commandRunner';
1212
import { KeyType } from './keyType';
1313
import { qlpackOfDatabase, resolveQueries } from './queryResolver';
1414

extensions/ql-vscode/src/contextual/templateProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as vscode from 'vscode';
33
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
44
import { CodeQLCliServer } from '../cli';
55
import { DatabaseManager } from '../databases';
6-
import { CachedOperation, ProgressCallback, withProgress } from '../helpers';
6+
import { CachedOperation } from '../helpers';
7+
import { ProgressCallback, withProgress } from '../commandRunner';
78
import * as messages from '../pure/messages';
89
import { QueryServerClient } from '../queryserver-client';
910
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';

extensions/ql-vscode/src/databaseFetcher.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import * as path from 'path';
1212

1313
import { DatabaseManager, DatabaseItem } from './databases';
1414
import {
15-
reportStreamProgress,
16-
ProgressCallback,
1715
showAndLogInformationMessage,
1816
} from './helpers';
17+
import {
18+
reportStreamProgress,
19+
ProgressCallback,
20+
} from './commandRunner';
1921
import { logger } from './logging';
2022
import { tmpDir } from './run-queries';
2123

extensions/ql-vscode/src/databases-ui.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import {
2222
import {
2323
commandRunner,
2424
commandRunnerWithProgress,
25-
getOnDiskWorkspaceFolders,
2625
ProgressCallback,
26+
} from './commandRunner';
27+
import {
28+
getOnDiskWorkspaceFolders,
2729
showAndLogErrorMessage,
2830
isLikelyDatabaseRoot,
2931
isLikelyDbLanguageFolder

extensions/ql-vscode/src/databases.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import {
99
showAndLogWarningMessage,
1010
showAndLogInformationMessage,
1111
isLikelyDatabaseRoot,
12+
} from './helpers';
13+
import {
1214
ProgressCallback,
1315
withProgress
14-
} from './helpers';
16+
} from './commandRunner';
1517
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
1618
import { DisposableObject } from './vscode-utils/disposable-object';
1719
import { Logger, logger } from './logging';

extensions/ql-vscode/src/distribution.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import * as unzipper from 'unzipper';
77
import * as url from 'url';
88
import { ExtensionContext, Event } from 'vscode';
99
import { DistributionConfig } from './config';
10-
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from './helpers';
10+
import {
11+
InvocationRateLimiter,
12+
InvocationRateLimiterResultKind,
13+
showAndLogErrorMessage,
14+
showAndLogWarningMessage
15+
} from './helpers';
1116
import { logger } from './logging';
12-
import * as helpers from './helpers';
1317
import { getCodeQlCliVersion } from './cli-version';
18+
import { ProgressCallback, reportStreamProgress } from './commandRunner';
1419

1520
/**
1621
* distribution.ts
@@ -221,7 +226,7 @@ export class DistributionManager implements DistributionProvider {
221226
* Returns a failed promise if an unexpected error occurs during installation.
222227
*/
223228
public installExtensionManagedDistributionRelease(release: Release,
224-
progressCallback?: helpers.ProgressCallback): Promise<void> {
229+
progressCallback?: ProgressCallback): Promise<void> {
225230
return this.extensionSpecificDistributionManager!.installDistributionRelease(release, progressCallback);
226231
}
227232

@@ -303,14 +308,14 @@ class ExtensionSpecificDistributionManager {
303308
* Returns a failed promise if an unexpected error occurs during installation.
304309
*/
305310
public async installDistributionRelease(release: Release,
306-
progressCallback?: helpers.ProgressCallback): Promise<void> {
311+
progressCallback?: ProgressCallback): Promise<void> {
307312
await this.downloadDistribution(release, progressCallback);
308313
// Store the installed release within the global extension state.
309314
this.storeInstalledRelease(release);
310315
}
311316

312317
private async downloadDistribution(release: Release,
313-
progressCallback?: helpers.ProgressCallback): Promise<void> {
318+
progressCallback?: ProgressCallback): Promise<void> {
314319
try {
315320
await this.removeDistribution();
316321
} catch (e) {
@@ -338,7 +343,7 @@ class ExtensionSpecificDistributionManager {
338343

339344
const contentLength = assetStream.headers.get('content-length');
340345
const totalNumBytes = contentLength ? parseInt(contentLength, 10) : undefined;
341-
helpers.reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback);
346+
reportStreamProgress(assetStream.body, 'Downloading CodeQL CLI…', totalNumBytes, progressCallback);
342347

343348
await new Promise((resolve, reject) =>
344349
assetStream.body.pipe(archiveFile)
@@ -707,7 +712,9 @@ export async function getExecutableFromDirectory(directory: string, warnWhenNotF
707712
}
708713

709714
function warnDeprecatedLauncher() {
710-
helpers.showAndLogWarningMessage(
715+
716+
showAndLogWarningMessage(
717+
711718
`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
712719
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
713720
);

0 commit comments

Comments
 (0)