Skip to content

Fix results directory and evaluator log for cancelled queries #2996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 23, 2023
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
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- The "Install Pack Dependencies" will now only list CodeQL packs located in the workspace. [#2960](https://github.com/github/vscode-codeql/pull/2960)
- Fix a bug where the "View Query Log" action for a query history item was not working. [#2984](https://github.com/github/vscode-codeql/pull/2984)
- Add a command to sort items in the databases view by language. [#2993](https://github.com/github/vscode-codeql/pull/2993)
- Fix not being able to open the results directory or evaluator log for a cancelled local query run. [#2996](https://github.com/github/vscode-codeql/pull/2996)

## 1.9.2 - 12 October 2023

Expand Down
14 changes: 9 additions & 5 deletions extensions/ql-vscode/src/local-queries/local-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,15 @@ export class LocalQueries extends DisposableObject {
);
}

const initialInfo = await createInitialQueryInfo(selectedQuery, {
databaseUri: dbItem.databaseUri.toString(),
name: dbItem.name,
language: tryGetQueryLanguage(dbItem.language),
});
const initialInfo = await createInitialQueryInfo(
selectedQuery,
{
databaseUri: dbItem.databaseUri.toString(),
name: dbItem.name,
language: tryGetQueryLanguage(dbItem.language),
},
outputDir,
);

// When cancellation is requested from the query history view, we just stop the debug session.
const queryInfo = new LocalQueryInfo(initialInfo, tokenSource);
Expand Down
9 changes: 9 additions & 0 deletions extensions/ql-vscode/src/local-queries/local-query-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ export class LocalQueryRun {
* Updates the UI in the case where query evaluation throws an exception.
*/
public async fail(err: Error): Promise<void> {
const evalLogPaths = await this.summarizeEvalLog(
QueryResultType.OTHER_ERROR,
this.outputDir,
this.logger,
);
if (evalLogPaths !== undefined) {
this.queryInfo.setEvaluatorLogPaths(evalLogPaths);
}

err.message = `Error running query: ${err.message}`;
this.queryInfo.failureReason = err.message;
await this.queryHistoryManager.refreshTreeView();
Expand Down
80 changes: 36 additions & 44 deletions extensions/ql-vscode/src/query-history/query-history-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,32 +702,15 @@ export class QueryHistoryManager extends DisposableObject {
}
}

async getQueryHistoryItemDirectory(
queryHistoryItem: QueryHistoryInfo,
): Promise<string> {
if (queryHistoryItem.t === "local") {
if (queryHistoryItem.completedQuery) {
return queryHistoryItem.completedQuery.query.querySaveDir;
}
} else if (queryHistoryItem.t === "variant-analysis") {
return this.variantAnalysisManager.getVariantAnalysisStorageLocation(
queryHistoryItem.variantAnalysis.id,
);
} else {
assertNever(queryHistoryItem);
}

throw new Error("Unable to get query directory");
}

async handleOpenQueryDirectory(item: QueryHistoryInfo) {
let externalFilePath: string | undefined;
if (item.t === "local") {
if (item.completedQuery) {
externalFilePath = join(
item.completedQuery.query.querySaveDir,
"timestamp",
);
const querySaveDir =
item.initialInfo.outputDir?.querySaveDir ??
item.completedQuery?.query.querySaveDir;

if (querySaveDir) {
externalFilePath = join(querySaveDir, "timestamp");
}
} else if (item.t === "variant-analysis") {
externalFilePath = join(
Expand Down Expand Up @@ -761,37 +744,51 @@ export class QueryHistoryManager extends DisposableObject {
`Failed to open ${externalFilePath}: ${getErrorMessage(e)}`,
);
}
} else {
this.warnNoQueryDir();
}
}

private warnNoEvalLogs() {
private warnNoQueryDir() {
void showAndLogWarningMessage(
this.app.logger,
`Evaluator log, summary, and viewer are not available for this run. Perhaps it failed before evaluation?`,
`Results directory is not available for this run.`,
);
}

private warnInProgressEvalLogSummary() {
private warnNoEvalLogs() {
void showAndLogWarningMessage(
this.app.logger,
'The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.',
`Evaluator log, summary, and viewer are not available for this run. Perhaps it failed before evaluation?`,
);
}

private warnInProgressEvalLogViewer() {
void showAndLogWarningMessage(
this.app.logger,
"The viewer's data is still being generated for this run. Please try again or re-run the query.",
);
private async warnNoEvalLogSummary(item: LocalQueryInfo) {
const evalLogLocation =
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;

// Summary log file doesn't exist.
if (evalLogLocation && (await pathExists(evalLogLocation))) {
// If raw log does exist, then the summary log is still being generated.
void showAndLogWarningMessage(
this.app.logger,
'The evaluator log summary is still being generated for this run. Please try again later. The summary generation process is tracked in the "CodeQL Extension Log" view.',
);
} else {
this.warnNoEvalLogs();
}
}

async handleShowEvalLog(item: QueryHistoryInfo) {
if (item.t !== "local") {
return;
}

if (item.evalLogLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogLocation);
const evalLogLocation =
item.evalLogLocation ?? item.initialInfo.outputDir?.evalLogPath;

if (evalLogLocation && (await pathExists(evalLogLocation))) {
await tryOpenExternalFile(this.app.commands, evalLogLocation);
} else {
this.warnNoEvalLogs();
}
Expand All @@ -802,18 +799,13 @@ export class QueryHistoryManager extends DisposableObject {
return;
}

if (item.evalLogSummaryLocation) {
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
// If the summary file location wasn't saved, display error
if (!item.evalLogSummaryLocation) {
await this.warnNoEvalLogSummary(item);
return;
}

// Summary log file doesn't exist.
if (item.evalLogLocation && (await pathExists(item.evalLogLocation))) {
// If raw log does exist, then the summary log is still being generated.
this.warnInProgressEvalLogSummary();
} else {
this.warnNoEvalLogs();
}
await tryOpenExternalFile(this.app.commands, item.evalLogSummaryLocation);
}

async handleShowEvalLogViewer(item: QueryHistoryInfo) {
Expand All @@ -823,7 +815,7 @@ export class QueryHistoryManager extends DisposableObject {

// If the JSON summary file location wasn't saved, display error
if (item.jsonEvalLogSummaryLocation === undefined) {
this.warnInProgressEvalLogViewer();
await this.warnNoEvalLogSummary(item);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ function mapInitialQueryInfoToDto(
},
start: localQueryInitialInfo.start,
id: localQueryInitialInfo.id,
outputDir: localQueryInitialInfo.outputDir
? {
querySaveDir: localQueryInitialInfo.outputDir.querySaveDir,
}
: undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
CompletedQueryInfo,
InitialQueryInfo,
} from "../../query-results";
import { QueryEvaluationInfo } from "../../run-queries-shared";
import { QueryEvaluationInfo, QueryOutputDir } from "../../run-queries-shared";
import {
CompletedQueryInfoDto,
QueryEvaluationInfoDto,
Expand All @@ -26,7 +26,10 @@ export function mapLocalQueryItemToDomainModel(
localQuery: QueryHistoryLocalQueryDto,
): LocalQueryInfo {
return new LocalQueryInfo(
mapInitialQueryInfoToDomainModel(localQuery.initialInfo),
mapInitialQueryInfoToDomainModel(
localQuery.initialInfo,
localQuery.completedQuery?.query?.querySaveDir,
),
undefined,
localQuery.failureReason,
localQuery.completedQuery &&
Expand Down Expand Up @@ -72,7 +75,14 @@ function mapCompletedQueryInfoToDomainModel(

function mapInitialQueryInfoToDomainModel(
initialInfo: InitialQueryInfoDto,
// The completedQuerySaveDir is a migration to support old query items that don't have
// the querySaveDir in the initialInfo. It should be removed once all query
// items have the querySaveDir in the initialInfo.
completedQuerySaveDir?: string,
): InitialQueryInfo {
const querySaveDir =
initialInfo.outputDir?.querySaveDir ?? completedQuerySaveDir;

return {
userSpecifiedLabel: initialInfo.userSpecifiedLabel,
queryText: initialInfo.queryText,
Expand All @@ -90,6 +100,7 @@ function mapInitialQueryInfoToDomainModel(
},
start: new Date(initialInfo.start),
id: initialInfo.id,
outputDir: querySaveDir ? new QueryOutputDir(querySaveDir) : undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface InitialQueryInfoDto {
databaseInfo: DatabaseInfoDto;
start: Date;
id: string;
outputDir?: QueryOutputDirDto; // Undefined for backwards compatibility
}

interface QueryOutputDirDto {
querySaveDir: string;
}

interface DatabaseInfoDto {
Expand Down
2 changes: 2 additions & 0 deletions extensions/ql-vscode/src/query-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { QueryStatus } from "./query-history/query-status";
import {
EvaluatorLogPaths,
QueryEvaluationInfo,
QueryOutputDir,
QueryWithResults,
} from "./run-queries-shared";
import { formatLegacyMessage } from "./query-server/legacy";
Expand Down Expand Up @@ -47,6 +48,7 @@ export interface InitialQueryInfo {
readonly databaseInfo: DatabaseInfo;
readonly start: Date;
readonly id: string; // unique id for this query.
readonly outputDir?: QueryOutputDir; // If missing, we do not have a query save dir. The query may have been cancelled. This is only for backwards compatibility.
}

export class CompletedQueryInfo implements QueryWithResults {
Expand Down
3 changes: 3 additions & 0 deletions extensions/ql-vscode/src/run-queries-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,13 @@ async function convertToQlPath(filePath: string): Promise<string> {
*
* @param selectedQuery The query to run, including any quickeval info.
* @param databaseInfo The database to run the query against.
* @param outputDir The output directory for this query.
* @returns The initial information for the query to be run.
*/
export async function createInitialQueryInfo(
selectedQuery: SelectedQuery,
databaseInfo: DatabaseInfo,
outputDir: QueryOutputDir,
): Promise<InitialQueryInfo> {
const isQuickEval = selectedQuery.quickEval !== undefined;
const isQuickEvalCount =
Expand All @@ -587,6 +589,7 @@ export async function createInitialQueryInfo(
: {
queryText: await readFile(selectedQuery.queryPath, "utf8"),
}),
outputDir,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { faker } from "@faker-js/faker";
import { InitialQueryInfo, LocalQueryInfo } from "../../../src/query-results";
import {
QueryEvaluationInfo,
QueryOutputDir,
QueryWithResults,
} from "../../../src/run-queries-shared";
import { CancellationTokenSource } from "vscode";
Expand All @@ -18,6 +19,7 @@ export function createMockLocalQueryInfo({
hasMetadata = false,
queryWithResults = undefined,
language = undefined,
outputDir = new QueryOutputDir("/a/b/c"),
}: {
startTime?: Date;
resultCount?: number;
Expand All @@ -27,6 +29,7 @@ export function createMockLocalQueryInfo({
hasMetadata?: boolean;
queryWithResults?: QueryWithResults | undefined;
language?: QueryLanguage;
outputDir?: QueryOutputDir | undefined;
}): LocalQueryInfo {
const cancellationToken = {
dispose: () => {
Expand All @@ -48,6 +51,7 @@ export function createMockLocalQueryInfo({
start: startTime,
id: faker.number.int().toString(),
userSpecifiedLabel,
outputDir,
} as InitialQueryInfo;

const localQuery = new LocalQueryInfo(initialQueryInfo, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
LocalQueryInfo,
InitialQueryInfo,
} from "../../../../../src/query-results";
import { QueryWithResults } from "../../../../../src/run-queries-shared";
import {
QueryOutputDir,
QueryWithResults,
} from "../../../../../src/run-queries-shared";
import { DatabaseInfo } from "../../../../../src/common/interface-types";
import { CancellationTokenSource, Uri } from "vscode";
import { tmpDir } from "../../../../../src/tmp-dir";
Expand Down Expand Up @@ -130,6 +133,38 @@ describe("write and read", () => {
expect(allHistoryActual.length).toEqual(expectedHistory.length);
});

it("should read query output dir from completed query if not present", async () => {
const historyPath = join(tmpDir.name, "workspace-query-history.json");

const queryItem = createMockFullQueryInfo(
"a",
createMockQueryWithResults(
`${queryPath}-a`,
false,
false,
"/a/b/c/a",
false,
),
false,
null,
);

// write and read
await writeQueryHistoryToFile([queryItem], historyPath);
const actual = await readQueryHistoryFromFile(historyPath);

expect(actual).toHaveLength(1);

expect(actual[0].t).toEqual("local");

if (actual[0].t === "local") {
expect(actual[0].initialInfo.outputDir?.querySaveDir).not.toBeUndefined();
expect(actual[0].initialInfo.outputDir?.querySaveDir).toEqual(
queryItem.completedQuery?.query?.querySaveDir,
);
}
});

it("should remove remote queries from the history", async () => {
const path = join(tmpDir.name, "query-history-with-remote.json");
await writeFile(
Expand Down Expand Up @@ -205,6 +240,9 @@ describe("write and read", () => {
dbName = "a",
queryWithResults?: QueryWithResults,
isFail = false,
outputDir: QueryOutputDir | null = new QueryOutputDir(
"/path/to/output/dir",
),
): LocalQueryInfo {
const fqi = new LocalQueryInfo(
{
Expand All @@ -218,6 +256,7 @@ describe("write and read", () => {
isQuickQuery: false,
isQuickEval: false,
id: `some-id-${dbName}`,
outputDir: outputDir ? outputDir : undefined,
} as InitialQueryInfo,
{
dispose: () => {
Expand Down
Loading