Skip to content

Commit 2da6895

Browse files
author
Dave Bartolomeo
committed
Rework debug query evaluation code to avoid lots of state-dependent properties
1 parent a0a3af2 commit 2da6895

1 file changed

Lines changed: 146 additions & 131 deletions

File tree

extensions/ql-vscode/src/debugger/debug-session.ts

Lines changed: 146 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,11 @@ import { Disposable } from "vscode";
1414
import { CancellationTokenSource } from "vscode-jsonrpc";
1515
import { BaseLogger, LogOptions, queryServerLogger } from "../common";
1616
import { QueryResultType } from "../pure/new-messages";
17-
import {
18-
CoreCompletedQuery,
19-
CoreQueryResults,
20-
CoreQueryRun,
21-
QueryRunner,
22-
} from "../queryRunner";
17+
import { CoreQueryResults, CoreQueryRun, QueryRunner } from "../queryRunner";
2318
import * as CodeQLProtocol from "./debug-protocol";
2419
import { QuickEvalContext } from "../run-queries-shared";
20+
import { getErrorMessage } from "../pure/helpers-pure";
21+
import { DisposableObject } from "../pure/disposable-object";
2522

2623
// More complete implementations of `Event` for certain events, because the classes from
2724
// `@vscode/debugadapter` make it more difficult to provide some of the message values.
@@ -131,21 +128,129 @@ const QUERY_THREAD_ID = 1;
131128
/** The user-visible name of the query evaluation thread. */
132129
const QUERY_THREAD_NAME = "Evaluation thread";
133130

131+
/**
132+
* An active query evaluation within a debug session.
133+
*
134+
* This class encapsulates the state and resources associated with the running query, to avoid
135+
* having multiple properties within `QLDebugSession` that are only defined during query evaluation.
136+
*/
137+
class RunningQuery extends DisposableObject {
138+
private readonly tokenSource = this.push(new CancellationTokenSource());
139+
public readonly queryRun: CoreQueryRun;
140+
141+
public constructor(
142+
queryRunner: QueryRunner,
143+
config: CodeQLProtocol.LaunchConfig,
144+
private readonly quickEvalContext: QuickEvalContext | undefined,
145+
queryStorageDir: string,
146+
private readonly logger: BaseLogger,
147+
private readonly sendEvent: (event: Event) => void,
148+
) {
149+
super();
150+
151+
// Create the query run, which will give us some information about the query even before the
152+
// evaluation has completed.
153+
this.queryRun = queryRunner.createQueryRun(
154+
config.database,
155+
{
156+
queryPath: config.query,
157+
quickEvalPosition: quickEvalContext?.quickEvalPosition,
158+
},
159+
true,
160+
config.additionalPacks,
161+
config.extensionPacks,
162+
queryStorageDir,
163+
undefined,
164+
undefined,
165+
);
166+
}
167+
168+
public get id(): string {
169+
return this.queryRun.id;
170+
}
171+
172+
/**
173+
* Evaluates the query, firing progress events along the way. The evaluation can be cancelled by
174+
* calling `cancel()`.
175+
*
176+
* This function does not throw exceptions to report query evaluation failure. It just returns an
177+
* evaluation result with a failure message instead.
178+
*/
179+
public async evaluate(): Promise<
180+
CodeQLProtocol.EvaluationCompletedEvent["body"]
181+
> {
182+
// Send the `EvaluationStarted` event first, to let the client known where the outputs are
183+
// going to show up.
184+
this.sendEvent(
185+
new EvaluationStartedEvent(
186+
this.queryRun.id,
187+
this.queryRun.outputDir.querySaveDir,
188+
this.quickEvalContext,
189+
),
190+
);
191+
192+
try {
193+
// Report progress via the debugger protocol.
194+
const progressStart = new ProgressStartEvent(
195+
this.queryRun.id,
196+
"Running query",
197+
undefined,
198+
0,
199+
);
200+
progressStart.body.cancellable = true;
201+
this.sendEvent(progressStart);
202+
try {
203+
return await this.queryRun.evaluate(
204+
(p) => {
205+
const progressUpdate = new ProgressUpdateEvent(
206+
this.queryRun.id,
207+
p.message,
208+
(p.step * 100) / p.maxStep,
209+
);
210+
this.sendEvent(progressUpdate);
211+
},
212+
this.tokenSource.token,
213+
this.logger,
214+
);
215+
} finally {
216+
this.sendEvent(new ProgressEndEvent(this.queryRun.id));
217+
}
218+
} catch (e) {
219+
const message = getErrorMessage(e);
220+
return {
221+
resultType: QueryResultType.OTHER_ERROR,
222+
message,
223+
evaluationTime: 0,
224+
};
225+
}
226+
}
227+
228+
/**
229+
* Attempts to cancel the running evaluation.
230+
*/
231+
public cancel(): void {
232+
this.tokenSource.cancel();
233+
}
234+
}
235+
134236
/**
135237
* An in-process implementation of the debug adapter for CodeQL queries.
136238
*
137239
* For now, this is pretty much just a wrapper around the query server.
138240
*/
139241
export class QLDebugSession extends LoggingDebugSession implements Disposable {
242+
/** A `BaseLogger` that sends output to the debug console. */
243+
private readonly logger: BaseLogger = {
244+
log: async (message: string, _options: LogOptions): Promise<void> => {
245+
this.sendEvent(new OutputEvent(message, "console"));
246+
},
247+
};
140248
private state: State = "uninitialized";
141249
private terminateOnComplete = false;
142250
private args: CodeQLProtocol.LaunchRequest["arguments"] | undefined =
143251
undefined;
144-
private tokenSource: CancellationTokenSource | undefined = undefined;
145-
private queryRun: CoreQueryRun | undefined = undefined;
146-
private lastResult:
147-
| CodeQLProtocol.EvaluationCompletedEvent["body"]
148-
| undefined = undefined;
252+
private runningQuery: RunningQuery | undefined = undefined;
253+
private lastResultType: QueryResultType = QueryResultType.CANCELLATION;
149254

150255
constructor(
151256
private readonly queryStorageDir: string,
@@ -155,7 +260,9 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
155260
}
156261

157262
public dispose(): void {
158-
this.cancelEvaluation();
263+
if (this.runningQuery !== undefined) {
264+
this.runningQuery.cancel();
265+
}
159266
}
160267

161268
protected dispatchRequest(request: Protocol.Request): void {
@@ -230,19 +337,11 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
230337
}
231338

232339
private terminateOrDisconnect(response: Protocol.Response): void {
233-
switch (this.state) {
234-
case "running":
235-
this.terminateOnComplete = true;
236-
this.cancelEvaluation();
237-
break;
238-
239-
case "stopped":
240-
this.terminateAndExit();
241-
break;
242-
243-
default:
244-
// Ignore
245-
break;
340+
if (this.runningQuery !== undefined) {
341+
this.terminateOnComplete = true;
342+
this.runningQuery.cancel();
343+
} else if (this.state === "stopped") {
344+
this.terminateAndExit();
246345
}
247346

248347
this.sendResponse(response);
@@ -349,18 +448,11 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
349448
args: Protocol.CancelArguments,
350449
_request?: Protocol.Request,
351450
): void {
352-
switch (this.state) {
353-
case "running":
354-
if (args.progressId !== undefined) {
355-
if (this.queryRun!.id === args.progressId) {
356-
this.cancelEvaluation();
357-
}
358-
}
359-
break;
360-
361-
default:
362-
// Ignore;
363-
break;
451+
if (
452+
args.progressId !== undefined &&
453+
this.runningQuery?.id === args.progressId
454+
) {
455+
this.runningQuery.cancel();
364456
}
365457

366458
this.sendResponse(response);
@@ -436,15 +528,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
436528
}
437529
}
438530

439-
/** Creates a `BaseLogger` that sends output to the debug console. */
440-
private createLogger(): BaseLogger {
441-
return {
442-
log: async (message: string, _options: LogOptions): Promise<void> => {
443-
this.sendEvent(new OutputEvent(message, "console"));
444-
},
445-
};
446-
}
447-
448531
/**
449532
* Runs the query or quickeval, and notifies the debugger client when the evaluation completes.
450533
*
@@ -456,75 +539,23 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
456539
): Promise<void> {
457540
const args = this.args!;
458541

459-
this.tokenSource = new CancellationTokenSource();
460-
try {
461-
// Create the query run, which will give us some information about the query even before the
462-
// evaluation has completed.
463-
this.queryRun = this.queryRunner.createQueryRun(
464-
args.database,
465-
{
466-
queryPath: args.query,
467-
quickEvalPosition: quickEvalContext?.quickEvalPosition,
468-
},
469-
true,
470-
args.additionalPacks,
471-
args.extensionPacks,
472-
this.queryStorageDir,
473-
undefined,
474-
undefined,
475-
);
476-
477-
this.state = "running";
478-
479-
// Send the `EvaluationStarted` event first, to let the client known where the outputs are
480-
// going to show up.
481-
this.sendEvent(
482-
new EvaluationStartedEvent(
483-
this.queryRun.id,
484-
this.queryRun.outputDir.querySaveDir,
485-
quickEvalContext,
486-
),
487-
);
542+
const runningQuery = new RunningQuery(
543+
this.queryRunner,
544+
args,
545+
quickEvalContext,
546+
this.queryStorageDir,
547+
this.logger,
548+
(event) => this.sendEvent(event),
549+
);
550+
this.runningQuery = runningQuery;
551+
this.state = "running";
488552

489-
try {
490-
// Report progress via the debugger protocol.
491-
const progressStart = new ProgressStartEvent(
492-
this.queryRun.id,
493-
"Running query",
494-
undefined,
495-
0,
496-
);
497-
progressStart.body.cancellable = true;
498-
this.sendEvent(progressStart);
499-
let result: CoreCompletedQuery;
500-
try {
501-
result = await this.queryRun.evaluate(
502-
(p) => {
503-
const progressUpdate = new ProgressUpdateEvent(
504-
this.queryRun!.id,
505-
p.message,
506-
(p.step * 100) / p.maxStep,
507-
);
508-
this.sendEvent(progressUpdate);
509-
},
510-
this.tokenSource!.token,
511-
this.createLogger(),
512-
);
513-
} finally {
514-
// Report the end of the progress
515-
this.sendEvent(new ProgressEndEvent(this.queryRun!.id));
516-
}
517-
this.completeEvaluation(result);
518-
} catch (e) {
519-
const message = e instanceof Error ? e.message : "Unknown error";
520-
this.completeEvaluation({
521-
resultType: QueryResultType.OTHER_ERROR,
522-
message,
523-
evaluationTime: 0,
524-
});
525-
}
553+
try {
554+
const result = await runningQuery.evaluate();
555+
this.completeEvaluation(result);
526556
} finally {
527-
this.disposeTokenSource();
557+
this.runningQuery = undefined;
558+
runningQuery.dispose();
528559
}
529560
}
530561

@@ -534,7 +565,7 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
534565
private completeEvaluation(
535566
result: CodeQLProtocol.EvaluationCompletedEvent["body"],
536567
): void {
537-
this.lastResult = result;
568+
this.lastResultType = result.resultType;
538569

539570
// Report the evaluation result
540571
this.sendEvent(new EvaluationCompletedEvent(result));
@@ -546,8 +577,6 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
546577
}
547578

548579
this.reportStopped();
549-
550-
this.queryRun = undefined;
551580
}
552581

553582
private reportStopped(): void {
@@ -566,22 +595,8 @@ export class QLDebugSession extends LoggingDebugSession implements Disposable {
566595
this.sendEvent(new TerminatedEvent());
567596

568597
// Report the debuggee as exited.
569-
this.sendEvent(new ExitedEvent(this.lastResult!.resultType));
598+
this.sendEvent(new ExitedEvent(this.lastResultType));
570599

571600
this.state = "terminated";
572601
}
573-
574-
private disposeTokenSource(): void {
575-
if (this.tokenSource !== undefined) {
576-
this.tokenSource!.dispose();
577-
this.tokenSource = undefined;
578-
}
579-
}
580-
581-
private cancelEvaluation(): void {
582-
if (this.tokenSource !== undefined) {
583-
this.tokenSource.cancel();
584-
this.disposeTokenSource();
585-
}
586-
}
587602
}

0 commit comments

Comments
 (0)