Move DatabaseManager to separate file
This commit is contained in:
@@ -1,41 +1,5 @@
|
||||
import { remove } from "fs-extra";
|
||||
import { join, dirname, extname } from "path";
|
||||
import { dirname } from "path";
|
||||
import * as vscode from "vscode";
|
||||
import * as cli from "../codeql-cli/cli";
|
||||
import { ExtensionContext } from "vscode";
|
||||
import {
|
||||
showAndLogExceptionWithTelemetry,
|
||||
isFolderAlreadyInWorkspace,
|
||||
getFirstWorkspaceFolder,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../helpers";
|
||||
import { ProgressCallback, withProgress } from "../common/vscode/progress";
|
||||
import { DisposableObject } from "../pure/disposable-object";
|
||||
import { Logger, extLogger } from "../common";
|
||||
import { asError, getErrorMessage } from "../pure/helpers-pure";
|
||||
import { QueryRunner } from "../query-server";
|
||||
import { containsPath } from "../pure/files";
|
||||
import { redactableError } from "../pure/errors";
|
||||
import {
|
||||
getAutogenerateQlPacks,
|
||||
isCodespacesTemplate,
|
||||
setAutogenerateQlPacks,
|
||||
} from "../config";
|
||||
import { QlPackGenerator } from "../qlpack-generator";
|
||||
import { QueryLanguage } from "../common/query-language";
|
||||
import { App } from "../common/app";
|
||||
import { existsSync } from "fs";
|
||||
import { FullDatabaseOptions } from "./local-databases/database-options";
|
||||
import {
|
||||
DatabaseItem,
|
||||
PersistedDatabaseItem,
|
||||
} from "./local-databases/database-item";
|
||||
import { DatabaseItemImpl } from "./local-databases/database-item-impl";
|
||||
import { DatabaseResolver } from "./local-databases/database-resolver";
|
||||
import {
|
||||
DatabaseChangedEvent,
|
||||
DatabaseEventKind,
|
||||
} from "./local-databases/database-events";
|
||||
|
||||
export { DatabaseContentsWithDbScheme } from "./local-databases/database-contents";
|
||||
export {
|
||||
@@ -43,6 +7,7 @@ export {
|
||||
DatabaseEventKind,
|
||||
} from "./local-databases/database-events";
|
||||
export { DatabaseItem } from "./local-databases/database-item";
|
||||
export { DatabaseManager } from "./local-databases/database-manager";
|
||||
export { DatabaseResolver } from "./local-databases/database-resolver";
|
||||
|
||||
/**
|
||||
@@ -55,622 +20,6 @@ export { DatabaseResolver } from "./local-databases/database-resolver";
|
||||
* `DatabaseManager` class below.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the current database across sessions.
|
||||
*/
|
||||
const CURRENT_DB = "currentDatabase";
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the list of databases across sessions.
|
||||
*/
|
||||
const DB_LIST = "databaseList";
|
||||
|
||||
/**
|
||||
* A promise that resolves to an event's result value when the event
|
||||
* `event` fires. If waiting for the event takes too long (by default
|
||||
* >1000ms) log a warning, and resolve to undefined.
|
||||
*/
|
||||
function eventFired<T>(
|
||||
event: vscode.Event<T>,
|
||||
timeoutMs = 1000,
|
||||
): Promise<T | undefined> {
|
||||
return new Promise((res, _rej) => {
|
||||
const timeout = setTimeout(() => {
|
||||
void extLogger.log(
|
||||
`Waiting for event ${event} timed out after ${timeoutMs}ms`,
|
||||
);
|
||||
res(undefined);
|
||||
dispose();
|
||||
}, timeoutMs);
|
||||
const disposable = event((e) => {
|
||||
res(e);
|
||||
dispose();
|
||||
});
|
||||
function dispose() {
|
||||
clearTimeout(timeout);
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class DatabaseManager extends DisposableObject {
|
||||
private readonly _onDidChangeDatabaseItem = this.push(
|
||||
new vscode.EventEmitter<DatabaseChangedEvent>(),
|
||||
);
|
||||
|
||||
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
|
||||
|
||||
private readonly _onDidChangeCurrentDatabaseItem = this.push(
|
||||
new vscode.EventEmitter<DatabaseChangedEvent>(),
|
||||
);
|
||||
readonly onDidChangeCurrentDatabaseItem =
|
||||
this._onDidChangeCurrentDatabaseItem.event;
|
||||
|
||||
private readonly _databaseItems: DatabaseItem[] = [];
|
||||
private _currentDatabaseItem: DatabaseItem | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly app: App,
|
||||
private readonly qs: QueryRunner,
|
||||
private readonly cli: cli.CodeQLCliServer,
|
||||
public logger: Logger,
|
||||
) {
|
||||
super();
|
||||
|
||||
qs.onStart(this.reregisterDatabases.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DatabaseItem} for the specified database, and adds it to the list of open
|
||||
* databases.
|
||||
*/
|
||||
public async openDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
uri: vscode.Uri,
|
||||
makeSelected = true,
|
||||
displayName?: string,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
const databaseItem = await this.createDatabaseItem(uri, displayName);
|
||||
|
||||
return await this.addExistingDatabaseItem(
|
||||
databaseItem,
|
||||
progress,
|
||||
makeSelected,
|
||||
token,
|
||||
isTutorialDatabase,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on
|
||||
* the list.
|
||||
*
|
||||
* Typically, the item will have been created by {@link createOrOpenDatabaseItem} or {@link openDatabase}.
|
||||
*/
|
||||
public async addExistingDatabaseItem(
|
||||
databaseItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
makeSelected: boolean,
|
||||
token: vscode.CancellationToken,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
|
||||
if (existingItem !== undefined) {
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(existingItem);
|
||||
}
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
await this.addDatabaseItem(progress, token, databaseItem);
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(databaseItem);
|
||||
}
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
|
||||
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
||||
await this.createSkeletonPacks(databaseItem);
|
||||
}
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DatabaseItem} for the specified database, without adding it to the list of
|
||||
* open databases.
|
||||
*/
|
||||
private async createDatabaseItem(
|
||||
uri: vscode.Uri,
|
||||
displayName: string | undefined,
|
||||
): Promise<DatabaseItem> {
|
||||
const contents = await DatabaseResolver.resolveDatabaseContents(uri);
|
||||
// Ignore the source archive for QLTest databases by default.
|
||||
const isQLTestDatabase = extname(uri.fsPath) === ".testproj";
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive: isQLTestDatabase,
|
||||
// If a displayName is not passed in, the basename of folder containing the database is used.
|
||||
displayName,
|
||||
dateAdded: Date.now(),
|
||||
language: await this.getPrimaryLanguage(uri.fsPath),
|
||||
};
|
||||
const databaseItem = new DatabaseItemImpl(
|
||||
uri,
|
||||
contents,
|
||||
fullOptions,
|
||||
(event) => {
|
||||
this._onDidChangeDatabaseItem.fire(event);
|
||||
},
|
||||
);
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the specified database is already on the list of open databases, returns that database's
|
||||
* {@link DatabaseItem}. Otherwise, creates a new {@link DatabaseItem} without adding it to the
|
||||
* list of open databases.
|
||||
*
|
||||
* The {@link DatabaseItem} can be added to the list of open databases later, via {@link addExistingDatabaseItem}.
|
||||
*/
|
||||
public async createOrOpenDatabaseItem(
|
||||
uri: vscode.Uri,
|
||||
): Promise<DatabaseItem> {
|
||||
const existingItem = this.findDatabaseItem(uri);
|
||||
if (existingItem !== undefined) {
|
||||
// Use the one we already have.
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
// We don't add this to the list automatically, but the user can add it later.
|
||||
return this.createDatabaseItem(uri, undefined);
|
||||
}
|
||||
|
||||
public async createSkeletonPacks(databaseItem: DatabaseItem) {
|
||||
if (databaseItem === undefined) {
|
||||
void this.logger.log(
|
||||
"Could not create QL pack because no database is selected. Please add a database.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (databaseItem.language === "") {
|
||||
void this.logger.log(
|
||||
"Could not create skeleton QL pack because the selected database's language is not set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstWorkspaceFolder = getFirstWorkspaceFolder();
|
||||
const folderName = `codeql-custom-queries-${databaseItem.language}`;
|
||||
|
||||
if (
|
||||
existsSync(join(firstWorkspaceFolder, folderName)) ||
|
||||
isFolderAlreadyInWorkspace(folderName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getAutogenerateQlPacks() === "never") {
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await showNeverAskAgainDialog(
|
||||
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
|
||||
);
|
||||
|
||||
if (answer === "No") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer === "No, and never ask me again") {
|
||||
await setAutogenerateQlPacks("never");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const qlPackGenerator = new QlPackGenerator(
|
||||
folderName,
|
||||
databaseItem.language as QueryLanguage,
|
||||
this.cli,
|
||||
firstWorkspaceFolder,
|
||||
);
|
||||
await qlPackGenerator.generate();
|
||||
} catch (e: unknown) {
|
||||
void this.logger.log(
|
||||
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async reregisterDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
let completed = 0;
|
||||
await Promise.all(
|
||||
this._databaseItems.map(async (databaseItem) => {
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
completed++;
|
||||
progress({
|
||||
maxStep: this._databaseItems.length,
|
||||
step: completed,
|
||||
message: "Re-registering databases",
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
|
||||
// The folder may already be in workspace state from a previous
|
||||
// session. If not, add it.
|
||||
const index = this.getDatabaseWorkspaceFolderIndex(item);
|
||||
if (index === -1) {
|
||||
// Add that filesystem as a folder to the current workspace.
|
||||
//
|
||||
// It's important that we add workspace folders to the end,
|
||||
// rather than beginning of the list, because the first
|
||||
// workspace folder is special; if it gets updated, the entire
|
||||
// extension host is restarted. (cf.
|
||||
// https://github.com/microsoft/vscode/blob/e0d2ed907d1b22808c56127678fb436d604586a7/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts#L209-L214)
|
||||
//
|
||||
// This is undesirable, as we might be adding and removing many
|
||||
// workspace folders as the user adds and removes databases.
|
||||
const end = (vscode.workspace.workspaceFolders || []).length;
|
||||
|
||||
const msg = item.verifyZippedSources();
|
||||
if (msg) {
|
||||
void extLogger.log(`Could not add source folder because ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = item.getSourceArchiveExplorerUri();
|
||||
void extLogger.log(
|
||||
`Adding workspace folder for ${item.name} source archive at index ${end}`,
|
||||
);
|
||||
if ((vscode.workspace.workspaceFolders || []).length < 2) {
|
||||
// Adding this workspace folder makes the workspace
|
||||
// multi-root, which may surprise the user. Let them know
|
||||
// we're doing this.
|
||||
void vscode.window.showInformationMessage(
|
||||
`Adding workspace folder for source archive of database ${item.name}.`,
|
||||
);
|
||||
}
|
||||
vscode.workspace.updateWorkspaceFolders(end, 0, {
|
||||
name: `[${item.name} source archive]`,
|
||||
uri,
|
||||
});
|
||||
// vscode api documentation says we must to wait for this event
|
||||
// between multiple `updateWorkspaceFolders` calls.
|
||||
await eventFired(vscode.workspace.onDidChangeWorkspaceFolders);
|
||||
}
|
||||
}
|
||||
|
||||
private async createDatabaseItemFromPersistedState(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
state: PersistedDatabaseItem,
|
||||
): Promise<DatabaseItem> {
|
||||
let displayName: string | undefined = undefined;
|
||||
let ignoreSourceArchive = false;
|
||||
let dateAdded = undefined;
|
||||
let language = undefined;
|
||||
if (state.options) {
|
||||
if (typeof state.options.displayName === "string") {
|
||||
displayName = state.options.displayName;
|
||||
}
|
||||
if (typeof state.options.ignoreSourceArchive === "boolean") {
|
||||
ignoreSourceArchive = state.options.ignoreSourceArchive;
|
||||
}
|
||||
if (typeof state.options.dateAdded === "number") {
|
||||
dateAdded = state.options.dateAdded;
|
||||
}
|
||||
language = state.options.language;
|
||||
}
|
||||
|
||||
const dbBaseUri = vscode.Uri.parse(state.uri, true);
|
||||
if (language === undefined) {
|
||||
// we haven't been successful yet at getting the language. try again
|
||||
language = await this.getPrimaryLanguage(dbBaseUri.fsPath);
|
||||
}
|
||||
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive,
|
||||
displayName,
|
||||
dateAdded,
|
||||
language,
|
||||
};
|
||||
const item = new DatabaseItemImpl(
|
||||
dbBaseUri,
|
||||
undefined,
|
||||
fullOptions,
|
||||
(event) => {
|
||||
this._onDidChangeDatabaseItem.fire(event);
|
||||
},
|
||||
);
|
||||
|
||||
// Avoid persisting the database state after adding since that should happen only after
|
||||
// all databases have been added.
|
||||
await this.addDatabaseItem(progress, token, item, false);
|
||||
return item;
|
||||
}
|
||||
|
||||
public async loadPersistedState(): Promise<void> {
|
||||
return withProgress(async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${database.options?.displayName || "databases"}`,
|
||||
step: ++step,
|
||||
});
|
||||
|
||||
const databaseItem = await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
});
|
||||
}
|
||||
|
||||
public get databaseItems(): readonly DatabaseItem[] {
|
||||
return this._databaseItems;
|
||||
}
|
||||
|
||||
public get currentDatabaseItem(): DatabaseItem | undefined {
|
||||
return this._currentDatabaseItem;
|
||||
}
|
||||
|
||||
public async setCurrentDatabaseItem(
|
||||
item: DatabaseItem | undefined,
|
||||
skipRefresh = false,
|
||||
): Promise<void> {
|
||||
if (!skipRefresh && item !== undefined) {
|
||||
await item.refresh(); // Will throw on invalid database.
|
||||
}
|
||||
if (this._currentDatabaseItem !== item) {
|
||||
this._currentDatabaseItem = item;
|
||||
this.updatePersistedCurrentDatabaseItem();
|
||||
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQL.currentDatabaseItem",
|
||||
item?.name,
|
||||
);
|
||||
|
||||
this._onDidChangeCurrentDatabaseItem.fire({
|
||||
item,
|
||||
kind: DatabaseEventKind.Change,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the workspace folder that corresponds to the source archive of `item`
|
||||
* if there is one, and -1 otherwise.
|
||||
*/
|
||||
private getDatabaseWorkspaceFolderIndex(item: DatabaseItem): number {
|
||||
return (vscode.workspace.workspaceFolders || []).findIndex((folder) =>
|
||||
item.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
}
|
||||
|
||||
public findDatabaseItem(uri: vscode.Uri): DatabaseItem | undefined {
|
||||
const uriString = uri.toString(true);
|
||||
return this._databaseItems.find(
|
||||
(item) => item.databaseUri.toString(true) === uriString,
|
||||
);
|
||||
}
|
||||
|
||||
public findDatabaseItemBySourceArchive(
|
||||
uri: vscode.Uri,
|
||||
): DatabaseItem | undefined {
|
||||
const uriString = uri.toString(true);
|
||||
return this._databaseItems.find(
|
||||
(item) =>
|
||||
item.sourceArchive && item.sourceArchive.toString(true) === uriString,
|
||||
);
|
||||
}
|
||||
|
||||
private async addDatabaseItem(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
item: DatabaseItem,
|
||||
updatePersistedState = true,
|
||||
) {
|
||||
this._databaseItems.push(item);
|
||||
|
||||
if (updatePersistedState) {
|
||||
await this.updatePersistedDatabaseList();
|
||||
}
|
||||
|
||||
// Add this database item to the allow-list
|
||||
// Database items reconstituted from persisted state
|
||||
// will not have their contents yet.
|
||||
if (item.contents?.datasetUri) {
|
||||
await this.registerDatabase(progress, token, item);
|
||||
}
|
||||
// note that we use undefined as the item in order to reset the entire tree
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Add,
|
||||
});
|
||||
}
|
||||
|
||||
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
|
||||
item.name = newName;
|
||||
await this.updatePersistedDatabaseList();
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
// pass undefined so that the entire tree is rebuilt in order to re-sort
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Rename,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeDatabaseItem(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
item: DatabaseItem,
|
||||
) {
|
||||
if (this._currentDatabaseItem === item) {
|
||||
this._currentDatabaseItem = undefined;
|
||||
}
|
||||
const index = this.databaseItems.findIndex(
|
||||
(searchItem) => searchItem === item,
|
||||
);
|
||||
if (index >= 0) {
|
||||
this._databaseItems.splice(index, 1);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
|
||||
// Delete folder from workspace, if it is still there
|
||||
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(
|
||||
(folder) => item.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
if (folderIndex >= 0) {
|
||||
void extLogger.log(`Removing workspace folder at index ${folderIndex}`);
|
||||
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
|
||||
}
|
||||
|
||||
// Remove this database item from the allow-list
|
||||
await this.deregisterDatabase(progress, token, item);
|
||||
|
||||
// Delete folder from file system only if it is controlled by the extension
|
||||
if (this.isExtensionControlledLocation(item.databaseUri)) {
|
||||
void extLogger.log("Deleting database from filesystem.");
|
||||
await remove(item.databaseUri.fsPath).then(
|
||||
() => void extLogger.log(`Deleted '${item.databaseUri.fsPath}'`),
|
||||
(e: unknown) =>
|
||||
void extLogger.log(
|
||||
`Failed to delete '${
|
||||
item.databaseUri.fsPath
|
||||
}'. Reason: ${getErrorMessage(e)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// note that we use undefined as the item in order to reset the entire tree
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Remove,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeAllDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
for (const item of this.databaseItems) {
|
||||
await this.removeDatabaseItem(progress, token, item);
|
||||
}
|
||||
}
|
||||
|
||||
private async deregisterDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
dbItem: DatabaseItem,
|
||||
) {
|
||||
try {
|
||||
await this.qs.deregisterDatabase(progress, token, dbItem);
|
||||
} catch (e) {
|
||||
const message = getErrorMessage(e);
|
||||
if (message === "Connection is disposed.") {
|
||||
// This is expected if the query server is not running.
|
||||
void extLogger.log(
|
||||
`Could not de-register database '${dbItem.name}' because query server is not running.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
private async registerDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
dbItem: DatabaseItem,
|
||||
) {
|
||||
await this.qs.registerDatabase(progress, token, dbItem);
|
||||
}
|
||||
|
||||
private updatePersistedCurrentDatabaseItem(): void {
|
||||
void this.ctx.workspaceState.update(
|
||||
CURRENT_DB,
|
||||
this._currentDatabaseItem
|
||||
? this._currentDatabaseItem.databaseUri.toString(true)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
private async updatePersistedDatabaseList(): Promise<void> {
|
||||
await this.ctx.workspaceState.update(
|
||||
DB_LIST,
|
||||
this._databaseItems.map((item) => item.getPersistedState()),
|
||||
);
|
||||
}
|
||||
|
||||
private isExtensionControlledLocation(uri: vscode.Uri) {
|
||||
const storageUri = this.ctx.storageUri || this.ctx.globalStorageUri;
|
||||
if (storageUri) {
|
||||
return containsPath(storageUri.fsPath, uri.fsPath, process.platform);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getPrimaryLanguage(dbPath: string) {
|
||||
const dbInfo = await this.cli.resolveDatabase(dbPath);
|
||||
return dbInfo.languages?.[0] || "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of directories containing upgrades, given a list of
|
||||
* scripts returned by the cli's upgrade resolution.
|
||||
|
||||
@@ -0,0 +1,647 @@
|
||||
import vscode, { ExtensionContext } from "vscode";
|
||||
import { extLogger, Logger } from "../../common";
|
||||
import { DisposableObject } from "../../pure/disposable-object";
|
||||
import { App } from "../../common/app";
|
||||
import { QueryRunner } from "../../query-server";
|
||||
import * as cli from "../../codeql-cli/cli";
|
||||
import { ProgressCallback, withProgress } from "../../common/vscode/progress";
|
||||
import {
|
||||
getAutogenerateQlPacks,
|
||||
isCodespacesTemplate,
|
||||
setAutogenerateQlPacks,
|
||||
} from "../../config";
|
||||
import { extname, join } from "path";
|
||||
import { FullDatabaseOptions } from "./database-options";
|
||||
import { DatabaseItemImpl } from "./database-item-impl";
|
||||
import {
|
||||
getFirstWorkspaceFolder,
|
||||
isFolderAlreadyInWorkspace,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
showNeverAskAgainDialog,
|
||||
} from "../../helpers";
|
||||
import { existsSync } from "fs";
|
||||
import { QlPackGenerator } from "../../qlpack-generator";
|
||||
import { QueryLanguage } from "../../common/query-language";
|
||||
import { asError, getErrorMessage } from "../../pure/helpers-pure";
|
||||
import { DatabaseItem, PersistedDatabaseItem } from "./database-item";
|
||||
import { redactableError } from "../../pure/errors";
|
||||
import { remove } from "fs-extra";
|
||||
import { containsPath } from "../../pure/files";
|
||||
import { DatabaseChangedEvent, DatabaseEventKind } from "./database-events";
|
||||
import { DatabaseResolver } from "./database-resolver";
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the current database across sessions.
|
||||
*/
|
||||
const CURRENT_DB = "currentDatabase";
|
||||
|
||||
/**
|
||||
* The name of the key in the workspaceState dictionary in which we
|
||||
* persist the list of databases across sessions.
|
||||
*/
|
||||
const DB_LIST = "databaseList";
|
||||
|
||||
/**
|
||||
* A promise that resolves to an event's result value when the event
|
||||
* `event` fires. If waiting for the event takes too long (by default
|
||||
* >1000ms) log a warning, and resolve to undefined.
|
||||
*/
|
||||
function eventFired<T>(
|
||||
event: vscode.Event<T>,
|
||||
timeoutMs = 1000,
|
||||
): Promise<T | undefined> {
|
||||
return new Promise((res, _rej) => {
|
||||
const timeout = setTimeout(() => {
|
||||
void extLogger.log(
|
||||
`Waiting for event ${event} timed out after ${timeoutMs}ms`,
|
||||
);
|
||||
res(undefined);
|
||||
dispose();
|
||||
}, timeoutMs);
|
||||
const disposable = event((e) => {
|
||||
res(e);
|
||||
dispose();
|
||||
});
|
||||
function dispose() {
|
||||
clearTimeout(timeout);
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class DatabaseManager extends DisposableObject {
|
||||
private readonly _onDidChangeDatabaseItem = this.push(
|
||||
new vscode.EventEmitter<DatabaseChangedEvent>(),
|
||||
);
|
||||
|
||||
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
|
||||
|
||||
private readonly _onDidChangeCurrentDatabaseItem = this.push(
|
||||
new vscode.EventEmitter<DatabaseChangedEvent>(),
|
||||
);
|
||||
readonly onDidChangeCurrentDatabaseItem =
|
||||
this._onDidChangeCurrentDatabaseItem.event;
|
||||
|
||||
private readonly _databaseItems: DatabaseItem[] = [];
|
||||
private _currentDatabaseItem: DatabaseItem | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private readonly ctx: ExtensionContext,
|
||||
private readonly app: App,
|
||||
private readonly qs: QueryRunner,
|
||||
private readonly cli: cli.CodeQLCliServer,
|
||||
public logger: Logger,
|
||||
) {
|
||||
super();
|
||||
|
||||
qs.onStart(this.reregisterDatabases.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DatabaseItem} for the specified database, and adds it to the list of open
|
||||
* databases.
|
||||
*/
|
||||
public async openDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
uri: vscode.Uri,
|
||||
makeSelected = true,
|
||||
displayName?: string,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
const databaseItem = await this.createDatabaseItem(uri, displayName);
|
||||
|
||||
return await this.addExistingDatabaseItem(
|
||||
databaseItem,
|
||||
progress,
|
||||
makeSelected,
|
||||
token,
|
||||
isTutorialDatabase,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link DatabaseItem} to the list of open databases, if that database is not already on
|
||||
* the list.
|
||||
*
|
||||
* Typically, the item will have been created by {@link createOrOpenDatabaseItem} or {@link openDatabase}.
|
||||
*/
|
||||
public async addExistingDatabaseItem(
|
||||
databaseItem: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
makeSelected: boolean,
|
||||
token: vscode.CancellationToken,
|
||||
isTutorialDatabase?: boolean,
|
||||
): Promise<DatabaseItem> {
|
||||
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
|
||||
if (existingItem !== undefined) {
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(existingItem);
|
||||
}
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
await this.addDatabaseItem(progress, token, databaseItem);
|
||||
if (makeSelected) {
|
||||
await this.setCurrentDatabaseItem(databaseItem);
|
||||
}
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
|
||||
if (isCodespacesTemplate() && !isTutorialDatabase) {
|
||||
await this.createSkeletonPacks(databaseItem);
|
||||
}
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DatabaseItem} for the specified database, without adding it to the list of
|
||||
* open databases.
|
||||
*/
|
||||
private async createDatabaseItem(
|
||||
uri: vscode.Uri,
|
||||
displayName: string | undefined,
|
||||
): Promise<DatabaseItem> {
|
||||
const contents = await DatabaseResolver.resolveDatabaseContents(uri);
|
||||
// Ignore the source archive for QLTest databases by default.
|
||||
const isQLTestDatabase = extname(uri.fsPath) === ".testproj";
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive: isQLTestDatabase,
|
||||
// If a displayName is not passed in, the basename of folder containing the database is used.
|
||||
displayName,
|
||||
dateAdded: Date.now(),
|
||||
language: await this.getPrimaryLanguage(uri.fsPath),
|
||||
};
|
||||
const databaseItem = new DatabaseItemImpl(
|
||||
uri,
|
||||
contents,
|
||||
fullOptions,
|
||||
(event) => {
|
||||
this._onDidChangeDatabaseItem.fire(event);
|
||||
},
|
||||
);
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the specified database is already on the list of open databases, returns that database's
|
||||
* {@link DatabaseItem}. Otherwise, creates a new {@link DatabaseItem} without adding it to the
|
||||
* list of open databases.
|
||||
*
|
||||
* The {@link DatabaseItem} can be added to the list of open databases later, via {@link addExistingDatabaseItem}.
|
||||
*/
|
||||
public async createOrOpenDatabaseItem(
|
||||
uri: vscode.Uri,
|
||||
): Promise<DatabaseItem> {
|
||||
const existingItem = this.findDatabaseItem(uri);
|
||||
if (existingItem !== undefined) {
|
||||
// Use the one we already have.
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
// We don't add this to the list automatically, but the user can add it later.
|
||||
return this.createDatabaseItem(uri, undefined);
|
||||
}
|
||||
|
||||
public async createSkeletonPacks(databaseItem: DatabaseItem) {
|
||||
if (databaseItem === undefined) {
|
||||
void this.logger.log(
|
||||
"Could not create QL pack because no database is selected. Please add a database.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (databaseItem.language === "") {
|
||||
void this.logger.log(
|
||||
"Could not create skeleton QL pack because the selected database's language is not set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstWorkspaceFolder = getFirstWorkspaceFolder();
|
||||
const folderName = `codeql-custom-queries-${databaseItem.language}`;
|
||||
|
||||
if (
|
||||
existsSync(join(firstWorkspaceFolder, folderName)) ||
|
||||
isFolderAlreadyInWorkspace(folderName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getAutogenerateQlPacks() === "never") {
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = await showNeverAskAgainDialog(
|
||||
`We've noticed you don't have a CodeQL pack available to analyze this database. Can we set up a query pack for you?`,
|
||||
);
|
||||
|
||||
if (answer === "No") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (answer === "No, and never ask me again") {
|
||||
await setAutogenerateQlPacks("never");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const qlPackGenerator = new QlPackGenerator(
|
||||
folderName,
|
||||
databaseItem.language as QueryLanguage,
|
||||
this.cli,
|
||||
firstWorkspaceFolder,
|
||||
);
|
||||
await qlPackGenerator.generate();
|
||||
} catch (e: unknown) {
|
||||
void this.logger.log(
|
||||
`Could not create skeleton QL pack: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async reregisterDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
let completed = 0;
|
||||
await Promise.all(
|
||||
this._databaseItems.map(async (databaseItem) => {
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
completed++;
|
||||
progress({
|
||||
maxStep: this._databaseItems.length,
|
||||
step: completed,
|
||||
message: "Re-registering databases",
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
|
||||
// The folder may already be in workspace state from a previous
|
||||
// session. If not, add it.
|
||||
const index = this.getDatabaseWorkspaceFolderIndex(item);
|
||||
if (index === -1) {
|
||||
// Add that filesystem as a folder to the current workspace.
|
||||
//
|
||||
// It's important that we add workspace folders to the end,
|
||||
// rather than beginning of the list, because the first
|
||||
// workspace folder is special; if it gets updated, the entire
|
||||
// extension host is restarted. (cf.
|
||||
// https://github.com/microsoft/vscode/blob/e0d2ed907d1b22808c56127678fb436d604586a7/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts#L209-L214)
|
||||
//
|
||||
// This is undesirable, as we might be adding and removing many
|
||||
// workspace folders as the user adds and removes databases.
|
||||
const end = (vscode.workspace.workspaceFolders || []).length;
|
||||
|
||||
const msg = item.verifyZippedSources();
|
||||
if (msg) {
|
||||
void extLogger.log(`Could not add source folder because ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = item.getSourceArchiveExplorerUri();
|
||||
void extLogger.log(
|
||||
`Adding workspace folder for ${item.name} source archive at index ${end}`,
|
||||
);
|
||||
if ((vscode.workspace.workspaceFolders || []).length < 2) {
|
||||
// Adding this workspace folder makes the workspace
|
||||
// multi-root, which may surprise the user. Let them know
|
||||
// we're doing this.
|
||||
void vscode.window.showInformationMessage(
|
||||
`Adding workspace folder for source archive of database ${item.name}.`,
|
||||
);
|
||||
}
|
||||
vscode.workspace.updateWorkspaceFolders(end, 0, {
|
||||
name: `[${item.name} source archive]`,
|
||||
uri,
|
||||
});
|
||||
// vscode api documentation says we must to wait for this event
|
||||
// between multiple `updateWorkspaceFolders` calls.
|
||||
await eventFired(vscode.workspace.onDidChangeWorkspaceFolders);
|
||||
}
|
||||
}
|
||||
|
||||
private async createDatabaseItemFromPersistedState(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
state: PersistedDatabaseItem,
|
||||
): Promise<DatabaseItem> {
|
||||
let displayName: string | undefined = undefined;
|
||||
let ignoreSourceArchive = false;
|
||||
let dateAdded = undefined;
|
||||
let language = undefined;
|
||||
if (state.options) {
|
||||
if (typeof state.options.displayName === "string") {
|
||||
displayName = state.options.displayName;
|
||||
}
|
||||
if (typeof state.options.ignoreSourceArchive === "boolean") {
|
||||
ignoreSourceArchive = state.options.ignoreSourceArchive;
|
||||
}
|
||||
if (typeof state.options.dateAdded === "number") {
|
||||
dateAdded = state.options.dateAdded;
|
||||
}
|
||||
language = state.options.language;
|
||||
}
|
||||
|
||||
const dbBaseUri = vscode.Uri.parse(state.uri, true);
|
||||
if (language === undefined) {
|
||||
// we haven't been successful yet at getting the language. try again
|
||||
language = await this.getPrimaryLanguage(dbBaseUri.fsPath);
|
||||
}
|
||||
|
||||
const fullOptions: FullDatabaseOptions = {
|
||||
ignoreSourceArchive,
|
||||
displayName,
|
||||
dateAdded,
|
||||
language,
|
||||
};
|
||||
const item = new DatabaseItemImpl(
|
||||
dbBaseUri,
|
||||
undefined,
|
||||
fullOptions,
|
||||
(event) => {
|
||||
this._onDidChangeDatabaseItem.fire(event);
|
||||
},
|
||||
);
|
||||
|
||||
// Avoid persisting the database state after adding since that should happen only after
|
||||
// all databases have been added.
|
||||
await this.addDatabaseItem(progress, token, item, false);
|
||||
return item;
|
||||
}
|
||||
|
||||
public async loadPersistedState(): Promise<void> {
|
||||
return withProgress(async (progress, token) => {
|
||||
const currentDatabaseUri =
|
||||
this.ctx.workspaceState.get<string>(CURRENT_DB);
|
||||
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(
|
||||
DB_LIST,
|
||||
[],
|
||||
);
|
||||
let step = 0;
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: "Loading persisted databases",
|
||||
step,
|
||||
});
|
||||
try {
|
||||
void this.logger.log(
|
||||
`Found ${databases.length} persisted databases: ${databases
|
||||
.map((db) => db.uri)
|
||||
.join(", ")}`,
|
||||
);
|
||||
for (const database of databases) {
|
||||
progress({
|
||||
maxStep: databases.length,
|
||||
message: `Loading ${database.options?.displayName || "databases"}`,
|
||||
step: ++step,
|
||||
});
|
||||
|
||||
const databaseItem = await this.createDatabaseItemFromPersistedState(
|
||||
progress,
|
||||
token,
|
||||
database,
|
||||
);
|
||||
try {
|
||||
await databaseItem.refresh();
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
if (currentDatabaseUri === database.uri) {
|
||||
await this.setCurrentDatabaseItem(databaseItem, true);
|
||||
}
|
||||
void this.logger.log(
|
||||
`Loaded database ${databaseItem.name} at URI ${database.uri}.`,
|
||||
);
|
||||
} catch (e) {
|
||||
// When loading from persisted state, leave invalid databases in the list. They will be
|
||||
// marked as invalid, and cannot be set as the current database.
|
||||
void this.logger.log(
|
||||
`Error loading database ${database.uri}: ${e}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
} catch (e) {
|
||||
// database list had an unexpected type - nothing to be done?
|
||||
void showAndLogExceptionWithTelemetry(
|
||||
redactableError(
|
||||
asError(e),
|
||||
)`Database list loading failed: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
|
||||
void this.logger.log("Finished loading persisted databases.");
|
||||
});
|
||||
}
|
||||
|
||||
public get databaseItems(): readonly DatabaseItem[] {
|
||||
return this._databaseItems;
|
||||
}
|
||||
|
||||
public get currentDatabaseItem(): DatabaseItem | undefined {
|
||||
return this._currentDatabaseItem;
|
||||
}
|
||||
|
||||
public async setCurrentDatabaseItem(
|
||||
item: DatabaseItem | undefined,
|
||||
skipRefresh = false,
|
||||
): Promise<void> {
|
||||
if (!skipRefresh && item !== undefined) {
|
||||
await item.refresh(); // Will throw on invalid database.
|
||||
}
|
||||
if (this._currentDatabaseItem !== item) {
|
||||
this._currentDatabaseItem = item;
|
||||
this.updatePersistedCurrentDatabaseItem();
|
||||
|
||||
await this.app.commands.execute(
|
||||
"setContext",
|
||||
"codeQL.currentDatabaseItem",
|
||||
item?.name,
|
||||
);
|
||||
|
||||
this._onDidChangeCurrentDatabaseItem.fire({
|
||||
item,
|
||||
kind: DatabaseEventKind.Change,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the workspace folder that corresponds to the source archive of `item`
|
||||
* if there is one, and -1 otherwise.
|
||||
*/
|
||||
private getDatabaseWorkspaceFolderIndex(item: DatabaseItem): number {
|
||||
return (vscode.workspace.workspaceFolders || []).findIndex((folder) =>
|
||||
item.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
}
|
||||
|
||||
public findDatabaseItem(uri: vscode.Uri): DatabaseItem | undefined {
|
||||
const uriString = uri.toString(true);
|
||||
return this._databaseItems.find(
|
||||
(item) => item.databaseUri.toString(true) === uriString,
|
||||
);
|
||||
}
|
||||
|
||||
public findDatabaseItemBySourceArchive(
|
||||
uri: vscode.Uri,
|
||||
): DatabaseItem | undefined {
|
||||
const uriString = uri.toString(true);
|
||||
return this._databaseItems.find(
|
||||
(item) =>
|
||||
item.sourceArchive && item.sourceArchive.toString(true) === uriString,
|
||||
);
|
||||
}
|
||||
|
||||
private async addDatabaseItem(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
item: DatabaseItem,
|
||||
updatePersistedState = true,
|
||||
) {
|
||||
this._databaseItems.push(item);
|
||||
|
||||
if (updatePersistedState) {
|
||||
await this.updatePersistedDatabaseList();
|
||||
}
|
||||
|
||||
// Add this database item to the allow-list
|
||||
// Database items reconstituted from persisted state
|
||||
// will not have their contents yet.
|
||||
if (item.contents?.datasetUri) {
|
||||
await this.registerDatabase(progress, token, item);
|
||||
}
|
||||
// note that we use undefined as the item in order to reset the entire tree
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Add,
|
||||
});
|
||||
}
|
||||
|
||||
public async renameDatabaseItem(item: DatabaseItem, newName: string) {
|
||||
item.name = newName;
|
||||
await this.updatePersistedDatabaseList();
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
// pass undefined so that the entire tree is rebuilt in order to re-sort
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Rename,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeDatabaseItem(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
item: DatabaseItem,
|
||||
) {
|
||||
if (this._currentDatabaseItem === item) {
|
||||
this._currentDatabaseItem = undefined;
|
||||
}
|
||||
const index = this.databaseItems.findIndex(
|
||||
(searchItem) => searchItem === item,
|
||||
);
|
||||
if (index >= 0) {
|
||||
this._databaseItems.splice(index, 1);
|
||||
}
|
||||
await this.updatePersistedDatabaseList();
|
||||
|
||||
// Delete folder from workspace, if it is still there
|
||||
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(
|
||||
(folder) => item.belongsToSourceArchiveExplorerUri(folder.uri),
|
||||
);
|
||||
if (folderIndex >= 0) {
|
||||
void extLogger.log(`Removing workspace folder at index ${folderIndex}`);
|
||||
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
|
||||
}
|
||||
|
||||
// Remove this database item from the allow-list
|
||||
await this.deregisterDatabase(progress, token, item);
|
||||
|
||||
// Delete folder from file system only if it is controlled by the extension
|
||||
if (this.isExtensionControlledLocation(item.databaseUri)) {
|
||||
void extLogger.log("Deleting database from filesystem.");
|
||||
await remove(item.databaseUri.fsPath).then(
|
||||
() => void extLogger.log(`Deleted '${item.databaseUri.fsPath}'`),
|
||||
(e: unknown) =>
|
||||
void extLogger.log(
|
||||
`Failed to delete '${
|
||||
item.databaseUri.fsPath
|
||||
}'. Reason: ${getErrorMessage(e)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// note that we use undefined as the item in order to reset the entire tree
|
||||
this._onDidChangeDatabaseItem.fire({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Remove,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeAllDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
for (const item of this.databaseItems) {
|
||||
await this.removeDatabaseItem(progress, token, item);
|
||||
}
|
||||
}
|
||||
|
||||
private async deregisterDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
dbItem: DatabaseItem,
|
||||
) {
|
||||
try {
|
||||
await this.qs.deregisterDatabase(progress, token, dbItem);
|
||||
} catch (e) {
|
||||
const message = getErrorMessage(e);
|
||||
if (message === "Connection is disposed.") {
|
||||
// This is expected if the query server is not running.
|
||||
void extLogger.log(
|
||||
`Could not de-register database '${dbItem.name}' because query server is not running.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
private async registerDatabase(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
dbItem: DatabaseItem,
|
||||
) {
|
||||
await this.qs.registerDatabase(progress, token, dbItem);
|
||||
}
|
||||
|
||||
private updatePersistedCurrentDatabaseItem(): void {
|
||||
void this.ctx.workspaceState.update(
|
||||
CURRENT_DB,
|
||||
this._currentDatabaseItem
|
||||
? this._currentDatabaseItem.databaseUri.toString(true)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
private async updatePersistedDatabaseList(): Promise<void> {
|
||||
await this.ctx.workspaceState.update(
|
||||
DB_LIST,
|
||||
this._databaseItems.map((item) => item.getPersistedState()),
|
||||
);
|
||||
}
|
||||
|
||||
private isExtensionControlledLocation(uri: vscode.Uri) {
|
||||
const storageUri = this.ctx.storageUri || this.ctx.globalStorageUri;
|
||||
if (storageUri) {
|
||||
return containsPath(storageUri.fsPath, uri.fsPath, process.platform);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async getPrimaryLanguage(dbPath: string) {
|
||||
const dbInfo = await this.cli.resolveDatabase(dbPath);
|
||||
return dbInfo.languages?.[0] || "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user