tmux-ide 2.9.4 → 2.9.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/bin/cli.js +4354 -3376
- package/package.json +7 -6
- package/packages/contracts/src/session-runtime.ts +2 -0
- package/packages/contracts/src/terminal-delivery.ts +4 -0
- package/packages/core/src/terminal-compact-capability.ts +31 -1
- package/packages/core/src/terminal-delivery.ts +234 -32
- package/packages/core/src/terminal-replica-hash-cache.ts +162 -21
- package/packages/core/src/terminal-replica-owned-row.ts +165 -10
- package/packages/core/src/terminal-replica.ts +43 -21
- package/packages/daemon/dist/lib/daemon-embed.js +19 -34
- package/packages/daemon/dist/lib/runtime-trace-writer.js +133 -0
- package/packages/daemon/dist/lib/tmux-client-execution.js +37 -2
- package/packages/daemon/dist/lib/tmux-server-owner.js +20 -4
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/darwin-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/darwin-x64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/licenses/libevent_core-2.1.so.7.txt +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +6 -6
- package/packages/daemon/dist/terminal/mirror/control-channel.js +379 -75
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +43 -3
- package/packages/daemon/dist/terminal/mirror/native-grid-projection.js +20 -11
- package/packages/daemon/dist/terminal/mirror/session-channel.js +639 -811
- package/packages/daemon/dist/terminal/mirror/stock-pane-snapshot.js +177 -0
- package/packages/daemon/dist/terminal/session-runtime/causal-cell-ledger.js +1 -1
- package/packages/daemon/dist/terminal/session-runtime/registry.js +145 -15
- package/packages/daemon/dist/terminal/session-runtime/stock-capture-fidelity.js +12 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +128 -10
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +21 -4
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +17 -19
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +64 -4
- package/packages/daemon/dist/tui/mirror/pane-surface.jsx +10 -1
- package/packages/daemon/dist/tui/mirror/reference-performance-trace.js +117 -71
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +27 -3
- package/packages/daemon/dist/tui/mirror/runtime/pane-scoped-terminal-surface.jsx +17 -4
- package/packages/daemon/dist/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.js +19 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +10 -2
- package/packages/daemon/dist/tui/mirror/testing/styled-frame-oracle.js +156 -0
- package/packages/daemon/src/lib/daemon-embed.ts +22 -34
- package/packages/daemon/src/lib/runtime-trace-writer.ts +132 -0
- package/packages/daemon/src/lib/tmux-client-execution.ts +47 -2
- package/packages/daemon/src/lib/tmux-server-owner.ts +19 -3
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +6 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +428 -84
- package/packages/daemon/src/terminal/mirror/events.ts +7 -7
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +49 -3
- package/packages/daemon/src/terminal/mirror/native-grid-projection.ts +21 -11
- package/packages/daemon/src/terminal/mirror/pane-feed.ts +9 -4
- package/packages/daemon/src/terminal/mirror/session-channel.ts +705 -859
- package/packages/daemon/src/terminal/mirror/stock-pane-snapshot.ts +228 -0
- package/packages/daemon/src/terminal/session-runtime/causal-cell-ledger.ts +1 -1
- package/packages/daemon/src/terminal/session-runtime/registry.ts +203 -32
- package/packages/daemon/src/terminal/session-runtime/runtime-observability.ts +7 -0
- package/packages/daemon/src/terminal/session-runtime/stock-capture-fidelity.ts +13 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +142 -8
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -3
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +25 -1
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +26 -20
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +63 -4
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -1
- package/packages/daemon/src/tui/mirror/reference-performance-trace.ts +141 -90
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +27 -4
- package/packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface.tsx +61 -31
- package/packages/daemon/src/tui/mirror/runtime/terminal-fast-lane-renderer-adapter.ts +31 -3
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +9 -6
- package/packages/daemon/src/tui/mirror/testing/styled-frame-oracle.ts +209 -0
- package/packages/daemon-client/src/terminal-fast-lane.ts +53 -4
- package/packages/daemon-client/src/workspace-client-types.ts +1 -0
- package/packages/daemon-client/src/workspace-client.ts +78 -5
- package/packages/tmux-bridge/src/index.ts +1 -0
- package/packages/tmux-bridge/src/runner.ts +25 -1
|
@@ -21,6 +21,7 @@ import { withBundledTmuxResources } from "../../lib/bundled-tmux.ts";
|
|
|
21
21
|
* chance to detach us — killing the reader first can wedge the server.
|
|
22
22
|
*/
|
|
23
23
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
24
25
|
import { shellEscape } from "../../lib/shell.ts";
|
|
25
26
|
import type { NativeTmuxServerIdentity } from "../../lib/tmux-server-generation-runner.ts";
|
|
26
27
|
import {
|
|
@@ -116,8 +117,13 @@ export type AtomicPaneSnapshotFailureReason =
|
|
|
116
117
|
| "retired";
|
|
117
118
|
|
|
118
119
|
export interface AtomicPaneSnapshotResult {
|
|
120
|
+
/** Present only for an owned pause hook; an already-paused target emits no new notification.
|
|
121
|
+
* The caller must authenticate the live target and exact nonwaiting hook body. */
|
|
122
|
+
readonly pauseObserved?: boolean;
|
|
119
123
|
readonly ok: boolean;
|
|
120
124
|
readonly captureLines: readonly string[];
|
|
125
|
+
/** Optional second representation; bounded together with captureLines. */
|
|
126
|
+
readonly ansiCaptureLines?: readonly string[];
|
|
121
127
|
readonly cursorLine: string | null;
|
|
122
128
|
readonly continueObserved: boolean;
|
|
123
129
|
readonly statusObserved: boolean;
|
|
@@ -140,6 +146,10 @@ export interface AtomicPaneSnapshotProgress {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
export interface AtomicPaneSnapshotCollector {
|
|
149
|
+
/** Pause uses start / refresh-client :pause / complete, sharing this sole wire owner. */
|
|
150
|
+
readonly kind?: "snapshot" | "pause";
|
|
151
|
+
/** Adds ANSI capture / ansi-capture-end after capture-end, before cursor. */
|
|
152
|
+
readonly dualCapture?: boolean;
|
|
143
153
|
readonly nonce: string;
|
|
144
154
|
readonly runtimePaneId: string;
|
|
145
155
|
readonly maxCaptureBytes: number;
|
|
@@ -151,6 +161,8 @@ export interface AtomicPaneSnapshotCollector {
|
|
|
151
161
|
/** Authenticated, bounded forward progress for the currently armed owner. */
|
|
152
162
|
readonly onProgress?: (progress: AtomicPaneSnapshotProgress) => void;
|
|
153
163
|
readonly onSettled: (result: AtomicPaneSnapshotResult) => void;
|
|
164
|
+
/** Sole wire-ownership release signal; failed/retired work requires a FIFO fence or exit. */
|
|
165
|
+
readonly onDrained?: (reason: "complete" | "fence" | "channel-exit") => void;
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
const ATOMIC_CAPTURE_BYTE_HARD_CAP = 16 * 1024 * 1024;
|
|
@@ -288,15 +300,19 @@ export class ControlChannelCore {
|
|
|
288
300
|
private failed = false;
|
|
289
301
|
private atomicCollector: {
|
|
290
302
|
spec: AtomicPaneSnapshotCollector;
|
|
303
|
+
retired: boolean;
|
|
304
|
+
settled: boolean;
|
|
291
305
|
started: boolean;
|
|
292
306
|
blockOrdinal: number;
|
|
293
307
|
blockContentCount: number;
|
|
294
308
|
captureLines: string[];
|
|
309
|
+
ansiCaptureLines: string[];
|
|
295
310
|
captureBytes: number;
|
|
296
311
|
totalLines: number;
|
|
297
312
|
totalBytes: number;
|
|
298
313
|
cursorLine: string | null;
|
|
299
314
|
continueObserved: boolean;
|
|
315
|
+
pauseObserved: boolean;
|
|
300
316
|
statusObserved: boolean;
|
|
301
317
|
observerEmissionObserved: boolean;
|
|
302
318
|
lastCompletedOrdinal: number;
|
|
@@ -437,6 +453,10 @@ export class ControlChannelCore {
|
|
|
437
453
|
|
|
438
454
|
armAtomicPaneSnapshotCollector(spec: AtomicPaneSnapshotCollector): boolean {
|
|
439
455
|
if (this.failed || this.atomicCollector) return false;
|
|
456
|
+
if (spec.kind !== undefined && spec.kind !== "snapshot" && spec.kind !== "pause") return false;
|
|
457
|
+
if (spec.kind === "pause" && (spec.observerCommandCount !== 0 || spec.dualCapture))
|
|
458
|
+
return false;
|
|
459
|
+
if (spec.dualCapture !== undefined && typeof spec.dualCapture !== "boolean") return false;
|
|
440
460
|
if (!/^[0-9a-f]{32,128}$/.test(spec.nonce) || !/^%\d+$/.test(spec.runtimePaneId)) return false;
|
|
441
461
|
if (
|
|
442
462
|
!Number.isSafeInteger(spec.maxCaptureBytes) ||
|
|
@@ -455,15 +475,19 @@ export class ControlChannelCore {
|
|
|
455
475
|
return false;
|
|
456
476
|
this.atomicCollector = {
|
|
457
477
|
spec,
|
|
478
|
+
retired: false,
|
|
479
|
+
settled: false,
|
|
458
480
|
started: false,
|
|
459
481
|
blockOrdinal: -1,
|
|
460
482
|
blockContentCount: 0,
|
|
461
483
|
captureLines: [],
|
|
484
|
+
ansiCaptureLines: [],
|
|
462
485
|
captureBytes: 0,
|
|
463
486
|
totalLines: 0,
|
|
464
487
|
totalBytes: 0,
|
|
465
488
|
cursorLine: null,
|
|
466
489
|
continueObserved: false,
|
|
490
|
+
pauseObserved: false,
|
|
467
491
|
statusObserved: false,
|
|
468
492
|
observerEmissionObserved: false,
|
|
469
493
|
lastCompletedOrdinal: -1,
|
|
@@ -477,27 +501,120 @@ export class ControlChannelCore {
|
|
|
477
501
|
reason: AtomicPaneSnapshotFailureReason = "retired",
|
|
478
502
|
): boolean {
|
|
479
503
|
const collector = this.atomicCollector;
|
|
480
|
-
if (!collector || collector.spec.nonce !== nonce) return false;
|
|
504
|
+
if (!collector || collector.spec.nonce !== nonce || collector.settled) return false;
|
|
505
|
+
collector.retired = true;
|
|
506
|
+
collector.settled = true;
|
|
507
|
+
const result: AtomicPaneSnapshotResult = Object.freeze({
|
|
508
|
+
ok: false,
|
|
509
|
+
captureLines: Object.freeze([]),
|
|
510
|
+
...(collector.spec.dualCapture ? { ansiCaptureLines: Object.freeze([]) } : {}),
|
|
511
|
+
cursorLine: null,
|
|
512
|
+
continueObserved: collector.continueObserved,
|
|
513
|
+
statusObserved: collector.statusObserved,
|
|
514
|
+
observerEmissionObserved: collector.observerEmissionObserved,
|
|
515
|
+
started: collector.started,
|
|
516
|
+
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
517
|
+
captureLineCount: collector.captureLines.length + collector.ansiCaptureLines.length,
|
|
518
|
+
captureByteCount: collector.captureBytes,
|
|
519
|
+
failureReason: collector.failureReason ?? reason,
|
|
520
|
+
...(collector.spec.kind === "pause" ? { pauseObserved: collector.pauseObserved } : {}),
|
|
521
|
+
});
|
|
522
|
+
// Release payload memory before callbacks can throw or reenter retirement.
|
|
523
|
+
collector.captureLines.length = 0;
|
|
524
|
+
collector.ansiCaptureLines.length = 0;
|
|
525
|
+
collector.cursorLine = null;
|
|
526
|
+
collector.totalBytes = 0;
|
|
527
|
+
collector.totalLines = 0;
|
|
528
|
+
collector.spec.onSettled(result);
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Caller has validated the separate ordinary command's exact fence reply. */
|
|
533
|
+
releaseRetiredCollector(nonce: string): boolean {
|
|
534
|
+
const collector = this.atomicCollector;
|
|
535
|
+
if (!collector?.retired || collector.spec.nonce !== nonce) return false;
|
|
481
536
|
this.atomicCollector = null;
|
|
482
|
-
|
|
483
|
-
Object.freeze({
|
|
484
|
-
ok: false,
|
|
485
|
-
captureLines: Object.freeze([]),
|
|
486
|
-
cursorLine: null,
|
|
487
|
-
continueObserved: collector.continueObserved,
|
|
488
|
-
statusObserved: collector.statusObserved,
|
|
489
|
-
observerEmissionObserved: collector.observerEmissionObserved,
|
|
490
|
-
started: collector.started,
|
|
491
|
-
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
492
|
-
captureLineCount: collector.captureLines.length,
|
|
493
|
-
captureByteCount: collector.captureBytes,
|
|
494
|
-
failureReason: collector.failureReason ?? reason,
|
|
495
|
-
}),
|
|
496
|
-
);
|
|
537
|
+
this.notifyAtomicDrained(collector, "fence");
|
|
497
538
|
return true;
|
|
498
539
|
}
|
|
499
540
|
|
|
541
|
+
private notifyAtomicDrained(
|
|
542
|
+
collector: NonNullable<ControlChannelCore["atomicCollector"]>,
|
|
543
|
+
reason: "complete" | "fence" | "channel-exit",
|
|
544
|
+
): void {
|
|
545
|
+
try {
|
|
546
|
+
collector.spec.onDrained?.(reason);
|
|
547
|
+
} catch {
|
|
548
|
+
this.failRetiredDrain("snapshot drain callback failed");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private failRetiredDrain(reason: string): void {
|
|
553
|
+
try {
|
|
554
|
+
this.fail(reason);
|
|
555
|
+
} finally {
|
|
556
|
+
this.handlers.onExit(reason);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private consumeRetiredSnapshotLine(line: string): boolean {
|
|
561
|
+
const collector = this.atomicCollector;
|
|
562
|
+
if (!collector?.retired) return false;
|
|
563
|
+
const event = parseControlLine(line, this.inReply);
|
|
564
|
+
if (this.inReply && this.currentReplyFlags === 0) {
|
|
565
|
+
collector.totalBytes += Buffer.byteLength(line, "latin1") + 1;
|
|
566
|
+
collector.totalLines += 1;
|
|
567
|
+
if (
|
|
568
|
+
collector.totalBytes > collector.spec.maxCaptureBytes + 64 * 1024 ||
|
|
569
|
+
collector.totalLines > collector.spec.maxCaptureLines + 64
|
|
570
|
+
) {
|
|
571
|
+
this.failRetiredDrain("retired snapshot drain exceeded bounds");
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
// Raw rows may look like %begin, a sentinel, or the fence payload. Only
|
|
575
|
+
// the matching raw terminator changes framing; none can redeem a FIFO slot.
|
|
576
|
+
if (event.kind === "end" || event.kind === "error") {
|
|
577
|
+
if (event.flags !== 0 || event.num !== this.currentReplyNum) {
|
|
578
|
+
this.failRetiredDrain("invalid retired snapshot framing");
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
this.inReply = false;
|
|
582
|
+
this.currentReplyNum = null;
|
|
583
|
+
this.currentReplyFlags = null;
|
|
584
|
+
this.currentReplyConsumesPending = false;
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (
|
|
589
|
+
event.kind === "begin" &&
|
|
590
|
+
(!Number.isSafeInteger(event.num) || event.num < 0 || ![0, 1].includes(event.flags))
|
|
591
|
+
) {
|
|
592
|
+
this.failRetiredDrain("invalid retired snapshot opening frame");
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
if (event.kind === "begin" && event.flags === 0) {
|
|
596
|
+
this.inReply = true;
|
|
597
|
+
this.currentReplyNum = event.num;
|
|
598
|
+
this.currentReplyFlags = 0;
|
|
599
|
+
this.currentReplyConsumesPending = false;
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
if (event.kind === "end" || event.kind === "error") {
|
|
603
|
+
if (
|
|
604
|
+
!this.inReply ||
|
|
605
|
+
event.num !== this.currentReplyNum ||
|
|
606
|
+
event.flags !== this.currentReplyFlags
|
|
607
|
+
) {
|
|
608
|
+
this.failRetiredDrain("invalid retired snapshot fence framing");
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Ordinary flags=1 responses still own their existing pending FIFO slots.
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
500
616
|
feed(chunk: string, receivedAtMicros?: number): void {
|
|
617
|
+
if (this.failed) return;
|
|
501
618
|
if (this.droppingOversizedReplyLine) {
|
|
502
619
|
const newline = chunk.indexOf("\n");
|
|
503
620
|
if (newline < 0) return;
|
|
@@ -515,6 +632,18 @@ export class ControlChannelCore {
|
|
|
515
632
|
const lineReceivedAtMicros = this.bufferReceivedAtMicros;
|
|
516
633
|
this.bufferReceivedAtMicros = this.buffer.length > 0 ? (receivedAtMicros ?? null) : null;
|
|
517
634
|
this.handleLine(line, lineReceivedAtMicros);
|
|
635
|
+
if (this.failed) {
|
|
636
|
+
this.buffer = "";
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (
|
|
641
|
+
this.atomicCollector?.retired &&
|
|
642
|
+
this.buffer.length > this.atomicCollector.spec.maxCaptureBytes + 64 * 1024
|
|
643
|
+
) {
|
|
644
|
+
this.buffer = "";
|
|
645
|
+
this.failRetiredDrain("retired snapshot partial line exceeded bounds");
|
|
646
|
+
return;
|
|
518
647
|
}
|
|
519
648
|
const head = this.currentBudget();
|
|
520
649
|
// Protocol terminators may themselves exceed a tiny payload budget and
|
|
@@ -535,23 +664,39 @@ export class ControlChannelCore {
|
|
|
535
664
|
fail(reason: string): void {
|
|
536
665
|
if (this.failed) return;
|
|
537
666
|
this.failed = true;
|
|
538
|
-
if (this.atomicCollector)
|
|
539
|
-
this.
|
|
667
|
+
if (this.atomicCollector) {
|
|
668
|
+
const collector = this.atomicCollector;
|
|
669
|
+
try {
|
|
670
|
+
this.retireAtomicPaneSnapshotCollector(collector.spec.nonce, "channel-exit");
|
|
671
|
+
} catch {
|
|
672
|
+
// A failed owner cannot prevent sibling requests from settling on exit.
|
|
673
|
+
}
|
|
674
|
+
this.atomicCollector = null;
|
|
675
|
+
try {
|
|
676
|
+
this.notifyAtomicDrained(collector, "channel-exit");
|
|
677
|
+
} catch {
|
|
678
|
+
// Exit notification callbacks may also fail; the pending FIFO still drains.
|
|
679
|
+
}
|
|
680
|
+
}
|
|
540
681
|
for (const sink of this.pending.splice(0)) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
sink.
|
|
544
|
-
|
|
545
|
-
sink.state.settled
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
sink.state.settled
|
|
554
|
-
|
|
682
|
+
try {
|
|
683
|
+
if (sink.kind === "promise") sink.reject(new Error(reason));
|
|
684
|
+
else if (sink.kind === "inline" || sink.kind === "bounded")
|
|
685
|
+
sink.onReply({ ok: false, lines: [reason] });
|
|
686
|
+
else if (sink.kind === "native-wrapper" && !sink.state.settled) {
|
|
687
|
+
sink.state.settled = true;
|
|
688
|
+
notifyNativeWrapper(sink.state, {
|
|
689
|
+
ok: false,
|
|
690
|
+
lines: [],
|
|
691
|
+
metadataStatus: sink.state.metadataStatus,
|
|
692
|
+
acknowledgement: sink.state.acknowledgement,
|
|
693
|
+
});
|
|
694
|
+
} else if (sink.kind === "command-list" && !sink.state.settled) {
|
|
695
|
+
sink.state.settled = true;
|
|
696
|
+
sink.state.onReply({ ok: false, lines: [reason] });
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
// Keep failure fan-out independent of any one request callback.
|
|
555
700
|
}
|
|
556
701
|
}
|
|
557
702
|
}
|
|
@@ -709,6 +854,7 @@ export class ControlChannelCore {
|
|
|
709
854
|
this.handlers.onOutput(event.pane, event.data, event.ageMs, timing);
|
|
710
855
|
break;
|
|
711
856
|
case "exit":
|
|
857
|
+
this.fail(event.reason ?? "control channel exited");
|
|
712
858
|
this.handlers.onExit(event.reason);
|
|
713
859
|
break;
|
|
714
860
|
case "notify":
|
|
@@ -720,6 +866,7 @@ export class ControlChannelCore {
|
|
|
720
866
|
private consumeAtomicPaneSnapshotLine(line: string): boolean {
|
|
721
867
|
const collector = this.atomicCollector;
|
|
722
868
|
if (!collector) return false;
|
|
869
|
+
if (collector.retired) return this.consumeRetiredSnapshotLine(line);
|
|
723
870
|
const prefix = "%tmux-ide-atomic-v1 ";
|
|
724
871
|
const ownPrefix = `${prefix}${collector.spec.nonce} `;
|
|
725
872
|
if (line.startsWith(prefix) && !line.startsWith(ownPrefix)) {
|
|
@@ -733,7 +880,9 @@ export class ControlChannelCore {
|
|
|
733
880
|
token !== "start" ||
|
|
734
881
|
!this.inReply ||
|
|
735
882
|
this.currentReplyFlags !== 0 ||
|
|
736
|
-
this.currentReplyNum === null
|
|
883
|
+
this.currentReplyNum === null ||
|
|
884
|
+
(collector.spec.kind === "pause" &&
|
|
885
|
+
(!Number.isSafeInteger(this.currentReplyNum) || this.currentReplyNum < 0))
|
|
737
886
|
) {
|
|
738
887
|
collector.failureReason ??= "sentinel-order";
|
|
739
888
|
return true;
|
|
@@ -751,6 +900,9 @@ export class ControlChannelCore {
|
|
|
751
900
|
if (collector.totalBytes > collector.spec.maxCaptureBytes + 64 * 1024)
|
|
752
901
|
collector.failureReason ??= "capture-byte-cap";
|
|
753
902
|
|
|
903
|
+
if (collector.spec.kind === "pause") return this.consumeOwnedPauseLine(line, token);
|
|
904
|
+
|
|
905
|
+
const shift = collector.spec.dualCapture ? 2 : 0;
|
|
754
906
|
const guard = parseControlLine(line, this.inReply);
|
|
755
907
|
if (guard.kind === "begin") {
|
|
756
908
|
if (guard.flags !== 0 || this.inReply) collector.failureReason ??= "sentinel-order";
|
|
@@ -772,12 +924,21 @@ export class ControlChannelCore {
|
|
|
772
924
|
// The ownership compare-and-unset is an if-shell command plus exactly
|
|
773
925
|
// one selected branch command. Both commands retain the control client
|
|
774
926
|
// and therefore each has its own flags=0 guard block.
|
|
775
|
-
const markerBranchOrdinal = 7 + collector.spec.observerCommandCount;
|
|
776
|
-
const statusOrdinal = 8 + collector.spec.observerCommandCount;
|
|
777
|
-
const completeOrdinal = 10 + collector.spec.observerCommandCount;
|
|
778
|
-
const contentRequired = new Set([
|
|
927
|
+
const markerBranchOrdinal = 7 + shift + collector.spec.observerCommandCount;
|
|
928
|
+
const statusOrdinal = 8 + shift + collector.spec.observerCommandCount;
|
|
929
|
+
const completeOrdinal = 10 + shift + collector.spec.observerCommandCount;
|
|
930
|
+
const contentRequired = new Set([
|
|
931
|
+
0,
|
|
932
|
+
2,
|
|
933
|
+
3 + shift,
|
|
934
|
+
4 + shift,
|
|
935
|
+
...(shift ? [4] : []),
|
|
936
|
+
statusOrdinal,
|
|
937
|
+
completeOrdinal,
|
|
938
|
+
]);
|
|
779
939
|
const contentSilent =
|
|
780
940
|
collector.blockOrdinal !== 1 &&
|
|
941
|
+
!(shift && collector.blockOrdinal === 3) &&
|
|
781
942
|
collector.blockOrdinal !== markerBranchOrdinal &&
|
|
782
943
|
!contentRequired.has(collector.blockOrdinal);
|
|
783
944
|
const markerBranchValid =
|
|
@@ -792,7 +953,7 @@ export class ControlChannelCore {
|
|
|
792
953
|
guard.kind === "end" &&
|
|
793
954
|
!guardInvalid &&
|
|
794
955
|
collector.spec.observerCommandCount > 0 &&
|
|
795
|
-
collector.blockOrdinal === 5 + collector.spec.observerCommandCount
|
|
956
|
+
collector.blockOrdinal === 5 + shift + collector.spec.observerCommandCount
|
|
796
957
|
)
|
|
797
958
|
collector.observerEmissionObserved = true;
|
|
798
959
|
if (!guardInvalid && guard.kind === "end") {
|
|
@@ -803,31 +964,19 @@ export class ControlChannelCore {
|
|
|
803
964
|
this.currentReplyNum = null;
|
|
804
965
|
this.currentReplyFlags = null;
|
|
805
966
|
if (collector.blockOrdinal === completeOrdinal) {
|
|
806
|
-
this.atomicCollector = null;
|
|
807
967
|
const ok =
|
|
808
968
|
collector.failureReason === null &&
|
|
809
969
|
collector.cursorLine !== null &&
|
|
810
970
|
collector.statusObserved;
|
|
811
|
-
|
|
812
|
-
Object.freeze({
|
|
813
|
-
ok,
|
|
814
|
-
captureLines: Object.freeze(ok ? [...collector.captureLines] : []),
|
|
815
|
-
cursorLine: ok ? collector.cursorLine : null,
|
|
816
|
-
continueObserved: collector.continueObserved,
|
|
817
|
-
statusObserved: collector.statusObserved,
|
|
818
|
-
observerEmissionObserved: collector.observerEmissionObserved,
|
|
819
|
-
started: collector.started,
|
|
820
|
-
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
821
|
-
captureLineCount: collector.captureLines.length,
|
|
822
|
-
captureByteCount: collector.captureBytes,
|
|
823
|
-
failureReason: ok ? null : (collector.failureReason ?? "sentinel-order"),
|
|
824
|
-
}),
|
|
825
|
-
);
|
|
971
|
+
this.completeAtomicCollector(collector, ok);
|
|
826
972
|
}
|
|
827
973
|
return true;
|
|
828
974
|
}
|
|
829
975
|
|
|
830
|
-
if (
|
|
976
|
+
if (
|
|
977
|
+
line === `%continue ${collector.spec.runtimePaneId}` &&
|
|
978
|
+
collector.blockOrdinal === 5 + shift
|
|
979
|
+
) {
|
|
831
980
|
if (collector.continueObserved) collector.failureReason ??= "duplicate-sentinel";
|
|
832
981
|
collector.continueObserved = true;
|
|
833
982
|
return true;
|
|
@@ -842,21 +991,27 @@ export class ControlChannelCore {
|
|
|
842
991
|
return true;
|
|
843
992
|
}
|
|
844
993
|
|
|
845
|
-
const markerBranchOrdinal = 7 + collector.spec.observerCommandCount;
|
|
846
|
-
const statusOrdinal = 8 + collector.spec.observerCommandCount;
|
|
847
|
-
const completeOrdinal = 10 + collector.spec.observerCommandCount;
|
|
994
|
+
const markerBranchOrdinal = 7 + shift + collector.spec.observerCommandCount;
|
|
995
|
+
const statusOrdinal = 8 + shift + collector.spec.observerCommandCount;
|
|
996
|
+
const completeOrdinal = 10 + shift + collector.spec.observerCommandCount;
|
|
848
997
|
collector.blockContentCount += 1;
|
|
849
|
-
if (collector.blockOrdinal === 1) {
|
|
998
|
+
if (collector.blockOrdinal === 1 || (shift && collector.blockOrdinal === 3)) {
|
|
850
999
|
collector.captureBytes += Buffer.byteLength(line, "latin1") + 1;
|
|
851
1000
|
if (collector.captureBytes > collector.spec.maxCaptureBytes)
|
|
852
1001
|
collector.failureReason ??= "capture-byte-cap";
|
|
853
|
-
if (
|
|
1002
|
+
if (
|
|
1003
|
+
collector.captureLines.length + collector.ansiCaptureLines.length >=
|
|
1004
|
+
collector.spec.maxCaptureLines
|
|
1005
|
+
)
|
|
854
1006
|
collector.failureReason ??= "capture-line-cap";
|
|
855
|
-
else
|
|
1007
|
+
else
|
|
1008
|
+
(collector.blockOrdinal === 1 ? collector.captureLines : collector.ansiCaptureLines).push(
|
|
1009
|
+
line,
|
|
1010
|
+
);
|
|
856
1011
|
this.reportAtomicPaneSnapshotProgress(collector);
|
|
857
1012
|
return true;
|
|
858
1013
|
}
|
|
859
|
-
if (collector.blockOrdinal === 3) {
|
|
1014
|
+
if (collector.blockOrdinal === 3 + shift) {
|
|
860
1015
|
if (collector.cursorLine !== null) collector.failureReason ??= "cursor-cardinality";
|
|
861
1016
|
else if (Buffer.byteLength(line, "latin1") > collector.spec.maxCursorBytes)
|
|
862
1017
|
collector.failureReason ??= "cursor-byte-cap";
|
|
@@ -871,13 +1026,15 @@ export class ControlChannelCore {
|
|
|
871
1026
|
const expectedToken =
|
|
872
1027
|
collector.blockOrdinal === 2
|
|
873
1028
|
? "capture-end"
|
|
874
|
-
: collector.blockOrdinal === 4
|
|
875
|
-
? "
|
|
876
|
-
: collector.blockOrdinal ===
|
|
877
|
-
? "
|
|
878
|
-
: collector.blockOrdinal ===
|
|
879
|
-
? "
|
|
880
|
-
:
|
|
1029
|
+
: shift && collector.blockOrdinal === 4
|
|
1030
|
+
? "ansi-capture-end"
|
|
1031
|
+
: collector.blockOrdinal === 4 + shift
|
|
1032
|
+
? "cursor-end"
|
|
1033
|
+
: collector.blockOrdinal === statusOrdinal
|
|
1034
|
+
? "status-ok"
|
|
1035
|
+
: collector.blockOrdinal === completeOrdinal
|
|
1036
|
+
? "complete"
|
|
1037
|
+
: null;
|
|
881
1038
|
if (expectedToken === null || token !== expectedToken)
|
|
882
1039
|
collector.failureReason ??= token === "start" ? "duplicate-sentinel" : "sentinel-order";
|
|
883
1040
|
if (collector.blockOrdinal === statusOrdinal && token === "status-ok") {
|
|
@@ -887,6 +1044,106 @@ export class ControlChannelCore {
|
|
|
887
1044
|
return true;
|
|
888
1045
|
}
|
|
889
1046
|
|
|
1047
|
+
private completeAtomicCollector(
|
|
1048
|
+
collector: NonNullable<ControlChannelCore["atomicCollector"]>,
|
|
1049
|
+
ok: boolean,
|
|
1050
|
+
): void {
|
|
1051
|
+
if (!ok) {
|
|
1052
|
+
this.retireAtomicPaneSnapshotCollector(
|
|
1053
|
+
collector.spec.nonce,
|
|
1054
|
+
collector.failureReason ?? "sentinel-order",
|
|
1055
|
+
);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const result: AtomicPaneSnapshotResult = Object.freeze({
|
|
1059
|
+
ok: true,
|
|
1060
|
+
captureLines: Object.freeze([...collector.captureLines]),
|
|
1061
|
+
...(collector.spec.dualCapture
|
|
1062
|
+
? { ansiCaptureLines: Object.freeze([...collector.ansiCaptureLines]) }
|
|
1063
|
+
: {}),
|
|
1064
|
+
cursorLine: collector.cursorLine,
|
|
1065
|
+
continueObserved: collector.continueObserved,
|
|
1066
|
+
statusObserved: collector.statusObserved,
|
|
1067
|
+
observerEmissionObserved: collector.observerEmissionObserved,
|
|
1068
|
+
started: collector.started,
|
|
1069
|
+
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
1070
|
+
captureLineCount: collector.captureLines.length + collector.ansiCaptureLines.length,
|
|
1071
|
+
captureByteCount: collector.captureBytes,
|
|
1072
|
+
failureReason: null,
|
|
1073
|
+
...(collector.spec.kind === "pause" ? { pauseObserved: collector.pauseObserved } : {}),
|
|
1074
|
+
});
|
|
1075
|
+
// Settlement cannot admit another collector before terminal framing drains.
|
|
1076
|
+
collector.settled = true;
|
|
1077
|
+
try {
|
|
1078
|
+
collector.spec.onSettled(result);
|
|
1079
|
+
} catch {
|
|
1080
|
+
this.failRetiredDrain("snapshot settlement callback failed");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (this.atomicCollector === collector) {
|
|
1084
|
+
this.atomicCollector = null;
|
|
1085
|
+
this.notifyAtomicDrained(collector, "complete");
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private consumeOwnedPauseLine(line: string, token: string | null): boolean {
|
|
1090
|
+
const collector = this.atomicCollector!;
|
|
1091
|
+
const guard = parseControlLine(line, this.inReply);
|
|
1092
|
+
if (guard.kind === "begin") {
|
|
1093
|
+
if (guard.flags !== 0 || this.inReply || !Number.isSafeInteger(guard.num) || guard.num < 0)
|
|
1094
|
+
collector.failureReason ??= "sentinel-order";
|
|
1095
|
+
this.inReply = true;
|
|
1096
|
+
this.currentReplyNum = guard.num;
|
|
1097
|
+
this.currentReplyFlags = guard.flags;
|
|
1098
|
+
collector.blockOrdinal += 1;
|
|
1099
|
+
collector.blockContentCount = 0;
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
if (guard.kind === "end" || guard.kind === "error") {
|
|
1103
|
+
const valid =
|
|
1104
|
+
this.inReply &&
|
|
1105
|
+
guard.kind === "end" &&
|
|
1106
|
+
guard.flags === 0 &&
|
|
1107
|
+
this.currentReplyFlags === 0 &&
|
|
1108
|
+
guard.num === this.currentReplyNum;
|
|
1109
|
+
if (
|
|
1110
|
+
!valid ||
|
|
1111
|
+
(collector.blockOrdinal === 1
|
|
1112
|
+
? collector.blockContentCount > 1
|
|
1113
|
+
: collector.blockContentCount !== 1)
|
|
1114
|
+
)
|
|
1115
|
+
collector.failureReason ??= "sentinel-order";
|
|
1116
|
+
if (valid) collector.lastCompletedOrdinal = collector.blockOrdinal;
|
|
1117
|
+
this.inReply = false;
|
|
1118
|
+
this.currentReplyNum = null;
|
|
1119
|
+
this.currentReplyFlags = null;
|
|
1120
|
+
this.currentReplyConsumesPending = false;
|
|
1121
|
+
if (collector.blockOrdinal === 2)
|
|
1122
|
+
this.completeAtomicCollector(
|
|
1123
|
+
collector,
|
|
1124
|
+
collector.failureReason === null && collector.statusObserved,
|
|
1125
|
+
);
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
if (!this.inReply && (line === "%exit" || line.startsWith("%exit "))) {
|
|
1129
|
+
this.retireAtomicPaneSnapshotCollector(collector.spec.nonce, "channel-exit");
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
if (!this.inReply) {
|
|
1133
|
+
collector.failureReason ??= "unexpected-post-line";
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
collector.blockContentCount += 1;
|
|
1137
|
+
if (collector.blockOrdinal === 1 && line === `%pause ${collector.spec.runtimePaneId}`) {
|
|
1138
|
+
if (collector.pauseObserved) collector.failureReason ??= "duplicate-sentinel";
|
|
1139
|
+
collector.pauseObserved = true;
|
|
1140
|
+
} else if (collector.blockOrdinal === 2 && token === "complete") {
|
|
1141
|
+
if (collector.statusObserved) collector.failureReason ??= "duplicate-sentinel";
|
|
1142
|
+
collector.statusObserved = true;
|
|
1143
|
+
} else collector.failureReason ??= "sentinel-order";
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
890
1147
|
private reportAtomicPaneSnapshotProgress(
|
|
891
1148
|
collector: NonNullable<ControlChannelCore["atomicCollector"]>,
|
|
892
1149
|
): void {
|
|
@@ -895,7 +1152,10 @@ export class ControlChannelCore {
|
|
|
895
1152
|
Object.freeze({
|
|
896
1153
|
started: collector.started,
|
|
897
1154
|
lastCompletedOrdinal: collector.lastCompletedOrdinal,
|
|
898
|
-
captureLineCount: Math.min(
|
|
1155
|
+
captureLineCount: Math.min(
|
|
1156
|
+
collector.captureLines.length + collector.ansiCaptureLines.length,
|
|
1157
|
+
ATOMIC_CAPTURE_LINE_HARD_CAP,
|
|
1158
|
+
),
|
|
899
1159
|
captureByteCount: Math.min(collector.captureBytes, ATOMIC_CAPTURE_BYTE_HARD_CAP),
|
|
900
1160
|
continueObserved: collector.continueObserved,
|
|
901
1161
|
statusObserved: collector.statusObserved,
|
|
@@ -985,7 +1245,13 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
985
1245
|
private viewerPending = 0;
|
|
986
1246
|
private verifiedAttach = false;
|
|
987
1247
|
private guardedStart: { resolve: () => void; reject: (error: Error) => void } | null = null;
|
|
988
|
-
private
|
|
1248
|
+
private atomicCollectorOwner: {
|
|
1249
|
+
nonce: string;
|
|
1250
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
1251
|
+
drainTimer: ReturnType<typeof setTimeout> | null;
|
|
1252
|
+
retiring: boolean;
|
|
1253
|
+
completed: boolean;
|
|
1254
|
+
} | null = null;
|
|
989
1255
|
|
|
990
1256
|
constructor(opts: MirrorControlChannelOptions) {
|
|
991
1257
|
this.opts = opts;
|
|
@@ -1002,7 +1268,13 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
1002
1268
|
}
|
|
1003
1269
|
if (!opts.nativeServerIdentity || this.verifiedAttach) opts.handlers.onNotify(name, rest);
|
|
1004
1270
|
},
|
|
1005
|
-
onExit: (reason) =>
|
|
1271
|
+
onExit: (reason) => {
|
|
1272
|
+
try {
|
|
1273
|
+
this.noteExit(reason);
|
|
1274
|
+
} finally {
|
|
1275
|
+
void this.dispose();
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1006
1278
|
},
|
|
1007
1279
|
opts.nowMicros,
|
|
1008
1280
|
);
|
|
@@ -1303,21 +1575,47 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
1303
1575
|
}
|
|
1304
1576
|
|
|
1305
1577
|
armAtomicPaneSnapshotCollector(spec: AtomicPaneSnapshotCollector, timeoutMs: number): boolean {
|
|
1578
|
+
if (this.exited || !this.proc?.stdin?.writable) return false;
|
|
1306
1579
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 5_000) return false;
|
|
1580
|
+
const owner = {
|
|
1581
|
+
nonce: spec.nonce,
|
|
1582
|
+
timer: null as ReturnType<typeof setTimeout> | null,
|
|
1583
|
+
drainTimer: null as ReturnType<typeof setTimeout> | null,
|
|
1584
|
+
retiring: false,
|
|
1585
|
+
completed: false,
|
|
1586
|
+
};
|
|
1587
|
+
const clear = () => {
|
|
1588
|
+
if (owner.timer) clearTimeout(owner.timer);
|
|
1589
|
+
if (owner.drainTimer) clearTimeout(owner.drainTimer);
|
|
1590
|
+
owner.timer = owner.drainTimer = null;
|
|
1591
|
+
if (this.atomicCollectorOwner === owner) this.atomicCollectorOwner = null;
|
|
1592
|
+
};
|
|
1307
1593
|
const wrapped = {
|
|
1308
1594
|
...spec,
|
|
1309
1595
|
onSettled: (result: AtomicPaneSnapshotResult): void => {
|
|
1310
|
-
if (
|
|
1311
|
-
|
|
1596
|
+
if (owner.timer) clearTimeout(owner.timer);
|
|
1597
|
+
owner.timer = null;
|
|
1598
|
+
owner.completed = result.ok;
|
|
1599
|
+
if (!result.ok && result.failureReason !== "channel-exit" && !owner.retiring) {
|
|
1600
|
+
this.beginCollectorDrain(spec.nonce, result.failureReason ?? "retired", () =>
|
|
1601
|
+
spec.onSettled(result),
|
|
1602
|
+
);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1312
1605
|
spec.onSettled(result);
|
|
1313
1606
|
},
|
|
1607
|
+
onDrained: (reason: "complete" | "fence" | "channel-exit"): void => {
|
|
1608
|
+
clear();
|
|
1609
|
+
spec.onDrained?.(reason);
|
|
1610
|
+
},
|
|
1314
1611
|
};
|
|
1315
1612
|
if (!this.core.armAtomicPaneSnapshotCollector(wrapped)) return false;
|
|
1316
|
-
this.
|
|
1317
|
-
|
|
1318
|
-
this.
|
|
1319
|
-
|
|
1320
|
-
|
|
1613
|
+
this.atomicCollectorOwner = owner;
|
|
1614
|
+
owner.timer = setTimeout(
|
|
1615
|
+
() => this.retireAtomicPaneSnapshotCollector(spec.nonce, "timeout"),
|
|
1616
|
+
timeoutMs,
|
|
1617
|
+
);
|
|
1618
|
+
owner.timer.unref?.();
|
|
1321
1619
|
return true;
|
|
1322
1620
|
}
|
|
1323
1621
|
|
|
@@ -1325,9 +1623,54 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
1325
1623
|
nonce: string,
|
|
1326
1624
|
reason: AtomicPaneSnapshotFailureReason = "retired",
|
|
1327
1625
|
): void {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1626
|
+
this.beginCollectorDrain(nonce, reason);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
private beginCollectorDrain(
|
|
1630
|
+
nonce: string,
|
|
1631
|
+
reason: AtomicPaneSnapshotFailureReason,
|
|
1632
|
+
settle?: () => void,
|
|
1633
|
+
): void {
|
|
1634
|
+
const owner = this.atomicCollectorOwner;
|
|
1635
|
+
if (!owner || owner.nonce !== nonce || owner.retiring || owner.completed) return;
|
|
1636
|
+
owner.retiring = true;
|
|
1637
|
+
if (owner.timer) clearTimeout(owner.timer);
|
|
1638
|
+
owner.timer = null;
|
|
1639
|
+
const abort = (message: string) => {
|
|
1640
|
+
if (this.atomicCollectorOwner !== owner) return;
|
|
1641
|
+
try {
|
|
1642
|
+
this.core.fail(message);
|
|
1643
|
+
this.noteExit(message);
|
|
1644
|
+
} finally {
|
|
1645
|
+
void this.dispose();
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
// Establish drain ownership before onSettled can reenter and try another arm.
|
|
1649
|
+
owner.drainTimer = setTimeout(() => abort("retired snapshot drain timed out"), 5_000);
|
|
1650
|
+
owner.drainTimer.unref?.();
|
|
1651
|
+
try {
|
|
1652
|
+
this.core.retireAtomicPaneSnapshotCollector(nonce, reason);
|
|
1653
|
+
// Natural failed completion already settled the core tombstone. Retire
|
|
1654
|
+
// application ownership before queuing a fence behind dispatched work.
|
|
1655
|
+
settle?.();
|
|
1656
|
+
} catch {
|
|
1657
|
+
abort("retired snapshot settlement callback failed");
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (this.atomicCollectorOwner !== owner) return;
|
|
1661
|
+
const fence = `tmux-ide-collector-drain-v1:${randomBytes(24).toString("hex")}`;
|
|
1662
|
+
this.commandBoundedInline(
|
|
1663
|
+
`display-message -p -l ${fence}`,
|
|
1664
|
+
{ maxBytes: 256, maxLines: 1 },
|
|
1665
|
+
(reply) => {
|
|
1666
|
+
if (this.atomicCollectorOwner !== owner) return;
|
|
1667
|
+
if (!reply.ok || reply.lines.length !== 1 || reply.lines[0] !== fence) {
|
|
1668
|
+
abort("invalid retired snapshot drain fence");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
if (!this.core.releaseRetiredCollector(nonce)) abort("retired snapshot owner changed");
|
|
1672
|
+
},
|
|
1673
|
+
);
|
|
1331
1674
|
}
|
|
1332
1675
|
|
|
1333
1676
|
send(cmd: string, onReply?: (reply: ControlReply) => void): void {
|
|
@@ -1352,8 +1695,9 @@ export class MirrorControlChannel implements MirrorChannelIo {
|
|
|
1352
1695
|
*/
|
|
1353
1696
|
async dispose(): Promise<void> {
|
|
1354
1697
|
this.retireNativeViewer();
|
|
1355
|
-
|
|
1356
|
-
|
|
1698
|
+
const collectorOwner = this.atomicCollectorOwner;
|
|
1699
|
+
if (collectorOwner?.timer) clearTimeout(collectorOwner.timer);
|
|
1700
|
+
if (collectorOwner?.drainTimer) clearTimeout(collectorOwner.drainTimer);
|
|
1357
1701
|
const proc = this.proc;
|
|
1358
1702
|
this.proc = null;
|
|
1359
1703
|
if (!proc) return;
|