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 { LOGGING_FLAGS } from "./cli-command";
import type { CliFeatures, VersionAndFeatures } from "./cli-version"; import type { CliFeatures, VersionAndFeatures } from "./cli-version";
import { ExitCodeError, getCliError } from "./cli-errors"; import { ExitCodeError, getCliError } from "./cli-errors";
import { UserCancellationException } from "../common/vscode/progress";
/** /**
* The version of the SARIF format that we are using. * 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. * If true, don't print logs to the CodeQL extension log.
*/ */
silent?: boolean; 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 & { 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( private async handleProcessOutput(
process: ChildProcessWithoutNullStreams, 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 progressReporter Used to output progress messages, e.g. to the status bar.
* @param onLine Used for responding to interactive output on stdout/stdin. * @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 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. * @returns The contents of the command's stdout, if the command succeeded.
*/ */
runCodeQlCliCommand( runCodeQlCliCommand(
command: string[], command: string[],
commandArgs: string[], commandArgs: string[],
description: string, description: string,
{ progressReporter, onLine, silent = false }: RunOptions = {}, {
progressReporter,
onLine,
silent = false,
runInNewProcess = false,
token,
}: RunOptions = {},
): Promise<string> { ): Promise<string> {
if (progressReporter) { if (progressReporter) {
progressReporter.report({ message: description }); progressReporter.report({ message: description });
} }
if (runInNewProcess) {
return this.runCodeQlCliInNewProcess(
command,
commandArgs,
description,
onLine,
silent,
token,
);
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Construct the command that actually does the work // Construct the command that actually does the work
const callback = (): void => { const callback = (): void => {
@@ -1537,6 +1628,7 @@ export class CodeQLCliServer implements Disposable {
* @param outputBundleFile The path to the output bundle file. * @param outputBundleFile The path to the output bundle file.
* @param outputPackDir The directory to contain the unbundled output pack. * @param outputPackDir The directory to contain the unbundled output pack.
* @param moreOptions Additional options to be passed to `codeql pack bundle`. * @param moreOptions Additional options to be passed to `codeql pack bundle`.
* @param token Cancellation token for the operation.
*/ */
async packBundle( async packBundle(
sourcePackDir: string, sourcePackDir: string,
@@ -1544,6 +1636,7 @@ export class CodeQLCliServer implements Disposable {
outputBundleFile: string, outputBundleFile: string,
outputPackDir: string, outputPackDir: string,
moreOptions: string[], moreOptions: string[],
token?: CancellationToken,
): Promise<void> { ): Promise<void> {
const args = [ const args = [
"-o", "-o",
@@ -1559,6 +1652,10 @@ export class CodeQLCliServer implements Disposable {
["pack", "bundle"], ["pack", "bundle"],
args, args,
"Bundling pack", "Bundling pack",
{
runInNewProcess: true,
token,
},
); );
} }

View File

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

View File

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

View File

@@ -221,42 +221,48 @@ export class VariantAnalysisManager
} }
public async runVariantAnalysisFromPublishedPack(): Promise<void> { public async runVariantAnalysisFromPublishedPack(): Promise<void> {
return withProgress(async (progress, token) => { return withProgress(
progress({ async (progress, token) => {
maxStep: 7, progress({
step: 0, maxStep: 7,
message: "Determining query language", step: 0,
}); message: "Determining query language",
});
const language = await askForLanguage(this.cliServer); const language = await askForLanguage(this.cliServer);
if (!language) { if (!language) {
return; return;
} }
progress({ progress({
maxStep: 7, maxStep: 7,
step: 2, step: 2,
message: "Downloading query pack and resolving queries", message: "Downloading query pack and resolving queries",
}); });
// Build up details to pass to the functions that run the variant analysis. // Build up details to pass to the functions that run the variant analysis.
const qlPackDetails = await resolveCodeScanningQueryPack( const qlPackDetails = await resolveCodeScanningQueryPack(
this.app.logger, this.app.logger,
this.cliServer, this.cliServer,
language, language,
); );
await this.runVariantAnalysis( await this.runVariantAnalysis(
qlPackDetails, qlPackDetails,
(p) => (p) =>
progress({ progress({
...p, ...p,
maxStep: p.maxStep + 3, maxStep: p.maxStep + 3,
step: p.step + 3, step: p.step + 3,
}), }),
token, token,
); );
}); },
{
title: "Run Variant Analysis",
cancellable: true,
},
);
} }
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> { private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {