@oh-my-pi/pi-utils 6.8.1 → 6.8.3
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/package.json +1 -1
- package/src/ptree.ts +136 -18
package/package.json
CHANGED
package/src/ptree.ts
CHANGED
|
@@ -16,6 +16,59 @@ const isWindows = process.platform === "win32";
|
|
|
16
16
|
// Set of live children for managed termination/cleanup on shutdown.
|
|
17
17
|
const managedChildren = new Set<PipedSubprocess>();
|
|
18
18
|
|
|
19
|
+
class AsyncQueue<T> {
|
|
20
|
+
#items: T[] = [];
|
|
21
|
+
#resolvers: Array<(result: IteratorResult<T>) => void> = [];
|
|
22
|
+
#closed = false;
|
|
23
|
+
|
|
24
|
+
push(item: T): void {
|
|
25
|
+
if (this.#closed) return;
|
|
26
|
+
const resolver = this.#resolvers.shift();
|
|
27
|
+
if (resolver) {
|
|
28
|
+
resolver({ value: item, done: false });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.#items.push(item);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
close(): void {
|
|
35
|
+
if (this.#closed) return;
|
|
36
|
+
this.#closed = true;
|
|
37
|
+
while (this.#resolvers.length > 0) {
|
|
38
|
+
const resolver = this.#resolvers.shift();
|
|
39
|
+
if (resolver) {
|
|
40
|
+
resolver({ value: undefined, done: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async next(): Promise<IteratorResult<T>> {
|
|
46
|
+
if (this.#items.length > 0) {
|
|
47
|
+
return { value: this.#items.shift() as T, done: false };
|
|
48
|
+
}
|
|
49
|
+
if (this.#closed) {
|
|
50
|
+
return { value: undefined, done: true };
|
|
51
|
+
}
|
|
52
|
+
return await new Promise<IteratorResult<T>>((resolve) => {
|
|
53
|
+
this.#resolvers.push(resolve);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createProcessStream(queue: AsyncQueue<Uint8Array>): ReadableStream<Uint8Array> {
|
|
59
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
60
|
+
pull: async (controller) => {
|
|
61
|
+
const result = await queue.next();
|
|
62
|
+
if (result.done) {
|
|
63
|
+
controller.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
controller.enqueue(result.value);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return stream;
|
|
70
|
+
}
|
|
71
|
+
|
|
19
72
|
/**
|
|
20
73
|
* Kill a child process and its descendents.
|
|
21
74
|
* - Windows: uses taskkill for tree and forceful kill (/T /F)
|
|
@@ -71,8 +124,8 @@ function registerManaged(child: PipedSubprocess): void {
|
|
|
71
124
|
});
|
|
72
125
|
}
|
|
73
126
|
|
|
74
|
-
// A Bun subprocess with stdin=Writable, stdout/stderr=pipe (for tracking/cleanup).
|
|
75
|
-
type PipedSubprocess = Subprocess<"pipe" | null, "pipe", "pipe">;
|
|
127
|
+
// A Bun subprocess with stdin=Writable/ignore, stdout/stderr=pipe (for tracking/cleanup).
|
|
128
|
+
type PipedSubprocess = Subprocess<"pipe" | "ignore" | null, "pipe", "pipe">;
|
|
76
129
|
|
|
77
130
|
/**
|
|
78
131
|
* ChildProcess wraps a managed subprocess, capturing output, errors, and providing
|
|
@@ -82,8 +135,11 @@ export class ChildProcess {
|
|
|
82
135
|
#proc: PipedSubprocess;
|
|
83
136
|
#detached = false;
|
|
84
137
|
#nothrow = false;
|
|
85
|
-
#stderrTee: ReadableStream<Uint8Array<ArrayBuffer>>;
|
|
86
138
|
#stderrBuffer = "";
|
|
139
|
+
#stdoutQueue = new AsyncQueue<Uint8Array>();
|
|
140
|
+
#stderrQueue = new AsyncQueue<Uint8Array>();
|
|
141
|
+
#stdoutStream?: ReadableStream<Uint8Array>;
|
|
142
|
+
#stderrStream?: ReadableStream<Uint8Array>;
|
|
87
143
|
#exitReason?: Exception;
|
|
88
144
|
#exitReasonPending?: Exception;
|
|
89
145
|
#exited: Promise<void>;
|
|
@@ -92,23 +148,75 @@ export class ChildProcess {
|
|
|
92
148
|
constructor(proc: PipedSubprocess) {
|
|
93
149
|
registerManaged(proc);
|
|
94
150
|
|
|
95
|
-
const
|
|
96
|
-
|
|
151
|
+
const exitSettled = proc.exited.then(
|
|
152
|
+
() => {},
|
|
153
|
+
() => {},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Capture stdout at all times. Close the passthrough when the process exits.
|
|
157
|
+
void (async () => {
|
|
158
|
+
const reader = proc.stdout.getReader();
|
|
159
|
+
try {
|
|
160
|
+
while (true) {
|
|
161
|
+
const result = await Promise.race([
|
|
162
|
+
reader.read(),
|
|
163
|
+
exitSettled.then(() => ({ done: true, value: undefined as Uint8Array | undefined })),
|
|
164
|
+
]);
|
|
165
|
+
if (result.done) break;
|
|
166
|
+
if (!result.value) continue;
|
|
167
|
+
this.#stdoutQueue.push(result.value);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// ignore
|
|
171
|
+
} finally {
|
|
172
|
+
try {
|
|
173
|
+
await reader.cancel();
|
|
174
|
+
} catch {}
|
|
175
|
+
try {
|
|
176
|
+
reader.releaseLock();
|
|
177
|
+
} catch {}
|
|
178
|
+
this.#stdoutQueue.close();
|
|
179
|
+
}
|
|
180
|
+
})().catch(() => {
|
|
181
|
+
this.#stdoutQueue.close();
|
|
182
|
+
});
|
|
97
183
|
|
|
98
184
|
// Capture stderr at all times, with a capped buffer for errors.
|
|
99
185
|
const decoder = new TextDecoder();
|
|
100
186
|
void (async () => {
|
|
101
|
-
|
|
102
|
-
|
|
187
|
+
const reader = proc.stderr.getReader();
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
const result = await Promise.race([
|
|
191
|
+
reader.read(),
|
|
192
|
+
exitSettled.then(() => ({ done: true, value: undefined as Uint8Array | undefined })),
|
|
193
|
+
]);
|
|
194
|
+
if (result.done) break;
|
|
195
|
+
if (!result.value) continue;
|
|
196
|
+
this.#stderrQueue.push(result.value);
|
|
197
|
+
this.#stderrBuffer += decoder.decode(result.value, { stream: true });
|
|
198
|
+
if (this.#stderrBuffer.length > NonZeroExitError.MAX_TRACE) {
|
|
199
|
+
this.#stderrBuffer = this.#stderrBuffer.slice(-NonZeroExitError.MAX_TRACE);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore
|
|
204
|
+
} finally {
|
|
205
|
+
this.#stderrBuffer += decoder.decode();
|
|
103
206
|
if (this.#stderrBuffer.length > NonZeroExitError.MAX_TRACE) {
|
|
104
207
|
this.#stderrBuffer = this.#stderrBuffer.slice(-NonZeroExitError.MAX_TRACE);
|
|
105
208
|
}
|
|
209
|
+
try {
|
|
210
|
+
await reader.cancel();
|
|
211
|
+
} catch {}
|
|
212
|
+
try {
|
|
213
|
+
reader.releaseLock();
|
|
214
|
+
} catch {}
|
|
215
|
+
this.#stderrQueue.close();
|
|
106
216
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
})().catch(() => {});
|
|
217
|
+
})().catch(() => {
|
|
218
|
+
this.#stderrQueue.close();
|
|
219
|
+
});
|
|
112
220
|
|
|
113
221
|
const { promise, resolve } = Promise.withResolvers<Exception | undefined>();
|
|
114
222
|
|
|
@@ -152,11 +260,17 @@ export class ChildProcess {
|
|
|
152
260
|
get stdin(): FileSink | undefined {
|
|
153
261
|
return this.#proc.stdin;
|
|
154
262
|
}
|
|
155
|
-
get stdout(): ReadableStream<Uint8Array
|
|
156
|
-
|
|
263
|
+
get stdout(): ReadableStream<Uint8Array> {
|
|
264
|
+
if (!this.#stdoutStream) {
|
|
265
|
+
this.#stdoutStream = createProcessStream(this.#stdoutQueue);
|
|
266
|
+
}
|
|
267
|
+
return this.#stdoutStream;
|
|
157
268
|
}
|
|
158
|
-
get stderr(): ReadableStream<Uint8Array
|
|
159
|
-
|
|
269
|
+
get stderr(): ReadableStream<Uint8Array> {
|
|
270
|
+
if (!this.#stderrStream) {
|
|
271
|
+
this.#stderrStream = createProcessStream(this.#stderrQueue);
|
|
272
|
+
}
|
|
273
|
+
return this.#stderrStream;
|
|
160
274
|
}
|
|
161
275
|
|
|
162
276
|
/**
|
|
@@ -227,7 +341,7 @@ export class ChildProcess {
|
|
|
227
341
|
async blob() {
|
|
228
342
|
const { promise, resolve, reject } = Promise.withResolvers<Blob>();
|
|
229
343
|
|
|
230
|
-
const blob = this
|
|
344
|
+
const blob = this.stdout.blob();
|
|
231
345
|
if (!this.#nothrow) {
|
|
232
346
|
this.#exited.catch((ex: Exception) => {
|
|
233
347
|
reject(ex);
|
|
@@ -343,7 +457,10 @@ export class TimeoutError extends AbortError {
|
|
|
343
457
|
/**
|
|
344
458
|
* Options for cspawn (child spawn). Always pipes stdout/stderr, allows signal.
|
|
345
459
|
*/
|
|
346
|
-
type ChildSpawnOptions = Omit<
|
|
460
|
+
type ChildSpawnOptions = Omit<
|
|
461
|
+
Spawn.SpawnOptions<"pipe" | "ignore" | Buffer | null, "pipe", "pipe">,
|
|
462
|
+
"stdout" | "stderr"
|
|
463
|
+
> & {
|
|
347
464
|
signal?: AbortSignal;
|
|
348
465
|
};
|
|
349
466
|
|
|
@@ -355,6 +472,7 @@ type ChildSpawnOptions = Omit<Spawn.SpawnOptions<"pipe" | null, "pipe", "pipe">,
|
|
|
355
472
|
export function cspawn(cmd: string[], options?: ChildSpawnOptions): ChildProcess {
|
|
356
473
|
const { timeout, ...rest } = options ?? {};
|
|
357
474
|
const child = spawn(cmd, {
|
|
475
|
+
stdin: "ignore",
|
|
358
476
|
...rest,
|
|
359
477
|
stdout: "pipe",
|
|
360
478
|
stderr: "pipe",
|