@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
package/src/vm/io.ts
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The VM's I/O model, modelled on POSIX file descriptions.
|
|
3
|
+
*
|
|
4
|
+
* An {@link OpenFile} is an open file description: a pipe end, a real file,
|
|
5
|
+
* a capture buffer (command substitution), /dev/null, or a host stream. It is
|
|
6
|
+
* reference-counted: an {@link FdTable} (one per shell "process") holds
|
|
7
|
+
* references, subshells and pipeline stages clone the table (adding
|
|
8
|
+
* references), and closing the last write reference of a pipe delivers EOF to
|
|
9
|
+
* its reader — exactly the behaviour scripts rely on.
|
|
10
|
+
*
|
|
11
|
+
* Every OpenFile belongs to a {@link Channel}, the identity trace consumers
|
|
12
|
+
* see. Data events are reported once per byte: on write for output channels
|
|
13
|
+
* (pipes, files, captures, stdout/stderr) and on read for input-only channels
|
|
14
|
+
* (input files, here-docs).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs'
|
|
18
|
+
|
|
19
|
+
export type ChannelKind = 'pipe' | 'file' | 'capture' | 'null' | 'host' | 'heredoc' | 'procsubst'
|
|
20
|
+
|
|
21
|
+
export interface Channel {
|
|
22
|
+
/** Unique id within one machine. */
|
|
23
|
+
id: number
|
|
24
|
+
kind: ChannelKind
|
|
25
|
+
/** Human-readable label, e.g. `pipe(grep foo → wc -l)` or `> out.txt`. */
|
|
26
|
+
label: string
|
|
27
|
+
/** File path for file channels. */
|
|
28
|
+
path?: string
|
|
29
|
+
/** AST node that created the channel (pipeline, redirect, substitution). */
|
|
30
|
+
nodeId?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DataListener = (channel: Channel, chunk: Uint8Array, direction: 'write' | 'read') => void
|
|
34
|
+
|
|
35
|
+
/** Thrown by writes to a pipe whose read end is fully closed (EPIPE / SIGPIPE). */
|
|
36
|
+
export class BrokenPipeError extends Error {
|
|
37
|
+
constructor() {
|
|
38
|
+
super('Broken pipe')
|
|
39
|
+
this.name = 'BrokenPipeError'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const encoder = new TextEncoder()
|
|
44
|
+
|
|
45
|
+
export function toBytes(data: string | Uint8Array): Uint8Array {
|
|
46
|
+
return typeof data === 'string' ? encoder.encode(data) : data
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
50
|
+
if (a.length === 0) return b
|
|
51
|
+
if (b.length === 0) return a
|
|
52
|
+
const out = new Uint8Array(a.length + b.length)
|
|
53
|
+
out.set(a, 0)
|
|
54
|
+
out.set(b, a.length)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An open file description. Subclasses implement the raw operations; this
|
|
60
|
+
* base class adds reference counting, a shared read-ahead buffer (so `read`
|
|
61
|
+
* can consume one line and leave the rest for the next reader) and tracing.
|
|
62
|
+
*/
|
|
63
|
+
export abstract class OpenFile {
|
|
64
|
+
refs = 1
|
|
65
|
+
readonly channel: Channel
|
|
66
|
+
readonly readable: boolean
|
|
67
|
+
readonly writable: boolean
|
|
68
|
+
protected listeners: DataListener[]
|
|
69
|
+
private pushback: Uint8Array = new Uint8Array(0)
|
|
70
|
+
private closedForGood = false
|
|
71
|
+
/** Report reads to listeners (input-only channels). */
|
|
72
|
+
protected traceReads = false
|
|
73
|
+
|
|
74
|
+
constructor(channel: Channel, readable: boolean, writable: boolean, listeners: DataListener[]) {
|
|
75
|
+
this.channel = channel
|
|
76
|
+
this.readable = readable
|
|
77
|
+
this.writable = writable
|
|
78
|
+
this.listeners = listeners
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Add a reference (fd duplication / table clone). */
|
|
82
|
+
retain(): this {
|
|
83
|
+
this.refs++
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drop a reference; the last one closes the description. */
|
|
88
|
+
release(): void {
|
|
89
|
+
if (this.refs <= 0) return
|
|
90
|
+
this.refs--
|
|
91
|
+
if (this.refs === 0 && !this.closedForGood) {
|
|
92
|
+
this.closedForGood = true
|
|
93
|
+
this.onLastClose()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get closed(): boolean {
|
|
98
|
+
return this.closedForGood
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected abstract rawRead(): Promise<Uint8Array | null>
|
|
102
|
+
protected abstract rawWrite(chunk: Uint8Array): Promise<void>
|
|
103
|
+
protected abstract onLastClose(): void
|
|
104
|
+
|
|
105
|
+
async write(data: string | Uint8Array): Promise<void> {
|
|
106
|
+
if (!this.writable) throw new Error(`${this.channel.label}: not open for writing`)
|
|
107
|
+
const bytes = toBytes(data)
|
|
108
|
+
if (bytes.length === 0) return
|
|
109
|
+
await this.rawWrite(bytes)
|
|
110
|
+
for (const l of this.listeners) l(this.channel, bytes, 'write')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private inflight: Promise<void> | null = null
|
|
114
|
+
private sawEof = false
|
|
115
|
+
private readError: unknown = null
|
|
116
|
+
|
|
117
|
+
/** Bytes already read from the underlying source but not yet consumed. */
|
|
118
|
+
get buffered(): number {
|
|
119
|
+
return this.pushback.length
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Next chunk, or null at EOF. With `signal`, returns null early when the
|
|
124
|
+
* signal aborts — without consuming anything: data that arrives later stays
|
|
125
|
+
* buffered for the next reader. (This is what lets the VM stop feeding a
|
|
126
|
+
* child process that has exited without losing the rest of a shared stdin.)
|
|
127
|
+
*/
|
|
128
|
+
async read(signal?: AbortSignal): Promise<Uint8Array | null> {
|
|
129
|
+
if (!this.readable) throw new Error(`${this.channel.label}: not open for reading`)
|
|
130
|
+
for (;;) {
|
|
131
|
+
if (this.pushback.length > 0) {
|
|
132
|
+
const b = this.pushback
|
|
133
|
+
this.pushback = new Uint8Array(0)
|
|
134
|
+
return b
|
|
135
|
+
}
|
|
136
|
+
if (this.readError !== null) {
|
|
137
|
+
const e = this.readError
|
|
138
|
+
this.readError = null
|
|
139
|
+
throw e
|
|
140
|
+
}
|
|
141
|
+
if (this.sawEof) return null
|
|
142
|
+
if (signal?.aborted) return null
|
|
143
|
+
if (!this.inflight) {
|
|
144
|
+
this.inflight = this.rawRead().then(
|
|
145
|
+
(chunk) => {
|
|
146
|
+
this.inflight = null
|
|
147
|
+
if (chunk === null) this.sawEof = true
|
|
148
|
+
else {
|
|
149
|
+
this.pushback = concat(this.pushback, chunk)
|
|
150
|
+
if (this.traceReads) for (const l of this.listeners) l(this.channel, chunk, 'read')
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
(err: unknown) => {
|
|
154
|
+
this.inflight = null
|
|
155
|
+
this.readError = err
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
if (signal) {
|
|
160
|
+
await new Promise<void>((resolve) => {
|
|
161
|
+
const onAbort = (): void => resolve()
|
|
162
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
163
|
+
this.inflight!.then(() => {
|
|
164
|
+
signal.removeEventListener('abort', onAbort)
|
|
165
|
+
resolve()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
} else {
|
|
169
|
+
await this.inflight
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Return unconsumed bytes to the front of the stream. */
|
|
175
|
+
unread(bytes: Uint8Array): void {
|
|
176
|
+
if (bytes.length > 0) this.pushback = concat(bytes, this.pushback)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read up to and including `delim` (a byte), or to EOF. Returns null at EOF
|
|
181
|
+
* with nothing read. Leaves the remainder buffered for the next reader.
|
|
182
|
+
*/
|
|
183
|
+
async readUntil(delim: number | null, maxBytes = Infinity): Promise<Uint8Array | null> {
|
|
184
|
+
let acc: Uint8Array = new Uint8Array(0)
|
|
185
|
+
for (;;) {
|
|
186
|
+
const chunk = await this.read()
|
|
187
|
+
if (chunk === null) return acc.length > 0 ? acc : null
|
|
188
|
+
const idx = delim === null ? -1 : chunk.indexOf(delim)
|
|
189
|
+
if (idx >= 0 && acc.length + idx + 1 <= maxBytes) {
|
|
190
|
+
acc = concat(acc, chunk.subarray(0, idx + 1))
|
|
191
|
+
this.unread(chunk.subarray(idx + 1))
|
|
192
|
+
return acc
|
|
193
|
+
}
|
|
194
|
+
if (acc.length + chunk.length >= maxBytes) {
|
|
195
|
+
const take = maxBytes - acc.length
|
|
196
|
+
acc = concat(acc, chunk.subarray(0, take))
|
|
197
|
+
this.unread(chunk.subarray(take))
|
|
198
|
+
return acc
|
|
199
|
+
}
|
|
200
|
+
acc = concat(acc, chunk)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Read everything to EOF. */
|
|
205
|
+
async readAll(): Promise<Uint8Array> {
|
|
206
|
+
let acc: Uint8Array = new Uint8Array(0)
|
|
207
|
+
for (;;) {
|
|
208
|
+
const chunk = await this.read()
|
|
209
|
+
if (chunk === null) return acc
|
|
210
|
+
acc = concat(acc, chunk)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Pipes
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/** An in-memory pipe with bounded buffering (backpressure) and POSIX EOF/EPIPE semantics. */
|
|
220
|
+
export class Pipe {
|
|
221
|
+
readonly channel: Channel
|
|
222
|
+
private chunks: Uint8Array[] = []
|
|
223
|
+
private buffered = 0
|
|
224
|
+
private readonly capacity: number
|
|
225
|
+
writersOpen = true
|
|
226
|
+
readersOpen = true
|
|
227
|
+
private readWaiters: (() => void)[] = []
|
|
228
|
+
private writeWaiters: (() => void)[] = []
|
|
229
|
+
readonly readEnd: PipeEnd
|
|
230
|
+
readonly writeEnd: PipeEnd
|
|
231
|
+
|
|
232
|
+
constructor(channel: Channel, listeners: DataListener[], capacity = 64 * 1024) {
|
|
233
|
+
this.channel = channel
|
|
234
|
+
this.capacity = capacity
|
|
235
|
+
this.readEnd = new PipeEnd(this, 'read', listeners)
|
|
236
|
+
this.writeEnd = new PipeEnd(this, 'write', listeners)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private wakeReaders(): void {
|
|
240
|
+
const w = this.readWaiters
|
|
241
|
+
this.readWaiters = []
|
|
242
|
+
for (const f of w) f()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private wakeWriters(): void {
|
|
246
|
+
const w = this.writeWaiters
|
|
247
|
+
this.writeWaiters = []
|
|
248
|
+
for (const f of w) f()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async write(chunk: Uint8Array): Promise<void> {
|
|
252
|
+
while (this.readersOpen && this.buffered >= this.capacity) {
|
|
253
|
+
await new Promise<void>((r) => this.writeWaiters.push(r))
|
|
254
|
+
}
|
|
255
|
+
if (!this.readersOpen) throw new BrokenPipeError()
|
|
256
|
+
this.chunks.push(chunk)
|
|
257
|
+
this.buffered += chunk.length
|
|
258
|
+
this.wakeReaders()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async read(): Promise<Uint8Array | null> {
|
|
262
|
+
while (this.chunks.length === 0 && this.writersOpen) {
|
|
263
|
+
await new Promise<void>((r) => this.readWaiters.push(r))
|
|
264
|
+
}
|
|
265
|
+
const chunk = this.chunks.shift()
|
|
266
|
+
if (chunk === undefined) return null
|
|
267
|
+
this.buffered -= chunk.length
|
|
268
|
+
this.wakeWriters()
|
|
269
|
+
return chunk
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
closeWriters(): void {
|
|
273
|
+
this.writersOpen = false
|
|
274
|
+
this.wakeReaders()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
closeReaders(): void {
|
|
278
|
+
this.readersOpen = false
|
|
279
|
+
this.chunks = []
|
|
280
|
+
this.buffered = 0
|
|
281
|
+
this.wakeWriters()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export class PipeEnd extends OpenFile {
|
|
286
|
+
private readonly pipe: Pipe
|
|
287
|
+
|
|
288
|
+
constructor(pipe: Pipe, end: 'read' | 'write', listeners: DataListener[]) {
|
|
289
|
+
super(pipe.channel, end === 'read', end === 'write', listeners)
|
|
290
|
+
this.pipe = pipe
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
protected rawRead(): Promise<Uint8Array | null> {
|
|
294
|
+
return this.pipe.read()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
protected rawWrite(chunk: Uint8Array): Promise<void> {
|
|
298
|
+
return this.pipe.write(chunk)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
protected onLastClose(): void {
|
|
302
|
+
if (this.writable) this.pipe.closeWriters()
|
|
303
|
+
else this.pipe.closeReaders()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Files
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
export type FileMode = 'read' | 'write' | 'append' | 'readwrite'
|
|
312
|
+
|
|
313
|
+
/** A real file opened on the host. */
|
|
314
|
+
export class HostFile extends OpenFile {
|
|
315
|
+
readonly fd: number
|
|
316
|
+
private eof = false
|
|
317
|
+
|
|
318
|
+
private constructor(channel: Channel, fd: number, mode: FileMode, listeners: DataListener[]) {
|
|
319
|
+
super(channel, mode === 'read' || mode === 'readwrite', mode !== 'read', listeners)
|
|
320
|
+
this.fd = fd
|
|
321
|
+
this.traceReads = mode === 'read'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
static async open(channel: Channel, path: string, mode: FileMode, listeners: DataListener[], noclobber = false, umask?: number): Promise<HostFile> {
|
|
325
|
+
const existed = mode !== 'read' && umask !== undefined && fs.existsSync(path)
|
|
326
|
+
const flags = mode === 'read' ? 'r' : mode === 'write' ? (noclobber ? 'wx' : 'w') : mode === 'append' ? 'a' : 'r+'
|
|
327
|
+
let fd: number
|
|
328
|
+
try {
|
|
329
|
+
fd = await new Promise<number>((resolve, reject) => fs.open(path, mode === 'readwrite' && !fs.existsSync(path) ? 'w+' : flags, 0o666, (err, f) => (err ? reject(err) : resolve(f))))
|
|
330
|
+
} catch (e) {
|
|
331
|
+
throw describeFsError(e, path)
|
|
332
|
+
}
|
|
333
|
+
if (mode !== 'read' && umask !== undefined && !existed) {
|
|
334
|
+
try {
|
|
335
|
+
fs.fchmodSync(fd, 0o666 & ~umask)
|
|
336
|
+
} catch {
|
|
337
|
+
// ignore
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return new HostFile(channel, fd, mode, listeners)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected rawRead(): Promise<Uint8Array | null> {
|
|
344
|
+
if (this.eof) return Promise.resolve(null)
|
|
345
|
+
return new Promise((resolve, reject) => {
|
|
346
|
+
const buf = Buffer.allocUnsafe(64 * 1024)
|
|
347
|
+
fs.read(this.fd, buf, 0, buf.length, null, (err, n) => {
|
|
348
|
+
if (err) return reject(err)
|
|
349
|
+
if (n === 0) {
|
|
350
|
+
this.eof = true
|
|
351
|
+
return resolve(null)
|
|
352
|
+
}
|
|
353
|
+
resolve(new Uint8Array(buf.buffer, buf.byteOffset, n).slice())
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected rawWrite(chunk: Uint8Array): Promise<void> {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
fs.write(this.fd, chunk, 0, chunk.length, null, (err) => (err ? reject(err) : resolve()))
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
protected onLastClose(): void {
|
|
365
|
+
fs.close(this.fd, () => {})
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export class FsError extends Error {
|
|
370
|
+
readonly code: string
|
|
371
|
+
constructor(message: string, code: string) {
|
|
372
|
+
super(message)
|
|
373
|
+
this.code = code
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function describeFsError(e: unknown, path: string): FsError {
|
|
378
|
+
const code = (e as NodeJS.ErrnoException).code ?? 'EIO'
|
|
379
|
+
const text: Record<string, string> = {
|
|
380
|
+
ENOENT: 'No such file or directory',
|
|
381
|
+
EACCES: 'Permission denied',
|
|
382
|
+
EPERM: 'Operation not permitted',
|
|
383
|
+
EISDIR: 'Is a directory',
|
|
384
|
+
ENOTDIR: 'Not a directory',
|
|
385
|
+
EEXIST: 'cannot overwrite existing file',
|
|
386
|
+
EROFS: 'Read-only file system',
|
|
387
|
+
}
|
|
388
|
+
return new FsError(`${path}: ${text[code] ?? code}`, code)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Captures, null, constant input, host streams
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
/** Collects written bytes (command substitution, `run()` output capture). */
|
|
396
|
+
export class Capture extends OpenFile {
|
|
397
|
+
private parts: Uint8Array[] = []
|
|
398
|
+
size = 0
|
|
399
|
+
/** Bytes beyond this are discarded (but counted). */
|
|
400
|
+
readonly limit: number
|
|
401
|
+
truncated = false
|
|
402
|
+
|
|
403
|
+
constructor(channel: Channel, listeners: DataListener[], limit = 64 * 1024 * 1024) {
|
|
404
|
+
super(channel, false, true, listeners)
|
|
405
|
+
this.limit = limit
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protected rawRead(): Promise<Uint8Array | null> {
|
|
409
|
+
return Promise.resolve(null)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
protected async rawWrite(chunk: Uint8Array): Promise<void> {
|
|
413
|
+
if (this.size + chunk.length > this.limit) {
|
|
414
|
+
this.truncated = true
|
|
415
|
+
const room = Math.max(0, this.limit - this.size)
|
|
416
|
+
if (room > 0) this.parts.push(chunk.slice(0, room))
|
|
417
|
+
this.size += chunk.length
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
this.parts.push(chunk)
|
|
421
|
+
this.size += chunk.length
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected onLastClose(): void {}
|
|
425
|
+
|
|
426
|
+
bytes(): Uint8Array {
|
|
427
|
+
let n = 0
|
|
428
|
+
for (const p of this.parts) n += p.length
|
|
429
|
+
const out = new Uint8Array(n)
|
|
430
|
+
let o = 0
|
|
431
|
+
for (const p of this.parts) {
|
|
432
|
+
out.set(p, o)
|
|
433
|
+
o += p.length
|
|
434
|
+
}
|
|
435
|
+
return out
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
text(): string {
|
|
439
|
+
return new TextDecoder().decode(this.bytes())
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export class NullFile extends OpenFile {
|
|
444
|
+
constructor(channel: Channel, listeners: DataListener[]) {
|
|
445
|
+
super(channel, true, true, listeners)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
protected rawRead(): Promise<Uint8Array | null> {
|
|
449
|
+
return Promise.resolve(null)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
protected rawWrite(): Promise<void> {
|
|
453
|
+
return Promise.resolve()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
protected onLastClose(): void {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Fixed input (here-docs, here-strings). */
|
|
460
|
+
export class ConstInput extends OpenFile {
|
|
461
|
+
private data: Uint8Array | null
|
|
462
|
+
|
|
463
|
+
constructor(channel: Channel, data: string | Uint8Array, listeners: DataListener[]) {
|
|
464
|
+
super(channel, true, false, listeners)
|
|
465
|
+
this.data = toBytes(data)
|
|
466
|
+
this.traceReads = true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
protected rawRead(): Promise<Uint8Array | null> {
|
|
470
|
+
const d = this.data
|
|
471
|
+
this.data = null
|
|
472
|
+
return Promise.resolve(d && d.length > 0 ? d : null)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
protected rawWrite(): Promise<void> {
|
|
476
|
+
return Promise.reject(new Error('not writable'))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
protected onLastClose(): void {}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Adapts a host Node stream (or callback) as the VM's stdin/stdout/stderr. */
|
|
483
|
+
export class HostStream extends OpenFile {
|
|
484
|
+
private readonly onWrite?: (chunk: Uint8Array) => void | Promise<void>
|
|
485
|
+
private readonly iterator?: AsyncIterator<Uint8Array | string>
|
|
486
|
+
|
|
487
|
+
constructor(
|
|
488
|
+
channel: Channel,
|
|
489
|
+
listeners: DataListener[],
|
|
490
|
+
opts: { write?: (chunk: Uint8Array) => void | Promise<void>; input?: AsyncIterable<Uint8Array | string> },
|
|
491
|
+
) {
|
|
492
|
+
super(channel, opts.input !== undefined, opts.write !== undefined, listeners)
|
|
493
|
+
this.onWrite = opts.write
|
|
494
|
+
this.iterator = opts.input?.[Symbol.asyncIterator]()
|
|
495
|
+
this.traceReads = opts.input !== undefined
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
protected async rawRead(): Promise<Uint8Array | null> {
|
|
499
|
+
if (!this.iterator) return null
|
|
500
|
+
const r = await this.iterator.next()
|
|
501
|
+
if (r.done) return null
|
|
502
|
+
return toBytes(r.value)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
protected async rawWrite(chunk: Uint8Array): Promise<void> {
|
|
506
|
+
await this.onWrite?.(chunk)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
protected onLastClose(): void {}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Fd tables
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
/** One shell process's file descriptor table. */
|
|
517
|
+
export class FdTable {
|
|
518
|
+
private readonly fds = new Map<number, OpenFile>()
|
|
519
|
+
|
|
520
|
+
get(fd: number): OpenFile | undefined {
|
|
521
|
+
return this.fds.get(fd)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Install `file` at `fd` (taking ownership of one reference), closing what was there. */
|
|
525
|
+
set(fd: number, file: OpenFile): void {
|
|
526
|
+
const old = this.fds.get(fd)
|
|
527
|
+
this.fds.set(fd, file)
|
|
528
|
+
old?.release()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
close(fd: number): void {
|
|
532
|
+
const old = this.fds.get(fd)
|
|
533
|
+
if (old) {
|
|
534
|
+
this.fds.delete(fd)
|
|
535
|
+
old.release()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** `n>&m` */
|
|
540
|
+
dup(from: number, to: number): boolean {
|
|
541
|
+
const f = this.fds.get(from)
|
|
542
|
+
if (!f) return false
|
|
543
|
+
if (from === to) return true
|
|
544
|
+
this.set(to, f.retain())
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** A copy sharing every open file (each gains a reference). */
|
|
549
|
+
clone(): FdTable {
|
|
550
|
+
const t = new FdTable()
|
|
551
|
+
for (const [fd, f] of this.fds) t.fds.set(fd, f.retain())
|
|
552
|
+
return t
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
closeAll(): void {
|
|
556
|
+
for (const f of this.fds.values()) f.release()
|
|
557
|
+
this.fds.clear()
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
entries(): [number, OpenFile][] {
|
|
561
|
+
return [...this.fds.entries()]
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Lowest free fd ≥ `min` (for `{var}>file`, bash starts at 10). */
|
|
565
|
+
freeFd(min = 10): number {
|
|
566
|
+
let n = min
|
|
567
|
+
while (this.fds.has(n)) n++
|
|
568
|
+
return n
|
|
569
|
+
}
|
|
570
|
+
}
|