Run pack bundle in separate process and allow cancelling it

This commit is contained in:
Koen Vlaswinkel
2024-03-08 12:03:45 +01:00
parent b961a7ae55
commit 97eaaacce6
4 changed files with 145 additions and 35 deletions

View File

@@ -36,6 +36,7 @@ import type { Position } from "../query-server/messages";
import { LOGGING_FLAGS } from "./cli-command";
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
import { ExitCodeError, getCliError } from "./cli-errors";
import { UserCancellationException } from "../common/vscode/progress";
/**
* The version of the SARIF format that we are using.
@@ -230,6 +231,15 @@ type RunOptions = {
* If true, don't print logs to the CodeQL extension log.
*/
silent?: boolean;
/**
* If true, run this command in a new process rather than in the CLI server.
*/
runInNewProcess?: boolean;
/**
* If runInNewProcess is true, allows cancelling the command. If runInNewProcess
* is false or not specified, this option is ignored.
*/
token?: CancellationToken;
};
type JsonRunOptions = RunOptions & {
@@ -438,6 +448,67 @@ export class CodeQLCliServer implements Disposable {
}
}
private async runCodeQlCliInNewProcess(
command: string[],
commandArgs: string[],
description: string,
onLine?: OnLineCallback,
silent?: boolean,
token?: CancellationToken,
): Promise<string> {
const codeqlPath = await this.getCodeQlPath();
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
// If we are running silently, we don't want to print anything to the console.
if (!silent) {
void this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
}
const process = spawnChildProcess(codeqlPath, args);
if (!process || !process.pid) {
throw new Error(
`Failed to start ${description} using command ${codeqlPath} ${argsString}.`,
);
}
let cancellationRegistration: Disposable | undefined = undefined;
try {
cancellationRegistration = token?.onCancellationRequested((_e) => {
tk(process.pid || 0);
});
return await this.handleProcessOutput(process, {
handleNullTerminator: true,
onListenStart: (process) => {
// Write the command followed by a null terminator.
process.stdin.write(JSON.stringify(args), "utf8");
process.stdin.write(this.nullBuffer);
},
description,
args,
silent,
onLine,
});
} catch (e) {
// If cancellation was requested, the error is probably just because the process was exited with SIGTERM.
if (token?.isCancellationRequested) {
void this.logger.log(
`The process was cancelled and exited with: ${getErrorMessage(e)}`,
);
throw new UserCancellationException(
`Command ${argsString} was cancelled.`,
true, // Don't show a warning message when the user manually cancelled the command.
);
}
throw e;
} finally {
cancellationRegistration?.dispose();
}
}
private async handleProcessOutput(
process: ChildProcessWithoutNullStreams,
{
@@ -714,18 +785,38 @@ export class CodeQLCliServer implements Disposable {
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @param onLine Used for responding to interactive output on stdout/stdin.
* @param silent If true, don't print logs to the CodeQL extension log.
* @param runInNewProcess If true, run this command in a new process rather than in the CLI server.
* @param token If runInNewProcess is true, allows cancelling the command. If runInNewProcess
* is false or not specified, this option is ignored.
* @returns The contents of the command's stdout, if the command succeeded.
*/
runCodeQlCliCommand(
command: string[],
commandArgs: string[],
description: string,
{ progressReporter, onLine, silent = false }: RunOptions = {},
{
progressReporter,
onLine,
silent = false,
runInNewProcess = false,
token,
}: RunOptions = {},
): Promise<string> {
if (progressReporter) {
progressReporter.report({ message: description });
}
if (runInNewProcess) {
return this.runCodeQlCliInNewProcess(
command,
commandArgs,
description,
onLine,
silent,
token,
);
}
return new Promise((resolve, reject) => {
// Construct the command that actually does the work
const callback = (): void => {
@@ -1537,6 +1628,7 @@ export class CodeQLCliServer implements Disposable {
* @param outputBundleFile The path to the output bundle file.
* @param outputPackDir The directory to contain the unbundled output pack.
* @param moreOptions Additional options to be passed to `codeql pack bundle`.
* @param token Cancellation token for the operation.
*/
async packBundle(
sourcePackDir: string,
@@ -1544,6 +1636,7 @@ export class CodeQLCliServer implements Disposable {
outputBundleFile: string,
outputPackDir: string,
moreOptions: string[],
token?: CancellationToken,
): Promise<void> {
const args = [
"-o",
@@ -1559,6 +1652,10 @@ export class CodeQLCliServer implements Disposable {
["pack", "bundle"],
args,
"Bundling pack",
{
runInNewProcess: true,
token,
},
);
}

View File

@@ -75,7 +75,7 @@ export class ModelEvaluator extends DisposableObject {
),
{
title: "Run model evaluation",
cancellable: false,
cancellable: true,
},
);
}

View File

@@ -54,6 +54,7 @@ async function generateQueryPack(
cliServer: CodeQLCliServer,
qlPackDetails: QlPackDetails,
tmpDir: RemoteQueryTempDir,
token: CancellationToken,
): Promise<string> {
const workspaceFolders = getOnDiskWorkspaceFolders();
const extensionPacks = await getExtensionPacksToInject(
@@ -148,6 +149,7 @@ async function generateQueryPack(
bundlePath,
tmpDir.compiledPackDir,
precompilationOpts,
token,
);
const base64Pack = (await readFile(bundlePath)).toString("base64");
return base64Pack;
@@ -331,7 +333,12 @@ export async function prepareRemoteQueryRun(
let base64Pack: string;
try {
base64Pack = await generateQueryPack(cliServer, qlPackDetails, tempDir);
base64Pack = await generateQueryPack(
cliServer,
qlPackDetails,
tempDir,
token,
);
} finally {
await tempDir.remoteQueryDir.cleanup();
}

View File

@@ -221,7 +221,8 @@ export class VariantAnalysisManager
}
public async runVariantAnalysisFromPublishedPack(): Promise<void> {
return withProgress(async (progress, token) => {
return withProgress(
async (progress, token) => {
progress({
maxStep: 7,
step: 0,
@@ -256,7 +257,12 @@ export class VariantAnalysisManager
}),
token,
);
});
},
{
title: "Run Variant Analysis",
cancellable: true,
},
);
}
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {