Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode"
"--extensionDevelopmentPath=${workspaceRoot}/extensions/ql-vscode",
// Add a reference to a workspace to open. Eg-
// "${workspaceRoot}/../vscode-codeql-starter/vscode-codeql-starter.code-workspace"
],
"stopOnEntry": false,
"sourceMaps": true,
Expand Down
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/astViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DatabaseItem } from './databases';
import { UrlValue, BqrsId } from './pure/bqrs-cli-types';
import { showLocation } from './interface-utils';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';
import { commandRunner } from './helpers';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './vscode-utils/disposable-object';

export interface AstItem {
Expand Down
215 changes: 215 additions & 0 deletions extensions/ql-vscode/src/commandRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import {
CancellationToken,
ProgressOptions,
window as Window,
commands,
Disposable,
ProgressLocation
} from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
import { logger } from './logging';

export class UserCancellationException extends Error {
/**
* @param message The error message
* @param silent If silent is true, then this exception will avoid showing a warning message to the user.
*/
constructor(message?: string, public readonly silent = false) {
super(message);
}
}

export interface ProgressUpdate {
/**
* The current step
*/
step: number;
/**
* The maximum step. This *should* be constant for a single job.
*/
maxStep: number;
/**
* The current progress message
*/
message: string;
}

export type ProgressCallback = (p: ProgressUpdate) => void;

/**
* A task that handles command invocations from `commandRunner`
* and includes a progress monitor.
*
*
* Arguments passed to the command handler are passed along,
* untouched to this `ProgressTask` instance.
*
* @param progress a progress handler function. Call this
* function with a `ProgressUpdate` instance in order to
* denote some progress being achieved on this task.
* @param token a cencellation token
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
export type ProgressTask<R> = (
progress: ProgressCallback,
token: CancellationToken,
...args: any[]
) => Thenable<R>;

/**
* A task that handles command invocations from `commandRunner`.
* Arguments passed to the command handler are passed along,
* untouched to this `NoProgressTask` instance.
*
* @param args arguments passed to this task passed on from
* `commands.registerCommand`.
*/
type NoProgressTask = ((...args: any[]) => Promise<any>);

/**
* This mediates between the kind of progress callbacks we want to
* write (where we *set* current progress position and give
* `maxSteps`) and the kind vscode progress api expects us to write
* (which increment progress by a certain amount out of 100%).
*
* Where possible, the `commandRunner` function below should be used
* instead of this function. The commandRunner is meant for wrapping
* top-level commands and provides error handling and other support
* automatically.
*
* Only use this function if you need a progress monitor and the
* control flow does not always come from a command (eg- during
* extension activation, or from an internal language server
* request).
*/
export function withProgress<R>(
options: ProgressOptions,
task: ProgressTask<R>,
...args: any[]
): Thenable<R> {
let progressAchieved = 0;
return Window.withProgress(options,
(progress, token) => {
return task(p => {
const { message, step, maxStep } = p;
const increment = 100 * (step - progressAchieved) / maxStep;
progressAchieved = step;
progress.report({ message, increment });
}, token, ...args);
});
}

/**
* A generic wrapper for command registration. This wrapper adds uniform error handling for commands.
*
* In this variant of the command runner, no progress monitor is used.
*
* @param commandId The ID of the command to register.
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task.
*/
export function commandRunner(
commandId: string,
task: NoProgressTask,
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
try {
return await task(...args);
} catch (e) {
const errorMessage = `${e.message || e} (${commandId})`;
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(errorMessage);
} else {
showAndLogWarningMessage(errorMessage);
}
} else {
showAndLogErrorMessage(errorMessage);
}
return undefined;
}
});
}

/**
* A generic wrapper for command registration. This wrapper adds uniform error handling,
* progress monitoring, and cancellation for commands.
*
* @param commandId The ID of the command to register.
* @param task The task to run. It is passed directly to `commands.registerCommand`. Any
* arguments to the command handler are passed on to the task after the progress callback
* and cancellation token.
* @param progressOptions Progress options to be sent to the progress monitor.
*/
export function commandRunnerWithProgress<R>(
commandId: string,
task: ProgressTask<R>,
progressOptions: Partial<ProgressOptions>
): Disposable {
return commands.registerCommand(commandId, async (...args: any[]) => {
const progressOptionsWithDefaults = {
location: ProgressLocation.Notification,
...progressOptions
};
try {
return await withProgress(progressOptionsWithDefaults, task, ...args);
} catch (e) {
const errorMessage = `${e.message || e} (${commandId})`;
if (e instanceof UserCancellationException) {
// User has cancelled this action manually
if (e.silent) {
logger.log(errorMessage);
} else {
showAndLogWarningMessage(errorMessage);
}
} else {
showAndLogErrorMessage(errorMessage);
}
return undefined;
}
});
}

/**
* Displays a progress monitor that indicates how much progess has been made
* reading from a stream.
*
* @param readable The stream to read progress from
* @param messagePrefix A prefix for displaying the message
* @param totalNumBytes Total number of bytes in this stream
* @param progress The progress callback used to set messages
*/
export function reportStreamProgress(
readable: NodeJS.ReadableStream,
messagePrefix: string,
totalNumBytes?: number,
progress?: ProgressCallback
) {
if (progress && totalNumBytes) {
let numBytesDownloaded = 0;
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
const updateProgress = () => {
progress({
step: numBytesDownloaded,
maxStep: totalNumBytes,
message: `${messagePrefix} [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`,
});
};

// Display the progress straight away rather than waiting for the first chunk.
updateProgress();

readable.on('data', data => {
numBytesDownloaded += data.length;
updateProgress();
});
} else if (progress) {
progress({
step: 1,
maxStep: 2,
message: `${messagePrefix} (Size unknown)`,
});
}
}
2 changes: 1 addition & 1 deletion extensions/ql-vscode/src/contextual/locationFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import fileRangeFromURI from './fileRangeFromURI';
import * as messages from '../pure/messages';
import { QueryServerClient } from '../queryserver-client';
import { QueryWithResults, compileAndRunQueryAgainstDatabase } from '../run-queries';
import { ProgressCallback } from '../helpers';
import { ProgressCallback } from '../commandRunner';
import { KeyType } from './keyType';
import { qlpackOfDatabase, resolveQueries } from './queryResolver';

Expand Down
3 changes: 2 additions & 1 deletion extensions/ql-vscode/src/contextual/templateProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as vscode from 'vscode';
import { decodeSourceArchiveUri, encodeArchiveBasePath, zipArchiveScheme } from '../archive-filesystem-provider';
import { CodeQLCliServer } from '../cli';
import { DatabaseManager } from '../databases';
import { CachedOperation, ProgressCallback, withProgress } from '../helpers';
import { CachedOperation } from '../helpers';
import { ProgressCallback, withProgress } from '../commandRunner';
import * as messages from '../pure/messages';
import { QueryServerClient } from '../queryserver-client';
import { compileAndRunQueryAgainstDatabase, QueryWithResults } from '../run-queries';
Expand Down
6 changes: 4 additions & 2 deletions extensions/ql-vscode/src/databaseFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import * as path from 'path';

import { DatabaseManager, DatabaseItem } from './databases';
import {
reportStreamProgress,
ProgressCallback,
showAndLogInformationMessage,
} from './helpers';
import {
reportStreamProgress,
ProgressCallback,
} from './commandRunner';
import { logger } from './logging';
import { tmpDir } from './run-queries';

Expand Down
4 changes: 3 additions & 1 deletion extensions/ql-vscode/src/databases-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import {
import {
commandRunner,
commandRunnerWithProgress,
getOnDiskWorkspaceFolders,
ProgressCallback,
} from './commandRunner';
import {
getOnDiskWorkspaceFolders,
showAndLogErrorMessage,
isLikelyDatabaseRoot,
isLikelyDbLanguageFolder
Expand Down
4 changes: 3 additions & 1 deletion extensions/ql-vscode/src/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
showAndLogWarningMessage,
showAndLogInformationMessage,
isLikelyDatabaseRoot,
} from './helpers';
import {
ProgressCallback,
withProgress
} from './helpers';
} from './commandRunner';
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from './vscode-utils/disposable-object';
import { Logger, logger } from './logging';
Expand Down
21 changes: 14 additions & 7 deletions extensions/ql-vscode/src/distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import * as unzipper from 'unzipper';
import * as url from 'url';
import { ExtensionContext, Event } from 'vscode';
import { DistributionConfig } from './config';
import { InvocationRateLimiter, InvocationRateLimiterResultKind, showAndLogErrorMessage } from './helpers';
import {
InvocationRateLimiter,
InvocationRateLimiterResultKind,
showAndLogErrorMessage,
showAndLogWarningMessage
} from './helpers';
import { logger } from './logging';
import * as helpers from './helpers';
import { getCodeQlCliVersion } from './cli-version';
import { ProgressCallback, reportStreamProgress } from './commandRunner';

/**
* distribution.ts
Expand Down Expand Up @@ -221,7 +226,7 @@ export class DistributionManager implements DistributionProvider {
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
progressCallback?: helpers.ProgressCallback): Promise<void> {
progressCallback?: ProgressCallback): Promise<void> {
return this.extensionSpecificDistributionManager!.installDistributionRelease(release, progressCallback);
}

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

private async downloadDistribution(release: Release,
progressCallback?: helpers.ProgressCallback): Promise<void> {
progressCallback?: ProgressCallback): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {
Expand Down Expand Up @@ -338,7 +343,7 @@ class ExtensionSpecificDistributionManager {

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

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

function warnDeprecatedLauncher() {
helpers.showAndLogWarningMessage(

showAndLogWarningMessage(

`The "${deprecatedCodeQlLauncherName()!}" launcher has been deprecated and will be removed in a future version. ` +
`Please use "${codeQlLauncherName()}" instead. It is recommended to update to the latest CodeQL binaries.`
);
Expand Down
Loading