@vzn/vx 0.0.396 → 0.0.398
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.398",
|
|
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.398",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.398",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.398",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.398"
|
|
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
|
|
@@ -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,10 +120,16 @@ 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
|
|
130
|
+
// The umask is the process's, not the worker's: a config's umask(0o777)
|
|
131
|
+
// here left every file vx wrote after it 000 (D-125). Put back below.
|
|
132
|
+
const umaskIn = globalThis.process.umask()
|
|
116
133
|
const same = (a, b) =>
|
|
117
134
|
a !== undefined && b !== undefined && Object.is(a.value, b.value) && a.get === b.get && a.set === b.set &&
|
|
118
135
|
a.writable === b.writable && a.enumerable === b.enumerable && a.configurable === b.configurable
|
|
@@ -127,8 +144,14 @@ self.onmessage = async (e) => {
|
|
|
127
144
|
for (const k of new Set([...Object.keys(envBefore), ...Object.keys(live)]))
|
|
128
145
|
if (envBefore[k] !== live[k]) out.push('process.env.' + k)
|
|
129
146
|
if (globalThis.process.cwd() !== cwdBefore) out.push('process.cwd (a chdir)')
|
|
147
|
+
if (globalThis.process.umask() !== umaskIn) out.push('process.umask')
|
|
130
148
|
return out
|
|
131
149
|
}
|
|
150
|
+
const umaskBack = () => {
|
|
151
|
+
if (globalThis.process.umask() === umaskIn) return false
|
|
152
|
+
globalThis.process.umask(umaskIn)
|
|
153
|
+
return true
|
|
154
|
+
}
|
|
132
155
|
try {
|
|
133
156
|
const ns = await import(path)
|
|
134
157
|
// Awaited as the in-process load's async return flattens it: a Promise
|
|
@@ -144,6 +167,7 @@ self.onmessage = async (e) => {
|
|
|
144
167
|
json: isObject && nonJson.length === 0 ? JSON.stringify(mod) : null,
|
|
145
168
|
fn: typeof mod === 'function',
|
|
146
169
|
changed: changed(),
|
|
170
|
+
umask: umaskBack(),
|
|
147
171
|
})
|
|
148
172
|
} catch (err) {
|
|
149
173
|
postMessage({
|
|
@@ -153,6 +177,7 @@ self.onmessage = async (e) => {
|
|
|
153
177
|
message: err?.message ?? String(err),
|
|
154
178
|
stack: err?.stack ?? null,
|
|
155
179
|
changed: changed(),
|
|
180
|
+
umask: umaskBack(),
|
|
156
181
|
position:
|
|
157
182
|
err?.position && typeof err.position === 'object'
|
|
158
183
|
? { file: err.position.file, line: err.position.line, column: err.position.column }
|
|
@@ -164,6 +189,9 @@ self.onmessage = async (e) => {
|
|
|
164
189
|
|
|
165
190
|
const FUNCTION_EXPORT = (): void => {}
|
|
166
191
|
|
|
192
|
+
/** A repeat load moved the process's umask; the loader names the config (D-125). */
|
|
193
|
+
export class UmaskChanged extends Error {}
|
|
194
|
+
|
|
167
195
|
const WORKER_URL = `data:text/javascript,${encodeURIComponent(WORKER_SRC)}`
|
|
168
196
|
|
|
169
197
|
interface WorkerReply {
|
|
@@ -172,6 +200,8 @@ interface WorkerReply {
|
|
|
172
200
|
json: string | null
|
|
173
201
|
/** The default export is a function, which JSON cannot carry back. */
|
|
174
202
|
fn?: boolean
|
|
203
|
+
/** The evaluation moved the process's umask; the worker put it back. */
|
|
204
|
+
umask?: boolean
|
|
175
205
|
nonJson: NonJsonValue[]
|
|
176
206
|
name: string
|
|
177
207
|
message: string
|
|
@@ -333,6 +363,7 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
|
|
|
333
363
|
timer.unref?.()
|
|
334
364
|
w.postMessage({ id, path: abs, env: { ...process.env } })
|
|
335
365
|
})
|
|
366
|
+
if (reply.umask === true) throw new UmaskChanged()
|
|
336
367
|
const [nonJson] = reply.nonJson
|
|
337
368
|
if (nonJson !== undefined) throw new UserError(nonJsonMessage(configPath, nonJson))
|
|
338
369
|
// A function stands in for the one the config exported, so the caller's
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CONFIG_EXIT,
|
|
10
10
|
evalBudgetMs,
|
|
11
11
|
evaluateConfigFresh,
|
|
12
|
+
UmaskChanged,
|
|
12
13
|
WATCHED_BUILTIN_NAMES,
|
|
13
14
|
} from './config-eval.js'
|
|
14
15
|
import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
|
|
@@ -448,9 +449,10 @@ export async function loadProjectConfigs(
|
|
|
448
449
|
// Taken only when a config is evaluated in this process: reading every
|
|
449
450
|
// descriptor of `Bun` makes Bun build its lazy members (`bun:sql`,
|
|
450
451
|
// `node:stream`), ~7 ms of a two-config warm run where every load hit.
|
|
451
|
-
let builtins: BuiltinSnapshot
|
|
452
|
+
let builtins: BuiltinSnapshot | undefined
|
|
452
453
|
let env: Readonly<Record<string, string | undefined>> = {}
|
|
453
454
|
let cwd = ''
|
|
455
|
+
let umask = -1
|
|
454
456
|
// More than one evaluation in flight: a change seen after one load may
|
|
455
457
|
// be another's.
|
|
456
458
|
let overlapping = false
|
|
@@ -489,6 +491,7 @@ export async function loadProjectConfigs(
|
|
|
489
491
|
if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
|
|
490
492
|
const mod = repeat
|
|
491
493
|
? await evaluateConfigFresh(configPath).catch((err: unknown) => {
|
|
494
|
+
if (err instanceof UmaskChanged) throw builtinsChanged(['process.umask'], configPath)
|
|
492
495
|
throw configLoadError(err, configPath, 'Project') ?? err
|
|
493
496
|
})
|
|
494
497
|
: await loadDefaultExport(configPath, 'Project', bytes!)
|
|
@@ -496,7 +499,12 @@ export async function loadProjectConfigs(
|
|
|
496
499
|
// turned the JSON-data walk's own check into "a cyclic reference".
|
|
497
500
|
const changed = repeat
|
|
498
501
|
? []
|
|
499
|
-
: [
|
|
502
|
+
: [
|
|
503
|
+
...restoreBuiltins(builtins),
|
|
504
|
+
...restoreEnv(env),
|
|
505
|
+
...restoreCwd(cwd),
|
|
506
|
+
...restoreUmask(umask),
|
|
507
|
+
]
|
|
500
508
|
// Loads overlap, so another config's change can surface after this
|
|
501
509
|
// one: named here, the refusal blamed the wrong file (D-119). The round
|
|
502
510
|
// finds the one that made it.
|
|
@@ -549,6 +557,7 @@ export async function loadProjectConfigs(
|
|
|
549
557
|
builtins = builtinSnapshot()
|
|
550
558
|
env = { ...process.env }
|
|
551
559
|
cwd = process.cwd()
|
|
560
|
+
umask = currentUmask()
|
|
552
561
|
}
|
|
553
562
|
overlapping = misses.length > 1
|
|
554
563
|
let next = 0
|
|
@@ -569,7 +578,12 @@ export async function loadProjectConfigs(
|
|
|
569
578
|
}
|
|
570
579
|
const changed =
|
|
571
580
|
misses.length > 0
|
|
572
|
-
? [
|
|
581
|
+
? [
|
|
582
|
+
...restoreBuiltins(builtins),
|
|
583
|
+
...restoreEnv(env),
|
|
584
|
+
...restoreCwd(cwd),
|
|
585
|
+
...restoreUmask(umask),
|
|
586
|
+
]
|
|
573
587
|
: []
|
|
574
588
|
if (first?.failed instanceof ChangedInRound) {
|
|
575
589
|
for (const i of misses) {
|
|
@@ -618,20 +632,51 @@ const WATCHED_BUILTINS: ReadonlyArray<readonly [string, object]> = WATCHED_BUILT
|
|
|
618
632
|
] as const,
|
|
619
633
|
)
|
|
620
634
|
|
|
635
|
+
// The check reads through these, taken when a round's snapshot is, before
|
|
636
|
+
// any of its configs runs: a config that set `Reflect.ownKeys = () => []`
|
|
637
|
+
// blinded it, and its `Object.prototype.exec` ran in another project's
|
|
638
|
+
// task (D-124). Each round keeps its own, so an overlapping round cannot
|
|
639
|
+
// hand it a replaced one. The compare and put-back use indexed loops only
|
|
640
|
+
// (a replaced `Array.prototype.forEach` skipped them).
|
|
641
|
+
interface Primitives {
|
|
642
|
+
ownKeys: typeof Reflect.ownKeys
|
|
643
|
+
descriptorOf: typeof Object.getOwnPropertyDescriptor
|
|
644
|
+
defineOwn: typeof Object.defineProperty
|
|
645
|
+
deleteOwn: typeof Reflect.deleteProperty
|
|
646
|
+
hasOwn: typeof Object.hasOwn
|
|
647
|
+
same: typeof Object.is
|
|
648
|
+
keyName: typeof String
|
|
649
|
+
}
|
|
650
|
+
|
|
621
651
|
interface OwnProperties {
|
|
622
652
|
keys: PropertyKey[]
|
|
623
653
|
descriptors: PropertyDescriptor[]
|
|
624
|
-
byKey: ReadonlyMap<PropertyKey, PropertyDescriptor>
|
|
625
654
|
}
|
|
626
655
|
|
|
627
|
-
|
|
656
|
+
interface BuiltinSnapshot {
|
|
657
|
+
readonly prims: Primitives
|
|
658
|
+
readonly props: readonly OwnProperties[]
|
|
659
|
+
}
|
|
628
660
|
|
|
629
661
|
function builtinSnapshot(): BuiltinSnapshot {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
662
|
+
const prims: Primitives = {
|
|
663
|
+
ownKeys: Reflect.ownKeys,
|
|
664
|
+
descriptorOf: Object.getOwnPropertyDescriptor,
|
|
665
|
+
defineOwn: Object.defineProperty,
|
|
666
|
+
deleteOwn: Reflect.deleteProperty,
|
|
667
|
+
hasOwn: Object.hasOwn,
|
|
668
|
+
same: Object.is,
|
|
669
|
+
keyName: String,
|
|
670
|
+
}
|
|
671
|
+
const props: OwnProperties[] = []
|
|
672
|
+
for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
|
|
673
|
+
const proto = WATCHED_BUILTINS[i]![1]
|
|
674
|
+
const keys = prims.ownKeys(proto)
|
|
675
|
+
const descriptors: PropertyDescriptor[] = []
|
|
676
|
+
for (let j = 0; j < keys.length; j++) descriptors[j] = prims.descriptorOf(proto, keys[j]!)!
|
|
677
|
+
props[i] = { keys, descriptors }
|
|
678
|
+
}
|
|
679
|
+
return { prims, props }
|
|
635
680
|
}
|
|
636
681
|
|
|
637
682
|
/**
|
|
@@ -641,13 +686,17 @@ function builtinSnapshot(): BuiltinSnapshot {
|
|
|
641
686
|
* evaluated config: the full check was ~0.1 ms a config, ~100 ms of a
|
|
642
687
|
* 1,000-config cold load.
|
|
643
688
|
*/
|
|
644
|
-
function unchanged(
|
|
689
|
+
function unchanged(
|
|
690
|
+
p: Primitives,
|
|
691
|
+
proto: object,
|
|
692
|
+
keys: readonly PropertyKey[],
|
|
693
|
+
was: OwnProperties,
|
|
694
|
+
): boolean {
|
|
645
695
|
if (keys.length !== was.keys.length) return false
|
|
646
696
|
for (let j = 0; j < keys.length; j++) {
|
|
647
697
|
const key = keys[j]!
|
|
648
698
|
if (key !== was.keys[j]) return false
|
|
649
|
-
if (!sameDescriptor(was.descriptors[j]!,
|
|
650
|
-
return false
|
|
699
|
+
if (!sameDescriptor(p, was.descriptors[j]!, p.descriptorOf(proto, key)!)) return false
|
|
651
700
|
}
|
|
652
701
|
return true
|
|
653
702
|
}
|
|
@@ -667,6 +716,9 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
|
|
|
667
716
|
(changed.some((c) => c.startsWith('process.cwd'))
|
|
668
717
|
? '; a task runs in its project directory, and `cd <dir> && …` in `exec.command` moves it'
|
|
669
718
|
: '') +
|
|
719
|
+
(changed.some((c) => c.startsWith('process.umask'))
|
|
720
|
+
? '; a task sets its own with `umask <mode> && …` in `exec.command`'
|
|
721
|
+
: '') +
|
|
670
722
|
(changed.some((c) => c.startsWith('globalThis.'))
|
|
671
723
|
? '; a constant configs share goes in a module each one imports'
|
|
672
724
|
: '')
|
|
@@ -676,27 +728,38 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
|
|
|
676
728
|
}
|
|
677
729
|
|
|
678
730
|
/** Puts back what changed since `before`, naming each property it put back. */
|
|
679
|
-
function restoreBuiltins(before: BuiltinSnapshot): string[] {
|
|
731
|
+
function restoreBuiltins(before: BuiltinSnapshot | undefined): string[] {
|
|
680
732
|
const changed: string[] = []
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
733
|
+
if (before === undefined) return changed
|
|
734
|
+
const p = before.prims
|
|
735
|
+
for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
|
|
736
|
+
const name = WATCHED_BUILTINS[i]![0]
|
|
737
|
+
const proto = WATCHED_BUILTINS[i]![1]
|
|
738
|
+
const was = before.props[i]!
|
|
739
|
+
const keys = p.ownKeys(proto)
|
|
740
|
+
if (unchanged(p, proto, keys, was)) continue
|
|
741
|
+
// Slow path, a change only: a linear lookup keeps it off `Map`.
|
|
742
|
+
for (let j = 0; j < keys.length; j++) {
|
|
743
|
+
const key = keys[j]!
|
|
744
|
+
let prior: PropertyDescriptor | undefined
|
|
745
|
+
for (let k = 0; k < was.keys.length; k++) {
|
|
746
|
+
if (was.keys[k] === key) {
|
|
747
|
+
prior = was.descriptors[k]
|
|
748
|
+
break
|
|
749
|
+
}
|
|
697
750
|
}
|
|
751
|
+
if (prior !== undefined && sameDescriptor(p, prior, p.descriptorOf(proto, key)!)) continue
|
|
752
|
+
changed[changed.length] = name + '.' + p.keyName(key)
|
|
753
|
+
if (prior === undefined) p.deleteOwn(proto, key)
|
|
754
|
+
else p.defineOwn(proto, key, prior)
|
|
698
755
|
}
|
|
699
|
-
|
|
756
|
+
for (let k = 0; k < was.keys.length; k++) {
|
|
757
|
+
const key = was.keys[k]!
|
|
758
|
+
if (p.hasOwn(proto, key)) continue
|
|
759
|
+
changed[changed.length] = name + '.' + p.keyName(key)
|
|
760
|
+
p.defineOwn(proto, key, was.descriptors[k]!)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
700
763
|
return changed
|
|
701
764
|
}
|
|
702
765
|
|
|
@@ -739,10 +802,30 @@ function restoreCwd(before: string): string[] {
|
|
|
739
802
|
return ['process.cwd (a chdir)']
|
|
740
803
|
}
|
|
741
804
|
|
|
742
|
-
|
|
805
|
+
/**
|
|
806
|
+
* A config's `process.umask()` set the mode of every file vx and its tasks
|
|
807
|
+
* wrote after it: a cache artifact and a task's outputs landed `000`, which
|
|
808
|
+
* a user other than root could not read back (D-125). Put back, and named.
|
|
809
|
+
*/
|
|
810
|
+
function currentUmask(): number {
|
|
811
|
+
// Through globalThis, as restoreCwd: the playground's shim has no umask.
|
|
812
|
+
const proc = globalThis.process as { umask?: (mask?: number) => number }
|
|
813
|
+
if (typeof proc.umask !== 'function') return -1
|
|
814
|
+
// No argument reads it: setting 0 and back would open a window where a
|
|
815
|
+
// file written by an overlapping write landed world-writable.
|
|
816
|
+
return proc.umask()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function restoreUmask(before: number): string[] {
|
|
820
|
+
if (before === -1 || currentUmask() === before) return []
|
|
821
|
+
;(globalThis.process as { umask: (mask: number) => number }).umask(before)
|
|
822
|
+
return ['process.umask']
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function sameDescriptor(p: Primitives, a: PropertyDescriptor, b: PropertyDescriptor): boolean {
|
|
743
826
|
return (
|
|
744
827
|
// `globalThis.NaN` is watched (D-122), and NaN !== NaN.
|
|
745
|
-
|
|
828
|
+
p.same(a.value, b.value) &&
|
|
746
829
|
a.get === b.get &&
|
|
747
830
|
a.set === b.set &&
|
|
748
831
|
a.writable === b.writable &&
|