Implement CodeQL debug adapter
This commit is contained in:
31
extensions/ql-vscode/package-lock.json
generated
31
extensions/ql-vscode/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -180,6 +180,7 @@ export type LocalDatabasesCommands = {
|
||||
|
||||
// Internal commands
|
||||
"codeQLDatabases.removeOrphanedDatabases": () => Promise<void>;
|
||||
"codeQL.getCurrentDatabase": () => Promise<string | undefined>;
|
||||
};
|
||||
|
||||
// Commands tied to variant analysis
|
||||
|
||||
78
extensions/ql-vscode/src/debugger/debug-configuration.ts
Normal file
78
extensions/ql-vscode/src/debugger/debug-configuration.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
69
extensions/ql-vscode/src/debugger/debug-protocol.ts
Normal file
69
extensions/ql-vscode/src/debugger/debug-protocol.ts
Normal 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[];
|
||||
}
|
||||
324
extensions/ql-vscode/src/debugger/debug-session.ts
Normal file
324
extensions/ql-vscode/src/debugger/debug-session.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
extensions/ql-vscode/src/debugger/debugger-factory.ts
Normal file
57
extensions/ql-vscode/src/debugger/debugger-factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
167
extensions/ql-vscode/src/debugger/debugger-ui.ts
Normal file
167
extensions/ql-vscode/src/debugger/debugger-ui.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user