Remember remote queries across restarts

Remote query items will be stored in query history and will remain
available across restarts.

When the extension is restarted, any `InProgress` remote queries will
be monitored until they complete.

When clicked on, a remote query is opened and its results can be
downloaded. The query text and the query file can be opened from the
history menu. A remote query can be deleted as well, which will purge
all results from global storage.

Limitations:

1. Labels are not editable
2. Running multiple queries that each run on the same repository
   will have conflicting results and there will be errors when trying
   to view the results of the second query. This limitation is not new,
   but it is easier to hit now. See #1089.

Both of these limitations will be addressed in future PRs.
This commit is contained in:
Andrew Eisenberg
2022-02-18 09:38:04 -08:00
parent e11aa7af18
commit 5e06a615cd
12 changed files with 209 additions and 127 deletions

View File

@@ -90,13 +90,13 @@ import { CodeQlStatusBarHandler } from './status-bar';
import { Credentials } from './authentication';
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQuery } from './remote-queries/remote-query';
import { RemoteQueryResult } from './remote-queries/remote-query-result';
import { URLSearchParams } from 'url';
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
import * as sampleData from './remote-queries/sample-data';
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
/**
* extension.ts
@@ -455,11 +455,15 @@ async function activateWithInstalledDistribution(
queryStorageDir,
ctx,
queryHistoryConfigurationListener,
showResults,
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
showResultsForComparison(from, to),
);
await qhm.readQueryHistory();
qhm.onWillOpenQueryItem(async item => {
if (item.t === 'local' && item.completed) {
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.Forced);
}
});
ctx.subscriptions.push(qhm);
void logger.log('Initializing results panel interface.');
@@ -534,7 +538,6 @@ async function activateWithInstalledDistribution(
source.token,
);
item.completeThisQuery(completedQueryInfo);
await qhm.writeQueryHistory();
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.NotForced);
// Note we must update the query history view after showing results as the
// display and sorting might depend on the number of results
@@ -543,7 +546,7 @@ async function activateWithInstalledDistribution(
item.failureReason = e.message;
throw e;
} finally {
qhm.refreshTreeView();
await qhm.refreshTreeView();
source.dispose();
}
}
@@ -834,6 +837,12 @@ async function activateWithInstalledDistribution(
void logger.log('Initializing remote queries interface.');
const rqm = new RemoteQueriesManager(ctx, cliServer, qhm, queryStorageDir, logger);
ctx.subscriptions.push(rqm);
// wait until after the remote queries manager is initialized to read the query history
// since the rqm is notified of queries being added.
await qhm.readQueryHistory();
registerRemoteQueryTextProvider();
@@ -866,9 +875,9 @@ async function activateWithInstalledDistribution(
ctx.subscriptions.push(
commandRunner('codeQL.monitorRemoteQuery', async (
query: RemoteQuery,
queryItem: RemoteQueryHistoryItem,
token: CancellationToken) => {
await rqm.monitorRemoteQuery(query, token);
await rqm.monitorRemoteQuery(queryItem, token);
}));
ctx.subscriptions.push(

View File

@@ -288,13 +288,24 @@ export class QueryHistoryManager extends DisposableObject {
queryHistoryScrubber: Disposable | undefined;
private queryMetadataStorageLocation;
private readonly _onDidAddQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onDidAddQueryItem: Event<QueryHistoryInfo> = this
._onDidAddQueryItem.event;
private readonly _onDidRemoveQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onDidRemoveQueryItem: Event<QueryHistoryInfo> = this
._onDidRemoveQueryItem.event;
private readonly _onWillOpenQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
readonly onWillOpenQueryItem: Event<QueryHistoryInfo> = this
._onWillOpenQueryItem.event;
constructor(
private qs: QueryServerClient,
private dbm: DatabaseManager,
private queryStorageDir: string,
ctx: ExtensionContext,
private queryHistoryConfigListener: QueryHistoryConfig,
private selectedCallback: (item: CompletedLocalQueryInfo) => Promise<void>,
private doCompareCallback: (
from: CompletedLocalQueryInfo,
to: CompletedLocalQueryInfo
@@ -484,53 +495,54 @@ export class QueryHistoryManager extends DisposableObject {
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
this.treeDataProvider.allHistory = history;
this.treeDataProvider.allHistory.forEach((item) => {
this._onDidAddQueryItem.fire(item);
});
}
async writeQueryHistory(): Promise<void> {
const toSave = this.treeDataProvider.allHistory.filter(q => q.isCompleted());
await splatQueryHistory(toSave, this.queryMetadataStorageLocation);
}
async invokeCallbackOn(queryHistoryItem: QueryHistoryInfo) {
if (this.selectedCallback && queryHistoryItem.isCompleted()) {
const sc = this.selectedCallback;
await sc(queryHistoryItem as CompletedLocalQueryInfo);
}
await splatQueryHistory(this.treeDataProvider.allHistory, this.queryMetadataStorageLocation);
}
async handleOpenQuery(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
): Promise<void> {
// TODO will support remote queries
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
const queryPath = finalSingleItem.t === 'local'
? finalSingleItem.initialInfo.queryPath
: finalSingleItem.remoteQuery.queryFilePath;
const textDocument = await workspace.openTextDocument(
Uri.file(finalSingleItem.initialInfo.queryPath)
Uri.file(queryPath)
);
const editor = await window.showTextDocument(
textDocument,
ViewColumn.One
);
const queryText = finalSingleItem.initialInfo.queryText;
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
new Range(0, 0, textDocument.lineCount, 0)
),
queryText
)
);
if (finalSingleItem.t === 'local') {
const queryText = finalSingleItem.initialInfo.queryText;
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
await editor.edit((edit) =>
edit.replace(
textDocument.validateRange(
new Range(0, 0, textDocument.lineCount, 0)
),
queryText
)
);
}
}
}
async handleRemoveHistoryItem(
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
multiSelect: QueryHistoryInfo[] = []
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
const toDelete = (finalMultiSelect || [finalSingleItem]);
@@ -548,15 +560,13 @@ export class QueryHistoryManager extends DisposableObject {
} else {
// Remote queries can be removed locally, but not remotely.
// The user must cancel the query on GitHub Actions explicitly.
this.treeDataProvider.remove(item);
await item.deleteQuery();
void logger.log(`Deleted ${item.label}.`);
if (item.status === QueryStatus.InProgress) {
void showAndLogInformationMessage(
'The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.'
);
void logger.log('The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.');
}
this._onDidRemoveQueryItem.fire(item);
}
}));
@@ -564,7 +574,7 @@ export class QueryHistoryManager extends DisposableObject {
const current = this.treeDataProvider.getCurrent();
if (current !== undefined) {
await this.treeView.reveal(current, { select: true });
await this.invokeCallbackOn(current);
await this._onWillOpenQueryItem.fire(current);
}
}
@@ -635,7 +645,7 @@ export class QueryHistoryManager extends DisposableObject {
const from = this.compareWithItem || singleItem;
const to = await this.findOtherQueryToCompare(from, finalMultiSelect);
if (from.isCompleted() && to?.isCompleted()) {
if (from.completed && to?.completed) {
await this.doCompareCallback(from as CompletedLocalQueryInfo, to as CompletedLocalQueryInfo);
}
} catch (e) {
@@ -647,9 +657,8 @@ export class QueryHistoryManager extends DisposableObject {
singleItem: QueryHistoryInfo,
multiSelect: QueryHistoryInfo[]
) {
// TODO will support remote queries
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
@@ -668,7 +677,7 @@ export class QueryHistoryManager extends DisposableObject {
await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);
} else {
// show results on single click
await this.invokeCallbackOn(finalSingleItem);
await this._onWillOpenQueryItem.fire(finalSingleItem);
}
}
@@ -713,17 +722,20 @@ export class QueryHistoryManager extends DisposableObject {
) {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
// TODO will support remote queries
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}
const params = new URLSearchParams({
isQuickEval: String(!!finalSingleItem.initialInfo.quickEvalPosition),
isQuickEval: String(!!(finalSingleItem.t === 'local' && finalSingleItem.initialInfo.quickEvalPosition)),
queryText: encodeURIComponent(await this.getQueryText(finalSingleItem)),
});
const queryId = finalSingleItem.t === 'local'
? finalSingleItem.initialInfo.id
: finalSingleItem.queryId;
const uri = Uri.parse(
`codeql:${finalSingleItem.initialInfo.id}?${params.toString()}`, true
`codeql:${queryId}?${params.toString()}`, true
);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
@@ -809,13 +821,15 @@ export class QueryHistoryManager extends DisposableObject {
}
async getQueryText(item: QueryHistoryInfo): Promise<string> {
// TODO the query text for remote queries is not yet available
return item.t === 'local' ? item.initialInfo.queryText : '';
return item.t === 'local'
? item.initialInfo.queryText
: item.remoteQuery.queryText;
}
addQuery(item: QueryHistoryInfo) {
this.treeDataProvider.pushQuery(item);
this.updateTreeViewSelectionIfVisible();
this._onDidAddQueryItem.fire(item);
}
/**
@@ -1011,7 +1025,8 @@ the file in the file explorer and dragging it into the workspace.`
};
}
refreshTreeView(): void {
async refreshTreeView(): Promise<void> {
this.treeDataProvider.refresh();
await this.writeQueryHistory();
}
}

View File

@@ -291,7 +291,7 @@ export class LocalQueryInfo {
}
}
isCompleted(): boolean {
get completed(): boolean {
return !!this.completedQuery;
}

View File

@@ -38,7 +38,7 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
q.completedQuery.dispose = () => { /**/ };
}
} else if (q.t === 'remote') {
// TODO Remote queries are not implemented yet.
// noop
}
return q;
});
@@ -47,8 +47,11 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
// most likely another workspace has deleted them because the
// queries aged out.
return asyncFilter(parsedQueries, async (q) => {
if (q.t !== 'local') {
return false;
if (q.t === 'remote') {
// the slurper doesn't know where the remote queries are stored
// so we need to assume here that they exist. Later, we check to
// see if they exist on disk.
return true;
}
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
return !!resultsPath && await fs.pathExists(resultsPath);
@@ -57,6 +60,8 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
void showAndLogErrorMessage('Error loading query history.', {
fullMessage: ['Error loading query history.', e.stack].join('\n'),
});
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
await fs.remove(fsPath);
return [];
}
}
@@ -75,8 +80,8 @@ export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: str
if (!(await fs.pathExists(fsPath))) {
await fs.mkdir(path.dirname(fsPath), { recursive: true });
}
// remove incomplete queries since they cannot be recreated on restart
const filteredQueries = queries.filter(q => q.t === 'local' && q.completedQuery !== undefined);
// remove incomplete local queries since they cannot be recreated on restart
const filteredQueries = queries.filter(q => q.t === 'local' ? q.completedQuery !== undefined : true);
const data = JSON.stringify(filteredQueries, null, 2);
await fs.writeFile(fsPath, data);
} catch (e) {

View File

@@ -261,8 +261,8 @@ export class RemoteQueriesInterfaceManager {
return this.getPanel().webview.postMessage(msg);
}
private getDuration(startTime: Date, endTime: Date): string {
const diffInMs = startTime.getTime() - endTime.getTime();
private getDuration(startTime: number, endTime: number): string {
const diffInMs = startTime - endTime;
return this.formatDuration(diffInMs);
}
@@ -282,7 +282,8 @@ export class RemoteQueriesInterfaceManager {
}
}
private formatDate = (d: Date): string => {
private formatDate = (millis: number): string => {
const d = new Date(millis);
const datePart = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
const timePart = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', hour12: true });
return `${datePart} at ${timePart}`;

View File

@@ -21,11 +21,14 @@ import { assertNever } from '../pure/helpers-pure';
import { RemoteQueryHistoryItem } from './remote-query-history-item';
import { QueryHistoryManager } from '../query-history';
import { QueryStatus } from '../query-status';
import { DisposableObject } from '../pure/disposable-object';
import { QueryHistoryInfo } from '../query-results';
const autoDownloadMaxSize = 300 * 1024;
const autoDownloadMaxCount = 100;
export class RemoteQueriesManager {
const noop = () => { /* do nothing */ };
export class RemoteQueriesManager extends DisposableObject {
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;
private readonly analysesResultsManager: AnalysesResultsManager;
private readonly interfaceManager: RemoteQueriesInterfaceManager;
@@ -37,9 +40,50 @@ export class RemoteQueriesManager {
private readonly storagePath: string,
logger: Logger,
) {
super();
this.analysesResultsManager = new AnalysesResultsManager(ctx, storagePath, logger);
this.interfaceManager = new RemoteQueriesInterfaceManager(ctx, logger, this.analysesResultsManager);
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
// Handle events from the query history manager
this.push(this.qhm.onDidAddQueryItem(this.handleAddQueryItem.bind(this)));
this.push(this.qhm.onDidRemoveQueryItem(this.handleRemoveQueryItem.bind(this)));
this.push(this.qhm.onWillOpenQueryItem(this.handleOpenQueryItem.bind(this)));
}
private async handleAddQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
if (!(await this.queryHistoryItemExists(queryItem))) {
// In this case, the query was deleted from disk, most likely because it was purged
// by another workspace. We should remove it from the history manager.
await this.qhm.handleRemoveHistoryItem(queryItem);
} else if (queryItem.status === QueryStatus.InProgress) {
// In this case, last time we checked, the query was still in progress.
// We need to setup the monitor to check for completion.
await commands.executeCommand('codeQL.monitorRemoteQuery', queryItem);
}
}
}
private async handleRemoveQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
await this.removeStorageDirectory(queryItem);
}
}
private async handleOpenQueryItem(queryItem: QueryHistoryInfo) {
if (queryItem?.t === 'remote') {
try {
const remoteQueryResult = await this.retrieveJsonFile(queryItem, 'query-result.json') as RemoteQueryResult;
// open results in the background
void this.openResults(queryItem.remoteQuery, remoteQueryResult).then(
noop,
err => void showAndLogErrorMessage(err)
);
} catch (e) {
void showAndLogErrorMessage(`Could not open query results. ${e}`);
}
}
}
public async runRemoteQuery(
@@ -56,66 +100,74 @@ export class RemoteQueriesManager {
progress,
token);
if (querySubmission && querySubmission.query) {
void commands.executeCommand('codeQL.monitorRemoteQuery', querySubmission.query);
if (querySubmission?.query) {
const query = querySubmission.query;
const queryId = this.createQueryId(query.queryName);
const queryHistoryItem: RemoteQueryHistoryItem = {
t: 'remote',
status: QueryStatus.InProgress,
completed: false,
queryId,
label: query.queryName,
remoteQuery: query,
};
await this.prepareStorageDirectory(queryHistoryItem);
await this.storeJsonFile(queryHistoryItem, 'query.json', query);
this.qhm.addQuery(queryHistoryItem);
await this.qhm.refreshTreeView();
}
}
public async monitorRemoteQuery(
query: RemoteQuery,
queryItem: RemoteQueryHistoryItem,
cancellationToken: CancellationToken
): Promise<void> {
const queryId = this.createQueryId(query.queryName);
await this.prepareStorageDirectory(queryId);
await this.storeFile(queryId, 'query.json', query);
const queryHistoryItem = new RemoteQueryHistoryItem(query.queryName, queryId, this.storagePath);
this.qhm.addQuery(queryHistoryItem);
const credentials = await Credentials.initialize(this.ctx);
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(query, cancellationToken);
const queryWorkflowResult = await this.remoteQueriesMonitor.monitorQuery(queryItem.remoteQuery, cancellationToken);
const executionEndTime = new Date();
const executionEndTime = Date.now();
if (queryWorkflowResult.status === 'CompletedSuccessfully') {
const resultIndex = await getRemoteQueryIndex(credentials, query);
const resultIndex = await getRemoteQueryIndex(credentials, queryItem.remoteQuery);
if (!resultIndex) {
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${query.queryName}`);
void showAndLogErrorMessage(`There was an issue retrieving the result for the query ${queryItem.label}`);
return;
}
queryHistoryItem.completed = true;
queryHistoryItem.status = QueryStatus.Completed;
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryId);
queryItem.completed = true;
queryItem.status = QueryStatus.Completed;
const queryResult = this.mapQueryResult(executionEndTime, resultIndex, queryItem.queryId);
await this.storeFile(queryId, 'query-result.json', queryResult);
await this.storeJsonFile(queryItem, 'query-result.json', queryResult);
// Kick off auto-download of results in the background.
void commands.executeCommand('codeQL.autoDownloadRemoteQueryResults', queryResult);
// Ask if the user wants to open the results in the background.
void this.askToOpenResults(query, queryResult).then(
() => { /* do nothing */ },
void this.askToOpenResults(queryItem.remoteQuery, queryResult).then(
noop,
err => {
void showAndLogErrorMessage(err);
}
);
} else if (queryWorkflowResult.status === 'CompletedUnsuccessfully') {
queryHistoryItem.failureReason = queryWorkflowResult.error;
queryHistoryItem.status = QueryStatus.Failed;
await showAndLogErrorMessage(`Remote query execution failed. Error: ${queryWorkflowResult.error}`);
queryItem.failureReason = queryWorkflowResult.error;
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage(`Remote query execution failed. Error: ${queryWorkflowResult.error}`);
} else if (queryWorkflowResult.status === 'Cancelled') {
queryHistoryItem.failureReason = 'Cancelled';
queryHistoryItem.status = QueryStatus.Failed;
await showAndLogErrorMessage('Remote query monitoring was cancelled');
queryItem.failureReason = 'Cancelled';
queryItem.status = QueryStatus.Failed;
void showAndLogErrorMessage('Remote query monitoring was cancelled');
} else if (queryWorkflowResult.status === 'InProgress') {
// Should not get here
await showAndLogErrorMessage(`Unexpected status: ${queryWorkflowResult.status}`);
// Should not get here. Only including this to ensure `assertNever` uses proper type checking.
void showAndLogErrorMessage(`Unexpected status: ${queryWorkflowResult.status}`);
} else {
// Ensure all cases are covered
assertNever(queryWorkflowResult.status);
}
this.qhm.refreshTreeView();
await this.qhm.refreshTreeView();
}
public async autoDownloadRemoteQueryResults(
@@ -138,7 +190,7 @@ export class RemoteQueriesManager {
results => this.interfaceManager.setAnalysisResults(results));
}
private mapQueryResult(executionEndTime: Date, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult {
private mapQueryResult(executionEndTime: number, resultIndex: RemoteQueryResultIndex, queryId: string): RemoteQueryResult {
const analysisSummaries = resultIndex.successes.map(item => ({
nwo: item.nwo,
@@ -163,13 +215,17 @@ export class RemoteQueriesManager {
};
}
public async openResults(query: RemoteQuery, queryResult: RemoteQueryResult) {
await this.interfaceManager.showResults(query, queryResult);
}
private async askToOpenResults(query: RemoteQuery, queryResult: RemoteQueryResult): Promise<void> {
const totalResultCount = queryResult.analysisSummaries.reduce((acc, cur) => acc + cur.resultCount, 0);
const message = `Query "${query.queryName}" run on ${query.repositories.length} repositories and returned ${totalResultCount} results`;
const shouldOpenView = await showInformationMessageWithAction(message, 'View');
if (shouldOpenView) {
await this.interfaceManager.showResults(query, queryResult);
await this.openResults(query, queryResult);
}
}
@@ -191,13 +247,27 @@ export class RemoteQueriesManager {
*
* @param queryName The name of the query that was run.
*/
private async prepareStorageDirectory(queryId: string): Promise<void> {
await createTimestampFile(path.join(this.storagePath, queryId));
private async prepareStorageDirectory(queryHistoryItem: RemoteQueryHistoryItem): Promise<void> {
await createTimestampFile(path.join(this.storagePath, queryHistoryItem.queryId));
}
private async storeFile(queryId: string, fileName: string, obj: any): Promise<void> {
const filePath = path.join(this.storagePath, queryId, fileName);
private async storeJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string, obj: T): Promise<void> {
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), 'utf8');
}
private async retrieveJsonFile<T>(queryHistoryItem: RemoteQueryHistoryItem, fileName: string): Promise<T> {
const filePath = path.join(this.storagePath, queryHistoryItem.queryId, fileName);
return JSON.parse(await fs.readFile(filePath, 'utf8'));
}
private async removeStorageDirectory(queryItem: RemoteQueryHistoryItem): Promise<void> {
const filePath = path.join(this.storagePath, queryItem.queryId);
await fs.remove(filePath);
}
private async queryHistoryItemExists(queryItem: RemoteQueryHistoryItem): Promise<boolean> {
const filePath = path.join(this.storagePath, queryItem.queryId);
return await fs.pathExists(filePath);
}
}

View File

@@ -1,33 +1,15 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { QueryStatus } from '../query-status';
import { RemoteQuery } from './remote-query';
/**
* Information about a remote query.
*/
export class RemoteQueryHistoryItem {
readonly t = 'remote';
failureReason: string | undefined;
export interface RemoteQueryHistoryItem {
readonly t: 'remote';
failureReason?: string;
status: QueryStatus;
completed = false;
constructor(
public label: string, // TODO, the query label should have interpolation like local queries
public readonly queryId: string,
private readonly storagePath: string,
) {
this.status = QueryStatus.InProgress;
}
isCompleted(): boolean {
return this.completed;
}
async deleteQuery(): Promise<void> {
await fs.remove(this.querySaveDir);
}
get querySaveDir(): string {
return path.join(this.storagePath, this.queryId);
}
completed: boolean;
readonly queryId: string,
label: string, // TODO, the query label should have interpolation like local queries
remoteQuery: RemoteQuery,
}

View File

@@ -2,7 +2,7 @@ import { DownloadLink } from './download-link';
import { AnalysisFailure } from './shared/analysis-failure';
export interface RemoteQueryResult {
executionEndTime: Date;
executionEndTime: number; // Can't use a Date here since it needs to be serialized and desserialized.
analysisSummaries: AnalysisSummary[];
analysisFailures: AnalysisFailure[];
}

View File

@@ -6,6 +6,6 @@ export interface RemoteQuery {
queryText: string;
controllerRepository: Repository;
repositories: Repository[];
executionStartTime: Date;
executionStartTime: number; // Use number here since it needs to be serialized and desserialized.
actionsWorkflowRunId: number;
}

View File

@@ -303,7 +303,7 @@ export async function runRemoteQuery(
});
const workflowRunId = await runRemoteQueriesApiRequest(credentials, 'main', language, repositories, owner, repo, base64Pack, dryRun);
const queryStartTime = new Date();
const queryStartTime = Date.now();
const queryMetadata = await tryGetQueryMetadata(cliServer, queryFile);
if (dryRun) {
@@ -435,7 +435,7 @@ async function buildRemoteQueryEntity(
queryMetadata: QueryMetadata | undefined,
controllerRepoOwner: string,
controllerRepoName: string,
queryStartTime: Date,
queryStartTime: number,
workflowRunId: number
): Promise<RemoteQuery> {
// The query name is either the name as specified in the query metadata, or the file name.

View File

@@ -32,12 +32,12 @@ export const sampleRemoteQuery: RemoteQuery = {
name: 'repo5'
}
],
executionStartTime: new Date('2022-01-06T17:02:15.026Z'),
executionStartTime: new Date('2022-01-06T17:02:15.026Z').getTime(),
actionsWorkflowRunId: 1662757118
};
export const sampleRemoteQueryResult: RemoteQueryResult = {
executionEndTime: new Date('2022-01-06T17:04:37.026Z'),
executionEndTime: new Date('2022-01-06T17:04:37.026Z').getTime(),
analysisSummaries: [
{
nwo: 'big-corp/repo1',

View File

@@ -743,12 +743,12 @@ describe('query-history', () => {
extensionPath: vscode.Uri.file('/x/y/z').fsPath,
} as vscode.ExtensionContext,
configListener,
selectedCallback,
doCompareCallback
);
qhm.onWillOpenQueryItem(selectedCallback);
(qhm.treeDataProvider as any).history = [...allHistory];
await vscode.workspace.saveAll();
qhm.refreshTreeView();
await qhm.refreshTreeView();
return qhm;
}
});