@secure-exec/core 0.2.0-rc.2 → 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 +1 -1
- package/dist/generated/isolate-runtime.js +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +145 -7
- 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/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -119
- 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/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/console-formatter.js +8 -8
- package/dist/shared/in-memory-fs.d.ts +14 -62
- package/dist/shared/in-memory-fs.js +101 -636
- 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
|
}
|
|
@@ -88,6 +94,7 @@ class KernelImpl {
|
|
|
88
94
|
this.timerTable = new TimerTable();
|
|
89
95
|
// Clean up FD table and sockets when a process exits
|
|
90
96
|
this.processTable.onProcessExit = (pid) => {
|
|
97
|
+
this.log.debug({ pid }, "process exit cleanup");
|
|
91
98
|
this.cleanupProcessFDs(pid);
|
|
92
99
|
this.socketTable.closeAllForProcess(pid);
|
|
93
100
|
this.timerTable.clearAllForProcess(pid);
|
|
@@ -112,6 +119,7 @@ class KernelImpl {
|
|
|
112
119
|
this.posixDirsReady = this.initPosixDirs();
|
|
113
120
|
}
|
|
114
121
|
async initPosixDirs() {
|
|
122
|
+
// /dev and /proc are auto-created by MountTable mounts — don't create them here.
|
|
115
123
|
const dirs = [
|
|
116
124
|
"/tmp",
|
|
117
125
|
"/bin",
|
|
@@ -123,7 +131,6 @@ class KernelImpl {
|
|
|
123
131
|
"/run",
|
|
124
132
|
"/srv",
|
|
125
133
|
"/sys",
|
|
126
|
-
"/proc",
|
|
127
134
|
"/opt",
|
|
128
135
|
"/mnt",
|
|
129
136
|
"/media",
|
|
@@ -170,6 +177,7 @@ class KernelImpl {
|
|
|
170
177
|
async mount(driver) {
|
|
171
178
|
this.assertNotDisposed();
|
|
172
179
|
await this.posixDirsReady;
|
|
180
|
+
this.log.debug({ driver: driver.name, commands: driver.commands }, "mounting runtime driver");
|
|
173
181
|
// Track PIDs owned by this driver
|
|
174
182
|
if (!this.driverPids.has(driver.name)) {
|
|
175
183
|
this.driverPids.set(driver.name, new Set());
|
|
@@ -181,11 +189,21 @@ class KernelImpl {
|
|
|
181
189
|
this.drivers.push(driver);
|
|
182
190
|
// Populate /bin stubs for shell PATH lookup
|
|
183
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);
|
|
184
201
|
}
|
|
185
202
|
async dispose() {
|
|
186
203
|
if (this.disposed)
|
|
187
204
|
return;
|
|
188
205
|
this.disposed = true;
|
|
206
|
+
this.log.info({}, "kernel disposing");
|
|
189
207
|
// Terminate all running processes
|
|
190
208
|
await this.processTable.terminateAll();
|
|
191
209
|
// Clean up all sockets
|
|
@@ -214,6 +232,7 @@ class KernelImpl {
|
|
|
214
232
|
}
|
|
215
233
|
async exec(command, options) {
|
|
216
234
|
this.assertNotDisposed();
|
|
235
|
+
this.log.debug({ command, timeout: options?.timeout, cwd: options?.cwd }, "exec start");
|
|
217
236
|
// Flush pending /bin stubs before shell PATH lookup
|
|
218
237
|
await this.flushPendingBinEntries();
|
|
219
238
|
// Route through shell
|
|
@@ -254,6 +273,7 @@ class KernelImpl {
|
|
|
254
273
|
new Promise((_, reject) => {
|
|
255
274
|
timer = setTimeout(() => {
|
|
256
275
|
// Kill process and detach output callbacks
|
|
276
|
+
this.log.warn({ command, timeout: options.timeout }, "exec timeout, sending SIGTERM");
|
|
257
277
|
proc.onStdout = null;
|
|
258
278
|
proc.onStderr = null;
|
|
259
279
|
proc.kill(SIGTERM);
|
|
@@ -284,6 +304,7 @@ class KernelImpl {
|
|
|
284
304
|
this.assertNotDisposed();
|
|
285
305
|
const command = options?.command ?? "sh";
|
|
286
306
|
const args = options?.args ?? [];
|
|
307
|
+
this.log.debug({ command, args, cols: options?.cols, rows: options?.rows, cwd: options?.cwd }, "openShell start");
|
|
287
308
|
// Allocate a controller PID with an FD table to hold the PTY master
|
|
288
309
|
const controllerPid = this.processTable.allocatePid();
|
|
289
310
|
const controllerTable = this.fdTableManager.create(controllerPid);
|
|
@@ -291,8 +312,16 @@ class KernelImpl {
|
|
|
291
312
|
const { masterFd, slaveFd } = this.ptyManager.createPtyFDs(controllerTable);
|
|
292
313
|
const masterDescId = controllerTable.get(masterFd).description.id;
|
|
293
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);
|
|
294
323
|
const proc = this.spawnInternal(command, args, {
|
|
295
|
-
env: options?.env,
|
|
324
|
+
env: { ...options?.env, ...dimEnv },
|
|
296
325
|
cwd: options?.cwd,
|
|
297
326
|
stdinFd: slaveFd,
|
|
298
327
|
stdoutFd: slaveFd,
|
|
@@ -302,6 +331,7 @@ class KernelImpl {
|
|
|
302
331
|
this.processTable.setpgid(proc.pid, proc.pid);
|
|
303
332
|
this.ptyManager.setForegroundPgid(masterDescId, proc.pid);
|
|
304
333
|
this.ptyManager.setSessionLeader(masterDescId, proc.pid);
|
|
334
|
+
this.log.debug({ shellPid: proc.pid, controllerPid, masterFd, masterDescId }, "openShell PTY attached");
|
|
305
335
|
// Close controller's copy of slave FD (child inherited its own copy via fork).
|
|
306
336
|
// Without this, slave refCount stays >0 after shell exits, preventing EOF on master.
|
|
307
337
|
const slaveEntry = controllerTable.get(slaveFd);
|
|
@@ -356,6 +386,7 @@ class KernelImpl {
|
|
|
356
386
|
set onData(fn) { pump.onData = fn; },
|
|
357
387
|
resize: (_cols, _rows) => {
|
|
358
388
|
const fgPgid = this.ptyManager.getForegroundPgid(masterDescId);
|
|
389
|
+
this.log.trace({ shellPid: proc.pid, cols: _cols, rows: _rows, fgPgid }, "PTY resize");
|
|
359
390
|
if (fgPgid > 0) {
|
|
360
391
|
try {
|
|
361
392
|
this.processTable.kill(-fgPgid, SIGWINCH);
|
|
@@ -371,6 +402,7 @@ class KernelImpl {
|
|
|
371
402
|
}
|
|
372
403
|
async connectTerminal(options) {
|
|
373
404
|
this.assertNotDisposed();
|
|
405
|
+
this.log.debug({ command: options?.command, cols: options?.cols, rows: options?.rows }, "connectTerminal start");
|
|
374
406
|
const stdin = process.stdin;
|
|
375
407
|
const stdout = process.stdout;
|
|
376
408
|
const isTTY = stdin.isTTY;
|
|
@@ -389,11 +421,13 @@ class KernelImpl {
|
|
|
389
421
|
const outputHandler = options?.onData
|
|
390
422
|
?? ((data) => { stdout.write(data); });
|
|
391
423
|
shell.onData = outputHandler;
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
424
|
+
// Forward terminal resize → PTY SIGWINCH
|
|
425
|
+
if (stdout.isTTY) {
|
|
426
|
+
onResize = () => {
|
|
427
|
+
shell.resize(stdout.columns, stdout.rows);
|
|
428
|
+
};
|
|
429
|
+
stdout.on("resize", onResize);
|
|
430
|
+
}
|
|
397
431
|
return await shell.wait();
|
|
398
432
|
}
|
|
399
433
|
finally {
|
|
@@ -414,6 +448,9 @@ class KernelImpl {
|
|
|
414
448
|
readdir(path) { return this.vfs.readDir(path); }
|
|
415
449
|
stat(path) { return this.vfs.stat(path); }
|
|
416
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); }
|
|
417
454
|
// Introspection
|
|
418
455
|
get commands() {
|
|
419
456
|
return this.commandRegistry.list();
|
|
@@ -428,6 +465,7 @@ class KernelImpl {
|
|
|
428
465
|
// Internal spawn
|
|
429
466
|
// -----------------------------------------------------------------------
|
|
430
467
|
spawnInternal(command, args, options, callerPid) {
|
|
468
|
+
this.log.debug({ command, args, callerPid, cwd: options?.cwd }, "spawn start");
|
|
431
469
|
let driver = this.commandRegistry.resolve(command);
|
|
432
470
|
// On-demand discovery: ask mounted drivers to resolve unknown commands
|
|
433
471
|
if (!driver) {
|
|
@@ -453,12 +491,20 @@ class KernelImpl {
|
|
|
453
491
|
}
|
|
454
492
|
}
|
|
455
493
|
if (!driver) {
|
|
494
|
+
this.log.warn({ command }, "command not found");
|
|
456
495
|
throw new KernelError("ENOENT", `command not found: ${command}`);
|
|
457
496
|
}
|
|
458
497
|
// Check childProcess permission
|
|
459
|
-
|
|
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
|
+
}
|
|
460
505
|
// Enforce maxProcesses budget
|
|
461
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");
|
|
462
508
|
throw new KernelError("EAGAIN", "maximum process limit reached");
|
|
463
509
|
}
|
|
464
510
|
// Allocate PID atomically
|
|
@@ -483,44 +529,34 @@ class KernelImpl {
|
|
|
483
529
|
// Buffer stdout/stderr — wired before spawn so nothing is lost
|
|
484
530
|
const stdoutBuf = [];
|
|
485
531
|
const stderrBuf = [];
|
|
486
|
-
// Resolve output callbacks
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
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;
|
|
493
543
|
if (stdoutPiped) {
|
|
494
|
-
|
|
544
|
+
ctxStdoutCb = this.createPipedOutputCallback(table, 1, pid);
|
|
495
545
|
}
|
|
496
|
-
else {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
else if (callerPid !== undefined) {
|
|
501
|
-
const parent = this.processTable.get(callerPid);
|
|
502
|
-
if (parent?.driverProcess.onStdout) {
|
|
503
|
-
stdoutCb = parent.driverProcess.onStdout;
|
|
504
|
-
}
|
|
546
|
+
else if (!options?.onStdout && callerPid !== undefined) {
|
|
547
|
+
const parent = this.processTable.get(callerPid);
|
|
548
|
+
if (parent?.driverProcess.onStdout) {
|
|
549
|
+
ctxStdoutCb = parent.driverProcess.onStdout;
|
|
505
550
|
}
|
|
506
|
-
if (!stdoutCb)
|
|
507
|
-
stdoutCb = (data) => stdoutBuf.push(data);
|
|
508
551
|
}
|
|
509
552
|
if (stderrPiped) {
|
|
510
|
-
|
|
553
|
+
ctxStderrCb = this.createPipedOutputCallback(table, 2, pid);
|
|
511
554
|
}
|
|
512
|
-
else {
|
|
513
|
-
|
|
514
|
-
|
|
555
|
+
else if (!options?.onStderr && callerPid !== undefined) {
|
|
556
|
+
const parent = this.processTable.get(callerPid);
|
|
557
|
+
if (parent?.driverProcess.onStderr) {
|
|
558
|
+
ctxStderrCb = parent.driverProcess.onStderr;
|
|
515
559
|
}
|
|
516
|
-
else if (callerPid !== undefined) {
|
|
517
|
-
const parent = this.processTable.get(callerPid);
|
|
518
|
-
if (parent?.driverProcess.onStderr) {
|
|
519
|
-
stderrCb = parent.driverProcess.onStderr;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (!stderrCb)
|
|
523
|
-
stderrCb = (data) => stderrBuf.push(data);
|
|
524
560
|
}
|
|
525
561
|
// Inherit env from parent process if spawned by another process, else use kernel defaults
|
|
526
562
|
const parentEntry = callerPid ? this.processTable.get(callerPid) : undefined;
|
|
@@ -529,27 +565,45 @@ class KernelImpl {
|
|
|
529
565
|
const stdinIsTTY = this.isFdPtySlave(table, 0);
|
|
530
566
|
const stdoutIsTTY = this.isFdPtySlave(table, 1);
|
|
531
567
|
const stderrIsTTY = this.isFdPtySlave(table, 2);
|
|
532
|
-
// 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;
|
|
533
572
|
const ctx = {
|
|
534
573
|
pid,
|
|
535
574
|
ppid: callerPid ?? 0,
|
|
536
|
-
env: { ...baseEnv, ...options?.env },
|
|
537
|
-
cwd:
|
|
575
|
+
env: { ...baseEnv, ...options?.env, PWD: resolvedCwd },
|
|
576
|
+
cwd: resolvedCwd,
|
|
538
577
|
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
539
578
|
stdinIsTTY,
|
|
540
579
|
stdoutIsTTY,
|
|
541
580
|
stderrIsTTY,
|
|
542
|
-
|
|
543
|
-
|
|
581
|
+
streamStdin: options?.streamStdin,
|
|
582
|
+
onStdout: ctxStdoutCb ?? (stdoutPiped ? undefined : (data) => stdoutBuf.push(data)),
|
|
583
|
+
onStderr: ctxStderrCb ?? (stderrPiped ? undefined : (data) => stderrBuf.push(data)),
|
|
544
584
|
};
|
|
545
585
|
// Spawn via driver
|
|
546
586
|
const driverProcess = driver.spawn(command, args, ctx);
|
|
547
|
-
|
|
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).
|
|
594
|
+
if (!stdoutPiped) {
|
|
595
|
+
ctx.onStdout = ctxStdoutCb;
|
|
596
|
+
}
|
|
597
|
+
if (!stderrPiped) {
|
|
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.
|
|
548
602
|
if (!stdoutPiped) {
|
|
549
|
-
driverProcess.onStdout =
|
|
603
|
+
driverProcess.onStdout = options?.onStdout ?? ((data) => stdoutBuf.push(data));
|
|
550
604
|
}
|
|
551
605
|
if (!stderrPiped) {
|
|
552
|
-
driverProcess.onStderr =
|
|
606
|
+
driverProcess.onStderr = options?.onStderr ?? ((data) => stderrBuf.push(data));
|
|
553
607
|
}
|
|
554
608
|
// Register in process table
|
|
555
609
|
const entry = this.processTable.register(pid, driver.name, command, args, ctx, driverProcess);
|
|
@@ -643,9 +697,6 @@ class KernelImpl {
|
|
|
643
697
|
const filetype = FILETYPE_REGULAR_FILE;
|
|
644
698
|
const fd = table.open(path, flags, filetype);
|
|
645
699
|
const fdEntry = table.get(fd);
|
|
646
|
-
if (fdEntry) {
|
|
647
|
-
this.trackDescriptionInode(fdEntry.description);
|
|
648
|
-
}
|
|
649
700
|
// Stash the effective mode for the first write that materializes a new file.
|
|
650
701
|
if (created && (flags & O_CREAT)) {
|
|
651
702
|
const entry = this.processTable.get(pid);
|
|
@@ -768,14 +819,8 @@ class KernelImpl {
|
|
|
768
819
|
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
769
820
|
throw new KernelError("ESPIPE", "illegal seek");
|
|
770
821
|
}
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
const pos = Number(offset);
|
|
774
|
-
const endPos = pos + data.length;
|
|
775
|
-
const newContent = new Uint8Array(Math.max(content.length, endPos));
|
|
776
|
-
newContent.set(content);
|
|
777
|
-
newContent.set(data, pos);
|
|
778
|
-
await this.writeDescriptionFile(entry.description, newContent);
|
|
822
|
+
// Delegate positional write to VFS.
|
|
823
|
+
await this.pwriteDescription(entry.description, Number(offset), data);
|
|
779
824
|
return data.length;
|
|
780
825
|
},
|
|
781
826
|
fdDup: (pid, fd) => {
|
|
@@ -894,6 +939,7 @@ class KernelImpl {
|
|
|
894
939
|
return this.spawnManaged(command, args, {
|
|
895
940
|
env: ctx.env,
|
|
896
941
|
cwd: ctx.cwd,
|
|
942
|
+
streamStdin: ctx.streamStdin,
|
|
897
943
|
onStdout: ctx.onStdout,
|
|
898
944
|
onStderr: ctx.onStderr,
|
|
899
945
|
stdinFd: ctx.stdinFd,
|
|
@@ -914,6 +960,7 @@ class KernelImpl {
|
|
|
914
960
|
// Negative PID = process group kill, handled by kernel directly
|
|
915
961
|
if (pid >= 0)
|
|
916
962
|
assertOwns(pid);
|
|
963
|
+
this.log.debug({ pid, signal }, "signal delivery");
|
|
917
964
|
this.processTable.kill(pid, signal);
|
|
918
965
|
},
|
|
919
966
|
getpid: (pid) => {
|
|
@@ -1095,6 +1142,7 @@ class KernelImpl {
|
|
|
1095
1142
|
throw new KernelError("ENOTDIR", `not a directory: ${path}`);
|
|
1096
1143
|
}
|
|
1097
1144
|
entry.cwd = path;
|
|
1145
|
+
entry.env.PWD = path;
|
|
1098
1146
|
},
|
|
1099
1147
|
// Alarm (SIGALRM)
|
|
1100
1148
|
alarm: (pid, seconds) => {
|
|
@@ -1273,7 +1321,7 @@ class KernelImpl {
|
|
|
1273
1321
|
}
|
|
1274
1322
|
// Close all FDs and remove the table
|
|
1275
1323
|
this.fdTableManager.remove(pid);
|
|
1276
|
-
//
|
|
1324
|
+
// Flush buffered writes when the last shared reference closes.
|
|
1277
1325
|
for (const description of descriptions.values()) {
|
|
1278
1326
|
if (description.refCount <= 0) {
|
|
1279
1327
|
this.releaseDescriptionInode(description);
|
|
@@ -1316,56 +1364,30 @@ class KernelImpl {
|
|
|
1316
1364
|
entry.description.cursor = BigInt(endPos);
|
|
1317
1365
|
return data.length;
|
|
1318
1366
|
}
|
|
1319
|
-
trackDescriptionInode(description) {
|
|
1320
|
-
if (!this.rawInMemoryFs || description.inode !== undefined)
|
|
1321
|
-
return;
|
|
1322
|
-
const ino = this.rawInMemoryFs.getInodeForPath(description.path);
|
|
1323
|
-
if (ino === null)
|
|
1324
|
-
return;
|
|
1325
|
-
description.inode = ino;
|
|
1326
|
-
this.inodeTable.incrementOpenRefs(ino);
|
|
1327
|
-
}
|
|
1328
1367
|
releaseDescriptionInode(description) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
this.inodeTable.decrementOpenRefs(description.inode);
|
|
1332
|
-
if (this.inodeTable.shouldDelete(description.inode)) {
|
|
1333
|
-
this.rawInMemoryFs?.deleteInodeData(description.inode);
|
|
1334
|
-
this.inodeTable.delete(description.inode);
|
|
1335
|
-
}
|
|
1336
|
-
description.inode = undefined;
|
|
1368
|
+
// Flush buffered writes to durable storage when the last FD is closed.
|
|
1369
|
+
void this.vfs.fsync?.(description.path);
|
|
1337
1370
|
}
|
|
1338
1371
|
async readDescriptionFile(description) {
|
|
1339
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1340
|
-
return this.rawInMemoryFs.readFileByInode(description.inode);
|
|
1341
|
-
}
|
|
1342
1372
|
return this.vfs.readFile(description.path);
|
|
1343
1373
|
}
|
|
1344
1374
|
async writeDescriptionFile(description, content) {
|
|
1345
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1346
|
-
this.rawInMemoryFs.writeFileByInode(description.inode, content);
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
1375
|
await this.vfs.writeFile(description.path, content);
|
|
1350
|
-
this.trackDescriptionInode(description);
|
|
1351
1376
|
}
|
|
1352
1377
|
prepareOpenSync(path, flags) {
|
|
1353
1378
|
const syncVfs = this.vfs;
|
|
1354
1379
|
return syncVfs.prepareOpenSync?.(path, flags) ?? false;
|
|
1355
1380
|
}
|
|
1356
1381
|
async preadDescription(description, offset, length) {
|
|
1357
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1358
|
-
return this.rawInMemoryFs.preadByInode(description.inode, offset, length);
|
|
1359
|
-
}
|
|
1360
1382
|
return this.vfs.pread(description.path, offset, length);
|
|
1361
1383
|
}
|
|
1384
|
+
async pwriteDescription(description, offset, data) {
|
|
1385
|
+
await this.vfs.pwrite(description.path, offset, data);
|
|
1386
|
+
}
|
|
1362
1387
|
async getDescriptionSize(description) {
|
|
1363
1388
|
return (await this.statDescription(description)).size;
|
|
1364
1389
|
}
|
|
1365
1390
|
async statDescription(description) {
|
|
1366
|
-
if (description.inode !== undefined && this.rawInMemoryFs) {
|
|
1367
|
-
return this.rawInMemoryFs.statByInode(description.inode);
|
|
1368
|
-
}
|
|
1369
1391
|
return this.vfs.stat(description.path);
|
|
1370
1392
|
}
|
|
1371
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
|
+
}
|