@ai-ecoverse/slicc-kernel 1.0.0 → 1.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/README.md +31 -3
- package/dist/index.js +43 -5
- package/dist/index.js.map +3 -3
- package/dist/kernel-worker.js +167 -69
- package/dist/kernel-worker.js.map +4 -4
- package/dist/process-worker.js +1 -0
- package/dist/process-worker.js.map +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,10 +37,30 @@ Runs `argv[0]` with `argv` as its arguments and resolves when it exits. `stdout`
|
|
|
37
37
|
| option | |
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `cwd` | working directory, default `/`; created in OPFS if missing |
|
|
40
|
-
| `env` | extra environment; the defaults are `PATH=/usr/bin:/bin`, `HOME
|
|
40
|
+
| `env` | extra environment; the defaults are `PATH=/usr/bin:/bin`, `HOME=/home`, `PWD=<cwd>` |
|
|
41
41
|
| `stdin` | a string or `Uint8Array`; without it stdin is `/dev/null` |
|
|
42
42
|
| `onStdout`, `onStderr` | called with each chunk of text as it is written |
|
|
43
43
|
|
|
44
|
+
### `kernel.openTerminal(argv, options?) → Promise<Terminal>`
|
|
45
|
+
|
|
46
|
+
Starts `argv` as the session leader on a new terminal and resolves once it is running, or rejects if `argv[0]` cannot be found. Its stdin, stdout and stderr are the terminal, which has a line discipline with echo, canonical mode and `ISIG`, and supports job control, so `kernel.openTerminal(['bash', '-i'])` gives an interactive shell. The API was designed with [slicc-spectrum](https://github.com/ai-ecoverse/slicc-spectrum), a terminal web component, and these names are kept stable.
|
|
47
|
+
|
|
48
|
+
| option | |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `cwd`, `env` | as for `run`; `TERM=xterm-256color` and `COLORTERM=truecolor` are added |
|
|
51
|
+
| `cols`, `rows` | initial size, default 80 × 24, so `$COLUMNS` and `$LINES` are right before the first prompt |
|
|
52
|
+
| `onData` | called with each chunk of output as raw bytes (escape sequences included) |
|
|
53
|
+
|
|
54
|
+
| `Terminal` | |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `pid` | the session leader's pid |
|
|
57
|
+
| `onData` | the output listener; output that arrives while it is `null` is kept and delivered when one is set |
|
|
58
|
+
| `write(data)` | types a string or bytes into the terminal; with `ISIG` on, `^C`, `^Z` and `^\` signal the foreground process group |
|
|
59
|
+
| `resize(cols, rows)` | sets the window size (`TIOCGWINSZ`) and sends `SIGWINCH` to the foreground process group |
|
|
60
|
+
| `signal(name)` | sends a signal (`'SIGINT'`, `'SIGTSTP'`, `'SIGQUIT'`, `'SIGHUP'`, …) to the foreground process group, regardless of termios |
|
|
61
|
+
| `exited` | resolves with the session leader's exit status (`128 + n` if killed by signal `n`) |
|
|
62
|
+
| `close()` | hangs up: `SIGHUP` to the foreground process group, and reads from the terminal return end-of-file |
|
|
63
|
+
|
|
44
64
|
### `kernel.terminate()`
|
|
45
65
|
|
|
46
66
|
Stops the kernel worker and every process. Pending and later calls reject.
|
|
@@ -53,14 +73,18 @@ Commands come from installed packages in npm's `node_modules` layout: every `<mo
|
|
|
53
73
|
{ "slicc": { "abi": "emscripten", "commands": { "bash": { "glue": "bin/bash", "wasm": "bin/bash.wasm" } } } }
|
|
54
74
|
```
|
|
55
75
|
|
|
56
|
-
Entries may also set `argv0`, `args` and `env` (with `${package}` expanded to the package directory), and a package-wide `slicc.env`. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, and a script through its `#!` line (including `#!/usr/bin/env name`). The catalog is re-read at the start of every `run`, so packages installed in between are picked up.
|
|
76
|
+
Entries may also set `argv0`, `args` and `env` (with `${package}` expanded to the package directory), and a package-wide `slicc.env`. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. The catalog is re-read at the start of every `run`, so packages installed in between are picked up.
|
|
57
77
|
|
|
58
78
|
## Filesystem
|
|
59
79
|
|
|
60
|
-
`/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `/tmp
|
|
80
|
+
`/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. `/dev`, `/proc` and anything created directly in `/` while a process runs live in that process's memory unless it already exists in OPFS. File contents are buffered per open file and written back on close, `fsync` and exit; metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
|
|
61
81
|
|
|
62
82
|
OPFS stores no POSIX metadata, so modes are synthesized: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. `chmod` and `utime` are remembered for the lifetime of the kernel worker. Symlinks are not supported (`ENOSYS`). Directories are renamed with `FileSystemHandle.move()` where available, else by copy and delete.
|
|
63
83
|
|
|
84
|
+
Reads retry briefly when a concurrent write has invalidated the OPFS file snapshot (`NotReadableError`).
|
|
85
|
+
|
|
86
|
+
The filesystem is our own rather than [ZenFS](https://github.com/zen-fs/core) (which SLICC uses), because OPFS stays the single source of truth: other writers, such as the BIOS installing packages or a page writing files, need no index to stay consistent with, and nothing is preloaded into memory at mount. In exchange there are no persistent POSIX modes, times, inode numbers or symlinks.
|
|
87
|
+
|
|
64
88
|
## What is in here
|
|
65
89
|
|
|
66
90
|
The kernel is ported from SLICC's `packages/webapp/src/kernel/` with the browser-specific VFS replaced by an OPFS one:
|
|
@@ -73,6 +97,10 @@ The kernel is ported from SLICC's `packages/webapp/src/kernel/` with the browser
|
|
|
73
97
|
|
|
74
98
|
SLICC's networking (sockets, the TLS proxy) and WASI support are not included.
|
|
75
99
|
|
|
100
|
+
## Installing from git
|
|
101
|
+
|
|
102
|
+
`npm install github:ai-ecoverse/slicc-kernel#<sha>` works: the `prepare` script builds `dist/`.
|
|
103
|
+
|
|
76
104
|
## Development
|
|
77
105
|
|
|
78
106
|
```sh
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ async function createKernel(options = {}) {
|
|
|
20
20
|
const call2 = pending.get(data.id);
|
|
21
21
|
if (!call2) return;
|
|
22
22
|
if (data.fd !== void 0) return call2.output?.(data.fd, data.bytes);
|
|
23
|
+
if (data.started !== void 0) return call2.started?.(data.started);
|
|
23
24
|
pending.delete(data.id);
|
|
24
25
|
if (data.error !== void 0) call2.reject(new Error(data.error));
|
|
25
26
|
else call2.resolve(data.result);
|
|
@@ -28,13 +29,49 @@ async function createKernel(options = {}) {
|
|
|
28
29
|
event.preventDefault();
|
|
29
30
|
fail(new Error(`slicc-kernel worker failed: ${event.message}`));
|
|
30
31
|
});
|
|
31
|
-
function
|
|
32
|
-
if (failure) return Promise.reject(failure);
|
|
32
|
+
function request(req, handlers = {}) {
|
|
33
33
|
const id = ++nextId;
|
|
34
|
-
return
|
|
35
|
-
|
|
34
|
+
if (failure) return { id, done: Promise.reject(failure) };
|
|
35
|
+
const done = new Promise((resolve, reject) => {
|
|
36
|
+
pending.set(id, { resolve, reject, ...handlers });
|
|
36
37
|
worker.postMessage({ ...req, id });
|
|
37
38
|
});
|
|
39
|
+
return { id, done };
|
|
40
|
+
}
|
|
41
|
+
const call = (req, handlers) => request(req, handlers).done;
|
|
42
|
+
async function openTerminal(argv, opts = {}) {
|
|
43
|
+
const { onData, ...options2 } = opts;
|
|
44
|
+
let listener = onData ?? null;
|
|
45
|
+
const backlog = [];
|
|
46
|
+
const started = Promise.withResolvers();
|
|
47
|
+
const { id, done } = request(
|
|
48
|
+
{ op: "open-terminal", argv, options: options2 },
|
|
49
|
+
{
|
|
50
|
+
output: (_fd, bytes) => listener ? listener(bytes) : backlog.push(bytes),
|
|
51
|
+
started: started.resolve
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
done.catch(started.reject);
|
|
55
|
+
const pid = await started.promise;
|
|
56
|
+
const send = (action) => void call({ op: "terminal", terminal: id, ...action }).catch(() => void 0);
|
|
57
|
+
return {
|
|
58
|
+
pid,
|
|
59
|
+
exited: done,
|
|
60
|
+
get onData() {
|
|
61
|
+
return listener;
|
|
62
|
+
},
|
|
63
|
+
set onData(next) {
|
|
64
|
+
listener = next;
|
|
65
|
+
if (next) for (const bytes of backlog.splice(0)) next(bytes);
|
|
66
|
+
},
|
|
67
|
+
write: (data) => send({
|
|
68
|
+
action: "write",
|
|
69
|
+
bytes: typeof data === "string" ? new TextEncoder().encode(data) : data
|
|
70
|
+
}),
|
|
71
|
+
resize: (cols, rows) => send({ action: "resize", cols, rows }),
|
|
72
|
+
signal: (name) => send({ action: "signal", signal: name }),
|
|
73
|
+
close: () => send({ action: "close" })
|
|
74
|
+
};
|
|
38
75
|
}
|
|
39
76
|
await call({
|
|
40
77
|
op: "init",
|
|
@@ -49,7 +86,7 @@ async function createKernel(options = {}) {
|
|
|
49
86
|
const input = typeof stdin === "string" ? new TextEncoder().encode(stdin) : stdin;
|
|
50
87
|
const result = await call(
|
|
51
88
|
{ op: "run", argv, options: { ...rest, ...input ? { stdin: input } : {} } },
|
|
52
|
-
(fd, bytes) => streams[fd](bytes)
|
|
89
|
+
{ output: (fd, bytes) => streams[fd](bytes) }
|
|
53
90
|
);
|
|
54
91
|
const decoder = new TextDecoder();
|
|
55
92
|
return {
|
|
@@ -58,6 +95,7 @@ async function createKernel(options = {}) {
|
|
|
58
95
|
stderr: decoder.decode(result.stderr)
|
|
59
96
|
};
|
|
60
97
|
},
|
|
98
|
+
openTerminal,
|
|
61
99
|
terminate() {
|
|
62
100
|
fail(new Error("slicc-kernel terminated"));
|
|
63
101
|
worker.terminate();
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["import type { KernelCall } from './serve.ts';\n\nexport interface KernelOptions {\n root?: FileSystemDirectoryHandle;\n modules?: string;\n env?: Record<string, string>;\n worker?: string | URL;\n}\n\nexport interface RunOptions {\n cwd?: string;\n env?: Record<string, string>;\n stdin?: string | Uint8Array;\n onStdout?: (text: string) => void;\n onStderr?: (text: string) => void;\n}\n\nexport interface RunResult {\n status: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface Kernel {\n run(argv: string[], options?: RunOptions): Promise<RunResult>;\n terminate(): void;\n}\n\ninterface Reply {\n id: number;\n result?: unknown;\n error?: string;\n fd?: 1 | 2;\n bytes?: Uint8Array;\n}\n\ninterface Pending {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n output?: (fd: 1 | 2, bytes: Uint8Array) => void;\n}\n\nconst ISOLATION =\n 'slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)';\n\nfunction streamer(callback: ((text: string) => void) | undefined) {\n const decoder = new TextDecoder();\n return (bytes: Uint8Array) => callback?.(decoder.decode(bytes, { stream: true }));\n}\n\nexport async function createKernel(options: KernelOptions = {}): Promise<Kernel> {\n if (!globalThis.crossOriginIsolated) throw new Error(ISOLATION);\n const url = options.worker ?? new URL('./kernel-worker.js', import.meta.url);\n const worker = new Worker(url, { type: 'module', name: 'slicc-kernel' });\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let failure: Error | undefined;\n\n const fail = (error: Error) => {\n failure = error;\n for (const call of pending.values()) call.reject(error);\n pending.clear();\n };\n worker.addEventListener('message', ({ data }: MessageEvent<Reply>) => {\n const call = pending.get(data.id);\n if (!call) return;\n if (data.fd !== undefined) return call.output?.(data.fd, data.bytes as Uint8Array);\n pending.delete(data.id);\n if (data.error !== undefined) call.reject(new Error(data.error));\n else call.resolve(data.result);\n });\n worker.addEventListener('error', (event) => {\n event.preventDefault();\n fail(new Error(`slicc-kernel worker failed: ${event.message}`));\n });\n\n function
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": ["call"]
|
|
4
|
+
"sourcesContent": ["import type { KernelCall, TerminalAction } from './serve.ts';\n\nexport interface KernelOptions {\n root?: FileSystemDirectoryHandle;\n modules?: string;\n env?: Record<string, string>;\n worker?: string | URL;\n}\n\nexport interface RunOptions {\n cwd?: string;\n env?: Record<string, string>;\n stdin?: string | Uint8Array;\n onStdout?: (text: string) => void;\n onStderr?: (text: string) => void;\n}\n\nexport interface RunResult {\n status: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface Kernel {\n run(argv: string[], options?: RunOptions): Promise<RunResult>;\n openTerminal(argv: string[], options?: TerminalOptions): Promise<Terminal>;\n terminate(): void;\n}\n\nexport interface TerminalOptions {\n cwd?: string;\n env?: Record<string, string>;\n cols?: number;\n rows?: number;\n onData?: (bytes: Uint8Array) => void;\n}\n\nexport type TerminalSignal =\n | 'SIGINT'\n | 'SIGTSTP'\n | 'SIGQUIT'\n | 'SIGHUP'\n | 'SIGTERM'\n | 'SIGKILL'\n | 'SIGCONT';\n\nexport interface Terminal {\n readonly pid: number;\n readonly exited: Promise<number>;\n onData: ((bytes: Uint8Array) => void) | null;\n write(data: string | Uint8Array): void;\n resize(cols: number, rows: number): void;\n signal(name: TerminalSignal): void;\n close(): void;\n}\n\ninterface Reply {\n id: number;\n result?: unknown;\n error?: string;\n fd?: 1 | 2;\n bytes?: Uint8Array;\n started?: number;\n}\n\ninterface Pending {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n output?: (fd: 1 | 2, bytes: Uint8Array) => void;\n started?: (pid: number) => void;\n}\n\ninterface Handlers {\n output?: Pending['output'];\n started?: Pending['started'];\n}\n\nconst ISOLATION =\n 'slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)';\n\nfunction streamer(callback: ((text: string) => void) | undefined) {\n const decoder = new TextDecoder();\n return (bytes: Uint8Array) => callback?.(decoder.decode(bytes, { stream: true }));\n}\n\nexport async function createKernel(options: KernelOptions = {}): Promise<Kernel> {\n if (!globalThis.crossOriginIsolated) throw new Error(ISOLATION);\n const url = options.worker ?? new URL('./kernel-worker.js', import.meta.url);\n const worker = new Worker(url, { type: 'module', name: 'slicc-kernel' });\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let failure: Error | undefined;\n\n const fail = (error: Error) => {\n failure = error;\n for (const call of pending.values()) call.reject(error);\n pending.clear();\n };\n worker.addEventListener('message', ({ data }: MessageEvent<Reply>) => {\n const call = pending.get(data.id);\n if (!call) return;\n if (data.fd !== undefined) return call.output?.(data.fd, data.bytes as Uint8Array);\n if (data.started !== undefined) return call.started?.(data.started);\n pending.delete(data.id);\n if (data.error !== undefined) call.reject(new Error(data.error));\n else call.resolve(data.result);\n });\n worker.addEventListener('error', (event) => {\n event.preventDefault();\n fail(new Error(`slicc-kernel worker failed: ${event.message}`));\n });\n\n function request(\n req: KernelCall,\n handlers: Handlers = {}\n ): { id: number; done: Promise<unknown> } {\n const id = ++nextId;\n if (failure) return { id, done: Promise.reject(failure) };\n const done = new Promise((resolve, reject) => {\n pending.set(id, { resolve, reject, ...handlers });\n worker.postMessage({ ...req, id });\n });\n return { id, done };\n }\n\n const call = (req: KernelCall, handlers?: Handlers) => request(req, handlers).done;\n\n async function openTerminal(argv: string[], opts: TerminalOptions = {}): Promise<Terminal> {\n const { onData, ...options } = opts;\n let listener = onData ?? null;\n const backlog: Uint8Array[] = [];\n const started = Promise.withResolvers<number>();\n const { id, done } = request(\n { op: 'open-terminal', argv, options },\n {\n output: (_fd, bytes) => (listener ? listener(bytes) : backlog.push(bytes)),\n started: started.resolve,\n }\n );\n done.catch(started.reject);\n const pid = await started.promise;\n const send = (action: TerminalAction) =>\n void call({ op: 'terminal', terminal: id, ...action }).catch(() => undefined);\n return {\n pid,\n exited: done as Promise<number>,\n get onData() {\n return listener;\n },\n set onData(next) {\n listener = next;\n if (next) for (const bytes of backlog.splice(0)) next(bytes);\n },\n write: (data) =>\n send({\n action: 'write',\n bytes: typeof data === 'string' ? new TextEncoder().encode(data) : data,\n }),\n resize: (cols, rows) => send({ action: 'resize', cols, rows }),\n signal: (name) => send({ action: 'signal', signal: name }),\n close: () => send({ action: 'close' }),\n };\n }\n\n await call({\n op: 'init',\n ...(options.root ? { root: options.root } : {}),\n ...(options.modules ? { modules: options.modules } : {}),\n ...(options.env ? { env: options.env } : {}),\n });\n\n return {\n async run(argv, runOptions = {}) {\n const { onStdout, onStderr, stdin, ...rest } = runOptions;\n const streams = { 1: streamer(onStdout), 2: streamer(onStderr) };\n const input = typeof stdin === 'string' ? new TextEncoder().encode(stdin) : stdin;\n const result = (await call(\n { op: 'run', argv, options: { ...rest, ...(input ? { stdin: input } : {}) } },\n { output: (fd, bytes) => streams[fd](bytes) }\n )) as { status: number; stdout: Uint8Array; stderr: Uint8Array };\n const decoder = new TextDecoder();\n return {\n status: result.status,\n stdout: decoder.decode(result.stdout),\n stderr: decoder.decode(result.stderr),\n };\n },\n openTerminal,\n terminate() {\n fail(new Error('slicc-kernel terminated'));\n worker.terminate();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["call", "options"]
|
|
7
7
|
}
|
package/dist/kernel-worker.js
CHANGED
|
@@ -19,6 +19,7 @@ function resolvePath(base, path) {
|
|
|
19
19
|
var S_IFDIR = 16384;
|
|
20
20
|
var S_IFREG = 32768;
|
|
21
21
|
var PERM_MASK = 4095;
|
|
22
|
+
var READ_ATTEMPTS = 3;
|
|
22
23
|
var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
|
|
23
24
|
var ERRNO_BY_DOM_ERROR = {
|
|
24
25
|
NotFoundError: "ENOENT",
|
|
@@ -102,7 +103,16 @@ var OpfsFs = class {
|
|
|
102
103
|
async readFileBuffer(path) {
|
|
103
104
|
const handle = await this.handle(path);
|
|
104
105
|
if (handle.kind === "directory") throw fsError("EISDIR", path);
|
|
105
|
-
|
|
106
|
+
for (let attempt = 1; ; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
return new Uint8Array(await (await handle.getFile()).arrayBuffer());
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err?.name !== "NotReadableError" || attempt >= READ_ATTEMPTS) {
|
|
111
|
+
throw translate(err, path);
|
|
112
|
+
}
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 10));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
117
|
async readFile(path) {
|
|
108
118
|
return new TextDecoder().decode(await this.readFileBuffer(path));
|
|
@@ -225,6 +235,61 @@ var OpfsFs = class {
|
|
|
225
235
|
}
|
|
226
236
|
};
|
|
227
237
|
|
|
238
|
+
// src/kernel/signals.ts
|
|
239
|
+
var SIG = {
|
|
240
|
+
HUP: 1,
|
|
241
|
+
INT: 2,
|
|
242
|
+
QUIT: 3,
|
|
243
|
+
ILL: 4,
|
|
244
|
+
TRAP: 5,
|
|
245
|
+
ABRT: 6,
|
|
246
|
+
BUS: 7,
|
|
247
|
+
FPE: 8,
|
|
248
|
+
KILL: 9,
|
|
249
|
+
USR1: 10,
|
|
250
|
+
SEGV: 11,
|
|
251
|
+
USR2: 12,
|
|
252
|
+
PIPE: 13,
|
|
253
|
+
ALRM: 14,
|
|
254
|
+
TERM: 15,
|
|
255
|
+
STKFLT: 16,
|
|
256
|
+
CHLD: 17,
|
|
257
|
+
CONT: 18,
|
|
258
|
+
STOP: 19,
|
|
259
|
+
TSTP: 20,
|
|
260
|
+
TTIN: 21,
|
|
261
|
+
TTOU: 22,
|
|
262
|
+
URG: 23,
|
|
263
|
+
XCPU: 24,
|
|
264
|
+
XFSZ: 25,
|
|
265
|
+
VTALRM: 26,
|
|
266
|
+
PROF: 27,
|
|
267
|
+
WINCH: 28,
|
|
268
|
+
POLL: 29,
|
|
269
|
+
PWR: 30,
|
|
270
|
+
SYS: 31
|
|
271
|
+
};
|
|
272
|
+
var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
|
|
273
|
+
var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
|
|
274
|
+
function defaultAction(sig) {
|
|
275
|
+
if (sig === SIG.CONT) return "continue";
|
|
276
|
+
if (STOPPING.has(sig)) return "stop";
|
|
277
|
+
return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
|
|
278
|
+
}
|
|
279
|
+
function isSignal(sig) {
|
|
280
|
+
return Number.isInteger(sig) && sig >= 1 && sig <= 31;
|
|
281
|
+
}
|
|
282
|
+
function sigbit(sig) {
|
|
283
|
+
return 1 << sig;
|
|
284
|
+
}
|
|
285
|
+
var SIGNAL_BY_NAME = {
|
|
286
|
+
SIGINT: SIG.INT,
|
|
287
|
+
SIGTERM: SIG.TERM,
|
|
288
|
+
SIGKILL: SIG.KILL,
|
|
289
|
+
SIGSTOP: SIG.STOP,
|
|
290
|
+
SIGCONT: SIG.CONT
|
|
291
|
+
};
|
|
292
|
+
|
|
228
293
|
// src/commands.ts
|
|
229
294
|
var NAME = /^[A-Za-z0-9._+-]+$/;
|
|
230
295
|
var ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -1163,61 +1228,6 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1163
1228
|
};
|
|
1164
1229
|
}
|
|
1165
1230
|
|
|
1166
|
-
// src/kernel/signals.ts
|
|
1167
|
-
var SIG = {
|
|
1168
|
-
HUP: 1,
|
|
1169
|
-
INT: 2,
|
|
1170
|
-
QUIT: 3,
|
|
1171
|
-
ILL: 4,
|
|
1172
|
-
TRAP: 5,
|
|
1173
|
-
ABRT: 6,
|
|
1174
|
-
BUS: 7,
|
|
1175
|
-
FPE: 8,
|
|
1176
|
-
KILL: 9,
|
|
1177
|
-
USR1: 10,
|
|
1178
|
-
SEGV: 11,
|
|
1179
|
-
USR2: 12,
|
|
1180
|
-
PIPE: 13,
|
|
1181
|
-
ALRM: 14,
|
|
1182
|
-
TERM: 15,
|
|
1183
|
-
STKFLT: 16,
|
|
1184
|
-
CHLD: 17,
|
|
1185
|
-
CONT: 18,
|
|
1186
|
-
STOP: 19,
|
|
1187
|
-
TSTP: 20,
|
|
1188
|
-
TTIN: 21,
|
|
1189
|
-
TTOU: 22,
|
|
1190
|
-
URG: 23,
|
|
1191
|
-
XCPU: 24,
|
|
1192
|
-
XFSZ: 25,
|
|
1193
|
-
VTALRM: 26,
|
|
1194
|
-
PROF: 27,
|
|
1195
|
-
WINCH: 28,
|
|
1196
|
-
POLL: 29,
|
|
1197
|
-
PWR: 30,
|
|
1198
|
-
SYS: 31
|
|
1199
|
-
};
|
|
1200
|
-
var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
|
|
1201
|
-
var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
|
|
1202
|
-
function defaultAction(sig) {
|
|
1203
|
-
if (sig === SIG.CONT) return "continue";
|
|
1204
|
-
if (STOPPING.has(sig)) return "stop";
|
|
1205
|
-
return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
|
|
1206
|
-
}
|
|
1207
|
-
function isSignal(sig) {
|
|
1208
|
-
return Number.isInteger(sig) && sig >= 1 && sig <= 31;
|
|
1209
|
-
}
|
|
1210
|
-
function sigbit(sig) {
|
|
1211
|
-
return 1 << sig;
|
|
1212
|
-
}
|
|
1213
|
-
var SIGNAL_BY_NAME = {
|
|
1214
|
-
SIGINT: SIG.INT,
|
|
1215
|
-
SIGTERM: SIG.TERM,
|
|
1216
|
-
SIGKILL: SIG.KILL,
|
|
1217
|
-
SIGSTOP: SIG.STOP,
|
|
1218
|
-
SIGCONT: SIG.CONT
|
|
1219
|
-
};
|
|
1220
|
-
|
|
1221
1231
|
// src/kernel/tty.ts
|
|
1222
1232
|
var ICRNL = 256;
|
|
1223
1233
|
var IUTF8 = 16384;
|
|
@@ -2719,6 +2729,7 @@ var JobTable = class {
|
|
|
2719
2729
|
var COMMAND2 = /^\/(?:usr\/)?bin\/([^/]+)$/;
|
|
2720
2730
|
var SHEBANG_MAX = 256;
|
|
2721
2731
|
var NOT_FOUND = 127;
|
|
2732
|
+
var SHARED_DIRS = ["/tmp", "/home"];
|
|
2722
2733
|
var encoder3 = new TextEncoder();
|
|
2723
2734
|
function baseName(path) {
|
|
2724
2735
|
return path.slice(path.lastIndexOf("/") + 1);
|
|
@@ -2766,6 +2777,7 @@ var Launcher = class {
|
|
|
2766
2777
|
jobs = new JobTable();
|
|
2767
2778
|
ptys = new PtyTable((tty, sig) => this.jobs.signalOwnedForeground(tty, sig));
|
|
2768
2779
|
nextPid = 1e3;
|
|
2780
|
+
terminals = 0;
|
|
2769
2781
|
constructor(options) {
|
|
2770
2782
|
this.base = options.fs;
|
|
2771
2783
|
this.createWorker = options.createWorker;
|
|
@@ -2815,6 +2827,10 @@ var Launcher = class {
|
|
|
2815
2827
|
if (direct) return { target: direct, args: [...direct.prefix ?? [], ...argv.slice(1)] };
|
|
2816
2828
|
return this.interpreted(file, argv, cwd);
|
|
2817
2829
|
}
|
|
2830
|
+
async unrunnable(file, cwd) {
|
|
2831
|
+
const head = await this.base.readFileBuffer(this.fs.resolvePath(cwd, file)).catch(() => null);
|
|
2832
|
+
return head && !(head[0] === 35 && head[1] === 33) ? "ENOEXEC" : "ENOENT";
|
|
2833
|
+
}
|
|
2818
2834
|
module(path) {
|
|
2819
2835
|
return this.base.stat(path).then((st) => {
|
|
2820
2836
|
const key = `${path}:${st.size}:${st.mtime.getTime()}`;
|
|
@@ -2889,7 +2905,7 @@ var Launcher = class {
|
|
|
2889
2905
|
const planned = await this.plan(req.file, req.argv, req.cwd);
|
|
2890
2906
|
if (!planned) {
|
|
2891
2907
|
await fds.closeAll();
|
|
2892
|
-
throw new SpawnError(
|
|
2908
|
+
throw new SpawnError(await this.unrunnable(req.file, req.cwd));
|
|
2893
2909
|
}
|
|
2894
2910
|
const handle = await this.launch(planned, { env: req.env, cwd: req.cwd, fds, report, ppid });
|
|
2895
2911
|
return childHandle(handle);
|
|
@@ -2905,10 +2921,59 @@ var Launcher = class {
|
|
|
2905
2921
|
if (sig !== 0) handle.signal(sig);
|
|
2906
2922
|
return true;
|
|
2907
2923
|
}
|
|
2908
|
-
async
|
|
2924
|
+
async prepare() {
|
|
2925
|
+
for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
|
|
2926
|
+
}
|
|
2927
|
+
environment(cwd, extra) {
|
|
2928
|
+
return { PATH: "/usr/bin:/bin", HOME: "/home", ...this.env, PWD: cwd, ...extra };
|
|
2929
|
+
}
|
|
2930
|
+
async starting(argv, dir) {
|
|
2909
2931
|
this.catalog = void 0;
|
|
2910
|
-
const cwd = this.fs.resolvePath("/",
|
|
2911
|
-
const
|
|
2932
|
+
const cwd = this.fs.resolvePath("/", dir ?? "/");
|
|
2933
|
+
const [file = ""] = argv;
|
|
2934
|
+
await this.base.mkdir(cwd, { recursive: true });
|
|
2935
|
+
return { cwd, file, planned: await this.plan(file, argv, cwd) };
|
|
2936
|
+
}
|
|
2937
|
+
async openTerminal(argv, options) {
|
|
2938
|
+
const { cwd, file, planned } = await this.starting(argv, options.cwd);
|
|
2939
|
+
if (!planned) throw new Error(`${file}: command not found`);
|
|
2940
|
+
let leader = 0;
|
|
2941
|
+
const tty = new KernelTty(
|
|
2942
|
+
{ write: (bytes) => options.onData(bytes.slice()) },
|
|
2943
|
+
(sig) => this.jobs.signalForeground(tty, leader, sig)
|
|
2944
|
+
);
|
|
2945
|
+
tty.name = `/dev/tty${++this.terminals}`;
|
|
2946
|
+
tty.setSize(options.cols ?? 80, options.rows ?? 24);
|
|
2947
|
+
const fds = new FdTable();
|
|
2948
|
+
const stdio = tty.file();
|
|
2949
|
+
fds.installAt(0, stdio);
|
|
2950
|
+
fds.installAt(1, stdio.retain());
|
|
2951
|
+
fds.installAt(2, stdio.retain());
|
|
2952
|
+
const env = this.environment(cwd, {
|
|
2953
|
+
TERM: "xterm-256color",
|
|
2954
|
+
COLORTERM: "truecolor",
|
|
2955
|
+
...options.env
|
|
2956
|
+
});
|
|
2957
|
+
const report = (message) => options.onData(encoder3.encode(`${message}\r
|
|
2958
|
+
`));
|
|
2959
|
+
const handle = await this.launch(planned, { env, cwd, fds, report });
|
|
2960
|
+
leader = handle.pid;
|
|
2961
|
+
void handle.exited.then(() => tty.hangup());
|
|
2962
|
+
return {
|
|
2963
|
+
pid: handle.pid,
|
|
2964
|
+
exited: handle.exited,
|
|
2965
|
+
write: (bytes) => tty.receive(bytes),
|
|
2966
|
+
resize: (cols, rows) => tty.resize(cols, rows),
|
|
2967
|
+
signal: (sig) => this.jobs.signalForeground(tty, leader, sig),
|
|
2968
|
+
close: () => {
|
|
2969
|
+
tty.signalHangup();
|
|
2970
|
+
tty.hangup();
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
async run(argv, options = {}) {
|
|
2975
|
+
const { cwd, file, planned } = await this.starting(argv, options.cwd);
|
|
2976
|
+
const env = this.environment(cwd, options.env);
|
|
2912
2977
|
const out = [];
|
|
2913
2978
|
const err = [];
|
|
2914
2979
|
const collect = (chunks, tee) => (bytes) => {
|
|
@@ -2919,9 +2984,6 @@ var Launcher = class {
|
|
|
2919
2984
|
const stderr = collect(err, options.onStderr);
|
|
2920
2985
|
const report = (message) => stderr(encoder3.encode(`${message}
|
|
2921
2986
|
`));
|
|
2922
|
-
const [file = ""] = argv;
|
|
2923
|
-
await this.base.mkdir(cwd, { recursive: true });
|
|
2924
|
-
const planned = await this.plan(file, argv, cwd);
|
|
2925
2987
|
if (!planned) {
|
|
2926
2988
|
report(`${file}: command not found`);
|
|
2927
2989
|
return { status: NOT_FOUND, stdout: concat2(out), stderr: concat2(err) };
|
|
@@ -2937,24 +2999,60 @@ var Launcher = class {
|
|
|
2937
2999
|
};
|
|
2938
3000
|
|
|
2939
3001
|
// src/serve.ts
|
|
3002
|
+
function signalNumber(name) {
|
|
3003
|
+
const key = name.slice(3);
|
|
3004
|
+
if (!name.startsWith("SIG") || !Object.hasOwn(SIG, key))
|
|
3005
|
+
throw new Error(`unknown signal ${name}`);
|
|
3006
|
+
return SIG[key];
|
|
3007
|
+
}
|
|
3008
|
+
function act(session, req) {
|
|
3009
|
+
if (req.action === "write") session.write(req.bytes);
|
|
3010
|
+
else if (req.action === "resize") session.resize(req.cols, req.rows);
|
|
3011
|
+
else if (req.action === "signal") session.signal(signalNumber(req.signal));
|
|
3012
|
+
else session.close();
|
|
3013
|
+
}
|
|
2940
3014
|
function serveKernel(port, deps) {
|
|
2941
3015
|
let launcher;
|
|
3016
|
+
const terminals = /* @__PURE__ */ new Map();
|
|
2942
3017
|
const reply = (id, body) => port.postMessage({ id, ...body });
|
|
3018
|
+
async function ready2() {
|
|
3019
|
+
if (!launcher) throw new Error("the kernel is not initialized");
|
|
3020
|
+
return launcher;
|
|
3021
|
+
}
|
|
3022
|
+
async function terminal(req) {
|
|
3023
|
+
const onData = (bytes) => reply(req.id, { fd: 1, bytes });
|
|
3024
|
+
const session = await (await ready2()).openTerminal(req.argv, { ...req.options, onData });
|
|
3025
|
+
terminals.set(req.id, session);
|
|
3026
|
+
reply(req.id, { started: session.pid });
|
|
3027
|
+
try {
|
|
3028
|
+
return await session.exited;
|
|
3029
|
+
} finally {
|
|
3030
|
+
terminals.delete(req.id);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
2943
3033
|
async function handle(req) {
|
|
2944
3034
|
if (req.op === "init") {
|
|
2945
|
-
launcher = (req.root ? Promise.resolve(req.root) : deps.storage()).then(
|
|
2946
|
-
|
|
3035
|
+
launcher = (req.root ? Promise.resolve(req.root) : deps.storage()).then(async (root) => {
|
|
3036
|
+
const started = new Launcher({
|
|
2947
3037
|
fs: new OpfsFs(root),
|
|
2948
3038
|
createWorker: deps.createWorker,
|
|
2949
3039
|
...req.modules ? { modules: req.modules } : {},
|
|
2950
3040
|
...req.env ? { env: req.env } : {}
|
|
2951
|
-
})
|
|
2952
|
-
|
|
3041
|
+
});
|
|
3042
|
+
await started.prepare();
|
|
3043
|
+
return started;
|
|
3044
|
+
});
|
|
2953
3045
|
await launcher;
|
|
2954
3046
|
return true;
|
|
2955
3047
|
}
|
|
2956
|
-
if (
|
|
2957
|
-
|
|
3048
|
+
if (req.op === "open-terminal") return terminal(req);
|
|
3049
|
+
if (req.op === "terminal") {
|
|
3050
|
+
const session = terminals.get(req.terminal);
|
|
3051
|
+
if (!session) throw new Error(`no terminal ${req.terminal}`);
|
|
3052
|
+
act(session, req);
|
|
3053
|
+
return true;
|
|
3054
|
+
}
|
|
3055
|
+
return (await ready2()).run(req.argv, {
|
|
2958
3056
|
...req.options,
|
|
2959
3057
|
onStdout: (bytes) => reply(req.id, { fd: 1, bytes }),
|
|
2960
3058
|
onStderr: (bytes) => reply(req.id, { fd: 2, bytes })
|