Implement CodeQL debug adapter

This commit is contained in:
Dave Bartolomeo
2023-03-27 13:41:32 -04:00
parent 0da5aab1e9
commit a3306da1bc
11 changed files with 857 additions and 5 deletions

View File

@@ -13,6 +13,8 @@
"@octokit/plugin-retry": "^3.0.9",
"@octokit/rest": "^19.0.4",
"@vscode/codicons": "^0.0.31",
"@vscode/debugadapter": "^1.59.0",
"@vscode/debugprotocol": "^1.59.0",
"@vscode/webview-ui-toolkit": "^1.0.1",
"ajv": "^8.11.0",
"child-process-promise": "^2.2.1",
@@ -14379,6 +14381,22 @@
"resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz",
"integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA=="
},
"node_modules/@vscode/debugadapter": {
"version": "1.59.0",
"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.59.0.tgz",
"integrity": "sha512-KfrQ/9QhTxBumxkqIWs9rsFLScdBIqEXx5pGbTXP7V9I3IIcwgdi5N55FbMxQY9tq6xK3KfJHAZLIXDwO7YfVg==",
"dependencies": {
"@vscode/debugprotocol": "1.59.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@vscode/debugprotocol": {
"version": "1.59.0",
"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.59.0.tgz",
"integrity": "sha512-Ks8NiZrCvybf9ebGLP8OUZQbEMIJYC8X0Ds54Q/szpT/SYEDjTksPvZlcWGTo7B9t5abjvbd0jkNH3blYaSuVw=="
},
"node_modules/@vscode/test-electron": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.2.0.tgz",
@@ -52467,6 +52485,19 @@
"resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.31.tgz",
"integrity": "sha512-fldpXy7pHsQAMlU1pnGI23ypQ6xLk5u6SiABMFoAmlj4f2MR0iwg7C19IB1xvAEGG+dkxOfRSrbKF8ry7QqGQA=="
},
"@vscode/debugadapter": {
"version": "1.59.0",
"resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.59.0.tgz",
"integrity": "sha512-KfrQ/9QhTxBumxkqIWs9rsFLScdBIqEXx5pGbTXP7V9I3IIcwgdi5N55FbMxQY9tq6xK3KfJHAZLIXDwO7YfVg==",
"requires": {
"@vscode/debugprotocol": "1.59.0"
}
},
"@vscode/debugprotocol": {
"version": "1.59.0",
"resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.59.0.tgz",
"integrity": "sha512-Ks8NiZrCvybf9ebGLP8OUZQbEMIJYC8X0Ds54Q/szpT/SYEDjTksPvZlcWGTo7B9t5abjvbd0jkNH3blYaSuVw=="
},
"@vscode/test-electron": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.2.0.tgz",

View File

@@ -76,6 +76,40 @@
"editor.wordBasedSuggestions": false
}
},
"debuggers": [
{
"type": "codeql",
"label": "CodeQL Debugger",
"languages": [
"ql"
],
"configurationAttributes": {
"launch": {
"properties": {
"query": {
"type": "string",
"description": "Path to query file (.ql)",
"default": "${file}"
},
"database": {
"type": "string",
"description": "Path to the target database"
},
"additionalPacks": {
"type": [
"array",
"string"
],
"description": "Additional folders to search for library packs. Defaults to searching all workspace folders."
}
}
}
},
"variables": {
"currentDatabase": "codeQL.getCurrentDatabase"
}
}
],
"jsonValidation": [
{
"fileMatch": "GitHub.vscode-codeql/databases.json",
@@ -444,6 +478,10 @@
"command": "codeQL.setCurrentDatabase",
"title": "CodeQL: Set Current Database"
},
{
"command": "codeQL.getCurrentDatabase",
"title": "CodeQL: Get Current Database"
},
{
"command": "codeQL.viewAst",
"title": "CodeQL: View AST"
@@ -1062,6 +1100,10 @@
"command": "codeQL.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQL.getCurrentDatabase",
"when": "false"
},
{
"command": "codeQL.viewAst",
"when": "resourceScheme == codeql-zip-archive"
@@ -1434,6 +1476,8 @@
"@octokit/plugin-retry": "^3.0.9",
"@octokit/rest": "^19.0.4",
"@vscode/codicons": "^0.0.31",
"@vscode/debugadapter": "^1.59.0",
"@vscode/debugprotocol": "^1.59.0",
"@vscode/webview-ui-toolkit": "^1.0.1",
"ajv": "^8.11.0",
"child-process-promise": "^2.2.1",

View File

@@ -180,6 +180,7 @@ export type LocalDatabasesCommands = {
// Internal commands
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
"codeQL.getCurrentDatabase": () => Promise<string | undefined>;
};
// Commands tied to variant analysis

View File

@@ -0,0 +1,78 @@
import {
CancellationToken,
DebugConfiguration,
DebugConfigurationProvider,
WorkspaceFolder,
} from "vscode";
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from "../helpers";
interface QLDebugArgs {
query: string;
database: string;
additionalPacks: string[] | string;
}
type QLDebugConfiguration = DebugConfiguration & Partial<QLDebugArgs>;
export type QLResolvedDebugConfiguration = DebugConfiguration &
QLDebugArgs & {
additionalPacks: string[];
};
export class QLDebugConfigurationProvider
implements DebugConfigurationProvider
{
public resolveDebugConfiguration(
_folder: WorkspaceFolder | undefined,
debugConfiguration: DebugConfiguration,
_token?: CancellationToken,
): DebugConfiguration {
const qlConfiguration = <QLDebugConfiguration>debugConfiguration;
// Fill in defaults
const resultConfiguration: QLDebugConfiguration = {
...qlConfiguration,
query: qlConfiguration.query ?? "${file}",
database: qlConfiguration.database ?? "${command:currentDatabase}",
};
return resultConfiguration;
}
public async resolveDebugConfigurationWithSubstitutedVariables(
_folder: WorkspaceFolder | undefined,
debugConfiguration: DebugConfiguration,
_token?: CancellationToken,
): Promise<DebugConfiguration | null> {
const qlConfiguration = <QLDebugConfiguration>debugConfiguration;
if (qlConfiguration.query === undefined) {
await showAndLogErrorMessage(
"No query was specified in the debug configuration.",
);
return null;
}
if (qlConfiguration.database === undefined) {
await showAndLogErrorMessage(
"No database was specified in the debug configuration.",
);
return null;
}
const resultConfiguration: QLResolvedDebugConfiguration = {
...qlConfiguration,
query: qlConfiguration.query,
database: qlConfiguration.database,
additionalPacks:
// Fill in defaults here, instead of in `resolveDebugConfiguration`, to avoid the highly
// unusual case where one of the workspace folder paths contains something that looks like a
// variable substitution.
qlConfiguration.additionalPacks === undefined
? getOnDiskWorkspaceFolders()
: typeof qlConfiguration.additionalPacks === "string"
? [qlConfiguration.additionalPacks]
: qlConfiguration.additionalPacks,
};
return resultConfiguration;
}
}

View File

@@ -0,0 +1,69 @@
import { DebugProtocol } from "@vscode/debugprotocol";
import { QueryResultType } from "../pure/new-messages";
export type Event = { type: "event" };
export type StoppedEvent = DebugProtocol.StoppedEvent &
Event & { event: "stopped" };
export type InitializedEvent = DebugProtocol.InitializedEvent &
Event & { event: "initialized" };
export type OutputEvent = DebugProtocol.OutputEvent &
Event & { event: "output" };
export interface EvaluationStartedEventBody {
id: string;
outputDir: string;
}
export interface EvaluationStartedEvent extends DebugProtocol.Event {
event: "codeql-evaluation-started";
body: EvaluationStartedEventBody;
}
export interface EvaluationCompletedEventBody {
resultType: QueryResultType;
message: string | undefined;
evaluationTime: number;
}
export interface EvaluationCompletedEvent extends DebugProtocol.Event {
event: "codeql-evaluation-completed";
body: EvaluationCompletedEventBody;
}
export type AnyEvent =
| StoppedEvent
| InitializedEvent
| OutputEvent
| EvaluationStartedEvent
| EvaluationCompletedEvent;
export type Request = DebugProtocol.Request & { type: "request" };
export interface DebugResultRequest extends Request {
command: "codeql-debug-result";
arguments: undefined;
}
export type InitializeRequest = DebugProtocol.InitializeRequest &
Request & { command: "initialize" };
export type AnyRequest = InitializeRequest | DebugResultRequest;
export type Response = DebugProtocol.Response & { type: "response" };
export type InitializeResponse = DebugProtocol.InitializeResponse &
Response & { command: "initialize" };
export type AnyResponse = InitializeResponse;
export type AnyProtocolMessage = AnyEvent | AnyRequest | AnyResponse;
export interface LaunchRequestArguments
extends DebugProtocol.LaunchRequestArguments {
query: string;
database: string;
additionalPacks: string[];
}

View File

@@ -0,0 +1,324 @@
import {
Event,
ExitedEvent,
InitializedEvent,
LoggingDebugSession,
OutputEvent,
ProgressEndEvent,
TerminatedEvent,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { Disposable } from "vscode";
import { CancellationTokenSource } from "vscode-jsonrpc";
import { BaseLogger, LogOptions } from "../common";
import { QueryResultType } from "../pure/new-messages";
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
import * as CodeQLDebugProtocol from "./debug-protocol";
class ProgressStartEvent
extends Event
implements DebugProtocol.ProgressStartEvent
{
public readonly event = "progressStart";
public readonly body: {
progressId: string;
title: string;
requestId?: number;
cancellable?: boolean;
message?: string;
percentage?: number;
};
constructor(
progressId: string,
title: string,
message?: string,
percentage?: number,
) {
super("progressStart");
this.body = {
progressId,
title,
message,
percentage,
};
}
}
class ProgressUpdateEvent
extends Event
implements DebugProtocol.ProgressUpdateEvent
{
public readonly event = "progressUpdate";
public readonly body: {
progressId: string;
message?: string;
percentage?: number;
};
constructor(progressId: string, message?: string, percentage?: number) {
super("progressUpdate");
this.body = {
progressId,
message,
percentage,
};
}
}
class EvaluationStartedEvent
extends Event
implements CodeQLDebugProtocol.EvaluationStartedEvent
{
public readonly event = "codeql-evaluation-started";
public readonly body: CodeQLDebugProtocol.EvaluationStartedEventBody;
constructor(id: string, outputDir: string) {
super("codeql-evaluation-started");
this.body = {
id,
outputDir,
};
}
}
class EvaluationCompletedEvent
extends Event
implements CodeQLDebugProtocol.EvaluationCompletedEvent
{
public readonly event = "codeql-evaluation-completed";
public readonly body: CodeQLDebugProtocol.EvaluationCompletedEventBody;
constructor(results: CoreQueryResults) {
super("codeql-evaluation-completed");
this.body = results;
}
}
export class QLDebugSession extends LoggingDebugSession implements Disposable {
private args: CodeQLDebugProtocol.LaunchRequestArguments | undefined =
undefined;
private tokenSource: CancellationTokenSource | undefined = undefined;
private queryRun: CoreQueryRun | undefined = undefined;
constructor(
private readonly queryStorageDir: string,
private readonly queryRunner: QueryRunner,
) {
super();
}
public dispose(): void {
this.cancelEvaluation();
}
protected dispatchRequest(request: DebugProtocol.Request): void {
super.dispatchRequest(request);
}
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
_args: DebugProtocol.InitializeRequestArguments,
): void {
response.body = response.body ?? {};
response.body.supportsStepBack = false;
response.body.supportsStepInTargetsRequest = false;
response.body.supportsRestartFrame = false;
response.body.supportsGotoTargetsRequest = false;
this.sendResponse(response);
this.sendEvent(new InitializedEvent());
}
protected configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments,
request?: DebugProtocol.Request,
): void {
super.configurationDoneRequest(response, args, request);
}
protected disconnectRequest(
response: DebugProtocol.DisconnectResponse,
_args: DebugProtocol.DisconnectArguments,
_request?: DebugProtocol.Request,
): void {
response.body = response.body ?? {};
// Neither of the args (`terminateDebuggee` and `restart`) matter for CodeQL.
this.sendResponse(response);
}
protected launchRequest(
response: DebugProtocol.LaunchResponse,
args: CodeQLDebugProtocol.LaunchRequestArguments,
_request?: DebugProtocol.Request,
): void {
void this.launch(response, args); //TODO: Cancelation?
}
protected cancelRequest(
response: DebugProtocol.CancelResponse,
args: DebugProtocol.CancelArguments,
_request?: DebugProtocol.Request,
): void {
if (args.progressId !== undefined) {
if (this.queryRun?.id === args.progressId) {
this.cancelEvaluation();
}
}
this.sendResponse(response);
}
protected threadsRequest(
response: DebugProtocol.ThreadsResponse,
request?: DebugProtocol.Request,
): void {
response.body = response.body ?? {};
response.body.threads = [
{
id: 1,
name: "Evaluation thread",
},
];
super.threadsRequest(response, request);
}
protected stackTraceRequest(
response: DebugProtocol.StackTraceResponse,
_args: DebugProtocol.StackTraceArguments,
_request?: DebugProtocol.Request,
): void {
response.body = response.body ?? {};
response.body.stackFrames = [];
super.stackTraceRequest(response, _args, _request);
}
private async launch(
response: DebugProtocol.LaunchResponse,
args: CodeQLDebugProtocol.LaunchRequestArguments,
): Promise<void> {
response.body = response.body ?? {};
this.args = args;
void this.evaluate(response);
}
private createLogger(): BaseLogger {
return {
log: async (message: string, _options: LogOptions): Promise<void> => {
this.sendEvent(new OutputEvent(message, "console"));
},
};
}
private async evaluate(
response: DebugProtocol.LaunchResponse,
): Promise<void> {
// Send the response immediately. We'll send a "stopped" message when the evaluation is complete.
this.sendResponse(response);
const args = this.args!;
this.tokenSource = new CancellationTokenSource();
try {
this.queryRun = this.queryRunner.createQueryRun(
args.database,
{
queryPath: args.query,
quickEvalPosition: undefined,
},
true,
args.additionalPacks,
this.queryStorageDir,
undefined,
undefined,
);
// Send the `EvaluationStarted` event first, to let the client known where the outputs are
// going to show up.
this.sendEvent(
new EvaluationStartedEvent(
this.queryRun.id,
this.queryRun.outputDir.querySaveDir,
),
);
const progressStart = new ProgressStartEvent(
this.queryRun.id,
"Running query",
undefined,
0,
);
progressStart.body.cancellable = true;
this.sendEvent(progressStart);
try {
const result = await this.queryRun.evaluate(
(p) => {
const progressUpdate = new ProgressUpdateEvent(
this.queryRun!.id,
p.message,
(p.step * 100) / p.maxStep,
);
this.sendEvent(progressUpdate);
},
this.tokenSource!.token,
this.createLogger(),
);
this.completeEvaluation(result);
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
this.completeEvaluation({
resultType: QueryResultType.OTHER_ERROR,
message,
evaluationTime: 0,
});
}
} finally {
this.disposeTokenSource();
}
}
private completeEvaluation(
result: CodeQLDebugProtocol.EvaluationCompletedEventBody,
): void {
// Report the end of the progress
this.sendEvent(new ProgressEndEvent(this.queryRun!.id));
// Report the evaluation result
this.sendEvent(new EvaluationCompletedEvent(result));
if (result.resultType !== QueryResultType.SUCCESS) {
// Report the result message as "important" output
const message = result.message ?? "Unknown error";
const outputEvent = new OutputEvent(message, "console");
this.sendEvent(outputEvent);
}
// Report the debugging session as terminated.
this.sendEvent(new TerminatedEvent());
// Report the debuggee as exited.
this.sendEvent(new ExitedEvent(result.resultType));
this.queryRun = undefined;
}
private disposeTokenSource(): void {
if (this.tokenSource !== undefined) {
this.tokenSource!.dispose();
this.tokenSource = undefined;
}
}
private cancelEvaluation(): void {
if (this.tokenSource !== undefined) {
this.tokenSource.cancel();
this.disposeTokenSource();
}
}
}

View File

@@ -0,0 +1,57 @@
import {
debug,
DebugAdapterDescriptor,
DebugAdapterDescriptorFactory,
DebugAdapterExecutable,
DebugAdapterInlineImplementation,
DebugAdapterServer,
DebugConfigurationProviderTriggerKind,
DebugSession,
ProviderResult,
} from "vscode";
import { DisposableObject } from "../pure/disposable-object";
import { QueryRunner } from "../queryRunner";
import { QLDebugConfigurationProvider } from "./debug-configuration";
import { QLDebugSession } from "./debug-session";
const useInlineImplementation = true;
export class QLDebugAdapterDescriptorFactory
extends DisposableObject
implements DebugAdapterDescriptorFactory
{
constructor(
private readonly queryStorageDir: string,
private readonly queryRunner: QueryRunner,
) {
super();
this.push(debug.registerDebugAdapterDescriptorFactory("codeql", this));
this.push(
debug.registerDebugConfigurationProvider(
"codeql",
new QLDebugConfigurationProvider(),
DebugConfigurationProviderTriggerKind.Dynamic,
),
);
this.push(debug.onDidStartDebugSession(this.handleOnDidStartDebugSession));
}
public createDebugAdapterDescriptor(
_session: DebugSession,
_executable: DebugAdapterExecutable | undefined,
): ProviderResult<DebugAdapterDescriptor> {
if (useInlineImplementation) {
return new DebugAdapterInlineImplementation(
new QLDebugSession(this.queryStorageDir, this.queryRunner),
);
} else {
return new DebugAdapterServer(2112);
}
}
private handleOnDidStartDebugSession(session: DebugSession): void {
const config = session.configuration;
void config;
}
}

View File

@@ -0,0 +1,167 @@
import {
DebugAdapterTracker,
DebugAdapterTrackerFactory,
DebugSession,
debug,
// window,
Uri,
CancellationTokenSource,
} from "vscode";
import { ResultsView } from "../interface";
import { WebviewReveal } from "../interface-utils";
import { DatabaseManager } from "../local-databases";
import { LocalQueries, LocalQueryRun } from "../local-queries";
import { DisposableObject } from "../pure/disposable-object";
import { CompletedLocalQueryInfo } from "../query-results";
import { CoreQueryResults } from "../queryRunner";
import { QueryOutputDir } from "../run-queries-shared";
import { QLResolvedDebugConfiguration } from "./debug-configuration";
import * as CodeQLDebugProtocol from "./debug-protocol";
class QLDebugAdapterTracker
extends DisposableObject
implements DebugAdapterTracker
{
private readonly configuration: QLResolvedDebugConfiguration;
private localQueryRun: LocalQueryRun | undefined;
/** The promise of the most recently queued deferred message handler. */
private lastDeferredMessageHandler: Promise<void> = Promise.resolve();
constructor(
private readonly session: DebugSession,
private readonly ui: DebuggerUI,
private readonly localQueries: LocalQueries,
private readonly dbm: DatabaseManager,
) {
super();
this.configuration = <QLResolvedDebugConfiguration>session.configuration;
}
public onDidSendMessage(
message: CodeQLDebugProtocol.AnyProtocolMessage,
): void {
if (message.type === "event") {
switch (message.event) {
case "codeql-evaluation-started":
this.queueMessageHandler(() =>
this.onEvaluationStarted(message.body),
);
break;
case "codeql-evaluation-completed":
this.queueMessageHandler(() =>
this.onEvaluationCompleted(message.body),
);
break;
case "output":
if (message.body.category === "console") {
void this.localQueryRun?.logger.log(message.body.output);
}
break;
}
}
}
public onWillStopSession(): void {
this.ui.onSessionClosed(this.session);
this.dispose();
}
private queueMessageHandler(handler: () => Promise<void>): void {
this.lastDeferredMessageHandler =
this.lastDeferredMessageHandler.finally(handler);
}
private async onEvaluationStarted(
body: CodeQLDebugProtocol.EvaluationStartedEventBody,
): Promise<void> {
const dbUri = Uri.file(this.configuration.database);
const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri);
// When cancellation is requested from the query history view, we just stop the debug session.
const tokenSource = new CancellationTokenSource();
tokenSource.token.onCancellationRequested(() =>
debug.stopDebugging(this.session),
);
this.localQueryRun = await this.localQueries.createLocalQueryRun(
{
queryPath: this.configuration.query,
quickEvalPosition: undefined,
quickEvalText: undefined,
},
dbItem,
new QueryOutputDir(body.outputDir),
tokenSource,
);
}
private async onEvaluationCompleted(
body: CodeQLDebugProtocol.EvaluationCompletedEventBody,
): Promise<void> {
if (this.localQueryRun !== undefined) {
const results: CoreQueryResults = body;
await this.localQueryRun.complete(results);
this.localQueryRun = undefined;
}
}
}
export class DebuggerUI
extends DisposableObject
implements DebugAdapterTrackerFactory
{
private readonly sessions = new Map<string, QLDebugAdapterTracker>();
constructor(
private readonly localQueryResultsView: ResultsView,
private readonly localQueries: LocalQueries,
private readonly dbm: DatabaseManager,
) {
super();
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
}
public createDebugAdapterTracker(
session: DebugSession,
): DebugAdapterTracker | undefined {
if (session.type === "codeql") {
const tracker = new QLDebugAdapterTracker(
session,
this,
this.localQueries,
this.dbm,
);
this.sessions.set(session.id, tracker);
return tracker;
} else {
return undefined;
}
}
public onSessionClosed(session: DebugSession): void {
this.sessions.delete(session.id);
}
private getTrackerForSession(
session: DebugSession,
): QLDebugAdapterTracker | undefined {
return this.sessions.get(session.id);
}
public get activeTracker(): QLDebugAdapterTracker | undefined {
const session = debug.activeDebugSession;
if (session === undefined) {
return undefined;
}
return this.getTrackerForSession(session);
}
public async showResultsForCompletedQuery(
query: CompletedLocalQueryInfo,
forceReveal: WebviewReveal,
): Promise<void> {
await this.localQueryResultsView.showResults(query, forceReveal, false);
}
}

View File

@@ -107,6 +107,7 @@ import { VariantAnalysisResultsManager } from "./variant-analysis/variant-analys
import { ExtensionApp } from "./common/vscode/vscode-app";
import { DbModule } from "./databases/db-module";
import { redactableError } from "./pure/errors";
import { QLDebugAdapterDescriptorFactory } from "./debugger/debugger-factory";
import { QueryHistoryDirs } from "./query-history/query-history-dirs";
import {
AllExtensionCommands,
@@ -120,6 +121,7 @@ import { getAstCfgCommands } from "./ast-cfg-commands";
import { getQueryEditorCommands } from "./query-editor";
import { App } from "./common/app";
import { registerCommandWithErrorHandling } from "./common/vscode/commands";
import { DebuggerUI } from "./debugger/debugger-ui";
/**
* extension.ts
@@ -860,6 +862,18 @@ async function activateWithInstalledDistribution(
);
ctx.subscriptions.push(localQueries);
void extLogger.log("Initializing debugger factory.");
const debuggerFactory = ctx.subscriptions.push(
new QLDebugAdapterDescriptorFactory(queryStorageDir, qs),
);
void debuggerFactory;
void extLogger.log("Initializing debugger UI.");
const debuggerUI = ctx.subscriptions.push(
new DebuggerUI(localQueryResultsView, localQueries, dbm),
);
void debuggerUI;
void extLogger.log("Initializing QLTest interface.");
const testExplorerExtension = extensions.getExtension<TestHub>(
testExplorerExtensionId,

View File

@@ -208,6 +208,7 @@ export class DatabaseUI extends DisposableObject {
public getCommands(): LocalDatabasesCommands {
return {
"codeQL.getCurrentDatabase": this.handleGetCurrentDatabase.bind(this),
"codeQL.chooseDatabaseFolder":
this.handleChooseDatabaseFolderFromPalette.bind(this),
"codeQL.chooseDatabaseArchive":
@@ -602,6 +603,10 @@ export class DatabaseUI extends DisposableObject {
);
}
private async handleGetCurrentDatabase(): Promise<string | undefined> {
return this.databaseManager.currentDatabaseItem?.databaseUri.fsPath;
}
private async handleSetCurrentDatabase(uri: Uri): Promise<void> {
return withProgress(
async (progress, token) => {

View File

@@ -611,12 +611,61 @@ export class DatabaseManager extends DisposableObject {
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,
displayName?: string,
isTutorialDatabase?: boolean,
): Promise<DatabaseItem> {
const databaseItem = await this.createDatabaseItem(uri, displayName);
return await this.addExistingDatabaseItem(
databaseItem,
progress,
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}.
*/
public async addExistingDatabaseItem(
databaseItem: DatabaseItem,
progress: ProgressCallback,
token: vscode.CancellationToken,
isTutorialDatabase?: boolean,
): Promise<DatabaseItem> {
const existingItem = this.findDatabaseItem(databaseItem.databaseUri);
if (existingItem !== undefined) {
return existingItem;
}
await this.addDatabaseItem(progress, token, 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.
@@ -637,14 +686,27 @@ export class DatabaseManager extends DisposableObject {
},
);
await this.addDatabaseItem(progress, token, databaseItem);
await this.addDatabaseSourceArchiveFolder(databaseItem);
return databaseItem;
}
if (isCodespacesTemplate() && !isTutorialDatabase) {
await this.createSkeletonPacks(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;
}
return databaseItem;
// 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) {