@vzn/vx 0.0.58 → 0.0.60
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 +5 -5
- package/src/exec/kill-tree.ts +23 -10
- package/src/exec/runner.ts +52 -33
- package/src/exec/sandbox-runtime.ts +44 -17
- package/src/orchestrator/events.ts +11 -14
- package/src/orchestrator/telemetry.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.60",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.60",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.60",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.60"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/exec/kill-tree.ts
CHANGED
|
@@ -103,18 +103,31 @@ function guardWrite(line: string): void {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* Spawn a task child
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* of vx
|
|
110
|
-
*
|
|
111
|
-
*
|
|
106
|
+
* Spawn a task child whose group the guard kills if vx dies holding it.
|
|
107
|
+
* The CHILD lists its own group, before it runs anything: vx wrote the
|
|
108
|
+
* line after the spawn returned, and under load the child ran first, so a
|
|
109
|
+
* `kill -9` of vx in between left its group unlisted and alive (4 of 40
|
|
110
|
+
* runs at tenfold load; B-9). `spawn` gets the guard's pipe, to hand the
|
|
111
|
+
* child at some descriptor `n`, and runs `guardLine(n)` in the child's
|
|
112
|
+
* shell first. The child holds the pipe until it has written, so the
|
|
113
|
+
* guard cannot reach EOF before the line: vx's death alone no longer
|
|
114
|
+
* races it. The guard starts BEFORE the spawn, so there is a pipe to hand.
|
|
115
|
+
* `undefined` when the guard could not start: nothing lists anything.
|
|
112
116
|
*/
|
|
113
|
-
export function spawnGuarded(spawn: () => Child): Child {
|
|
117
|
+
export function spawnGuarded(spawn: (guard: number | undefined) => Child): Child {
|
|
114
118
|
startGuard()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
return spawn(typeof guardFd === 'number' ? guardFd : undefined)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The shell line that lists `$$`'s group on the guard's pipe at `fd` and
|
|
124
|
+
* closes it, so the task never holds the guard open. `$$` is the spawned
|
|
125
|
+
* shell, which leads its group (every guarded spawn is `detached`); a
|
|
126
|
+
* program that is not a shell is `exec`'d after it. A write to a guard
|
|
127
|
+
* that has died is ignored rather than a SIGPIPE that kills the task.
|
|
128
|
+
*/
|
|
129
|
+
export function guardLine(fd: number): string {
|
|
130
|
+
return `trap '' PIPE; printf '+%s\\n' $$ >&${fd} 2>/dev/null; trap - PIPE; exec ${fd}>&-; `
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/**
|
package/src/exec/runner.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
markGroupIfGone,
|
|
15
15
|
releaseGroup,
|
|
16
16
|
signalThrough,
|
|
17
|
+
guardLine,
|
|
17
18
|
spawnGuarded,
|
|
18
19
|
untilGroupsGone,
|
|
19
20
|
} from './kill-tree.js'
|
|
@@ -435,27 +436,40 @@ export function runPersistent(opts: PersistentOptions): PersistentSpawn {
|
|
|
435
436
|
|
|
436
437
|
let child: ReturnType<typeof Bun.spawn>
|
|
437
438
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
439
|
+
const signalFd = opts.signalChannel === true
|
|
440
|
+
child = spawnGuarded((guard) =>
|
|
441
|
+
Bun.spawn(
|
|
442
|
+
[
|
|
443
|
+
executablePath('sh'),
|
|
444
|
+
'-c',
|
|
445
|
+
(guard === undefined ? '' : guardLine(signalFd ? 4 : 3)) + execWrap(opts.command),
|
|
446
|
+
],
|
|
447
|
+
{
|
|
448
|
+
argv0: 'sh',
|
|
449
|
+
cwd: opts.cwd,
|
|
450
|
+
env: opts.env as Record<string, string>,
|
|
451
|
+
// A pipe vx holds and never writes: stdin stays open while vx
|
|
452
|
+
// lives and ends when it does. A dev server that exits on stdin
|
|
453
|
+
// EOF (esbuild --watch, Vite's case in turborepo#8915) became ready
|
|
454
|
+
// and exited 0 under 'ignore'. Not the terminal: several servers
|
|
455
|
+
// would steal each other's keystrokes, and a CI's /dev/null stdin
|
|
456
|
+
// is the same EOF. The one-shot spawn below keeps 'ignore', so a
|
|
457
|
+
// task that reads stdin can never hang CI.
|
|
458
|
+
stdio: [
|
|
459
|
+
'pipe',
|
|
460
|
+
'pipe',
|
|
461
|
+
'pipe',
|
|
462
|
+
...(signalFd ? ['pipe' as const] : []),
|
|
463
|
+
...(guard === undefined ? [] : [guard]),
|
|
464
|
+
],
|
|
465
|
+
// Its own session and process group, so a kill reaches what it
|
|
466
|
+
// forked (kill-tree.ts). stdin is a pipe, so a background group
|
|
467
|
+
// never stops on a terminal read.
|
|
468
|
+
detached: true,
|
|
469
|
+
},
|
|
470
|
+
),
|
|
457
471
|
)
|
|
458
|
-
if (
|
|
472
|
+
if (signalFd) {
|
|
459
473
|
signalThrough(child, child.stdio[3] as number)
|
|
460
474
|
const spawned = child
|
|
461
475
|
void spawned.exited.then(() => closeSignalChannel(spawned))
|
|
@@ -651,19 +665,24 @@ export async function runCommand(opts: RunOptions): Promise<RunResult> {
|
|
|
651
665
|
|
|
652
666
|
let proc: ReturnType<typeof Bun.spawn>
|
|
653
667
|
try {
|
|
654
|
-
proc = spawnGuarded(() =>
|
|
655
|
-
Bun.spawn(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
668
|
+
proc = spawnGuarded((guard) =>
|
|
669
|
+
Bun.spawn(
|
|
670
|
+
[
|
|
671
|
+
executablePath('sh'),
|
|
672
|
+
'-c',
|
|
673
|
+
(guard === undefined ? '' : guardLine(3)) + execWrap(fullCommand),
|
|
674
|
+
],
|
|
675
|
+
{
|
|
676
|
+
argv0: 'sh',
|
|
677
|
+
cwd: opts.cwd,
|
|
678
|
+
env: opts.env as Record<string, string>,
|
|
679
|
+
stdio: ['ignore', 'pipe', 'pipe', ...(guard === undefined ? [] : [guard])],
|
|
680
|
+
// Its own session and process group, so a kill reaches what it
|
|
681
|
+
// forked (kill-tree.ts). stdin is ignored, so a background group
|
|
682
|
+
// never stops on a terminal read.
|
|
683
|
+
detached: true,
|
|
684
|
+
},
|
|
685
|
+
),
|
|
667
686
|
)
|
|
668
687
|
} catch (err) {
|
|
669
688
|
const stderr = spawnFailureText(err, opts.cwd)
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
killTree,
|
|
65
65
|
releaseGroup,
|
|
66
66
|
signalThrough,
|
|
67
|
+
guardLine,
|
|
67
68
|
spawnGuarded,
|
|
68
69
|
} from './kill-tree.js'
|
|
69
70
|
|
|
@@ -902,13 +903,20 @@ function spawnHostBridges(ports: readonly number[], tag: string): void {
|
|
|
902
903
|
// listening on the port under init, where the next run's bridge
|
|
903
904
|
// could not bind it (item 873).
|
|
904
905
|
procs.push(
|
|
905
|
-
spawnGuarded(() =>
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
906
|
+
spawnGuarded((guard) =>
|
|
907
|
+
guard === undefined
|
|
908
|
+
? Bun.spawn(portBridgeHostArgv(tag, p), {
|
|
909
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
910
|
+
detached: true,
|
|
911
|
+
})
|
|
912
|
+
: Bun.spawn(
|
|
913
|
+
[
|
|
914
|
+
executablePath('sh'),
|
|
915
|
+
'-c',
|
|
916
|
+
`${guardLine(3)}exec ${portBridgeHostArgv(tag, p).map(shellQuote).join(' ')}`,
|
|
917
|
+
],
|
|
918
|
+
{ stdio: ['ignore', 'ignore', 'ignore', guard], detached: true },
|
|
919
|
+
),
|
|
912
920
|
),
|
|
913
921
|
)
|
|
914
922
|
} catch {
|
|
@@ -1086,16 +1094,35 @@ async function runSandboxedOnce(
|
|
|
1086
1094
|
wrapped,
|
|
1087
1095
|
]
|
|
1088
1096
|
: [sh, '-c', wrapped]
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1097
|
+
// With the guard, a shell lists the group first and `exec`s the
|
|
1098
|
+
// spawn: strace itself would hold the guard's pipe for its lifetime.
|
|
1099
|
+
const guardAt = forwardsSignals ? 4 : 3
|
|
1100
|
+
proc = spawnGuarded((guard) =>
|
|
1101
|
+
Bun.spawn(
|
|
1102
|
+
guard === undefined
|
|
1103
|
+
? spawnArgv
|
|
1104
|
+
: [
|
|
1105
|
+
sh,
|
|
1106
|
+
'-c',
|
|
1107
|
+
guardLine(guardAt) +
|
|
1108
|
+
(straceLog ? `exec ${spawnArgv.map(shellQuote).join(' ')}` : wrapped),
|
|
1109
|
+
],
|
|
1110
|
+
{
|
|
1111
|
+
argv0: straceLog && guard === undefined ? 'strace' : 'sh',
|
|
1112
|
+
cwd: args.cwd,
|
|
1113
|
+
env: args.env as Record<string, string>,
|
|
1114
|
+
// fd 3 is the signal channel the in-sandbox watcher reads.
|
|
1115
|
+
stdio: [
|
|
1116
|
+
'ignore',
|
|
1117
|
+
'pipe',
|
|
1118
|
+
'pipe',
|
|
1119
|
+
...(forwardsSignals ? ['pipe' as const] : []),
|
|
1120
|
+
...(guard === undefined ? [] : [guard]),
|
|
1121
|
+
],
|
|
1122
|
+
// As the unsandboxed spawn: its own process group (kill-tree.ts).
|
|
1123
|
+
detached: true,
|
|
1124
|
+
},
|
|
1125
|
+
),
|
|
1099
1126
|
)
|
|
1100
1127
|
if (forwardsSignals) signalThrough(proc, proc.stdio[3] as number)
|
|
1101
1128
|
} catch (err) {
|
|
@@ -91,6 +91,9 @@ export function createEventBus(): EventBus {
|
|
|
91
91
|
* renderer subscribes via `terminalSubscriber`.
|
|
92
92
|
*/
|
|
93
93
|
export function busLogger(bus: EventBus): Logger {
|
|
94
|
+
// run() reaches runEnd on its success path, in its finally and from a
|
|
95
|
+
// signal; a plugin's onRunEnd hears the first only.
|
|
96
|
+
let ended = false
|
|
94
97
|
return {
|
|
95
98
|
status: (line) => bus.emit({ kind: 'run:status', line }),
|
|
96
99
|
taskStdout: (node, chunk) => bus.emit({ kind: 'task:stdout', node, chunk }),
|
|
@@ -98,7 +101,11 @@ export function busLogger(bus: EventBus): Logger {
|
|
|
98
101
|
taskComplete: (node, outcome) => bus.emit({ kind: 'task:complete', node, outcome }),
|
|
99
102
|
runStart: (info) => bus.emit({ kind: 'run:start', info }),
|
|
100
103
|
taskStart: (node) => bus.emit({ kind: 'task:start', node }),
|
|
101
|
-
runEnd: () =>
|
|
104
|
+
runEnd: () => {
|
|
105
|
+
if (ended) return
|
|
106
|
+
ended = true
|
|
107
|
+
bus.emit({ kind: 'run:end' })
|
|
108
|
+
},
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -142,19 +149,12 @@ export function terminalSubscriber(sink: Logger): RunEventSubscriber {
|
|
|
142
149
|
* it to `send` — the shared run→consumer forwarding path for whatever an
|
|
143
150
|
* embedder builds on it (an NDJSON stream, an enveloped protocol; core
|
|
144
151
|
* ships neither). Group-task start/complete events are dropped (no command,
|
|
145
|
-
* pure scheduling noise).
|
|
146
|
-
*
|
|
147
|
-
* quiet, so a consumer sees exactly one terminal frame per run. `send`
|
|
152
|
+
* pure scheduling noise). Summary status lines that follow run:end are
|
|
153
|
+
* forwarded too. `send`
|
|
148
154
|
* must be fire-and-forget and tolerant of its own failures (a dead
|
|
149
155
|
* consumer can never break the run); the bus already isolates throws.
|
|
150
156
|
*/
|
|
151
157
|
export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscriber {
|
|
152
|
-
// run() emits run:end TWICE (normal + finally), with the summary footer
|
|
153
|
-
// (run:status lines) emitted in BETWEEN. Drop the duplicate run:end, but
|
|
154
|
-
// keep forwarding everything else — including those post-run:end status
|
|
155
|
-
// lines, so a consumer that renders them gets the footer, and one that
|
|
156
|
-
// ignores run:status is unaffected.
|
|
157
|
-
let endForwarded = false
|
|
158
158
|
// Which tasks the consumer has been told about. A skipped task never
|
|
159
159
|
// reaches the scheduler's onStart, so its completion arrives with no
|
|
160
160
|
// preceding task:start — and `createWireRenderer` resolves a completion's
|
|
@@ -164,10 +164,7 @@ export function wireForwarder(send: (event: WireEvent) => void): RunEventSubscri
|
|
|
164
164
|
// fidelity (real requested/surfaced/command) instead of a stand-in.
|
|
165
165
|
const started = new Set<string>()
|
|
166
166
|
return (event) => {
|
|
167
|
-
if (
|
|
168
|
-
if (endForwarded) return
|
|
169
|
-
endForwarded = true
|
|
170
|
-
} else if (
|
|
167
|
+
if (
|
|
171
168
|
(event.kind === 'task:start' || event.kind === 'task:complete') &&
|
|
172
169
|
isGroupTask(event.node)
|
|
173
170
|
) {
|
|
@@ -413,7 +413,6 @@ export function createTelemetrySource(args: {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
let endEmitted = false
|
|
417
416
|
const subscriber: RunEventSubscriber = (event: RunEvent) => {
|
|
418
417
|
const ts = Date.now()
|
|
419
418
|
switch (event.kind) {
|
|
@@ -472,9 +471,6 @@ export function createTelemetrySource(args: {
|
|
|
472
471
|
case 'run:status':
|
|
473
472
|
return // status lines are terminal-rendering noise, not telemetry
|
|
474
473
|
case 'run:end':
|
|
475
|
-
// run() emits run:end twice (normal + finally); emit one record.
|
|
476
|
-
if (endEmitted) return
|
|
477
|
-
endEmitted = true
|
|
478
474
|
deliver({ v: TELEMETRY_SCHEMA_VERSION, kind: 'run.end', runId, ts })
|
|
479
475
|
return
|
|
480
476
|
}
|