Consume codeql version JSON output for feature capabilities

This commit is contained in:
Dave Bartolomeo
2024-01-11 16:57:42 -05:00
parent b67efeeacd
commit fb63ec7db0
5 changed files with 119 additions and 40 deletions

View File

@@ -3,7 +3,10 @@ import { promisify } from "util";
import type { BaseLogger } from "../common/logging";
import type { ProgressReporter } from "../common/logging/vscode";
import { getChildProcessErrorMessage } from "../common/helpers-pure";
import {
getChildProcessErrorMessage,
getErrorMessage,
} from "../common/helpers-pure";
/**
* Flags to pass to all cli commands.
@@ -11,26 +14,27 @@ import { getChildProcessErrorMessage } from "../common/helpers-pure";
export const LOGGING_FLAGS = ["-v", "--log-to-stderr"];
/**
* Runs a CodeQL CLI command without invoking the CLI server, returning the output as a string.
* Runs a CodeQL CLI command without invoking the CLI server, deserializing the output as JSON.
* @param codeQlPath The path to the CLI.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param logger Logger to write command log messages, e.g. to an output channel.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The contents of the command's stdout, if the command succeeded.
* @returns A JSON object parsed from the contents of the command's stdout, if the command succeeded.
*/
export async function runCodeQlCliCommand(
export async function runJsonCodeQlCliCommand<OutputType>(
codeQlPath: string,
command: string[],
commandArgs: string[],
description: string,
logger: BaseLogger,
progressReporter?: ProgressReporter,
): Promise<string> {
): Promise<OutputType> {
// Add logging arguments first, in case commandArgs contains positional parameters.
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
let stdout: string;
try {
if (progressReporter !== undefined) {
progressReporter.report({ message: description });
@@ -41,10 +45,18 @@ export async function runCodeQlCliCommand(
const result = await promisify(execFile)(codeQlPath, args);
void logger.log(result.stderr);
void logger.log("CLI command succeeded.");
return result.stdout;
stdout = result.stdout;
} catch (err) {
throw new Error(
`${description} failed: ${getChildProcessErrorMessage(err)}`,
);
}
try {
return JSON.parse(stdout) as OutputType;
} catch (err) {
throw new Error(
`Parsing output of ${description} failed: ${getErrorMessage(err)}`,
);
}
}

View File

@@ -1,25 +1,49 @@
import type { SemVer } from "semver";
import { parse } from "semver";
import { runCodeQlCliCommand } from "./cli-command";
import { runJsonCodeQlCliCommand } from "./cli-command";
import type { Logger } from "../common/logging";
import { getErrorMessage } from "../common/helpers-pure";
interface VersionResult {
version: string;
features: CliFeatures | undefined;
}
export interface CliFeatures {
featuresInVersionResult?: boolean;
mrvaPackCreate?: boolean;
}
export interface VersionAndFeatures {
version: SemVer;
features: CliFeatures;
}
/**
* Get the version of a CodeQL CLI.
*/
export async function getCodeQlCliVersion(
codeQlPath: string,
logger: Logger,
): Promise<SemVer | undefined> {
): Promise<VersionAndFeatures | undefined> {
try {
const output: string = await runCodeQlCliCommand(
const output: VersionResult = await runJsonCodeQlCliCommand<VersionResult>(
codeQlPath,
["version"],
["--format=terse"],
["--format=json"],
"Checking CodeQL version",
logger,
);
return parse(output.trim()) || undefined;
const version = parse(output.version.trim()) || undefined;
if (version === undefined) {
return undefined;
}
return {
version,
features: output.features ?? {},
};
} catch (e) {
// Failed to run the version command. This might happen if the cli version is _really_ old, or it is corrupted.
// Either way, we can't determine compatibility.

View File

@@ -34,6 +34,7 @@ import { QueryLanguage } from "../common/query-language";
import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
import type { Position } from "../query-server/messages";
import { LOGGING_FLAGS } from "./cli-command";
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
/**
* The version of the SARIF format that we are using.
@@ -193,7 +194,9 @@ type OnLineCallback = (
line: string,
) => Promise<string | undefined> | string | undefined;
type VersionChangedListener = (newVersion: SemVer | undefined) => void;
type VersionChangedListener = (
newVersionAndFeatures: VersionAndFeatures | undefined,
) => void;
/**
* This class manages a cli server started by `codeql execute cli-server` to
@@ -211,8 +214,8 @@ export class CodeQLCliServer implements Disposable {
/** A buffer with a single null byte. */
nullBuffer: Buffer;
/** Version of current cli, lazily computed by the `getVersion()` method */
private _version: SemVer | undefined;
/** Version of current cli and its supported features, lazily computed by the `getVersion()` method */
private _versionAndFeatures: VersionAndFeatures | undefined;
private _versionChangedListeners: VersionChangedListener[] = [];
@@ -288,7 +291,7 @@ export class CodeQLCliServer implements Disposable {
const callback = (): void => {
try {
this.killProcessIfRunning();
this._version = undefined;
this._versionAndFeatures = undefined;
this._supportedLanguages = undefined;
} finally {
this.runNext();
@@ -1417,6 +1420,27 @@ export class CodeQLCliServer implements Disposable {
);
}
public async packCreate(
dir: string,
workspaceFolders: string[],
outputPath: string,
moreOptions: string[],
): Promise<void> {
const args = [
"--output",
outputPath,
dir,
...moreOptions,
...this.getAdditionalPacksArg(workspaceFolders),
];
return this.runJsonCodeQlCliCommandWithAuthentication(
["pack", "create"],
args,
"Creating pack",
);
}
async packBundle(
dir: string,
workspaceFolders: string[],
@@ -1481,27 +1505,35 @@ export class CodeQLCliServer implements Disposable {
);
}
public async getVersion() {
if (!this._version) {
public async getVersion(): Promise<SemVer> {
return (await this.getVersionAndFeatures()).version;
}
public async getFeatures(): Promise<CliFeatures> {
return (await this.getVersionAndFeatures()).features;
}
public async getVersionAndFeatures(): Promise<VersionAndFeatures> {
if (!this._versionAndFeatures) {
try {
const newVersion = await this.refreshVersion();
this._version = newVersion;
const newVersionAndFeatures = await this.refreshVersion();
this._versionAndFeatures = newVersionAndFeatures;
this._versionChangedListeners.forEach((listener) =>
listener(newVersion),
listener(newVersionAndFeatures),
);
// this._version is only undefined upon config change, so we reset CLI-based context key only when necessary.
await this.app.commands.execute(
"setContext",
"codeql.supportsQuickEvalCount",
newVersion.compare(
newVersionAndFeatures.version.compare(
CliVersionConstraint.CLI_VERSION_WITH_QUICK_EVAL_COUNT,
) >= 0,
);
await this.app.commands.execute(
"setContext",
"codeql.supportsTrimCache",
newVersion.compare(
newVersionAndFeatures.version.compare(
CliVersionConstraint.CLI_VERSION_WITH_TRIM_CACHE,
) >= 0,
);
@@ -1512,23 +1544,23 @@ export class CodeQLCliServer implements Disposable {
throw e;
}
}
return this._version;
return this._versionAndFeatures;
}
public addVersionChangedListener(listener: VersionChangedListener) {
if (this._version) {
listener(this._version);
if (this._versionAndFeatures) {
listener(this._versionAndFeatures);
}
this._versionChangedListeners.push(listener);
}
private async refreshVersion() {
private async refreshVersion(): Promise<VersionAndFeatures> {
const distribution = await this.distributionProvider.getDistribution();
switch (distribution.kind) {
case FindDistributionResultKind.CompatibleDistribution:
// eslint-disable-next-line no-fallthrough -- Intentional fallthrough
case FindDistributionResultKind.IncompatibleDistribution:
return distribution.version;
return distribution.versionAndFeatures;
default:
// We should not get here because if no distributions are available, then
@@ -1745,4 +1777,8 @@ export class CliVersionConstraint {
CliVersionConstraint.CLI_VERSION_WITH_EXTENSIBLE_PREDICATE_METADATA,
);
}
async supportsMrvaPackCreate(): Promise<boolean> {
return (await this.cli.getFeatures()).mrvaPackCreate === true;
}
}

View File

@@ -1,11 +1,11 @@
import { createWriteStream, mkdtemp, pathExists, remove } from "fs-extra";
import { tmpdir } from "os";
import { delimiter, dirname, join } from "path";
import type { SemVer } from "semver";
import { Range, satisfies } from "semver";
import type { Event, ExtensionContext } from "vscode";
import type { DistributionConfig } from "../config";
import { extLogger } from "../common/logging/vscode";
import type { VersionAndFeatures } from "./cli-version";
import { getCodeQlCliVersion } from "./cli-version";
import type { ProgressCallback } from "../common/vscode/progress";
import { reportStreamProgress } from "../common/vscode/progress";
@@ -88,11 +88,11 @@ export class DistributionManager implements DistributionProvider {
kind: FindDistributionResultKind.NoDistribution,
};
}
const version = await getCodeQlCliVersion(
const versionAndFeatures = await getCodeQlCliVersion(
distribution.codeQlPath,
extLogger,
);
if (version === undefined) {
if (versionAndFeatures === undefined) {
return {
distribution,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
@@ -119,17 +119,21 @@ export class DistributionManager implements DistributionProvider {
distribution.kind !== DistributionKind.ExtensionManaged ||
this.config.includePrerelease;
if (!satisfies(version, this.versionRange, { includePrerelease })) {
if (
!satisfies(versionAndFeatures.version, this.versionRange, {
includePrerelease,
})
) {
return {
distribution,
kind: FindDistributionResultKind.IncompatibleDistribution,
version,
versionAndFeatures,
};
}
return {
distribution,
kind: FindDistributionResultKind.CompatibleDistribution,
version,
versionAndFeatures,
};
}
@@ -599,7 +603,7 @@ interface DistributionResult {
interface CompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.CompatibleDistribution;
version: SemVer;
versionAndFeatures: VersionAndFeatures;
}
interface UnknownCompatibilityDistributionResult extends DistributionResult {
@@ -608,7 +612,7 @@ interface UnknownCompatibilityDistributionResult extends DistributionResult {
interface IncompatibleDistributionResult extends DistributionResult {
kind: FindDistributionResultKind.IncompatibleDistribution;
version: SemVer;
versionAndFeatures: VersionAndFeatures;
}
interface NoDistributionResult {

View File

@@ -418,7 +418,7 @@ export async function activate(
codeQlExtension.variantAnalysisManager,
);
codeQlExtension.cliServer.addVersionChangedListener((ver) => {
telemetryListener.cliVersion = ver;
telemetryListener.cliVersion = ver?.version;
});
let unsupportedWarningShown = false;
@@ -431,13 +431,16 @@ export async function activate(
return;
}
if (CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver) < 0) {
if (
CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION.compare(ver.version) <
0
) {
return;
}
void showAndLogWarningMessage(
extLogger,
`You are using an unsupported version of the CodeQL CLI (${ver}). ` +
`You are using an unsupported version of the CodeQL CLI (${ver.version}). ` +
`The minimum supported version is ${CliVersionConstraint.OLDEST_SUPPORTED_CLI_VERSION}. ` +
`Please upgrade to a newer version of the CodeQL CLI.`,
);
@@ -592,7 +595,7 @@ async function getDistributionDisplayingDistributionWarnings(
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
void extLogger.log(
`Found compatible version of CodeQL CLI (version ${result.version.raw})`,
`Found compatible version of CodeQL CLI (version ${result.versionAndFeatures.version.raw})`,
);
break;
case FindDistributionResultKind.IncompatibleDistribution: {
@@ -612,7 +615,7 @@ async function getDistributionDisplayingDistributionWarnings(
void showAndLogWarningMessage(
extLogger,
`The current version of the CodeQL CLI (${result.version.raw}) ` +
`The current version of the CodeQL CLI (${result.versionAndFeatures.version.raw}) ` +
`is incompatible with this extension. ${fixGuidanceMessage}`,
);
break;