@oh-my-pi/pi-utils 18.17.0 → 18.18.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.
- package/CHANGELOG.md +6 -0
- package/package.json +3 -3
- package/src/ptree.ts +55 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.18.0] - 2026-11-07
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed abort signals leaving command output reads blocked when a descendant keeps stdout or stderr open after the root process exits. ([#18309](https://github.com/can1357/oh-my-pi/pull/18309) by [@eggpeat](https://github.com/eggpeat))
|
|
10
|
+
|
|
5
11
|
## [18.17.0] - 2026-11-06
|
|
6
12
|
|
|
7
13
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-utils",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.18.0",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@oh-my-pi/omptype": "18.
|
|
60
|
-
"@oh-my-pi/pi-natives": "18.
|
|
59
|
+
"@oh-my-pi/omptype": "18.18.0",
|
|
60
|
+
"@oh-my-pi/pi-natives": "18.18.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.3.14"
|
package/src/ptree.ts
CHANGED
|
@@ -184,12 +184,15 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
184
184
|
#stderrDone: Promise<void>;
|
|
185
185
|
#exited: Promise<number>;
|
|
186
186
|
#openPipeReaders = 1;
|
|
187
|
-
// Set
|
|
188
|
-
//
|
|
189
|
-
//
|
|
187
|
+
// Set at the command deadline or on abort; otherwise commands keep complete
|
|
188
|
+
// EOF-based capture. Active pipe readers are cancelled at cutoff so their
|
|
189
|
+
// pending read() settles as done and partial output is kept.
|
|
190
190
|
#cutoff = false;
|
|
191
|
-
/** Readers of piped stdio;
|
|
191
|
+
/** Readers of piped stdio; timeout and abort cancel them so pending reads settle. */
|
|
192
192
|
#pipeReaders = new Set<{ cancel(reason?: unknown): Promise<void> }>();
|
|
193
|
+
// Each registration reconciles its listener with root-exit and reader activity.
|
|
194
|
+
// Keeping the registration after idle lets a later output read re-arm the signal.
|
|
195
|
+
#abortRegistrations?: Set<() => void>;
|
|
193
196
|
#timeoutTimer?: NodeJS.Timeout;
|
|
194
197
|
#stderrStream?: ReadableStream<Uint8Array>;
|
|
195
198
|
// Termination in flight after kill(); aborted exits await it before reporting.
|
|
@@ -249,11 +252,13 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
249
252
|
this.#openPipeReaders--;
|
|
250
253
|
this.#stderrTail += dec.decode();
|
|
251
254
|
trim();
|
|
255
|
+
this.#syncAbortListeners();
|
|
252
256
|
})();
|
|
253
257
|
|
|
254
258
|
proc.exited
|
|
255
259
|
.catch(() => null)
|
|
256
260
|
.then(async exitCode => {
|
|
261
|
+
this.#syncAbortListeners();
|
|
257
262
|
if (this.#exitReasonPending) {
|
|
258
263
|
this.#exitReason = this.#exitReasonPending;
|
|
259
264
|
reject(this.#exitReasonPending);
|
|
@@ -522,12 +527,13 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
522
527
|
}
|
|
523
528
|
|
|
524
529
|
/**
|
|
525
|
-
* Read a pipe fully, stopping early only at an explicit command deadline.
|
|
530
|
+
* Read a pipe fully, stopping early only at an explicit command deadline or abort.
|
|
526
531
|
*/
|
|
527
532
|
async #readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
528
|
-
this.#openPipeReaders++;
|
|
529
533
|
const reader = stream.getReader();
|
|
534
|
+
this.#openPipeReaders++;
|
|
530
535
|
this.#pipeReaders.add(reader);
|
|
536
|
+
this.#syncAbortListeners();
|
|
531
537
|
const dec = new TextDecoder();
|
|
532
538
|
let out = "";
|
|
533
539
|
try {
|
|
@@ -543,15 +549,18 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
543
549
|
} catch {
|
|
544
550
|
// A cancelled or failed read keeps whatever was already collected.
|
|
545
551
|
}
|
|
552
|
+
const tail = dec.decode();
|
|
546
553
|
this.#pipeReaders.delete(reader);
|
|
547
554
|
this.#openPipeReaders--;
|
|
548
|
-
|
|
555
|
+
this.#syncAbortListeners();
|
|
556
|
+
return out + tail;
|
|
549
557
|
}
|
|
550
558
|
|
|
551
559
|
async #readBytes(): Promise<Uint8Array<ArrayBuffer>> {
|
|
552
560
|
const reader = this.proc.stdout.getReader();
|
|
553
561
|
this.#openPipeReaders++;
|
|
554
562
|
this.#pipeReaders.add(reader);
|
|
563
|
+
this.#syncAbortListeners();
|
|
555
564
|
const chunks: Uint8Array[] = [];
|
|
556
565
|
let length = 0;
|
|
557
566
|
try {
|
|
@@ -571,6 +580,7 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
571
580
|
this.#pipeReaders.delete(reader);
|
|
572
581
|
this.#openPipeReaders--;
|
|
573
582
|
reader.releaseLock();
|
|
583
|
+
this.#syncAbortListeners();
|
|
574
584
|
}
|
|
575
585
|
const bytes = new Uint8Array(length);
|
|
576
586
|
let offset = 0;
|
|
@@ -653,11 +663,43 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
653
663
|
// ── Signal / timeout ─────────────────────────────────────────────────
|
|
654
664
|
|
|
655
665
|
attachSignal(signal: AbortSignal): void {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
666
|
+
this.#exited.catch(() => {});
|
|
667
|
+
let attached = false;
|
|
668
|
+
const onAbort = () => {
|
|
669
|
+
// The { once: true } listener has removed itself at this point.
|
|
670
|
+
attached = false;
|
|
671
|
+
const rootRunning = this.proc.exitCode === null && this.proc.signalCode === null;
|
|
672
|
+
if (rootRunning || this.#openPipeReaders > 0) {
|
|
673
|
+
this.#abortRegistrations?.delete(sync);
|
|
674
|
+
}
|
|
675
|
+
this.kill(new AbortError(signal.reason, "<cancelled>"));
|
|
676
|
+
this.#cutoffOutput();
|
|
677
|
+
};
|
|
678
|
+
const sync = () => {
|
|
679
|
+
const rootRunning = this.proc.exitCode === null && this.proc.signalCode === null;
|
|
680
|
+
const active = rootRunning || this.#openPipeReaders > 0;
|
|
681
|
+
if (signal.aborted) {
|
|
682
|
+
if (active) onAbort();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (active === attached) return;
|
|
686
|
+
if (active) signal.addEventListener("abort", onAbort, { once: true });
|
|
687
|
+
else signal.removeEventListener("abort", onAbort);
|
|
688
|
+
attached = active;
|
|
689
|
+
};
|
|
690
|
+
(this.#abortRegistrations ??= new Set()).add(sync);
|
|
691
|
+
if (signal.aborted) onAbort();
|
|
692
|
+
else sync();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#syncAbortListeners(): void {
|
|
696
|
+
if (!this.#abortRegistrations) return;
|
|
697
|
+
for (const sync of this.#abortRegistrations) sync();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
#cutoffOutput(): void {
|
|
701
|
+
this.#cutoff = true;
|
|
702
|
+
for (const reader of this.#pipeReaders) reader.cancel().catch(() => {});
|
|
661
703
|
}
|
|
662
704
|
|
|
663
705
|
#clearTimeout(): void {
|
|
@@ -684,8 +726,7 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
684
726
|
) {
|
|
685
727
|
this.kill(new TimeoutError(ms, this.#stderrTail), -1);
|
|
686
728
|
}
|
|
687
|
-
this.#
|
|
688
|
-
for (const reader of this.#pipeReaders) reader.cancel().catch(() => {});
|
|
729
|
+
this.#cutoffOutput();
|
|
689
730
|
}, ms);
|
|
690
731
|
timer.unref?.();
|
|
691
732
|
this.#timeoutTimer = timer;
|