Skip to content

Commit 97eaaac

Browse files
committed
Run pack bundle in separate process and allow cancelling it
1 parent b961a7a commit 97eaaac

4 files changed

Lines changed: 147 additions & 37 deletions

File tree

extensions/ql-vscode/src/codeql-cli/cli.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type { Position } from "../query-server/messages";
3636
import { LOGGING_FLAGS } from "./cli-command";
3737
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
3838
import { ExitCodeError, getCliError } from "./cli-errors";
39+
import { UserCancellationException } from "../common/vscode/progress";
3940

4041
/**
4142
* The version of the SARIF format that we are using.
@@ -230,6 +231,15 @@ type RunOptions = {
230231
* If true, don't print logs to the CodeQL extension log.
231232
*/
232233
silent?: boolean;
234+
/**
235+
* If true, run this command in a new process rather than in the CLI server.
236+
*/
237+
runInNewProcess?: boolean;
238+
/**
239+
* If runInNewProcess is true, allows cancelling the command. If runInNewProcess
240+
* is false or not specified, this option is ignored.
241+
*/
242+
token?: CancellationToken;
233243
};
234244

235245
type JsonRunOptions = RunOptions & {
@@ -438,6 +448,67 @@ export class CodeQLCliServer implements Disposable {
438448
}
439449
}
440450

451+
private async runCodeQlCliInNewProcess(
452+
command: string[],
453+
commandArgs: string[],
454+
description: string,
455+
onLine?: OnLineCallback,
456+
silent?: boolean,
457+
token?: CancellationToken,
458+
): Promise<string> {
459+
const codeqlPath = await this.getCodeQlPath();
460+
461+
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
462+
const argsString = args.join(" ");
463+
464+
// If we are running silently, we don't want to print anything to the console.
465+
if (!silent) {
466+
void this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
467+
}
468+
469+
const process = spawnChildProcess(codeqlPath, args);
470+
if (!process || !process.pid) {
471+
throw new Error(
472+
`Failed to start ${description} using command ${codeqlPath} ${argsString}.`,
473+
);
474+
}
475+
476+
let cancellationRegistration: Disposable | undefined = undefined;
477+
try {
478+
cancellationRegistration = token?.onCancellationRequested((_e) => {
479+
tk(process.pid || 0);
480+
});
481+
482+
return await this.handleProcessOutput(process, {
483+
handleNullTerminator: true,
484+
onListenStart: (process) => {
485+
// Write the command followed by a null terminator.
486+
process.stdin.write(JSON.stringify(args), "utf8");
487+
process.stdin.write(this.nullBuffer);
488+
},
489+
description,
490+
args,
491+
silent,
492+
onLine,
493+
});
494+
} catch (e) {
495+
// If cancellation was requested, the error is probably just because the process was exited with SIGTERM.
496+
if (token?.isCancellationRequested) {
497+
void this.logger.log(
498+
`The process was cancelled and exited with: ${getErrorMessage(e)}`,
499+
);
500+
throw new UserCancellationException(
501+
`Command ${argsString} was cancelled.`,
502+
true, // Don't show a warning message when the user manually cancelled the command.
503+
);
504+
}
505+
506+
throw e;
507+
} finally {
508+
cancellationRegistration?.dispose();
509+
}
510+
}
511+
441512
private async handleProcessOutput(
442513
process: ChildProcessWithoutNullStreams,
443514
{
@@ -714,18 +785,38 @@ export class CodeQLCliServer implements Disposable {
714785
* @param progressReporter Used to output progress messages, e.g. to the status bar.
715786
* @param onLine Used for responding to interactive output on stdout/stdin.
716787
* @param silent If true, don't print logs to the CodeQL extension log.
788+
* @param runInNewProcess If true, run this command in a new process rather than in the CLI server.
789+
* @param token If runInNewProcess is true, allows cancelling the command. If runInNewProcess
790+
* is false or not specified, this option is ignored.
717791
* @returns The contents of the command's stdout, if the command succeeded.
718792
*/
719793
runCodeQlCliCommand(
720794
command: string[],
721795
commandArgs: string[],
722796
description: string,
723-
{ progressReporter, onLine, silent = false }: RunOptions = {},
797+
{
798+
progressReporter,
799+
onLine,
800+
silent = false,
801+
runInNewProcess = false,
802+
token,
803+
}: RunOptions = {},
724804
): Promise<string> {
725805
if (progressReporter) {
726806
progressReporter.report({ message: description });
727807
}
728808

809+
if (runInNewProcess) {
810+
return this.runCodeQlCliInNewProcess(
811+
command,
812+
commandArgs,
813+
description,
814+
onLine,
815+
silent,
816+
token,
817+
);
818+
}
819+
729820
return new Promise((resolve, reject) => {
730821
// Construct the command that actually does the work
731822
const callback = (): void => {
@@ -1537,13 +1628,15 @@ export class CodeQLCliServer implements Disposable {
15371628
* @param outputBundleFile The path to the output bundle file.
15381629
* @param outputPackDir The directory to contain the unbundled output pack.
15391630
* @param moreOptions Additional options to be passed to `codeql pack bundle`.
1631+
* @param token Cancellation token for the operation.
15401632
*/
15411633
async packBundle(
15421634
sourcePackDir: string,
15431635
workspaceFolders: string[],
15441636
outputBundleFile: string,
15451637
outputPackDir: string,
15461638
moreOptions: string[],
1639+
token?: CancellationToken,
15471640
): Promise<void> {
15481641
const args = [
15491642
"-o",
@@ -1559,6 +1652,10 @@ export class CodeQLCliServer implements Disposable {
15591652
["pack", "bundle"],
15601653
args,
15611654
"Bundling pack",
1655+
{
1656+
runInNewProcess: true,
1657+
token,
1658+
},
15621659
);
15631660
}
15641661

extensions/ql-vscode/src/model-editor/model-evaluator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export class ModelEvaluator extends DisposableObject {
7575
),
7676
{
7777
title: "Run model evaluation",
78-
cancellable: false,
78+
cancellable: true,
7979
},
8080
);
8181
}

extensions/ql-vscode/src/variant-analysis/run-remote-query.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async function generateQueryPack(
5454
cliServer: CodeQLCliServer,
5555
qlPackDetails: QlPackDetails,
5656
tmpDir: RemoteQueryTempDir,
57+
token: CancellationToken,
5758
): Promise<string> {
5859
const workspaceFolders = getOnDiskWorkspaceFolders();
5960
const extensionPacks = await getExtensionPacksToInject(
@@ -148,6 +149,7 @@ async function generateQueryPack(
148149
bundlePath,
149150
tmpDir.compiledPackDir,
150151
precompilationOpts,
152+
token,
151153
);
152154
const base64Pack = (await readFile(bundlePath)).toString("base64");
153155
return base64Pack;
@@ -331,7 +333,12 @@ export async function prepareRemoteQueryRun(
331333
let base64Pack: string;
332334

333335
try {
334-
base64Pack = await generateQueryPack(cliServer, qlPackDetails, tempDir);
336+
base64Pack = await generateQueryPack(
337+
cliServer,
338+
qlPackDetails,
339+
tempDir,
340+
token,
341+
);
335342
} finally {
336343
await tempDir.remoteQueryDir.cleanup();
337344
}

extensions/ql-vscode/src/variant-analysis/variant-analysis-manager.ts

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -221,42 +221,48 @@ export class VariantAnalysisManager
221221
}
222222

223223
public async runVariantAnalysisFromPublishedPack(): Promise<void> {
224-
return withProgress(async (progress, token) => {
225-
progress({
226-
maxStep: 7,
227-
step: 0,
228-
message: "Determining query language",
229-
});
230-
231-
const language = await askForLanguage(this.cliServer);
232-
if (!language) {
233-
return;
234-
}
235-
236-
progress({
237-
maxStep: 7,
238-
step: 2,
239-
message: "Downloading query pack and resolving queries",
240-
});
224+
return withProgress(
225+
async (progress, token) => {
226+
progress({
227+
maxStep: 7,
228+
step: 0,
229+
message: "Determining query language",
230+
});
231+
232+
const language = await askForLanguage(this.cliServer);
233+
if (!language) {
234+
return;
235+
}
241236

242-
// Build up details to pass to the functions that run the variant analysis.
243-
const qlPackDetails = await resolveCodeScanningQueryPack(
244-
this.app.logger,
245-
this.cliServer,
246-
language,
247-
);
237+
progress({
238+
maxStep: 7,
239+
step: 2,
240+
message: "Downloading query pack and resolving queries",
241+
});
242+
243+
// Build up details to pass to the functions that run the variant analysis.
244+
const qlPackDetails = await resolveCodeScanningQueryPack(
245+
this.app.logger,
246+
this.cliServer,
247+
language,
248+
);
248249

249-
await this.runVariantAnalysis(
250-
qlPackDetails,
251-
(p) =>
252-
progress({
253-
...p,
254-
maxStep: p.maxStep + 3,
255-
step: p.step + 3,
256-
}),
257-
token,
258-
);
259-
});
250+
await this.runVariantAnalysis(
251+
qlPackDetails,
252+
(p) =>
253+
progress({
254+
...p,
255+
maxStep: p.maxStep + 3,
256+
step: p.step + 3,
257+
}),
258+
token,
259+
);
260+
},
261+
{
262+
title: "Run Variant Analysis",
263+
cancellable: true,
264+
},
265+
);
260266
}
261267

262268
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {

0 commit comments

Comments
 (0)