Skip to content

Commit cf0728a

Browse files
committed
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.
1 parent e11aa7a commit cf0728a

12 files changed

+209
-127
lines changed

extensions/ql-vscode/src/extension.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ import { CodeQlStatusBarHandler } from './status-bar';
9090

9191
import { Credentials } from './authentication';
9292
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
93-
import { RemoteQuery } from './remote-queries/remote-query';
9493
import { RemoteQueryResult } from './remote-queries/remote-query-result';
9594
import { URLSearchParams } from 'url';
9695
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
9796
import * as sampleData from './remote-queries/sample-data';
9897
import { handleDownloadPacks, handleInstallPackDependencies } from './packaging';
9998
import { AnalysesResultsManager } from './remote-queries/analyses-results-manager';
99+
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
100100

101101
/**
102102
* extension.ts
@@ -455,11 +455,15 @@ async function activateWithInstalledDistribution(
455455
queryStorageDir,
456456
ctx,
457457
queryHistoryConfigurationListener,
458-
showResults,
459458
async (from: CompletedLocalQueryInfo, to: CompletedLocalQueryInfo) =>
460459
showResultsForComparison(from, to),
461460
);
462-
await qhm.readQueryHistory();
461+
462+
qhm.onWillOpenQueryItem(async item => {
463+
if (item.t === 'local' && item.completed) {
464+
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.Forced);
465+
}
466+
});
463467

464468
ctx.subscriptions.push(qhm);
465469
void logger.log('Initializing results panel interface.');
@@ -534,7 +538,6 @@ async function activateWithInstalledDistribution(
534538
source.token,
535539
);
536540
item.completeThisQuery(completedQueryInfo);
537-
await qhm.writeQueryHistory();
538541
await showResultsForCompletedQuery(item as CompletedLocalQueryInfo, WebviewReveal.NotForced);
539542
// Note we must update the query history view after showing results as the
540543
// display and sorting might depend on the number of results
@@ -543,7 +546,7 @@ async function activateWithInstalledDistribution(
543546
item.failureReason = e.message;
544547
throw e;
545548
} finally {
546-
qhm.refreshTreeView();
549+
await qhm.refreshTreeView();
547550
source.dispose();
548551
}
549552
}
@@ -834,6 +837,12 @@ async function activateWithInstalledDistribution(
834837

835838
void logger.log('Initializing remote queries interface.');
836839
const rqm = new RemoteQueriesManager(ctx, cliServer, qhm, queryStorageDir, logger);
840+
ctx.subscriptions.push(rqm);
841+
842+
// wait until after the remote queries manager is initialized to read the query history
843+
// since the rqm is notified of queries being added.
844+
await qhm.readQueryHistory();
845+
837846

838847
registerRemoteQueryTextProvider();
839848

@@ -866,9 +875,9 @@ async function activateWithInstalledDistribution(
866875

867876
ctx.subscriptions.push(
868877
commandRunner('codeQL.monitorRemoteQuery', async (
869-
query: RemoteQuery,
878+
queryItem: RemoteQueryHistoryItem,
870879
token: CancellationToken) => {
871-
await rqm.monitorRemoteQuery(query, token);
880+
await rqm.monitorRemoteQuery(queryItem, token);
872881
}));
873882

874883
ctx.subscriptions.push(

extensions/ql-vscode/src/query-history.ts

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,24 @@ export class QueryHistoryManager extends DisposableObject {
288288
queryHistoryScrubber: Disposable | undefined;
289289
private queryMetadataStorageLocation;
290290

291+
private readonly _onDidAddQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
292+
readonly onDidAddQueryItem: Event<QueryHistoryInfo> = this
293+
._onDidAddQueryItem.event;
294+
295+
private readonly _onDidRemoveQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
296+
readonly onDidRemoveQueryItem: Event<QueryHistoryInfo> = this
297+
._onDidRemoveQueryItem.event;
298+
299+
private readonly _onWillOpenQueryItem = super.push(new EventEmitter<QueryHistoryInfo>());
300+
readonly onWillOpenQueryItem: Event<QueryHistoryInfo> = this
301+
._onWillOpenQueryItem.event;
302+
291303
constructor(
292304
private qs: QueryServerClient,
293305
private dbm: DatabaseManager,
294306
private queryStorageDir: string,
295307
ctx: ExtensionContext,
296308
private queryHistoryConfigListener: QueryHistoryConfig,
297-
private selectedCallback: (item: CompletedLocalQueryInfo) => Promise<void>,
298309
private doCompareCallback: (
299310
from: CompletedLocalQueryInfo,
300311
to: CompletedLocalQueryInfo
@@ -484,53 +495,54 @@ export class QueryHistoryManager extends DisposableObject {
484495
void logger.log(`Reading cached query history from '${this.queryMetadataStorageLocation}'.`);
485496
const history = await slurpQueryHistory(this.queryMetadataStorageLocation, this.queryHistoryConfigListener);
486497
this.treeDataProvider.allHistory = history;
498+
this.treeDataProvider.allHistory.forEach((item) => {
499+
this._onDidAddQueryItem.fire(item);
500+
});
487501
}
488502

489503
async writeQueryHistory(): Promise<void> {
490-
const toSave = this.treeDataProvider.allHistory.filter(q => q.isCompleted());
491-
await splatQueryHistory(toSave, this.queryMetadataStorageLocation);
492-
}
493-
494-
async invokeCallbackOn(queryHistoryItem: QueryHistoryInfo) {
495-
if (this.selectedCallback && queryHistoryItem.isCompleted()) {
496-
const sc = this.selectedCallback;
497-
await sc(queryHistoryItem as CompletedLocalQueryInfo);
498-
}
504+
await splatQueryHistory(this.treeDataProvider.allHistory, this.queryMetadataStorageLocation);
499505
}
500506

501507
async handleOpenQuery(
502508
singleItem: QueryHistoryInfo,
503509
multiSelect: QueryHistoryInfo[]
504510
): Promise<void> {
505-
// TODO will support remote queries
506511
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
507-
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
512+
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
508513
return;
509514
}
510515

516+
const queryPath = finalSingleItem.t === 'local'
517+
? finalSingleItem.initialInfo.queryPath
518+
: finalSingleItem.remoteQuery.queryFilePath;
519+
511520
const textDocument = await workspace.openTextDocument(
512-
Uri.file(finalSingleItem.initialInfo.queryPath)
521+
Uri.file(queryPath)
513522
);
514523
const editor = await window.showTextDocument(
515524
textDocument,
516525
ViewColumn.One
517526
);
518-
const queryText = finalSingleItem.initialInfo.queryText;
519-
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
520-
await editor.edit((edit) =>
521-
edit.replace(
522-
textDocument.validateRange(
523-
new Range(0, 0, textDocument.lineCount, 0)
524-
),
525-
queryText
526-
)
527-
);
527+
528+
if (finalSingleItem.t === 'local') {
529+
const queryText = finalSingleItem.initialInfo.queryText;
530+
if (queryText !== undefined && finalSingleItem.initialInfo.isQuickQuery) {
531+
await editor.edit((edit) =>
532+
edit.replace(
533+
textDocument.validateRange(
534+
new Range(0, 0, textDocument.lineCount, 0)
535+
),
536+
queryText
537+
)
538+
);
539+
}
528540
}
529541
}
530542

531543
async handleRemoveHistoryItem(
532544
singleItem: QueryHistoryInfo,
533-
multiSelect: QueryHistoryInfo[]
545+
multiSelect: QueryHistoryInfo[] = []
534546
) {
535547
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
536548
const toDelete = (finalMultiSelect || [finalSingleItem]);
@@ -548,23 +560,21 @@ export class QueryHistoryManager extends DisposableObject {
548560
} else {
549561
// Remote queries can be removed locally, but not remotely.
550562
// The user must cancel the query on GitHub Actions explicitly.
551-
552563
this.treeDataProvider.remove(item);
553-
await item.deleteQuery();
554564
void logger.log(`Deleted ${item.label}.`);
555565
if (item.status === QueryStatus.InProgress) {
556-
void showAndLogInformationMessage(
557-
'The remote query is still running on GitHub Actions. To cancel there, you must go to the query run in your browser.'
558-
);
566+
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.');
559567
}
568+
569+
this._onDidRemoveQueryItem.fire(item);
560570
}
561571

562572
}));
563573
await this.writeQueryHistory();
564574
const current = this.treeDataProvider.getCurrent();
565575
if (current !== undefined) {
566576
await this.treeView.reveal(current, { select: true });
567-
await this.invokeCallbackOn(current);
577+
await this._onWillOpenQueryItem.fire(current);
568578
}
569579
}
570580

@@ -635,7 +645,7 @@ export class QueryHistoryManager extends DisposableObject {
635645
const from = this.compareWithItem || singleItem;
636646
const to = await this.findOtherQueryToCompare(from, finalMultiSelect);
637647

638-
if (from.isCompleted() && to?.isCompleted()) {
648+
if (from.completed && to?.completed) {
639649
await this.doCompareCallback(from as CompletedLocalQueryInfo, to as CompletedLocalQueryInfo);
640650
}
641651
} catch (e) {
@@ -647,9 +657,8 @@ export class QueryHistoryManager extends DisposableObject {
647657
singleItem: QueryHistoryInfo,
648658
multiSelect: QueryHistoryInfo[]
649659
) {
650-
// TODO will support remote queries
651660
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
652-
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
661+
if (!this.assertSingleQuery(finalMultiSelect)) {
653662
return;
654663
}
655664

@@ -668,7 +677,7 @@ export class QueryHistoryManager extends DisposableObject {
668677
await this.handleOpenQuery(finalSingleItem, [finalSingleItem]);
669678
} else {
670679
// show results on single click
671-
await this.invokeCallbackOn(finalSingleItem);
680+
await this._onWillOpenQueryItem.fire(finalSingleItem);
672681
}
673682
}
674683

@@ -713,17 +722,20 @@ export class QueryHistoryManager extends DisposableObject {
713722
) {
714723
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
715724

716-
// TODO will support remote queries
717-
if (!this.assertSingleQuery(finalMultiSelect) || finalSingleItem?.t !== 'local') {
725+
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
718726
return;
719727
}
720728

721729
const params = new URLSearchParams({
722-
isQuickEval: String(!!finalSingleItem.initialInfo.quickEvalPosition),
730+
isQuickEval: String(!!(finalSingleItem.t === 'local' && finalSingleItem.initialInfo.quickEvalPosition)),
723731
queryText: encodeURIComponent(await this.getQueryText(finalSingleItem)),
724732
});
733+
const queryId = finalSingleItem.t === 'local'
734+
? finalSingleItem.initialInfo.id
735+
: finalSingleItem.queryId;
736+
725737
const uri = Uri.parse(
726-
`codeql:${finalSingleItem.initialInfo.id}?${params.toString()}`, true
738+
`codeql:${queryId}?${params.toString()}`, true
727739
);
728740
const doc = await workspace.openTextDocument(uri);
729741
await window.showTextDocument(doc, { preview: false });
@@ -809,13 +821,15 @@ export class QueryHistoryManager extends DisposableObject {
809821
}
810822

811823
async getQueryText(item: QueryHistoryInfo): Promise<string> {
812-
// TODO the query text for remote queries is not yet available
813-
return item.t === 'local' ? item.initialInfo.queryText : '';
824+
return item.t === 'local'
825+
? item.initialInfo.queryText
826+
: item.remoteQuery.queryText;
814827
}
815828

816829
addQuery(item: QueryHistoryInfo) {
817830
this.treeDataProvider.pushQuery(item);
818831
this.updateTreeViewSelectionIfVisible();
832+
this._onDidAddQueryItem.fire(item);
819833
}
820834

821835
/**
@@ -1011,7 +1025,8 @@ the file in the file explorer and dragging it into the workspace.`
10111025
};
10121026
}
10131027

1014-
refreshTreeView(): void {
1028+
async refreshTreeView(): Promise<void> {
10151029
this.treeDataProvider.refresh();
1030+
await this.writeQueryHistory();
10161031
}
10171032
}

extensions/ql-vscode/src/query-results.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export class LocalQueryInfo {
291291
}
292292
}
293293

294-
isCompleted(): boolean {
294+
get completed(): boolean {
295295
return !!this.completedQuery;
296296
}
297297

extensions/ql-vscode/src/query-serialization.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
3838
q.completedQuery.dispose = () => { /**/ };
3939
}
4040
} else if (q.t === 'remote') {
41-
// TODO Remote queries are not implemented yet.
41+
// noop
4242
}
4343
return q;
4444
});
@@ -47,8 +47,11 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
4747
// most likely another workspace has deleted them because the
4848
// queries aged out.
4949
return asyncFilter(parsedQueries, async (q) => {
50-
if (q.t !== 'local') {
51-
return false;
50+
if (q.t === 'remote') {
51+
// the slurper doesn't know where the remote queries are stored
52+
// so we need to assume here that they exist. Later, we check to
53+
// see if they exist on disk.
54+
return true;
5255
}
5356
const resultsPath = q.completedQuery?.query.resultsPaths.resultsPath;
5457
return !!resultsPath && await fs.pathExists(resultsPath);
@@ -57,6 +60,8 @@ export async function slurpQueryHistory(fsPath: string, config: QueryHistoryConf
5760
void showAndLogErrorMessage('Error loading query history.', {
5861
fullMessage: ['Error loading query history.', e.stack].join('\n'),
5962
});
63+
// since the query history is invalid, it should be deleted so this error does not happen on next startup.
64+
await fs.remove(fsPath);
6065
return [];
6166
}
6267
}
@@ -75,8 +80,8 @@ export async function splatQueryHistory(queries: QueryHistoryInfo[], fsPath: str
7580
if (!(await fs.pathExists(fsPath))) {
7681
await fs.mkdir(path.dirname(fsPath), { recursive: true });
7782
}
78-
// remove incomplete queries since they cannot be recreated on restart
79-
const filteredQueries = queries.filter(q => q.t === 'local' && q.completedQuery !== undefined);
83+
// remove incomplete local queries since they cannot be recreated on restart
84+
const filteredQueries = queries.filter(q => q.t === 'local' ? q.completedQuery !== undefined : true);
8085
const data = JSON.stringify(filteredQueries, null, 2);
8186
await fs.writeFile(fsPath, data);
8287
} catch (e) {

extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ export class RemoteQueriesInterfaceManager {
261261
return this.getPanel().webview.postMessage(msg);
262262
}
263263

264-
private getDuration(startTime: Date, endTime: Date): string {
265-
const diffInMs = startTime.getTime() - endTime.getTime();
264+
private getDuration(startTime: number, endTime: number): string {
265+
const diffInMs = startTime - endTime;
266266
return this.formatDuration(diffInMs);
267267
}
268268

@@ -282,7 +282,8 @@ export class RemoteQueriesInterfaceManager {
282282
}
283283
}
284284

285-
private formatDate = (d: Date): string => {
285+
private formatDate = (millis: number): string => {
286+
const d = new Date(millis);
286287
const datePart = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
287288
const timePart = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', hour12: true });
288289
return `${datePart} at ${timePart}`;

0 commit comments

Comments
 (0)