-
Notifications
You must be signed in to change notification settings - Fork 229
Expand file tree
/
Copy pathdebugger-ui.ts
More file actions
251 lines (226 loc) · 7.9 KB
/
debugger-ui.ts
File metadata and controls
251 lines (226 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import { basename } from "path";
import type {
DebugAdapterTracker,
DebugAdapterTrackerFactory,
DebugSession,
} from "vscode";
import { debug, Uri, CancellationTokenSource } from "vscode";
import type { DebuggerCommands } from "../common/commands";
import type { DatabaseManager } from "../databases/local-databases";
import { DisposableObject } from "../common/disposable-object";
import type { CoreQueryResult } from "../query-server";
import {
getQuickEvalContext,
saveBeforeStart,
validateQueryUri,
} from "../run-queries-shared";
import { QueryOutputDir } from "../local-queries/query-output-dir";
import type { QLResolvedDebugConfiguration } from "./debug-configuration";
import type {
AnyProtocolMessage,
EvaluationCompletedEvent,
EvaluationStartedEvent,
QuickEvalRequest,
} from "./debug-protocol";
import type { App } from "../common/app";
import type { LocalQueryRun, LocalQueries } from "../local-queries";
/**
* Listens to messages passing between VS Code and the debug adapter, so that we can supplement the
* UI.
*/
class QLDebugAdapterTracker
extends DisposableObject
implements DebugAdapterTracker
{
private readonly configuration: QLResolvedDebugConfiguration;
/** The `LocalQueryRun` of the current evaluation, if one is running. */
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: 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();
}
public async quickEval(): Promise<void> {
// Since we're not going through VS Code's launch path, we need to save dirty files ourselves.
await saveBeforeStart();
const args: QuickEvalRequest["arguments"] = {
quickEvalContext: await getQuickEvalContext(undefined, false),
};
await this.session.customRequest("codeql-quickeval", args);
}
/**
* Queues a message handler to be executed once all other pending message handlers have completed.
*
* The `onDidSendMessage()` function is synchronous, so it needs to return before any async
* handling of the msssage is completed. We can't just launch the message handler directly from
* `onDidSendMessage()`, though, because if the message handler's implementation blocks awaiting
* a promise, then another event might be received by `onDidSendMessage()` while the first message
* handler is still incomplete.
*
* To enforce sequential execution of event handlers, we queue each new handler as a `finally()`
* handler for the most recently queued message.
*/
private queueMessageHandler(handler: () => Promise<void>): void {
this.lastDeferredMessageHandler =
this.lastDeferredMessageHandler.finally(handler);
}
/** Updates the UI to track the currently executing query. */
private async onEvaluationStarted(
body: EvaluationStartedEvent["body"],
): Promise<void> {
const dbUri = Uri.file(this.configuration.database);
const dbItem = await this.dbm.createOrOpenDatabaseItem(dbUri, {
type: "debugger",
});
// 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,
quickEval: body.quickEvalContext,
},
dbItem,
new QueryOutputDir(body.outputDir),
tokenSource,
);
}
/** Update the UI after a query has finished evaluating. */
private async onEvaluationCompleted(
body: EvaluationCompletedEvent["body"],
): Promise<void> {
if (this.localQueryRun !== undefined) {
const results: CoreQueryResult = body;
await this.localQueryRun.complete(
{
results: new Map<string, CoreQueryResult>([
[this.configuration.query, results],
]),
},
(_) => {},
);
this.localQueryRun = undefined;
}
}
}
/** Service handling the UI for CodeQL debugging. */
export class DebuggerUI
extends DisposableObject
implements DebugAdapterTrackerFactory
{
private readonly sessions = new Map<string, QLDebugAdapterTracker>();
constructor(
private readonly app: App,
private readonly localQueries: LocalQueries,
private readonly dbm: DatabaseManager,
) {
super();
this.push(debug.registerDebugAdapterTrackerFactory("codeql", this));
}
public getCommands(): DebuggerCommands {
return {
"codeQL.debugQuery": this.debugQuery.bind(this),
"codeQL.debugQueryContextEditor": this.debugQuery.bind(this),
"codeQL.startDebuggingSelectionContextEditor":
this.startDebuggingSelection.bind(this),
"codeQL.startDebuggingSelection": this.startDebuggingSelection.bind(this),
"codeQL.continueDebuggingSelection":
this.continueDebuggingSelection.bind(this),
"codeQL.continueDebuggingSelectionContextEditor":
this.continueDebuggingSelection.bind(this),
};
}
public createDebugAdapterTracker(
session: DebugSession,
): DebugAdapterTracker | undefined {
if (session.type === "codeql") {
// The tracker will be disposed in its own `onWillStopSession` handler.
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 async debugQuery(uri: Uri | undefined): Promise<void> {
const queryPath =
uri !== undefined
? validateQueryUri(uri, false)
: await this.localQueries.getCurrentQuery(false);
// Start debugging with a default configuration that just specifies the query path.
await debug.startDebugging(undefined, {
name: basename(queryPath),
type: "codeql",
request: "launch",
query: queryPath,
});
}
private async startDebuggingSelection(): Promise<void> {
// Launch the currently selected debug configuration, but specifying QuickEval mode.
await this.app.commands.execute("workbench.action.debug.start", {
config: {
quickEval: true,
},
});
}
private async continueDebuggingSelection(): Promise<void> {
const activeTracker = this.activeTracker;
if (activeTracker === undefined) {
throw new Error("No CodeQL debug session is active.");
}
await activeTracker.quickEval();
}
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);
}
}