Merge pull request #1162 from github/aeisenberg/remote-query-restart
Remember remote queries across restarts
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ export class LocalQueryInfo {
|
||||
}
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
get completed(): boolean {
|
||||
return !!this.completedQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user