@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5
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.
- package/CHANGELOG.md +86 -43
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/src/dap/session.ts
CHANGED
|
@@ -93,6 +93,15 @@ interface DapSession {
|
|
|
93
93
|
initializedSeen: boolean;
|
|
94
94
|
needsConfigurationDone: boolean;
|
|
95
95
|
configurationDoneSent: boolean;
|
|
96
|
+
parentSessionId?: string;
|
|
97
|
+
childSessionIds: Set<string>;
|
|
98
|
+
port?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface DapTreeOutcomeWaiter {
|
|
102
|
+
rootSessionId: string;
|
|
103
|
+
resolve(value: unknown): void;
|
|
104
|
+
reject(reason: unknown): void;
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
export interface DapOutputSnapshot {
|
|
@@ -243,6 +252,8 @@ function buildSummary(session: DapSession): DapSessionSummary {
|
|
|
243
252
|
outputTruncated: session.outputTruncated,
|
|
244
253
|
exitCode: session.exitCode,
|
|
245
254
|
needsConfigurationDone: session.needsConfigurationDone && !session.configurationDoneSent,
|
|
255
|
+
parentSessionId: session.parentSessionId,
|
|
256
|
+
childSessionIds: session.childSessionIds.size > 0 ? [...session.childSessionIds] : undefined,
|
|
246
257
|
};
|
|
247
258
|
}
|
|
248
259
|
|
|
@@ -251,6 +262,7 @@ export class DapSessionManager {
|
|
|
251
262
|
#activeSessionId: string | null = null;
|
|
252
263
|
#cleanupLoopPromise?: Promise<void>;
|
|
253
264
|
#nextId = 0;
|
|
265
|
+
#treeOutcomeWaiters = new Set<DapTreeOutcomeWaiter>();
|
|
254
266
|
|
|
255
267
|
constructor() {
|
|
256
268
|
this.#startCleanupTimer();
|
|
@@ -318,17 +330,22 @@ export class DapSessionManager {
|
|
|
318
330
|
await launchPromise;
|
|
319
331
|
// Try to capture initial stopped state (e.g. stopOnEntry).
|
|
320
332
|
// Timeout is acceptable — the program may simply be running.
|
|
333
|
+
let resultSession = session;
|
|
321
334
|
try {
|
|
322
335
|
await untilAborted(signal, initialStopPromise);
|
|
323
|
-
|
|
324
|
-
|
|
336
|
+
const active = this.#getActiveSessionOrNull();
|
|
337
|
+
if (active && this.#getRootSession(active).id === session.id) {
|
|
338
|
+
resultSession = active;
|
|
339
|
+
}
|
|
340
|
+
if (resultSession.status === "stopped") {
|
|
341
|
+
await this.#fetchTopFrame(resultSession, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
|
|
325
342
|
}
|
|
326
343
|
} catch {
|
|
327
344
|
if (session.initializedSeen && session.status === "launching") {
|
|
328
345
|
session.status = session.configurationDoneSent ? "running" : "configuring";
|
|
329
346
|
}
|
|
330
347
|
}
|
|
331
|
-
return buildSummary(
|
|
348
|
+
return buildSummary(resultSession);
|
|
332
349
|
} catch (error) {
|
|
333
350
|
await this.#disposeSession(session);
|
|
334
351
|
const mapped = mapDebugpyMissingModule(options.adapter.name, error);
|
|
@@ -376,17 +393,22 @@ export class DapSessionManager {
|
|
|
376
393
|
await throwPreferredDapStartError("attach", attachFailure, error);
|
|
377
394
|
}
|
|
378
395
|
await attachPromise;
|
|
396
|
+
let resultSession = session;
|
|
379
397
|
try {
|
|
380
398
|
await untilAborted(signal, initialStopPromise);
|
|
381
|
-
|
|
382
|
-
|
|
399
|
+
const active = this.#getActiveSessionOrNull();
|
|
400
|
+
if (active && this.#getRootSession(active).id === session.id) {
|
|
401
|
+
resultSession = active;
|
|
402
|
+
}
|
|
403
|
+
if (resultSession.status === "stopped") {
|
|
404
|
+
await this.#fetchTopFrame(resultSession, signal, Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS));
|
|
383
405
|
}
|
|
384
406
|
} catch {
|
|
385
407
|
if (session.initializedSeen && session.status === "launching") {
|
|
386
408
|
session.status = session.configurationDoneSent ? "running" : "configuring";
|
|
387
409
|
}
|
|
388
410
|
}
|
|
389
|
-
return buildSummary(
|
|
411
|
+
return buildSummary(resultSession);
|
|
390
412
|
} catch (error) {
|
|
391
413
|
await this.#disposeSession(session);
|
|
392
414
|
const mapped = mapDebugpyMissingModule(options.adapter.name, error);
|
|
@@ -414,6 +436,62 @@ export class DapSessionManager {
|
|
|
414
436
|
);
|
|
415
437
|
return run;
|
|
416
438
|
}
|
|
439
|
+
async #syncBreakpointTree(
|
|
440
|
+
origin: DapSession,
|
|
441
|
+
command: string,
|
|
442
|
+
args: unknown,
|
|
443
|
+
prepare: (session: DapSession) => void,
|
|
444
|
+
apply: (session: DapSession, breakpoints: DapBreakpoint[] | undefined) => void,
|
|
445
|
+
signal?: AbortSignal,
|
|
446
|
+
timeoutMs: number = 30_000,
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
const sessions = this.#getTreeSessions(origin).filter(
|
|
449
|
+
session => session.status !== "terminated" && session.client.isAlive(),
|
|
450
|
+
);
|
|
451
|
+
for (const session of sessions) prepare(session);
|
|
452
|
+
await this.#serializeBreakpointMutation(
|
|
453
|
+
origin,
|
|
454
|
+
async () => {
|
|
455
|
+
const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
|
|
456
|
+
origin,
|
|
457
|
+
command,
|
|
458
|
+
args,
|
|
459
|
+
signal,
|
|
460
|
+
timeoutMs,
|
|
461
|
+
);
|
|
462
|
+
apply(origin, response?.breakpoints);
|
|
463
|
+
},
|
|
464
|
+
signal,
|
|
465
|
+
);
|
|
466
|
+
await Promise.all(
|
|
467
|
+
sessions
|
|
468
|
+
.filter(session => session !== origin)
|
|
469
|
+
.map(async session => {
|
|
470
|
+
try {
|
|
471
|
+
await this.#serializeBreakpointMutation(
|
|
472
|
+
session,
|
|
473
|
+
async () => {
|
|
474
|
+
const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
|
|
475
|
+
session,
|
|
476
|
+
command,
|
|
477
|
+
args,
|
|
478
|
+
signal,
|
|
479
|
+
timeoutMs,
|
|
480
|
+
);
|
|
481
|
+
apply(session, response?.breakpoints);
|
|
482
|
+
},
|
|
483
|
+
signal,
|
|
484
|
+
);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
logger.warn("Failed to synchronize breakpoint request with child debug session", {
|
|
487
|
+
sessionId: session.id,
|
|
488
|
+
command,
|
|
489
|
+
error: toErrorMessage(error),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
417
495
|
|
|
418
496
|
async setBreakpoint(
|
|
419
497
|
file: string,
|
|
@@ -423,123 +501,127 @@ export class DapSessionManager {
|
|
|
423
501
|
timeoutMs: number = 30_000,
|
|
424
502
|
) {
|
|
425
503
|
const session = this.#touchActiveSession();
|
|
426
|
-
|
|
504
|
+
const sourcePath = normalizePath(file);
|
|
505
|
+
const root = this.#getRootSession(session);
|
|
506
|
+
const current = [...(root.breakpoints.get(sourcePath) ?? [])].filter(entry => entry.line !== line);
|
|
507
|
+
current.push({ verified: false, line, condition });
|
|
508
|
+
current.sort((left, right) => left.line - right.line);
|
|
509
|
+
const args = {
|
|
510
|
+
source: { path: sourcePath, name: path.basename(sourcePath) },
|
|
511
|
+
breakpoints: current.map<DapSourceBreakpoint>(entry => ({
|
|
512
|
+
line: entry.line,
|
|
513
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
514
|
+
})),
|
|
515
|
+
};
|
|
516
|
+
await this.#syncBreakpointTree(
|
|
427
517
|
session,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
deduped.push({ verified: false, line, condition });
|
|
433
|
-
deduped.sort((left, right) => left.line - right.line);
|
|
434
|
-
const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
|
|
435
|
-
session,
|
|
436
|
-
"setBreakpoints",
|
|
437
|
-
{
|
|
438
|
-
source: { path: sourcePath, name: path.basename(sourcePath) },
|
|
439
|
-
breakpoints: deduped.map<DapSourceBreakpoint>(entry => ({
|
|
440
|
-
line: entry.line,
|
|
441
|
-
...(entry.condition ? { condition: entry.condition } : {}),
|
|
442
|
-
})),
|
|
443
|
-
},
|
|
444
|
-
signal,
|
|
445
|
-
timeoutMs,
|
|
446
|
-
);
|
|
447
|
-
session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(deduped, response?.breakpoints));
|
|
448
|
-
return {
|
|
449
|
-
snapshot: buildSummary(session),
|
|
450
|
-
breakpoints: session.breakpoints.get(sourcePath) ?? [],
|
|
518
|
+
"setBreakpoints",
|
|
519
|
+
args,
|
|
520
|
+
target =>
|
|
521
|
+
target.breakpoints.set(
|
|
451
522
|
sourcePath,
|
|
452
|
-
|
|
453
|
-
|
|
523
|
+
current.map(entry => ({ ...entry, verified: false })),
|
|
524
|
+
),
|
|
525
|
+
(target, response) => target.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(current, response)),
|
|
454
526
|
signal,
|
|
527
|
+
timeoutMs,
|
|
455
528
|
);
|
|
529
|
+
return {
|
|
530
|
+
snapshot: buildSummary(session),
|
|
531
|
+
breakpoints: session.breakpoints.get(sourcePath) ?? [],
|
|
532
|
+
sourcePath,
|
|
533
|
+
};
|
|
456
534
|
}
|
|
457
535
|
|
|
458
536
|
async removeBreakpoint(file: string, line: number, signal?: AbortSignal, timeoutMs: number = 30_000) {
|
|
459
537
|
const session = this.#touchActiveSession();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
},
|
|
475
|
-
signal,
|
|
476
|
-
timeoutMs,
|
|
477
|
-
);
|
|
478
|
-
if (current.length === 0) {
|
|
479
|
-
session.breakpoints.delete(sourcePath);
|
|
480
|
-
} else {
|
|
481
|
-
session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(current, response?.breakpoints));
|
|
482
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
snapshot: buildSummary(session),
|
|
485
|
-
breakpoints: session.breakpoints.get(sourcePath) ?? [],
|
|
538
|
+
const sourcePath = normalizePath(file);
|
|
539
|
+
const root = this.#getRootSession(session);
|
|
540
|
+
const current = [...(root.breakpoints.get(sourcePath) ?? [])].filter(entry => entry.line !== line);
|
|
541
|
+
const args = {
|
|
542
|
+
source: { path: sourcePath, name: path.basename(sourcePath) },
|
|
543
|
+
breakpoints: current.map<DapSourceBreakpoint>(entry => ({
|
|
544
|
+
line: entry.line,
|
|
545
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
546
|
+
})),
|
|
547
|
+
};
|
|
548
|
+
const prepare = (target: DapSession) => {
|
|
549
|
+
if (current.length === 0) target.breakpoints.delete(sourcePath);
|
|
550
|
+
else
|
|
551
|
+
target.breakpoints.set(
|
|
486
552
|
sourcePath,
|
|
487
|
-
|
|
553
|
+
current.map(entry => ({ ...entry, verified: false })),
|
|
554
|
+
);
|
|
555
|
+
};
|
|
556
|
+
await this.#syncBreakpointTree(
|
|
557
|
+
session,
|
|
558
|
+
"setBreakpoints",
|
|
559
|
+
args,
|
|
560
|
+
prepare,
|
|
561
|
+
(target, response) => {
|
|
562
|
+
if (current.length === 0) target.breakpoints.delete(sourcePath);
|
|
563
|
+
else target.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(current, response));
|
|
488
564
|
},
|
|
489
565
|
signal,
|
|
566
|
+
timeoutMs,
|
|
490
567
|
);
|
|
568
|
+
return {
|
|
569
|
+
snapshot: buildSummary(session),
|
|
570
|
+
breakpoints: session.breakpoints.get(sourcePath) ?? [],
|
|
571
|
+
sourcePath,
|
|
572
|
+
};
|
|
491
573
|
}
|
|
492
574
|
|
|
493
575
|
async setFunctionBreakpoint(name: string, condition?: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
|
|
494
576
|
const session = this.#touchActiveSession();
|
|
495
|
-
|
|
577
|
+
const current = this.#getRootSession(session).functionBreakpoints.filter(entry => entry.name !== name);
|
|
578
|
+
current.push({ verified: false, name, condition });
|
|
579
|
+
current.sort((left, right) => left.name.localeCompare(right.name));
|
|
580
|
+
const args = {
|
|
581
|
+
breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
|
|
582
|
+
name: entry.name,
|
|
583
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
584
|
+
})),
|
|
585
|
+
};
|
|
586
|
+
await this.#syncBreakpointTree(
|
|
496
587
|
session,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
current.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{
|
|
505
|
-
breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
|
|
506
|
-
name: entry.name,
|
|
507
|
-
...(entry.condition ? { condition: entry.condition } : {}),
|
|
508
|
-
})),
|
|
509
|
-
},
|
|
510
|
-
signal,
|
|
511
|
-
timeoutMs,
|
|
512
|
-
);
|
|
513
|
-
session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
|
|
514
|
-
return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
|
|
588
|
+
"setFunctionBreakpoints",
|
|
589
|
+
args,
|
|
590
|
+
target => {
|
|
591
|
+
target.functionBreakpoints = current.map(entry => ({ ...entry, verified: false }));
|
|
592
|
+
},
|
|
593
|
+
(target, response) => {
|
|
594
|
+
target.functionBreakpoints = this.#mapFunctionBreakpoints(current, response);
|
|
515
595
|
},
|
|
516
596
|
signal,
|
|
597
|
+
timeoutMs,
|
|
517
598
|
);
|
|
599
|
+
return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
|
|
518
600
|
}
|
|
519
601
|
|
|
520
602
|
async removeFunctionBreakpoint(name: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
|
|
521
603
|
const session = this.#touchActiveSession();
|
|
522
|
-
|
|
604
|
+
const current = this.#getRootSession(session).functionBreakpoints.filter(entry => entry.name !== name);
|
|
605
|
+
const args = {
|
|
606
|
+
breakpoints: current.map<DapFunctionBreakpoint>(entry => ({
|
|
607
|
+
name: entry.name,
|
|
608
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
609
|
+
})),
|
|
610
|
+
};
|
|
611
|
+
await this.#syncBreakpointTree(
|
|
523
612
|
session,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
name: entry.name,
|
|
532
|
-
...(entry.condition ? { condition: entry.condition } : {}),
|
|
533
|
-
})),
|
|
534
|
-
},
|
|
535
|
-
signal,
|
|
536
|
-
timeoutMs,
|
|
537
|
-
);
|
|
538
|
-
session.functionBreakpoints = this.#mapFunctionBreakpoints(current, response?.breakpoints);
|
|
539
|
-
return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
|
|
613
|
+
"setFunctionBreakpoints",
|
|
614
|
+
args,
|
|
615
|
+
target => {
|
|
616
|
+
target.functionBreakpoints = current.map(entry => ({ ...entry, verified: false }));
|
|
617
|
+
},
|
|
618
|
+
(target, response) => {
|
|
619
|
+
target.functionBreakpoints = this.#mapFunctionBreakpoints(current, response);
|
|
540
620
|
},
|
|
541
621
|
signal,
|
|
622
|
+
timeoutMs,
|
|
542
623
|
);
|
|
624
|
+
return { snapshot: buildSummary(session), breakpoints: session.functionBreakpoints };
|
|
543
625
|
}
|
|
544
626
|
|
|
545
627
|
async setInstructionBreakpoint(
|
|
@@ -551,37 +633,33 @@ export class DapSessionManager {
|
|
|
551
633
|
timeoutMs: number = 30_000,
|
|
552
634
|
) {
|
|
553
635
|
const session = this.#touchActiveSession();
|
|
554
|
-
|
|
636
|
+
const current = this.#getRootSession(session).instructionBreakpoints.filter(
|
|
637
|
+
entry => entry.instructionReference !== instructionReference || entry.offset !== offset,
|
|
638
|
+
);
|
|
639
|
+
current.push({ instructionReference, offset, condition, hitCondition });
|
|
640
|
+
current.sort((left, right) => {
|
|
641
|
+
const referenceOrder = left.instructionReference.localeCompare(right.instructionReference);
|
|
642
|
+
return referenceOrder !== 0 ? referenceOrder : (left.offset ?? 0) - (right.offset ?? 0);
|
|
643
|
+
});
|
|
644
|
+
const args = { breakpoints: current } satisfies DapSetInstructionBreakpointsArguments;
|
|
645
|
+
let responseBreakpoints: DapBreakpoint[] | undefined;
|
|
646
|
+
await this.#syncBreakpointTree(
|
|
555
647
|
session,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (referenceOrder !== 0) {
|
|
564
|
-
return referenceOrder;
|
|
565
|
-
}
|
|
566
|
-
return (left.offset ?? 0) - (right.offset ?? 0);
|
|
567
|
-
});
|
|
568
|
-
const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
|
|
569
|
-
session,
|
|
570
|
-
"setInstructionBreakpoints",
|
|
571
|
-
{
|
|
572
|
-
breakpoints: current,
|
|
573
|
-
} satisfies DapSetInstructionBreakpointsArguments,
|
|
574
|
-
signal,
|
|
575
|
-
timeoutMs,
|
|
576
|
-
);
|
|
577
|
-
session.instructionBreakpoints = current;
|
|
578
|
-
return {
|
|
579
|
-
snapshot: buildSummary(session),
|
|
580
|
-
breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
|
|
581
|
-
};
|
|
648
|
+
"setInstructionBreakpoints",
|
|
649
|
+
args,
|
|
650
|
+
target => {
|
|
651
|
+
target.instructionBreakpoints = current.map(entry => ({ ...entry }));
|
|
652
|
+
},
|
|
653
|
+
(target, response) => {
|
|
654
|
+
if (target === session) responseBreakpoints = response;
|
|
582
655
|
},
|
|
583
656
|
signal,
|
|
657
|
+
timeoutMs,
|
|
584
658
|
);
|
|
659
|
+
return {
|
|
660
|
+
snapshot: buildSummary(session),
|
|
661
|
+
breakpoints: this.#mapInstructionBreakpoints(current, responseBreakpoints),
|
|
662
|
+
};
|
|
585
663
|
}
|
|
586
664
|
|
|
587
665
|
async removeInstructionBreakpoint(
|
|
@@ -591,35 +669,29 @@ export class DapSessionManager {
|
|
|
591
669
|
timeoutMs: number = 30_000,
|
|
592
670
|
) {
|
|
593
671
|
const session = this.#touchActiveSession();
|
|
594
|
-
|
|
672
|
+
const current = this.#getRootSession(session).instructionBreakpoints.filter(entry => {
|
|
673
|
+
if (entry.instructionReference !== instructionReference) return true;
|
|
674
|
+
return offset !== undefined && entry.offset !== offset;
|
|
675
|
+
});
|
|
676
|
+
const args = { breakpoints: current } satisfies DapSetInstructionBreakpointsArguments;
|
|
677
|
+
let responseBreakpoints: DapBreakpoint[] | undefined;
|
|
678
|
+
await this.#syncBreakpointTree(
|
|
595
679
|
session,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
return entry.offset !== offset;
|
|
605
|
-
});
|
|
606
|
-
const response = await this.#sendRequestWithConfig<{ breakpoints?: DapBreakpoint[] }>(
|
|
607
|
-
session,
|
|
608
|
-
"setInstructionBreakpoints",
|
|
609
|
-
{
|
|
610
|
-
breakpoints: current,
|
|
611
|
-
} satisfies DapSetInstructionBreakpointsArguments,
|
|
612
|
-
signal,
|
|
613
|
-
timeoutMs,
|
|
614
|
-
);
|
|
615
|
-
session.instructionBreakpoints = current;
|
|
616
|
-
return {
|
|
617
|
-
snapshot: buildSummary(session),
|
|
618
|
-
breakpoints: this.#mapInstructionBreakpoints(current, response?.breakpoints),
|
|
619
|
-
};
|
|
680
|
+
"setInstructionBreakpoints",
|
|
681
|
+
args,
|
|
682
|
+
target => {
|
|
683
|
+
target.instructionBreakpoints = current.map(entry => ({ ...entry }));
|
|
684
|
+
},
|
|
685
|
+
(target, response) => {
|
|
686
|
+
if (target === session) responseBreakpoints = response;
|
|
620
687
|
},
|
|
621
688
|
signal,
|
|
689
|
+
timeoutMs,
|
|
622
690
|
);
|
|
691
|
+
return {
|
|
692
|
+
snapshot: buildSummary(session),
|
|
693
|
+
breakpoints: this.#mapInstructionBreakpoints(current, responseBreakpoints),
|
|
694
|
+
};
|
|
623
695
|
}
|
|
624
696
|
|
|
625
697
|
async dataBreakpointInfo(
|
|
@@ -653,54 +725,52 @@ export class DapSessionManager {
|
|
|
653
725
|
timeoutMs: number = 30_000,
|
|
654
726
|
) {
|
|
655
727
|
const session = this.#touchActiveSession();
|
|
656
|
-
|
|
728
|
+
const current = this.#getRootSession(session).dataBreakpoints.filter(entry => entry.dataId !== dataId);
|
|
729
|
+
current.push({ dataId, accessType, condition, hitCondition });
|
|
730
|
+
current.sort((left, right) => left.dataId.localeCompare(right.dataId));
|
|
731
|
+
const args = { breakpoints: current } satisfies DapSetDataBreakpointsArguments;
|
|
732
|
+
let responseBreakpoints: DapBreakpoint[] | undefined;
|
|
733
|
+
await this.#syncBreakpointTree(
|
|
657
734
|
session,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
current.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
{
|
|
666
|
-
breakpoints: current,
|
|
667
|
-
} satisfies DapSetDataBreakpointsArguments,
|
|
668
|
-
signal,
|
|
669
|
-
timeoutMs,
|
|
670
|
-
);
|
|
671
|
-
session.dataBreakpoints = current;
|
|
672
|
-
return {
|
|
673
|
-
snapshot: buildSummary(session),
|
|
674
|
-
breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
|
|
675
|
-
};
|
|
735
|
+
"setDataBreakpoints",
|
|
736
|
+
args,
|
|
737
|
+
target => {
|
|
738
|
+
target.dataBreakpoints = current.map(entry => ({ ...entry }));
|
|
739
|
+
},
|
|
740
|
+
(target, response) => {
|
|
741
|
+
if (target === session) responseBreakpoints = response;
|
|
676
742
|
},
|
|
677
743
|
signal,
|
|
744
|
+
timeoutMs,
|
|
678
745
|
);
|
|
746
|
+
return {
|
|
747
|
+
snapshot: buildSummary(session),
|
|
748
|
+
breakpoints: this.#mapDataBreakpoints(current, responseBreakpoints),
|
|
749
|
+
};
|
|
679
750
|
}
|
|
680
751
|
|
|
681
752
|
async removeDataBreakpoint(dataId: string, signal?: AbortSignal, timeoutMs: number = 30_000) {
|
|
682
753
|
const session = this.#touchActiveSession();
|
|
683
|
-
|
|
754
|
+
const current = this.#getRootSession(session).dataBreakpoints.filter(entry => entry.dataId !== dataId);
|
|
755
|
+
const args = { breakpoints: current } satisfies DapSetDataBreakpointsArguments;
|
|
756
|
+
let responseBreakpoints: DapBreakpoint[] | undefined;
|
|
757
|
+
await this.#syncBreakpointTree(
|
|
684
758
|
session,
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
} satisfies DapSetDataBreakpointsArguments,
|
|
693
|
-
signal,
|
|
694
|
-
timeoutMs,
|
|
695
|
-
);
|
|
696
|
-
session.dataBreakpoints = current;
|
|
697
|
-
return {
|
|
698
|
-
snapshot: buildSummary(session),
|
|
699
|
-
breakpoints: this.#mapDataBreakpoints(current, response?.breakpoints),
|
|
700
|
-
};
|
|
759
|
+
"setDataBreakpoints",
|
|
760
|
+
args,
|
|
761
|
+
target => {
|
|
762
|
+
target.dataBreakpoints = current.map(entry => ({ ...entry }));
|
|
763
|
+
},
|
|
764
|
+
(target, response) => {
|
|
765
|
+
if (target === session) responseBreakpoints = response;
|
|
701
766
|
},
|
|
702
767
|
signal,
|
|
768
|
+
timeoutMs,
|
|
703
769
|
);
|
|
770
|
+
return {
|
|
771
|
+
snapshot: buildSummary(session),
|
|
772
|
+
breakpoints: this.#mapDataBreakpoints(current, responseBreakpoints),
|
|
773
|
+
};
|
|
704
774
|
}
|
|
705
775
|
|
|
706
776
|
async disassemble(
|
|
@@ -998,25 +1068,33 @@ export class DapSessionManager {
|
|
|
998
1068
|
async terminate(signal?: AbortSignal, timeoutMs: number = 30_000): Promise<DapSessionSummary | null> {
|
|
999
1069
|
const session = this.#getActiveSessionOrNull();
|
|
1000
1070
|
if (!session) return null;
|
|
1001
|
-
session
|
|
1002
|
-
|
|
1071
|
+
this.#touchSessionAndAncestors(session);
|
|
1072
|
+
const root = this.#getRootSession(session);
|
|
1073
|
+
const summary = buildSummary(session);
|
|
1074
|
+
await this.#terminateSessionTree(root, signal, timeoutMs);
|
|
1075
|
+
return summary;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async #terminateSessionTree(session: DapSession, signal?: AbortSignal, timeoutMs: number = 30_000): Promise<void> {
|
|
1079
|
+
session.status = "terminated";
|
|
1080
|
+
try {
|
|
1081
|
+
for (const childId of [...session.childSessionIds]) {
|
|
1082
|
+
const child = this.#sessions.get(childId);
|
|
1083
|
+
if (child) {
|
|
1084
|
+
await this.#terminateSessionTree(child, signal, timeoutMs);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1003
1087
|
if (session.capabilities?.supportsTerminateRequest) {
|
|
1004
|
-
await
|
|
1005
|
-
signal,
|
|
1006
|
-
session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
|
|
1007
|
-
);
|
|
1088
|
+
await session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined);
|
|
1008
1089
|
}
|
|
1009
|
-
await
|
|
1010
|
-
signal,
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1090
|
+
await session.client
|
|
1091
|
+
.sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
|
|
1092
|
+
.catch(() => undefined);
|
|
1093
|
+
} catch {
|
|
1094
|
+
/* Disposal remains mandatory when a caller aborts best-effort DAP shutdown. */
|
|
1095
|
+
} finally {
|
|
1096
|
+
this.#disposeSession(session);
|
|
1015
1097
|
}
|
|
1016
|
-
session.status = "terminated";
|
|
1017
|
-
const summary = buildSummary(session);
|
|
1018
|
-
await this.#disposeSession(session);
|
|
1019
|
-
return summary;
|
|
1020
1098
|
}
|
|
1021
1099
|
|
|
1022
1100
|
#startCleanupTimer(): void {
|
|
@@ -1048,17 +1126,156 @@ export class DapSessionManager {
|
|
|
1048
1126
|
}
|
|
1049
1127
|
}
|
|
1050
1128
|
|
|
1129
|
+
async #startChildSession(
|
|
1130
|
+
parent: DapSession,
|
|
1131
|
+
request: "launch" | "attach",
|
|
1132
|
+
configuration: Record<string, unknown>,
|
|
1133
|
+
timeoutMs: number = 30_000,
|
|
1134
|
+
): Promise<void> {
|
|
1135
|
+
if (parent.adapter.connectMode !== "tcp" || parent.port === undefined) {
|
|
1136
|
+
throw new Error(`DAP adapter ${parent.adapter.name} cannot accept child session connections`);
|
|
1137
|
+
}
|
|
1138
|
+
const cwd = path.resolve(parent.cwd, typeof configuration.cwd === "string" ? configuration.cwd : ".");
|
|
1139
|
+
const client = await DapClient.connect({
|
|
1140
|
+
adapter: parent.adapter,
|
|
1141
|
+
cwd,
|
|
1142
|
+
host: "127.0.0.1",
|
|
1143
|
+
port: parent.port,
|
|
1144
|
+
});
|
|
1145
|
+
const child = this.#registerSession(
|
|
1146
|
+
client,
|
|
1147
|
+
parent.adapter,
|
|
1148
|
+
cwd,
|
|
1149
|
+
typeof configuration.program === "string" ? configuration.program : undefined,
|
|
1150
|
+
parent.id,
|
|
1151
|
+
);
|
|
1152
|
+
try {
|
|
1153
|
+
child.capabilities = await client.initialize(
|
|
1154
|
+
this.#buildInitializeArguments(parent.adapter),
|
|
1155
|
+
undefined,
|
|
1156
|
+
timeoutMs,
|
|
1157
|
+
);
|
|
1158
|
+
child.needsConfigurationDone = child.capabilities.supportsConfigurationDoneRequest === true;
|
|
1159
|
+
const startFailure: DapStartRequestFailure = { rejected: false };
|
|
1160
|
+
const startPromise = trackDapStartRequest(
|
|
1161
|
+
client.sendRequest(request, { ...configuration, cwd }, undefined, timeoutMs),
|
|
1162
|
+
startFailure,
|
|
1163
|
+
);
|
|
1164
|
+
startPromise.catch(() => {});
|
|
1165
|
+
try {
|
|
1166
|
+
await this.#completeConfigurationHandshake(child, undefined, timeoutMs);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
await throwPreferredDapStartError(request, startFailure, error);
|
|
1169
|
+
}
|
|
1170
|
+
await startPromise;
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
await this.#disposeSession(child);
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async #applyRootBreakpointsToSession(
|
|
1178
|
+
session: DapSession,
|
|
1179
|
+
signal?: AbortSignal,
|
|
1180
|
+
timeoutMs: number = 30_000,
|
|
1181
|
+
): Promise<void> {
|
|
1182
|
+
const root = this.#getRootSession(session);
|
|
1183
|
+
for (const [sourcePath, entries] of root.breakpoints) {
|
|
1184
|
+
try {
|
|
1185
|
+
const response = await session.client.sendRequest<{ breakpoints?: DapBreakpoint[] }>(
|
|
1186
|
+
"setBreakpoints",
|
|
1187
|
+
{
|
|
1188
|
+
source: { path: sourcePath, name: path.basename(sourcePath) },
|
|
1189
|
+
breakpoints: entries.map<DapSourceBreakpoint>(entry => ({
|
|
1190
|
+
line: entry.line,
|
|
1191
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
1192
|
+
})),
|
|
1193
|
+
},
|
|
1194
|
+
signal,
|
|
1195
|
+
timeoutMs,
|
|
1196
|
+
);
|
|
1197
|
+
session.breakpoints.set(sourcePath, this.#mapSourceBreakpoints(entries, response?.breakpoints));
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
logger.warn("Failed to bind source breakpoints in child debug session", {
|
|
1200
|
+
sessionId: session.id,
|
|
1201
|
+
sourcePath,
|
|
1202
|
+
error: toErrorMessage(error),
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (root.functionBreakpoints.length > 0) {
|
|
1207
|
+
try {
|
|
1208
|
+
const response = await session.client.sendRequest<{ breakpoints?: DapBreakpoint[] }>(
|
|
1209
|
+
"setFunctionBreakpoints",
|
|
1210
|
+
{
|
|
1211
|
+
breakpoints: root.functionBreakpoints.map<DapFunctionBreakpoint>(entry => ({
|
|
1212
|
+
name: entry.name,
|
|
1213
|
+
...(entry.condition ? { condition: entry.condition } : {}),
|
|
1214
|
+
})),
|
|
1215
|
+
},
|
|
1216
|
+
signal,
|
|
1217
|
+
timeoutMs,
|
|
1218
|
+
);
|
|
1219
|
+
session.functionBreakpoints = this.#mapFunctionBreakpoints(root.functionBreakpoints, response?.breakpoints);
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
logger.warn("Failed to bind function breakpoints in child debug session", {
|
|
1222
|
+
sessionId: session.id,
|
|
1223
|
+
error: toErrorMessage(error),
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (root.instructionBreakpoints.length > 0) {
|
|
1228
|
+
try {
|
|
1229
|
+
await session.client.sendRequest(
|
|
1230
|
+
"setInstructionBreakpoints",
|
|
1231
|
+
{ breakpoints: root.instructionBreakpoints } satisfies DapSetInstructionBreakpointsArguments,
|
|
1232
|
+
signal,
|
|
1233
|
+
timeoutMs,
|
|
1234
|
+
);
|
|
1235
|
+
session.instructionBreakpoints = root.instructionBreakpoints.map(entry => ({ ...entry }));
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
logger.warn("Failed to bind instruction breakpoints in child debug session", {
|
|
1238
|
+
sessionId: session.id,
|
|
1239
|
+
error: toErrorMessage(error),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (root.dataBreakpoints.length > 0) {
|
|
1244
|
+
try {
|
|
1245
|
+
await session.client.sendRequest(
|
|
1246
|
+
"setDataBreakpoints",
|
|
1247
|
+
{ breakpoints: root.dataBreakpoints } satisfies DapSetDataBreakpointsArguments,
|
|
1248
|
+
signal,
|
|
1249
|
+
timeoutMs,
|
|
1250
|
+
);
|
|
1251
|
+
session.dataBreakpoints = root.dataBreakpoints.map(entry => ({ ...entry }));
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
logger.debug("Failed to bind data breakpoints in child debug session", {
|
|
1254
|
+
sessionId: session.id,
|
|
1255
|
+
error: toErrorMessage(error),
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1051
1261
|
async #ensureLaunchSlot(): Promise<void> {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
return;
|
|
1262
|
+
for (const session of [...this.#sessions.values()]) {
|
|
1263
|
+
if (session.status === "terminated" || !session.client.isAlive()) {
|
|
1264
|
+
this.#disposeSession(session);
|
|
1265
|
+
}
|
|
1057
1266
|
}
|
|
1058
|
-
|
|
1267
|
+
const root = [...this.#sessions.values()].find(session => !session.parentSessionId);
|
|
1268
|
+
if (!root) return;
|
|
1269
|
+
throw new Error(`Debug session ${root.id} is still active. Terminate it before launching another.`);
|
|
1059
1270
|
}
|
|
1060
1271
|
|
|
1061
|
-
#registerSession(
|
|
1272
|
+
#registerSession(
|
|
1273
|
+
client: DapClient,
|
|
1274
|
+
adapter: DapResolvedAdapter,
|
|
1275
|
+
cwd: string,
|
|
1276
|
+
program?: string,
|
|
1277
|
+
parentSessionId?: string,
|
|
1278
|
+
): DapSession {
|
|
1062
1279
|
const session: DapSession = {
|
|
1063
1280
|
id: `debug-${++this.#nextId}`,
|
|
1064
1281
|
adapter,
|
|
@@ -1083,6 +1300,9 @@ export class DapSessionManager {
|
|
|
1083
1300
|
initializedSeen: false,
|
|
1084
1301
|
needsConfigurationDone: false,
|
|
1085
1302
|
configurationDoneSent: false,
|
|
1303
|
+
parentSessionId,
|
|
1304
|
+
childSessionIds: new Set(),
|
|
1305
|
+
port: client.port,
|
|
1086
1306
|
};
|
|
1087
1307
|
client.onReverseRequest("runInTerminal", async rawArgs => {
|
|
1088
1308
|
const args = (rawArgs ?? {}) as DapRunInTerminalArguments;
|
|
@@ -1093,7 +1313,7 @@ export class DapSessionManager {
|
|
|
1093
1313
|
Object.entries(args.env ?? {}).filter((entry): entry is [string, string] => entry[1] !== null),
|
|
1094
1314
|
);
|
|
1095
1315
|
const proc = ptree.spawn(args.args, {
|
|
1096
|
-
cwd: args.cwd ??
|
|
1316
|
+
cwd: path.resolve(session.cwd, args.cwd ?? "."),
|
|
1097
1317
|
stdin: "pipe",
|
|
1098
1318
|
env: {
|
|
1099
1319
|
...Bun.env,
|
|
@@ -1115,6 +1335,7 @@ export class DapSessionManager {
|
|
|
1115
1335
|
request,
|
|
1116
1336
|
name: typeof configuration.name === "string" ? configuration.name : undefined,
|
|
1117
1337
|
});
|
|
1338
|
+
await this.#startChildSession(session, request, configuration);
|
|
1118
1339
|
return {};
|
|
1119
1340
|
});
|
|
1120
1341
|
client.onEvent("output", body => {
|
|
@@ -1126,6 +1347,8 @@ export class DapSessionManager {
|
|
|
1126
1347
|
});
|
|
1127
1348
|
client.onEvent("stopped", body => {
|
|
1128
1349
|
this.#handleStoppedEvent(session, body as DapStoppedEventBody);
|
|
1350
|
+
this.#activeSessionId = session.id;
|
|
1351
|
+
this.#resolveTreeOutcome(session);
|
|
1129
1352
|
});
|
|
1130
1353
|
client.onEvent("continued", body => {
|
|
1131
1354
|
const continued = body as { threadId?: number } | undefined;
|
|
@@ -1135,11 +1358,19 @@ export class DapSessionManager {
|
|
|
1135
1358
|
});
|
|
1136
1359
|
client.onEvent("exited", body => {
|
|
1137
1360
|
session.exitCode = (body as DapExitedEventBody | undefined)?.exitCode;
|
|
1361
|
+
session.status = "terminated";
|
|
1362
|
+
this.#reactivateAfterTermination(session);
|
|
1363
|
+
this.#resolveTreeOutcome(session);
|
|
1138
1364
|
});
|
|
1139
1365
|
client.onEvent("terminated", () => {
|
|
1140
1366
|
session.status = "terminated";
|
|
1367
|
+
this.#reactivateAfterTermination(session);
|
|
1368
|
+
this.#resolveTreeOutcome(session);
|
|
1141
1369
|
});
|
|
1142
1370
|
this.#sessions.set(session.id, session);
|
|
1371
|
+
if (parentSessionId) {
|
|
1372
|
+
this.#sessions.get(parentSessionId)?.childSessionIds.add(session.id);
|
|
1373
|
+
}
|
|
1143
1374
|
this.#activeSessionId = session.id;
|
|
1144
1375
|
const heartbeat = setInterval(() => {
|
|
1145
1376
|
if (!client.isAlive()) {
|
|
@@ -1147,7 +1378,12 @@ export class DapSessionManager {
|
|
|
1147
1378
|
}
|
|
1148
1379
|
}, HEARTBEAT_INTERVAL_MS);
|
|
1149
1380
|
heartbeat.unref?.();
|
|
1150
|
-
client.proc.exited.finally(() =>
|
|
1381
|
+
void client.proc.exited.finally(() => {
|
|
1382
|
+
clearInterval(heartbeat);
|
|
1383
|
+
session.status = "terminated";
|
|
1384
|
+
this.#reactivateAfterTermination(session);
|
|
1385
|
+
this.#resolveTreeOutcome(session);
|
|
1386
|
+
});
|
|
1151
1387
|
return session;
|
|
1152
1388
|
}
|
|
1153
1389
|
|
|
@@ -1178,7 +1414,11 @@ export class DapSessionManager {
|
|
|
1178
1414
|
signal?: AbortSignal,
|
|
1179
1415
|
timeoutMs: number = 30_000,
|
|
1180
1416
|
): Promise<void> {
|
|
1181
|
-
if (
|
|
1417
|
+
if (session.configurationDoneSent) return;
|
|
1418
|
+
if (!session.needsConfigurationDone) {
|
|
1419
|
+
if (session.parentSessionId) {
|
|
1420
|
+
await this.#applyRootBreakpointsToSession(session, signal, timeoutMs);
|
|
1421
|
+
}
|
|
1182
1422
|
return;
|
|
1183
1423
|
}
|
|
1184
1424
|
// Wait for the initialized event if we haven't seen it yet.
|
|
@@ -1191,6 +1431,9 @@ export class DapSessionManager {
|
|
|
1191
1431
|
return;
|
|
1192
1432
|
}
|
|
1193
1433
|
}
|
|
1434
|
+
if (session.parentSessionId) {
|
|
1435
|
+
await this.#applyRootBreakpointsToSession(session, signal, timeoutMs);
|
|
1436
|
+
}
|
|
1194
1437
|
await session.client.sendRequest("configurationDone", {}, signal, timeoutMs);
|
|
1195
1438
|
session.configurationDoneSent = true;
|
|
1196
1439
|
if (session.status === "configuring") {
|
|
@@ -1261,19 +1504,39 @@ export class DapSessionManager {
|
|
|
1261
1504
|
* MUST be called before the command that triggers the event.
|
|
1262
1505
|
*/
|
|
1263
1506
|
#prepareStopOutcome(session: DapSession, signal?: AbortSignal, timeoutMs: number = 30_000): Promise<unknown> {
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1507
|
+
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
1508
|
+
const rootSessionId = this.#getRootSession(session).id;
|
|
1509
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
1510
|
+
let abortHandler: (() => void) | undefined;
|
|
1511
|
+
const cleanup = () => {
|
|
1512
|
+
clearTimeout(timeout);
|
|
1513
|
+
if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1514
|
+
this.#treeOutcomeWaiters.delete(waiter);
|
|
1515
|
+
};
|
|
1516
|
+
const waiter: DapTreeOutcomeWaiter = {
|
|
1517
|
+
rootSessionId,
|
|
1518
|
+
resolve: value => {
|
|
1519
|
+
cleanup();
|
|
1520
|
+
resolve(value);
|
|
1521
|
+
},
|
|
1522
|
+
reject: reason => {
|
|
1523
|
+
cleanup();
|
|
1524
|
+
reject(reason);
|
|
1525
|
+
},
|
|
1526
|
+
};
|
|
1527
|
+
this.#treeOutcomeWaiters.add(waiter);
|
|
1528
|
+
timeout = setTimeout(
|
|
1529
|
+
() => waiter.reject(new Error(`DAP session tree outcome timed out after ${timeoutMs}ms`)),
|
|
1530
|
+
timeoutMs,
|
|
1531
|
+
);
|
|
1532
|
+
if (signal) {
|
|
1533
|
+
abortHandler = () =>
|
|
1534
|
+
waiter.reject(signal.reason instanceof Error ? signal.reason : new Error("Debug operation aborted"));
|
|
1535
|
+
if (signal.aborted) abortHandler();
|
|
1536
|
+
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
1273
1537
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
return outcome;
|
|
1538
|
+
promise.catch(() => {});
|
|
1539
|
+
return promise;
|
|
1277
1540
|
}
|
|
1278
1541
|
|
|
1279
1542
|
/**
|
|
@@ -1287,17 +1550,29 @@ export class DapSessionManager {
|
|
|
1287
1550
|
): Promise<DapContinueOutcome> {
|
|
1288
1551
|
try {
|
|
1289
1552
|
await untilAborted(signal, outcomePromise);
|
|
1290
|
-
|
|
1291
|
-
|
|
1553
|
+
const active = this.#getActiveSessionOrNull();
|
|
1554
|
+
const resultSession =
|
|
1555
|
+
active && this.#getRootSession(active).id === this.#getRootSession(session).id ? active : session;
|
|
1556
|
+
if (resultSession.status === "stopped") {
|
|
1557
|
+
await this.#fetchTopFrame(resultSession, signal, Math.min(timeoutMs, 5_000));
|
|
1292
1558
|
}
|
|
1293
1559
|
const state =
|
|
1294
|
-
|
|
1295
|
-
|
|
1560
|
+
resultSession.status === "stopped"
|
|
1561
|
+
? "stopped"
|
|
1562
|
+
: resultSession.status === "terminated"
|
|
1563
|
+
? "terminated"
|
|
1564
|
+
: "running";
|
|
1565
|
+
return { snapshot: buildSummary(resultSession), state, timedOut: false };
|
|
1296
1566
|
} catch (error) {
|
|
1297
|
-
if (signal?.aborted)
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1567
|
+
if (signal?.aborted) throw error;
|
|
1568
|
+
const active = this.#getActiveSessionOrNull();
|
|
1569
|
+
const resultSession =
|
|
1570
|
+
active && this.#getRootSession(active).id === this.#getRootSession(session).id ? active : session;
|
|
1571
|
+
return {
|
|
1572
|
+
snapshot: buildSummary(resultSession),
|
|
1573
|
+
state: "running",
|
|
1574
|
+
timedOut: resultSession.status === "running",
|
|
1575
|
+
};
|
|
1301
1576
|
}
|
|
1302
1577
|
}
|
|
1303
1578
|
|
|
@@ -1326,7 +1601,7 @@ export class DapSessionManager {
|
|
|
1326
1601
|
): Promise<TBody> {
|
|
1327
1602
|
await this.#ensureConfigurationDone(session, signal, timeoutMs);
|
|
1328
1603
|
const body = await session.client.sendRequest<TBody>(command, args, signal, timeoutMs);
|
|
1329
|
-
session
|
|
1604
|
+
this.#touchSessionAndAncestors(session);
|
|
1330
1605
|
return body;
|
|
1331
1606
|
}
|
|
1332
1607
|
|
|
@@ -1403,7 +1678,7 @@ export class DapSessionManager {
|
|
|
1403
1678
|
|
|
1404
1679
|
#touchActiveSession(): DapSession {
|
|
1405
1680
|
const session = this.#getActiveSessionOrThrow();
|
|
1406
|
-
session
|
|
1681
|
+
this.#touchSessionAndAncestors(session);
|
|
1407
1682
|
if (session.status !== "terminated" && !session.client.isAlive()) {
|
|
1408
1683
|
session.status = "terminated";
|
|
1409
1684
|
}
|
|
@@ -1429,11 +1704,77 @@ export class DapSessionManager {
|
|
|
1429
1704
|
return session;
|
|
1430
1705
|
}
|
|
1431
1706
|
|
|
1432
|
-
#
|
|
1433
|
-
|
|
1434
|
-
|
|
1707
|
+
#getRootSession(session: DapSession): DapSession {
|
|
1708
|
+
let root = session;
|
|
1709
|
+
while (root.parentSessionId) {
|
|
1710
|
+
const parent = this.#sessions.get(root.parentSessionId);
|
|
1711
|
+
if (!parent) break;
|
|
1712
|
+
root = parent;
|
|
1713
|
+
}
|
|
1714
|
+
return root;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
#getTreeSessions(session: DapSession): DapSession[] {
|
|
1718
|
+
const sessions: DapSession[] = [];
|
|
1719
|
+
const pending = [this.#getRootSession(session)];
|
|
1720
|
+
while (pending.length > 0) {
|
|
1721
|
+
const current = pending.pop();
|
|
1722
|
+
if (!current) continue;
|
|
1723
|
+
sessions.push(current);
|
|
1724
|
+
for (const childId of current.childSessionIds) {
|
|
1725
|
+
const child = this.#sessions.get(childId);
|
|
1726
|
+
if (child) pending.push(child);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return sessions;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
#touchSessionAndAncestors(session: DapSession): void {
|
|
1733
|
+
const now = Date.now();
|
|
1734
|
+
let current: DapSession | undefined = session;
|
|
1735
|
+
while (current) {
|
|
1736
|
+
current.lastUsedAt = now;
|
|
1737
|
+
current = current.parentSessionId ? this.#sessions.get(current.parentSessionId) : undefined;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/** Point the active session at a live tree member when the active one terminates. */
|
|
1742
|
+
#reactivateAfterTermination(session: DapSession): void {
|
|
1743
|
+
if (this.#activeSessionId !== session.id) return;
|
|
1744
|
+
const live = this.#getTreeSessions(session).filter(
|
|
1745
|
+
candidate => candidate.status !== "terminated" && candidate.client.isAlive(),
|
|
1746
|
+
);
|
|
1747
|
+
if (live.length === 0) return;
|
|
1748
|
+
const replacement =
|
|
1749
|
+
live.find(candidate => candidate.status === "stopped") ??
|
|
1750
|
+
live.find(candidate => candidate.parentSessionId !== undefined) ??
|
|
1751
|
+
live[0];
|
|
1752
|
+
this.#activeSessionId = replacement.id;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
#resolveTreeOutcome(session: DapSession): void {
|
|
1756
|
+
const rootId = this.#getRootSession(session).id;
|
|
1757
|
+
for (const waiter of [...this.#treeOutcomeWaiters]) {
|
|
1758
|
+
if (waiter.rootSessionId === rootId) {
|
|
1759
|
+
waiter.resolve(undefined);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
#disposeSession(session: DapSession): void {
|
|
1765
|
+
if (!this.#sessions.has(session.id)) return;
|
|
1766
|
+
for (const childId of [...session.childSessionIds]) {
|
|
1767
|
+
const child = this.#sessions.get(childId);
|
|
1768
|
+
if (child) this.#disposeSession(child);
|
|
1435
1769
|
}
|
|
1436
1770
|
this.#sessions.delete(session.id);
|
|
1771
|
+
if (session.parentSessionId) {
|
|
1772
|
+
this.#sessions.get(session.parentSessionId)?.childSessionIds.delete(session.id);
|
|
1773
|
+
}
|
|
1774
|
+
if (this.#activeSessionId === session.id) {
|
|
1775
|
+
const parent = session.parentSessionId ? this.#sessions.get(session.parentSessionId) : undefined;
|
|
1776
|
+
this.#activeSessionId = parent?.id ?? this.#sessions.values().next().value?.id ?? null;
|
|
1777
|
+
}
|
|
1437
1778
|
void session.client.dispose().catch(() => {});
|
|
1438
1779
|
}
|
|
1439
1780
|
}
|