934 lines
29 KiB
TypeScript
934 lines
29 KiB
TypeScript
import { join } from "path";
|
|
|
|
import {
|
|
submitVariantAnalysis,
|
|
getVariantAnalysisRepo,
|
|
} from "./gh-api/gh-api-client";
|
|
import type { VariantAnalysis as ApiVariantAnalysis } from "./gh-api/variant-analysis";
|
|
import type {
|
|
AuthenticationSessionsChangeEvent,
|
|
CancellationToken,
|
|
} from "vscode";
|
|
import {
|
|
authentication,
|
|
env,
|
|
EventEmitter,
|
|
Uri,
|
|
ViewColumn,
|
|
window as Window,
|
|
workspace,
|
|
} from "vscode";
|
|
import { DisposableObject } from "../common/disposable-object";
|
|
import { VariantAnalysisMonitor } from "./variant-analysis-monitor";
|
|
import type {
|
|
VariantAnalysis,
|
|
VariantAnalysisQueries,
|
|
VariantAnalysisRepositoryTask,
|
|
VariantAnalysisScannedRepository,
|
|
VariantAnalysisScannedRepositoryResult,
|
|
VariantAnalysisScannedRepositoryState,
|
|
VariantAnalysisSubmission,
|
|
} from "./shared/variant-analysis";
|
|
import {
|
|
getActionsWorkflowRunUrl,
|
|
isVariantAnalysisComplete,
|
|
parseVariantAnalysisQueryLanguage,
|
|
VariantAnalysisScannedRepositoryDownloadStatus,
|
|
} from "./shared/variant-analysis";
|
|
import { getErrorMessage } from "../common/helpers-pure";
|
|
import { VariantAnalysisView } from "./variant-analysis-view";
|
|
import type { VariantAnalysisViewManager } from "./variant-analysis-view-manager";
|
|
import type {
|
|
LoadResultsOptions,
|
|
VariantAnalysisResultsManager,
|
|
} from "./variant-analysis-results-manager";
|
|
import { getQueryName, prepareRemoteQueryRun } from "./run-remote-query";
|
|
import {
|
|
mapVariantAnalysis,
|
|
mapVariantAnalysisRepositoryTask,
|
|
} from "./variant-analysis-mapper";
|
|
import PQueue from "p-queue";
|
|
import { createTimestampFile, saveBeforeStart } from "../run-queries-shared";
|
|
import { readFile, remove, pathExists } from "fs-extra";
|
|
import { EOL } from "os";
|
|
import { cancelVariantAnalysis } from "./gh-api/gh-actions-api-client";
|
|
import type { ProgressCallback } from "../common/vscode/progress";
|
|
import {
|
|
UserCancellationException,
|
|
withProgress,
|
|
} from "../common/vscode/progress";
|
|
import type { CodeQLCliServer } from "../codeql-cli/cli";
|
|
import type { RepositoriesFilterSortStateWithIds } from "./shared/variant-analysis-filter-sort";
|
|
import {
|
|
defaultFilterSortState,
|
|
filterAndSortRepositoriesWithResults,
|
|
} from "./shared/variant-analysis-filter-sort";
|
|
import { URLSearchParams } from "url";
|
|
import type { DbManager } from "../databases/db-manager";
|
|
import type { App } from "../common/app";
|
|
import { redactableError } from "../common/errors";
|
|
import type {
|
|
AppCommandManager,
|
|
VariantAnalysisCommands,
|
|
} from "../common/commands";
|
|
import { exportVariantAnalysisResults } from "./export-results";
|
|
import {
|
|
readRepoStates,
|
|
REPO_STATES_FILENAME,
|
|
writeRepoStates,
|
|
} from "./repo-states-store";
|
|
import { GITHUB_AUTH_PROVIDER_ID } from "../common/vscode/authentication";
|
|
import { FetchError } from "node-fetch";
|
|
import {
|
|
showAndLogExceptionWithTelemetry,
|
|
showAndLogInformationMessage,
|
|
showAndLogWarningMessage,
|
|
} from "../common/logging";
|
|
import type { QueryTreeViewItem } from "../queries-panel/query-tree-view-item";
|
|
import { RequestError } from "@octokit/request-error";
|
|
import { handleRequestError } from "./custom-errors";
|
|
import { createMultiSelectionCommand } from "../common/vscode/selection-commands";
|
|
import { askForLanguage, findLanguage } from "../codeql-cli/query-language";
|
|
import type { QlPackDetails } from "./ql-pack-details";
|
|
import { getQlPackFilePath } from "../common/ql";
|
|
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
|
|
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
|
|
import { findVariantAnalysisQlPackRoot } from "./ql";
|
|
import { resolveCodeScanningQueryPack } from "./code-scanning-pack";
|
|
|
|
const maxRetryCount = 3;
|
|
|
|
export class VariantAnalysisManager
|
|
extends DisposableObject
|
|
implements VariantAnalysisViewManager<VariantAnalysisView>
|
|
{
|
|
private static readonly DOWNLOAD_PERCENTAGE_UPDATE_DELAY_MS = 500;
|
|
|
|
private readonly _onVariantAnalysisAdded = this.push(
|
|
new EventEmitter<VariantAnalysis>(),
|
|
);
|
|
public readonly onVariantAnalysisAdded = this._onVariantAnalysisAdded.event;
|
|
private readonly _onVariantAnalysisStatusUpdated = this.push(
|
|
new EventEmitter<VariantAnalysis>(),
|
|
);
|
|
public readonly onVariantAnalysisStatusUpdated =
|
|
this._onVariantAnalysisStatusUpdated.event;
|
|
|
|
private readonly _onVariantAnalysisRemoved = this.push(
|
|
new EventEmitter<VariantAnalysis>(),
|
|
);
|
|
public readonly onVariantAnalysisRemoved =
|
|
this._onVariantAnalysisRemoved.event;
|
|
|
|
private readonly variantAnalysisMonitor: VariantAnalysisMonitor;
|
|
private readonly variantAnalyses = new Map<number, VariantAnalysis>();
|
|
private readonly views = new Map<number, VariantAnalysisView>();
|
|
private static readonly maxConcurrentDownloads = 3;
|
|
private readonly queue = new PQueue({
|
|
concurrency: VariantAnalysisManager.maxConcurrentDownloads,
|
|
});
|
|
|
|
private readonly repoStates = new Map<
|
|
number,
|
|
Record<number, VariantAnalysisScannedRepositoryState>
|
|
>();
|
|
|
|
constructor(
|
|
private readonly app: App,
|
|
private readonly cliServer: CodeQLCliServer,
|
|
private readonly storagePath: string,
|
|
private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager,
|
|
private readonly dbManager: DbManager,
|
|
) {
|
|
super();
|
|
this.variantAnalysisMonitor = this.push(
|
|
new VariantAnalysisMonitor(
|
|
app,
|
|
this.shouldCancelMonitorVariantAnalysis.bind(this),
|
|
),
|
|
);
|
|
this.variantAnalysisMonitor.onVariantAnalysisChange(
|
|
this.onVariantAnalysisUpdated.bind(this),
|
|
);
|
|
|
|
this.variantAnalysisResultsManager = variantAnalysisResultsManager;
|
|
this.variantAnalysisResultsManager.onResultLoaded(
|
|
this.onRepoResultLoaded.bind(this),
|
|
);
|
|
|
|
this.push(
|
|
authentication.onDidChangeSessions(this.onDidChangeSessions.bind(this)),
|
|
);
|
|
}
|
|
|
|
getCommands(): VariantAnalysisCommands {
|
|
return {
|
|
"codeQL.autoDownloadVariantAnalysisResult":
|
|
this.enqueueDownload.bind(this),
|
|
"codeQL.loadVariantAnalysisRepoResults": this.loadResults.bind(this),
|
|
"codeQL.monitorNewVariantAnalysis":
|
|
this.monitorVariantAnalysis.bind(this),
|
|
"codeQL.monitorRehydratedVariantAnalysis":
|
|
this.monitorVariantAnalysis.bind(this),
|
|
"codeQL.monitorReauthenticatedVariantAnalysis":
|
|
this.monitorVariantAnalysis.bind(this),
|
|
"codeQL.openVariantAnalysisLogs": this.openVariantAnalysisLogs.bind(this),
|
|
"codeQL.openVariantAnalysisView": this.showView.bind(this),
|
|
"codeQL.runVariantAnalysis":
|
|
this.runVariantAnalysisFromCommandPalette.bind(this),
|
|
"codeQL.runVariantAnalysisContextEditor":
|
|
this.runVariantAnalysisFromContextEditor.bind(this),
|
|
"codeQL.runVariantAnalysisContextExplorer": createMultiSelectionCommand(
|
|
this.runVariantAnalysisFromExplorer.bind(this),
|
|
),
|
|
"codeQLQueries.runVariantAnalysisContextMenu":
|
|
this.runVariantAnalysisFromQueriesPanel.bind(this),
|
|
"codeQL.runVariantAnalysisPublishedPack":
|
|
this.runVariantAnalysisFromPublishedPack.bind(this),
|
|
};
|
|
}
|
|
|
|
get commandManager(): AppCommandManager {
|
|
return this.app.commands;
|
|
}
|
|
|
|
private async runVariantAnalysisFromCommandPalette() {
|
|
const fileUri = Window.activeTextEditor?.document.uri;
|
|
if (!fileUri) {
|
|
throw new Error("Please select a .ql file to run as a variant analysis");
|
|
}
|
|
|
|
await this.runVariantAnalysisCommand([fileUri]);
|
|
}
|
|
|
|
private async runVariantAnalysisFromContextEditor(uri: Uri) {
|
|
await this.runVariantAnalysisCommand([uri]);
|
|
}
|
|
|
|
private async runVariantAnalysisFromExplorer(fileURIs: Uri[]): Promise<void> {
|
|
return this.runVariantAnalysisCommand(fileURIs);
|
|
}
|
|
|
|
private async runVariantAnalysisFromQueriesPanel(
|
|
queryTreeViewItem: QueryTreeViewItem,
|
|
): Promise<void> {
|
|
if (queryTreeViewItem.path !== undefined) {
|
|
await this.runVariantAnalysisCommand([Uri.file(queryTreeViewItem.path)]);
|
|
}
|
|
}
|
|
|
|
public async runVariantAnalysisFromPublishedPack(): Promise<void> {
|
|
return withProgress(async (progress, token) => {
|
|
progress({
|
|
maxStep: 7,
|
|
step: 0,
|
|
message: "Determining query language",
|
|
});
|
|
|
|
const language = await askForLanguage(this.cliServer);
|
|
if (!language) {
|
|
return;
|
|
}
|
|
|
|
progress({
|
|
maxStep: 7,
|
|
step: 2,
|
|
message: "Downloading query pack and resolving queries",
|
|
});
|
|
|
|
// Build up details to pass to the functions that run the variant analysis.
|
|
const qlPackDetails = await resolveCodeScanningQueryPack(
|
|
this.app.logger,
|
|
this.cliServer,
|
|
language,
|
|
);
|
|
|
|
await this.runVariantAnalysis(
|
|
qlPackDetails,
|
|
(p) =>
|
|
progress({
|
|
...p,
|
|
maxStep: p.maxStep + 3,
|
|
step: p.step + 3,
|
|
}),
|
|
token,
|
|
);
|
|
});
|
|
}
|
|
|
|
private async runVariantAnalysisCommand(queryFiles: Uri[]): Promise<void> {
|
|
if (queryFiles.length === 0) {
|
|
throw new Error("Please select a .ql file to run as a variant analysis");
|
|
}
|
|
|
|
const qlPackRootPath = await findVariantAnalysisQlPackRoot(
|
|
queryFiles.map((f) => f.fsPath),
|
|
getOnDiskWorkspaceFolders(),
|
|
);
|
|
const qlPackFilePath = await getQlPackFilePath(qlPackRootPath);
|
|
|
|
// Open popup to ask for language if not already hardcoded
|
|
const language = qlPackFilePath
|
|
? await findLanguage(this.cliServer, queryFiles[0])
|
|
: await askForLanguage(this.cliServer);
|
|
|
|
if (!language) {
|
|
throw new UserCancellationException("Could not determine query language");
|
|
}
|
|
|
|
const qlPackDetails: QlPackDetails = {
|
|
queryFiles: queryFiles.map((uri) => uri.fsPath),
|
|
qlPackRootPath,
|
|
qlPackFilePath,
|
|
language,
|
|
};
|
|
|
|
return withProgress(
|
|
async (progress, token) => {
|
|
await this.runVariantAnalysis(qlPackDetails, progress, token);
|
|
},
|
|
{
|
|
title: "Run Variant Analysis",
|
|
cancellable: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async runVariantAnalysis(
|
|
qlPackDetails: QlPackDetails,
|
|
progress: ProgressCallback,
|
|
token: CancellationToken,
|
|
): Promise<void> {
|
|
await saveBeforeStart();
|
|
|
|
progress({
|
|
maxStep: 5,
|
|
step: 0,
|
|
message: "Getting credentials",
|
|
});
|
|
|
|
const {
|
|
actionBranch,
|
|
base64Pack,
|
|
repoSelection,
|
|
controllerRepo,
|
|
queryStartTime,
|
|
} = await prepareRemoteQueryRun(
|
|
this.cliServer,
|
|
this.app.credentials,
|
|
qlPackDetails,
|
|
progress,
|
|
token,
|
|
this.dbManager,
|
|
);
|
|
|
|
// For now we get the metadata for the first query in the pack.
|
|
// and use that in the submission and query history. In the future
|
|
// we'll need to consider how to handle having multiple queries.
|
|
const firstQueryFile = qlPackDetails.queryFiles[0];
|
|
const queryMetadata = await tryGetQueryMetadata(
|
|
this.cliServer,
|
|
firstQueryFile,
|
|
);
|
|
const queryName = getQueryName(queryMetadata, firstQueryFile);
|
|
const variantAnalysisLanguage = parseVariantAnalysisQueryLanguage(
|
|
qlPackDetails.language,
|
|
);
|
|
if (variantAnalysisLanguage === undefined) {
|
|
throw new UserCancellationException(
|
|
`Found unsupported language: ${qlPackDetails.language}`,
|
|
);
|
|
}
|
|
|
|
const queryText = await readFile(firstQueryFile, "utf8");
|
|
|
|
const queries: VariantAnalysisQueries | undefined =
|
|
qlPackDetails.queryFiles.length === 1
|
|
? undefined
|
|
: {
|
|
language: qlPackDetails.language,
|
|
count: qlPackDetails.queryFiles.length,
|
|
};
|
|
|
|
const variantAnalysisSubmission: VariantAnalysisSubmission = {
|
|
startTime: queryStartTime,
|
|
actionRepoRef: actionBranch,
|
|
controllerRepoId: controllerRepo.id,
|
|
language: variantAnalysisLanguage,
|
|
pack: base64Pack,
|
|
query: {
|
|
name: queryName,
|
|
filePath: firstQueryFile,
|
|
text: queryText,
|
|
kind: queryMetadata?.kind,
|
|
},
|
|
queries,
|
|
databases: {
|
|
repositories: repoSelection.repositories,
|
|
repositoryLists: repoSelection.repositoryLists,
|
|
repositoryOwners: repoSelection.owners,
|
|
},
|
|
};
|
|
|
|
let variantAnalysisResponse: ApiVariantAnalysis;
|
|
try {
|
|
variantAnalysisResponse = await submitVariantAnalysis(
|
|
this.app.credentials,
|
|
variantAnalysisSubmission,
|
|
);
|
|
} catch (e: unknown) {
|
|
// If the error is handled by the handleRequestError function, we don't need to throw
|
|
if (e instanceof RequestError && handleRequestError(e, this.app.logger)) {
|
|
return;
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
|
|
const processedVariantAnalysis = mapVariantAnalysis(
|
|
variantAnalysisSubmission,
|
|
variantAnalysisResponse,
|
|
);
|
|
|
|
await this.onVariantAnalysisSubmitted(processedVariantAnalysis);
|
|
|
|
void showAndLogInformationMessage(
|
|
this.app.logger,
|
|
`Variant analysis ${processedVariantAnalysis.query.name} submitted for processing`,
|
|
);
|
|
|
|
void this.app.commands.execute(
|
|
"codeQL.openVariantAnalysisView",
|
|
processedVariantAnalysis.id,
|
|
);
|
|
void this.app.commands.execute(
|
|
"codeQL.monitorNewVariantAnalysis",
|
|
processedVariantAnalysis,
|
|
);
|
|
}
|
|
|
|
public async rehydrateVariantAnalysis(variantAnalysis: VariantAnalysis) {
|
|
if (!(await this.variantAnalysisRecordExists(variantAnalysis.id))) {
|
|
// In this case, the variant analysis was deleted from disk, most likely because
|
|
// it was purged by another workspace.
|
|
this._onVariantAnalysisRemoved.fire(variantAnalysis);
|
|
} else {
|
|
await this.setVariantAnalysis(variantAnalysis);
|
|
|
|
const repoStatesFromDisk = await readRepoStates(
|
|
this.getRepoStatesStoragePath(variantAnalysis.id),
|
|
);
|
|
|
|
this.repoStates.set(variantAnalysis.id, repoStatesFromDisk || {});
|
|
|
|
if (
|
|
!(await isVariantAnalysisComplete(
|
|
variantAnalysis,
|
|
this.makeResultDownloadChecker(variantAnalysis),
|
|
))
|
|
) {
|
|
void this.app.commands.execute(
|
|
"codeQL.monitorRehydratedVariantAnalysis",
|
|
variantAnalysis,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private makeResultDownloadChecker(
|
|
variantAnalysis: VariantAnalysis,
|
|
): (repo: VariantAnalysisScannedRepository) => Promise<boolean> {
|
|
const storageLocation = this.getVariantAnalysisStorageLocation(
|
|
variantAnalysis.id,
|
|
);
|
|
return (repo) =>
|
|
this.variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(
|
|
storageLocation,
|
|
repo.repository.fullName,
|
|
);
|
|
}
|
|
|
|
public async removeVariantAnalysis(variantAnalysis: VariantAnalysis) {
|
|
this.variantAnalysisResultsManager.removeAnalysisResults(variantAnalysis);
|
|
await this.removeStorageDirectory(variantAnalysis.id);
|
|
this.variantAnalyses.delete(variantAnalysis.id);
|
|
|
|
// This will automatically unregister the view
|
|
this.views.get(variantAnalysis.id)?.dispose();
|
|
}
|
|
|
|
private async removeStorageDirectory(variantAnalysisId: number) {
|
|
const storageLocation =
|
|
this.getVariantAnalysisStorageLocation(variantAnalysisId);
|
|
await remove(storageLocation);
|
|
}
|
|
|
|
public async showView(variantAnalysisId: number): Promise<void> {
|
|
if (!this.variantAnalyses.get(variantAnalysisId)) {
|
|
void showAndLogExceptionWithTelemetry(
|
|
this.app.logger,
|
|
this.app.telemetry,
|
|
redactableError`No variant analysis found with id: ${variantAnalysisId}.`,
|
|
);
|
|
}
|
|
if (!this.views.has(variantAnalysisId)) {
|
|
// The view will register itself with the manager, so we don't need to do anything here.
|
|
this.track(new VariantAnalysisView(this.app, variantAnalysisId, this));
|
|
}
|
|
|
|
const variantAnalysisView = this.views.get(variantAnalysisId)!;
|
|
await variantAnalysisView.openView();
|
|
return;
|
|
}
|
|
|
|
public async openQueryText(variantAnalysisId: number): Promise<void> {
|
|
const variantAnalysis = await this.getVariantAnalysis(variantAnalysisId);
|
|
if (!variantAnalysis) {
|
|
void showAndLogWarningMessage(
|
|
this.app.logger,
|
|
"Could not open variant analysis query text. Variant analysis not found.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const filename = variantAnalysis.query.filePath;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
variantAnalysisId: variantAnalysis.id.toString(),
|
|
});
|
|
const uri = Uri.from({
|
|
scheme: "codeql-variant-analysis",
|
|
path: filename,
|
|
query: params.toString(),
|
|
});
|
|
const doc = await workspace.openTextDocument(uri);
|
|
await Window.showTextDocument(doc, { preview: false });
|
|
} catch (error) {
|
|
void showAndLogWarningMessage(
|
|
this.app.logger,
|
|
"Could not open variant analysis query text. Failed to open text document.",
|
|
);
|
|
}
|
|
}
|
|
|
|
public async openQueryFile(variantAnalysisId: number): Promise<void> {
|
|
const variantAnalysis = await this.getVariantAnalysis(variantAnalysisId);
|
|
|
|
if (!variantAnalysis) {
|
|
void showAndLogWarningMessage(
|
|
this.app.logger,
|
|
"Could not open variant analysis query file",
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const textDocument = await workspace.openTextDocument(
|
|
variantAnalysis.query.filePath,
|
|
);
|
|
await Window.showTextDocument(textDocument, ViewColumn.One);
|
|
} catch (error) {
|
|
void showAndLogWarningMessage(
|
|
this.app.logger,
|
|
`Could not open file: ${variantAnalysis.query.filePath}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
public registerView(view: VariantAnalysisView): void {
|
|
if (this.views.has(view.variantAnalysisId)) {
|
|
throw new Error(
|
|
`View for variant analysis with id: ${view.variantAnalysisId} already exists`,
|
|
);
|
|
}
|
|
|
|
this.views.set(view.variantAnalysisId, view);
|
|
}
|
|
|
|
public unregisterView(view: VariantAnalysisView): void {
|
|
this.views.delete(view.variantAnalysisId);
|
|
this.disposeAndStopTracking(view);
|
|
}
|
|
|
|
public getView(variantAnalysisId: number): VariantAnalysisView | undefined {
|
|
return this.views.get(variantAnalysisId);
|
|
}
|
|
|
|
public async getVariantAnalysis(
|
|
variantAnalysisId: number,
|
|
): Promise<VariantAnalysis | undefined> {
|
|
return this.variantAnalyses.get(variantAnalysisId);
|
|
}
|
|
|
|
public async getRepoStates(
|
|
variantAnalysisId: number,
|
|
): Promise<VariantAnalysisScannedRepositoryState[]> {
|
|
return Object.values(this.repoStates.get(variantAnalysisId) ?? {});
|
|
}
|
|
|
|
public get variantAnalysesSize(): number {
|
|
return this.variantAnalyses.size;
|
|
}
|
|
|
|
public async loadResults(
|
|
variantAnalysisId: number,
|
|
repositoryFullName: string,
|
|
options?: LoadResultsOptions,
|
|
): Promise<VariantAnalysisScannedRepositoryResult> {
|
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
|
if (!variantAnalysis) {
|
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
|
}
|
|
|
|
return this.variantAnalysisResultsManager.loadResults(
|
|
variantAnalysisId,
|
|
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
|
repositoryFullName,
|
|
options,
|
|
);
|
|
}
|
|
|
|
private async variantAnalysisRecordExists(
|
|
variantAnalysisId: number,
|
|
): Promise<boolean> {
|
|
const filePath = this.getVariantAnalysisStorageLocation(variantAnalysisId);
|
|
return await pathExists(filePath);
|
|
}
|
|
|
|
private async shouldCancelMonitorVariantAnalysis(
|
|
variantAnalysisId: number,
|
|
): Promise<boolean> {
|
|
return !this.variantAnalyses.has(variantAnalysisId);
|
|
}
|
|
|
|
public async onVariantAnalysisUpdated(
|
|
variantAnalysis: VariantAnalysis | undefined,
|
|
): Promise<void> {
|
|
if (!variantAnalysis) {
|
|
return;
|
|
}
|
|
|
|
if (!this.variantAnalyses.has(variantAnalysis.id)) {
|
|
return;
|
|
}
|
|
|
|
await this.setVariantAnalysis(variantAnalysis);
|
|
this._onVariantAnalysisStatusUpdated.fire(variantAnalysis);
|
|
}
|
|
|
|
private async onVariantAnalysisSubmitted(
|
|
variantAnalysis: VariantAnalysis,
|
|
): Promise<void> {
|
|
await this.setVariantAnalysis(variantAnalysis);
|
|
|
|
await this.prepareStorageDirectory(variantAnalysis.id);
|
|
|
|
this.repoStates.set(variantAnalysis.id, {});
|
|
|
|
this._onVariantAnalysisAdded.fire(variantAnalysis);
|
|
}
|
|
|
|
private async setVariantAnalysis(
|
|
variantAnalysis: VariantAnalysis,
|
|
): Promise<void> {
|
|
this.variantAnalyses.set(variantAnalysis.id, variantAnalysis);
|
|
await this.getView(variantAnalysis.id)?.updateView(variantAnalysis);
|
|
}
|
|
|
|
private async onRepoResultLoaded(
|
|
repositoryResult: VariantAnalysisScannedRepositoryResult,
|
|
): Promise<void> {
|
|
await this.getView(
|
|
repositoryResult.variantAnalysisId,
|
|
)?.sendRepositoryResults([repositoryResult]);
|
|
}
|
|
|
|
private async onRepoStateUpdated(
|
|
variantAnalysisId: number,
|
|
repoState: VariantAnalysisScannedRepositoryState,
|
|
): Promise<void> {
|
|
await this.getView(variantAnalysisId)?.updateRepoState(repoState);
|
|
|
|
let repoStates = this.repoStates.get(variantAnalysisId);
|
|
if (!repoStates) {
|
|
repoStates = {};
|
|
this.repoStates.set(variantAnalysisId, repoStates);
|
|
}
|
|
|
|
repoStates[repoState.repositoryId] = repoState;
|
|
}
|
|
|
|
private async onDidChangeSessions(
|
|
event: AuthenticationSessionsChangeEvent,
|
|
): Promise<void> {
|
|
if (event.provider.id !== GITHUB_AUTH_PROVIDER_ID) {
|
|
return;
|
|
}
|
|
|
|
for (const variantAnalysis of this.variantAnalyses.values()) {
|
|
if (
|
|
this.variantAnalysisMonitor.isMonitoringVariantAnalysis(
|
|
variantAnalysis.id,
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
await isVariantAnalysisComplete(
|
|
variantAnalysis,
|
|
this.makeResultDownloadChecker(variantAnalysis),
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
void this.app.commands.execute(
|
|
"codeQL.monitorReauthenticatedVariantAnalysis",
|
|
variantAnalysis,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async monitorVariantAnalysis(
|
|
variantAnalysis: VariantAnalysis,
|
|
): Promise<void> {
|
|
await this.variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis);
|
|
}
|
|
|
|
public async autoDownloadVariantAnalysisResult(
|
|
scannedRepo: VariantAnalysisScannedRepository,
|
|
variantAnalysis: VariantAnalysis,
|
|
): Promise<void> {
|
|
if (
|
|
this.repoStates.get(variantAnalysis.id)?.[scannedRepo.repository.id]
|
|
?.downloadStatus ===
|
|
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const repoState = {
|
|
repositoryId: scannedRepo.repository.id,
|
|
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Pending,
|
|
};
|
|
|
|
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
|
|
|
let repoTask: VariantAnalysisRepositoryTask;
|
|
try {
|
|
const repoTaskResponse = await getVariantAnalysisRepo(
|
|
this.app.credentials,
|
|
variantAnalysis.controllerRepo.id,
|
|
variantAnalysis.id,
|
|
scannedRepo.repository.id,
|
|
);
|
|
|
|
repoTask = mapVariantAnalysisRepositoryTask(repoTaskResponse);
|
|
} catch (e) {
|
|
repoState.downloadStatus =
|
|
VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
|
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
|
throw new Error(
|
|
`Could not download the results for variant analysis with id: ${
|
|
variantAnalysis.id
|
|
}. Error: ${getErrorMessage(e)}`,
|
|
);
|
|
}
|
|
|
|
if (repoTask.artifactUrl) {
|
|
repoState.downloadStatus =
|
|
VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
|
|
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
|
|
|
try {
|
|
let lastRepoStateUpdate = 0;
|
|
const updateRepoStateCallback = async (downloadPercentage: number) => {
|
|
const now = new Date().getTime();
|
|
if (
|
|
lastRepoStateUpdate <
|
|
now - VariantAnalysisManager.DOWNLOAD_PERCENTAGE_UPDATE_DELAY_MS
|
|
) {
|
|
lastRepoStateUpdate = now;
|
|
await this.onRepoStateUpdated(variantAnalysis.id, {
|
|
repositoryId: scannedRepo.repository.id,
|
|
downloadStatus:
|
|
VariantAnalysisScannedRepositoryDownloadStatus.InProgress,
|
|
downloadPercentage,
|
|
});
|
|
}
|
|
};
|
|
let retry = 0;
|
|
for (;;) {
|
|
try {
|
|
await this.variantAnalysisResultsManager.download(
|
|
variantAnalysis.id,
|
|
repoTask,
|
|
this.getVariantAnalysisStorageLocation(variantAnalysis.id),
|
|
updateRepoStateCallback,
|
|
);
|
|
break;
|
|
} catch (e) {
|
|
if (
|
|
retry++ < maxRetryCount &&
|
|
e instanceof FetchError &&
|
|
(e.code === "ETIMEDOUT" || e.code === "ECONNRESET")
|
|
) {
|
|
void this.app.logger.log(
|
|
`Timeout while trying to download variant analysis with id: ${
|
|
variantAnalysis.id
|
|
}. Error: ${getErrorMessage(e)}. Retrying...`,
|
|
);
|
|
continue;
|
|
}
|
|
void this.app.logger.log(
|
|
`Failed to download variant analysis after ${retry} attempts.`,
|
|
);
|
|
throw e;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
repoState.downloadStatus =
|
|
VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
|
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
|
throw new Error(
|
|
`Could not download the results for variant analysis with id: ${
|
|
variantAnalysis.id
|
|
}. Error: ${getErrorMessage(e)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
repoState.downloadStatus =
|
|
VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
|
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
|
|
|
const repoStates = this.repoStates.get(variantAnalysis.id);
|
|
if (repoStates) {
|
|
await writeRepoStates(
|
|
this.getRepoStatesStoragePath(variantAnalysis.id),
|
|
repoStates,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async enqueueDownload(
|
|
scannedRepo: VariantAnalysisScannedRepository,
|
|
variantAnalysis: VariantAnalysis,
|
|
): Promise<void> {
|
|
await this.queue.add(() =>
|
|
this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysis),
|
|
);
|
|
}
|
|
|
|
public downloadsQueueSize(): number {
|
|
return this.queue.pending;
|
|
}
|
|
|
|
public getVariantAnalysisStorageLocation(variantAnalysisId: number): string {
|
|
return join(this.storagePath, `${variantAnalysisId}`);
|
|
}
|
|
|
|
public async cancelVariantAnalysis(variantAnalysisId: number) {
|
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
|
if (!variantAnalysis) {
|
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
|
}
|
|
|
|
if (!variantAnalysis.actionsWorkflowRunId) {
|
|
throw new Error(
|
|
`No workflow run id for variant analysis with id: ${variantAnalysis.id}`,
|
|
);
|
|
}
|
|
|
|
void showAndLogInformationMessage(
|
|
this.app.logger,
|
|
"Cancelling variant analysis. This may take a while.",
|
|
);
|
|
await cancelVariantAnalysis(this.app.credentials, variantAnalysis);
|
|
}
|
|
|
|
public async openVariantAnalysisLogs(variantAnalysisId: number) {
|
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
|
if (!variantAnalysis) {
|
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
|
}
|
|
|
|
const actionsWorkflowRunUrl = getActionsWorkflowRunUrl(variantAnalysis);
|
|
|
|
await this.app.commands.execute(
|
|
"vscode.open",
|
|
Uri.parse(actionsWorkflowRunUrl),
|
|
);
|
|
}
|
|
|
|
public async copyRepoListToClipboard(
|
|
variantAnalysisId: number,
|
|
filterSort: RepositoriesFilterSortStateWithIds = defaultFilterSortState,
|
|
) {
|
|
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
|
|
if (!variantAnalysis) {
|
|
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
|
|
}
|
|
|
|
const filteredRepositories = filterAndSortRepositoriesWithResults(
|
|
variantAnalysis.scannedRepos,
|
|
filterSort,
|
|
);
|
|
|
|
const fullNames = filteredRepositories
|
|
?.filter((a) => a.resultCount && a.resultCount > 0)
|
|
.map((a) => a.repository.fullName);
|
|
if (!fullNames || fullNames.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const text = [
|
|
"{",
|
|
` "name": "new-repo-list",`,
|
|
` "repositories": [`,
|
|
...fullNames.slice(0, -1).map((repo) => ` "${repo}",`),
|
|
` "${fullNames[fullNames.length - 1]}"`,
|
|
` ]`,
|
|
"}",
|
|
];
|
|
|
|
await env.clipboard.writeText(text.join(EOL));
|
|
}
|
|
|
|
public async exportResults(
|
|
variantAnalysisId: number,
|
|
filterSort?: RepositoriesFilterSortStateWithIds,
|
|
) {
|
|
await exportVariantAnalysisResults(
|
|
this,
|
|
variantAnalysisId,
|
|
filterSort,
|
|
this.app.commands,
|
|
this.app.credentials,
|
|
);
|
|
}
|
|
|
|
private getRepoStatesStoragePath(variantAnalysisId: number): string {
|
|
return join(
|
|
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
|
REPO_STATES_FILENAME,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prepares a directory for storing results for a variant analysis.
|
|
* This directory contains a timestamp file, which will be
|
|
* used by the query history manager to determine when the directory
|
|
* should be deleted.
|
|
*/
|
|
private async prepareStorageDirectory(
|
|
variantAnalysisId: number,
|
|
): Promise<void> {
|
|
await createTimestampFile(
|
|
this.getVariantAnalysisStorageLocation(variantAnalysisId),
|
|
);
|
|
}
|
|
}
|