@taliesinb/mash 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- package/src/vm/test.ts +253 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running external commands as real child processes.
|
|
3
|
+
*
|
|
4
|
+
* stdio is connected through the VM's open files: pipes and captures are
|
|
5
|
+
* pumped (so trace listeners see the bytes), /dev/null maps to `ignore`, and
|
|
6
|
+
* untraced host files are passed as raw fds. Process-substitution paths
|
|
7
|
+
* (`/dev/fd/N` naming a VM fd) are mapped onto extra child stdio slots
|
|
8
|
+
* (`devfd`, macOS) or onto named FIFOs (`fifo`, Linux; see fifo.ts).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn as nodeSpawn, type ChildProcess, type StdioOptions } from 'node:child_process'
|
|
12
|
+
import * as os from 'node:os'
|
|
13
|
+
import type { Interpreter } from './interp.ts'
|
|
14
|
+
import { FifoSet } from './fifo.ts'
|
|
15
|
+
import { BrokenPipeError, HostFile, NullFile, type OpenFile } from './io.ts'
|
|
16
|
+
import type { Proc } from './proc.ts'
|
|
17
|
+
import { HOST_UMASK } from './state.ts'
|
|
18
|
+
|
|
19
|
+
export interface SpawnRequest {
|
|
20
|
+
file: string
|
|
21
|
+
args: string[]
|
|
22
|
+
argv0: string
|
|
23
|
+
cwd: string
|
|
24
|
+
env: Record<string, string>
|
|
25
|
+
stdio: StdioOptions
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Spawn a child. Replace it to confine children (e.g. through the harness's `ctx.sandbox.confine`). */
|
|
29
|
+
export type SpawnFn = (req: SpawnRequest) => ChildProcess | Promise<ChildProcess>
|
|
30
|
+
|
|
31
|
+
export const defaultSpawn: SpawnFn = (req) =>
|
|
32
|
+
nodeSpawn(req.file, req.args, { argv0: req.argv0, cwd: req.cwd, env: req.env, stdio: req.stdio })
|
|
33
|
+
|
|
34
|
+
export interface ExternalRequest {
|
|
35
|
+
execId: number
|
|
36
|
+
path: string
|
|
37
|
+
argv: string[]
|
|
38
|
+
env: Record<string, string>
|
|
39
|
+
cwd: string
|
|
40
|
+
proc: Proc
|
|
41
|
+
readsStdin: boolean | 'maybe'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function signalNumber(sig: NodeJS.Signals | null): number {
|
|
45
|
+
if (!sig) return 0
|
|
46
|
+
return (os.constants.signals as Record<string, number>)[sig] ?? 15
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type Slot =
|
|
50
|
+
| { kind: 'ignore' }
|
|
51
|
+
| { kind: 'fd'; fd: number }
|
|
52
|
+
| { kind: 'pump-in'; file: OpenFile }
|
|
53
|
+
| { kind: 'pump-out'; file: OpenFile }
|
|
54
|
+
|
|
55
|
+
function outSlot(file: OpenFile | undefined, traced: boolean): Slot {
|
|
56
|
+
if (!file || !file.writable) return { kind: 'ignore' }
|
|
57
|
+
if (file instanceof NullFile) return { kind: 'ignore' }
|
|
58
|
+
if (file instanceof HostFile && !traced) return { kind: 'fd', fd: file.fd }
|
|
59
|
+
return { kind: 'pump-out', file }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function inSlot(file: OpenFile | undefined, traced: boolean, readsStdin: boolean | 'maybe'): Slot {
|
|
63
|
+
if (!file || !file.readable || readsStdin === false) return { kind: 'ignore' }
|
|
64
|
+
if (file instanceof NullFile) return { kind: 'ignore' }
|
|
65
|
+
if (file instanceof HostFile && !traced && file.buffered === 0) return { kind: 'fd', fd: file.fd }
|
|
66
|
+
return { kind: 'pump-in', file }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Arguments that open one of the child's own stdio fds by name. */
|
|
70
|
+
const STD_NAMES: Record<string, number> = { '/dev/stdin': 0, '/dev/stdout': 1, '/dev/stderr': 2, '/dev/fd/0': 0, '/dev/fd/1': 1, '/dev/fd/2': 2 }
|
|
71
|
+
|
|
72
|
+
/** An argument served through a named FIFO (`fifo` fd passing). */
|
|
73
|
+
interface FifoArg {
|
|
74
|
+
arg: number
|
|
75
|
+
file: OpenFile
|
|
76
|
+
/** The child reads it (mash feeds the FIFO); else the child writes it (mash drains it). */
|
|
77
|
+
feed: boolean
|
|
78
|
+
/** The VM fd it names (≥ 3), for the `/dev/fd/K` fallback when no FIFO can be made. */
|
|
79
|
+
vmFd?: number
|
|
80
|
+
/** stdin slot this argument displaced (restored on fallback). */
|
|
81
|
+
displaced?: Slot
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runExternal(interp: Interpreter, req: ExternalRequest): Promise<number> {
|
|
85
|
+
const rt = interp.rt
|
|
86
|
+
const { proc } = req
|
|
87
|
+
const traced = rt.listeners.length > 0
|
|
88
|
+
// a behavior's "file operands, so no stdin" does not hold for an operand that names stdin
|
|
89
|
+
const readsStdin = req.readsStdin === false && req.argv.some((a, i) => i > 0 && STD_NAMES[a] === 0) ? 'maybe' : req.readsStdin
|
|
90
|
+
const slots: Slot[] = [
|
|
91
|
+
inSlot(proc.fds.get(0), traced, readsStdin),
|
|
92
|
+
outSlot(proc.fds.get(1), traced),
|
|
93
|
+
outSlot(proc.fds.get(2), traced),
|
|
94
|
+
]
|
|
95
|
+
const fifoMode = rt.fdPassing === 'fifo'
|
|
96
|
+
const fifoArgs: FifoArg[] = []
|
|
97
|
+
const extraSlot = (file: OpenFile): string => {
|
|
98
|
+
const k = slots.length
|
|
99
|
+
slots.push(file.readable && !file.writable ? { kind: 'pump-in', file } : { kind: 'pump-out', file })
|
|
100
|
+
return `/dev/fd/${k}`
|
|
101
|
+
}
|
|
102
|
+
// /dev/fd/N arguments naming VM fds (process substitution, `exec 3<...`)
|
|
103
|
+
const args = req.argv.slice(1).map((a, i) => {
|
|
104
|
+
const m = /^\/dev\/fd\/(\d+)$/.exec(a)
|
|
105
|
+
const n = m ? Number(m[1]) : fifoMode ? STD_NAMES[a] : undefined
|
|
106
|
+
if (n === undefined) return a
|
|
107
|
+
if (n < 3) {
|
|
108
|
+
// The child's own fd 0-2. Under `fifo` (Linux) a pumped slot is a socket, which the
|
|
109
|
+
// child cannot open by name: serve the argument through a FIFO on the same VM file.
|
|
110
|
+
const slot = slots[n]!
|
|
111
|
+
if (!fifoMode || (slot.kind !== 'pump-in' && slot.kind !== 'pump-out')) return a
|
|
112
|
+
const feed = slot.kind === 'pump-in'
|
|
113
|
+
// one consumer for the stdin data: the child's fd 0 becomes /dev/null
|
|
114
|
+
if (feed) slots[n] = { kind: 'ignore' }
|
|
115
|
+
fifoArgs.push({ arg: i, file: slot.file, feed, displaced: feed ? slot : undefined })
|
|
116
|
+
return a
|
|
117
|
+
}
|
|
118
|
+
const file = proc.fds.get(n)
|
|
119
|
+
if (!file) return a
|
|
120
|
+
if (!fifoMode) return extraSlot(file)
|
|
121
|
+
fifoArgs.push({ arg: i, file, feed: file.readable && !file.writable, vmFd: n })
|
|
122
|
+
return a
|
|
123
|
+
})
|
|
124
|
+
let fifos: { set: FifoSet; paths: string[] } | undefined
|
|
125
|
+
if (fifoArgs.length > 0) {
|
|
126
|
+
try {
|
|
127
|
+
fifos = await FifoSet.create(fifoArgs.length)
|
|
128
|
+
fifoArgs.forEach((f, j) => (args[f.arg] = fifos!.paths[j]!))
|
|
129
|
+
} catch {
|
|
130
|
+
// no FIFO possible (mkfifo missing, temp dir not writable): fall back to socket slots
|
|
131
|
+
for (const f of fifoArgs) {
|
|
132
|
+
if (f.vmFd !== undefined) args[f.arg] = extraSlot(f.file)
|
|
133
|
+
else if (f.displaced) slots[0] = f.displaced
|
|
134
|
+
}
|
|
135
|
+
fifoArgs.length = 0
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return await spawnAndPump(interp, req, slots, args, fifos ? fifoArgs.map((f, j) => ({ ...f, path: fifos!.paths[j]! })) : [], fifos?.set)
|
|
140
|
+
} finally {
|
|
141
|
+
fifos?.set.cleanup()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function spawnAndPump(
|
|
146
|
+
interp: Interpreter,
|
|
147
|
+
req: ExternalRequest,
|
|
148
|
+
slots: Slot[],
|
|
149
|
+
args: string[],
|
|
150
|
+
fifoArgs: (FifoArg & { path: string })[],
|
|
151
|
+
fifoSet: FifoSet | undefined,
|
|
152
|
+
): Promise<number> {
|
|
153
|
+
const rt = interp.rt
|
|
154
|
+
const { proc } = req
|
|
155
|
+
const stdio: StdioOptions = slots.map((s) => (s.kind === 'ignore' ? 'ignore' : s.kind === 'fd' ? s.fd : 'pipe'))
|
|
156
|
+
|
|
157
|
+
// Node cannot start a child with a closed fd or another umask: use a tiny sh exec prelude when needed
|
|
158
|
+
const prelude: string[] = []
|
|
159
|
+
if (proc.state.umask !== HOST_UMASK) prelude.push(`umask ${proc.state.umask.toString(8).padStart(4, '0')}`)
|
|
160
|
+
for (const fd of [0, 1, 2]) if (!proc.fds.get(fd)) prelude.push(`exec ${fd}>&-`)
|
|
161
|
+
let file = req.path
|
|
162
|
+
let spawnArgs = args
|
|
163
|
+
if (prelude.length > 0) {
|
|
164
|
+
file = '/bin/sh'
|
|
165
|
+
spawnArgs = ['-c', `${prelude.join('; ')}; exec "$@"`, 'sh', req.path, ...args]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let child: ChildProcess
|
|
169
|
+
try {
|
|
170
|
+
child = await rt.spawn({ file, args: spawnArgs, argv0: prelude.length > 0 ? 'sh' : req.argv[0], cwd: req.cwd, env: req.env, stdio })
|
|
171
|
+
} catch (e) {
|
|
172
|
+
interp.errorMessage(proc, `${req.argv[0]}: ${(e as Error).message}`)
|
|
173
|
+
return 126
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const exited = new AbortController()
|
|
177
|
+
const pumps: Promise<void>[] = []
|
|
178
|
+
let spawnError: NodeJS.ErrnoException | null = null
|
|
179
|
+
|
|
180
|
+
const exit = new Promise<number>((resolve) => {
|
|
181
|
+
child.once('error', (e: NodeJS.ErrnoException) => {
|
|
182
|
+
spawnError = e
|
|
183
|
+
exited.abort()
|
|
184
|
+
resolve(e.code === 'ENOENT' ? 127 : 126)
|
|
185
|
+
})
|
|
186
|
+
child.once('exit', (code, signal) => {
|
|
187
|
+
exited.abort()
|
|
188
|
+
resolve(code ?? 128 + signalNumber(signal))
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
rt.emit({ type: 'command:spawn', execId: req.execId, osPid: child.pid })
|
|
193
|
+
|
|
194
|
+
const onAbort = (): void => {
|
|
195
|
+
child.kill('SIGTERM')
|
|
196
|
+
setTimeout(() => child.kill('SIGKILL'), 2000).unref()
|
|
197
|
+
}
|
|
198
|
+
rt.signal.addEventListener('abort', onAbort, { once: true })
|
|
199
|
+
proc.jobChildren?.add(child)
|
|
200
|
+
if (proc.jobSignal?.aborted) child.kill('SIGTERM')
|
|
201
|
+
|
|
202
|
+
slots.forEach((slot, i) => {
|
|
203
|
+
const stream = child.stdio[i]
|
|
204
|
+
if (!stream) return
|
|
205
|
+
if (slot.kind === 'pump-in') {
|
|
206
|
+
const w = stream as NodeJS.WritableStream & { destroyed?: boolean; writableNeedDrain?: boolean }
|
|
207
|
+
w.on('error', () => {}) // EPIPE when the child exits without reading everything
|
|
208
|
+
pumps.push((async () => {
|
|
209
|
+
try {
|
|
210
|
+
for (;;) {
|
|
211
|
+
const chunk = await slot.file.read(exited.signal)
|
|
212
|
+
if (chunk === null) {
|
|
213
|
+
if (!exited.signal.aborted) w.end()
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (exited.signal.aborted || w.destroyed) {
|
|
217
|
+
slot.file.unread(chunk)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (!w.write(chunk)) {
|
|
221
|
+
await new Promise<void>((resolve) => {
|
|
222
|
+
const done = (): void => {
|
|
223
|
+
w.removeListener('drain', done)
|
|
224
|
+
exited.signal.removeEventListener('abort', done)
|
|
225
|
+
resolve()
|
|
226
|
+
}
|
|
227
|
+
w.once('drain', done)
|
|
228
|
+
exited.signal.addEventListener('abort', done, { once: true })
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// reading the source failed: give the child EOF
|
|
234
|
+
w.end()
|
|
235
|
+
}
|
|
236
|
+
})())
|
|
237
|
+
} else if (slot.kind === 'pump-out') {
|
|
238
|
+
const r = stream as NodeJS.ReadableStream & { destroy(): void }
|
|
239
|
+
pumps.push((async () => {
|
|
240
|
+
try {
|
|
241
|
+
for await (const chunk of r) {
|
|
242
|
+
await slot.file.write(chunk as Uint8Array)
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
if (e instanceof BrokenPipeError) r.destroy()
|
|
246
|
+
else r.destroy()
|
|
247
|
+
}
|
|
248
|
+
})())
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
if (fifoSet) {
|
|
253
|
+
for (const f of fifoArgs) pumps.push(f.feed ? fifoSet.feed(f.path, f.file, exited.signal) : fifoSet.drain(f.path, f.file, exited.signal))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const status = await exit
|
|
257
|
+
// stdout/stderr pumps finish at EOF; stdin pumps stop at exit
|
|
258
|
+
await Promise.all(pumps)
|
|
259
|
+
rt.signal.removeEventListener('abort', onAbort)
|
|
260
|
+
proc.jobChildren?.delete(child)
|
|
261
|
+
if (spawnError) {
|
|
262
|
+
interp.errorMessage(proc, `${req.argv[0]}: ${(spawnError as NodeJS.ErrnoException).message}`)
|
|
263
|
+
}
|
|
264
|
+
return status
|
|
265
|
+
}
|
package/src/vm/fifo.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-FIFO fallback for handing VM file descriptors to external commands.
|
|
3
|
+
*
|
|
4
|
+
* Node creates child stdio pipes as UNIX socketpairs. macOS lets a child open
|
|
5
|
+
* `/dev/fd/N` on a socket (it dups the descriptor); Linux reopens the file
|
|
6
|
+
* through `/proc/self/fd/N`, which fails with ENXIO for sockets. So on Linux a
|
|
7
|
+
* process substitution (`diff <(a) <(b)`) or an `exec 3<f; cmd /dev/fd/3`
|
|
8
|
+
* passed to an external command cannot use the socket slot. Instead the child
|
|
9
|
+
* gets the path of a named FIFO in a private temp dir, and mash pumps between
|
|
10
|
+
* the FIFO and the VM file (so taps and tracing still see the bytes).
|
|
11
|
+
*
|
|
12
|
+
* Opening a FIFO blocks until the other side opens it, and a blocking open
|
|
13
|
+
* would hold one of libuv's four threadpool threads. So mash never does a
|
|
14
|
+
* blocking open: everything here uses `O_NONBLOCK` descriptors and polls with
|
|
15
|
+
* a short backoff (immediate while data flows, at most {@link MAX_DELAY} ms
|
|
16
|
+
* when idle).
|
|
17
|
+
*
|
|
18
|
+
* - **The child reads** (`<(cmd)`): mash polls `open(O_WRONLY|O_NONBLOCK)`,
|
|
19
|
+
* which fails with ENXIO until the child has opened the FIFO for reading.
|
|
20
|
+
* Only then does it write, so no data can be lost in a pipe nobody holds,
|
|
21
|
+
* and closing it at the VM file's EOF gives the child EOF. If the child
|
|
22
|
+
* exits without opening it, nothing is consumed from the VM file.
|
|
23
|
+
* - **The child writes** (`>(cmd)`): mash opens the read side
|
|
24
|
+
* `O_RDONLY|O_NONBLOCK` up front (that never blocks, and lets the child's
|
|
25
|
+
* own open for writing succeed at once) and polls `read`. A read of 0 means
|
|
26
|
+
* "no writer right now", so the pump only stops once the child has exited
|
|
27
|
+
* and every writer (including grandchildren) has closed.
|
|
28
|
+
*
|
|
29
|
+
* The strategy is chosen per machine: {@link defaultFdPassing} (Linux and the
|
|
30
|
+
* other non-macOS systems → `fifo`, macOS → `devfd`), overridable with the
|
|
31
|
+
* `MASH_PROCSUBST=fifo|devfd` environment variable or `MachineOptions.procSubst`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execFile } from 'node:child_process'
|
|
35
|
+
import * as fs from 'node:fs'
|
|
36
|
+
import * as os from 'node:os'
|
|
37
|
+
import * as path from 'node:path'
|
|
38
|
+
import type { OpenFile } from './io.ts'
|
|
39
|
+
|
|
40
|
+
/** How VM fds named by `/dev/fd/N` arguments reach an external command. */
|
|
41
|
+
export type FdPassing = 'devfd' | 'fifo'
|
|
42
|
+
|
|
43
|
+
/** `MASH_PROCSUBST` if set to `fifo`/`devfd`, else `devfd` on macOS and `fifo` elsewhere. */
|
|
44
|
+
export function defaultFdPassing(env: Record<string, string | undefined> = process.env): FdPassing {
|
|
45
|
+
const v = env.MASH_PROCSUBST
|
|
46
|
+
if (v === 'fifo' || v === 'devfd') return v
|
|
47
|
+
return process.platform === 'darwin' ? 'devfd' : 'fifo'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MAX_DELAY = 16
|
|
51
|
+
|
|
52
|
+
/** Exponential backoff for polling: 0 (a macrotask turn), 1, 2, 4 … MAX_DELAY ms. */
|
|
53
|
+
class Backoff {
|
|
54
|
+
private delay = 0
|
|
55
|
+
reset(): void {
|
|
56
|
+
this.delay = 0
|
|
57
|
+
}
|
|
58
|
+
/** Sleep; if `signal` aborts *during* the sleep, wake early. */
|
|
59
|
+
wait(signal?: AbortSignal): Promise<void> {
|
|
60
|
+
const ms = this.delay
|
|
61
|
+
this.delay = Math.min(MAX_DELAY, ms === 0 ? 1 : ms * 2)
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
if (signal?.aborted) signal = undefined
|
|
64
|
+
const done = (): void => {
|
|
65
|
+
clearTimeout(t)
|
|
66
|
+
signal?.removeEventListener('abort', done)
|
|
67
|
+
resolve()
|
|
68
|
+
}
|
|
69
|
+
const t = ms === 0 ? setTimeout(done, 0) : setTimeout(done, ms)
|
|
70
|
+
signal?.addEventListener('abort', done, { once: true })
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unlink(p: string): void {
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(p)
|
|
78
|
+
} catch {
|
|
79
|
+
// already gone
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function code(e: unknown): string | undefined {
|
|
84
|
+
return (e as NodeJS.ErrnoException | null)?.code
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A private temp dir holding the FIFOs of one external command. */
|
|
88
|
+
export class FifoSet {
|
|
89
|
+
readonly dir: string
|
|
90
|
+
private readonly feeds: string[] = []
|
|
91
|
+
private constructor(dir: string) {
|
|
92
|
+
this.dir = dir
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Create a private dir with `n` FIFOs (`0`, `1`, …), mode 0600, via one `mkfifo` call. */
|
|
96
|
+
static async create(n: number): Promise<{ set: FifoSet; paths: string[] }> {
|
|
97
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'mash-fifo-'))
|
|
98
|
+
const set = new FifoSet(dir)
|
|
99
|
+
const paths = Array.from({ length: n }, (_, i) => path.join(dir, String(i)))
|
|
100
|
+
try {
|
|
101
|
+
await new Promise<void>((resolve, reject) => {
|
|
102
|
+
execFile('mkfifo', ['-m', '600', ...paths], { env: { PATH: `/usr/bin:/bin:${process.env.PATH ?? ''}` } }, (err) => (err ? reject(err) : resolve()))
|
|
103
|
+
})
|
|
104
|
+
} catch (e) {
|
|
105
|
+
set.cleanup()
|
|
106
|
+
throw e
|
|
107
|
+
}
|
|
108
|
+
return { set, paths }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Feed the FIFO at `p` from `file` (the child reads it). Resolves at the
|
|
113
|
+
* file's EOF, when the child stops reading, or once `exited` aborts.
|
|
114
|
+
*/
|
|
115
|
+
async feed(p: string, file: OpenFile, exited: AbortSignal): Promise<void> {
|
|
116
|
+
this.feeds.push(p)
|
|
117
|
+
const C = fs.constants
|
|
118
|
+
const backoff = new Backoff()
|
|
119
|
+
let fd = -1
|
|
120
|
+
while (fd < 0) {
|
|
121
|
+
if (exited.aborted) return
|
|
122
|
+
try {
|
|
123
|
+
fd = fs.openSync(p, C.O_WRONLY | C.O_NONBLOCK)
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (code(e) !== 'ENXIO') return // no reader yet is ENXIO; anything else: give up
|
|
126
|
+
await backoff.wait(exited)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
backoff.reset()
|
|
130
|
+
try {
|
|
131
|
+
for (;;) {
|
|
132
|
+
const chunk = await file.read(exited)
|
|
133
|
+
if (chunk === null) return // EOF (or the child exited: nothing consumed)
|
|
134
|
+
let off = 0
|
|
135
|
+
while (off < chunk.length) {
|
|
136
|
+
if (exited.aborted) {
|
|
137
|
+
file.unread(chunk.subarray(off))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
off += fs.writeSync(fd, chunk, off, chunk.length - off)
|
|
142
|
+
backoff.reset()
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (code(e) === 'EAGAIN') {
|
|
145
|
+
await backoff.wait(exited)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
// EPIPE: the child closed it; leave the rest for other readers of a shared file
|
|
149
|
+
file.unread(chunk.subarray(off))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// reading the source failed: the child gets EOF
|
|
156
|
+
} finally {
|
|
157
|
+
fs.closeSync(fd)
|
|
158
|
+
// a second open for reading would wait for a writer forever: make it fail instead
|
|
159
|
+
unlink(p)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Drain the FIFO at `p` into `file` (the child writes it). Resolves once
|
|
165
|
+
* the child has exited and no writer holds the FIFO, or when `file` has no
|
|
166
|
+
* readers left (the child's writes then fail with EPIPE/SIGPIPE).
|
|
167
|
+
*/
|
|
168
|
+
async drain(p: string, file: OpenFile, exited: AbortSignal): Promise<void> {
|
|
169
|
+
const C = fs.constants
|
|
170
|
+
let fd: number
|
|
171
|
+
try {
|
|
172
|
+
fd = fs.openSync(p, C.O_RDONLY | C.O_NONBLOCK)
|
|
173
|
+
} catch {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
const buf = Buffer.allocUnsafe(65536)
|
|
177
|
+
const backoff = new Backoff()
|
|
178
|
+
try {
|
|
179
|
+
for (;;) {
|
|
180
|
+
let n: number
|
|
181
|
+
try {
|
|
182
|
+
n = fs.readSync(fd, buf, 0, buf.length, null)
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (code(e) !== 'EAGAIN') return
|
|
185
|
+
await backoff.wait() // a writer is connected but has nothing yet
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (n > 0) {
|
|
189
|
+
backoff.reset()
|
|
190
|
+
await file.write(Buffer.from(buf.subarray(0, n)))
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
// n === 0: no writer at the moment (not opened yet, or closed)
|
|
194
|
+
if (exited.aborted) return
|
|
195
|
+
await backoff.wait(exited)
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// BrokenPipeError: nobody reads any more. A later open for writing must fail, not block
|
|
199
|
+
unlink(p)
|
|
200
|
+
} finally {
|
|
201
|
+
fs.closeSync(fd)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove the FIFOs and the dir. A reader of a fed FIFO still blocked in
|
|
207
|
+
* `open` (a grandchild that opened it late) is released first: a brief
|
|
208
|
+
* non-blocking writer open lets its open return, and it reads EOF.
|
|
209
|
+
*/
|
|
210
|
+
cleanup(): void {
|
|
211
|
+
const C = fs.constants
|
|
212
|
+
for (const p of this.feeds) {
|
|
213
|
+
if (!fs.existsSync(p)) continue
|
|
214
|
+
try {
|
|
215
|
+
fs.closeSync(fs.openSync(p, C.O_WRONLY | C.O_NONBLOCK))
|
|
216
|
+
} catch {
|
|
217
|
+
// ENXIO: no reader waiting
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
fs.rmSync(this.dir, { recursive: true, force: true })
|
|
222
|
+
} catch {
|
|
223
|
+
// best effort
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|