@vzn/vx 0.0.650 → 0.0.652
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/README.md +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/exec/kill-tree.ts
CHANGED
|
@@ -98,6 +98,9 @@ let guardProc: Child | undefined
|
|
|
98
98
|
*/
|
|
99
99
|
const GUARD_STALL_MS = 1_000
|
|
100
100
|
|
|
101
|
+
/** Bytes per write to the guard: under a socket's buffer on every platform. */
|
|
102
|
+
const GUARD_PIECE = 1024
|
|
103
|
+
|
|
101
104
|
function startGuard(): void {
|
|
102
105
|
if (guardFd !== undefined) return
|
|
103
106
|
guardFd = null
|
|
@@ -114,6 +117,8 @@ function startGuard(): void {
|
|
|
114
117
|
const fd = guard.stdio[3] as number
|
|
115
118
|
guardFd = fd
|
|
116
119
|
guardProc = guard
|
|
120
|
+
// A clean exit strikes what it released: the EOF kills what is listed.
|
|
121
|
+
process.once('exit', flushGuard)
|
|
117
122
|
// A guard that has died (killed, the OOM killer) is handed to no later
|
|
118
123
|
// spawn: its pipe is broken, and a shell whose printf buffers (bash as
|
|
119
124
|
// macOS's sh) flushed the failed `+<pgid>` line into the task's own
|
|
@@ -134,6 +139,7 @@ function startGuard(): void {
|
|
|
134
139
|
function stopGuard(): void {
|
|
135
140
|
const fd = guardFd
|
|
136
141
|
guardFd = null
|
|
142
|
+
pending = ''
|
|
137
143
|
guardProc?.kill('SIGKILL')
|
|
138
144
|
if (typeof fd !== 'number') return
|
|
139
145
|
try {
|
|
@@ -145,11 +151,18 @@ function stopGuard(): void {
|
|
|
145
151
|
|
|
146
152
|
function guardWrite(line: string): void {
|
|
147
153
|
if (typeof guardFd !== 'number') return
|
|
148
|
-
|
|
154
|
+
let deadline = Date.now() + GUARD_STALL_MS
|
|
155
|
+
// A turn's batch can be more than the queue takes at once: it goes in
|
|
156
|
+
// pieces as the guard drains it, and only a guard that takes nothing for
|
|
157
|
+
// the whole stall is gone. With one deadline for the batch, 2000 strikes
|
|
158
|
+
// outlasted it on macOS and the guard was stopped under a live run.
|
|
159
|
+
let bytes = Buffer.from(line)
|
|
149
160
|
for (;;) {
|
|
150
161
|
try {
|
|
151
|
-
writeSync(guardFd,
|
|
152
|
-
return
|
|
162
|
+
const n = writeSync(guardFd, bytes, 0, Math.min(bytes.length, GUARD_PIECE))
|
|
163
|
+
if (n === bytes.length) return
|
|
164
|
+
bytes = bytes.subarray(n)
|
|
165
|
+
deadline = Date.now() + GUARD_STALL_MS
|
|
153
166
|
} catch (err) {
|
|
154
167
|
if ((err as NodeJS.ErrnoException).code !== 'EAGAIN' || Date.now() >= deadline) break
|
|
155
168
|
Bun.sleepSync(1)
|
|
@@ -158,6 +171,43 @@ function guardWrite(line: string): void {
|
|
|
158
171
|
stopGuard()
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Lines waiting for this turn's one write: strikes, and the listings of
|
|
176
|
+
* spare shells. Each line was a write of its own and a wake of the
|
|
177
|
+
* guard's shell: on a cold run of 500 tasks, 38 ms of vx's main thread
|
|
178
|
+
* for the strikes and 140 ms of the guard's CPU (2026-11-03). If vx dies
|
|
179
|
+
* in the turn a task ended, what that task left in the background dies
|
|
180
|
+
* with it, as when vx dies a moment before the task ends.
|
|
181
|
+
*/
|
|
182
|
+
let pending = ''
|
|
183
|
+
let pendingTimer: ReturnType<typeof setImmediate> | undefined
|
|
184
|
+
|
|
185
|
+
function queueLine(line: string): void {
|
|
186
|
+
pending += line
|
|
187
|
+
pendingTimer ??= setImmediate(flushGuard)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Write the lines this turn queued now: before a listed child is handed its work. */
|
|
191
|
+
export function flushGuard(): void {
|
|
192
|
+
if (pendingTimer !== undefined) clearImmediate(pendingTimer)
|
|
193
|
+
pendingTimer = undefined
|
|
194
|
+
const lines = pending
|
|
195
|
+
pending = ''
|
|
196
|
+
if (lines !== '') guardWrite(lines)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* List `pid`'s group from vx's side, for a child that runs nothing until
|
|
201
|
+
* vx hands it work (a spare shell waiting on its script): the line goes
|
|
202
|
+
* with the turn's others, and `flushGuard` before the hand-over. A spare
|
|
203
|
+
* vx dies before listing reads the end of its script's pipe and exits,
|
|
204
|
+
* having run nothing. Starts the guard.
|
|
205
|
+
*/
|
|
206
|
+
export function listGroup(pid: number): void {
|
|
207
|
+
startGuard()
|
|
208
|
+
if (typeof guardFd === 'number' && pid > 0) queueLine(`+${pid}\n`)
|
|
209
|
+
}
|
|
210
|
+
|
|
161
211
|
/**
|
|
162
212
|
* Spawn a task child whose group the guard kills if vx dies holding it.
|
|
163
213
|
* The CHILD lists its own group, before it runs anything: vx wrote the
|
|
@@ -170,8 +220,11 @@ function guardWrite(line: string): void {
|
|
|
170
220
|
* races it. The guard starts BEFORE the spawn, so there is a pipe to hand.
|
|
171
221
|
* `undefined` when the guard could not start: nothing lists anything.
|
|
172
222
|
*/
|
|
173
|
-
export function spawnGuarded(spawn: (guard: number | undefined) =>
|
|
223
|
+
export function spawnGuarded<C = Child>(spawn: (guard: number | undefined) => C): C {
|
|
174
224
|
startGuard()
|
|
225
|
+
// The turn's lines go first, as they did one write each: a child's own
|
|
226
|
+
// listing behind them, not raced by a batch vx writes once it runs.
|
|
227
|
+
flushGuard()
|
|
175
228
|
if (typeof guardFd !== 'number') return spawn(undefined)
|
|
176
229
|
handingTo = guardProc?.pid
|
|
177
230
|
try {
|
|
@@ -223,7 +276,7 @@ export function releaseGroup(child: Child): void {
|
|
|
223
276
|
if (!(child.pid > 0)) return
|
|
224
277
|
const hold = holds.get(child.pid)
|
|
225
278
|
if (hold !== undefined) hold.released = true
|
|
226
|
-
else
|
|
279
|
+
else queueLine(`-${child.pid}\n`)
|
|
227
280
|
}
|
|
228
281
|
|
|
229
282
|
/**
|
|
@@ -44,6 +44,7 @@ export function localExecutor(): TaskExecutor {
|
|
|
44
44
|
...(req.liveChildren !== undefined ? { liveChildren: req.liveChildren } : {}),
|
|
45
45
|
...(req.onSpawn !== undefined ? { onSpawn: req.onSpawn } : {}),
|
|
46
46
|
...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
|
|
47
|
+
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
47
48
|
}
|
|
48
49
|
if (req.sandbox === undefined) {
|
|
49
50
|
const res = await runCommand(req.terminal === true ? { ...common, terminal: true } : common)
|
|
@@ -56,7 +57,6 @@ export function localExecutor(): TaskExecutor {
|
|
|
56
57
|
reportWithin: req.sandbox.reportWithin,
|
|
57
58
|
reportLinked: req.sandbox.reportLinked,
|
|
58
59
|
config: req.sandbox.config,
|
|
59
|
-
...(req.signal !== undefined ? { signal: req.signal } : {}),
|
|
60
60
|
})
|
|
61
61
|
},
|
|
62
62
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// A task's stdout, stderr and held stdin as pipes vx makes itself (Linux),
|
|
2
|
+
// shared by the runner, the spare shells and the sandbox's spawn.
|
|
3
|
+
|
|
4
|
+
import type { Pointer } from 'bun:ffi'
|
|
5
|
+
import { closeSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
const O_NONBLOCK = 0o4000
|
|
8
|
+
const O_CLOEXEC = 0o2000000
|
|
9
|
+
const F_SETFL = 4
|
|
10
|
+
const F_SETPIPE_SZ = 1031
|
|
11
|
+
/**
|
|
12
|
+
* A pipe holds 64 KiB by default, and a 200 MB stdout drained 13% slower
|
|
13
|
+
* through one than through Bun's socketpair (min of 7: 293 vs 259 ms);
|
|
14
|
+
* at 256 KiB it drained in 230. Past the user's pipe quota the kernel
|
|
15
|
+
* refuses the resize and the pipe keeps its 64 KiB.
|
|
16
|
+
*/
|
|
17
|
+
const PIPE_BYTES = 256 * 1024
|
|
18
|
+
|
|
19
|
+
type PipeCalls = {
|
|
20
|
+
pipe2: (fds: Pointer, flags: number) => number
|
|
21
|
+
fcntl: (fd: number, cmd: number, arg: number) => number
|
|
22
|
+
ptr: (view: Int32Array, byteOffset?: number) => Pointer
|
|
23
|
+
}
|
|
24
|
+
let pipeCalls: PipeCalls | null | undefined
|
|
25
|
+
|
|
26
|
+
/** libc's `pipe2` and `fcntl`, bound once; null off Linux or where libc will not load. */
|
|
27
|
+
function linuxPipeCalls(): PipeCalls | null {
|
|
28
|
+
if (pipeCalls !== undefined) return pipeCalls
|
|
29
|
+
pipeCalls = null
|
|
30
|
+
if (process.platform !== 'linux') return null
|
|
31
|
+
try {
|
|
32
|
+
// Loaded at the first spawn, not with the module: a static import cost
|
|
33
|
+
// every vx start 0.3 ms, a warm run that spawns nothing included.
|
|
34
|
+
const { dlopen, FFIType, ptr } = require('bun:ffi') as typeof import('bun:ffi')
|
|
35
|
+
// glibc's soname; musl's loader answers any `libc.` name with itself
|
|
36
|
+
// (ldso/dynlink.c's reserved names).
|
|
37
|
+
const { symbols } = dlopen('libc.so.6', {
|
|
38
|
+
pipe2: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
|
|
39
|
+
fcntl: { args: [FFIType.i32, FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
40
|
+
})
|
|
41
|
+
pipeCalls = { pipe2: symbols.pipe2, fcntl: symbols.fcntl, ptr }
|
|
42
|
+
} catch {
|
|
43
|
+
// Bun's socketpair, as before: only `/dev/stdout` by path fails on it.
|
|
44
|
+
}
|
|
45
|
+
return pipeCalls
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Two pipes, `[out read, out write, err read, err write]`, or undefined
|
|
50
|
+
* when libc is not bound or a descriptor is not to be had (Bun's own
|
|
51
|
+
* socketpair then fails the spawn with the reason).
|
|
52
|
+
*/
|
|
53
|
+
function realPipes(): Int32Array | undefined {
|
|
54
|
+
const calls = linuxPipeCalls()
|
|
55
|
+
if (calls === null) return undefined
|
|
56
|
+
const fds = new Int32Array(4)
|
|
57
|
+
// Bun's spawn closes every descriptor it is not handed (probed, Bun
|
|
58
|
+
// 1.4.2); O_CLOEXEC keeps any other exec from holding a write end open.
|
|
59
|
+
if (calls.pipe2(calls.ptr(fds), O_CLOEXEC) !== 0) return undefined
|
|
60
|
+
if (calls.pipe2(calls.ptr(fds, 8), O_CLOEXEC) !== 0) {
|
|
61
|
+
closeSync(fds[0]!)
|
|
62
|
+
closeSync(fds[1]!)
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
// vx's ends only: a task whose stdout is non-blocking fails its writes with EAGAIN.
|
|
66
|
+
calls.fcntl(fds[0]!, F_SETFL, O_NONBLOCK)
|
|
67
|
+
calls.fcntl(fds[2]!, F_SETFL, O_NONBLOCK)
|
|
68
|
+
calls.fcntl(fds[0]!, F_SETPIPE_SZ, PIPE_BYTES)
|
|
69
|
+
calls.fcntl(fds[2]!, F_SETPIPE_SZ, PIPE_BYTES)
|
|
70
|
+
return fds
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One pipe, `[read, write]`, both close-on-exec, or undefined as {@link realPipes}. */
|
|
74
|
+
export function heldPipe(): Int32Array | undefined {
|
|
75
|
+
const calls = linuxPipeCalls()
|
|
76
|
+
if (calls === null) return undefined
|
|
77
|
+
const fds = new Int32Array(2)
|
|
78
|
+
return calls.pipe2(calls.ptr(fds), O_CLOEXEC) === 0 ? fds : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A task's stdout and stderr. Bun's `'pipe'` is a socketpair, and Linux
|
|
83
|
+
* opens `/dev/stdout` through `/proc/self/fd/1`, which a socket refuses
|
|
84
|
+
* (ENXIO): `echo x > /dev/stdout` and `cmd | tee /dev/stderr` failed in
|
|
85
|
+
* every task, where a terminal or Turbo hands real pipes (X-113). So on
|
|
86
|
+
* Linux vx makes the pipes (`pipe2` through `bun:ffi`) and hands the child
|
|
87
|
+
* the write ends. macOS opens `/dev/fd/N` as a dup of the descriptor,
|
|
88
|
+
* which a socket allows, so Bun's pipe stays there.
|
|
89
|
+
*
|
|
90
|
+
* `held`: stdin is a pipe vx keeps open and never writes (a persistent
|
|
91
|
+
* task's, schema.md), for the same reason: `cat /dev/stdin` on Bun's
|
|
92
|
+
* socketpair failed with ENXIO instead of blocking. Otherwise `'ignore'`.
|
|
93
|
+
*/
|
|
94
|
+
export class TaskPipes {
|
|
95
|
+
/** Descriptor 0 for `Bun.spawn`'s `stdio`. */
|
|
96
|
+
readonly stdin: number | 'pipe' | 'ignore'
|
|
97
|
+
/** Descriptors 1 and 2 for `Bun.spawn`'s `stdio`. */
|
|
98
|
+
readonly stdio: readonly [number | 'pipe', number | 'pipe']
|
|
99
|
+
readonly #read: readonly number[]
|
|
100
|
+
readonly #stdinWrite: number | undefined
|
|
101
|
+
|
|
102
|
+
constructor(terminal?: boolean, held?: boolean) {
|
|
103
|
+
const fds = terminal === true ? undefined : realPipes()
|
|
104
|
+
this.stdio = fds === undefined ? ['pipe', 'pipe'] : [fds[1]!, fds[3]!]
|
|
105
|
+
this.#read = fds === undefined ? [] : [fds[0]!, fds[2]!]
|
|
106
|
+
const inFds = held === true && terminal !== true ? heldPipe() : undefined
|
|
107
|
+
this.stdin = held !== true ? 'ignore' : inFds === undefined ? 'pipe' : inFds[0]!
|
|
108
|
+
this.#stdinWrite = inFds?.[1]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Close vx's copies of the child's ends once the spawn has returned or
|
|
113
|
+
* thrown: the child holds its own, and a write end left here is a writer
|
|
114
|
+
* the reader's EOF would wait for.
|
|
115
|
+
*/
|
|
116
|
+
spawned(): void {
|
|
117
|
+
for (const fd of [this.stdin, ...this.stdio]) if (typeof fd === 'number') closeSync(fd)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The child's stdout and stderr. */
|
|
121
|
+
streams(
|
|
122
|
+
child: ReturnType<typeof Bun.spawn>,
|
|
123
|
+
): [
|
|
124
|
+
ReadableStream<Uint8Array> | number | undefined,
|
|
125
|
+
ReadableStream<Uint8Array> | number | undefined,
|
|
126
|
+
] {
|
|
127
|
+
const [out, err] = this.#read
|
|
128
|
+
return out === undefined
|
|
129
|
+
? [child.stdout, child.stderr]
|
|
130
|
+
: [Bun.file(out).stream(), Bun.file(err!).stream()]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Close the read ends, once both streams have ended or been cancelled.
|
|
135
|
+
* Bun reads a dup of each, but takes it on the stream's first pull: a
|
|
136
|
+
* number closed before then was reused by the next spawn's pipe, and
|
|
137
|
+
* the stream read THAT (probed, Bun 1.4.2).
|
|
138
|
+
*/
|
|
139
|
+
close(): void {
|
|
140
|
+
for (const fd of this.#read) closeSync(fd)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Close the held stdin's write end, once the child has exited and its
|
|
145
|
+
* output has ended. Not with the output alone: a server that sends it
|
|
146
|
+
* to a log (`exec >log 2>&1`) ended its streams at once, read stdin EOF
|
|
147
|
+
* and exited.
|
|
148
|
+
*/
|
|
149
|
+
closeStdin(): void {
|
|
150
|
+
if (this.#stdinWrite !== undefined) closeSync(this.#stdinWrite)
|
|
151
|
+
}
|
|
152
|
+
}
|