@vzn/vx 0.0.395 → 0.0.397
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.397",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.397",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.397",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.397",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.397"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -1145,6 +1145,15 @@ export async function wrapSandboxedCommand(
|
|
|
1145
1145
|
wrapped = `exec ${readOnlyMasks(wrapped, scratch)}`
|
|
1146
1146
|
if (process.platform === 'linux' && !hostHasIpv6())
|
|
1147
1147
|
wrapped = `SOCAT_DEFAULT_LISTEN_IP=4 ${wrapped}`
|
|
1148
|
+
const held = portsHeld(ports)
|
|
1149
|
+
if (held.length > 0) {
|
|
1150
|
+
throw new UserError(
|
|
1151
|
+
`sandbox: localBinding port${held.length === 1 ? '' : 's'} ${held.join(', ')} ` +
|
|
1152
|
+
`${held.length === 1 ? 'is' : 'are'} already in use on this machine, so the task's own ` +
|
|
1153
|
+
`cannot be exposed there and a client would reach the other listener; stop what ` +
|
|
1154
|
+
`holds ${held.length === 1 ? 'it' : 'them'} or list another port`,
|
|
1155
|
+
)
|
|
1156
|
+
}
|
|
1148
1157
|
if (args.server === true) liveServers.add(tag)
|
|
1149
1158
|
if (ports.length > 0) {
|
|
1150
1159
|
spawnHostBridges(ports, tag)
|
|
@@ -1402,6 +1411,37 @@ const hostBridges = new Map<
|
|
|
1402
1411
|
{ ports: readonly number[]; procs: Array<ReturnType<typeof Bun.spawn>> }
|
|
1403
1412
|
>()
|
|
1404
1413
|
|
|
1414
|
+
/**
|
|
1415
|
+
* The ports a host listener already holds where the bridge would bind
|
|
1416
|
+
* (`127.0.0.1`, or every address). The listen wait below reads the same
|
|
1417
|
+
* table and took that listener for the bridge, whose bind had failed: the
|
|
1418
|
+
* task passed and a client of the port reached the other process
|
|
1419
|
+
* (2026-10-03). Linux, own procfs only, as the wait.
|
|
1420
|
+
*/
|
|
1421
|
+
function portsHeld(ports: readonly number[]): number[] {
|
|
1422
|
+
if (ports.length === 0 || !procfsIsOwn()) return []
|
|
1423
|
+
const held = new Set<string>()
|
|
1424
|
+
for (const [file, any, loop] of [
|
|
1425
|
+
['/proc/net/tcp', '00000000', '0100007F'],
|
|
1426
|
+
['/proc/net/tcp6', '00000000000000000000000000000000', '00000000000000000000000001000000'],
|
|
1427
|
+
] as const) {
|
|
1428
|
+
let table: string
|
|
1429
|
+
try {
|
|
1430
|
+
table = readFileSync(file, 'utf8')
|
|
1431
|
+
} catch {
|
|
1432
|
+
continue
|
|
1433
|
+
}
|
|
1434
|
+
for (const line of table.split('\n')) {
|
|
1435
|
+
const f = line.trim().split(/\s+/)
|
|
1436
|
+
if (f[3] !== '0A' || f[1] === undefined) continue
|
|
1437
|
+
const [addr, port] = f[1].split(':')
|
|
1438
|
+
// `::1` does not hold 127.0.0.1's port; every address does.
|
|
1439
|
+
if (addr === any || (file === '/proc/net/tcp' && addr === loop)) held.add(port!)
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return ports.filter((p) => held.has(p.toString(16).toUpperCase().padStart(4, '0')))
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1405
1445
|
/**
|
|
1406
1446
|
* Wait until each host-side bridge listens on its port, so a server that
|
|
1407
1447
|
* says it is ready inside the sandbox is reachable on the host: the socat
|
|
@@ -1409,7 +1449,8 @@ const hostBridges = new Map<
|
|
|
1409
1449
|
* right after its ready line under I/O load (M-22). Read off
|
|
1410
1450
|
* /proc/net/tcp (127.0.0.1, state 0A); skipped where /proc is not this
|
|
1411
1451
|
* process's (its net table could be another namespace's), ended early by
|
|
1412
|
-
* a bridge that exited
|
|
1452
|
+
* a bridge that exited, and bounded at 5 s. A port another listener holds
|
|
1453
|
+
* reads as listening here, so the wrap refuses it first (`portsHeld`).
|
|
1413
1454
|
*/
|
|
1414
1455
|
async function hostBridgesListen(ports: readonly number[], tag: string): Promise<void> {
|
|
1415
1456
|
if (!procfsIsOwn()) return
|
|
@@ -396,6 +396,26 @@ async function executePersistentTask(args: ExecuteArgs): Promise<TaskOutcome> {
|
|
|
396
396
|
if (spawn.child === undefined) void onExit()
|
|
397
397
|
else void spawn.child.exited.then(onExit, onExit)
|
|
398
398
|
}
|
|
399
|
+
// Said once if readiness is slow: a dependency's output is hidden unless
|
|
400
|
+
// it fails, and with no `exec.timeout` the wait never ends, so a run
|
|
401
|
+
// whose `readyWhen` never matched showed nothing at all.
|
|
402
|
+
const readyWhen = step.persistent.readyWhen
|
|
403
|
+
const noticeMs = readyNoticeMs()
|
|
404
|
+
const notice =
|
|
405
|
+
readyWhen === undefined
|
|
406
|
+
? undefined
|
|
407
|
+
: setTimeout(() => {
|
|
408
|
+
if (isAborted(args.stopSignal)) return
|
|
409
|
+
const after = noticeMs < 1000 ? `${noticeMs} ms` : `${noticeMs / 1000} s`
|
|
410
|
+
const unbounded = effectiveTimeout === undefined ? ', with no exec.timeout' : ''
|
|
411
|
+
log.status(
|
|
412
|
+
`vx: ${node.id} not ready after ${after}: waiting for a line matching /${readyWhen}/ (readyWhen)${unbounded}`,
|
|
413
|
+
)
|
|
414
|
+
}, noticeMs)
|
|
415
|
+
if (notice !== undefined) {
|
|
416
|
+
const quiet = (): void => clearTimeout(notice)
|
|
417
|
+
void spawn.ready.then(quiet, quiet)
|
|
418
|
+
}
|
|
399
419
|
try {
|
|
400
420
|
await spawn.ready
|
|
401
421
|
} catch (err) {
|
|
@@ -1337,3 +1357,13 @@ function taskEnv(node: TaskNode, step: ExecConfig, workspaceRoot: string): NodeJ
|
|
|
1337
1357
|
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
1338
1358
|
return signal?.aborted === true
|
|
1339
1359
|
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* How long a server may take to match `readyWhen` before vx says it waits.
|
|
1363
|
+
* `VX_READY_NOTICE_MS` overrides it, as `VX_KILL_GRACE_MS` does the grace.
|
|
1364
|
+
*/
|
|
1365
|
+
function readyNoticeMs(): number {
|
|
1366
|
+
const raw = process.env['VX_READY_NOTICE_MS']
|
|
1367
|
+
if (raw !== undefined && /^[0-9]+$/.test(raw) && Number(raw) > 0) return Number(raw)
|
|
1368
|
+
return 10_000
|
|
1369
|
+
}
|
|
@@ -61,6 +61,17 @@ export const WATCHED_BUILTIN_NAMES = [
|
|
|
61
61
|
// A global one config set reached every config loaded after it in the
|
|
62
62
|
// process: `--all` read it, `--filter` of the reader alone did not (D-122).
|
|
63
63
|
'globalThis',
|
|
64
|
+
// What the check itself and vx's matching read through: a replaced
|
|
65
|
+
// `Reflect.ownKeys` blinded the check, and `RegExp.prototype.test` or
|
|
66
|
+
// `Date.now` ran vx on a config's choice (D-124).
|
|
67
|
+
'Reflect',
|
|
68
|
+
'Object',
|
|
69
|
+
'Array',
|
|
70
|
+
'RegExp.prototype',
|
|
71
|
+
'Function.prototype',
|
|
72
|
+
'Date',
|
|
73
|
+
'Date.prototype',
|
|
74
|
+
'Number.prototype',
|
|
64
75
|
] as const
|
|
65
76
|
|
|
66
77
|
const WORKER_SRC = `
|
|
@@ -109,7 +120,10 @@ self.onmessage = async (e) => {
|
|
|
109
120
|
const watched = e.data.blame
|
|
110
121
|
? ${JSON.stringify(WATCHED_BUILTIN_NAMES)}.map((n) => [n, n.split('.').reduce((o, k) => o[k], globalThis)])
|
|
111
122
|
: null
|
|
112
|
-
|
|
123
|
+
// Taken before the import: a config can replace them (D-124).
|
|
124
|
+
const RO = Reflect.ownKeys
|
|
125
|
+
const GD = Object.getOwnPropertyDescriptor
|
|
126
|
+
const own = (o) => new Map(RO(o).map((k) => [k, GD(o, k)]))
|
|
113
127
|
const before = watched?.map(([, o]) => own(o))
|
|
114
128
|
const envBefore = watched ? { ...live } : null
|
|
115
129
|
const cwdBefore = watched ? globalThis.process.cwd() : null
|
|
@@ -448,7 +448,7 @@ export async function loadProjectConfigs(
|
|
|
448
448
|
// Taken only when a config is evaluated in this process: reading every
|
|
449
449
|
// descriptor of `Bun` makes Bun build its lazy members (`bun:sql`,
|
|
450
450
|
// `node:stream`), ~7 ms of a two-config warm run where every load hit.
|
|
451
|
-
let builtins: BuiltinSnapshot
|
|
451
|
+
let builtins: BuiltinSnapshot | undefined
|
|
452
452
|
let env: Readonly<Record<string, string | undefined>> = {}
|
|
453
453
|
let cwd = ''
|
|
454
454
|
// More than one evaluation in flight: a change seen after one load may
|
|
@@ -618,20 +618,51 @@ const WATCHED_BUILTINS: ReadonlyArray<readonly [string, object]> = WATCHED_BUILT
|
|
|
618
618
|
] as const,
|
|
619
619
|
)
|
|
620
620
|
|
|
621
|
+
// The check reads through these, taken when a round's snapshot is, before
|
|
622
|
+
// any of its configs runs: a config that set `Reflect.ownKeys = () => []`
|
|
623
|
+
// blinded it, and its `Object.prototype.exec` ran in another project's
|
|
624
|
+
// task (D-124). Each round keeps its own, so an overlapping round cannot
|
|
625
|
+
// hand it a replaced one. The compare and put-back use indexed loops only
|
|
626
|
+
// (a replaced `Array.prototype.forEach` skipped them).
|
|
627
|
+
interface Primitives {
|
|
628
|
+
ownKeys: typeof Reflect.ownKeys
|
|
629
|
+
descriptorOf: typeof Object.getOwnPropertyDescriptor
|
|
630
|
+
defineOwn: typeof Object.defineProperty
|
|
631
|
+
deleteOwn: typeof Reflect.deleteProperty
|
|
632
|
+
hasOwn: typeof Object.hasOwn
|
|
633
|
+
same: typeof Object.is
|
|
634
|
+
keyName: typeof String
|
|
635
|
+
}
|
|
636
|
+
|
|
621
637
|
interface OwnProperties {
|
|
622
638
|
keys: PropertyKey[]
|
|
623
639
|
descriptors: PropertyDescriptor[]
|
|
624
|
-
byKey: ReadonlyMap<PropertyKey, PropertyDescriptor>
|
|
625
640
|
}
|
|
626
641
|
|
|
627
|
-
|
|
642
|
+
interface BuiltinSnapshot {
|
|
643
|
+
readonly prims: Primitives
|
|
644
|
+
readonly props: readonly OwnProperties[]
|
|
645
|
+
}
|
|
628
646
|
|
|
629
647
|
function builtinSnapshot(): BuiltinSnapshot {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
648
|
+
const prims: Primitives = {
|
|
649
|
+
ownKeys: Reflect.ownKeys,
|
|
650
|
+
descriptorOf: Object.getOwnPropertyDescriptor,
|
|
651
|
+
defineOwn: Object.defineProperty,
|
|
652
|
+
deleteOwn: Reflect.deleteProperty,
|
|
653
|
+
hasOwn: Object.hasOwn,
|
|
654
|
+
same: Object.is,
|
|
655
|
+
keyName: String,
|
|
656
|
+
}
|
|
657
|
+
const props: OwnProperties[] = []
|
|
658
|
+
for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
|
|
659
|
+
const proto = WATCHED_BUILTINS[i]![1]
|
|
660
|
+
const keys = prims.ownKeys(proto)
|
|
661
|
+
const descriptors: PropertyDescriptor[] = []
|
|
662
|
+
for (let j = 0; j < keys.length; j++) descriptors[j] = prims.descriptorOf(proto, keys[j]!)!
|
|
663
|
+
props[i] = { keys, descriptors }
|
|
664
|
+
}
|
|
665
|
+
return { prims, props }
|
|
635
666
|
}
|
|
636
667
|
|
|
637
668
|
/**
|
|
@@ -641,13 +672,17 @@ function builtinSnapshot(): BuiltinSnapshot {
|
|
|
641
672
|
* evaluated config: the full check was ~0.1 ms a config, ~100 ms of a
|
|
642
673
|
* 1,000-config cold load.
|
|
643
674
|
*/
|
|
644
|
-
function unchanged(
|
|
675
|
+
function unchanged(
|
|
676
|
+
p: Primitives,
|
|
677
|
+
proto: object,
|
|
678
|
+
keys: readonly PropertyKey[],
|
|
679
|
+
was: OwnProperties,
|
|
680
|
+
): boolean {
|
|
645
681
|
if (keys.length !== was.keys.length) return false
|
|
646
682
|
for (let j = 0; j < keys.length; j++) {
|
|
647
683
|
const key = keys[j]!
|
|
648
684
|
if (key !== was.keys[j]) return false
|
|
649
|
-
if (!sameDescriptor(was.descriptors[j]!,
|
|
650
|
-
return false
|
|
685
|
+
if (!sameDescriptor(p, was.descriptors[j]!, p.descriptorOf(proto, key)!)) return false
|
|
651
686
|
}
|
|
652
687
|
return true
|
|
653
688
|
}
|
|
@@ -676,27 +711,38 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
|
|
|
676
711
|
}
|
|
677
712
|
|
|
678
713
|
/** Puts back what changed since `before`, naming each property it put back. */
|
|
679
|
-
function restoreBuiltins(before: BuiltinSnapshot): string[] {
|
|
714
|
+
function restoreBuiltins(before: BuiltinSnapshot | undefined): string[] {
|
|
680
715
|
const changed: string[] = []
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
if (before === undefined) return changed
|
|
717
|
+
const p = before.prims
|
|
718
|
+
for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
|
|
719
|
+
const name = WATCHED_BUILTINS[i]![0]
|
|
720
|
+
const proto = WATCHED_BUILTINS[i]![1]
|
|
721
|
+
const was = before.props[i]!
|
|
722
|
+
const keys = p.ownKeys(proto)
|
|
723
|
+
if (unchanged(p, proto, keys, was)) continue
|
|
724
|
+
// Slow path, a change only: a linear lookup keeps it off `Map`.
|
|
725
|
+
for (let j = 0; j < keys.length; j++) {
|
|
726
|
+
const key = keys[j]!
|
|
727
|
+
let prior: PropertyDescriptor | undefined
|
|
728
|
+
for (let k = 0; k < was.keys.length; k++) {
|
|
729
|
+
if (was.keys[k] === key) {
|
|
730
|
+
prior = was.descriptors[k]
|
|
731
|
+
break
|
|
732
|
+
}
|
|
697
733
|
}
|
|
734
|
+
if (prior !== undefined && sameDescriptor(p, prior, p.descriptorOf(proto, key)!)) continue
|
|
735
|
+
changed[changed.length] = name + '.' + p.keyName(key)
|
|
736
|
+
if (prior === undefined) p.deleteOwn(proto, key)
|
|
737
|
+
else p.defineOwn(proto, key, prior)
|
|
698
738
|
}
|
|
699
|
-
|
|
739
|
+
for (let k = 0; k < was.keys.length; k++) {
|
|
740
|
+
const key = was.keys[k]!
|
|
741
|
+
if (p.hasOwn(proto, key)) continue
|
|
742
|
+
changed[changed.length] = name + '.' + p.keyName(key)
|
|
743
|
+
p.defineOwn(proto, key, was.descriptors[k]!)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
700
746
|
return changed
|
|
701
747
|
}
|
|
702
748
|
|
|
@@ -739,10 +785,10 @@ function restoreCwd(before: string): string[] {
|
|
|
739
785
|
return ['process.cwd (a chdir)']
|
|
740
786
|
}
|
|
741
787
|
|
|
742
|
-
function sameDescriptor(a: PropertyDescriptor, b: PropertyDescriptor): boolean {
|
|
788
|
+
function sameDescriptor(p: Primitives, a: PropertyDescriptor, b: PropertyDescriptor): boolean {
|
|
743
789
|
return (
|
|
744
790
|
// `globalThis.NaN` is watched (D-122), and NaN !== NaN.
|
|
745
|
-
|
|
791
|
+
p.same(a.value, b.value) &&
|
|
746
792
|
a.get === b.get &&
|
|
747
793
|
a.set === b.set &&
|
|
748
794
|
a.writable === b.writable &&
|