@secure-exec/core 0.2.0-rc.1 → 0.2.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/dist/generated/isolate-runtime.d.ts +2 -2
- package/dist/generated/isolate-runtime.js +2 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +1489 -239
- package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
- package/dist/kernel/device-backend.d.ts +14 -0
- package/dist/kernel/device-backend.js +251 -0
- package/dist/kernel/device-layer.js +9 -0
- package/dist/kernel/file-lock.js +2 -3
- package/dist/kernel/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -122
- package/dist/kernel/mount-table.d.ts +75 -0
- package/dist/kernel/mount-table.js +353 -0
- package/dist/kernel/permissions.d.ts +9 -0
- package/dist/kernel/permissions.js +33 -1
- package/dist/kernel/proc-backend.d.ts +30 -0
- package/dist/kernel/proc-backend.js +428 -0
- package/dist/kernel/proc-layer.js +6 -0
- package/dist/kernel/process-table.d.ts +3 -1
- package/dist/kernel/process-table.js +23 -3
- package/dist/kernel/pty.d.ts +3 -2
- package/dist/kernel/pty.js +13 -2
- package/dist/kernel/socket-table.d.ts +7 -0
- package/dist/kernel/socket-table.js +99 -35
- package/dist/kernel/types.d.ts +45 -4
- package/dist/kernel/types.js +9 -0
- package/dist/kernel/vfs.d.ts +30 -2
- package/dist/kernel/vfs.js +19 -2
- package/dist/shared/api-types.d.ts +6 -0
- package/dist/shared/bridge-contract.d.ts +21 -3
- package/dist/shared/bridge-contract.js +2 -0
- package/dist/shared/console-formatter.js +8 -8
- package/dist/shared/global-exposure.js +95 -0
- package/dist/shared/in-memory-fs.d.ts +14 -59
- package/dist/shared/in-memory-fs.js +97 -597
- package/dist/shared/permissions.js +5 -0
- package/dist/test/block-store-conformance.d.ts +34 -0
- package/dist/test/block-store-conformance.js +251 -0
- package/dist/test/metadata-store-conformance.d.ts +37 -0
- package/dist/test/metadata-store-conformance.js +646 -0
- package/dist/test/vfs-conformance.d.ts +65 -0
- package/dist/test/vfs-conformance.js +842 -0
- package/dist/types.d.ts +1 -0
- package/dist/vfs/chunked-vfs.d.ts +66 -0
- package/dist/vfs/chunked-vfs.js +1290 -0
- package/dist/vfs/host-block-store.d.ts +19 -0
- package/dist/vfs/host-block-store.js +97 -0
- package/dist/vfs/memory-block-store.d.ts +16 -0
- package/dist/vfs/memory-block-store.js +45 -0
- package/dist/vfs/memory-metadata.d.ts +75 -0
- package/dist/vfs/memory-metadata.js +528 -0
- package/dist/vfs/sqlite-metadata.d.ts +91 -0
- package/dist/vfs/sqlite-metadata.js +582 -0
- package/dist/vfs/types.d.ts +210 -0
- package/dist/vfs/types.js +8 -0
- package/package.json +20 -1
- package/dist/kernel/inode-table.d.ts +0 -43
- package/dist/kernel/inode-table.js +0 -85
package/dist/kernel/kernel.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* pipe manager, command registry, and permissions. Runtimes are execution
|
|
6
6
|
* engines that make "syscalls" to the kernel.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createDeviceBackend } from "./device-backend.js";
|
|
9
|
+
import { createProcBackend } from "./proc-backend.js";
|
|
10
|
+
import { MountTable } from "./mount-table.js";
|
|
10
11
|
import { FDTableManager } from "./fd-table.js";
|
|
11
12
|
import { ProcessTable } from "./process-table.js";
|
|
12
13
|
import { PipeManager } from "./pipe-manager.js";
|
|
@@ -17,33 +18,21 @@ import { wrapFileSystem, checkChildProcess } from "./permissions.js";
|
|
|
17
18
|
import { UserManager } from "./user.js";
|
|
18
19
|
import { SocketTable } from "./socket-table.js";
|
|
19
20
|
import { TimerTable } from "./timer-table.js";
|
|
20
|
-
import {
|
|
21
|
-
import { InMemoryFileSystem } from "../shared/in-memory-fs.js";
|
|
22
|
-
import { FILETYPE_REGULAR_FILE, SEEK_SET, SEEK_CUR, SEEK_END, O_APPEND, O_CREAT, O_EXCL, O_TRUNC, SIGTERM, SIGPIPE, SIGWINCH, F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_DUPFD_CLOEXEC, FD_CLOEXEC, KernelError, } from "./types.js";
|
|
21
|
+
import { FILETYPE_REGULAR_FILE, SEEK_SET, SEEK_CUR, SEEK_END, O_APPEND, O_CREAT, O_EXCL, O_TRUNC, SIGTERM, SIGPIPE, SIGWINCH, F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_DUPFD_CLOEXEC, FD_CLOEXEC, KernelError, noopKernelLogger, } from "./types.js";
|
|
23
22
|
export function createKernel(options) {
|
|
24
23
|
return new KernelImpl(options);
|
|
25
24
|
}
|
|
26
25
|
class KernelImpl {
|
|
27
26
|
vfs;
|
|
28
|
-
|
|
27
|
+
mountTable;
|
|
29
28
|
fdTableManager = new FDTableManager();
|
|
30
|
-
processTable
|
|
29
|
+
processTable;
|
|
31
30
|
pipeManager = new PipeManager();
|
|
32
|
-
ptyManager
|
|
33
|
-
try {
|
|
34
|
-
if (excludeLeaders) {
|
|
35
|
-
return this.processTable.killGroupExcludeLeaders(pgid, signal);
|
|
36
|
-
}
|
|
37
|
-
this.processTable.kill(-pgid, signal);
|
|
38
|
-
}
|
|
39
|
-
catch { /* no-op if pgid gone */ }
|
|
40
|
-
return 0;
|
|
41
|
-
});
|
|
31
|
+
ptyManager;
|
|
42
32
|
fileLockManager = new FileLockManager();
|
|
43
33
|
commandRegistry = new CommandRegistry();
|
|
44
34
|
socketTable;
|
|
45
35
|
timerTable;
|
|
46
|
-
inodeTable;
|
|
47
36
|
userManager;
|
|
48
37
|
drivers = [];
|
|
49
38
|
driverPids = new Map();
|
|
@@ -54,21 +43,38 @@ class KernelImpl {
|
|
|
54
43
|
disposed = false;
|
|
55
44
|
pendingBinEntries = [];
|
|
56
45
|
posixDirsReady;
|
|
46
|
+
log;
|
|
57
47
|
constructor(options) {
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
this.log = options.logger ?? noopKernelLogger;
|
|
49
|
+
this.processTable = new ProcessTable(this.log.child({ component: "process" }));
|
|
50
|
+
this.ptyManager = new PtyManager((pgid, signal, excludeLeaders) => {
|
|
51
|
+
try {
|
|
52
|
+
if (excludeLeaders) {
|
|
53
|
+
return this.processTable.killGroupExcludeLeaders(pgid, signal);
|
|
54
|
+
}
|
|
55
|
+
this.processTable.kill(-pgid, signal);
|
|
56
|
+
}
|
|
57
|
+
catch { /* no-op if pgid gone */ }
|
|
58
|
+
return 0;
|
|
59
|
+
}, this.log.child({ component: "pty" }));
|
|
60
|
+
// Build mount table: root FS → /dev → /proc → user mounts.
|
|
61
|
+
const mt = new MountTable(options.filesystem);
|
|
62
|
+
mt.mount("/dev", createDeviceBackend());
|
|
63
|
+
mt.mount("/proc", createProcBackend({
|
|
67
64
|
processTable: this.processTable,
|
|
68
65
|
fdTableManager: this.fdTableManager,
|
|
69
66
|
hostname: options.env?.HOSTNAME,
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
mountTable: mt,
|
|
68
|
+
}));
|
|
69
|
+
// Mount user-supplied filesystems
|
|
70
|
+
if (options.mounts) {
|
|
71
|
+
for (const m of options.mounts) {
|
|
72
|
+
mt.mount(m.path, m.fs, { readOnly: m.readOnly });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.mountTable = mt;
|
|
76
|
+
// Apply permission wrapping on top of the mount table
|
|
77
|
+
let fs = mt;
|
|
72
78
|
if (options.permissions) {
|
|
73
79
|
fs = wrapFileSystem(fs, options.permissions);
|
|
74
80
|
}
|
|
@@ -80,12 +86,15 @@ class KernelImpl {
|
|
|
80
86
|
this.userManager = new UserManager();
|
|
81
87
|
this.socketTable = new SocketTable({
|
|
82
88
|
vfs: this.vfs,
|
|
89
|
+
networkCheck: options.permissions?.network,
|
|
83
90
|
hostAdapter: options.hostNetworkAdapter,
|
|
84
91
|
getSignalState: (pid) => this.processTable.getSignalState(pid),
|
|
92
|
+
processExists: (pid) => this.processTable.get(pid) !== undefined,
|
|
85
93
|
});
|
|
86
94
|
this.timerTable = new TimerTable();
|
|
87
95
|
// Clean up FD table and sockets when a process exits
|
|
88
96
|
this.processTable.onProcessExit = (pid) => {
|
|
97
|
+
this.log.debug({ pid }, "process exit cleanup");
|
|
89
98
|
this.cleanupProcessFDs(pid);
|
|
90
99
|
this.socketTable.closeAllForProcess(pid);
|
|
91
100
|
this.timerTable.clearAllForProcess(pid);
|
|
@@ -110,6 +119,7 @@ class KernelImpl {
|
|
|
110
119
|
this.posixDirsReady = this.initPosixDirs();
|
|
111
120
|
}
|
|
112
121
|
async initPosixDirs() {
|
|
122
|
+
// /dev and /proc are auto-created by MountTable mounts — don't create them here.
|
|
113
123
|
const dirs = [
|
|
114
124
|
"/tmp",
|
|
115
125
|
"/bin",
|
|
@@ -121,7 +131,6 @@ class KernelImpl {
|
|
|
121
131
|
"/run",
|
|
122
132
|
"/srv",
|
|
123
133
|
"/sys",
|
|
124
|
-
"/proc",
|
|
125
134
|
"/opt",
|
|
126
135
|
"/mnt",
|
|
127
136
|
"/media",
|
|
@@ -168,6 +177,7 @@ class KernelImpl {
|
|
|
168
177
|
async mount(driver) {
|
|
169
178
|
this.assertNotDisposed();
|
|
170
179
|
await this.posixDirsReady;
|
|
180
|
+
this.log.debug({ driver: driver.name, commands: driver.commands }, "mounting runtime driver");
|
|
171
181
|
// Track PIDs owned by this driver
|
|
172
182
|
if (!this.driverPids.has(driver.name)) {
|
|
173
183
|
this.driverPids.set(driver.name, new Set());
|
|
@@ -179,11 +189,21 @@ class KernelImpl {
|
|
|
179
189
|
this.drivers.push(driver);
|
|
180
190
|
// Populate /bin stubs for shell PATH lookup
|
|
181
191
|
await this.commandRegistry.populateBin(this.vfs);
|
|
192
|
+
this.log.info({ driver: driver.name, commands: driver.commands }, "runtime driver mounted");
|
|
193
|
+
}
|
|
194
|
+
mountFs(path, fs, options) {
|
|
195
|
+
this.assertNotDisposed();
|
|
196
|
+
this.mountTable.mount(path, fs, options);
|
|
197
|
+
}
|
|
198
|
+
unmountFs(path) {
|
|
199
|
+
this.assertNotDisposed();
|
|
200
|
+
this.mountTable.unmount(path);
|
|
182
201
|
}
|
|
183
202
|
async dispose() {
|
|
184
203
|
if (this.disposed)
|
|
185
204
|
return;
|
|
186
205
|
this.disposed = true;
|
|
206
|
+
this.log.info({}, "kernel disposing");
|
|
187
207
|
// Terminate all running processes
|
|
188
208
|
await this.processTable.terminateAll();
|
|
189
209
|
// Clean up all sockets
|
|
@@ -212,6 +232,7 @@ class KernelImpl {
|
|
|
212
232
|
}
|
|
213
233
|
async exec(command, options) {
|
|
214
234
|
this.assertNotDisposed();
|
|
235
|
+
this.log.debug({ command, timeout: options?.timeout, cwd: options?.cwd }, "exec start");
|
|
215
236
|
// Flush pending /bin stubs before shell PATH lookup
|
|
216
237
|
await this.flushPendingBinEntries();
|
|
217
238
|
// Route through shell
|
|
@@ -252,6 +273,7 @@ class KernelImpl {
|
|
|
252
273
|
new Promise((_, reject) => {
|
|
253
274
|
timer = setTimeout(() => {
|
|
254
275
|
// Kill process and detach output callbacks
|
|
276
|
+
this.log.warn({ command, timeout: options.timeout }, "exec timeout, sending SIGTERM");
|
|
255
277
|
proc.onStdout = null;
|
|
256
278
|
proc.onStderr = null;
|
|
257
279
|
proc.kill(SIGTERM);
|
|
@@ -282,6 +304,7 @@ class KernelImpl {
|
|
|
282
304
|
this.assertNotDisposed();
|
|
283
305
|
const command = options?.command ?? "sh";
|
|
284
306
|
const args = options?.args ?? [];
|
|
307
|
+
this.log.debug({ command, args, cols: options?.cols, rows: options?.rows, cwd: options?.cwd }, "openShell start");
|
|
285
308
|
// Allocate a controller PID with an FD table to hold the PTY master
|
|
286
309
|
const controllerPid = this.processTable.allocatePid();
|
|
287
310
|
const controllerTable = this.fdTableManager.create(controllerPid);
|
|
@@ -289,8 +312,16 @@ class KernelImpl {
|
|
|
289
312
|
const { masterFd, slaveFd } = this.ptyManager.createPtyFDs(controllerTable);
|
|
290
313
|
const masterDescId = controllerTable.get(masterFd).description.id;
|
|
291
314
|
// Spawn shell with PTY slave as stdin/stdout/stderr
|
|
315
|
+
// Propagate terminal dimensions as POSIX COLUMNS/LINES env vars
|
|
316
|
+
const cols = options?.cols;
|
|
317
|
+
const rows = options?.rows;
|
|
318
|
+
const dimEnv = {};
|
|
319
|
+
if (cols !== undefined)
|
|
320
|
+
dimEnv.COLUMNS = String(cols);
|
|
321
|
+
if (rows !== undefined)
|
|
322
|
+
dimEnv.LINES = String(rows);
|
|
292
323
|
const proc = this.spawnInternal(command, args, {
|
|
293
|
-
env: options?.env,
|
|
324
|
+
env: { ...options?.env, ...dimEnv },
|
|
294
325
|
cwd: options?.cwd,
|
|
295
326
|
stdinFd: slaveFd,
|
|
296
327
|
stdoutFd: slaveFd,
|
|
@@ -300,6 +331,7 @@ class KernelImpl {
|
|
|
300
331
|
this.processTable.setpgid(proc.pid, proc.pid);
|
|
301
332
|
this.ptyManager.setForegroundPgid(masterDescId, proc.pid);
|
|
302
333
|
this.ptyManager.setSessionLeader(masterDescId, proc.pid);
|
|
334
|
+
this.log.debug({ shellPid: proc.pid, controllerPid, masterFd, masterDescId }, "openShell PTY attached");
|
|
303
335
|
// Close controller's copy of slave FD (child inherited its own copy via fork).
|
|
304
336
|
// Without this, slave refCount stays >0 after shell exits, preventing EOF on master.
|
|
305
337
|
const slaveEntry = controllerTable.get(slaveFd);
|
|
@@ -354,6 +386,7 @@ class KernelImpl {
|
|
|
354
386
|
set onData(fn) { pump.onData = fn; },
|
|
355
387
|
resize: (_cols, _rows) => {
|
|
356
388
|
const fgPgid = this.ptyManager.getForegroundPgid(masterDescId);
|
|
389
|
+
this.log.trace({ shellPid: proc.pid, cols: _cols, rows: _rows, fgPgid }, "PTY resize");
|
|
357
390
|
if (fgPgid > 0) {
|
|
358
391
|
try {
|
|
359
392
|
this.processTable.kill(-fgPgid, SIGWINCH);
|
|
@@ -369,6 +402,7 @@ class KernelImpl {
|
|
|
369
402
|
}
|
|
370
403
|
async connectTerminal(options) {
|
|
371
404
|
this.assertNotDisposed();
|
|
405
|
+
this.log.debug({ command: options?.command, cols: options?.cols, rows: options?.rows }, "connectTerminal start");
|
|
372
406
|
const stdin = process.stdin;
|
|
373
407
|
const stdout = process.stdout;
|
|
374
408
|
const isTTY = stdin.isTTY;
|
|
@@ -387,15 +421,12 @@ class KernelImpl {
|
|
|
387
421
|
const outputHandler = options?.onData
|
|
388
422
|
?? ((data) => { stdout.write(data); });
|
|
389
423
|
shell.onData = outputHandler;
|
|
390
|
-
//
|
|
391
|
-
onResize = () => {
|
|
392
|
-
shell.resize(stdout.columns || 80, stdout.rows || 24);
|
|
393
|
-
};
|
|
394
|
-
if (stdout.isTTY)
|
|
395
|
-
stdout.on("resize", onResize);
|
|
396
|
-
// Set initial terminal size
|
|
424
|
+
// Forward terminal resize → PTY SIGWINCH
|
|
397
425
|
if (stdout.isTTY) {
|
|
398
|
-
|
|
426
|
+
onResize = () => {
|
|
427
|
+
shell.resize(stdout.columns, stdout.rows);
|
|
428
|
+
};
|
|
429
|
+
stdout.on("resize", onResize);
|
|
399
430
|
}
|
|
400
431
|
return await shell.wait();
|
|
401
432
|
}
|
|
@@ -417,6 +448,9 @@ class KernelImpl {
|
|
|
417
448
|
readdir(path) { return this.vfs.readDir(path); }
|
|
418
449
|
stat(path) { return this.vfs.stat(path); }
|
|
419
450
|
exists(path) { return this.vfs.exists(path); }
|
|
451
|
+
removeFile(path) { return this.vfs.removeFile(path); }
|
|
452
|
+
removeDir(path) { return this.vfs.removeDir(path); }
|
|
453
|
+
rename(oldPath, newPath) { return this.vfs.rename(oldPath, newPath); }
|
|
420
454
|
// Introspection
|
|
421
455
|
get commands() {
|
|
422
456
|
return this.commandRegistry.list();
|
|
@@ -431,6 +465,7 @@ class KernelImpl {
|
|
|
431
465
|
// Internal spawn
|
|
432
466
|
// -----------------------------------------------------------------------
|
|
433
467
|
spawnInternal(command, args, options, callerPid) {
|
|
468
|
+
this.log.debug({ command, args, callerPid, cwd: options?.cwd }, "spawn start");
|
|
434
469
|
let driver = this.commandRegistry.resolve(command);
|
|
435
470
|
// On-demand discovery: ask mounted drivers to resolve unknown commands
|
|
436
471
|
if (!driver) {
|
|
@@ -456,12 +491,20 @@ class KernelImpl {
|
|
|
456
491
|
}
|
|
457
492
|
}
|
|
458
493
|
if (!driver) {
|
|
494
|
+
this.log.warn({ command }, "command not found");
|
|
459
495
|
throw new KernelError("ENOENT", `command not found: ${command}`);
|
|
460
496
|
}
|
|
461
497
|
// Check childProcess permission
|
|
462
|
-
|
|
498
|
+
try {
|
|
499
|
+
checkChildProcess(this.permissions, command, args, options?.cwd);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
this.log.warn({ command, args }, "spawn permission denied");
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
463
505
|
// Enforce maxProcesses budget
|
|
464
506
|
if (this.maxProcesses !== undefined && this.processTable.runningCount() >= this.maxProcesses) {
|
|
507
|
+
this.log.warn({ command, running: this.processTable.runningCount(), max: this.maxProcesses }, "process limit reached");
|
|
465
508
|
throw new KernelError("EAGAIN", "maximum process limit reached");
|
|
466
509
|
}
|
|
467
510
|
// Allocate PID atomically
|
|
@@ -486,44 +529,34 @@ class KernelImpl {
|
|
|
486
529
|
// Buffer stdout/stderr — wired before spawn so nothing is lost
|
|
487
530
|
const stdoutBuf = [];
|
|
488
531
|
const stderrBuf = [];
|
|
489
|
-
// Resolve output callbacks
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
532
|
+
// Resolve output callbacks. Drivers invoke BOTH ctx.onStdout and
|
|
533
|
+
// proc.onStdout per message, so the two must never point at the same
|
|
534
|
+
// callback — otherwise the host sees every chunk twice.
|
|
535
|
+
//
|
|
536
|
+
// ctx callbacks — kernel-internal routing (pipes, parent forwarding)
|
|
537
|
+
// + temporary buffer during spawn() to catch any
|
|
538
|
+
// synchronous output (disabled right after spawn).
|
|
539
|
+
// proc callbacks — user / host callback (options.onStdout) or buffer
|
|
540
|
+
// for later replay. Set AFTER spawn returns.
|
|
541
|
+
let ctxStdoutCb;
|
|
542
|
+
let ctxStderrCb;
|
|
496
543
|
if (stdoutPiped) {
|
|
497
|
-
|
|
544
|
+
ctxStdoutCb = this.createPipedOutputCallback(table, 1, pid);
|
|
498
545
|
}
|
|
499
|
-
else {
|
|
500
|
-
|
|
501
|
-
|
|
546
|
+
else if (!options?.onStdout && callerPid !== undefined) {
|
|
547
|
+
const parent = this.processTable.get(callerPid);
|
|
548
|
+
if (parent?.driverProcess.onStdout) {
|
|
549
|
+
ctxStdoutCb = parent.driverProcess.onStdout;
|
|
502
550
|
}
|
|
503
|
-
else if (callerPid !== undefined) {
|
|
504
|
-
const parent = this.processTable.get(callerPid);
|
|
505
|
-
if (parent?.driverProcess.onStdout) {
|
|
506
|
-
stdoutCb = parent.driverProcess.onStdout;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
if (!stdoutCb)
|
|
510
|
-
stdoutCb = (data) => stdoutBuf.push(data);
|
|
511
551
|
}
|
|
512
552
|
if (stderrPiped) {
|
|
513
|
-
|
|
553
|
+
ctxStderrCb = this.createPipedOutputCallback(table, 2, pid);
|
|
514
554
|
}
|
|
515
|
-
else {
|
|
516
|
-
|
|
517
|
-
|
|
555
|
+
else if (!options?.onStderr && callerPid !== undefined) {
|
|
556
|
+
const parent = this.processTable.get(callerPid);
|
|
557
|
+
if (parent?.driverProcess.onStderr) {
|
|
558
|
+
ctxStderrCb = parent.driverProcess.onStderr;
|
|
518
559
|
}
|
|
519
|
-
else if (callerPid !== undefined) {
|
|
520
|
-
const parent = this.processTable.get(callerPid);
|
|
521
|
-
if (parent?.driverProcess.onStderr) {
|
|
522
|
-
stderrCb = parent.driverProcess.onStderr;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (!stderrCb)
|
|
526
|
-
stderrCb = (data) => stderrBuf.push(data);
|
|
527
560
|
}
|
|
528
561
|
// Inherit env from parent process if spawned by another process, else use kernel defaults
|
|
529
562
|
const parentEntry = callerPid ? this.processTable.get(callerPid) : undefined;
|
|
@@ -532,27 +565,45 @@ class KernelImpl {
|
|
|
532
565
|
const stdinIsTTY = this.isFdPtySlave(table, 0);
|
|
533
566
|
const stdoutIsTTY = this.isFdPtySlave(table, 1);
|
|
534
567
|
const stderrIsTTY = this.isFdPtySlave(table, 2);
|
|
535
|
-
// Build process context with pre-wired callbacks
|
|
568
|
+
// Build process context with pre-wired callbacks.
|
|
569
|
+
// When not piped/forwarded, ctx gets a temporary buffer so that any
|
|
570
|
+
// data emitted synchronously during driver.spawn() is captured.
|
|
571
|
+
const resolvedCwd = options?.cwd ?? this.cwd;
|
|
536
572
|
const ctx = {
|
|
537
573
|
pid,
|
|
538
574
|
ppid: callerPid ?? 0,
|
|
539
|
-
env: { ...baseEnv, ...options?.env },
|
|
540
|
-
cwd:
|
|
575
|
+
env: { ...baseEnv, ...options?.env, PWD: resolvedCwd },
|
|
576
|
+
cwd: resolvedCwd,
|
|
541
577
|
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
542
578
|
stdinIsTTY,
|
|
543
579
|
stdoutIsTTY,
|
|
544
580
|
stderrIsTTY,
|
|
545
|
-
|
|
546
|
-
|
|
581
|
+
streamStdin: options?.streamStdin,
|
|
582
|
+
onStdout: ctxStdoutCb ?? (stdoutPiped ? undefined : (data) => stdoutBuf.push(data)),
|
|
583
|
+
onStderr: ctxStderrCb ?? (stderrPiped ? undefined : (data) => stderrBuf.push(data)),
|
|
547
584
|
};
|
|
548
585
|
// Spawn via driver
|
|
549
586
|
const driverProcess = driver.spawn(command, args, ctx);
|
|
550
|
-
|
|
587
|
+
this.log.debug({
|
|
588
|
+
pid, command, driver: driver.name, callerPid,
|
|
589
|
+
stdinIsTTY, stdoutIsTTY, stderrIsTTY,
|
|
590
|
+
}, "process spawned");
|
|
591
|
+
// After spawn, disable the temporary ctx buffer so that async output
|
|
592
|
+
// flows only through proc.onStdout — prevents double-delivery.
|
|
593
|
+
// Pipe/parent-forwarding callbacks stay active (they live in ctxStdoutCb).
|
|
551
594
|
if (!stdoutPiped) {
|
|
552
|
-
|
|
595
|
+
ctx.onStdout = ctxStdoutCb;
|
|
553
596
|
}
|
|
554
597
|
if (!stderrPiped) {
|
|
555
|
-
|
|
598
|
+
ctx.onStderr = ctxStderrCb;
|
|
599
|
+
}
|
|
600
|
+
// User/host callback goes ONLY on driverProcess (never on ctx) to
|
|
601
|
+
// avoid double-delivery — drivers invoke both ctx and proc callbacks.
|
|
602
|
+
if (!stdoutPiped) {
|
|
603
|
+
driverProcess.onStdout = options?.onStdout ?? ((data) => stdoutBuf.push(data));
|
|
604
|
+
}
|
|
605
|
+
if (!stderrPiped) {
|
|
606
|
+
driverProcess.onStderr = options?.onStderr ?? ((data) => stderrBuf.push(data));
|
|
556
607
|
}
|
|
557
608
|
// Register in process table
|
|
558
609
|
const entry = this.processTable.register(pid, driver.name, command, args, ctx, driverProcess);
|
|
@@ -646,9 +697,6 @@ class KernelImpl {
|
|
|
646
697
|
const filetype = FILETYPE_REGULAR_FILE;
|
|
647
698
|
const fd = table.open(path, flags, filetype);
|
|
648
699
|
const fdEntry = table.get(fd);
|
|
649
|
-
if (fdEntry) {
|
|
650
|
-
this.trackDescriptionInode(fdEntry.description);
|
|
651
|
-
}
|
|
652
700
|
// Stash the effective mode for the first write that materializes a new file.
|
|
653
701
|
if (created && (flags & O_CREAT)) {
|
|
654
702
|
const entry = this.processTable.get(pid);
|
|
@@ -771,14 +819,8 @@ class KernelImpl {
|
|
|
771
819
|
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
772
820
|
throw new KernelError("ESPIPE", "illegal seek");
|
|
773
821
|
}
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
const pos = Number(offset);
|
|
777
|
-
const endPos = pos + data.length;
|
|
778
|
-
const newContent = new Uint8Array(Math.max(content.length, endPos));
|
|
779
|
-
newContent.set(content);
|
|
780
|
-
newContent.set(data, pos);
|
|
781
|
-
await this.writeDescriptionFile(entry.description, newContent);
|
|
822
|
+
// Delegate positional write to VFS.
|
|
823
|
+
await this.pwriteDescription(entry.description, Number(offset), data);
|
|
782
824
|
return data.length;
|
|
783
825
|
},
|
|
784
826
|
fdDup: (pid, fd) => {
|
|
@@ -897,6 +939,7 @@ class KernelImpl {
|
|
|
897
939
|
return this.spawnManaged(command, args, {
|
|
898
940
|
env: ctx.env,
|
|
899
941
|
cwd: ctx.cwd,
|
|
942
|
+
streamStdin: ctx.streamStdin,
|
|
900
943
|
onStdout: ctx.onStdout,
|
|
901
944
|
onStderr: ctx.onStderr,
|
|
902
945
|
stdinFd: ctx.stdinFd,
|
|
@@ -917,6 +960,7 @@ class KernelImpl {
|
|
|
917
960
|
// Negative PID = process group kill, handled by kernel directly
|
|
918
961
|
if (pid >= 0)
|
|
919
962
|
assertOwns(pid);
|
|
963
|
+
this.log.debug({ pid, signal }, "signal delivery");
|
|
920
964
|
this.processTable.kill(pid, signal);
|
|
921
965
|
},
|
|
922
966
|
getpid: (pid) => {
|
|
@@ -1098,6 +1142,7 @@ class KernelImpl {
|
|
|
1098
1142
|
throw new KernelError("ENOTDIR", `not a directory: ${path}`);
|
|
1099
1143
|
}
|
|
1100
1144
|
entry.cwd = path;
|
|
1145
|
+
entry.env.PWD = path;
|
|
1101
1146
|
},
|
|
1102
1147
|
// Alarm (SIGALRM)
|
|
1103
1148
|
alarm: (pid, seconds) => {
|
|
@@ -1276,7 +1321,7 @@ class KernelImpl {
|
|
|
1276
1321
|
}
|
|
1277
1322
|
// Close all FDs and remove the table
|
|
1278
1323
|
this.fdTableManager.remove(pid);
|
|
1279
|
-
//
|
|
1324
|
+
// Flush buffered writes when the last shared reference closes.
|
|
1280
1325
|
for (const description of descriptions.values()) {
|
|
1281
1326
|
if (description.refCount <= 0) {
|
|
1282
1327
|
this.releaseDescriptionInode(description);
|
|
@@ -1319,56 +1364,30 @@ class KernelImpl {
|
|
|
1319
1364
|
entry.description.cursor = BigInt(endPos);
|
|
1320
1365
|
return data.length;
|
|
1321
1366
|
}
|
|
1322
|
-
trackDescriptionInode(description) {
|
|
1323
|
-
if (!this.rawInMemoryFs || description.inode !== undefined)
|
|
1324
|
-
return;
|
|
1325
|
-
const ino = this.rawInMemoryFs.getInodeForPath(description.path);
|
|
1326
|
-
if (ino === null)
|
|
1327
|
-
return;
|
|
1328
|
-
description.inode = ino;
|
|
1329
|
-
this.inodeTable.incrementOpenRefs(ino);
|
|
1330
|
-
}
|
|
1331
1367
|
releaseDescriptionInode(description) {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
this.inodeTable.decrementOpenRefs(description.inode);
|
|
1335
|
-
if (this.inodeTable.shouldDelete(description.inode)) {
|
|
1336
|
-
this.rawInMemoryFs?.deleteInodeData(description.inode);
|
|
1337
|
-
this.inodeTable.delete(description.inode);
|
|
1338
|
-
}
|
|
1339
|
-
description.inode = undefined;
|
|
1368
|
+
// Flush buffered writes to durable storage when the last FD is closed.
|
|
1369
|
+
void this.vfs.fsync?.(description.path);
|
|
1340
1370
|
}
|
|
1341
1371
|
async readDescriptionFile(description) {
|
|
1342
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1343
|
-
return this.rawInMemoryFs.readFileByInode(description.inode);
|
|
1344
|
-
}
|
|
1345
1372
|
return this.vfs.readFile(description.path);
|
|
1346
1373
|
}
|
|
1347
1374
|
async writeDescriptionFile(description, content) {
|
|
1348
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1349
|
-
this.rawInMemoryFs.writeFileByInode(description.inode, content);
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
1375
|
await this.vfs.writeFile(description.path, content);
|
|
1353
|
-
this.trackDescriptionInode(description);
|
|
1354
1376
|
}
|
|
1355
1377
|
prepareOpenSync(path, flags) {
|
|
1356
1378
|
const syncVfs = this.vfs;
|
|
1357
1379
|
return syncVfs.prepareOpenSync?.(path, flags) ?? false;
|
|
1358
1380
|
}
|
|
1359
1381
|
async preadDescription(description, offset, length) {
|
|
1360
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1361
|
-
return this.rawInMemoryFs.preadByInode(description.inode, offset, length);
|
|
1362
|
-
}
|
|
1363
1382
|
return this.vfs.pread(description.path, offset, length);
|
|
1364
1383
|
}
|
|
1384
|
+
async pwriteDescription(description, offset, data) {
|
|
1385
|
+
await this.vfs.pwrite(description.path, offset, data);
|
|
1386
|
+
}
|
|
1365
1387
|
async getDescriptionSize(description) {
|
|
1366
1388
|
return (await this.statDescription(description)).size;
|
|
1367
1389
|
}
|
|
1368
1390
|
async statDescription(description) {
|
|
1369
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1370
|
-
return this.rawInMemoryFs.statByInode(description.inode);
|
|
1371
|
-
}
|
|
1372
1391
|
return this.vfs.stat(description.path);
|
|
1373
1392
|
}
|
|
1374
1393
|
getTable(pid) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount Table.
|
|
3
|
+
*
|
|
4
|
+
* Linux-style VFS mount table that routes paths to mounted filesystem backends
|
|
5
|
+
* by longest-prefix matching. Replaces the hardcoded layer composition
|
|
6
|
+
* (DeviceLayer wraps ProcLayer wraps base FS) with a unified routing table.
|
|
7
|
+
*/
|
|
8
|
+
import type { VirtualDirEntry, VirtualDirStatEntry, VirtualFileSystem, VirtualStat } from "./vfs.js";
|
|
9
|
+
export interface MountOptions {
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface MountEntry {
|
|
13
|
+
path: string;
|
|
14
|
+
readOnly: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class MountTable implements VirtualFileSystem {
|
|
17
|
+
/**
|
|
18
|
+
* Mounts sorted by path length descending so longest-prefix match is first hit.
|
|
19
|
+
*/
|
|
20
|
+
private mounts;
|
|
21
|
+
constructor(rootFs: VirtualFileSystem);
|
|
22
|
+
/**
|
|
23
|
+
* Mount a filesystem at the given path.
|
|
24
|
+
* Auto-creates the mount point directory in the parent filesystem if needed.
|
|
25
|
+
*/
|
|
26
|
+
mount(path: string, fs: VirtualFileSystem, options?: MountOptions): void;
|
|
27
|
+
/**
|
|
28
|
+
* Unmount the filesystem at the given path.
|
|
29
|
+
*/
|
|
30
|
+
unmount(path: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* List all current mounts.
|
|
33
|
+
*/
|
|
34
|
+
getMounts(): ReadonlyArray<MountEntry>;
|
|
35
|
+
private resolve;
|
|
36
|
+
private assertWritable;
|
|
37
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
38
|
+
readTextFile(path: string): Promise<string>;
|
|
39
|
+
readDir(path: string): Promise<string[]>;
|
|
40
|
+
readDirWithTypes(path: string): Promise<VirtualDirEntry[]>;
|
|
41
|
+
exists(path: string): Promise<boolean>;
|
|
42
|
+
stat(path: string): Promise<VirtualStat>;
|
|
43
|
+
lstat(path: string): Promise<VirtualStat>;
|
|
44
|
+
realpath(path: string): Promise<string>;
|
|
45
|
+
readlink(path: string): Promise<string>;
|
|
46
|
+
pread(path: string, offset: number, length: number): Promise<Uint8Array>;
|
|
47
|
+
pwrite(path: string, offset: number, data: Uint8Array): Promise<void>;
|
|
48
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
49
|
+
createDir(path: string): Promise<void>;
|
|
50
|
+
mkdir(path: string, options?: {
|
|
51
|
+
recursive?: boolean;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
removeFile(path: string): Promise<void>;
|
|
54
|
+
removeDir(path: string): Promise<void>;
|
|
55
|
+
symlink(target: string, linkPath: string): Promise<void>;
|
|
56
|
+
link(oldPath: string, newPath: string): Promise<void>;
|
|
57
|
+
rename(oldPath: string, newPath: string): Promise<void>;
|
|
58
|
+
chmod(path: string, mode: number): Promise<void>;
|
|
59
|
+
chown(path: string, uid: number, gid: number): Promise<void>;
|
|
60
|
+
utimes(path: string, atime: number, mtime: number): Promise<void>;
|
|
61
|
+
truncate(path: string, length: number): Promise<void>;
|
|
62
|
+
fsync(path: string): Promise<void>;
|
|
63
|
+
copy(srcPath: string, dstPath: string): Promise<void>;
|
|
64
|
+
readDirStat(path: string): Promise<VirtualDirStatEntry[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Get basenames of child mount points under a directory.
|
|
67
|
+
* For example, if mounts exist at /dev and /proc, calling with "/" returns ["dev", "proc"].
|
|
68
|
+
*/
|
|
69
|
+
private getChildMountBasenames;
|
|
70
|
+
/**
|
|
71
|
+
* Synchronous open preparation (O_CREAT, O_EXCL, O_TRUNC).
|
|
72
|
+
* Delegates to the underlying backend's prepareOpenSync if it exists.
|
|
73
|
+
*/
|
|
74
|
+
prepareOpenSync(path: string, flags: number): boolean;
|
|
75
|
+
}
|