Skip to content
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
8 changes: 0 additions & 8 deletions extensions/ql-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,6 @@
"command": "codeQL.runRemoteQuery",
"title": "CodeQL: Run Remote Query"
},
{
"command": "codeQL.openRemoteQueriesView",
"title": "CodeQL: Open Remote Queries View"
},
{
"command": "codeQL.runQueries",
"title": "CodeQL: Run Queries in Selected Files"
Expand Down Expand Up @@ -750,10 +746,6 @@
"command": "codeQL.runRemoteQuery",
"when": "config.codeQL.canary && editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.openRemoteQueriesView",
"when": "config.codeQL.canary"
},
{
"command": "codeQL.runQueries",
"when": "false"
Expand Down
27 changes: 17 additions & 10 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ import {
import { CodeQlStatusBarHandler } from './status-bar';

import { Credentials } from './authentication';
import { runRemoteQuery } from './remote-queries/run-remote-query';
import { RemoteQueriesInterfaceManager } from './remote-queries/remote-queries-interface';
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
import { RemoteQuery } from './remote-queries/remote-query';

/**
* extension.ts
Expand Down Expand Up @@ -745,12 +745,8 @@ async function activateWithInstalledDistribution(
)
);

void logger.log('Initializing remote queries panel interface.');
const rmpm = new RemoteQueriesInterfaceManager(
ctx,
logger
);
ctx.subscriptions.push(rmpm);
void logger.log('Initializing remote queries interface.');
const rqm = new RemoteQueriesManager(ctx, logger, cliServer);

// The "runRemoteQuery" command is internal-only.
ctx.subscriptions.push(
Expand All @@ -765,8 +761,11 @@ async function activateWithInstalledDistribution(
step: 0,
message: 'Getting credentials'
});
const credentials = await Credentials.initialize(ctx);
await runRemoteQuery(cliServer, credentials, uri || window.activeTextEditor?.document.uri, false, progress, token);
await rqm.runRemoteQuery(
uri || window.activeTextEditor?.document.uri,
progress,
token
);
} else {
throw new Error('Remote queries require the CodeQL Canary version to run.');
}
Expand All @@ -775,6 +774,14 @@ async function activateWithInstalledDistribution(
cancellable: true
})
);

ctx.subscriptions.push(
commandRunner('codeQL.monitorRemoteQuery', async (
Comment thread
charisk marked this conversation as resolved.
query: RemoteQuery,
token: CancellationToken) => {
await rqm.monitorRemoteQuery(query, token);
}));
Comment thread
charisk marked this conversation as resolved.

ctx.subscriptions.push(
commandRunner(
'codeQL.openReferencedFile',
Expand Down
8 changes: 5 additions & 3 deletions extensions/ql-vscode/src/pure/interface-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as sarif from 'sarif';
import { RemoteQueryResult } from '../remote-queries/shared/remote-query-result';
import { RawResultSet, ResultRow, ResultSetSchema, Column, ResolvableLocationValue } from './bqrs-cli-types';

/**
Expand Down Expand Up @@ -370,14 +371,15 @@ export type FromRemoteQueriesMessage =
| RemoteQueryErrorMessage;

export type ToRemoteQueriesMessage =
| OpenRemoteQueriesViewMessage;
| SetRemoteQueryResultMessage;

export interface RemoteQueryLoadedMessage {
t: 'remoteQueryLoaded';
}

export interface OpenRemoteQueriesViewMessage {
t: 'openRemoteQueriesView';
export interface SetRemoteQueryResultMessage {
t: 'setRemoteQueryResult';
queryResult: RemoteQueryResult
}

export interface RemoteQueryErrorMessage {
Expand Down
104 changes: 93 additions & 11 deletions extensions/ql-vscode/src/remote-queries/remote-queries-interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DisposableObject } from '../pure/disposable-object';
import {
WebviewPanel,
ExtensionContext,
Expand All @@ -16,10 +15,12 @@ import {
import { Logger } from '../logging';
import { getHtmlForWebview } from '../interface-utils';
import { assertNever } from '../pure/helpers-pure';
import { commandRunner } from '../commandRunner';
import { AnalysisResult, RemoteQueryResult } from './remote-query-result';
import { RemoteQuery } from './remote-query';
import { RemoteQueryResult as RemoteQueryResultViewModel } from './shared/remote-query-result';
import { AnalysisResult as AnalysisResultViewModel } from './shared/remote-query-result';


export class RemoteQueriesInterfaceManager extends DisposableObject {
export class RemoteQueriesInterfaceManager {
private panel: WebviewPanel | undefined;
private panelLoaded = false;
private panelLoadedCallBacks: (() => void)[] = [];
Expand All @@ -28,22 +29,49 @@ export class RemoteQueriesInterfaceManager extends DisposableObject {
private ctx: ExtensionContext,
private logger: Logger,
) {
super();
commandRunner('codeQL.openRemoteQueriesView', () => this.handleOpenRemoteQueriesView());
this.panelLoadedCallBacks.push(() => {
void logger.log('Remote queries view loaded');
});
}

async showResults() {
async showResults(query: RemoteQuery, queryResult: RemoteQueryResult) {
this.getPanel().reveal(undefined, true);

await this.waitForPanelLoaded();
await this.postMessage({
t: 'openRemoteQueriesView',
t: 'setRemoteQueryResult',
queryResult: this.buildViewModel(query, queryResult)
});
}

/**
* Builds up a model tailored to the view based on the query and result domain entities.
* The data is cleaned up, sorted where necessary, and transformed to a format that
* the view model can use.
* @param query Information about the query that was run.
* @param queryResult The result of the query.
* @returns A fully created view model.
*/
private buildViewModel(query: RemoteQuery, queryResult: RemoteQueryResult): RemoteQueryResultViewModel {
const queryFile = path.basename(query.queryFilePath);
const totalResultCount = queryResult.analysisResults.reduce((acc, cur) => acc + cur.resultCount, 0);
const executionDuration = this.getDuration(queryResult.executionEndTime, query.executionStartTime);
const analysisResults = this.buildAnalysisResults(queryResult.analysisResults);
const affectedRepositories = queryResult.analysisResults.filter(r => r.resultCount > 0);

return {
queryTitle: query.queryName,
queryFile: queryFile,
totalRepositoryCount: query.repositories.length,
affectedRepositoryCount: affectedRepositories.length,
totalResultCount: totalResultCount,
executionTimestamp: this.formatDate(query.executionStartTime),
executionDuration: executionDuration,
downloadLink: queryResult.allResultsDownloadUri,
results: analysisResults
};
}

getPanel(): WebviewPanel {
if (this.panel == undefined) {
const { ctx } = this;
Expand Down Expand Up @@ -124,11 +152,65 @@ export class RemoteQueriesInterfaceManager extends DisposableObject {
return this.getPanel().webview.postMessage(msg);
}

async handleOpenRemoteQueriesView() {
this.getPanel().reveal(undefined, true);
private getDuration(startTime: Date, endTime: Date): string {
const diffInMs = startTime.getTime() - endTime.getTime();
return this.formatDuration(diffInMs);
}

await this.waitForPanelLoaded();
private formatDuration(ms: number): string {
const seconds = ms / 1000;
const minutes = seconds / 60;
const hours = minutes / 60;
const days = hours / 24;
if (days > 1) {
return `${days.toFixed(2)} days`;
} else if (hours > 1) {
return `${hours.toFixed(2)} hours`;
} else if (minutes > 1) {
return `${minutes.toFixed(2)} minutes`;
} else {
return `${seconds.toFixed(2)} seconds`;
}
}

private formatDate = (d: Date): string => {
const datePart = d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
const timePart = d.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', hour12: true });
return `${datePart} at ${timePart}`;
};

private formatFileSize(bytes: number): string {
const kb = bytes / 1024;
const mb = kb / 1024;
const gb = mb / 1024;

if (bytes < 1024) {
return `${bytes} bytes`;
} else if (kb < 1024) {
return `${kb.toFixed(2)} KB`;
} else if (mb < 1024) {
return `${mb.toFixed(2)} MB`;
} else {
return `${gb.toFixed(2)} GB`;
}
}

/**
* Builds up a list of analysis results, in a data structure tailored to the view.
* @param analysisResults The results of a specific analysis.
* @returns A fully created view model.
*/
private buildAnalysisResults(analysisResults: AnalysisResult[]): AnalysisResultViewModel[] {
const filteredAnalysisResults = analysisResults.filter(r => r.resultCount > 0);

const sortedAnalysisResults = filteredAnalysisResults.sort((a, b) => b.resultCount - a.resultCount);

return sortedAnalysisResults.map((analysisResult) => ({
nwo: analysisResult.nwo,
resultCount: analysisResult.resultCount,
downloadLink: analysisResult.downloadUri,
fileSize: this.formatFileSize(analysisResult.fileSizeInBytes)
}));
}
}

100 changes: 100 additions & 0 deletions extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { CancellationToken, commands, ExtensionContext, Uri, window } from 'vscode';
import { Credentials } from '../authentication';
import { CodeQLCliServer } from '../cli';
import { ProgressCallback } from '../commandRunner';
import { showAndLogErrorMessage, showInformationMessageWithAction } from '../helpers';
import { Logger } from '../logging';
import { getResultIndex, ResultIndexItem, runRemoteQuery } from './run-remote-query';
import { RemoteQueriesInterfaceManager } from './remote-queries-interface';
import { RemoteQuery } from './remote-query';
import { RemoteQueriesMonitor } from './remote-queries-monitor';
import { RemoteQueryResult } from './remote-query-result';

export class RemoteQueriesManager {
private readonly remoteQueriesMonitor: RemoteQueriesMonitor;

constructor(
private readonly ctx: ExtensionContext,
private readonly logger: Logger,
private readonly cliServer: CodeQLCliServer
) {
this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger);
}

public async runRemoteQuery(
uri: Uri | undefined,
progress: ProgressCallback,
token: CancellationToken
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);

const querySubmission = await runRemoteQuery(
this.cliServer,
credentials, uri || window.activeTextEditor?.document.uri,
false,
progress,
token);

if (querySubmission && querySubmission.query) {
void commands.executeCommand('codeQL.monitorRemoteQuery', querySubmission.query);
}
}

public async monitorRemoteQuery(
Comment thread
charisk marked this conversation as resolved.
query: RemoteQuery,
cancellationToken: CancellationToken
): Promise<void> {
const credentials = await Credentials.initialize(this.ctx);

const queryResult = await this.remoteQueriesMonitor.monitorQuery(query, cancellationToken);

const executionEndTime = new Date();

if (queryResult.status === 'CompletedSuccessfully') {
const resultIndexItems = await this.downloadResultIndex(credentials, query);

const totalResultCount = resultIndexItems.reduce((acc, cur) => acc + cur.results_count, 0);
const message = `Query "${query.queryName}" run on ${query.repositories.length} repositories and returned ${totalResultCount} results`;

const shouldOpenView = await showInformationMessageWithAction(message, 'View');
if (shouldOpenView) {
const queryResult = this.mapQueryResult(executionEndTime, resultIndexItems);
const rqim = new RemoteQueriesInterfaceManager(this.ctx, this.logger);
await rqim.showResults(query, queryResult);
}
} else if (queryResult.status === 'CompletedUnsuccessfully') {
await showAndLogErrorMessage(`Remote query execution failed. Error: ${queryResult.error}`);
return;
} else if (queryResult.status === 'Cancelled') {
await showAndLogErrorMessage('Remote query monitoring was cancelled');
}
}

private async downloadResultIndex(credentials: Credentials, query: RemoteQuery) {
return await getResultIndex(
credentials,
query.controllerRepository.owner,
query.controllerRepository.name,
query.actionsWorkflowRunId);
}

private mapQueryResult(executionEndTime: Date, resultindexItems: ResultIndexItem[]): RemoteQueryResult {
// Example URIs are used for now, but a solution for downloading the results will soon be implemented.
const allResultsDownloadUri = 'www.example.com';
const analysisDownloadUri = 'www.example.com';

const analysisResults = resultindexItems.map(ri => ({
nwo: ri.nwo,
resultCount: ri.results_count,
downloadUri: analysisDownloadUri,
fileSizeInBytes: ri.sarif_file_size || ri.bqrs_file_size,
})
);

return {
executionEndTime,
analysisResults,
allResultsDownloadUri,
};
}
}
Loading