sandboxedjs 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 +27 -0
- package/README.md +353 -0
- package/bin/sandboxedjs.mjs +286 -0
- package/dist/index.cjs +19388 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1958 -0
- package/dist/index.d.ts +1958 -0
- package/dist/index.js +19318 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1958 @@
|
|
|
1
|
+
import { MemoryVolume, Nodepod } from '@scelar/nodepod/headless';
|
|
2
|
+
|
|
3
|
+
type FileKind = "file" | "directory" | "symlink" | "chardev" | "blockdev" | "fifo" | "socket";
|
|
4
|
+
/** Render as `drwxr-xr-x`, honouring setuid/setgid/sticky. */
|
|
5
|
+
declare function formatMode(mode: number): string;
|
|
6
|
+
/** Zero-padded octal permissions, as `stat -c %a`/`%04a` would show. */
|
|
7
|
+
declare function octalMode(mode: number, width?: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Apply a `chmod` spec to an existing mode. Accepts octal (`755`, `0644`) and
|
|
10
|
+
* the symbolic grammar (`u+rwx,go-w`, `a=r`, `+X`, `u+s`, `o+t`).
|
|
11
|
+
*
|
|
12
|
+
* @param isDir whether the target is a directory — needed for the `X` flag.
|
|
13
|
+
*/
|
|
14
|
+
declare function applyChmod(spec: string, current: number, isDir: boolean, umask?: number): number;
|
|
15
|
+
/** Parse the `umask` builtin's argument. */
|
|
16
|
+
declare function parseUmask(spec: string): number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The `Stats` object handed back by `Vfs.stat`. Shaped like `fs.Stats` so it
|
|
20
|
+
* feels familiar, but with a real `st_mode` that carries the file-type bits
|
|
21
|
+
* (which the underlying volume stores separately).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface StatInit {
|
|
25
|
+
mode: number;
|
|
26
|
+
size: number;
|
|
27
|
+
uid: number;
|
|
28
|
+
gid: number;
|
|
29
|
+
ino: number;
|
|
30
|
+
nlink: number;
|
|
31
|
+
atimeMs: number;
|
|
32
|
+
mtimeMs: number;
|
|
33
|
+
ctimeMs: number;
|
|
34
|
+
birthtimeMs?: number;
|
|
35
|
+
dev?: number;
|
|
36
|
+
rdev?: number;
|
|
37
|
+
blksize?: number;
|
|
38
|
+
}
|
|
39
|
+
declare class Stats {
|
|
40
|
+
readonly mode: number;
|
|
41
|
+
readonly size: number;
|
|
42
|
+
readonly uid: number;
|
|
43
|
+
readonly gid: number;
|
|
44
|
+
readonly ino: number;
|
|
45
|
+
readonly nlink: number;
|
|
46
|
+
readonly dev: number;
|
|
47
|
+
readonly rdev: number;
|
|
48
|
+
readonly blksize: number;
|
|
49
|
+
readonly atimeMs: number;
|
|
50
|
+
readonly mtimeMs: number;
|
|
51
|
+
readonly ctimeMs: number;
|
|
52
|
+
readonly birthtimeMs: number;
|
|
53
|
+
constructor(init: StatInit);
|
|
54
|
+
get blocks(): number;
|
|
55
|
+
get atime(): Date;
|
|
56
|
+
get mtime(): Date;
|
|
57
|
+
get ctime(): Date;
|
|
58
|
+
get birthtime(): Date;
|
|
59
|
+
get kind(): FileKind;
|
|
60
|
+
isFile(): boolean;
|
|
61
|
+
isDirectory(): boolean;
|
|
62
|
+
isSymbolicLink(): boolean;
|
|
63
|
+
isCharacterDevice(): boolean;
|
|
64
|
+
isBlockDevice(): boolean;
|
|
65
|
+
isFIFO(): boolean;
|
|
66
|
+
isSocket(): boolean;
|
|
67
|
+
/** Permission bits only, with the type bits masked off. */
|
|
68
|
+
get perms(): number;
|
|
69
|
+
}
|
|
70
|
+
interface DirEntry {
|
|
71
|
+
name: string;
|
|
72
|
+
kind: FileKind;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The container's virtual filesystem.
|
|
77
|
+
*
|
|
78
|
+
* Real file content lives in a Nodepod `MemoryVolume`, which is deliberately the
|
|
79
|
+
* *same* volume the Node.js worker processes see — so a file written by `echo`
|
|
80
|
+
* is readable by `require('fs')` inside a spawned script, and vice versa.
|
|
81
|
+
*
|
|
82
|
+
* On top of that volume this layer adds the parts a Linux userland expects and
|
|
83
|
+
* the raw volume does not have: file-type bits in `st_mode`, permission and
|
|
84
|
+
* ownership checks, an `O_*` open/fd table, and pluggable *virtual providers*
|
|
85
|
+
* that synthesise `/proc`, `/sys` and `/dev` on demand.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/** The identity a filesystem operation runs as. */
|
|
89
|
+
interface Cred {
|
|
90
|
+
uid: number;
|
|
91
|
+
gid: number;
|
|
92
|
+
groups: number[];
|
|
93
|
+
umask: number;
|
|
94
|
+
}
|
|
95
|
+
declare const ROOT_CRED: Cred;
|
|
96
|
+
declare function makeCred(uid: number, gid: number, groups?: number[], umask?: number): Cred;
|
|
97
|
+
/** A file that does not live in the volume — `/proc/uptime`, `/dev/null`, … */
|
|
98
|
+
interface VirtualNode {
|
|
99
|
+
kind: FileKind;
|
|
100
|
+
/** Permission bits only; the type bits are added from `kind`. */
|
|
101
|
+
mode: number;
|
|
102
|
+
uid?: number;
|
|
103
|
+
gid?: number;
|
|
104
|
+
size?: number;
|
|
105
|
+
mtimeMs?: number;
|
|
106
|
+
/** Symlink target, when `kind === "symlink"`. */
|
|
107
|
+
target?: string;
|
|
108
|
+
read?(): Uint8Array | string;
|
|
109
|
+
write?(data: Uint8Array, append: boolean): void;
|
|
110
|
+
/** Directory listing, when `kind === "directory"`. */
|
|
111
|
+
list?(): string[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Supplies a subtree of synthetic files. `resolve` receives the path *relative*
|
|
115
|
+
* to `root` ("" means the mount point itself) and returns null for misses.
|
|
116
|
+
*/
|
|
117
|
+
interface VirtualProvider {
|
|
118
|
+
root: string;
|
|
119
|
+
resolve(rel: string): VirtualNode | null;
|
|
120
|
+
/**
|
|
121
|
+
* When true (the default) the provider owns its whole subtree and a miss is
|
|
122
|
+
* `ENOENT` — that is what `/proc` wants, so a dead pid does not resolve to a
|
|
123
|
+
* stale on-disk file. `/dev` and `/sys` set this to false so that synthetic
|
|
124
|
+
* nodes overlay a real directory users can still write into.
|
|
125
|
+
*/
|
|
126
|
+
exclusive?: boolean;
|
|
127
|
+
}
|
|
128
|
+
interface WriteOptions {
|
|
129
|
+
mode?: number;
|
|
130
|
+
append?: boolean;
|
|
131
|
+
cred?: Cred;
|
|
132
|
+
/** Skip the permission check — used by kernel-internal writes. */
|
|
133
|
+
privileged?: boolean;
|
|
134
|
+
}
|
|
135
|
+
interface ResolveOptions {
|
|
136
|
+
cred?: Cred;
|
|
137
|
+
/** Follow a symlink in the final position. Off for `lstat`, `rm`, `chmod -h`. */
|
|
138
|
+
followFinal?: boolean;
|
|
139
|
+
}
|
|
140
|
+
declare class Vfs {
|
|
141
|
+
readonly volume: MemoryVolume;
|
|
142
|
+
private readonly providers;
|
|
143
|
+
private nextVirtualIno;
|
|
144
|
+
private readonly virtualInos;
|
|
145
|
+
constructor(volume: MemoryVolume);
|
|
146
|
+
addProvider(provider: VirtualProvider): void;
|
|
147
|
+
removeProvider(root: string): void;
|
|
148
|
+
/** The mount points currently served synthetically. */
|
|
149
|
+
get virtualRoots(): string[];
|
|
150
|
+
private lookupVirtual;
|
|
151
|
+
/** True when a miss at `abs` must be ENOENT rather than a volume lookup. */
|
|
152
|
+
private isUnderProvider;
|
|
153
|
+
/** Synthetic children a non-exclusive provider contributes to a directory. */
|
|
154
|
+
private virtualChildren;
|
|
155
|
+
private virtualIno;
|
|
156
|
+
/** True when `cred` may perform `mode` (R_OK/W_OK/X_OK) on a stat result. */
|
|
157
|
+
permitted(st: Stats, mode: number, cred: Cred): boolean;
|
|
158
|
+
private require;
|
|
159
|
+
/**
|
|
160
|
+
* Walk `abs` component by component, following symlinks and checking search
|
|
161
|
+
* (`+x`) permission on every directory along the way, exactly like `namei`.
|
|
162
|
+
*
|
|
163
|
+
* Returns the fully resolved absolute path. Does *not* require the final
|
|
164
|
+
* component to exist — callers decide whether a miss is fatal.
|
|
165
|
+
*/
|
|
166
|
+
resolvePath(abs: string, opts?: ResolveOptions): string;
|
|
167
|
+
/** lstat that returns null instead of throwing, for internal probing. */
|
|
168
|
+
private tryLstat;
|
|
169
|
+
private readlinkRaw;
|
|
170
|
+
/** stat(2) — follows symlinks. */
|
|
171
|
+
stat(abs: string, opts?: {
|
|
172
|
+
cred?: Cred;
|
|
173
|
+
}): Stats;
|
|
174
|
+
/** lstat(2) — does not follow a symlink in the final position. */
|
|
175
|
+
lstat(abs: string): Stats;
|
|
176
|
+
private statFromVirtual;
|
|
177
|
+
exists(abs: string, cred?: Cred): boolean;
|
|
178
|
+
lexists(abs: string): boolean;
|
|
179
|
+
access(abs: string, mode?: number, cred?: Cred): void;
|
|
180
|
+
realpath(abs: string, cred?: Cred): string;
|
|
181
|
+
readFile(abs: string, cred?: Cred): Uint8Array;
|
|
182
|
+
readText(abs: string, cred?: Cred): string;
|
|
183
|
+
readdir(abs: string, cred?: Cred): string[];
|
|
184
|
+
/** True when the volume itself has a real directory at `abs`. */
|
|
185
|
+
private volumeHasDir;
|
|
186
|
+
readdirWithTypes(abs: string, cred?: Cred): DirEntry[];
|
|
187
|
+
readlink(abs: string, cred?: Cred): string;
|
|
188
|
+
writeFile(abs: string, data: Uint8Array | string, opts?: WriteOptions): void;
|
|
189
|
+
appendFile(abs: string, data: Uint8Array | string, opts?: WriteOptions): void;
|
|
190
|
+
truncate(abs: string, len?: number, cred?: Cred): void;
|
|
191
|
+
mkdir(abs: string, opts?: {
|
|
192
|
+
mode?: number;
|
|
193
|
+
recursive?: boolean;
|
|
194
|
+
cred?: Cred;
|
|
195
|
+
}): void;
|
|
196
|
+
private mkdirOne;
|
|
197
|
+
rmdir(abs: string, cred?: Cred): void;
|
|
198
|
+
unlink(abs: string, cred?: Cred): void;
|
|
199
|
+
/** Recursive delete, the engine behind `rm -r`. */
|
|
200
|
+
rmrf(abs: string, cred?: Cred): void;
|
|
201
|
+
private requireParentWrite;
|
|
202
|
+
rename(from: string, to: string, cred?: Cred): void;
|
|
203
|
+
copyFile(from: string, to: string, cred?: Cred): void;
|
|
204
|
+
symlink(target: string, linkPath: string, cred?: Cred): void;
|
|
205
|
+
link(existing: string, newPath: string, cred?: Cred): void;
|
|
206
|
+
chmod(abs: string, mode: number, cred?: Cred, follow?: boolean): void;
|
|
207
|
+
chown(abs: string, uid: number, gid: number, cred?: Cred, follow?: boolean): void;
|
|
208
|
+
utimes(abs: string, atimeMs: number, mtimeMs: number, cred?: Cred): void;
|
|
209
|
+
/** `touch` semantics: create when missing, otherwise bump the timestamps. */
|
|
210
|
+
touch(abs: string, cred?: Cred, timeMs?: number): void;
|
|
211
|
+
/** Depth-first walk yielding absolute paths. Symlinks are not followed. */
|
|
212
|
+
walk(abs: string, opts?: {
|
|
213
|
+
includeSelf?: boolean;
|
|
214
|
+
cred?: Cred;
|
|
215
|
+
maxDepth?: number;
|
|
216
|
+
}): Generator<string>;
|
|
217
|
+
/** Recursive copy used by `cp -r` and the container's `copyIn` helper. */
|
|
218
|
+
copyTree(from: string, to: string, cred?: Cred): void;
|
|
219
|
+
/** Free/used byte accounting for `df` and `du`. */
|
|
220
|
+
usage(abs?: string): {
|
|
221
|
+
files: number;
|
|
222
|
+
dirs: number;
|
|
223
|
+
bytes: number;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Byte streams for stdin/stdout/stderr, pipelines and redirections.
|
|
229
|
+
*
|
|
230
|
+
* Everything here is promise-based rather than Node-stream based: commands are
|
|
231
|
+
* plain async functions, a pipeline is just a chain of `Pipe` objects, and a
|
|
232
|
+
* closed reader turns further writes into `EPIPE` the way a real pipe does.
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
interface OutputStream {
|
|
236
|
+
write(data: Uint8Array | string): void;
|
|
237
|
+
end(): void;
|
|
238
|
+
/** True once the far end went away — producers should stop. */
|
|
239
|
+
readonly closed: boolean;
|
|
240
|
+
isTTY: boolean;
|
|
241
|
+
/** Terminal width, when this is a TTY. */
|
|
242
|
+
columns?: number;
|
|
243
|
+
rows?: number;
|
|
244
|
+
}
|
|
245
|
+
interface InputStream {
|
|
246
|
+
/** Resolves to null at EOF. `size` is a maximum, not a guarantee. */
|
|
247
|
+
read(size?: number): Promise<Uint8Array | null>;
|
|
248
|
+
readAll(): Promise<Uint8Array>;
|
|
249
|
+
/** One line without its terminator; null at EOF. */
|
|
250
|
+
readLine(): Promise<string | null>;
|
|
251
|
+
/** Bytes already buffered, for non-blocking peeks. */
|
|
252
|
+
readonly available: number;
|
|
253
|
+
close(): void;
|
|
254
|
+
isTTY: boolean;
|
|
255
|
+
/**
|
|
256
|
+
* True when the far end is a live caller who may never signal EOF — a
|
|
257
|
+
* terminal, or the `stdin` pipe of `Container.spawn`. Programs that would
|
|
258
|
+
* otherwise slurp stdin before starting must not block on these.
|
|
259
|
+
*/
|
|
260
|
+
readonly interactive?: boolean;
|
|
261
|
+
}
|
|
262
|
+
interface Stdio {
|
|
263
|
+
stdin: InputStream;
|
|
264
|
+
stdout: OutputStream;
|
|
265
|
+
stderr: OutputStream;
|
|
266
|
+
}
|
|
267
|
+
/** An in-memory pipe: writable on one end, readable on the other. */
|
|
268
|
+
declare class Pipe implements InputStream, OutputStream {
|
|
269
|
+
private chunks;
|
|
270
|
+
private buffered;
|
|
271
|
+
private writerClosed;
|
|
272
|
+
private readerClosed;
|
|
273
|
+
private wakers;
|
|
274
|
+
isTTY: boolean;
|
|
275
|
+
/** Set on pipes owned by an outside caller, who may never call `end()`. */
|
|
276
|
+
interactive: boolean;
|
|
277
|
+
columns: number | undefined;
|
|
278
|
+
rows: number | undefined;
|
|
279
|
+
get closed(): boolean;
|
|
280
|
+
get ended(): boolean;
|
|
281
|
+
get available(): number;
|
|
282
|
+
write(data: Uint8Array | string): void;
|
|
283
|
+
end(): void;
|
|
284
|
+
close(): void;
|
|
285
|
+
private wake;
|
|
286
|
+
private waitForData;
|
|
287
|
+
read(size?: number): Promise<Uint8Array | null>;
|
|
288
|
+
readAll(): Promise<Uint8Array>;
|
|
289
|
+
private lineRemainder;
|
|
290
|
+
readLine(): Promise<string | null>;
|
|
291
|
+
/** Seed the pipe with content then close it — handy for here-docs. */
|
|
292
|
+
static from(data: Uint8Array | string): Pipe;
|
|
293
|
+
static empty(): Pipe;
|
|
294
|
+
}
|
|
295
|
+
/** Discards everything; `/dev/null` as an output stream. */
|
|
296
|
+
declare class NullOutput implements OutputStream {
|
|
297
|
+
readonly closed = false;
|
|
298
|
+
isTTY: boolean;
|
|
299
|
+
write(): void;
|
|
300
|
+
end(): void;
|
|
301
|
+
}
|
|
302
|
+
/** Always at EOF; `/dev/null` as an input stream. */
|
|
303
|
+
declare class NullInput implements InputStream {
|
|
304
|
+
readonly available = 0;
|
|
305
|
+
isTTY: boolean;
|
|
306
|
+
read(): Promise<null>;
|
|
307
|
+
readAll(): Promise<Uint8Array>;
|
|
308
|
+
readLine(): Promise<null>;
|
|
309
|
+
close(): void;
|
|
310
|
+
}
|
|
311
|
+
/** Collects everything written, for `exec()` and command substitution. */
|
|
312
|
+
declare class BufferSink implements OutputStream {
|
|
313
|
+
private readonly onWrite?;
|
|
314
|
+
private chunks;
|
|
315
|
+
private total;
|
|
316
|
+
private _closed;
|
|
317
|
+
isTTY: boolean;
|
|
318
|
+
columns: number | undefined;
|
|
319
|
+
rows: number | undefined;
|
|
320
|
+
constructor(onWrite?: ((chunk: Uint8Array) => void) | undefined);
|
|
321
|
+
get closed(): boolean;
|
|
322
|
+
write(data: Uint8Array | string): void;
|
|
323
|
+
end(): void;
|
|
324
|
+
bytes(): Uint8Array;
|
|
325
|
+
text(): string;
|
|
326
|
+
get length(): number;
|
|
327
|
+
reset(): void;
|
|
328
|
+
}
|
|
329
|
+
/** Forwards each write to a callback — used to stream into a terminal. */
|
|
330
|
+
declare class CallbackSink implements OutputStream {
|
|
331
|
+
private readonly sink;
|
|
332
|
+
private _closed;
|
|
333
|
+
isTTY: boolean;
|
|
334
|
+
columns: number | undefined;
|
|
335
|
+
rows: number | undefined;
|
|
336
|
+
constructor(sink: (text: string) => void, opts?: {
|
|
337
|
+
isTTY?: boolean;
|
|
338
|
+
columns?: number;
|
|
339
|
+
rows?: number;
|
|
340
|
+
});
|
|
341
|
+
get closed(): boolean;
|
|
342
|
+
write(data: Uint8Array | string): void;
|
|
343
|
+
end(): void;
|
|
344
|
+
}
|
|
345
|
+
/** Fan-out, for `tee` and for `2>&1`-style duplication. */
|
|
346
|
+
declare class TeeOutput implements OutputStream {
|
|
347
|
+
private readonly targets;
|
|
348
|
+
constructor(targets: OutputStream[]);
|
|
349
|
+
get closed(): boolean;
|
|
350
|
+
isTTY: boolean;
|
|
351
|
+
write(data: Uint8Array | string): void;
|
|
352
|
+
end(): void;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Writes into a VFS file. Buffers in memory and flushes on every write so a
|
|
356
|
+
* long-running redirect (`cmd > log &`) is observable while it runs.
|
|
357
|
+
*/
|
|
358
|
+
declare class FileOutput implements OutputStream {
|
|
359
|
+
private readonly vfs;
|
|
360
|
+
private readonly path;
|
|
361
|
+
private readonly opts;
|
|
362
|
+
private _closed;
|
|
363
|
+
isTTY: boolean;
|
|
364
|
+
constructor(vfs: Vfs, path: string, opts?: {
|
|
365
|
+
append?: boolean;
|
|
366
|
+
cred?: Cred;
|
|
367
|
+
mode?: number;
|
|
368
|
+
});
|
|
369
|
+
get closed(): boolean;
|
|
370
|
+
write(data: Uint8Array | string): void;
|
|
371
|
+
end(): void;
|
|
372
|
+
}
|
|
373
|
+
/** Reads a VFS file as an input stream, for `cmd < file`. */
|
|
374
|
+
declare class FileInput implements InputStream {
|
|
375
|
+
private pipe;
|
|
376
|
+
isTTY: boolean;
|
|
377
|
+
constructor(vfs: Vfs, path: string, cred?: Cred);
|
|
378
|
+
get available(): number;
|
|
379
|
+
read(size?: number): Promise<Uint8Array | null>;
|
|
380
|
+
readAll(): Promise<Uint8Array>;
|
|
381
|
+
readLine(): Promise<string | null>;
|
|
382
|
+
close(): void;
|
|
383
|
+
}
|
|
384
|
+
/** Convenience factory for a fully buffered stdio triple. */
|
|
385
|
+
declare function captureStdio(stdin?: Uint8Array | string): {
|
|
386
|
+
stdio: Stdio;
|
|
387
|
+
out: BufferSink;
|
|
388
|
+
err: BufferSink;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The process table.
|
|
393
|
+
*
|
|
394
|
+
* Processes here are cooperative JavaScript tasks, not OS processes, but they
|
|
395
|
+
* carry the state Linux tooling expects to see: pid/ppid/pgid/sid, a state
|
|
396
|
+
* letter, credentials, a cwd, an environment, and an exit status that encodes
|
|
397
|
+
* the killing signal. `ps`, `kill`, `jobs` and `/proc` all read from here.
|
|
398
|
+
*/
|
|
399
|
+
|
|
400
|
+
type ProcessState = "R" | "S" | "D" | "T" | "Z" | "X";
|
|
401
|
+
type Env = Record<string, string>;
|
|
402
|
+
interface ProcessOptions {
|
|
403
|
+
argv: string[];
|
|
404
|
+
cwd: string;
|
|
405
|
+
env: Env;
|
|
406
|
+
cred: Cred;
|
|
407
|
+
ppid?: number;
|
|
408
|
+
pgid?: number;
|
|
409
|
+
sid?: number;
|
|
410
|
+
stdio?: Partial<Stdio>;
|
|
411
|
+
tty?: string | null;
|
|
412
|
+
/** Marks the process as a shell builtin frame rather than a real command. */
|
|
413
|
+
kind?: ProcessKind;
|
|
414
|
+
}
|
|
415
|
+
type ProcessKind = "init" | "shell" | "builtin" | "command" | "node" | "python" | "script";
|
|
416
|
+
declare class Process {
|
|
417
|
+
readonly pid: number;
|
|
418
|
+
ppid: number;
|
|
419
|
+
pgid: number;
|
|
420
|
+
sid: number;
|
|
421
|
+
argv: string[];
|
|
422
|
+
cwd: string;
|
|
423
|
+
env: Env;
|
|
424
|
+
cred: Cred;
|
|
425
|
+
kind: ProcessKind;
|
|
426
|
+
tty: string | null;
|
|
427
|
+
state: ProcessState;
|
|
428
|
+
exitCode: number | null;
|
|
429
|
+
/** Signal that terminated the process, if any. */
|
|
430
|
+
termSignal: string | null;
|
|
431
|
+
readonly startTime: number;
|
|
432
|
+
cpuMs: number;
|
|
433
|
+
stdin: InputStream;
|
|
434
|
+
stdout: OutputStream;
|
|
435
|
+
stderr: OutputStream;
|
|
436
|
+
readonly children: Set<Process>;
|
|
437
|
+
private readonly aborter;
|
|
438
|
+
private readonly exitWaiters;
|
|
439
|
+
private readonly signalHandlers;
|
|
440
|
+
constructor(opts: ProcessOptions);
|
|
441
|
+
/** Basename of argv[0], the `comm` field in `ps`. */
|
|
442
|
+
get comm(): string;
|
|
443
|
+
get cmdline(): string;
|
|
444
|
+
get running(): boolean;
|
|
445
|
+
get signal(): AbortSignal;
|
|
446
|
+
/** Seconds since the process started, for `ps etime`. */
|
|
447
|
+
get elapsedMs(): number;
|
|
448
|
+
setStdio(stdio: Partial<Stdio>): void;
|
|
449
|
+
onSignal(sig: string, handler: "default" | "ignore" | ((sig: string) => void)): void;
|
|
450
|
+
/**
|
|
451
|
+
* Deliver a signal. Returns true when the process handled or died from it.
|
|
452
|
+
* Catchable signals run the installed handler; uncatchable ones always kill.
|
|
453
|
+
*/
|
|
454
|
+
deliver(sigSpec: string | number): boolean;
|
|
455
|
+
exit(code: number): void;
|
|
456
|
+
/** Remove from the table entirely — the parent has reaped us. */
|
|
457
|
+
reap(): void;
|
|
458
|
+
wait(): Promise<number>;
|
|
459
|
+
}
|
|
460
|
+
interface ProcessFilter {
|
|
461
|
+
pid?: number;
|
|
462
|
+
pgid?: number;
|
|
463
|
+
uid?: number;
|
|
464
|
+
comm?: string;
|
|
465
|
+
includeDead?: boolean;
|
|
466
|
+
}
|
|
467
|
+
declare class ProcessTable {
|
|
468
|
+
private readonly map;
|
|
469
|
+
create(opts: ProcessOptions): Process;
|
|
470
|
+
get(pid: number): Process | undefined;
|
|
471
|
+
has(pid: number): boolean;
|
|
472
|
+
remove(pid: number): void;
|
|
473
|
+
list(filter?: ProcessFilter): Process[];
|
|
474
|
+
get size(): number;
|
|
475
|
+
/** Send a signal to a pid, a process group (negative pid), or everything (-1). */
|
|
476
|
+
signal(target: number, sig: string | number): number;
|
|
477
|
+
/** Drop finished processes so the table does not grow without bound. */
|
|
478
|
+
gc(keepMs?: number): void;
|
|
479
|
+
clear(): void;
|
|
480
|
+
}
|
|
481
|
+
/** Reset the pid counter — used by tests and by `Container.reset()`. */
|
|
482
|
+
declare function resetPidCounter(start?: number): void;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The user and group database, backed by real `/etc/passwd`, `/etc/group` and
|
|
486
|
+
* `/etc/shadow` files so `cat /etc/passwd` and `id` agree with each other and
|
|
487
|
+
* `useradd` is just a file edit.
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
interface PasswdEntry {
|
|
491
|
+
name: string;
|
|
492
|
+
passwd: string;
|
|
493
|
+
uid: number;
|
|
494
|
+
gid: number;
|
|
495
|
+
gecos: string;
|
|
496
|
+
home: string;
|
|
497
|
+
shell: string;
|
|
498
|
+
}
|
|
499
|
+
interface GroupEntry {
|
|
500
|
+
name: string;
|
|
501
|
+
passwd: string;
|
|
502
|
+
gid: number;
|
|
503
|
+
members: string[];
|
|
504
|
+
}
|
|
505
|
+
declare class UserDatabase {
|
|
506
|
+
private readonly vfs;
|
|
507
|
+
constructor(vfs: Vfs);
|
|
508
|
+
private readLines;
|
|
509
|
+
users(): PasswdEntry[];
|
|
510
|
+
groups(): GroupEntry[];
|
|
511
|
+
userByName(name: string): PasswdEntry | undefined;
|
|
512
|
+
userByUid(uid: number): PasswdEntry | undefined;
|
|
513
|
+
groupByName(name: string): GroupEntry | undefined;
|
|
514
|
+
groupByGid(gid: number): GroupEntry | undefined;
|
|
515
|
+
/** Accepts a name or a numeric id, the way `chown` arguments do. */
|
|
516
|
+
resolveUid(spec: string): number | undefined;
|
|
517
|
+
resolveGid(spec: string): number | undefined;
|
|
518
|
+
nameForUid(uid: number): string;
|
|
519
|
+
nameForGid(gid: number): string;
|
|
520
|
+
/** Every group id a user belongs to, primary first. */
|
|
521
|
+
groupsFor(name: string): number[];
|
|
522
|
+
credFor(nameOrUid: string | number, umask?: number): Cred;
|
|
523
|
+
nextFreeUid(min?: number, max?: number): number;
|
|
524
|
+
nextFreeGid(min?: number, max?: number): number;
|
|
525
|
+
addUser(entry: PasswdEntry, opts?: {
|
|
526
|
+
createHome?: boolean;
|
|
527
|
+
password?: string;
|
|
528
|
+
}): void;
|
|
529
|
+
addGroup(entry: GroupEntry): void;
|
|
530
|
+
removeUser(name: string): boolean;
|
|
531
|
+
removeGroup(name: string): boolean;
|
|
532
|
+
/** Add `user` to the supplementary members of `group`. */
|
|
533
|
+
addUserToGroup(user: string, group: string): boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The contract every in-container program implements.
|
|
538
|
+
*
|
|
539
|
+
* A "binary" is an async function over an `ExecContext`, returning an exit
|
|
540
|
+
* code. The shell, the coreutils and the language runtimes all speak this one
|
|
541
|
+
* interface, which is what lets `node`, `python3` and `grep` sit side by side
|
|
542
|
+
* in `$PATH` and be pipelined together.
|
|
543
|
+
*/
|
|
544
|
+
|
|
545
|
+
interface ExecContext {
|
|
546
|
+
/** argv[0] is the command name as invoked. */
|
|
547
|
+
readonly argv: string[];
|
|
548
|
+
readonly stdin: InputStream;
|
|
549
|
+
readonly stdout: OutputStream;
|
|
550
|
+
readonly stderr: OutputStream;
|
|
551
|
+
readonly env: Env;
|
|
552
|
+
readonly cwd: string;
|
|
553
|
+
readonly vfs: Vfs;
|
|
554
|
+
readonly kernel: Kernel;
|
|
555
|
+
readonly proc: Process;
|
|
556
|
+
readonly cred: Cred;
|
|
557
|
+
readonly signal: AbortSignal;
|
|
558
|
+
/** Command name, for error prefixes. */
|
|
559
|
+
readonly name: string;
|
|
560
|
+
/** argv without argv[0]. */
|
|
561
|
+
readonly args: string[];
|
|
562
|
+
/** Resolve a possibly relative path against the process cwd. */
|
|
563
|
+
path(p: string): string;
|
|
564
|
+
/** Write to stdout verbatim. */
|
|
565
|
+
write(text: string | Uint8Array): void;
|
|
566
|
+
/** Write a line to stdout. */
|
|
567
|
+
line(text?: string): void;
|
|
568
|
+
/** Write `name: message` to stderr, followed by a newline. */
|
|
569
|
+
warn(message: string): void;
|
|
570
|
+
/** Report an error and produce an exit code in one expression. */
|
|
571
|
+
fail(message: string, code?: number): number;
|
|
572
|
+
/** Turn a caught filesystem error into the message coreutils would print. */
|
|
573
|
+
reportError(e: unknown, subject?: string): number;
|
|
574
|
+
/** Change the calling process's directory (used by `cd`, `chroot`). */
|
|
575
|
+
chdir(dir: string): void;
|
|
576
|
+
}
|
|
577
|
+
interface Command {
|
|
578
|
+
readonly name: string;
|
|
579
|
+
/** One-line summary shown by `help` and `whatis`. */
|
|
580
|
+
readonly summary?: string;
|
|
581
|
+
/** Usage string shown on `--help` and on a `UsageError`. */
|
|
582
|
+
readonly usage?: string;
|
|
583
|
+
/** Longer text shown by `man`. */
|
|
584
|
+
readonly manual?: string;
|
|
585
|
+
/** Aliases registered into the same PATH entry, e.g. `egrep` → `grep`. */
|
|
586
|
+
readonly aliases?: string[];
|
|
587
|
+
/** Where the binary claims to live, for `which` and `type`. */
|
|
588
|
+
readonly path?: string;
|
|
589
|
+
run(ctx: ExecContext): Promise<number> | number;
|
|
590
|
+
}
|
|
591
|
+
declare function defineCommand(cmd: Command): Command;
|
|
592
|
+
declare class CommandRegistry {
|
|
593
|
+
private readonly commands;
|
|
594
|
+
register(cmd: Command): void;
|
|
595
|
+
registerAll(cmds: Command[]): void;
|
|
596
|
+
get(name: string): Command | undefined;
|
|
597
|
+
has(name: string): boolean;
|
|
598
|
+
names(): string[];
|
|
599
|
+
all(): Command[];
|
|
600
|
+
/** Default install location, used when populating `/bin` and `/usr/bin`. */
|
|
601
|
+
binPath(name: string): string;
|
|
602
|
+
}
|
|
603
|
+
interface ContextInit {
|
|
604
|
+
argv: string[];
|
|
605
|
+
proc: Process;
|
|
606
|
+
kernel: Kernel;
|
|
607
|
+
stdin?: InputStream;
|
|
608
|
+
stdout?: OutputStream;
|
|
609
|
+
stderr?: OutputStream;
|
|
610
|
+
env?: Env;
|
|
611
|
+
cwd?: string;
|
|
612
|
+
cred?: Cred;
|
|
613
|
+
}
|
|
614
|
+
declare function createContext(init: ContextInit): ExecContext;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The container's network stack.
|
|
618
|
+
*
|
|
619
|
+
* There is no real socket layer: HTTP servers started inside the container are
|
|
620
|
+
* registered with Nodepod's request proxy, and this module is the routing and
|
|
621
|
+
* name-resolution layer on top — interfaces for `ip`/`ifconfig`, a hosts file
|
|
622
|
+
* resolver, a listening-port table for `ss`/`netstat`, and an outbound policy
|
|
623
|
+
* that decides whether `curl https://example.com` is allowed to touch the real
|
|
624
|
+
* network.
|
|
625
|
+
*/
|
|
626
|
+
|
|
627
|
+
interface NetInterface {
|
|
628
|
+
name: string;
|
|
629
|
+
mac: string;
|
|
630
|
+
ipv4: string;
|
|
631
|
+
netmask: string;
|
|
632
|
+
broadcast?: string;
|
|
633
|
+
ipv6?: string;
|
|
634
|
+
mtu: number;
|
|
635
|
+
up: boolean;
|
|
636
|
+
loopback: boolean;
|
|
637
|
+
rxBytes: number;
|
|
638
|
+
txBytes: number;
|
|
639
|
+
rxPackets: number;
|
|
640
|
+
txPackets: number;
|
|
641
|
+
}
|
|
642
|
+
interface ListeningPort {
|
|
643
|
+
port: number;
|
|
644
|
+
proto: "tcp" | "udp";
|
|
645
|
+
address: string;
|
|
646
|
+
pid: number;
|
|
647
|
+
program: string;
|
|
648
|
+
since: number;
|
|
649
|
+
}
|
|
650
|
+
interface NetworkOptions {
|
|
651
|
+
/**
|
|
652
|
+
* Allow outbound requests to the real internet. When false (the default),
|
|
653
|
+
* `curl`/`wget`/`fetch` only reach servers running inside the container.
|
|
654
|
+
*/
|
|
655
|
+
allowOutbound?: boolean;
|
|
656
|
+
/** Host allowlist applied when `allowOutbound` is on. `null` means any host. */
|
|
657
|
+
allowedHosts?: string[] | null;
|
|
658
|
+
/** Address handed to eth0. */
|
|
659
|
+
ipv4?: string;
|
|
660
|
+
gateway?: string;
|
|
661
|
+
}
|
|
662
|
+
declare class NetworkStack {
|
|
663
|
+
private readonly pod;
|
|
664
|
+
private readonly vfs;
|
|
665
|
+
private readonly ifaces;
|
|
666
|
+
private readonly listeners;
|
|
667
|
+
readonly options: Required<Pick<NetworkOptions, "allowOutbound">> & NetworkOptions;
|
|
668
|
+
constructor(pod: Nodepod, vfs: Vfs, options?: NetworkOptions);
|
|
669
|
+
interfaces(): NetInterface[];
|
|
670
|
+
interface(name: string): NetInterface | undefined;
|
|
671
|
+
setInterfaceUp(name: string, up: boolean): boolean;
|
|
672
|
+
get gateway(): string;
|
|
673
|
+
/** Resolve through `/etc/hosts`; returns null when the name is not local. */
|
|
674
|
+
resolve(host: string): string | null;
|
|
675
|
+
isLocal(host: string): boolean;
|
|
676
|
+
registerListener(port: number, info: Omit<ListeningPort, "port" | "since">): void;
|
|
677
|
+
unregisterListener(port: number): void;
|
|
678
|
+
listening(): ListeningPort[];
|
|
679
|
+
/** Ports Nodepod's proxy has registered for this instance. */
|
|
680
|
+
private knownPodPorts;
|
|
681
|
+
/** True when something inside the container answers on `port`. */
|
|
682
|
+
isPortOpen(port: number, timeoutMs?: number): Promise<boolean>;
|
|
683
|
+
/** Wait for an in-container server to start answering on `port`. */
|
|
684
|
+
waitForPort(port: number, opts?: {
|
|
685
|
+
timeoutMs?: number;
|
|
686
|
+
intervalMs?: number;
|
|
687
|
+
}): Promise<boolean>;
|
|
688
|
+
outboundAllowed(url: string): boolean;
|
|
689
|
+
procNetDev(): string;
|
|
690
|
+
procNetRoute(): string;
|
|
691
|
+
countTx(bytes: number, iface?: string): void;
|
|
692
|
+
countRx(bytes: number, iface?: string): void;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The kernel: the object that owns the filesystem, the process table, the user
|
|
697
|
+
* database and the executable namespace, and knows how to turn an `argv` into
|
|
698
|
+
* a running process.
|
|
699
|
+
*
|
|
700
|
+
* Executables in `$PATH` are real files. Built-in programs are installed as
|
|
701
|
+
* tiny stub files whose shebang points at the in-kernel implementation, so
|
|
702
|
+
* `ls -l /usr/bin/grep`, `which grep` and `file /usr/bin/grep` all behave, and
|
|
703
|
+
* a user-written script in `/usr/local/bin` is dispatched by exactly the same
|
|
704
|
+
* lookup path.
|
|
705
|
+
*/
|
|
706
|
+
|
|
707
|
+
interface KernelOptions {
|
|
708
|
+
pod: Nodepod;
|
|
709
|
+
hostname?: string;
|
|
710
|
+
/** Login user for interactive sessions. Defaults to `root`. */
|
|
711
|
+
user?: string;
|
|
712
|
+
env?: Env;
|
|
713
|
+
/** Total "RAM" reported by `free`, `/proc/meminfo` and `top`. */
|
|
714
|
+
memoryBytes?: number;
|
|
715
|
+
/** Simulated CPU count for `nproc` and `/proc/cpuinfo`. */
|
|
716
|
+
cpus?: number;
|
|
717
|
+
now?: () => number;
|
|
718
|
+
}
|
|
719
|
+
interface RunOptions {
|
|
720
|
+
cwd?: string;
|
|
721
|
+
env?: Env;
|
|
722
|
+
cred?: Cred;
|
|
723
|
+
stdin?: InputStream | string | Uint8Array;
|
|
724
|
+
stdout?: OutputStream;
|
|
725
|
+
stderr?: OutputStream;
|
|
726
|
+
ppid?: number;
|
|
727
|
+
pgid?: number;
|
|
728
|
+
kind?: ProcessKind;
|
|
729
|
+
tty?: string | null;
|
|
730
|
+
/** Wall-clock budget; the process is sent SIGKILL when it expires. */
|
|
731
|
+
timeoutMs?: number;
|
|
732
|
+
}
|
|
733
|
+
interface RunResult {
|
|
734
|
+
exitCode: number;
|
|
735
|
+
stdout: string;
|
|
736
|
+
stderr: string;
|
|
737
|
+
pid: number;
|
|
738
|
+
signal: string | null;
|
|
739
|
+
timedOut: boolean;
|
|
740
|
+
}
|
|
741
|
+
type ExecutableKind = "builtin" | "script" | "unknown";
|
|
742
|
+
interface ResolvedExecutable {
|
|
743
|
+
kind: ExecutableKind;
|
|
744
|
+
/** Absolute path of the file that was found. */
|
|
745
|
+
path: string;
|
|
746
|
+
/** Present when `kind === "builtin"`. */
|
|
747
|
+
command?: Command;
|
|
748
|
+
/** Interpreter argv from a `#!` line, when `kind === "script"`. */
|
|
749
|
+
interpreter?: string[];
|
|
750
|
+
}
|
|
751
|
+
interface MountEntry {
|
|
752
|
+
device: string;
|
|
753
|
+
mountpoint: string;
|
|
754
|
+
fstype: string;
|
|
755
|
+
options: string;
|
|
756
|
+
totalBytes: number;
|
|
757
|
+
}
|
|
758
|
+
declare class Kernel {
|
|
759
|
+
readonly vfs: Vfs;
|
|
760
|
+
readonly procs: ProcessTable;
|
|
761
|
+
readonly commands: CommandRegistry;
|
|
762
|
+
readonly users: UserDatabase;
|
|
763
|
+
readonly pod: Nodepod;
|
|
764
|
+
readonly bootTime: number;
|
|
765
|
+
readonly memoryBytes: number;
|
|
766
|
+
readonly cpus: number;
|
|
767
|
+
readonly now: () => number;
|
|
768
|
+
/** Populated by the network module once it is attached. */
|
|
769
|
+
net: NetworkStack;
|
|
770
|
+
/** init — pid 1, the ancestor of everything. */
|
|
771
|
+
readonly init: Process;
|
|
772
|
+
private readonly builtinByPath;
|
|
773
|
+
private readonly mounts;
|
|
774
|
+
private disposed;
|
|
775
|
+
private _current;
|
|
776
|
+
/**
|
|
777
|
+
* The process whose builtin is currently on the stack. `/proc/self` resolves
|
|
778
|
+
* through this. It is set around each dispatch, so a command that reads
|
|
779
|
+
* `/proc/self/...` synchronously always sees itself.
|
|
780
|
+
*/
|
|
781
|
+
get currentProcess(): Process | null;
|
|
782
|
+
constructor(opts: KernelOptions);
|
|
783
|
+
get hostname(): string;
|
|
784
|
+
set hostname(value: string);
|
|
785
|
+
get uptimeMs(): number;
|
|
786
|
+
addMount(entry: MountEntry): void;
|
|
787
|
+
removeMount(mountpoint: string): boolean;
|
|
788
|
+
mountTable(): MountEntry[];
|
|
789
|
+
/**
|
|
790
|
+
* Install a command as a real file in `$PATH`. The file holds a shebang that
|
|
791
|
+
* points at the in-kernel dispatcher, which is what `resolve` looks for.
|
|
792
|
+
*/
|
|
793
|
+
installCommand(cmd: Command, dir?: string): void;
|
|
794
|
+
installCommands(cmds: Command[], dir?: string): void;
|
|
795
|
+
/** Directories from `$PATH`, with a sane fallback. */
|
|
796
|
+
pathDirs(env: Env): string[];
|
|
797
|
+
/**
|
|
798
|
+
* Find `name` the way `execvp` does. Returns null when nothing matches.
|
|
799
|
+
*/
|
|
800
|
+
resolveExecutable(name: string, cwd: string, env: Env, cred?: Cred): ResolvedExecutable | null;
|
|
801
|
+
/** `which`-style lookup that only reports the path. */
|
|
802
|
+
which(name: string, cwd: string, env: Env, cred?: Cred): string | null;
|
|
803
|
+
/** Every executable name reachable through `$PATH`, for tab completion. */
|
|
804
|
+
executableNames(env: Env, cred?: Cred): string[];
|
|
805
|
+
private toInputStream;
|
|
806
|
+
/**
|
|
807
|
+
* Create a process for `argv` and start it. Returns the process immediately;
|
|
808
|
+
* `proc.wait()` resolves with the exit code.
|
|
809
|
+
*/
|
|
810
|
+
spawn(argv: string[], opts?: RunOptions): Process;
|
|
811
|
+
/** Run to completion and collect stdout/stderr. */
|
|
812
|
+
run(argv: string[], opts?: RunOptions): Promise<RunResult>;
|
|
813
|
+
/**
|
|
814
|
+
* Resolve `proc.argv` and execute it in-process. Interpreted scripts are
|
|
815
|
+
* dispatched by re-entering with the interpreter's argv, up to a small depth
|
|
816
|
+
* so a self-referential shebang cannot loop forever.
|
|
817
|
+
*/
|
|
818
|
+
private dispatch;
|
|
819
|
+
/** Convenience for internal callers that just want the text output. */
|
|
820
|
+
capture(argv: string[], opts?: RunOptions): Promise<string>;
|
|
821
|
+
dispose(): void;
|
|
822
|
+
get isDisposed(): boolean;
|
|
823
|
+
assertActive(): void;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Shell syntax tree. Words stay raw; `expand.ts` interprets them. */
|
|
827
|
+
type RedirectOp = ">" | ">>" | "<" | "<>" | ">|" | ">&" | "<&" | "&>" | "&>>" | "<<" | "<<<";
|
|
828
|
+
interface Redirect {
|
|
829
|
+
op: RedirectOp;
|
|
830
|
+
/** Source fd, e.g. `2` in `2>file`. Defaults per operator. */
|
|
831
|
+
fd?: number;
|
|
832
|
+
/** Target word (a filename, an fd number, or here-doc/string content). */
|
|
833
|
+
target: string;
|
|
834
|
+
/** Populated for `<<`. */
|
|
835
|
+
heredoc?: {
|
|
836
|
+
body: string;
|
|
837
|
+
expand: boolean;
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
interface Assignment {
|
|
841
|
+
name: string;
|
|
842
|
+
/** Raw value word; undefined for `name=` with nothing after it. */
|
|
843
|
+
value: string;
|
|
844
|
+
/** `name+=value`. */
|
|
845
|
+
append: boolean;
|
|
846
|
+
/** `name=(a b c)` array literal. */
|
|
847
|
+
arrayWords?: string[];
|
|
848
|
+
}
|
|
849
|
+
type Node = ListNode | PipelineNode | SimpleCommandNode | SubshellNode | GroupNode | IfNode | ForNode | ForArithNode | WhileNode | CaseNode | FunctionNode | ArithCommandNode | CondNode;
|
|
850
|
+
type ListOperator = ";" | "&" | "&&" | "||";
|
|
851
|
+
interface ListItem {
|
|
852
|
+
node: Node;
|
|
853
|
+
/** Operator that *follows* this item. */
|
|
854
|
+
op: ListOperator;
|
|
855
|
+
}
|
|
856
|
+
interface ListNode {
|
|
857
|
+
type: "list";
|
|
858
|
+
items: ListItem[];
|
|
859
|
+
}
|
|
860
|
+
interface PipelineNode {
|
|
861
|
+
type: "pipeline";
|
|
862
|
+
commands: Node[];
|
|
863
|
+
/** `! cmd` inverts the exit status. */
|
|
864
|
+
negated: boolean;
|
|
865
|
+
/** `cmd |& next` pipes stderr too. */
|
|
866
|
+
stderrToo: boolean[];
|
|
867
|
+
/** `time cmd` */
|
|
868
|
+
timed?: boolean;
|
|
869
|
+
}
|
|
870
|
+
interface SimpleCommandNode {
|
|
871
|
+
type: "command";
|
|
872
|
+
assignments: Assignment[];
|
|
873
|
+
words: string[];
|
|
874
|
+
redirects: Redirect[];
|
|
875
|
+
}
|
|
876
|
+
interface SubshellNode {
|
|
877
|
+
type: "subshell";
|
|
878
|
+
body: Node;
|
|
879
|
+
redirects: Redirect[];
|
|
880
|
+
}
|
|
881
|
+
interface GroupNode {
|
|
882
|
+
type: "group";
|
|
883
|
+
body: Node;
|
|
884
|
+
redirects: Redirect[];
|
|
885
|
+
}
|
|
886
|
+
interface IfClause {
|
|
887
|
+
condition: Node;
|
|
888
|
+
body: Node;
|
|
889
|
+
}
|
|
890
|
+
interface IfNode {
|
|
891
|
+
type: "if";
|
|
892
|
+
clauses: IfClause[];
|
|
893
|
+
elseBody?: Node;
|
|
894
|
+
redirects: Redirect[];
|
|
895
|
+
}
|
|
896
|
+
interface ForNode {
|
|
897
|
+
type: "for";
|
|
898
|
+
name: string;
|
|
899
|
+
/** Absent means `for x; do` which iterates `"$@"`. */
|
|
900
|
+
words?: string[];
|
|
901
|
+
body: Node;
|
|
902
|
+
redirects: Redirect[];
|
|
903
|
+
/** `select` shares the same shape. */
|
|
904
|
+
select?: boolean;
|
|
905
|
+
}
|
|
906
|
+
interface ForArithNode {
|
|
907
|
+
type: "for-arith";
|
|
908
|
+
init: string;
|
|
909
|
+
condition: string;
|
|
910
|
+
step: string;
|
|
911
|
+
body: Node;
|
|
912
|
+
redirects: Redirect[];
|
|
913
|
+
}
|
|
914
|
+
interface WhileNode {
|
|
915
|
+
type: "while";
|
|
916
|
+
condition: Node;
|
|
917
|
+
body: Node;
|
|
918
|
+
until: boolean;
|
|
919
|
+
redirects: Redirect[];
|
|
920
|
+
}
|
|
921
|
+
interface CaseItem {
|
|
922
|
+
patterns: string[];
|
|
923
|
+
body: Node | null;
|
|
924
|
+
/** `;;` stops, `;&` falls through, `;;&` retests. */
|
|
925
|
+
terminator: ";;" | ";&" | ";;&";
|
|
926
|
+
}
|
|
927
|
+
interface CaseNode {
|
|
928
|
+
type: "case";
|
|
929
|
+
word: string;
|
|
930
|
+
items: CaseItem[];
|
|
931
|
+
redirects: Redirect[];
|
|
932
|
+
}
|
|
933
|
+
interface FunctionNode {
|
|
934
|
+
type: "function";
|
|
935
|
+
name: string;
|
|
936
|
+
body: Node;
|
|
937
|
+
redirects: Redirect[];
|
|
938
|
+
}
|
|
939
|
+
interface ArithCommandNode {
|
|
940
|
+
type: "arith";
|
|
941
|
+
expression: string;
|
|
942
|
+
redirects: Redirect[];
|
|
943
|
+
}
|
|
944
|
+
/** `[[ ... ]]` — parsed as a small expression tree of its own. */
|
|
945
|
+
type CondExpr = {
|
|
946
|
+
type: "unary";
|
|
947
|
+
op: string;
|
|
948
|
+
operand: string;
|
|
949
|
+
} | {
|
|
950
|
+
type: "binary";
|
|
951
|
+
op: string;
|
|
952
|
+
left: string;
|
|
953
|
+
right: string;
|
|
954
|
+
} | {
|
|
955
|
+
type: "not";
|
|
956
|
+
operand: CondExpr;
|
|
957
|
+
} | {
|
|
958
|
+
type: "and";
|
|
959
|
+
left: CondExpr;
|
|
960
|
+
right: CondExpr;
|
|
961
|
+
} | {
|
|
962
|
+
type: "or";
|
|
963
|
+
left: CondExpr;
|
|
964
|
+
right: CondExpr;
|
|
965
|
+
} | {
|
|
966
|
+
type: "word";
|
|
967
|
+
value: string;
|
|
968
|
+
};
|
|
969
|
+
interface CondNode {
|
|
970
|
+
type: "cond";
|
|
971
|
+
expression: CondExpr;
|
|
972
|
+
redirects: Redirect[];
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Shell variable table: scalars, indexed arrays, export/readonly attributes,
|
|
977
|
+
* and the function-local scoping that `local` introduces.
|
|
978
|
+
*/
|
|
979
|
+
interface VarAttributes {
|
|
980
|
+
exported?: boolean;
|
|
981
|
+
readonly?: boolean;
|
|
982
|
+
integer?: boolean;
|
|
983
|
+
/** `declare -l` / `-u` case folding. */
|
|
984
|
+
lower?: boolean;
|
|
985
|
+
upper?: boolean;
|
|
986
|
+
}
|
|
987
|
+
interface VarEntry extends VarAttributes {
|
|
988
|
+
value: string;
|
|
989
|
+
array?: string[];
|
|
990
|
+
assoc?: Map<string, string>;
|
|
991
|
+
}
|
|
992
|
+
declare class Variables {
|
|
993
|
+
/** Innermost scope last; index 0 is the global scope. */
|
|
994
|
+
private readonly scopes;
|
|
995
|
+
constructor(initial?: Record<string, string>);
|
|
996
|
+
pushScope(): void;
|
|
997
|
+
popScope(): void;
|
|
998
|
+
get depth(): number;
|
|
999
|
+
private find;
|
|
1000
|
+
has(name: string): boolean;
|
|
1001
|
+
get(name: string): string | undefined;
|
|
1002
|
+
entry(name: string): VarEntry | undefined;
|
|
1003
|
+
getArray(name: string): string[] | undefined;
|
|
1004
|
+
isArray(name: string): boolean;
|
|
1005
|
+
set(name: string, value: string, attrs?: VarAttributes): void;
|
|
1006
|
+
/** Declare in the innermost scope, shadowing outer definitions (`local`). */
|
|
1007
|
+
setLocal(name: string, value: string, attrs?: VarAttributes): void;
|
|
1008
|
+
setArray(name: string, values: string[], attrs?: VarAttributes): void;
|
|
1009
|
+
setIndex(name: string, index: number, value: string): void;
|
|
1010
|
+
append(name: string, value: string): void;
|
|
1011
|
+
unset(name: string): boolean;
|
|
1012
|
+
export(name: string, exported?: boolean): void;
|
|
1013
|
+
markReadonly(name: string): void;
|
|
1014
|
+
names(): string[];
|
|
1015
|
+
/** The environment handed to a child process. */
|
|
1016
|
+
environment(): Record<string, string>;
|
|
1017
|
+
/** Every variable with its attributes, for `declare -p` / `set`. */
|
|
1018
|
+
all(): Array<{
|
|
1019
|
+
name: string;
|
|
1020
|
+
entry: VarEntry;
|
|
1021
|
+
}>;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* POSIX path helpers. Deliberately independent of `node:path` so container
|
|
1026
|
+
* paths behave identically no matter what platform the host runs on.
|
|
1027
|
+
*/
|
|
1028
|
+
declare const SEP = "/";
|
|
1029
|
+
declare function isAbsolute(p: string): boolean;
|
|
1030
|
+
/** Split into non-empty segments, dropping the leading/trailing slashes. */
|
|
1031
|
+
declare function segments(p: string): string[];
|
|
1032
|
+
/**
|
|
1033
|
+
* Lexical normalisation: collapse `//`, resolve `.` and `..` without touching
|
|
1034
|
+
* the filesystem. Symlinks are *not* followed — that is `Vfs.realpath`'s job.
|
|
1035
|
+
*/
|
|
1036
|
+
declare function normalize(p: string): string;
|
|
1037
|
+
/** Join fragments then normalise, like `path.posix.join`. */
|
|
1038
|
+
declare function join(...parts: string[]): string;
|
|
1039
|
+
/** Resolve `p` against `base` (usually a process cwd) to an absolute path. */
|
|
1040
|
+
declare function resolve(base: string, ...parts: string[]): string;
|
|
1041
|
+
declare function dirname(p: string): string;
|
|
1042
|
+
declare function basename(p: string, ext?: string): string;
|
|
1043
|
+
declare function extname(p: string): string;
|
|
1044
|
+
/** Relative path from `from` to `to`, both assumed absolute + normalised. */
|
|
1045
|
+
declare function relative(from: string, to: string): string;
|
|
1046
|
+
/** True when `child` is `parent` or lives beneath it. */
|
|
1047
|
+
declare function contains(parent: string, child: string): boolean;
|
|
1048
|
+
/** Drop a trailing slash except on the root, for display and map keys. */
|
|
1049
|
+
declare function clean(p: string): string;
|
|
1050
|
+
|
|
1051
|
+
declare const path_SEP: typeof SEP;
|
|
1052
|
+
declare const path_basename: typeof basename;
|
|
1053
|
+
declare const path_clean: typeof clean;
|
|
1054
|
+
declare const path_contains: typeof contains;
|
|
1055
|
+
declare const path_dirname: typeof dirname;
|
|
1056
|
+
declare const path_extname: typeof extname;
|
|
1057
|
+
declare const path_isAbsolute: typeof isAbsolute;
|
|
1058
|
+
declare const path_join: typeof join;
|
|
1059
|
+
declare const path_normalize: typeof normalize;
|
|
1060
|
+
declare const path_relative: typeof relative;
|
|
1061
|
+
declare const path_resolve: typeof resolve;
|
|
1062
|
+
declare const path_segments: typeof segments;
|
|
1063
|
+
declare namespace path {
|
|
1064
|
+
export { path_SEP as SEP, path_basename as basename, path_clean as clean, path_contains as contains, path_dirname as dirname, path_extname as extname, path_isAbsolute as isAbsolute, path_join as join, path_normalize as normalize, path_relative as relative, path_resolve as resolve, path_segments as segments };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Word expansion, in the order POSIX specifies:
|
|
1069
|
+
*
|
|
1070
|
+
* brace → tilde → parameter/command/arithmetic → field splitting →
|
|
1071
|
+
* pathname → quote removal
|
|
1072
|
+
*
|
|
1073
|
+
* The tricky part is that only text produced by *unquoted* expansions may be
|
|
1074
|
+
* field-split, and only unquoted text may be globbed. Each fragment therefore
|
|
1075
|
+
* carries `split` and `glob` flags through the pipeline, and quote removal is
|
|
1076
|
+
* the last thing that happens.
|
|
1077
|
+
*/
|
|
1078
|
+
|
|
1079
|
+
interface ExpandContext {
|
|
1080
|
+
vars: Variables;
|
|
1081
|
+
/** `$1`, `$2`, … */
|
|
1082
|
+
positional: string[];
|
|
1083
|
+
/** `$0` */
|
|
1084
|
+
scriptName: string;
|
|
1085
|
+
/** `$?` */
|
|
1086
|
+
lastStatus: number;
|
|
1087
|
+
/** `$$` */
|
|
1088
|
+
shellPid: number;
|
|
1089
|
+
/** `$!` */
|
|
1090
|
+
lastBackgroundPid: number;
|
|
1091
|
+
/** `$-` */
|
|
1092
|
+
optionFlags: string;
|
|
1093
|
+
cwd: string;
|
|
1094
|
+
vfs: Vfs;
|
|
1095
|
+
cred: Cred;
|
|
1096
|
+
/** Home directory lookup for `~user`. */
|
|
1097
|
+
homeFor(user: string): string | undefined;
|
|
1098
|
+
/** Runs a command substitution and returns its stdout. */
|
|
1099
|
+
runSubstitution(command: string): Promise<string>;
|
|
1100
|
+
/** Materialises `<(cmd)` / `>(cmd)` as a path. Optional. */
|
|
1101
|
+
processSubstitution?(command: string, direction: "in" | "out"): Promise<string>;
|
|
1102
|
+
/** `set -u` */
|
|
1103
|
+
nounset?: boolean;
|
|
1104
|
+
/** `set -f` */
|
|
1105
|
+
noglob?: boolean;
|
|
1106
|
+
/** Extended globbing (`shopt -s extglob`). */
|
|
1107
|
+
extglob?: boolean;
|
|
1108
|
+
/** Include dotfiles in globs (`shopt -s dotglob`). */
|
|
1109
|
+
dotglob?: boolean;
|
|
1110
|
+
/** Leave an unmatched glob as-is (bash default) or drop it (`nullglob`). */
|
|
1111
|
+
nullglob?: boolean;
|
|
1112
|
+
/** Error out on an unmatched glob (`failglob`). */
|
|
1113
|
+
failglob?: boolean;
|
|
1114
|
+
}
|
|
1115
|
+
/** Full expansion of a list of words, as used for command arguments. */
|
|
1116
|
+
declare function expandWords(words: string[], ctx: ExpandContext): Promise<string[]>;
|
|
1117
|
+
/** Expand one word into zero or more fields. */
|
|
1118
|
+
declare function expandWord(word: string, ctx: ExpandContext): Promise<string[]>;
|
|
1119
|
+
/** `a{b,c}d` → `abd acd`; `{1..5}` and `{a..e}` sequences too. */
|
|
1120
|
+
declare function braceExpand(word: string): string[];
|
|
1121
|
+
declare function shellQuote(value: string): string;
|
|
1122
|
+
|
|
1123
|
+
/** Registry of shell builtins. Lookup order in the interpreter is: function, builtin, then `$PATH`. */
|
|
1124
|
+
|
|
1125
|
+
interface BuiltinContext {
|
|
1126
|
+
shell: Shell;
|
|
1127
|
+
/** Full argv, including argv[0]. */
|
|
1128
|
+
argv: string[];
|
|
1129
|
+
io: ShellIO;
|
|
1130
|
+
}
|
|
1131
|
+
type Builtin = (ctx: BuiltinContext) => Promise<number> | number;
|
|
1132
|
+
declare function getBuiltin(name: string): Builtin | undefined;
|
|
1133
|
+
declare function isBuiltinName(name: string): boolean;
|
|
1134
|
+
declare function builtinNames(): string[];
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* The shell interpreter.
|
|
1138
|
+
*
|
|
1139
|
+
* Walks the syntax tree, applies redirections, wires pipelines together, and
|
|
1140
|
+
* dispatches each simple command to a function, a builtin, or the kernel's
|
|
1141
|
+
* executable lookup — in that order, which is the order bash uses.
|
|
1142
|
+
*/
|
|
1143
|
+
|
|
1144
|
+
interface ShellIO {
|
|
1145
|
+
stdin: InputStream;
|
|
1146
|
+
stdout: OutputStream;
|
|
1147
|
+
stderr: OutputStream;
|
|
1148
|
+
}
|
|
1149
|
+
interface ShellOptions {
|
|
1150
|
+
/** `set -e` */
|
|
1151
|
+
errexit: boolean;
|
|
1152
|
+
/** `set -u` */
|
|
1153
|
+
nounset: boolean;
|
|
1154
|
+
/** `set -x` */
|
|
1155
|
+
xtrace: boolean;
|
|
1156
|
+
/** `set -v` */
|
|
1157
|
+
verbose: boolean;
|
|
1158
|
+
/** `set -f` */
|
|
1159
|
+
noglob: boolean;
|
|
1160
|
+
/** `set -o pipefail` */
|
|
1161
|
+
pipefail: boolean;
|
|
1162
|
+
/** `set -n` */
|
|
1163
|
+
noexec: boolean;
|
|
1164
|
+
/** `set -m` */
|
|
1165
|
+
monitor: boolean;
|
|
1166
|
+
/** `set -C` */
|
|
1167
|
+
noclobber: boolean;
|
|
1168
|
+
/** `set -a` */
|
|
1169
|
+
allexport: boolean;
|
|
1170
|
+
/** Interactive shells print prompts and keep history. */
|
|
1171
|
+
interactive: boolean;
|
|
1172
|
+
/** Login shells source `/etc/profile`. */
|
|
1173
|
+
login: boolean;
|
|
1174
|
+
}
|
|
1175
|
+
interface Job {
|
|
1176
|
+
id: number;
|
|
1177
|
+
pgid: number;
|
|
1178
|
+
command: string;
|
|
1179
|
+
state: "running" | "done" | "stopped";
|
|
1180
|
+
exitCode: number | null;
|
|
1181
|
+
promise: Promise<number>;
|
|
1182
|
+
pids: number[];
|
|
1183
|
+
}
|
|
1184
|
+
declare class ShellExit {
|
|
1185
|
+
readonly code: number;
|
|
1186
|
+
constructor(code: number);
|
|
1187
|
+
}
|
|
1188
|
+
interface ShellInit {
|
|
1189
|
+
kernel: Kernel;
|
|
1190
|
+
proc: Process;
|
|
1191
|
+
cwd?: string;
|
|
1192
|
+
env?: Record<string, string>;
|
|
1193
|
+
cred?: Cred;
|
|
1194
|
+
options?: Partial<ShellOptions>;
|
|
1195
|
+
positional?: string[];
|
|
1196
|
+
scriptName?: string;
|
|
1197
|
+
}
|
|
1198
|
+
declare class Shell {
|
|
1199
|
+
readonly kernel: Kernel;
|
|
1200
|
+
readonly proc: Process;
|
|
1201
|
+
readonly vars: Variables;
|
|
1202
|
+
readonly functions: Map<string, Node>;
|
|
1203
|
+
readonly aliases: Map<string, string>;
|
|
1204
|
+
readonly traps: Map<string, string>;
|
|
1205
|
+
readonly dirStack: string[];
|
|
1206
|
+
readonly jobs: Job[];
|
|
1207
|
+
readonly history: string[];
|
|
1208
|
+
options: ShellOptions;
|
|
1209
|
+
shopts: Set<string>;
|
|
1210
|
+
positional: string[];
|
|
1211
|
+
scriptName: string;
|
|
1212
|
+
lastStatus: number;
|
|
1213
|
+
lastBackgroundPid: number;
|
|
1214
|
+
cwd: string;
|
|
1215
|
+
cred: Cred;
|
|
1216
|
+
/** Pipeline exit statuses, exposed as `PIPESTATUS`. */
|
|
1217
|
+
pipeStatus: number[];
|
|
1218
|
+
private nextJobId;
|
|
1219
|
+
private functionDepth;
|
|
1220
|
+
private tempFileCounter;
|
|
1221
|
+
private exiting;
|
|
1222
|
+
constructor(init: ShellInit);
|
|
1223
|
+
/** Parse and run a script fragment. Returns the last exit status. */
|
|
1224
|
+
execute(source: string, io: ShellIO): Promise<number>;
|
|
1225
|
+
get isExiting(): boolean;
|
|
1226
|
+
/** True when `source` is not yet a complete command (for REPL continuation). */
|
|
1227
|
+
static isIncomplete(source: string): boolean;
|
|
1228
|
+
expandContext(): ExpandContext;
|
|
1229
|
+
optionFlagString(): string;
|
|
1230
|
+
/** Run `command` in a subshell and return its stdout. */
|
|
1231
|
+
captureSubshell(command: string): Promise<string>;
|
|
1232
|
+
/** `<(cmd)` — run the command now and hand back a path holding its output. */
|
|
1233
|
+
private makeProcessSubstitution;
|
|
1234
|
+
/** A copy that shares nothing mutable with this shell. */
|
|
1235
|
+
fork(): Shell;
|
|
1236
|
+
private currentIO;
|
|
1237
|
+
run(node: Node, io: ShellIO): Promise<number>;
|
|
1238
|
+
private runList;
|
|
1239
|
+
/** Skip past short-circuited members of an `&&`/`||` chain. */
|
|
1240
|
+
private skipChain;
|
|
1241
|
+
private runPipeline;
|
|
1242
|
+
/**
|
|
1243
|
+
* Every stage of a pipeline runs in its own subshell in bash. Sharing the
|
|
1244
|
+
* variable table would let `echo x | read v` leak `v` into the parent.
|
|
1245
|
+
*/
|
|
1246
|
+
private forkForPipeline;
|
|
1247
|
+
private reportTime;
|
|
1248
|
+
private startBackgroundJob;
|
|
1249
|
+
private runSubshell;
|
|
1250
|
+
private runGroup;
|
|
1251
|
+
private runIf;
|
|
1252
|
+
/** Conditions are exempt from `set -e`. */
|
|
1253
|
+
private runCondition;
|
|
1254
|
+
private runFor;
|
|
1255
|
+
private runSelect;
|
|
1256
|
+
private runForArith;
|
|
1257
|
+
private runWhile;
|
|
1258
|
+
private runCase;
|
|
1259
|
+
private runFunctionDefinition;
|
|
1260
|
+
private runArithCommand;
|
|
1261
|
+
private runCond;
|
|
1262
|
+
private evalCond;
|
|
1263
|
+
private runSimpleCommand;
|
|
1264
|
+
private reportCommandError;
|
|
1265
|
+
/** One level of alias substitution on the command word. */
|
|
1266
|
+
private substituteAlias;
|
|
1267
|
+
private invoke;
|
|
1268
|
+
private callFunction;
|
|
1269
|
+
private runExternal;
|
|
1270
|
+
/** `VAR=x cmd` — set for the duration, then restore. */
|
|
1271
|
+
private applyTemporaryAssignments;
|
|
1272
|
+
applyAssignment(assignment: SimpleCommandNode["assignments"][number], exported: boolean): Promise<void>;
|
|
1273
|
+
applyRedirects(redirects: Redirect[], io: ShellIO): Promise<{
|
|
1274
|
+
io: ShellIO;
|
|
1275
|
+
cleanup: () => void;
|
|
1276
|
+
}>;
|
|
1277
|
+
private openInput;
|
|
1278
|
+
private openOutput;
|
|
1279
|
+
private expandHeredoc;
|
|
1280
|
+
setTrap(signal: string, action: string): void;
|
|
1281
|
+
runTrap(signal: string, io: ShellIO): Promise<void>;
|
|
1282
|
+
changeDirectory(target: string): void;
|
|
1283
|
+
throwBreak(levels: number): never;
|
|
1284
|
+
throwContinue(levels: number): never;
|
|
1285
|
+
throwReturn(code: number): never;
|
|
1286
|
+
throwExit(code: number): never;
|
|
1287
|
+
addJob(job: Job): void;
|
|
1288
|
+
reapJobs(): Job[];
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* A promise-based filesystem façade for host code, shaped like `fs/promises`
|
|
1293
|
+
* so it reads naturally from the outside.
|
|
1294
|
+
*/
|
|
1295
|
+
|
|
1296
|
+
declare class ContainerFs {
|
|
1297
|
+
private readonly kernel;
|
|
1298
|
+
constructor(kernel: Kernel);
|
|
1299
|
+
private get vfs();
|
|
1300
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
1301
|
+
readFile(path: string, encoding: "utf8" | "utf-8"): Promise<string>;
|
|
1302
|
+
writeFile(path: string, data: string | Uint8Array, opts?: {
|
|
1303
|
+
mode?: number;
|
|
1304
|
+
}): Promise<void>;
|
|
1305
|
+
appendFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
1306
|
+
readdir(path: string): Promise<string[]>;
|
|
1307
|
+
readdir(path: string, opts: {
|
|
1308
|
+
withFileTypes: true;
|
|
1309
|
+
}): Promise<DirEntry[]>;
|
|
1310
|
+
mkdir(path: string, opts?: {
|
|
1311
|
+
recursive?: boolean;
|
|
1312
|
+
mode?: number;
|
|
1313
|
+
}): Promise<void>;
|
|
1314
|
+
rm(path: string, opts?: {
|
|
1315
|
+
recursive?: boolean;
|
|
1316
|
+
force?: boolean;
|
|
1317
|
+
}): Promise<void>;
|
|
1318
|
+
rename(from: string, to: string): Promise<void>;
|
|
1319
|
+
copyFile(from: string, to: string): Promise<void>;
|
|
1320
|
+
stat(path: string): Promise<Stats>;
|
|
1321
|
+
lstat(path: string): Promise<Stats>;
|
|
1322
|
+
exists(path: string): Promise<boolean>;
|
|
1323
|
+
symlink(target: string, link: string): Promise<void>;
|
|
1324
|
+
readlink(path: string): Promise<string>;
|
|
1325
|
+
realpath(path: string): Promise<string>;
|
|
1326
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
1327
|
+
chown(path: string, uid: number, gid: number): Promise<void>;
|
|
1328
|
+
/** Every path beneath `root`, depth-first. */
|
|
1329
|
+
walk(root?: string): Promise<string[]>;
|
|
1330
|
+
/** Total bytes and file counts, as `df` reports them. */
|
|
1331
|
+
usage(root?: string): Promise<{
|
|
1332
|
+
files: number;
|
|
1333
|
+
dirs: number;
|
|
1334
|
+
bytes: number;
|
|
1335
|
+
}>;
|
|
1336
|
+
/** Read many files at once, keyed by path. */
|
|
1337
|
+
readAll(paths: string[]): Promise<Record<string, string>>;
|
|
1338
|
+
/** Write a whole map of files, creating parents. */
|
|
1339
|
+
writeAll(files: Record<string, string | Uint8Array>, opts?: {
|
|
1340
|
+
cwd?: string;
|
|
1341
|
+
}): Promise<void>;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* A stateful shell session — `cd`, variables, functions and history persist
|
|
1346
|
+
* across calls, the way a terminal does. `Container.exec` is deliberately
|
|
1347
|
+
* stateless; this is the other half.
|
|
1348
|
+
*/
|
|
1349
|
+
|
|
1350
|
+
interface SessionInit {
|
|
1351
|
+
cwd: string;
|
|
1352
|
+
env: Record<string, string>;
|
|
1353
|
+
cred: Cred;
|
|
1354
|
+
hooks?: {
|
|
1355
|
+
onStdout?: (chunk: string) => void;
|
|
1356
|
+
onStderr?: (chunk: string) => void;
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
interface SessionRunOptions {
|
|
1360
|
+
stdin?: string | Uint8Array | InputStream;
|
|
1361
|
+
onStdout?: (chunk: string) => void;
|
|
1362
|
+
onStderr?: (chunk: string) => void;
|
|
1363
|
+
timeoutMs?: number;
|
|
1364
|
+
/** Present the session as attached to a terminal. */
|
|
1365
|
+
tty?: boolean;
|
|
1366
|
+
columns?: number;
|
|
1367
|
+
rows?: number;
|
|
1368
|
+
}
|
|
1369
|
+
interface SessionResult {
|
|
1370
|
+
exitCode: number;
|
|
1371
|
+
stdout: string;
|
|
1372
|
+
stderr: string;
|
|
1373
|
+
output: string;
|
|
1374
|
+
}
|
|
1375
|
+
declare class Session {
|
|
1376
|
+
private readonly kernel;
|
|
1377
|
+
readonly shell: Shell;
|
|
1378
|
+
readonly proc: Process;
|
|
1379
|
+
private closed;
|
|
1380
|
+
constructor(kernel: Kernel, init: SessionInit);
|
|
1381
|
+
private readonly hooks;
|
|
1382
|
+
get cwd(): string;
|
|
1383
|
+
get env(): Record<string, string>;
|
|
1384
|
+
get history(): string[];
|
|
1385
|
+
/** Run a command line, keeping every side effect for the next call. */
|
|
1386
|
+
run(command: string, opts?: SessionRunOptions): Promise<SessionResult>;
|
|
1387
|
+
/** Stream a long-running command; resolves when it exits. */
|
|
1388
|
+
stream(command: string, handlers?: {
|
|
1389
|
+
onStdout?: (chunk: string) => void;
|
|
1390
|
+
onStderr?: (chunk: string) => void;
|
|
1391
|
+
}): Promise<number>;
|
|
1392
|
+
/** Feed the session an arbitrary output stream, for terminal integration. */
|
|
1393
|
+
pipeTo(command: string, stdout: OutputStream, stderr: OutputStream, stdin?: InputStream): Promise<number>;
|
|
1394
|
+
/** True when the last command asked the shell to exit. */
|
|
1395
|
+
get isExiting(): boolean;
|
|
1396
|
+
close(): void;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* The public surface: a Linux-like container you can boot inside any Node
|
|
1401
|
+
* process, run commands in, and throw away.
|
|
1402
|
+
*
|
|
1403
|
+
* ```ts
|
|
1404
|
+
* const box = await createContainer({ files: { "/app/index.js": "console.log(1)" } });
|
|
1405
|
+
* await box.exec("node /app/index.js");
|
|
1406
|
+
* ```
|
|
1407
|
+
*/
|
|
1408
|
+
|
|
1409
|
+
interface ContainerOptions {
|
|
1410
|
+
/**
|
|
1411
|
+
* Files to seed the filesystem with, keyed by absolute (or `cwd`-relative)
|
|
1412
|
+
* path. Parent directories are created automatically — this is the fast way
|
|
1413
|
+
* to drop a whole project in and run it.
|
|
1414
|
+
*/
|
|
1415
|
+
files?: Record<string, string | Uint8Array>;
|
|
1416
|
+
/** Directory `files` keys are resolved against, and the default cwd. Default `/`. */
|
|
1417
|
+
cwd?: string;
|
|
1418
|
+
hostname?: string;
|
|
1419
|
+
/**
|
|
1420
|
+
* Login user. `"root"` (default) runs privileged; any other name is created
|
|
1421
|
+
* with uid 1000 and sudo rights. Pass `null` for a root-only image.
|
|
1422
|
+
*/
|
|
1423
|
+
user?: string | null;
|
|
1424
|
+
env?: Record<string, string>;
|
|
1425
|
+
/** RAM reported by `free`, `top` and `/proc/meminfo`. Default 2 GiB. */
|
|
1426
|
+
memory?: number;
|
|
1427
|
+
/** CPU count reported by `nproc` and `/proc/cpuinfo`. Default 4. */
|
|
1428
|
+
cpus?: number;
|
|
1429
|
+
network?: NetworkOptions;
|
|
1430
|
+
timezone?: string;
|
|
1431
|
+
/** Default wall-clock limit for `exec`. Omit for no limit. */
|
|
1432
|
+
timeoutMs?: number;
|
|
1433
|
+
/** Called for every byte any command writes to stdout, across the container. */
|
|
1434
|
+
onStdout?: (chunk: string) => void;
|
|
1435
|
+
onStderr?: (chunk: string) => void;
|
|
1436
|
+
/** Invoked when an in-container HTTP server starts listening. */
|
|
1437
|
+
onServerReady?: (port: number, url: string) => void;
|
|
1438
|
+
}
|
|
1439
|
+
interface ExecOptions {
|
|
1440
|
+
cwd?: string;
|
|
1441
|
+
env?: Record<string, string>;
|
|
1442
|
+
/** Run as this user instead of the container default. */
|
|
1443
|
+
user?: string;
|
|
1444
|
+
stdin?: string | Uint8Array;
|
|
1445
|
+
/** Stream output as it is produced, in addition to buffering it. */
|
|
1446
|
+
onStdout?: (chunk: string) => void;
|
|
1447
|
+
onStderr?: (chunk: string) => void;
|
|
1448
|
+
timeoutMs?: number;
|
|
1449
|
+
/** Report the command as running on a terminal (affects `ls` colour, `-t`). */
|
|
1450
|
+
tty?: boolean;
|
|
1451
|
+
columns?: number;
|
|
1452
|
+
rows?: number;
|
|
1453
|
+
}
|
|
1454
|
+
interface ExecResult {
|
|
1455
|
+
exitCode: number;
|
|
1456
|
+
stdout: string;
|
|
1457
|
+
stderr: string;
|
|
1458
|
+
/** stdout and stderr interleaved in write order. */
|
|
1459
|
+
output: string;
|
|
1460
|
+
timedOut: boolean;
|
|
1461
|
+
durationMs: number;
|
|
1462
|
+
}
|
|
1463
|
+
interface SpawnHandle {
|
|
1464
|
+
pid: number;
|
|
1465
|
+
stdin: Pipe;
|
|
1466
|
+
stdout: Pipe;
|
|
1467
|
+
stderr: Pipe;
|
|
1468
|
+
/** Resolves with the exit code. */
|
|
1469
|
+
wait(): Promise<number>;
|
|
1470
|
+
kill(signal?: string): void;
|
|
1471
|
+
readonly exitCode: number | null;
|
|
1472
|
+
}
|
|
1473
|
+
interface HttpResponse {
|
|
1474
|
+
status: number;
|
|
1475
|
+
statusText: string;
|
|
1476
|
+
headers: Record<string, string>;
|
|
1477
|
+
body: string;
|
|
1478
|
+
bytes: Uint8Array;
|
|
1479
|
+
json<T = unknown>(): T;
|
|
1480
|
+
}
|
|
1481
|
+
declare class Container {
|
|
1482
|
+
readonly kernel: Kernel;
|
|
1483
|
+
readonly pod: Nodepod;
|
|
1484
|
+
readonly fs: ContainerFs;
|
|
1485
|
+
readonly net: NetworkStack;
|
|
1486
|
+
private readonly defaults;
|
|
1487
|
+
private readonly hooks;
|
|
1488
|
+
private disposed;
|
|
1489
|
+
private defaultSession;
|
|
1490
|
+
private constructor();
|
|
1491
|
+
static create(opts?: ContainerOptions): Promise<Container>;
|
|
1492
|
+
/**
|
|
1493
|
+
* Drop a map of files into the container. Keys may be absolute or relative
|
|
1494
|
+
* to `opts.cwd`; parent directories are created as needed.
|
|
1495
|
+
*/
|
|
1496
|
+
mount(files: Record<string, string | Uint8Array>, opts?: {
|
|
1497
|
+
cwd?: string;
|
|
1498
|
+
mode?: number;
|
|
1499
|
+
}): this;
|
|
1500
|
+
private mountSync;
|
|
1501
|
+
/** Copy a directory tree from the host filesystem into the container. */
|
|
1502
|
+
copyIn(hostPath: string, containerPath: string): Promise<void>;
|
|
1503
|
+
/** Copy a file or directory out of the container onto the host. */
|
|
1504
|
+
copyOut(containerPath: string, hostPath: string): Promise<void>;
|
|
1505
|
+
/** Run a shell command line and collect its output. */
|
|
1506
|
+
exec(command: string, opts?: ExecOptions): Promise<ExecResult>;
|
|
1507
|
+
/** Run a program directly, without a shell parsing the arguments. */
|
|
1508
|
+
run(argv: string[], opts?: ExecOptions): Promise<ExecResult>;
|
|
1509
|
+
/**
|
|
1510
|
+
* Start a command and get streams back, for long-running processes such as a
|
|
1511
|
+
* dev server that you want to watch and later kill.
|
|
1512
|
+
*/
|
|
1513
|
+
spawn(command: string, opts?: ExecOptions): SpawnHandle;
|
|
1514
|
+
/**
|
|
1515
|
+
* A stateful shell session: `cd`, variables and functions persist between
|
|
1516
|
+
* calls, the way a terminal behaves.
|
|
1517
|
+
*/
|
|
1518
|
+
session(opts?: {
|
|
1519
|
+
cwd?: string;
|
|
1520
|
+
env?: Record<string, string>;
|
|
1521
|
+
user?: string;
|
|
1522
|
+
}): Session;
|
|
1523
|
+
/** The container-wide session used by `shell()` shorthand helpers. */
|
|
1524
|
+
get shell(): Session;
|
|
1525
|
+
private makeStdio;
|
|
1526
|
+
/** Send an HTTP request to a server running inside the container. */
|
|
1527
|
+
request(port: number, init?: {
|
|
1528
|
+
method?: string;
|
|
1529
|
+
path?: string;
|
|
1530
|
+
headers?: Record<string, string>;
|
|
1531
|
+
body?: string | Uint8Array;
|
|
1532
|
+
}): Promise<HttpResponse>;
|
|
1533
|
+
/** Wait until something inside the container answers on `port`. */
|
|
1534
|
+
waitForPort(port: number, opts?: {
|
|
1535
|
+
timeoutMs?: number;
|
|
1536
|
+
intervalMs?: number;
|
|
1537
|
+
}): Promise<boolean>;
|
|
1538
|
+
/**
|
|
1539
|
+
* Bridge a container port onto a real host port, so a browser (or anything
|
|
1540
|
+
* else on your machine) can reach a dev server running inside the sandbox.
|
|
1541
|
+
*/
|
|
1542
|
+
expose(port: number, opts?: {
|
|
1543
|
+
hostPort?: number;
|
|
1544
|
+
hostname?: string;
|
|
1545
|
+
}): Promise<{
|
|
1546
|
+
url: string;
|
|
1547
|
+
port: number;
|
|
1548
|
+
close(): Promise<void>;
|
|
1549
|
+
}>;
|
|
1550
|
+
/** A serialisable snapshot of the whole filesystem. */
|
|
1551
|
+
snapshot(opts?: {
|
|
1552
|
+
shallow?: boolean;
|
|
1553
|
+
}): unknown;
|
|
1554
|
+
restore(snapshot: unknown): Promise<void>;
|
|
1555
|
+
get hostname(): string;
|
|
1556
|
+
get user(): string;
|
|
1557
|
+
get cwd(): string;
|
|
1558
|
+
get env(): Record<string, string>;
|
|
1559
|
+
private assertActive;
|
|
1560
|
+
/** Tear down every process and release the Nodepod instance. */
|
|
1561
|
+
dispose(): void;
|
|
1562
|
+
get isDisposed(): boolean;
|
|
1563
|
+
}
|
|
1564
|
+
/** Boot a container. The one function most callers need. */
|
|
1565
|
+
declare function createContainer(opts?: ContainerOptions): Promise<Container>;
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* An interactive terminal on top of a `Session`.
|
|
1569
|
+
*
|
|
1570
|
+
* Deliberately transport-agnostic: feed it keystrokes with `input()` and it
|
|
1571
|
+
* calls `write()` with what should appear on screen. That works equally well
|
|
1572
|
+
* for a real TTY (the `sandboxedjs` CLI) and for xterm.js in a browser app.
|
|
1573
|
+
*
|
|
1574
|
+
* Provides line editing, history, tab completion over commands and paths,
|
|
1575
|
+
* multi-line continuation for unfinished commands, and the usual control keys.
|
|
1576
|
+
*/
|
|
1577
|
+
|
|
1578
|
+
interface TerminalOptions {
|
|
1579
|
+
/** Called with text to display. */
|
|
1580
|
+
write(data: string): void;
|
|
1581
|
+
columns?: number;
|
|
1582
|
+
rows?: number;
|
|
1583
|
+
/** Overrides `$PS1` when provided. */
|
|
1584
|
+
prompt?: (session: Session) => string;
|
|
1585
|
+
/** Print `/etc/motd` when the terminal starts. Default true. */
|
|
1586
|
+
motd?: boolean;
|
|
1587
|
+
/** Invoked after the shell exits. */
|
|
1588
|
+
onExit?: (code: number) => void;
|
|
1589
|
+
}
|
|
1590
|
+
declare class Terminal {
|
|
1591
|
+
private readonly session;
|
|
1592
|
+
private readonly opts;
|
|
1593
|
+
private buffer;
|
|
1594
|
+
private cursor;
|
|
1595
|
+
private pending;
|
|
1596
|
+
private historyIndex;
|
|
1597
|
+
private savedLine;
|
|
1598
|
+
private running;
|
|
1599
|
+
private closed;
|
|
1600
|
+
private escapeBuffer;
|
|
1601
|
+
private currentStdin;
|
|
1602
|
+
private exitCode;
|
|
1603
|
+
columns: number;
|
|
1604
|
+
rows: number;
|
|
1605
|
+
constructor(session: Session, opts: TerminalOptions);
|
|
1606
|
+
/** Print the banner and the first prompt. */
|
|
1607
|
+
start(): void;
|
|
1608
|
+
resize(columns: number, rows: number): void;
|
|
1609
|
+
/** Feed raw keystrokes. */
|
|
1610
|
+
input(data: string): void;
|
|
1611
|
+
close(): void;
|
|
1612
|
+
get isClosed(): boolean;
|
|
1613
|
+
private write;
|
|
1614
|
+
private promptText;
|
|
1615
|
+
private writePrompt;
|
|
1616
|
+
/** Repaint the current line after an edit. */
|
|
1617
|
+
private redraw;
|
|
1618
|
+
private key;
|
|
1619
|
+
private handleEscape;
|
|
1620
|
+
private recallHistory;
|
|
1621
|
+
private complete;
|
|
1622
|
+
private completeCommand;
|
|
1623
|
+
private completePath;
|
|
1624
|
+
private submit;
|
|
1625
|
+
private execute;
|
|
1626
|
+
}
|
|
1627
|
+
/** Expand a `PS1`-style prompt string. */
|
|
1628
|
+
declare function expandPrompt(format: string, session: Session): string;
|
|
1629
|
+
|
|
1630
|
+
/** POSIX signal numbers, names and default dispositions. */
|
|
1631
|
+
declare const SIGNALS: Record<string, number>;
|
|
1632
|
+
declare const SIGNAL_NAMES: Record<number, string>;
|
|
1633
|
+
/**
|
|
1634
|
+
* Normalise anything `kill` might be handed — `9`, `TERM`, `SIGTERM`, `-9` —
|
|
1635
|
+
* into a canonical `SIGxxx` name. Returns null when unrecognised.
|
|
1636
|
+
*/
|
|
1637
|
+
declare function normalizeSignal(spec: string | number): string | null;
|
|
1638
|
+
/** The `$?` value a shell reports for a process killed by `sig`. */
|
|
1639
|
+
declare function exitCodeForSignal(sig: string): number;
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* The identity the container reports for itself.
|
|
1643
|
+
*
|
|
1644
|
+
* The release string deliberately matches what Nodepod's `os.release()` returns
|
|
1645
|
+
* inside a spawned Node process, so `uname -r` and
|
|
1646
|
+
* `node -p "os.release()"` agree.
|
|
1647
|
+
*/
|
|
1648
|
+
declare const KERNEL_NAME = "Linux";
|
|
1649
|
+
declare const KERNEL_RELEASE = "5.10.0";
|
|
1650
|
+
declare const OS_RELEASE = "PRETTY_NAME=\"SandboxedJS 1.0 (nodepod)\"\nNAME=\"SandboxedJS\"\nVERSION_ID=\"1.0\"\nVERSION=\"1.0 (nodepod)\"\nVERSION_CODENAME=nodepod\nID=sandboxedjs\nID_LIKE=debian\nHOME_URL=\"https://github.com/R1ck404/Nodepod\"\nSUPPORT_URL=\"https://www.npmjs.com/package/sandboxedjs\"\n";
|
|
1651
|
+
interface UnameInfo {
|
|
1652
|
+
sysname: string;
|
|
1653
|
+
nodename: string;
|
|
1654
|
+
release: string;
|
|
1655
|
+
version: string;
|
|
1656
|
+
machine: string;
|
|
1657
|
+
processor: string;
|
|
1658
|
+
hardwarePlatform: string;
|
|
1659
|
+
operatingSystem: string;
|
|
1660
|
+
}
|
|
1661
|
+
declare function unameInfo(hostname: string): UnameInfo;
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Linux errno numbers and the `SysError` type every syscall-ish helper throws.
|
|
1665
|
+
*
|
|
1666
|
+
* The numbers match Linux x86-64 so that programs which print `err.errno`
|
|
1667
|
+
* (or read `$?` after a failed syscall) see the values they would on a real box.
|
|
1668
|
+
*/
|
|
1669
|
+
declare const ERRNO: {
|
|
1670
|
+
readonly EPERM: 1;
|
|
1671
|
+
readonly ENOENT: 2;
|
|
1672
|
+
readonly ESRCH: 3;
|
|
1673
|
+
readonly EINTR: 4;
|
|
1674
|
+
readonly EIO: 5;
|
|
1675
|
+
readonly ENXIO: 6;
|
|
1676
|
+
readonly E2BIG: 7;
|
|
1677
|
+
readonly ENOEXEC: 8;
|
|
1678
|
+
readonly EBADF: 9;
|
|
1679
|
+
readonly ECHILD: 10;
|
|
1680
|
+
readonly EAGAIN: 11;
|
|
1681
|
+
readonly ENOMEM: 12;
|
|
1682
|
+
readonly EACCES: 13;
|
|
1683
|
+
readonly EFAULT: 14;
|
|
1684
|
+
readonly ENOTBLK: 15;
|
|
1685
|
+
readonly EBUSY: 16;
|
|
1686
|
+
readonly EEXIST: 17;
|
|
1687
|
+
readonly EXDEV: 18;
|
|
1688
|
+
readonly ENODEV: 19;
|
|
1689
|
+
readonly ENOTDIR: 20;
|
|
1690
|
+
readonly EISDIR: 21;
|
|
1691
|
+
readonly EINVAL: 22;
|
|
1692
|
+
readonly ENFILE: 23;
|
|
1693
|
+
readonly EMFILE: 24;
|
|
1694
|
+
readonly ENOTTY: 25;
|
|
1695
|
+
readonly ETXTBSY: 26;
|
|
1696
|
+
readonly EFBIG: 27;
|
|
1697
|
+
readonly ENOSPC: 28;
|
|
1698
|
+
readonly ESPIPE: 29;
|
|
1699
|
+
readonly EROFS: 30;
|
|
1700
|
+
readonly EMLINK: 31;
|
|
1701
|
+
readonly EPIPE: 32;
|
|
1702
|
+
readonly EDOM: 33;
|
|
1703
|
+
readonly ERANGE: 34;
|
|
1704
|
+
readonly EDEADLK: 35;
|
|
1705
|
+
readonly ENAMETOOLONG: 36;
|
|
1706
|
+
readonly ENOLCK: 37;
|
|
1707
|
+
readonly ENOSYS: 38;
|
|
1708
|
+
readonly ENOTEMPTY: 39;
|
|
1709
|
+
readonly ELOOP: 40;
|
|
1710
|
+
readonly ENOMSG: 42;
|
|
1711
|
+
readonly ENOTSOCK: 88;
|
|
1712
|
+
readonly EADDRINUSE: 98;
|
|
1713
|
+
readonly EADDRNOTAVAIL: 99;
|
|
1714
|
+
readonly ENETDOWN: 100;
|
|
1715
|
+
readonly ECONNRESET: 104;
|
|
1716
|
+
readonly ENOTCONN: 107;
|
|
1717
|
+
readonly ETIMEDOUT: 110;
|
|
1718
|
+
readonly ECONNREFUSED: 111;
|
|
1719
|
+
readonly EHOSTUNREACH: 113;
|
|
1720
|
+
readonly ENOTSUP: 95;
|
|
1721
|
+
};
|
|
1722
|
+
type ErrnoCode = keyof typeof ERRNO;
|
|
1723
|
+
/** An error carrying the same shape Node uses for `fs` failures. */
|
|
1724
|
+
declare class SysError extends Error {
|
|
1725
|
+
readonly code: ErrnoCode;
|
|
1726
|
+
readonly errno: number;
|
|
1727
|
+
readonly syscall: string;
|
|
1728
|
+
readonly path?: string;
|
|
1729
|
+
readonly dest?: string;
|
|
1730
|
+
constructor(code: ErrnoCode, syscall: string, path?: string, dest?: string);
|
|
1731
|
+
/** The message a coreutil prints: `ls: /nope: No such file or directory`. */
|
|
1732
|
+
toUserMessage(program: string, subject?: string | undefined): string;
|
|
1733
|
+
}
|
|
1734
|
+
declare function isSysError(e: unknown): e is SysError;
|
|
1735
|
+
declare function strerror(code: ErrnoCode): string;
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Pattern matching: `fnmatch(3)` semantics for `case`/`find -name`, plus the
|
|
1739
|
+
* pathname expansion the shell performs on unquoted words.
|
|
1740
|
+
*/
|
|
1741
|
+
|
|
1742
|
+
interface MatchOptions {
|
|
1743
|
+
/** `*` and `?` stop at `/` (shell pathname expansion). */
|
|
1744
|
+
pathname?: boolean;
|
|
1745
|
+
/** Match leading dots with wildcards (shell `dotglob`). */
|
|
1746
|
+
dot?: boolean;
|
|
1747
|
+
/** Case-insensitive comparison. */
|
|
1748
|
+
nocase?: boolean;
|
|
1749
|
+
/** Enable ksh-style `?(a|b)`, `*(a|b)`, `+(...)`, `@(...)`, `!(...)`. */
|
|
1750
|
+
extglob?: boolean;
|
|
1751
|
+
}
|
|
1752
|
+
declare function globToRegex(pattern: string, opts?: MatchOptions): RegExp;
|
|
1753
|
+
/** `fnmatch(3)`. */
|
|
1754
|
+
declare function fnmatch(pattern: string, str: string, opts?: MatchOptions): boolean;
|
|
1755
|
+
/** True when the word contains an unescaped glob metacharacter. */
|
|
1756
|
+
declare function hasMagic(word: string, extglob?: boolean): boolean;
|
|
1757
|
+
interface GlobOptions extends MatchOptions {
|
|
1758
|
+
cwd?: string;
|
|
1759
|
+
cred?: Cred;
|
|
1760
|
+
/** Only return paths that are directories (trailing `/` in the pattern). */
|
|
1761
|
+
onlyDirs?: boolean;
|
|
1762
|
+
/** Cap on results, to keep a runaway `/**` from exhausting memory. */
|
|
1763
|
+
limit?: number;
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Pathname expansion against the container filesystem. Returns absolute paths
|
|
1767
|
+
* when the pattern is absolute, otherwise paths relative to `cwd` — matching
|
|
1768
|
+
* how the shell substitutes the results back into the command line.
|
|
1769
|
+
*/
|
|
1770
|
+
declare function glob(vfs: Vfs, pattern: string, opts?: GlobOptions): string[];
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Shell tokenizer.
|
|
1774
|
+
*
|
|
1775
|
+
* Words are kept as *raw* text with their quotes intact — expansion happens
|
|
1776
|
+
* later, in `expand.ts`, which is the only stage that needs to distinguish
|
|
1777
|
+
* `"$x"` from `$x`. The lexer's job is to find word boundaries correctly in the
|
|
1778
|
+
* presence of quoting, `$(...)`, backticks and `${...}`, and to pull here-doc
|
|
1779
|
+
* bodies out of the stream at the right moment.
|
|
1780
|
+
*/
|
|
1781
|
+
type TokenType = "word" | "op" | "newline" | "eof" | "io_number";
|
|
1782
|
+
interface HeredocInfo {
|
|
1783
|
+
tag: string;
|
|
1784
|
+
/** `<<-` strips leading tabs from the body and the delimiter line. */
|
|
1785
|
+
stripTabs: boolean;
|
|
1786
|
+
/** A quoted tag disables expansion inside the body. */
|
|
1787
|
+
quoted: boolean;
|
|
1788
|
+
body: string;
|
|
1789
|
+
}
|
|
1790
|
+
interface Token {
|
|
1791
|
+
type: TokenType;
|
|
1792
|
+
value: string;
|
|
1793
|
+
pos: number;
|
|
1794
|
+
/** Attached to the `<<` operator token once the body has been collected. */
|
|
1795
|
+
heredoc?: HeredocInfo;
|
|
1796
|
+
}
|
|
1797
|
+
declare class ShellSyntaxError extends Error {
|
|
1798
|
+
readonly pos: number;
|
|
1799
|
+
constructor(message: string, pos: number);
|
|
1800
|
+
}
|
|
1801
|
+
/** Thrown when input ends mid-construct, so a REPL can ask for another line. */
|
|
1802
|
+
declare class IncompleteInputError extends ShellSyntaxError {
|
|
1803
|
+
constructor(message: string, pos: number);
|
|
1804
|
+
}
|
|
1805
|
+
declare class Lexer {
|
|
1806
|
+
private readonly src;
|
|
1807
|
+
private pos;
|
|
1808
|
+
private readonly tokens;
|
|
1809
|
+
/** Here-docs whose bodies are collected when the current line ends. */
|
|
1810
|
+
private pendingHeredocs;
|
|
1811
|
+
constructor(src: string);
|
|
1812
|
+
static tokenize(src: string): Token[];
|
|
1813
|
+
run(): Token[];
|
|
1814
|
+
private push;
|
|
1815
|
+
/** True when `prev` ends exactly where the current operator begins. */
|
|
1816
|
+
private adjacent;
|
|
1817
|
+
private skipBlanks;
|
|
1818
|
+
private matchOperator;
|
|
1819
|
+
private readWord;
|
|
1820
|
+
private readDoubleQuoted;
|
|
1821
|
+
/**
|
|
1822
|
+
* Consume a balanced `$( ... )` or `${ ... }`, honouring nesting and quotes
|
|
1823
|
+
* inside. `prefix` is emitted before the opening delimiter.
|
|
1824
|
+
*/
|
|
1825
|
+
private readBalanced;
|
|
1826
|
+
private skipDoubleQuoted;
|
|
1827
|
+
private findBacktickEnd;
|
|
1828
|
+
/**
|
|
1829
|
+
* Called right after a newline: read the bodies of every here-doc whose
|
|
1830
|
+
* operator appeared on the line just finished.
|
|
1831
|
+
*/
|
|
1832
|
+
private collectHeredocs;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Recursive-descent parser for the shell grammar.
|
|
1837
|
+
*
|
|
1838
|
+
* Covers the POSIX command language plus the bash extensions that scripts in
|
|
1839
|
+
* the wild actually depend on: `[[ ]]`, `(( ))`, `function name { }`,
|
|
1840
|
+
* `for ((;;))`, `select`, `|&`, `&>`, `<<<`, and `;&`/`;;&` in `case`.
|
|
1841
|
+
*/
|
|
1842
|
+
|
|
1843
|
+
declare function parse(source: string): Node;
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Arithmetic expansion — the `$(( ... ))` and `(( ... ))` evaluator.
|
|
1847
|
+
*
|
|
1848
|
+
* Implements the C-like operator set bash supports, including assignment,
|
|
1849
|
+
* pre/post increment, the ternary, comma, and `base#digits` literals. Values
|
|
1850
|
+
* are integers; division by zero is an error, as it is in bash.
|
|
1851
|
+
*/
|
|
1852
|
+
interface ArithScope {
|
|
1853
|
+
get(name: string): string | undefined;
|
|
1854
|
+
set(name: string, value: string): void;
|
|
1855
|
+
}
|
|
1856
|
+
declare class ArithError extends Error {
|
|
1857
|
+
constructor(message: string);
|
|
1858
|
+
}
|
|
1859
|
+
declare function evalArith(expression: string, scope: ArithScope): number;
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* The userland: every program installed into `$PATH` at boot.
|
|
1863
|
+
*
|
|
1864
|
+
* Each command is a real file under `/bin`, `/sbin` or `/usr/bin`, so `which`,
|
|
1865
|
+
* `ls -l /usr/bin` and shebang dispatch all behave the way they do on a real
|
|
1866
|
+
* system.
|
|
1867
|
+
*/
|
|
1868
|
+
|
|
1869
|
+
/** Every command, in installation order. */
|
|
1870
|
+
declare function allCommands(): Command[];
|
|
1871
|
+
/**
|
|
1872
|
+
* Install the userland into a kernel. Commands declaring their own `path` land
|
|
1873
|
+
* there; everything else goes to `/usr/bin`, with `/bin` and `/sbin` symlinked
|
|
1874
|
+
* the way merged-`/usr` distributions do.
|
|
1875
|
+
*/
|
|
1876
|
+
declare function installUserland(kernel: Kernel): void;
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* The root filesystem image.
|
|
1880
|
+
*
|
|
1881
|
+
* Everything here is a real file in the volume, not a special case in code —
|
|
1882
|
+
* `/etc/passwd` is what `id` reads, `/etc/profile` is what a login shell
|
|
1883
|
+
* sources, and `/etc/os-release` is what `lsb_release` parses. Editing them
|
|
1884
|
+
* inside the container changes behaviour, exactly as it would on a real box.
|
|
1885
|
+
*/
|
|
1886
|
+
|
|
1887
|
+
interface RootfsOptions {
|
|
1888
|
+
hostname?: string;
|
|
1889
|
+
/** Non-root login user created at boot. Pass null for a root-only image. */
|
|
1890
|
+
user?: {
|
|
1891
|
+
name: string;
|
|
1892
|
+
uid?: number;
|
|
1893
|
+
gid?: number;
|
|
1894
|
+
home?: string;
|
|
1895
|
+
shell?: string;
|
|
1896
|
+
} | null;
|
|
1897
|
+
timezone?: string;
|
|
1898
|
+
}
|
|
1899
|
+
declare function buildRootfs(vfs: Vfs, opts?: RootfsOptions): void;
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* The Node.js runtime, backed by Nodepod.
|
|
1903
|
+
*
|
|
1904
|
+
* Nodepod runs the script in an isolated worker over the *same* memory volume
|
|
1905
|
+
* the container's filesystem uses, so `require('fs')` inside a script sees the
|
|
1906
|
+
* files `echo` and `tar` created, and anything the script writes is visible to
|
|
1907
|
+
* the shell afterwards.
|
|
1908
|
+
*
|
|
1909
|
+
* Two gaps in the underlying `spawn` are papered over here:
|
|
1910
|
+
* - only `node` resolves as a command, so everything else is dispatched by our
|
|
1911
|
+
* own kernel rather than being handed to Nodepod's shell (which hangs on an
|
|
1912
|
+
* unknown command);
|
|
1913
|
+
* - the worker's stdin has no end-of-stream signal, so when a pipeline feeds
|
|
1914
|
+
* a script we materialise stdin as a file and install a real stdin stream
|
|
1915
|
+
* over it before the script loads.
|
|
1916
|
+
*/
|
|
1917
|
+
|
|
1918
|
+
declare const NODE_VERSION = "v22.12.0";
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* The Python runtime: MicroPython compiled to WebAssembly, wired to the
|
|
1922
|
+
* container's filesystem, argv, environment and standard streams.
|
|
1923
|
+
*
|
|
1924
|
+
* A fresh interpreter is created per process, which is both correct (no state
|
|
1925
|
+
* leaks between runs) and cheap — the WASM module is compiled once and reused,
|
|
1926
|
+
* so subsequent instantiations take single-digit milliseconds.
|
|
1927
|
+
*/
|
|
1928
|
+
|
|
1929
|
+
declare const PYTHON_VERSION = "3.4.0";
|
|
1930
|
+
/** True when a Python interpreter can be started in this process. */
|
|
1931
|
+
declare function isPythonAvailable(): Promise<boolean>;
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Package managers: `npm`/`npx`/`yarn`/`pnpm` on top of Nodepod's installer,
|
|
1935
|
+
* and an `apt`-shaped front end for the things a container image would ship.
|
|
1936
|
+
*/
|
|
1937
|
+
|
|
1938
|
+
declare const NPM_VERSION = "10.9.0";
|
|
1939
|
+
|
|
1940
|
+
/**
|
|
1941
|
+
* sandboxedjs — a Linux-like container that runs entirely inside Node.js.
|
|
1942
|
+
*
|
|
1943
|
+
* ```ts
|
|
1944
|
+
* import { createContainer } from "sandboxedjs";
|
|
1945
|
+
*
|
|
1946
|
+
* const box = await createContainer({
|
|
1947
|
+
* files: { "/app/server.js": "require('http').createServer(...).listen(3000)" },
|
|
1948
|
+
* });
|
|
1949
|
+
*
|
|
1950
|
+
* await box.exec("ls -la /app");
|
|
1951
|
+
* await box.exec("python3 -c 'print(sum(range(101)))'");
|
|
1952
|
+
* box.spawn("node /app/server.js");
|
|
1953
|
+
* await box.waitForPort(3000);
|
|
1954
|
+
* const res = await box.request(3000, { path: "/" });
|
|
1955
|
+
* ```
|
|
1956
|
+
*/
|
|
1957
|
+
|
|
1958
|
+
export { ArithError, BufferSink, CallbackSink, type Command, CommandRegistry, Container, ContainerFs, type ContainerOptions, type ContextInit, type Cred, type DirEntry, ERRNO, type Env, type ErrnoCode, type ExecContext, type ExecOptions, type ExecResult, FileInput, FileOutput, type GroupEntry, type HttpResponse, IncompleteInputError, type InputStream, type Job, KERNEL_NAME, KERNEL_RELEASE, Kernel, type KernelOptions, type ListeningPort, type MountEntry, NODE_VERSION, NPM_VERSION, type NetInterface, type NetworkOptions, NetworkStack, NullInput, NullOutput, OS_RELEASE, type OutputStream, PYTHON_VERSION, type PasswdEntry, Pipe, Process, type ProcessKind, type ProcessOptions, type ProcessState, ProcessTable, ROOT_CRED, type ResolvedExecutable, type RootfsOptions, type RunOptions, type RunResult, SIGNALS, SIGNAL_NAMES, Session, type SessionInit, type SessionResult, type SessionRunOptions, Shell, ShellExit, type ShellIO, type ShellInit, Lexer as ShellLexer, type ShellOptions, ShellSyntaxError, type SpawnHandle, Stats, type Stdio, SysError, TeeOutput, Terminal, type TerminalOptions, UserDatabase, Variables, Vfs, type VirtualNode, type VirtualProvider, type WriteOptions, allCommands, applyChmod, braceExpand, buildRootfs, builtinNames, captureStdio, createContainer, createContext, createContainer as default, defineCommand, evalArith, exitCodeForSignal, expandPrompt, expandWord, expandWords, fnmatch, formatMode, getBuiltin, glob, globToRegex, hasMagic, installUserland, isBuiltinName, isPythonAvailable, isSysError, makeCred, normalizeSignal, octalMode, parse as parseShell, parseUmask, path as posixPath, resetPidCounter, shellQuote, strerror, unameInfo };
|