@ian-pascoe/pi-dap 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1231 @@
1
+ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import type { DebugProtocol } from "@vscode/debugprotocol";
5
+ import { type Static, Type, type TSchema } from "typebox";
6
+ import { Value } from "typebox/value";
7
+ import {
8
+ DapProtocolClient,
9
+ DapProtocolClientError,
10
+ type DapProtocolRequestOptions,
11
+ type DapProtocolTransport,
12
+ type DapReverseRequestResult,
13
+ } from "./dap-protocol-client.js";
14
+ import {
15
+ createDapOutputBuffer,
16
+ type DapOutputBuffer,
17
+ type DapSessionFiles,
18
+ } from "./dap-session-files.js";
19
+ import type {
20
+ DapAdapterDefinition,
21
+ DapLaunchProfile,
22
+ ResolvedDapSettings,
23
+ } from "./pi-dap-settings.js";
24
+
25
+ const DapAdapterProtocolIdSchema = Type.String({ minLength: 1 });
26
+ const DapCapabilitiesSchema = Type.Object(
27
+ {
28
+ supportsConfigurationDoneRequest: Type.Optional(Type.Boolean()),
29
+ supportsTerminateRequest: Type.Optional(Type.Boolean()),
30
+ },
31
+ { additionalProperties: true },
32
+ );
33
+ const DapOutputEventBodySchema = Type.Object(
34
+ { output: Type.String() },
35
+ { additionalProperties: true },
36
+ );
37
+ const DapStoppedEventBodySchema = Type.Object(
38
+ {
39
+ reason: Type.String(),
40
+ threadId: Type.Optional(Type.Integer()),
41
+ },
42
+ { additionalProperties: true },
43
+ );
44
+ const DapContinuedEventBodySchema = Type.Object(
45
+ { threadId: Type.Optional(Type.Integer()) },
46
+ { additionalProperties: true },
47
+ );
48
+ const DapExitedEventBodySchema = Type.Object(
49
+ { exitCode: Type.Integer() },
50
+ { additionalProperties: true },
51
+ );
52
+ const DapSourceSchema = Type.Object(
53
+ {
54
+ name: Type.Optional(Type.String()),
55
+ path: Type.Optional(Type.String()),
56
+ },
57
+ { additionalProperties: true },
58
+ );
59
+ const DapBreakpointSchema = Type.Object(
60
+ {
61
+ id: Type.Optional(Type.Integer()),
62
+ verified: Type.Boolean(),
63
+ message: Type.Optional(Type.String()),
64
+ line: Type.Optional(Type.Integer()),
65
+ source: Type.Optional(DapSourceSchema),
66
+ },
67
+ { additionalProperties: true },
68
+ );
69
+ const DapSetBreakpointsBodySchema = Type.Object(
70
+ { breakpoints: Type.Array(DapBreakpointSchema) },
71
+ { additionalProperties: true },
72
+ );
73
+ const DapThreadSchema = Type.Object(
74
+ { id: Type.Integer(), name: Type.String() },
75
+ { additionalProperties: true },
76
+ );
77
+ const DapThreadsBodySchema = Type.Object(
78
+ { threads: Type.Array(DapThreadSchema) },
79
+ { additionalProperties: true },
80
+ );
81
+ const DapStackFrameSchema = Type.Object(
82
+ {
83
+ id: Type.Integer(),
84
+ name: Type.String(),
85
+ line: Type.Integer(),
86
+ column: Type.Integer(),
87
+ source: Type.Optional(DapSourceSchema),
88
+ },
89
+ { additionalProperties: true },
90
+ );
91
+ const DapStackTraceBodySchema = Type.Object(
92
+ {
93
+ stackFrames: Type.Array(DapStackFrameSchema),
94
+ totalFrames: Type.Optional(Type.Integer({ minimum: 0 })),
95
+ },
96
+ { additionalProperties: true },
97
+ );
98
+ const DapScopeSchema = Type.Object(
99
+ {
100
+ name: Type.String(),
101
+ variablesReference: Type.Integer({ minimum: 0 }),
102
+ expensive: Type.Boolean(),
103
+ namedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
104
+ indexedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
105
+ },
106
+ { additionalProperties: true },
107
+ );
108
+ const DapScopesBodySchema = Type.Object(
109
+ { scopes: Type.Array(DapScopeSchema) },
110
+ { additionalProperties: true },
111
+ );
112
+ const DapVariableSchema = Type.Object(
113
+ {
114
+ name: Type.String(),
115
+ value: Type.String(),
116
+ variablesReference: Type.Integer({ minimum: 0 }),
117
+ type: Type.Optional(Type.String()),
118
+ evaluateName: Type.Optional(Type.String()),
119
+ namedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
120
+ indexedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
121
+ memoryReference: Type.Optional(Type.String()),
122
+ },
123
+ { additionalProperties: true },
124
+ );
125
+ const DapVariablesBodySchema = Type.Object(
126
+ { variables: Type.Array(DapVariableSchema) },
127
+ { additionalProperties: true },
128
+ );
129
+ const DapEvaluateBodySchema = Type.Object(
130
+ {
131
+ result: Type.String(),
132
+ variablesReference: Type.Integer({ minimum: 0 }),
133
+ type: Type.Optional(Type.String()),
134
+ namedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
135
+ indexedVariables: Type.Optional(Type.Integer({ minimum: 0 })),
136
+ memoryReference: Type.Optional(Type.String()),
137
+ },
138
+ { additionalProperties: true },
139
+ );
140
+ const RunInTerminalArgumentsSchema = Type.Object(
141
+ {
142
+ args: Type.Array(Type.String(), { minItems: 1 }),
143
+ cwd: Type.String({ minLength: 1 }),
144
+ env: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Null()]))),
145
+ argsCanBeInterpretedByShell: Type.Optional(Type.Boolean()),
146
+ },
147
+ { additionalProperties: true },
148
+ );
149
+ const JsDebugPrimaryTargetArgumentsSchema = Type.Object(
150
+ {
151
+ request: Type.Literal("launch"),
152
+ configuration: Type.Object(
153
+ {
154
+ type: Type.Literal("pwa-node"),
155
+ __pendingTargetId: Type.String({ minLength: 1 }),
156
+ },
157
+ { additionalProperties: true },
158
+ ),
159
+ },
160
+ { additionalProperties: true },
161
+ );
162
+
163
+ /** Classified configuration, state, protocol, or Debug Adapter failure. */
164
+ export class DapSessionError extends Error {
165
+ readonly _tag = "DapSessionError" as const;
166
+
167
+ /** Construct a stable Debug Session failure for the Pi tool boundary. */
168
+ constructor(
169
+ readonly kind: "adapter" | "configuration" | "protocol" | "state",
170
+ message: string,
171
+ options?: ErrorOptions,
172
+ ) {
173
+ super(`DAP Session: ${message}`, options);
174
+ }
175
+ }
176
+
177
+ /** Optional Launch Profile overrides supplied by one launch operation. */
178
+ export interface DapLaunchInput {
179
+ readonly profile?: string;
180
+ readonly program?: string;
181
+ readonly args?: readonly string[];
182
+ readonly cwd?: string;
183
+ }
184
+
185
+ /** One desired source Breakpoint, with one-based line numbering. */
186
+ export interface DapDesiredBreakpoint {
187
+ readonly line: number;
188
+ readonly condition?: string;
189
+ }
190
+
191
+ /** Complete replacement of Desired Breakpoints for one source file. */
192
+ export interface DapSetBreakpointsInput {
193
+ readonly filePath: string;
194
+ readonly breakpoints: readonly DapDesiredBreakpoint[];
195
+ }
196
+
197
+ /** Paged Stack Frame request, defaulting to the stopped thread. */
198
+ export interface DapStackInput {
199
+ readonly threadId?: number;
200
+ readonly start?: number;
201
+ readonly count?: number;
202
+ }
203
+
204
+ /** Paged variables request by Stack Frame or child variables reference. */
205
+ export type DapVariablesInput =
206
+ | {
207
+ readonly frameId: number;
208
+ readonly variablesReference?: never;
209
+ readonly start?: number;
210
+ readonly count?: number;
211
+ }
212
+ | {
213
+ readonly frameId?: never;
214
+ readonly variablesReference: number;
215
+ readonly start?: number;
216
+ readonly count?: number;
217
+ };
218
+
219
+ /** Expression evaluation request, defaulting to the top Stack Frame. */
220
+ export interface DapEvaluateInput {
221
+ readonly expression: string;
222
+ readonly frameId?: number;
223
+ }
224
+
225
+ /** Public exhaustive Debug Session lifecycle snapshot. */
226
+ export type DapSessionSnapshot =
227
+ | { readonly state: "idle" }
228
+ | {
229
+ readonly state: "launching" | "running";
230
+ readonly adapterId: string;
231
+ readonly profileId: string;
232
+ }
233
+ | {
234
+ readonly state: "stopped";
235
+ readonly adapterId: string;
236
+ readonly profileId: string;
237
+ readonly stopReason: string;
238
+ readonly threadId?: number;
239
+ }
240
+ | {
241
+ readonly state: "terminated";
242
+ readonly adapterId: string;
243
+ readonly profileId: string;
244
+ readonly exitCode?: number;
245
+ readonly terminationReason?: string;
246
+ };
247
+
248
+ /** Desired Breakpoints retained for one source file across launches. */
249
+ export interface DapDesiredBreakpointFile {
250
+ readonly filePath: string;
251
+ readonly breakpoints: readonly DapDesiredBreakpoint[];
252
+ }
253
+
254
+ /** Variables fetched for one Stack Frame scope. */
255
+ export interface DapVariableGroup {
256
+ readonly scope: DebugProtocol.Scope;
257
+ readonly variables: readonly DebugProtocol.Variable[];
258
+ }
259
+
260
+ /** Successful Debug Session operation including unread Debuggee output. */
261
+ export interface DapSessionResult {
262
+ readonly snapshot: DapSessionSnapshot;
263
+ readonly output: string;
264
+ readonly discardedOutputBytes: number;
265
+ readonly desiredBreakpoints: readonly DapDesiredBreakpointFile[];
266
+ readonly breakpoints?: readonly DebugProtocol.Breakpoint[];
267
+ readonly stackFrames?: readonly DebugProtocol.StackFrame[];
268
+ readonly totalFrames?: number;
269
+ readonly variableGroups?: readonly DapVariableGroup[];
270
+ readonly variables?: readonly DebugProtocol.Variable[];
271
+ readonly evaluation?: DebugProtocol.EvaluateResponse["body"];
272
+ }
273
+
274
+ /** Construction values owned for one conversation-level Debug Session controller. */
275
+ export interface DapSessionOptions {
276
+ readonly cwd: string;
277
+ readonly settings: ResolvedDapSettings;
278
+ readonly sessionFiles: DapSessionFiles;
279
+ /** Observe lifecycle snapshots synchronously without gaining protocol authority. */
280
+ readonly onSnapshotChange?: (snapshot: DapSessionSnapshot) => void;
281
+ /** Observe an asynchronous adapter or protocol failure not caused by an operation request. */
282
+ readonly onUnexpectedFailure?: (error: Error) => void;
283
+ }
284
+
285
+ type DapLaunchArgumentValue =
286
+ | null
287
+ | boolean
288
+ | number
289
+ | string
290
+ | readonly DapLaunchArgumentValue[]
291
+ | { readonly [key: string]: DapLaunchArgumentValue };
292
+
293
+ interface ActiveDapSession {
294
+ readonly adapter: DapAdapterDefinition;
295
+ readonly profile: DapLaunchProfile;
296
+ readonly rootClient: DapProtocolClient;
297
+ client: DapProtocolClient;
298
+ targetClient?: DapProtocolClient;
299
+ targetChannelStarted: boolean;
300
+ readonly debuggeeProcesses: Set<ChildProcessWithoutNullStreams>;
301
+ readonly unsubscribeEvents: Set<() => void>;
302
+ capabilities: Static<typeof DapCapabilitiesSchema>;
303
+ phase: "launching" | "running" | "stopped";
304
+ stopReason: string | undefined;
305
+ threadId: number | undefined;
306
+ exitCode: number | undefined;
307
+ cleanupPromise?: Promise<void>;
308
+ stopping: boolean;
309
+ }
310
+
311
+ interface TerminatedDapSessionState {
312
+ readonly kind: "terminated";
313
+ readonly adapterId: string;
314
+ readonly profileId: string;
315
+ readonly exitCode?: number;
316
+ readonly terminationReason?: string;
317
+ readonly cleanupPromise: Promise<void>;
318
+ }
319
+
320
+ type InternalDapSessionState =
321
+ | { readonly kind: "idle" }
322
+ | { readonly kind: "active"; readonly active: ActiveDapSession }
323
+ | TerminatedDapSessionState;
324
+
325
+ interface ExecutionWait {
326
+ readonly promise: Promise<"cancelled" | "timeout" | "transition">;
327
+ cancel(): void;
328
+ }
329
+
330
+ type DapLaunchResponseOutcome =
331
+ | { readonly kind: "success" }
332
+ | { readonly kind: "failure"; readonly error: Error };
333
+
334
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- This is the owning runtime parser for untrusted DAP response and event bodies.
335
+ function parseDapBody<T extends TSchema>(schema: T, value: unknown, operation: string): Static<T> {
336
+ if (Value.Check(schema, value)) return value;
337
+ const issue = Value.Errors(schema, value)[0];
338
+ throw new DapSessionError(
339
+ "protocol",
340
+ `${operation} returned an invalid body${issue?.instancePath === undefined ? "" : ` at ${issue.instancePath || "/"}`}`,
341
+ );
342
+ }
343
+
344
+ function protocolTransport(adapter: DapAdapterDefinition): DapProtocolTransport {
345
+ return adapter.transport.type === "stdio" ? "stdio" : adapter.transport;
346
+ }
347
+
348
+ function supportsJsDebugPrimaryTarget(
349
+ adapter: DapAdapterDefinition,
350
+ profile: DapLaunchProfile,
351
+ ): boolean {
352
+ return adapter.transport.type === "tcp" && profile.arguments.type === "pwa-node";
353
+ }
354
+
355
+ function isProtocolCancellation(error: Error): boolean {
356
+ return error instanceof DapProtocolClientError && error.kind === "cancelled";
357
+ }
358
+
359
+ function dapRequestOptions(
360
+ signal: AbortSignal | undefined,
361
+ timeoutMs?: number,
362
+ ): DapProtocolRequestOptions {
363
+ if (signal === undefined && timeoutMs === undefined) return {};
364
+ if (signal === undefined) return { timeoutMs };
365
+ if (timeoutMs === undefined) return { signal };
366
+ return { signal, timeoutMs };
367
+ }
368
+
369
+ function terminatedDapSessionState(
370
+ active: ActiveDapSession,
371
+ cleanupPromise: Promise<void>,
372
+ terminationReason: string,
373
+ ): TerminatedDapSessionState {
374
+ if (active.exitCode !== undefined && terminationReason.length > 0) {
375
+ return {
376
+ kind: "terminated",
377
+ adapterId: active.adapter.id,
378
+ profileId: active.profile.id,
379
+ cleanupPromise,
380
+ exitCode: active.exitCode,
381
+ terminationReason,
382
+ };
383
+ }
384
+ if (active.exitCode !== undefined) {
385
+ return {
386
+ kind: "terminated",
387
+ adapterId: active.adapter.id,
388
+ profileId: active.profile.id,
389
+ cleanupPromise,
390
+ exitCode: active.exitCode,
391
+ };
392
+ }
393
+ if (terminationReason.length > 0) {
394
+ return {
395
+ kind: "terminated",
396
+ adapterId: active.adapter.id,
397
+ profileId: active.profile.id,
398
+ cleanupPromise,
399
+ terminationReason,
400
+ };
401
+ }
402
+ return {
403
+ kind: "terminated",
404
+ adapterId: active.adapter.id,
405
+ profileId: active.profile.id,
406
+ cleanupPromise,
407
+ };
408
+ }
409
+
410
+ function isProcessAlive(pid: number): boolean {
411
+ try {
412
+ process.kill(pid, 0);
413
+ return true;
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
418
+
419
+ async function stopOwnedDebuggeeProcess(
420
+ child: ChildProcessWithoutNullStreams,
421
+ shutdownMs: number,
422
+ ): Promise<void> {
423
+ const pid = child.pid;
424
+ if (pid === undefined || !isProcessAlive(pid)) return;
425
+ const processId = process.platform === "linux" ? -pid : pid;
426
+ try {
427
+ process.kill(processId, "SIGTERM");
428
+ } catch {
429
+ return;
430
+ }
431
+ const deadline = Date.now() + Math.max(1, Math.floor(shutdownMs / 2));
432
+ while (Date.now() < deadline && isProcessAlive(pid)) await delay(10);
433
+ if (!isProcessAlive(pid)) return;
434
+ try {
435
+ process.kill(processId, "SIGKILL");
436
+ } catch {
437
+ // The Debuggee exited between the liveness check and the signal.
438
+ }
439
+ }
440
+
441
+ /** Own one configured Debug Session at a time and retain Desired Breakpoints across launches. */
442
+ export class DapSession {
443
+ private readonly output: DapOutputBuffer = createDapOutputBuffer();
444
+ private readonly desiredBreakpoints = new Map<string, readonly DapDesiredBreakpoint[]>();
445
+ private readonly executionWaiters = new Set<() => void>();
446
+ private state: InternalDapSessionState = { kind: "idle" };
447
+ private shutdownPromise: Promise<void> | undefined;
448
+
449
+ /** Construct an inert Debug Session controller; launch starts the first Debug Adapter. */
450
+ constructor(private readonly options: DapSessionOptions) {}
451
+
452
+ /** Launch one configured Launch Profile and wait for its first stop, exit, cancellation, or execution timeout. */
453
+ async launch(input: DapLaunchInput = {}, signal?: AbortSignal): Promise<DapSessionResult> {
454
+ if (this.state.kind === "active") {
455
+ throw new DapSessionError("state", "launch requires no active Debug Session");
456
+ }
457
+ if (this.state.kind === "terminated") await this.state.cleanupPromise;
458
+ this.output.drain();
459
+
460
+ const profile = this.resolveLaunchProfile(input.profile);
461
+ const adapter = this.options.settings.adapters.get(profile.adapterId);
462
+ if (adapter === undefined) {
463
+ throw new DapSessionError(
464
+ "configuration",
465
+ `Launch Profile ${profile.id} references unavailable Adapter Definition ${profile.adapterId}`,
466
+ );
467
+ }
468
+ const launchArguments: { [key: string]: DapLaunchArgumentValue } = structuredClone(
469
+ profile.arguments,
470
+ );
471
+ const adapterProtocolId = Value.Check(DapAdapterProtocolIdSchema, profile.arguments.type)
472
+ ? profile.arguments.type
473
+ : adapter.id;
474
+ if (input.program !== undefined)
475
+ launchArguments.program = resolve(this.options.cwd, input.program);
476
+ if (input.args !== undefined) launchArguments.args = [...input.args];
477
+ if (input.cwd !== undefined) launchArguments.cwd = resolve(this.options.cwd, input.cwd);
478
+
479
+ const debuggeeProcesses = new Set<ChildProcessWithoutNullStreams>();
480
+ const stderrPath = await this.options.sessionFiles.getAdapterStderrPath(adapter.id);
481
+ let active: ActiveDapSession | undefined;
482
+ try {
483
+ const client = await DapProtocolClient.start({
484
+ adapterId: adapter.id,
485
+ cwd: this.options.cwd,
486
+ command: adapter.command,
487
+ args: adapter.args,
488
+ environment: adapter.environment,
489
+ transport: protocolTransport(adapter),
490
+ timeouts: {
491
+ startupMs: this.options.settings.timeouts.startupMs,
492
+ requestMs: this.options.settings.timeouts.requestMs,
493
+ shutdownMs: this.options.settings.timeouts.shutdownMs,
494
+ },
495
+ stderrPath,
496
+ startupSignal: signal,
497
+ onReverseRequest: (request) =>
498
+ this.handleReverseRequest(request, debuggeeProcesses, () => active, signal),
499
+ onFailure: (error) => {
500
+ if (active !== undefined) this.handleAdapterFailure(active, error);
501
+ },
502
+ });
503
+ const startedActive: ActiveDapSession = {
504
+ adapter,
505
+ profile,
506
+ rootClient: client,
507
+ client,
508
+ debuggeeProcesses,
509
+ targetChannelStarted: false,
510
+ unsubscribeEvents: new Set(),
511
+ capabilities: {},
512
+ phase: "launching",
513
+ stopReason: undefined,
514
+ threadId: undefined,
515
+ exitCode: undefined,
516
+ stopping: false,
517
+ };
518
+ active = startedActive;
519
+ this.state = { kind: "active", active: startedActive };
520
+ this.publishSnapshot();
521
+ startedActive.unsubscribeEvents.add(
522
+ client.onEvent((event) => this.handleDapEvent(startedActive, event)),
523
+ );
524
+
525
+ const initialized: Promise<DapLaunchResponseOutcome> = client
526
+ .waitForEvent(
527
+ "initialized",
528
+ dapRequestOptions(signal, this.options.settings.timeouts.startupMs),
529
+ )
530
+ .then(
531
+ () => ({ kind: "success" }),
532
+ (cause) => ({
533
+ kind: "failure",
534
+ error: cause instanceof Error ? cause : new Error(String(cause)),
535
+ }),
536
+ );
537
+ active.capabilities = parseDapBody(
538
+ DapCapabilitiesSchema,
539
+ await client.request(
540
+ "initialize",
541
+ {
542
+ adapterID: adapterProtocolId,
543
+ clientID: "pi-dap",
544
+ clientName: "Pi DAP",
545
+ columnsStartAt1: true,
546
+ linesStartAt1: true,
547
+ locale: "en-US",
548
+ pathFormat: "path",
549
+ supportsRunInTerminalRequest: true,
550
+ supportsStartDebuggingRequest: supportsJsDebugPrimaryTarget(adapter, profile),
551
+ },
552
+ dapRequestOptions(signal, this.options.settings.timeouts.startupMs),
553
+ ),
554
+ "initialize",
555
+ );
556
+ const launchResponse: Promise<DapLaunchResponseOutcome> = client
557
+ .request("launch", launchArguments, dapRequestOptions(signal))
558
+ .then(
559
+ () => ({ kind: "success" }),
560
+ (cause) => ({
561
+ kind: "failure",
562
+ error: cause instanceof Error ? cause : new Error(String(cause)),
563
+ }),
564
+ );
565
+ const initializedOutcome = await initialized;
566
+ if (initializedOutcome.kind === "failure") throw initializedOutcome.error;
567
+ await this.applyDesiredBreakpoints(active, signal);
568
+ if (active.capabilities.supportsConfigurationDoneRequest === true) {
569
+ await client.request("configurationDone", {}, dapRequestOptions(signal));
570
+ }
571
+ const launchOutcome = await launchResponse;
572
+ if (launchOutcome.kind === "failure") throw launchOutcome.error;
573
+ if (this.isCurrentActive(active) && active.phase === "launching") {
574
+ this.transitionActiveToRunning(active);
575
+ }
576
+ if (this.isCurrentActive(active) && active.phase === "running") {
577
+ const wait = this.waitForExecutionTransition(signal);
578
+ await wait.promise;
579
+ }
580
+ await active.cleanupPromise;
581
+ return this.result();
582
+ } catch (cause) {
583
+ if (active !== undefined) {
584
+ active.stopping = true;
585
+ await this.finishActiveSession(active, "launch failed");
586
+ } else {
587
+ await Promise.all(
588
+ [...debuggeeProcesses].map((child) =>
589
+ stopOwnedDebuggeeProcess(child, this.options.settings.timeouts.shutdownMs),
590
+ ),
591
+ );
592
+ this.state = { kind: "idle" };
593
+ this.publishSnapshot();
594
+ }
595
+ if (cause instanceof Error && isProtocolCancellation(cause)) {
596
+ throw new DapSessionError("adapter", "launch was cancelled and cleaned up", { cause });
597
+ }
598
+ if (cause instanceof DapSessionError) throw cause;
599
+ throw new DapSessionError(
600
+ "adapter",
601
+ `launch failed: ${cause instanceof Error ? cause.message : String(cause)}`,
602
+ { cause },
603
+ );
604
+ }
605
+ }
606
+
607
+ /** Replace all Desired Breakpoints for one file, preserving the prior list if the active update fails. */
608
+ async setBreakpoints(
609
+ input: DapSetBreakpointsInput,
610
+ signal?: AbortSignal,
611
+ ): Promise<DapSessionResult> {
612
+ const filePath = resolve(this.options.cwd, input.filePath);
613
+ const breakpoints = input.breakpoints.map((breakpoint) => ({ ...breakpoint }));
614
+ const active = this.currentActive();
615
+ if (active === undefined) {
616
+ this.desiredBreakpoints.set(filePath, breakpoints);
617
+ return this.result();
618
+ }
619
+ const body = await this.sendBreakpoints(active, filePath, breakpoints, signal);
620
+ this.desiredBreakpoints.set(filePath, breakpoints);
621
+ return this.result({ breakpoints: body.breakpoints });
622
+ }
623
+
624
+ /** Continue a stopped Debuggee and wait for its next stop or termination. */
625
+ continue(signal?: AbortSignal): Promise<DapSessionResult> {
626
+ return this.executeStoppedRequest("continue", signal);
627
+ }
628
+
629
+ /** Step over in a stopped Debuggee and wait for its next stop or termination. */
630
+ next(signal?: AbortSignal): Promise<DapSessionResult> {
631
+ return this.executeStoppedRequest("next", signal);
632
+ }
633
+
634
+ /** Step into in a stopped Debuggee and wait for its next stop or termination. */
635
+ stepIn(signal?: AbortSignal): Promise<DapSessionResult> {
636
+ return this.executeStoppedRequest("stepIn", signal);
637
+ }
638
+
639
+ /** Step out in a stopped Debuggee and wait for its next stop or termination. */
640
+ stepOut(signal?: AbortSignal): Promise<DapSessionResult> {
641
+ return this.executeStoppedRequest("stepOut", signal);
642
+ }
643
+
644
+ /** Pause a running Debuggee and wait for its stopped event. */
645
+ async pause(signal?: AbortSignal): Promise<DapSessionResult> {
646
+ const active = this.requireActivePhase("running", "pause");
647
+ const threadId = await this.resolveThreadId(active, signal);
648
+ const wait = this.waitForExecutionTransition(signal);
649
+ try {
650
+ await active.client.request("pause", { threadId }, dapRequestOptions(signal));
651
+ } catch (cause) {
652
+ wait.cancel();
653
+ if (cause instanceof Error && isProtocolCancellation(cause)) return this.result();
654
+ throw cause;
655
+ }
656
+ await wait.promise;
657
+ await active.cleanupPromise;
658
+ return this.result();
659
+ }
660
+
661
+ /** Retrieve a page of Stack Frames from the stopped thread. */
662
+ async stack(input: DapStackInput = {}, signal?: AbortSignal): Promise<DapSessionResult> {
663
+ const active = this.requireActivePhase("stopped", "stack");
664
+ const threadId = input.threadId ?? (await this.resolveThreadId(active, signal));
665
+ const body = parseDapBody(
666
+ DapStackTraceBodySchema,
667
+ await active.client.request(
668
+ "stackTrace",
669
+ { threadId, startFrame: input.start ?? 0, levels: input.count ?? 20 },
670
+ dapRequestOptions(signal),
671
+ ),
672
+ "stackTrace",
673
+ );
674
+ return this.result({
675
+ stackFrames: body.stackFrames,
676
+ totalFrames: body.totalFrames ?? body.stackFrames.length,
677
+ });
678
+ }
679
+
680
+ /** Retrieve paged variables by Stack Frame scopes or child variables reference. */
681
+ async variables(input: DapVariablesInput, signal?: AbortSignal): Promise<DapSessionResult> {
682
+ const active = this.requireActivePhase("stopped", "variables");
683
+ const start = input.start ?? 0;
684
+ const count = input.count ?? 100;
685
+ if (input.variablesReference !== undefined) {
686
+ const body = await this.requestVariables(
687
+ active,
688
+ input.variablesReference,
689
+ start,
690
+ count,
691
+ signal,
692
+ );
693
+ return this.result({ variables: body.variables });
694
+ }
695
+ const scopes = parseDapBody(
696
+ DapScopesBodySchema,
697
+ await active.client.request("scopes", { frameId: input.frameId }, dapRequestOptions(signal)),
698
+ "scopes",
699
+ ).scopes;
700
+ const variableGroups = await Promise.all(
701
+ scopes.map(async (scope): Promise<DapVariableGroup> => ({
702
+ scope,
703
+ variables: (
704
+ await this.requestVariables(active, scope.variablesReference, start, count, signal)
705
+ ).variables,
706
+ })),
707
+ );
708
+ return this.result({ variableGroups });
709
+ }
710
+
711
+ /** Evaluate an expression in a chosen or top stopped Stack Frame. */
712
+ async evaluate(input: DapEvaluateInput, signal?: AbortSignal): Promise<DapSessionResult> {
713
+ const active = this.requireActivePhase("stopped", "evaluate");
714
+ let frameId = input.frameId;
715
+ if (frameId === undefined) {
716
+ const stackResult = parseDapBody(
717
+ DapStackTraceBodySchema,
718
+ await active.client.request(
719
+ "stackTrace",
720
+ { threadId: await this.resolveThreadId(active, signal), startFrame: 0, levels: 1 },
721
+ dapRequestOptions(signal),
722
+ ),
723
+ "stackTrace",
724
+ );
725
+ frameId = stackResult.stackFrames.at(0)?.id;
726
+ if (frameId === undefined) {
727
+ throw new DapSessionError("state", "evaluate requires a top Stack Frame");
728
+ }
729
+ }
730
+ const evaluation = parseDapBody(
731
+ DapEvaluateBodySchema,
732
+ await active.client.request(
733
+ "evaluate",
734
+ { expression: input.expression, frameId, context: "repl" },
735
+ dapRequestOptions(signal),
736
+ ),
737
+ "evaluate",
738
+ );
739
+ return this.result({ evaluation });
740
+ }
741
+
742
+ /** Return the current lifecycle snapshot and drain currently unread Debuggee output. */
743
+ status(): DapSessionResult {
744
+ return this.result();
745
+ }
746
+
747
+ /** Idempotently stop the active Debug Session and preserve Desired Breakpoints. */
748
+ async stop(_signal?: AbortSignal): Promise<DapSessionResult> {
749
+ const active = this.currentActive();
750
+ if (active === undefined) {
751
+ if (this.state.kind === "terminated") await this.state.cleanupPromise;
752
+ return this.result();
753
+ }
754
+ active.stopping = true;
755
+ await this.finishActiveSession(active, "stopped by request");
756
+ return this.result();
757
+ }
758
+
759
+ /** Close the active Debug Session during Pi session shutdown. */
760
+ async shutdown(): Promise<void> {
761
+ if (this.shutdownPromise !== undefined) return this.shutdownPromise;
762
+ this.shutdownPromise = (async () => {
763
+ const active = this.currentActive();
764
+ if (active !== undefined) {
765
+ active.stopping = true;
766
+ await this.finishActiveSession(active, "Pi session shutdown");
767
+ } else if (this.state.kind === "terminated") {
768
+ await this.state.cleanupPromise;
769
+ }
770
+ })();
771
+ return this.shutdownPromise;
772
+ }
773
+
774
+ private resolveLaunchProfile(profileId: string | undefined): DapLaunchProfile {
775
+ if (profileId !== undefined) {
776
+ const profile = this.options.settings.profiles.get(profileId);
777
+ if (profile === undefined) {
778
+ throw new DapSessionError("configuration", `unknown Launch Profile ${profileId}`);
779
+ }
780
+ return profile;
781
+ }
782
+ if (this.options.settings.profiles.size !== 1) {
783
+ throw new DapSessionError(
784
+ "configuration",
785
+ "launch requires profile when there is not exactly one valid Launch Profile",
786
+ );
787
+ }
788
+ const profile = this.options.settings.profiles.values().next().value;
789
+ if (profile === undefined) {
790
+ throw new DapSessionError("configuration", "launch requires a valid Launch Profile");
791
+ }
792
+ return profile;
793
+ }
794
+
795
+ private async applyDesiredBreakpoints(
796
+ active: ActiveDapSession,
797
+ signal: AbortSignal | undefined,
798
+ ): Promise<void> {
799
+ for (const [filePath, breakpoints] of this.desiredBreakpoints) {
800
+ await this.sendBreakpoints(active, filePath, breakpoints, signal);
801
+ }
802
+ }
803
+
804
+ private async sendBreakpoints(
805
+ active: ActiveDapSession,
806
+ filePath: string,
807
+ breakpoints: readonly DapDesiredBreakpoint[],
808
+ signal: AbortSignal | undefined,
809
+ ): Promise<Static<typeof DapSetBreakpointsBodySchema>> {
810
+ const response = parseDapBody(
811
+ DapSetBreakpointsBodySchema,
812
+ await active.client.request(
813
+ "setBreakpoints",
814
+ {
815
+ source: { name: filePath.split(/[\\/]/).at(-1), path: filePath },
816
+ breakpoints: breakpoints.map((breakpoint) => ({ ...breakpoint })),
817
+ lines: breakpoints.map(({ line }) => line),
818
+ sourceModified: false,
819
+ },
820
+ dapRequestOptions(signal),
821
+ ),
822
+ "setBreakpoints",
823
+ );
824
+ return response;
825
+ }
826
+
827
+ private async executeStoppedRequest(
828
+ command: "continue" | "next" | "stepIn" | "stepOut",
829
+ signal: AbortSignal | undefined,
830
+ ): Promise<DapSessionResult> {
831
+ const active = this.requireActivePhase("stopped", command);
832
+ const threadId = await this.resolveThreadId(active, signal);
833
+ this.transitionActiveToRunning(active);
834
+ const wait = this.waitForExecutionTransition(signal);
835
+ try {
836
+ await active.client.request(command, { threadId }, dapRequestOptions(signal));
837
+ } catch (cause) {
838
+ wait.cancel();
839
+ if (cause instanceof Error && isProtocolCancellation(cause)) return this.result();
840
+ throw cause;
841
+ }
842
+ await wait.promise;
843
+ await active.cleanupPromise;
844
+ return this.result();
845
+ }
846
+
847
+ private async resolveThreadId(
848
+ active: ActiveDapSession,
849
+ signal: AbortSignal | undefined,
850
+ ): Promise<number> {
851
+ if (active.threadId !== undefined) return active.threadId;
852
+ const body = parseDapBody(
853
+ DapThreadsBodySchema,
854
+ await active.client.request("threads", {}, dapRequestOptions(signal)),
855
+ "threads",
856
+ );
857
+ const threadId = body.threads.at(0)?.id;
858
+ if (threadId === undefined) {
859
+ throw new DapSessionError("state", "Debuggee has no thread available for this operation");
860
+ }
861
+ active.threadId = threadId;
862
+ return threadId;
863
+ }
864
+
865
+ private requestVariables(
866
+ active: ActiveDapSession,
867
+ variablesReference: number,
868
+ start: number,
869
+ count: number,
870
+ signal: AbortSignal | undefined,
871
+ ): Promise<Static<typeof DapVariablesBodySchema>> {
872
+ return active.client
873
+ .request("variables", { variablesReference, start, count }, dapRequestOptions(signal))
874
+ .then((body) => parseDapBody(DapVariablesBodySchema, body, "variables"));
875
+ }
876
+
877
+ private requireActivePhase(phase: "running" | "stopped", operation: string): ActiveDapSession {
878
+ const active = this.currentActive();
879
+ if (active === undefined || active.phase !== phase) {
880
+ throw new DapSessionError("state", `${operation} requires a ${phase} Debuggee`);
881
+ }
882
+ return active;
883
+ }
884
+
885
+ private currentActive(): ActiveDapSession | undefined {
886
+ return this.state.kind === "active" ? this.state.active : undefined;
887
+ }
888
+
889
+ private isCurrentActive(active: ActiveDapSession): boolean {
890
+ return this.state.kind === "active" && this.state.active === active;
891
+ }
892
+
893
+ private handleDapEvent(active: ActiveDapSession, event: DebugProtocol.Event): void {
894
+ if (!this.isCurrentActive(active)) return;
895
+ try {
896
+ switch (event.event) {
897
+ case "output":
898
+ this.output.append(
899
+ parseDapBody(DapOutputEventBodySchema, event.body, "output event").output,
900
+ );
901
+ return;
902
+ case "stopped": {
903
+ const body = parseDapBody(DapStoppedEventBodySchema, event.body, "stopped event");
904
+ active.phase = "stopped";
905
+ active.stopReason = body.reason;
906
+ active.threadId = body.threadId;
907
+ this.publishSnapshot();
908
+ this.settleExecutionWaiters();
909
+ return;
910
+ }
911
+ case "continued": {
912
+ const body = parseDapBody(DapContinuedEventBodySchema, event.body, "continued event");
913
+ this.transitionActiveToRunning(active);
914
+ if (body.threadId !== undefined) active.threadId = body.threadId;
915
+ return;
916
+ }
917
+ case "exited":
918
+ active.exitCode = parseDapBody(
919
+ DapExitedEventBodySchema,
920
+ event.body,
921
+ "exited event",
922
+ ).exitCode;
923
+ if (!active.stopping) void this.finishActiveSession(active, "Debuggee exited");
924
+ return;
925
+ case "terminated":
926
+ if (!active.stopping) void this.finishActiveSession(active, "Debug Session terminated");
927
+ return;
928
+ default:
929
+ return;
930
+ }
931
+ } catch (cause) {
932
+ const error =
933
+ cause instanceof Error ? cause : new Error("DAP Session: invalid Debug Adapter event");
934
+ this.publishUnexpectedFailure(error);
935
+ void this.finishActiveSession(active, error.message);
936
+ }
937
+ }
938
+
939
+ private handleAdapterFailure(active: ActiveDapSession, error: DapProtocolClientError): void {
940
+ if (!this.isCurrentActive(active) || active.stopping) return;
941
+ this.publishUnexpectedFailure(error);
942
+ void this.finishActiveSession(active, error.message);
943
+ }
944
+
945
+ private handleReverseRequest(
946
+ request: DebugProtocol.Request,
947
+ debuggeeProcesses: Set<ChildProcessWithoutNullStreams>,
948
+ getActive: () => ActiveDapSession | undefined,
949
+ signal: AbortSignal | undefined,
950
+ ): Promise<DapReverseRequestResult> | DapReverseRequestResult {
951
+ if (request.command === "startDebugging") {
952
+ const active = getActive();
953
+ if (
954
+ active === undefined ||
955
+ active.targetChannelStarted ||
956
+ !supportsJsDebugPrimaryTarget(active.adapter, active.profile) ||
957
+ !Value.Check(JsDebugPrimaryTargetArgumentsSchema, request.arguments)
958
+ ) {
959
+ return { success: false, message: "Pi DAP: startDebugging is outside the V1 boundary" };
960
+ }
961
+ return this.startJsDebugPrimaryTarget(active, request.arguments, signal);
962
+ }
963
+ if (request.command !== "runInTerminal") {
964
+ return { success: false, message: `Pi DAP: unsupported reverse request ${request.command}` };
965
+ }
966
+ if (!Value.Check(RunInTerminalArgumentsSchema, request.arguments)) {
967
+ return { success: false, message: "Pi DAP: runInTerminal arguments are invalid" };
968
+ }
969
+ return this.spawnRunInTerminal(request.arguments, debuggeeProcesses);
970
+ }
971
+
972
+ private async startJsDebugPrimaryTarget(
973
+ active: ActiveDapSession,
974
+ argumentsValue: Static<typeof JsDebugPrimaryTargetArgumentsSchema>,
975
+ signal: AbortSignal | undefined,
976
+ ): Promise<DapReverseRequestResult> {
977
+ active.targetChannelStarted = true;
978
+ const targetClient = await active.rootClient.connectTargetChannel({
979
+ startupSignal: signal,
980
+ onReverseRequest: (request) =>
981
+ this.handleReverseRequest(request, active.debuggeeProcesses, () => active, signal),
982
+ onFailure: (error) => this.handleAdapterFailure(active, error),
983
+ });
984
+ active.targetClient = targetClient;
985
+ active.client = targetClient;
986
+ active.unsubscribeEvents.add(
987
+ targetClient.onEvent((event) => this.handleDapEvent(active, event)),
988
+ );
989
+
990
+ const initialized = targetClient.waitForEvent(
991
+ "initialized",
992
+ dapRequestOptions(signal, this.options.settings.timeouts.startupMs),
993
+ );
994
+ const capabilities = parseDapBody(
995
+ DapCapabilitiesSchema,
996
+ await targetClient.request(
997
+ "initialize",
998
+ {
999
+ adapterID: "pwa-node",
1000
+ clientID: "pi-dap",
1001
+ clientName: "Pi DAP",
1002
+ columnsStartAt1: true,
1003
+ linesStartAt1: true,
1004
+ locale: "en-US",
1005
+ pathFormat: "path",
1006
+ supportsRunInTerminalRequest: true,
1007
+ supportsStartDebuggingRequest: false,
1008
+ },
1009
+ dapRequestOptions(signal, this.options.settings.timeouts.startupMs),
1010
+ ),
1011
+ "initialize primary js-debug target",
1012
+ );
1013
+ const launchResponse = targetClient.request(
1014
+ argumentsValue.request,
1015
+ { ...argumentsValue.configuration },
1016
+ dapRequestOptions(signal),
1017
+ );
1018
+ await initialized;
1019
+ active.capabilities = capabilities;
1020
+ await this.applyDesiredBreakpoints(active, signal);
1021
+ if (capabilities.supportsConfigurationDoneRequest === true) {
1022
+ await targetClient.request("configurationDone", {}, dapRequestOptions(signal));
1023
+ }
1024
+ await launchResponse;
1025
+ return { success: true };
1026
+ }
1027
+
1028
+ private async spawnRunInTerminal(
1029
+ argumentsValue: Static<typeof RunInTerminalArgumentsSchema>,
1030
+ debuggeeProcesses: Set<ChildProcessWithoutNullStreams>,
1031
+ ): Promise<DapReverseRequestResult> {
1032
+ const environment: NodeJS.ProcessEnv = { ...process.env };
1033
+ for (const [name, value] of Object.entries(argumentsValue.env ?? {})) {
1034
+ if (value === null) delete environment[name];
1035
+ else environment[name] = value;
1036
+ }
1037
+ const interpretedByShell = argumentsValue.argsCanBeInterpretedByShell === true;
1038
+ const command = interpretedByShell ? argumentsValue.args.join(" ") : argumentsValue.args[0];
1039
+ if (command === undefined)
1040
+ return { success: false, message: "Pi DAP: runInTerminal has no command" };
1041
+ const commandArguments = interpretedByShell ? [] : argumentsValue.args.slice(1);
1042
+ const child = spawn(command, commandArguments, {
1043
+ cwd: argumentsValue.cwd,
1044
+ detached: process.platform === "linux",
1045
+ env: environment,
1046
+ shell: interpretedByShell,
1047
+ stdio: ["pipe", "pipe", "pipe"],
1048
+ });
1049
+ child.stdout.on("data", (chunk: Buffer) => this.output.append(chunk.toString("utf8")));
1050
+ child.stderr.on("data", (chunk: Buffer) => this.output.append(chunk.toString("utf8")));
1051
+ try {
1052
+ await new Promise<void>((resolveSpawn, rejectSpawn) => {
1053
+ child.once("spawn", resolveSpawn);
1054
+ child.once("error", rejectSpawn);
1055
+ });
1056
+ } catch (cause) {
1057
+ return {
1058
+ success: false,
1059
+ message: `Pi DAP: runInTerminal failed: ${cause instanceof Error ? cause.message : String(cause)}`,
1060
+ };
1061
+ }
1062
+ debuggeeProcesses.add(child);
1063
+ child.once("close", () => debuggeeProcesses.delete(child));
1064
+ return {
1065
+ success: true,
1066
+ body: { processId: child.pid, shellProcessId: child.pid },
1067
+ };
1068
+ }
1069
+
1070
+ private waitForExecutionTransition(signal: AbortSignal | undefined): ExecutionWait {
1071
+ let finish: ((outcome: "cancelled" | "timeout" | "transition") => void) | undefined;
1072
+ const promise = new Promise<"cancelled" | "timeout" | "transition">((resolveWait) => {
1073
+ const timer = setTimeout(
1074
+ () => finish?.("timeout"),
1075
+ this.options.settings.timeouts.executionMs,
1076
+ );
1077
+ const onAbort = () => finish?.("cancelled");
1078
+ finish = (outcome) => {
1079
+ clearTimeout(timer);
1080
+ signal?.removeEventListener("abort", onAbort);
1081
+ this.executionWaiters.delete(onTransition);
1082
+ finish = undefined;
1083
+ resolveWait(outcome);
1084
+ };
1085
+ const onTransition = () => finish?.("transition");
1086
+ this.executionWaiters.add(onTransition);
1087
+ signal?.addEventListener("abort", onAbort, { once: true });
1088
+ if (signal?.aborted === true) finish("cancelled");
1089
+ });
1090
+ return { promise, cancel: () => finish?.("cancelled") };
1091
+ }
1092
+
1093
+ private settleExecutionWaiters(): void {
1094
+ for (const settle of this.executionWaiters) settle();
1095
+ }
1096
+
1097
+ private async finishActiveSession(
1098
+ active: ActiveDapSession,
1099
+ terminationReason: string,
1100
+ ): Promise<void> {
1101
+ if (active.cleanupPromise !== undefined) return active.cleanupPromise;
1102
+ active.stopping = true;
1103
+ const cleanup = (async () => {
1104
+ for (const unsubscribe of active.unsubscribeEvents) unsubscribe();
1105
+ active.unsubscribeEvents.clear();
1106
+ try {
1107
+ await active.targetClient?.shutdown();
1108
+ } catch {
1109
+ // The terminal snapshot remains useful after a failed best-effort shutdown.
1110
+ }
1111
+ try {
1112
+ await active.rootClient.shutdown();
1113
+ } catch {
1114
+ // Process-group ownership is best effort after adapter failure.
1115
+ }
1116
+ await Promise.all(
1117
+ [...active.debuggeeProcesses].map((child) =>
1118
+ stopOwnedDebuggeeProcess(child, this.options.settings.timeouts.shutdownMs),
1119
+ ),
1120
+ );
1121
+ })();
1122
+ active.cleanupPromise = cleanup;
1123
+ if (this.isCurrentActive(active)) {
1124
+ this.state = terminatedDapSessionState(active, cleanup, terminationReason);
1125
+ this.publishSnapshot();
1126
+ }
1127
+ this.settleExecutionWaiters();
1128
+ await cleanup;
1129
+ }
1130
+
1131
+ private transitionActiveToRunning(active: ActiveDapSession): void {
1132
+ if (!this.isCurrentActive(active)) return;
1133
+ const changed = active.phase !== "running" || active.stopReason !== undefined;
1134
+ active.phase = "running";
1135
+ active.stopReason = undefined;
1136
+ if (changed) this.publishSnapshot();
1137
+ }
1138
+
1139
+ private publishSnapshot(): void {
1140
+ try {
1141
+ this.options.onSnapshotChange?.(this.snapshot());
1142
+ } catch {
1143
+ // Observer UI failures cannot change Debug Session cleanup or protocol behavior.
1144
+ }
1145
+ }
1146
+
1147
+ private publishUnexpectedFailure(error: Error): void {
1148
+ try {
1149
+ this.options.onUnexpectedFailure?.(error);
1150
+ } catch {
1151
+ // Observer UI failures cannot change Debug Session cleanup or protocol behavior.
1152
+ }
1153
+ }
1154
+
1155
+ private snapshot(): DapSessionSnapshot {
1156
+ if (this.state.kind === "idle") return { state: "idle" };
1157
+ if (this.state.kind === "terminated") {
1158
+ if (this.state.exitCode !== undefined && this.state.terminationReason !== undefined) {
1159
+ return {
1160
+ state: "terminated",
1161
+ adapterId: this.state.adapterId,
1162
+ profileId: this.state.profileId,
1163
+ exitCode: this.state.exitCode,
1164
+ terminationReason: this.state.terminationReason,
1165
+ };
1166
+ }
1167
+ if (this.state.exitCode !== undefined) {
1168
+ return {
1169
+ state: "terminated",
1170
+ adapterId: this.state.adapterId,
1171
+ profileId: this.state.profileId,
1172
+ exitCode: this.state.exitCode,
1173
+ };
1174
+ }
1175
+ if (this.state.terminationReason !== undefined) {
1176
+ return {
1177
+ state: "terminated",
1178
+ adapterId: this.state.adapterId,
1179
+ profileId: this.state.profileId,
1180
+ terminationReason: this.state.terminationReason,
1181
+ };
1182
+ }
1183
+ return {
1184
+ state: "terminated",
1185
+ adapterId: this.state.adapterId,
1186
+ profileId: this.state.profileId,
1187
+ };
1188
+ }
1189
+ const active = this.state.active;
1190
+ if (active.phase === "stopped") {
1191
+ if (active.threadId !== undefined) {
1192
+ return {
1193
+ state: "stopped",
1194
+ adapterId: active.adapter.id,
1195
+ profileId: active.profile.id,
1196
+ stopReason: active.stopReason ?? "unknown",
1197
+ threadId: active.threadId,
1198
+ };
1199
+ }
1200
+ return {
1201
+ state: "stopped",
1202
+ adapterId: active.adapter.id,
1203
+ profileId: active.profile.id,
1204
+ stopReason: active.stopReason ?? "unknown",
1205
+ };
1206
+ }
1207
+ return {
1208
+ state: active.phase,
1209
+ adapterId: active.adapter.id,
1210
+ profileId: active.profile.id,
1211
+ };
1212
+ }
1213
+
1214
+ private result(
1215
+ payload: Omit<
1216
+ DapSessionResult,
1217
+ "snapshot" | "output" | "discardedOutputBytes" | "desiredBreakpoints"
1218
+ > = {},
1219
+ ): DapSessionResult {
1220
+ const output = this.output.drain();
1221
+ return {
1222
+ snapshot: this.snapshot(),
1223
+ output: output.text,
1224
+ discardedOutputBytes: output.discardedBytes,
1225
+ desiredBreakpoints: [...this.desiredBreakpoints]
1226
+ .sort(([left], [right]) => left.localeCompare(right))
1227
+ .map(([filePath, breakpoints]) => ({ filePath, breakpoints })),
1228
+ ...payload,
1229
+ };
1230
+ }
1231
+ }