@secure-exec/core 0.2.1 → 0.3.0-rc.2
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 +5 -5
- package/dist/binary.d.ts +4 -0
- package/dist/binary.js +25 -0
- package/dist/bytes.d.ts +2 -0
- package/dist/bytes.js +6 -0
- package/dist/callbacks.d.ts +41 -0
- package/dist/callbacks.js +94 -0
- package/dist/cargo.d.ts +2 -0
- package/dist/cargo.js +142 -0
- package/dist/correlation.d.ts +10 -0
- package/dist/correlation.js +49 -0
- package/dist/descriptors.d.ts +34 -0
- package/dist/descriptors.js +37 -0
- package/dist/event-buffer.d.ts +90 -0
- package/dist/event-buffer.js +313 -0
- package/dist/ext.d.ts +7 -0
- package/dist/ext.js +13 -0
- package/dist/filesystem.d.ts +41 -0
- package/dist/filesystem.js +70 -0
- package/dist/frame-payload-codec.d.ts +8 -0
- package/dist/frame-payload-codec.js +14 -0
- package/dist/frame-rpc.d.ts +38 -0
- package/dist/frame-rpc.js +73 -0
- package/dist/frame-stream.d.ts +27 -0
- package/dist/frame-stream.js +99 -0
- package/dist/framing.d.ts +7 -0
- package/dist/framing.js +22 -0
- package/dist/generated/AcpLimitsConfig.d.ts +4 -0
- package/dist/generated/AcpLimitsConfig.js +2 -0
- package/dist/generated/CreateVmConfig.d.ts +19 -0
- package/dist/generated/FsPermissionRule.d.ts +6 -0
- package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
- package/dist/generated/FsPermissionRuleSet.js +1 -0
- package/dist/generated/FsPermissionScope.d.ts +3 -0
- package/dist/generated/FsPermissionScope.js +1 -0
- package/dist/generated/HttpLimitsConfig.d.ts +3 -0
- package/dist/generated/HttpLimitsConfig.js +2 -0
- package/dist/generated/JsModuleResolution.d.ts +1 -0
- package/dist/generated/JsModuleResolution.js +2 -0
- package/dist/generated/JsRuntimeConfig.d.ts +26 -0
- package/dist/generated/JsRuntimeConfig.js +1 -0
- package/dist/generated/JsRuntimeLimitsConfig.d.ts +7 -0
- package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
- package/dist/generated/JsRuntimePlatform.d.ts +1 -0
- package/dist/generated/JsRuntimePlatform.js +2 -0
- package/dist/generated/MountPluginDescriptor.d.ts +4 -0
- package/dist/generated/MountPluginDescriptor.js +2 -0
- package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
- package/dist/generated/NativeRootFilesystemConfig.js +1 -0
- package/dist/generated/PatternPermissionRule.d.ts +6 -0
- package/dist/generated/PatternPermissionRule.js +1 -0
- package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
- package/dist/generated/PatternPermissionRuleSet.js +1 -0
- package/dist/generated/PatternPermissionScope.d.ts +3 -0
- package/dist/generated/PatternPermissionScope.js +1 -0
- package/dist/generated/PermissionMode.d.ts +1 -0
- package/dist/generated/PermissionMode.js +2 -0
- package/dist/generated/PermissionsPolicy.d.ts +10 -0
- package/dist/generated/PermissionsPolicy.js +1 -0
- package/dist/generated/PluginLimitsConfig.d.ts +4 -0
- package/dist/generated/PluginLimitsConfig.js +2 -0
- package/dist/generated/PythonLimitsConfig.d.ts +5 -0
- package/dist/generated/PythonLimitsConfig.js +2 -0
- package/dist/generated/ResourceLimitsConfig.d.ts +22 -0
- package/dist/generated/ResourceLimitsConfig.js +2 -0
- package/dist/generated/RootFilesystemConfig.d.ts +9 -0
- package/dist/generated/RootFilesystemConfig.js +1 -0
- package/dist/generated/RootFilesystemEntry.d.ts +13 -0
- package/dist/generated/RootFilesystemEntry.js +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
- package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
- package/dist/generated/RootFilesystemEntryKind.js +2 -0
- package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
- package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
- package/dist/generated/RootFilesystemMode.d.ts +1 -0
- package/dist/generated/RootFilesystemMode.js +2 -0
- package/dist/generated/ToolLimitsConfig.d.ts +10 -0
- package/dist/generated/ToolLimitsConfig.js +2 -0
- package/dist/generated/VmDnsConfig.d.ts +6 -0
- package/dist/generated/VmDnsConfig.js +2 -0
- package/dist/generated/VmLimitsConfig.d.ts +18 -0
- package/dist/generated/VmLimitsConfig.js +1 -0
- package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
- package/dist/generated/VmListenPolicyConfig.js +2 -0
- package/dist/generated/WasmLimitsConfig.d.ts +5 -0
- package/dist/generated/WasmLimitsConfig.js +2 -0
- package/dist/generated-protocol.d.ts +1037 -0
- package/dist/generated-protocol.js +2887 -0
- package/dist/index.d.ts +24 -62
- package/dist/index.js +24 -53
- package/dist/json.d.ts +2 -0
- package/dist/json.js +20 -0
- package/dist/kernel-proxy.d.ts +149 -0
- package/dist/kernel-proxy.js +1733 -0
- package/dist/native-client.d.ts +41 -0
- package/dist/native-client.js +124 -0
- package/dist/node-runtime.d.ts +490 -0
- package/dist/node-runtime.js +585 -0
- package/dist/numbers.d.ts +1 -0
- package/dist/numbers.js +8 -0
- package/dist/ownership.d.ts +18 -0
- package/dist/ownership.js +77 -0
- package/dist/permissions.d.ts +29 -0
- package/dist/permissions.js +68 -0
- package/dist/process.d.ts +35 -0
- package/dist/process.js +125 -0
- package/dist/protocol-client.d.ts +46 -0
- package/dist/protocol-client.js +180 -0
- package/dist/protocol-frames.d.ts +68 -0
- package/dist/protocol-frames.js +139 -0
- package/dist/protocol-maps.d.ts +28 -0
- package/dist/protocol-maps.js +217 -0
- package/dist/protocol-schema.d.ts +10 -0
- package/dist/protocol-schema.js +11 -0
- package/dist/request-payloads.d.ts +137 -0
- package/dist/request-payloads.js +210 -0
- package/dist/response-payloads.d.ts +107 -0
- package/dist/response-payloads.js +161 -0
- package/dist/sidecar-client.d.ts +242 -0
- package/dist/sidecar-client.js +797 -0
- package/dist/state.d.ts +40 -0
- package/dist/state.js +44 -0
- package/dist/test-runtime.d.ts +526 -0
- package/dist/test-runtime.js +2119 -0
- package/dist/vm-config.d.ts +31 -0
- package/dist/vm-config.js +1 -0
- package/fixtures/alpine-defaults.json +520 -0
- package/fixtures/base-filesystem.json +528 -0
- package/package.json +193 -115
- package/LICENSE +0 -191
- package/dist/bridge-setup.d.ts +0 -6
- package/dist/bridge-setup.js +0 -9
- package/dist/esm-compiler.d.ts +0 -18
- package/dist/esm-compiler.js +0 -72
- package/dist/fs-helpers.d.ts +0 -23
- package/dist/fs-helpers.js +0 -41
- package/dist/generated/isolate-runtime.d.ts +0 -19
- package/dist/generated/isolate-runtime.js +0 -21
- package/dist/generated/polyfills.d.ts +0 -82
- package/dist/generated/polyfills.js +0 -82
- package/dist/isolate-runtime/apply-custom-global-policy.js +0 -53
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +0 -130
- package/dist/isolate-runtime/apply-timing-mitigation-off.js +0 -14
- package/dist/isolate-runtime/bridge-attach.js +0 -29
- package/dist/isolate-runtime/bridge-initial-globals.js +0 -385
- package/dist/isolate-runtime/eval-script-result.js +0 -8
- package/dist/isolate-runtime/global-exposure-helpers.js +0 -36
- package/dist/isolate-runtime/init-commonjs-module-globals.js +0 -28
- package/dist/isolate-runtime/override-process-cwd.js +0 -8
- package/dist/isolate-runtime/override-process-env.js +0 -8
- package/dist/isolate-runtime/require-setup.js +0 -4153
- package/dist/isolate-runtime/set-commonjs-file-globals.js +0 -36
- package/dist/isolate-runtime/set-stdin-data.js +0 -10
- package/dist/isolate-runtime/setup-dynamic-import.js +0 -123
- package/dist/isolate-runtime/setup-fs-facade.js +0 -87
- package/dist/kernel/command-registry.d.ts +0 -44
- package/dist/kernel/command-registry.js +0 -114
- package/dist/kernel/device-backend.d.ts +0 -14
- package/dist/kernel/device-backend.js +0 -251
- package/dist/kernel/device-layer.d.ts +0 -12
- package/dist/kernel/device-layer.js +0 -271
- package/dist/kernel/dns-cache.d.ts +0 -29
- package/dist/kernel/dns-cache.js +0 -52
- package/dist/kernel/fd-table.d.ts +0 -84
- package/dist/kernel/fd-table.js +0 -278
- package/dist/kernel/file-lock.d.ts +0 -34
- package/dist/kernel/file-lock.js +0 -122
- package/dist/kernel/host-adapter.d.ts +0 -50
- package/dist/kernel/host-adapter.js +0 -8
- package/dist/kernel/index.d.ts +0 -36
- package/dist/kernel/index.js +0 -34
- package/dist/kernel/kernel.d.ts +0 -9
- package/dist/kernel/kernel.js +0 -1415
- package/dist/kernel/mount-table.d.ts +0 -75
- package/dist/kernel/mount-table.js +0 -353
- package/dist/kernel/permissions.d.ts +0 -36
- package/dist/kernel/permissions.js +0 -150
- package/dist/kernel/pipe-manager.d.ts +0 -64
- package/dist/kernel/pipe-manager.js +0 -267
- package/dist/kernel/proc-backend.d.ts +0 -30
- package/dist/kernel/proc-backend.js +0 -428
- package/dist/kernel/proc-layer.d.ts +0 -11
- package/dist/kernel/proc-layer.js +0 -507
- package/dist/kernel/process-table.d.ts +0 -126
- package/dist/kernel/process-table.js +0 -651
- package/dist/kernel/pty.d.ts +0 -109
- package/dist/kernel/pty.js +0 -552
- package/dist/kernel/socket-table.d.ts +0 -312
- package/dist/kernel/socket-table.js +0 -1188
- package/dist/kernel/timer-table.d.ts +0 -54
- package/dist/kernel/timer-table.js +0 -108
- package/dist/kernel/types.d.ts +0 -541
- package/dist/kernel/types.js +0 -98
- package/dist/kernel/user.d.ts +0 -29
- package/dist/kernel/user.js +0 -35
- package/dist/kernel/vfs.d.ts +0 -82
- package/dist/kernel/vfs.js +0 -25
- package/dist/kernel/wait.d.ts +0 -45
- package/dist/kernel/wait.js +0 -112
- package/dist/kernel/wstatus.d.ts +0 -21
- package/dist/kernel/wstatus.js +0 -33
- package/dist/module-resolver.d.ts +0 -29
- package/dist/module-resolver.js +0 -314
- package/dist/package-bundler.d.ts +0 -41
- package/dist/package-bundler.js +0 -497
- package/dist/runtime-driver.d.ts +0 -66
- package/dist/shared/api-types.d.ts +0 -83
- package/dist/shared/bridge-contract.d.ts +0 -772
- package/dist/shared/bridge-contract.js +0 -169
- package/dist/shared/console-formatter.d.ts +0 -22
- package/dist/shared/console-formatter.js +0 -161
- package/dist/shared/constants.d.ts +0 -3
- package/dist/shared/constants.js +0 -3
- package/dist/shared/errors.d.ts +0 -16
- package/dist/shared/errors.js +0 -21
- package/dist/shared/esm-utils.d.ts +0 -28
- package/dist/shared/esm-utils.js +0 -97
- package/dist/shared/global-exposure.d.ts +0 -38
- package/dist/shared/global-exposure.js +0 -876
- package/dist/shared/in-memory-fs.d.ts +0 -16
- package/dist/shared/in-memory-fs.js +0 -115
- package/dist/shared/permissions.d.ts +0 -36
- package/dist/shared/permissions.js +0 -314
- package/dist/shared/require-setup.d.ts +0 -6
- package/dist/shared/require-setup.js +0 -9
- package/dist/test/block-store-conformance.d.ts +0 -34
- package/dist/test/block-store-conformance.js +0 -251
- package/dist/test/metadata-store-conformance.d.ts +0 -37
- package/dist/test/metadata-store-conformance.js +0 -646
- package/dist/test/vfs-conformance.d.ts +0 -65
- package/dist/test/vfs-conformance.js +0 -842
- package/dist/types.d.ts +0 -98
- package/dist/types.js +0 -6
- package/dist/vfs/chunked-vfs.d.ts +0 -66
- package/dist/vfs/chunked-vfs.js +0 -1290
- package/dist/vfs/host-block-store.d.ts +0 -19
- package/dist/vfs/host-block-store.js +0 -97
- package/dist/vfs/memory-block-store.d.ts +0 -16
- package/dist/vfs/memory-block-store.js +0 -45
- package/dist/vfs/memory-metadata.d.ts +0 -75
- package/dist/vfs/memory-metadata.js +0 -528
- package/dist/vfs/sqlite-metadata.d.ts +0 -91
- package/dist/vfs/sqlite-metadata.js +0 -582
- package/dist/vfs/types.d.ts +0 -210
- package/dist/vfs/types.js +0 -8
- /package/dist/{runtime-driver.js → generated/CreateVmConfig.js} +0 -0
- /package/dist/{shared/api-types.js → generated/FsPermissionRule.js} +0 -0
package/dist/kernel/kernel.js
DELETED
|
@@ -1,1415 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Kernel implementation.
|
|
3
|
-
*
|
|
4
|
-
* The kernel is the OS. It owns VFS, FD table, process table, device layer,
|
|
5
|
-
* pipe manager, command registry, and permissions. Runtimes are execution
|
|
6
|
-
* engines that make "syscalls" to the kernel.
|
|
7
|
-
*/
|
|
8
|
-
import { createDeviceBackend } from "./device-backend.js";
|
|
9
|
-
import { createProcBackend } from "./proc-backend.js";
|
|
10
|
-
import { MountTable } from "./mount-table.js";
|
|
11
|
-
import { FDTableManager } from "./fd-table.js";
|
|
12
|
-
import { ProcessTable } from "./process-table.js";
|
|
13
|
-
import { PipeManager } from "./pipe-manager.js";
|
|
14
|
-
import { PtyManager } from "./pty.js";
|
|
15
|
-
import { FileLockManager } from "./file-lock.js";
|
|
16
|
-
import { CommandRegistry } from "./command-registry.js";
|
|
17
|
-
import { wrapFileSystem, checkChildProcess } from "./permissions.js";
|
|
18
|
-
import { UserManager } from "./user.js";
|
|
19
|
-
import { SocketTable } from "./socket-table.js";
|
|
20
|
-
import { TimerTable } from "./timer-table.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";
|
|
22
|
-
export function createKernel(options) {
|
|
23
|
-
return new KernelImpl(options);
|
|
24
|
-
}
|
|
25
|
-
class KernelImpl {
|
|
26
|
-
vfs;
|
|
27
|
-
mountTable;
|
|
28
|
-
fdTableManager = new FDTableManager();
|
|
29
|
-
processTable;
|
|
30
|
-
pipeManager = new PipeManager();
|
|
31
|
-
ptyManager;
|
|
32
|
-
fileLockManager = new FileLockManager();
|
|
33
|
-
commandRegistry = new CommandRegistry();
|
|
34
|
-
socketTable;
|
|
35
|
-
timerTable;
|
|
36
|
-
userManager;
|
|
37
|
-
drivers = [];
|
|
38
|
-
driverPids = new Map();
|
|
39
|
-
permissions;
|
|
40
|
-
maxProcesses;
|
|
41
|
-
env;
|
|
42
|
-
cwd;
|
|
43
|
-
disposed = false;
|
|
44
|
-
pendingBinEntries = [];
|
|
45
|
-
posixDirsReady;
|
|
46
|
-
log;
|
|
47
|
-
constructor(options) {
|
|
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({
|
|
64
|
-
processTable: this.processTable,
|
|
65
|
-
fdTableManager: this.fdTableManager,
|
|
66
|
-
hostname: options.env?.HOSTNAME,
|
|
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;
|
|
78
|
-
if (options.permissions) {
|
|
79
|
-
fs = wrapFileSystem(fs, options.permissions);
|
|
80
|
-
}
|
|
81
|
-
this.vfs = fs;
|
|
82
|
-
this.permissions = options.permissions;
|
|
83
|
-
this.maxProcesses = options.maxProcesses;
|
|
84
|
-
this.env = { ...options.env };
|
|
85
|
-
this.cwd = options.cwd ?? "/home/user";
|
|
86
|
-
this.userManager = new UserManager();
|
|
87
|
-
this.socketTable = new SocketTable({
|
|
88
|
-
vfs: this.vfs,
|
|
89
|
-
networkCheck: options.permissions?.network,
|
|
90
|
-
hostAdapter: options.hostNetworkAdapter,
|
|
91
|
-
getSignalState: (pid) => this.processTable.getSignalState(pid),
|
|
92
|
-
processExists: (pid) => this.processTable.get(pid) !== undefined,
|
|
93
|
-
});
|
|
94
|
-
this.timerTable = new TimerTable();
|
|
95
|
-
// Clean up FD table and sockets when a process exits
|
|
96
|
-
this.processTable.onProcessExit = (pid) => {
|
|
97
|
-
this.log.debug({ pid }, "process exit cleanup");
|
|
98
|
-
this.cleanupProcessFDs(pid);
|
|
99
|
-
this.socketTable.closeAllForProcess(pid);
|
|
100
|
-
this.timerTable.clearAllForProcess(pid);
|
|
101
|
-
};
|
|
102
|
-
// Clean up driver PID ownership when zombie is reaped
|
|
103
|
-
this.processTable.onProcessReap = (pid) => {
|
|
104
|
-
const entry = this.processTable.get(pid);
|
|
105
|
-
if (entry)
|
|
106
|
-
this.driverPids.get(entry.driver)?.delete(pid);
|
|
107
|
-
};
|
|
108
|
-
// Deliver SIGPIPE default action: terminate writer with 128+SIGPIPE
|
|
109
|
-
this.pipeManager.onBrokenPipe = (pid) => {
|
|
110
|
-
try {
|
|
111
|
-
this.processTable.kill(pid, SIGPIPE);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// Process may already be exited
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
// Create standard POSIX directory hierarchy so all programs see /tmp,
|
|
118
|
-
// /usr, /etc, etc. — matching a real Linux root filesystem layout.
|
|
119
|
-
this.posixDirsReady = this.initPosixDirs();
|
|
120
|
-
}
|
|
121
|
-
async initPosixDirs() {
|
|
122
|
-
// /dev and /proc are auto-created by MountTable mounts — don't create them here.
|
|
123
|
-
const dirs = [
|
|
124
|
-
"/tmp",
|
|
125
|
-
"/bin",
|
|
126
|
-
"/lib",
|
|
127
|
-
"/sbin",
|
|
128
|
-
"/boot",
|
|
129
|
-
"/etc",
|
|
130
|
-
"/root",
|
|
131
|
-
"/run",
|
|
132
|
-
"/srv",
|
|
133
|
-
"/sys",
|
|
134
|
-
"/opt",
|
|
135
|
-
"/mnt",
|
|
136
|
-
"/media",
|
|
137
|
-
"/home",
|
|
138
|
-
"/usr",
|
|
139
|
-
"/usr/bin",
|
|
140
|
-
"/usr/games",
|
|
141
|
-
"/usr/include",
|
|
142
|
-
"/usr/lib",
|
|
143
|
-
"/usr/libexec",
|
|
144
|
-
"/usr/man",
|
|
145
|
-
"/usr/sbin",
|
|
146
|
-
"/usr/share",
|
|
147
|
-
"/usr/share/man",
|
|
148
|
-
"/var",
|
|
149
|
-
"/var/cache",
|
|
150
|
-
"/var/empty",
|
|
151
|
-
"/var/lib",
|
|
152
|
-
"/var/lock",
|
|
153
|
-
"/var/log",
|
|
154
|
-
"/var/run",
|
|
155
|
-
"/var/spool",
|
|
156
|
-
"/var/tmp",
|
|
157
|
-
];
|
|
158
|
-
for (const dir of dirs) {
|
|
159
|
-
try {
|
|
160
|
-
await this.vfs.mkdir(dir, { recursive: true });
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
// Directory may already exist
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// Standard utility that many scripts expect
|
|
167
|
-
try {
|
|
168
|
-
await this.vfs.writeFile("/usr/bin/env", new Uint8Array(1));
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// File may already exist
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// -----------------------------------------------------------------------
|
|
175
|
-
// Kernel public API
|
|
176
|
-
// -----------------------------------------------------------------------
|
|
177
|
-
async mount(driver) {
|
|
178
|
-
this.assertNotDisposed();
|
|
179
|
-
await this.posixDirsReady;
|
|
180
|
-
this.log.debug({ driver: driver.name, commands: driver.commands }, "mounting runtime driver");
|
|
181
|
-
// Track PIDs owned by this driver
|
|
182
|
-
if (!this.driverPids.has(driver.name)) {
|
|
183
|
-
this.driverPids.set(driver.name, new Set());
|
|
184
|
-
}
|
|
185
|
-
// Initialize the driver with a scoped kernel interface
|
|
186
|
-
await driver.init(this.createKernelInterface(driver.name));
|
|
187
|
-
// Register commands
|
|
188
|
-
this.commandRegistry.register(driver);
|
|
189
|
-
this.drivers.push(driver);
|
|
190
|
-
// Populate /bin stubs for shell PATH lookup
|
|
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);
|
|
201
|
-
}
|
|
202
|
-
async dispose() {
|
|
203
|
-
if (this.disposed)
|
|
204
|
-
return;
|
|
205
|
-
this.disposed = true;
|
|
206
|
-
this.log.info({}, "kernel disposing");
|
|
207
|
-
// Terminate all running processes
|
|
208
|
-
await this.processTable.terminateAll();
|
|
209
|
-
// Clean up all sockets
|
|
210
|
-
this.socketTable.disposeAll();
|
|
211
|
-
this.timerTable.disposeAll();
|
|
212
|
-
// Dispose all drivers (reverse mount order)
|
|
213
|
-
for (let i = this.drivers.length - 1; i >= 0; i--) {
|
|
214
|
-
try {
|
|
215
|
-
await this.drivers[i].dispose();
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// Best effort cleanup
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
this.drivers.length = 0;
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Flush pending /bin stub entries created by on-demand command discovery.
|
|
225
|
-
* Ensures VFS is consistent before shell PATH lookups.
|
|
226
|
-
*/
|
|
227
|
-
async flushPendingBinEntries() {
|
|
228
|
-
if (this.pendingBinEntries.length > 0) {
|
|
229
|
-
await Promise.all(this.pendingBinEntries);
|
|
230
|
-
this.pendingBinEntries.length = 0;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async exec(command, options) {
|
|
234
|
-
this.assertNotDisposed();
|
|
235
|
-
this.log.debug({ command, timeout: options?.timeout, cwd: options?.cwd }, "exec start");
|
|
236
|
-
// Flush pending /bin stubs before shell PATH lookup
|
|
237
|
-
await this.flushPendingBinEntries();
|
|
238
|
-
// Route through shell
|
|
239
|
-
const shell = this.commandRegistry.resolve("sh");
|
|
240
|
-
if (!shell) {
|
|
241
|
-
throw new Error("No shell available. Mount a WasmVM runtime to enable exec().");
|
|
242
|
-
}
|
|
243
|
-
const proc = this.spawnInternal("sh", ["-c", command], options);
|
|
244
|
-
// Write stdin if provided
|
|
245
|
-
if (options?.stdin) {
|
|
246
|
-
const data = typeof options.stdin === "string"
|
|
247
|
-
? new TextEncoder().encode(options.stdin)
|
|
248
|
-
: options.stdin;
|
|
249
|
-
proc.writeStdin(data);
|
|
250
|
-
proc.closeStdin();
|
|
251
|
-
}
|
|
252
|
-
// Collect output
|
|
253
|
-
const stdoutChunks = [];
|
|
254
|
-
const stderrChunks = [];
|
|
255
|
-
proc.onStdout = (data) => {
|
|
256
|
-
stdoutChunks.push(data);
|
|
257
|
-
options?.onStdout?.(data);
|
|
258
|
-
};
|
|
259
|
-
proc.onStderr = (data) => {
|
|
260
|
-
stderrChunks.push(data);
|
|
261
|
-
options?.onStderr?.(data);
|
|
262
|
-
};
|
|
263
|
-
// Wait with optional timeout
|
|
264
|
-
let exitCode;
|
|
265
|
-
if (options?.timeout) {
|
|
266
|
-
let timer;
|
|
267
|
-
try {
|
|
268
|
-
exitCode = await Promise.race([
|
|
269
|
-
proc.wait().then((code) => {
|
|
270
|
-
clearTimeout(timer);
|
|
271
|
-
return code;
|
|
272
|
-
}),
|
|
273
|
-
new Promise((_, reject) => {
|
|
274
|
-
timer = setTimeout(() => {
|
|
275
|
-
// Kill process and detach output callbacks
|
|
276
|
-
this.log.warn({ command, timeout: options.timeout }, "exec timeout, sending SIGTERM");
|
|
277
|
-
proc.onStdout = null;
|
|
278
|
-
proc.onStderr = null;
|
|
279
|
-
proc.kill(SIGTERM);
|
|
280
|
-
reject(new KernelError("ETIMEDOUT", "exec timeout"));
|
|
281
|
-
}, options.timeout);
|
|
282
|
-
}),
|
|
283
|
-
]);
|
|
284
|
-
}
|
|
285
|
-
catch (err) {
|
|
286
|
-
clearTimeout(timer);
|
|
287
|
-
throw err;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
exitCode = await proc.wait();
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
exitCode,
|
|
295
|
-
stdout: concatUint8(stdoutChunks),
|
|
296
|
-
stderr: concatUint8(stderrChunks),
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
spawn(command, args, options) {
|
|
300
|
-
this.assertNotDisposed();
|
|
301
|
-
return this.spawnManaged(command, args, options);
|
|
302
|
-
}
|
|
303
|
-
openShell(options) {
|
|
304
|
-
this.assertNotDisposed();
|
|
305
|
-
const command = options?.command ?? "sh";
|
|
306
|
-
const args = options?.args ?? [];
|
|
307
|
-
this.log.debug({ command, args, cols: options?.cols, rows: options?.rows, cwd: options?.cwd }, "openShell start");
|
|
308
|
-
// Allocate a controller PID with an FD table to hold the PTY master
|
|
309
|
-
const controllerPid = this.processTable.allocatePid();
|
|
310
|
-
const controllerTable = this.fdTableManager.create(controllerPid);
|
|
311
|
-
// Create PTY pair in the controller's FD table
|
|
312
|
-
const { masterFd, slaveFd } = this.ptyManager.createPtyFDs(controllerTable);
|
|
313
|
-
const masterDescId = controllerTable.get(masterFd).description.id;
|
|
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);
|
|
323
|
-
const proc = this.spawnInternal(command, args, {
|
|
324
|
-
env: { ...options?.env, ...dimEnv },
|
|
325
|
-
cwd: options?.cwd,
|
|
326
|
-
stdinFd: slaveFd,
|
|
327
|
-
stdoutFd: slaveFd,
|
|
328
|
-
stderrFd: slaveFd,
|
|
329
|
-
}, controllerPid);
|
|
330
|
-
// Shell becomes its own process group leader, set as PTY foreground
|
|
331
|
-
this.processTable.setpgid(proc.pid, proc.pid);
|
|
332
|
-
this.ptyManager.setForegroundPgid(masterDescId, proc.pid);
|
|
333
|
-
this.ptyManager.setSessionLeader(masterDescId, proc.pid);
|
|
334
|
-
this.log.debug({ shellPid: proc.pid, controllerPid, masterFd, masterDescId }, "openShell PTY attached");
|
|
335
|
-
// Close controller's copy of slave FD (child inherited its own copy via fork).
|
|
336
|
-
// Without this, slave refCount stays >0 after shell exits, preventing EOF on master.
|
|
337
|
-
const slaveEntry = controllerTable.get(slaveFd);
|
|
338
|
-
const slaveDescId = slaveEntry.description.id;
|
|
339
|
-
controllerTable.close(slaveFd);
|
|
340
|
-
if (slaveEntry.description.refCount <= 0) {
|
|
341
|
-
this.ptyManager.close(slaveDescId);
|
|
342
|
-
}
|
|
343
|
-
// Start read pump: master reads → onData callback
|
|
344
|
-
// Use object wrapper so TypeScript doesn't narrow to null in the async closure
|
|
345
|
-
const pump = { onData: null, exited: false };
|
|
346
|
-
const pumpPromise = (async () => {
|
|
347
|
-
try {
|
|
348
|
-
while (!pump.exited) {
|
|
349
|
-
const data = await this.ptyManager.read(masterDescId, 4096);
|
|
350
|
-
if (!data || data.length === 0)
|
|
351
|
-
break;
|
|
352
|
-
try {
|
|
353
|
-
pump.onData?.(data);
|
|
354
|
-
}
|
|
355
|
-
catch (cbErr) {
|
|
356
|
-
// Propagate callback errors — don't silently swallow
|
|
357
|
-
console.error("openShell readPump: onData callback error:", cbErr);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
catch (err) {
|
|
362
|
-
// Master closed or PTY gone — expected when shell exits
|
|
363
|
-
if (pump.exited)
|
|
364
|
-
return;
|
|
365
|
-
console.error("openShell readPump: PTY read error:", err);
|
|
366
|
-
}
|
|
367
|
-
})();
|
|
368
|
-
// wait() resolves after both shell exit AND pump drain
|
|
369
|
-
const waitPromise = proc.wait().then(async (exitCode) => {
|
|
370
|
-
pump.exited = true;
|
|
371
|
-
// Wait for pump to finish delivering remaining data
|
|
372
|
-
await pumpPromise;
|
|
373
|
-
// Clean up controller PID's FD table (incl. PTY master)
|
|
374
|
-
this.cleanupProcessFDs(controllerPid);
|
|
375
|
-
return exitCode;
|
|
376
|
-
});
|
|
377
|
-
return {
|
|
378
|
-
pid: proc.pid,
|
|
379
|
-
write: (data) => {
|
|
380
|
-
const bytes = typeof data === "string"
|
|
381
|
-
? new TextEncoder().encode(data)
|
|
382
|
-
: data;
|
|
383
|
-
this.ptyManager.write(masterDescId, bytes);
|
|
384
|
-
},
|
|
385
|
-
get onData() { return pump.onData; },
|
|
386
|
-
set onData(fn) { pump.onData = fn; },
|
|
387
|
-
resize: (_cols, _rows) => {
|
|
388
|
-
const fgPgid = this.ptyManager.getForegroundPgid(masterDescId);
|
|
389
|
-
this.log.trace({ shellPid: proc.pid, cols: _cols, rows: _rows, fgPgid }, "PTY resize");
|
|
390
|
-
if (fgPgid > 0) {
|
|
391
|
-
try {
|
|
392
|
-
this.processTable.kill(-fgPgid, SIGWINCH);
|
|
393
|
-
}
|
|
394
|
-
catch { /* pgid may be gone */ }
|
|
395
|
-
}
|
|
396
|
-
},
|
|
397
|
-
kill: (signal) => {
|
|
398
|
-
proc.kill(signal ?? SIGTERM);
|
|
399
|
-
},
|
|
400
|
-
wait: () => waitPromise,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
async connectTerminal(options) {
|
|
404
|
-
this.assertNotDisposed();
|
|
405
|
-
this.log.debug({ command: options?.command, cols: options?.cols, rows: options?.rows }, "connectTerminal start");
|
|
406
|
-
const stdin = process.stdin;
|
|
407
|
-
const stdout = process.stdout;
|
|
408
|
-
const isTTY = stdin.isTTY;
|
|
409
|
-
let onStdinData;
|
|
410
|
-
let onResize;
|
|
411
|
-
try {
|
|
412
|
-
const shell = this.openShell(options);
|
|
413
|
-
// Set raw mode so keypresses pass through directly
|
|
414
|
-
if (isTTY)
|
|
415
|
-
stdin.setRawMode(true);
|
|
416
|
-
// Forward stdin to shell
|
|
417
|
-
onStdinData = (data) => shell.write(data);
|
|
418
|
-
stdin.on("data", onStdinData);
|
|
419
|
-
stdin.resume();
|
|
420
|
-
// Forward shell output to stdout or custom handler
|
|
421
|
-
const outputHandler = options?.onData
|
|
422
|
-
?? ((data) => { stdout.write(data); });
|
|
423
|
-
shell.onData = outputHandler;
|
|
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
|
-
}
|
|
431
|
-
return await shell.wait();
|
|
432
|
-
}
|
|
433
|
-
finally {
|
|
434
|
-
// Restore terminal — guard each cleanup since setup may have partially completed
|
|
435
|
-
if (onStdinData)
|
|
436
|
-
stdin.removeListener("data", onStdinData);
|
|
437
|
-
stdin.pause();
|
|
438
|
-
if (isTTY)
|
|
439
|
-
stdin.setRawMode(false);
|
|
440
|
-
if (onResize && stdout.isTTY)
|
|
441
|
-
stdout.removeListener("resize", onResize);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Filesystem convenience wrappers
|
|
445
|
-
readFile(path) { return this.vfs.readFile(path); }
|
|
446
|
-
writeFile(path, content) { return this.vfs.writeFile(path, content); }
|
|
447
|
-
mkdir(path) { return this.vfs.mkdir(path); }
|
|
448
|
-
readdir(path) { return this.vfs.readDir(path); }
|
|
449
|
-
stat(path) { return this.vfs.stat(path); }
|
|
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); }
|
|
454
|
-
// Introspection
|
|
455
|
-
get commands() {
|
|
456
|
-
return this.commandRegistry.list();
|
|
457
|
-
}
|
|
458
|
-
get processes() {
|
|
459
|
-
return this.processTable.listProcesses();
|
|
460
|
-
}
|
|
461
|
-
get zombieTimerCount() {
|
|
462
|
-
return this.processTable.zombieTimerCount;
|
|
463
|
-
}
|
|
464
|
-
// -----------------------------------------------------------------------
|
|
465
|
-
// Internal spawn
|
|
466
|
-
// -----------------------------------------------------------------------
|
|
467
|
-
spawnInternal(command, args, options, callerPid) {
|
|
468
|
-
this.log.debug({ command, args, callerPid, cwd: options?.cwd }, "spawn start");
|
|
469
|
-
let driver = this.commandRegistry.resolve(command);
|
|
470
|
-
// On-demand discovery: ask mounted drivers to resolve unknown commands
|
|
471
|
-
if (!driver) {
|
|
472
|
-
const basename = command.includes("/")
|
|
473
|
-
? command.split("/").pop()
|
|
474
|
-
: command;
|
|
475
|
-
if (basename) {
|
|
476
|
-
for (const d of this.drivers) {
|
|
477
|
-
if (d.tryResolve?.(basename)) {
|
|
478
|
-
this.commandRegistry.registerCommand(basename, d);
|
|
479
|
-
// Store pending promise so exec() can flush before shell PATH lookup
|
|
480
|
-
const p = this.commandRegistry.populateBinEntry(this.vfs, basename);
|
|
481
|
-
this.pendingBinEntries.push(p);
|
|
482
|
-
p.then(() => {
|
|
483
|
-
const idx = this.pendingBinEntries.indexOf(p);
|
|
484
|
-
if (idx >= 0)
|
|
485
|
-
this.pendingBinEntries.splice(idx, 1);
|
|
486
|
-
});
|
|
487
|
-
driver = d;
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
if (!driver) {
|
|
494
|
-
this.log.warn({ command }, "command not found");
|
|
495
|
-
throw new KernelError("ENOENT", `command not found: ${command}`);
|
|
496
|
-
}
|
|
497
|
-
// Check childProcess permission
|
|
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
|
-
}
|
|
505
|
-
// Enforce maxProcesses budget
|
|
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");
|
|
508
|
-
throw new KernelError("EAGAIN", "maximum process limit reached");
|
|
509
|
-
}
|
|
510
|
-
// Allocate PID atomically
|
|
511
|
-
const pid = this.processTable.allocatePid();
|
|
512
|
-
// Register PID ownership before driver.spawn() so the driver can use it
|
|
513
|
-
this.driverPids.get(driver.name)?.add(pid);
|
|
514
|
-
// Cross-runtime spawn: parent driver must also track child PID so
|
|
515
|
-
// it can waitpid/kill/interact with the child process
|
|
516
|
-
if (callerPid !== undefined) {
|
|
517
|
-
for (const [name, pids] of this.driverPids) {
|
|
518
|
-
if (name !== driver.name && pids.has(callerPid)) {
|
|
519
|
-
pids.add(pid);
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
// Create FD table — wire pipe FDs when overrides are provided
|
|
525
|
-
const table = this.createChildFDTable(pid, options, callerPid);
|
|
526
|
-
// Check which stdio channels are piped (data flows through kernel, not callbacks)
|
|
527
|
-
const stdoutPiped = this.isStdioPiped(table, 1);
|
|
528
|
-
const stderrPiped = this.isStdioPiped(table, 2);
|
|
529
|
-
// Buffer stdout/stderr — wired before spawn so nothing is lost
|
|
530
|
-
const stdoutBuf = [];
|
|
531
|
-
const stderrBuf = [];
|
|
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;
|
|
543
|
-
if (stdoutPiped) {
|
|
544
|
-
ctxStdoutCb = this.createPipedOutputCallback(table, 1, pid);
|
|
545
|
-
}
|
|
546
|
-
else if (!options?.onStdout && callerPid !== undefined) {
|
|
547
|
-
const parent = this.processTable.get(callerPid);
|
|
548
|
-
if (parent?.driverProcess.onStdout) {
|
|
549
|
-
ctxStdoutCb = parent.driverProcess.onStdout;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
if (stderrPiped) {
|
|
553
|
-
ctxStderrCb = this.createPipedOutputCallback(table, 2, pid);
|
|
554
|
-
}
|
|
555
|
-
else if (!options?.onStderr && callerPid !== undefined) {
|
|
556
|
-
const parent = this.processTable.get(callerPid);
|
|
557
|
-
if (parent?.driverProcess.onStderr) {
|
|
558
|
-
ctxStderrCb = parent.driverProcess.onStderr;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
// Inherit env from parent process if spawned by another process, else use kernel defaults
|
|
562
|
-
const parentEntry = callerPid ? this.processTable.get(callerPid) : undefined;
|
|
563
|
-
const baseEnv = parentEntry?.env ?? this.env;
|
|
564
|
-
// Detect PTY slave on stdio FDs
|
|
565
|
-
const stdinIsTTY = this.isFdPtySlave(table, 0);
|
|
566
|
-
const stdoutIsTTY = this.isFdPtySlave(table, 1);
|
|
567
|
-
const stderrIsTTY = this.isFdPtySlave(table, 2);
|
|
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;
|
|
572
|
-
const ctx = {
|
|
573
|
-
pid,
|
|
574
|
-
ppid: callerPid ?? 0,
|
|
575
|
-
env: { ...baseEnv, ...options?.env, PWD: resolvedCwd },
|
|
576
|
-
cwd: resolvedCwd,
|
|
577
|
-
fds: { stdin: 0, stdout: 1, stderr: 2 },
|
|
578
|
-
stdinIsTTY,
|
|
579
|
-
stdoutIsTTY,
|
|
580
|
-
stderrIsTTY,
|
|
581
|
-
streamStdin: options?.streamStdin,
|
|
582
|
-
onStdout: ctxStdoutCb ?? (stdoutPiped ? undefined : (data) => stdoutBuf.push(data)),
|
|
583
|
-
onStderr: ctxStderrCb ?? (stderrPiped ? undefined : (data) => stderrBuf.push(data)),
|
|
584
|
-
};
|
|
585
|
-
// Spawn via driver
|
|
586
|
-
const driverProcess = driver.spawn(command, args, ctx);
|
|
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.
|
|
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));
|
|
607
|
-
}
|
|
608
|
-
// Register in process table
|
|
609
|
-
const entry = this.processTable.register(pid, driver.name, command, args, ctx, driverProcess);
|
|
610
|
-
return {
|
|
611
|
-
pid: entry.pid,
|
|
612
|
-
driverProcess,
|
|
613
|
-
wait: () => driverProcess.wait(),
|
|
614
|
-
writeStdin: (data) => driverProcess.writeStdin(data),
|
|
615
|
-
closeStdin: () => driverProcess.closeStdin(),
|
|
616
|
-
kill: (signal) => driverProcess.kill(signal ?? 15),
|
|
617
|
-
get onStdout() { return driverProcess.onStdout; },
|
|
618
|
-
set onStdout(fn) {
|
|
619
|
-
driverProcess.onStdout = fn;
|
|
620
|
-
// Replay buffered data
|
|
621
|
-
if (fn)
|
|
622
|
-
for (const chunk of stdoutBuf)
|
|
623
|
-
fn(chunk);
|
|
624
|
-
stdoutBuf.length = 0;
|
|
625
|
-
},
|
|
626
|
-
get onStderr() { return driverProcess.onStderr; },
|
|
627
|
-
set onStderr(fn) {
|
|
628
|
-
driverProcess.onStderr = fn;
|
|
629
|
-
if (fn)
|
|
630
|
-
for (const chunk of stderrBuf)
|
|
631
|
-
fn(chunk);
|
|
632
|
-
stderrBuf.length = 0;
|
|
633
|
-
},
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
spawnManaged(command, args, options, callerPid) {
|
|
637
|
-
const internal = this.spawnInternal(command, args, options, callerPid);
|
|
638
|
-
let exitCode = null;
|
|
639
|
-
// Note: options.onStdout/onStderr are already wired through ctx.onStdout
|
|
640
|
-
// by spawnInternal. Do NOT also set them on driverProcess.onStdout here —
|
|
641
|
-
// the driver calls both ctx.onStdout and proc.onStdout per message, so
|
|
642
|
-
// setting both to the same callback would double-deliver output.
|
|
643
|
-
internal.driverProcess.wait().then((code) => {
|
|
644
|
-
exitCode = code;
|
|
645
|
-
});
|
|
646
|
-
return {
|
|
647
|
-
pid: internal.pid,
|
|
648
|
-
writeStdin: (data) => {
|
|
649
|
-
const bytes = typeof data === "string"
|
|
650
|
-
? new TextEncoder().encode(data)
|
|
651
|
-
: data;
|
|
652
|
-
internal.writeStdin(bytes);
|
|
653
|
-
},
|
|
654
|
-
closeStdin: () => internal.closeStdin(),
|
|
655
|
-
kill: (signal) => this.processTable.kill(internal.pid, signal ?? 15),
|
|
656
|
-
wait: () => internal.driverProcess.wait(),
|
|
657
|
-
get exitCode() { return exitCode; },
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
// -----------------------------------------------------------------------
|
|
661
|
-
// Kernel interface (exposed to drivers)
|
|
662
|
-
// -----------------------------------------------------------------------
|
|
663
|
-
createKernelInterface(driverName) {
|
|
664
|
-
// Validate that the calling driver owns the target PID
|
|
665
|
-
const assertOwns = (pid) => {
|
|
666
|
-
if (this.driverPids.get(driverName)?.has(pid))
|
|
667
|
-
return;
|
|
668
|
-
// Check if any driver owns this PID — if not, the PID doesn't exist
|
|
669
|
-
for (const pids of this.driverPids.values()) {
|
|
670
|
-
if (pids.has(pid)) {
|
|
671
|
-
throw new KernelError("EPERM", `driver "${driverName}" does not own PID ${pid}`);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
throw new KernelError("ESRCH", `no such process ${pid}`);
|
|
675
|
-
};
|
|
676
|
-
const kernelInterface = {
|
|
677
|
-
vfs: this.vfs,
|
|
678
|
-
// FD operations
|
|
679
|
-
fdOpen: (pid, path, flags, mode) => {
|
|
680
|
-
assertOwns(pid);
|
|
681
|
-
// /dev/fd/N → dup(N): equivalent to open() on the underlying FD
|
|
682
|
-
if (path.startsWith("/dev/fd/")) {
|
|
683
|
-
const raw = path.slice(8);
|
|
684
|
-
const n = parseInt(raw, 10);
|
|
685
|
-
if (isNaN(n) || n < 0 || String(n) !== raw)
|
|
686
|
-
throw new KernelError("EBADF", `bad file descriptor: ${path}`);
|
|
687
|
-
const table = this.getTable(pid);
|
|
688
|
-
const entry = table.get(n);
|
|
689
|
-
if (!entry)
|
|
690
|
-
throw new KernelError("EBADF", `bad file descriptor ${n}`);
|
|
691
|
-
return table.dup(n);
|
|
692
|
-
}
|
|
693
|
-
const created = (flags & (O_CREAT | O_EXCL | O_TRUNC)) !== 0
|
|
694
|
-
? this.prepareOpenSync(path, flags)
|
|
695
|
-
: false;
|
|
696
|
-
const table = this.getTable(pid);
|
|
697
|
-
const filetype = FILETYPE_REGULAR_FILE;
|
|
698
|
-
const fd = table.open(path, flags, filetype);
|
|
699
|
-
const fdEntry = table.get(fd);
|
|
700
|
-
// Stash the effective mode for the first write that materializes a new file.
|
|
701
|
-
if (created && (flags & O_CREAT)) {
|
|
702
|
-
const entry = this.processTable.get(pid);
|
|
703
|
-
const umask = entry?.umask ?? 0o022;
|
|
704
|
-
const requestedMode = mode ?? 0o666;
|
|
705
|
-
if (fdEntry) {
|
|
706
|
-
fdEntry.description.creationMode = requestedMode & ~umask;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return fd;
|
|
710
|
-
},
|
|
711
|
-
fdRead: async (pid, fd, length) => {
|
|
712
|
-
assertOwns(pid);
|
|
713
|
-
const table = this.getTable(pid);
|
|
714
|
-
const entry = table.get(fd);
|
|
715
|
-
if (!entry)
|
|
716
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
717
|
-
// Pipe reads route through PipeManager
|
|
718
|
-
if (this.pipeManager.isPipe(entry.description.id)) {
|
|
719
|
-
const data = await this.pipeManager.read(entry.description.id, length);
|
|
720
|
-
return data ?? new Uint8Array(0);
|
|
721
|
-
}
|
|
722
|
-
// PTY reads route through PtyManager
|
|
723
|
-
if (this.ptyManager.isPty(entry.description.id)) {
|
|
724
|
-
const data = await this.ptyManager.read(entry.description.id, length);
|
|
725
|
-
return data ?? new Uint8Array(0);
|
|
726
|
-
}
|
|
727
|
-
// Positional read from VFS — avoids loading entire file
|
|
728
|
-
const cursor = Number(entry.description.cursor);
|
|
729
|
-
const slice = await this.preadDescription(entry.description, cursor, length);
|
|
730
|
-
entry.description.cursor += BigInt(slice.length);
|
|
731
|
-
return slice;
|
|
732
|
-
},
|
|
733
|
-
fdWrite: (pid, fd, data) => {
|
|
734
|
-
assertOwns(pid);
|
|
735
|
-
const table = this.getTable(pid);
|
|
736
|
-
const entry = table.get(fd);
|
|
737
|
-
if (!entry)
|
|
738
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
739
|
-
if (this.pipeManager.isPipe(entry.description.id)) {
|
|
740
|
-
return this.pipeManager.write(entry.description.id, data, pid);
|
|
741
|
-
}
|
|
742
|
-
if (this.ptyManager.isPty(entry.description.id)) {
|
|
743
|
-
return this.ptyManager.write(entry.description.id, data);
|
|
744
|
-
}
|
|
745
|
-
// Write to VFS at cursor position (async — returns Promise)
|
|
746
|
-
return this.vfsWrite(entry, data);
|
|
747
|
-
},
|
|
748
|
-
fdClose: (pid, fd) => {
|
|
749
|
-
assertOwns(pid);
|
|
750
|
-
const table = this.getTable(pid);
|
|
751
|
-
const entry = table.get(fd);
|
|
752
|
-
if (!entry)
|
|
753
|
-
return;
|
|
754
|
-
const descId = entry.description.id;
|
|
755
|
-
const isPipe = this.pipeManager.isPipe(descId);
|
|
756
|
-
const isPty = this.ptyManager.isPty(descId);
|
|
757
|
-
// Close FD first (decrements refCount on shared FileDescription)
|
|
758
|
-
table.close(fd);
|
|
759
|
-
// Only signal pipe/pty/lock closure when last reference is dropped
|
|
760
|
-
if (entry.description.refCount <= 0) {
|
|
761
|
-
this.releaseDescriptionInode(entry.description);
|
|
762
|
-
if (isPipe)
|
|
763
|
-
this.pipeManager.close(descId);
|
|
764
|
-
if (isPty)
|
|
765
|
-
this.ptyManager.close(descId);
|
|
766
|
-
this.fileLockManager.releaseByDescription(descId);
|
|
767
|
-
}
|
|
768
|
-
},
|
|
769
|
-
fdSeek: async (pid, fd, offset, whence) => {
|
|
770
|
-
assertOwns(pid);
|
|
771
|
-
const table = this.getTable(pid);
|
|
772
|
-
const entry = table.get(fd);
|
|
773
|
-
if (!entry)
|
|
774
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
775
|
-
// Pipes and PTYs are not seekable
|
|
776
|
-
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
777
|
-
throw new KernelError("ESPIPE", "illegal seek");
|
|
778
|
-
}
|
|
779
|
-
let newCursor;
|
|
780
|
-
switch (whence) {
|
|
781
|
-
case SEEK_SET:
|
|
782
|
-
newCursor = offset;
|
|
783
|
-
break;
|
|
784
|
-
case SEEK_CUR:
|
|
785
|
-
newCursor = entry.description.cursor + offset;
|
|
786
|
-
break;
|
|
787
|
-
case SEEK_END: {
|
|
788
|
-
newCursor = BigInt(await this.getDescriptionSize(entry.description)) + offset;
|
|
789
|
-
break;
|
|
790
|
-
}
|
|
791
|
-
default:
|
|
792
|
-
throw new KernelError("EINVAL", `invalid whence ${whence}`);
|
|
793
|
-
}
|
|
794
|
-
if (newCursor < 0n)
|
|
795
|
-
throw new KernelError("EINVAL", "negative seek position");
|
|
796
|
-
entry.description.cursor = newCursor;
|
|
797
|
-
return newCursor;
|
|
798
|
-
},
|
|
799
|
-
fdPread: async (pid, fd, length, offset) => {
|
|
800
|
-
assertOwns(pid);
|
|
801
|
-
const table = this.getTable(pid);
|
|
802
|
-
const entry = table.get(fd);
|
|
803
|
-
if (!entry)
|
|
804
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
805
|
-
// Pipes and PTYs are not seekable
|
|
806
|
-
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
807
|
-
throw new KernelError("ESPIPE", "illegal seek");
|
|
808
|
-
}
|
|
809
|
-
// Read from VFS at given offset without moving cursor
|
|
810
|
-
return this.preadDescription(entry.description, Number(offset), length);
|
|
811
|
-
},
|
|
812
|
-
fdPwrite: async (pid, fd, data, offset) => {
|
|
813
|
-
assertOwns(pid);
|
|
814
|
-
const table = this.getTable(pid);
|
|
815
|
-
const entry = table.get(fd);
|
|
816
|
-
if (!entry)
|
|
817
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
818
|
-
// Pipes and PTYs are not seekable
|
|
819
|
-
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
820
|
-
throw new KernelError("ESPIPE", "illegal seek");
|
|
821
|
-
}
|
|
822
|
-
// Delegate positional write to VFS.
|
|
823
|
-
await this.pwriteDescription(entry.description, Number(offset), data);
|
|
824
|
-
return data.length;
|
|
825
|
-
},
|
|
826
|
-
fdDup: (pid, fd) => {
|
|
827
|
-
assertOwns(pid);
|
|
828
|
-
return this.getTable(pid).dup(fd);
|
|
829
|
-
},
|
|
830
|
-
fdDup2: (pid, oldFd, newFd) => {
|
|
831
|
-
assertOwns(pid);
|
|
832
|
-
const table = this.getTable(pid);
|
|
833
|
-
const targetEntry = table.get(newFd);
|
|
834
|
-
const targetDesc = targetEntry?.description;
|
|
835
|
-
const targetDescId = targetDesc?.id;
|
|
836
|
-
table.dup2(oldFd, newFd);
|
|
837
|
-
if (targetDesc && targetDesc.refCount <= 0) {
|
|
838
|
-
this.releaseDescriptionInode(targetDesc);
|
|
839
|
-
if (targetDescId !== undefined) {
|
|
840
|
-
if (this.pipeManager.isPipe(targetDescId))
|
|
841
|
-
this.pipeManager.close(targetDescId);
|
|
842
|
-
if (this.ptyManager.isPty(targetDescId))
|
|
843
|
-
this.ptyManager.close(targetDescId);
|
|
844
|
-
this.fileLockManager.releaseByDescription(targetDescId);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
},
|
|
848
|
-
fdDupMin: (pid, fd, minFd) => {
|
|
849
|
-
assertOwns(pid);
|
|
850
|
-
return this.getTable(pid).dupMinFd(fd, minFd);
|
|
851
|
-
},
|
|
852
|
-
fdStat: (pid, fd) => {
|
|
853
|
-
assertOwns(pid);
|
|
854
|
-
return this.getTable(pid).stat(fd);
|
|
855
|
-
},
|
|
856
|
-
fdPoll: (pid, fd) => {
|
|
857
|
-
try {
|
|
858
|
-
const table = this.getTable(pid);
|
|
859
|
-
const entry = table.get(fd);
|
|
860
|
-
if (!entry)
|
|
861
|
-
return { readable: false, writable: false, hangup: false, invalid: true };
|
|
862
|
-
const descId = entry.description.id;
|
|
863
|
-
if (this.pipeManager.isPipe(descId)) {
|
|
864
|
-
const ps = this.pipeManager.pollState(descId);
|
|
865
|
-
return ps ? { ...ps, invalid: false } : { readable: false, writable: false, hangup: false, invalid: true };
|
|
866
|
-
}
|
|
867
|
-
// Regular files are always readable/writable
|
|
868
|
-
return { readable: true, writable: true, hangup: false, invalid: false };
|
|
869
|
-
}
|
|
870
|
-
catch {
|
|
871
|
-
return { readable: false, writable: false, hangup: false, invalid: true };
|
|
872
|
-
}
|
|
873
|
-
},
|
|
874
|
-
fdPollWait: async (pid, fd, timeoutMs) => {
|
|
875
|
-
assertOwns(pid);
|
|
876
|
-
const table = this.getTable(pid);
|
|
877
|
-
const entry = table.get(fd);
|
|
878
|
-
if (!entry)
|
|
879
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
880
|
-
const descId = entry.description.id;
|
|
881
|
-
if (this.pipeManager.isPipe(descId)) {
|
|
882
|
-
await this.pipeManager.waitForPoll(descId, timeoutMs);
|
|
883
|
-
}
|
|
884
|
-
},
|
|
885
|
-
fdSetCloexec: (pid, fd, value) => {
|
|
886
|
-
assertOwns(pid);
|
|
887
|
-
const table = this.getTable(pid);
|
|
888
|
-
const entry = table.get(fd);
|
|
889
|
-
if (!entry)
|
|
890
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
891
|
-
entry.cloexec = value;
|
|
892
|
-
},
|
|
893
|
-
fdGetCloexec: (pid, fd) => {
|
|
894
|
-
assertOwns(pid);
|
|
895
|
-
const table = this.getTable(pid);
|
|
896
|
-
const entry = table.get(fd);
|
|
897
|
-
if (!entry)
|
|
898
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
899
|
-
return entry.cloexec;
|
|
900
|
-
},
|
|
901
|
-
fcntl: (pid, fd, cmd, arg) => {
|
|
902
|
-
assertOwns(pid);
|
|
903
|
-
const table = this.getTable(pid);
|
|
904
|
-
const entry = table.get(fd);
|
|
905
|
-
if (!entry)
|
|
906
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
907
|
-
switch (cmd) {
|
|
908
|
-
case F_DUPFD:
|
|
909
|
-
return table.dupMinFd(fd, arg ?? 0);
|
|
910
|
-
case F_DUPFD_CLOEXEC: {
|
|
911
|
-
const newFd = table.dupMinFd(fd, arg ?? 0);
|
|
912
|
-
table.get(newFd).cloexec = true;
|
|
913
|
-
return newFd;
|
|
914
|
-
}
|
|
915
|
-
case F_GETFD:
|
|
916
|
-
return entry.cloexec ? FD_CLOEXEC : 0;
|
|
917
|
-
case F_SETFD:
|
|
918
|
-
entry.cloexec = ((arg ?? 0) & FD_CLOEXEC) !== 0;
|
|
919
|
-
return 0;
|
|
920
|
-
case F_GETFL:
|
|
921
|
-
return entry.description.flags;
|
|
922
|
-
default:
|
|
923
|
-
throw new KernelError("EINVAL", `unsupported fcntl command ${cmd}`);
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
// Advisory file locking
|
|
927
|
-
flock: async (pid, fd, operation) => {
|
|
928
|
-
assertOwns(pid);
|
|
929
|
-
const table = this.getTable(pid);
|
|
930
|
-
const entry = table.get(fd);
|
|
931
|
-
if (!entry)
|
|
932
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
933
|
-
await this.fileLockManager.flock(entry.description.path, entry.description.id, operation);
|
|
934
|
-
},
|
|
935
|
-
// Process operations
|
|
936
|
-
spawn: (command, args, ctx) => {
|
|
937
|
-
if (ctx.ppid)
|
|
938
|
-
assertOwns(ctx.ppid);
|
|
939
|
-
return this.spawnManaged(command, args, {
|
|
940
|
-
env: ctx.env,
|
|
941
|
-
cwd: ctx.cwd,
|
|
942
|
-
streamStdin: ctx.streamStdin,
|
|
943
|
-
onStdout: ctx.onStdout,
|
|
944
|
-
onStderr: ctx.onStderr,
|
|
945
|
-
stdinFd: ctx.stdinFd,
|
|
946
|
-
stdoutFd: ctx.stdoutFd,
|
|
947
|
-
stderrFd: ctx.stderrFd,
|
|
948
|
-
}, ctx.ppid);
|
|
949
|
-
},
|
|
950
|
-
waitpid: (pid, options) => {
|
|
951
|
-
try {
|
|
952
|
-
assertOwns(pid);
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
return Promise.reject(e);
|
|
956
|
-
}
|
|
957
|
-
return this.processTable.waitpid(pid, options);
|
|
958
|
-
},
|
|
959
|
-
kill: (pid, signal) => {
|
|
960
|
-
// Negative PID = process group kill, handled by kernel directly
|
|
961
|
-
if (pid >= 0)
|
|
962
|
-
assertOwns(pid);
|
|
963
|
-
this.log.debug({ pid, signal }, "signal delivery");
|
|
964
|
-
this.processTable.kill(pid, signal);
|
|
965
|
-
},
|
|
966
|
-
getpid: (pid) => {
|
|
967
|
-
assertOwns(pid);
|
|
968
|
-
return pid;
|
|
969
|
-
},
|
|
970
|
-
getppid: (pid) => {
|
|
971
|
-
assertOwns(pid);
|
|
972
|
-
return this.processTable.getppid(pid);
|
|
973
|
-
},
|
|
974
|
-
// Process group / session
|
|
975
|
-
setpgid: (pid, pgid) => {
|
|
976
|
-
assertOwns(pid);
|
|
977
|
-
this.processTable.setpgid(pid, pgid);
|
|
978
|
-
},
|
|
979
|
-
getpgid: (pid) => {
|
|
980
|
-
assertOwns(pid);
|
|
981
|
-
return this.processTable.getpgid(pid);
|
|
982
|
-
},
|
|
983
|
-
setsid: (pid) => {
|
|
984
|
-
assertOwns(pid);
|
|
985
|
-
return this.processTable.setsid(pid);
|
|
986
|
-
},
|
|
987
|
-
getsid: (pid) => {
|
|
988
|
-
assertOwns(pid);
|
|
989
|
-
return this.processTable.getsid(pid);
|
|
990
|
-
},
|
|
991
|
-
// Pipe operations
|
|
992
|
-
pipe: (pid) => {
|
|
993
|
-
assertOwns(pid);
|
|
994
|
-
const table = this.getTable(pid);
|
|
995
|
-
return this.pipeManager.createPipeFDs(table);
|
|
996
|
-
},
|
|
997
|
-
// PTY operations
|
|
998
|
-
openpty: (pid) => {
|
|
999
|
-
assertOwns(pid);
|
|
1000
|
-
const table = this.getTable(pid);
|
|
1001
|
-
return this.ptyManager.createPtyFDs(table);
|
|
1002
|
-
},
|
|
1003
|
-
isatty: (pid, fd) => {
|
|
1004
|
-
assertOwns(pid);
|
|
1005
|
-
const table = this.getTable(pid);
|
|
1006
|
-
const entry = table.get(fd);
|
|
1007
|
-
if (!entry)
|
|
1008
|
-
return false;
|
|
1009
|
-
return this.ptyManager.isSlave(entry.description.id);
|
|
1010
|
-
},
|
|
1011
|
-
ptySetDiscipline: (pid, fd, config) => {
|
|
1012
|
-
assertOwns(pid);
|
|
1013
|
-
const table = this.getTable(pid);
|
|
1014
|
-
const entry = table.get(fd);
|
|
1015
|
-
if (!entry)
|
|
1016
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1017
|
-
this.ptyManager.setDiscipline(entry.description.id, config);
|
|
1018
|
-
},
|
|
1019
|
-
ptySetForegroundPgid: (pid, fd, pgid) => {
|
|
1020
|
-
assertOwns(pid);
|
|
1021
|
-
const table = this.getTable(pid);
|
|
1022
|
-
const entry = table.get(fd);
|
|
1023
|
-
if (!entry)
|
|
1024
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1025
|
-
this.ptyManager.setForegroundPgid(entry.description.id, pgid);
|
|
1026
|
-
},
|
|
1027
|
-
// Termios operations
|
|
1028
|
-
tcgetattr: (pid, fd) => {
|
|
1029
|
-
assertOwns(pid);
|
|
1030
|
-
const table = this.getTable(pid);
|
|
1031
|
-
const entry = table.get(fd);
|
|
1032
|
-
if (!entry)
|
|
1033
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1034
|
-
return this.ptyManager.getTermios(entry.description.id);
|
|
1035
|
-
},
|
|
1036
|
-
tcsetattr: (pid, fd, termios) => {
|
|
1037
|
-
assertOwns(pid);
|
|
1038
|
-
const table = this.getTable(pid);
|
|
1039
|
-
const entry = table.get(fd);
|
|
1040
|
-
if (!entry)
|
|
1041
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1042
|
-
this.ptyManager.setTermios(entry.description.id, termios);
|
|
1043
|
-
},
|
|
1044
|
-
tcsetpgrp: (pid, fd, pgid) => {
|
|
1045
|
-
assertOwns(pid);
|
|
1046
|
-
const table = this.getTable(pid);
|
|
1047
|
-
const entry = table.get(fd);
|
|
1048
|
-
if (!entry)
|
|
1049
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1050
|
-
// Validate target PGID refers to an existing process group
|
|
1051
|
-
if (!this.processTable.hasProcessGroup(pgid)) {
|
|
1052
|
-
throw new KernelError("ESRCH", `no such process group ${pgid}`);
|
|
1053
|
-
}
|
|
1054
|
-
this.ptyManager.setForegroundPgid(entry.description.id, pgid);
|
|
1055
|
-
},
|
|
1056
|
-
tcgetpgrp: (pid, fd) => {
|
|
1057
|
-
assertOwns(pid);
|
|
1058
|
-
const table = this.getTable(pid);
|
|
1059
|
-
const entry = table.get(fd);
|
|
1060
|
-
if (!entry)
|
|
1061
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1062
|
-
return this.ptyManager.getForegroundPgid(entry.description.id);
|
|
1063
|
-
},
|
|
1064
|
-
// /dev/fd operations
|
|
1065
|
-
devFdReadDir: (pid) => {
|
|
1066
|
-
assertOwns(pid);
|
|
1067
|
-
const table = this.fdTableManager.get(pid);
|
|
1068
|
-
if (!table)
|
|
1069
|
-
return [];
|
|
1070
|
-
const fds = [];
|
|
1071
|
-
for (const entry of table)
|
|
1072
|
-
fds.push(entry.fd);
|
|
1073
|
-
return fds.sort((a, b) => a - b).map(String);
|
|
1074
|
-
},
|
|
1075
|
-
devFdStat: async (pid, fd) => {
|
|
1076
|
-
assertOwns(pid);
|
|
1077
|
-
const table = this.getTable(pid);
|
|
1078
|
-
const entry = table.get(fd);
|
|
1079
|
-
if (!entry)
|
|
1080
|
-
throw new KernelError("EBADF", `bad file descriptor ${fd}`);
|
|
1081
|
-
// Pipe/PTY FDs return a synthetic character device stat
|
|
1082
|
-
if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
|
|
1083
|
-
const now = Date.now();
|
|
1084
|
-
return {
|
|
1085
|
-
mode: 0o666,
|
|
1086
|
-
size: 0,
|
|
1087
|
-
isDirectory: false,
|
|
1088
|
-
isSymbolicLink: false,
|
|
1089
|
-
atimeMs: now,
|
|
1090
|
-
mtimeMs: now,
|
|
1091
|
-
ctimeMs: now,
|
|
1092
|
-
birthtimeMs: now,
|
|
1093
|
-
ino: entry.description.id,
|
|
1094
|
-
nlink: 1,
|
|
1095
|
-
uid: 0,
|
|
1096
|
-
gid: 0,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
// Regular file — stat the underlying path
|
|
1100
|
-
return this.statDescription(entry.description);
|
|
1101
|
-
},
|
|
1102
|
-
// Environment
|
|
1103
|
-
getenv: (pid) => {
|
|
1104
|
-
assertOwns(pid);
|
|
1105
|
-
const entry = this.processTable.get(pid);
|
|
1106
|
-
return entry?.env ?? { ...this.env };
|
|
1107
|
-
},
|
|
1108
|
-
setenv: (pid, key, value) => {
|
|
1109
|
-
assertOwns(pid);
|
|
1110
|
-
const entry = this.processTable.get(pid);
|
|
1111
|
-
if (!entry)
|
|
1112
|
-
throw new KernelError("ESRCH", `no such process ${pid}`);
|
|
1113
|
-
entry.env[key] = value;
|
|
1114
|
-
},
|
|
1115
|
-
unsetenv: (pid, key) => {
|
|
1116
|
-
assertOwns(pid);
|
|
1117
|
-
const entry = this.processTable.get(pid);
|
|
1118
|
-
if (!entry)
|
|
1119
|
-
throw new KernelError("ESRCH", `no such process ${pid}`);
|
|
1120
|
-
delete entry.env[key];
|
|
1121
|
-
},
|
|
1122
|
-
getcwd: (pid) => {
|
|
1123
|
-
assertOwns(pid);
|
|
1124
|
-
const entry = this.processTable.get(pid);
|
|
1125
|
-
return entry?.cwd ?? this.cwd;
|
|
1126
|
-
},
|
|
1127
|
-
// Working directory
|
|
1128
|
-
chdir: async (pid, path) => {
|
|
1129
|
-
assertOwns(pid);
|
|
1130
|
-
const entry = this.processTable.get(pid);
|
|
1131
|
-
if (!entry)
|
|
1132
|
-
throw new KernelError("ESRCH", `no such process ${pid}`);
|
|
1133
|
-
// Validate path exists and is a directory
|
|
1134
|
-
let st;
|
|
1135
|
-
try {
|
|
1136
|
-
st = await this.vfs.stat(path);
|
|
1137
|
-
}
|
|
1138
|
-
catch {
|
|
1139
|
-
throw new KernelError("ENOENT", `no such file or directory: ${path}`);
|
|
1140
|
-
}
|
|
1141
|
-
if (!st.isDirectory) {
|
|
1142
|
-
throw new KernelError("ENOTDIR", `not a directory: ${path}`);
|
|
1143
|
-
}
|
|
1144
|
-
entry.cwd = path;
|
|
1145
|
-
entry.env.PWD = path;
|
|
1146
|
-
},
|
|
1147
|
-
// Alarm (SIGALRM)
|
|
1148
|
-
alarm: (pid, seconds) => {
|
|
1149
|
-
assertOwns(pid);
|
|
1150
|
-
return this.processTable.alarm(pid, seconds);
|
|
1151
|
-
},
|
|
1152
|
-
// File mode creation mask
|
|
1153
|
-
umask: (pid, newMask) => {
|
|
1154
|
-
assertOwns(pid);
|
|
1155
|
-
const entry = this.processTable.get(pid);
|
|
1156
|
-
if (!entry)
|
|
1157
|
-
throw new KernelError("ESRCH", `no such process ${pid}`);
|
|
1158
|
-
const old = entry.umask;
|
|
1159
|
-
if (newMask !== undefined) {
|
|
1160
|
-
entry.umask = newMask & 0o777;
|
|
1161
|
-
}
|
|
1162
|
-
return old;
|
|
1163
|
-
},
|
|
1164
|
-
// Directory creation with umask
|
|
1165
|
-
mkdir: async (pid, path, mode) => {
|
|
1166
|
-
assertOwns(pid);
|
|
1167
|
-
const entry = this.processTable.get(pid);
|
|
1168
|
-
const umask = entry?.umask ?? 0o022;
|
|
1169
|
-
const requestedMode = mode ?? 0o777;
|
|
1170
|
-
const effectiveMode = requestedMode & ~umask;
|
|
1171
|
-
await this.vfs.mkdir(path);
|
|
1172
|
-
await this.vfs.chmod(path, effectiveMode);
|
|
1173
|
-
},
|
|
1174
|
-
// Socket table (shared across runtimes)
|
|
1175
|
-
socketTable: this.socketTable,
|
|
1176
|
-
timerTable: this.timerTable,
|
|
1177
|
-
// Process table (shared across runtimes)
|
|
1178
|
-
processTable: this.processTable,
|
|
1179
|
-
};
|
|
1180
|
-
return kernelInterface;
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Create FD table for a child process via fork + optional FD overrides.
|
|
1184
|
-
*
|
|
1185
|
-
* When callerPid exists, forks the parent's FD table so the child inherits
|
|
1186
|
-
* all open FDs (shared cursors via refcounted FileDescription). Then applies
|
|
1187
|
-
* stdinFd/stdoutFd/stderrFd overrides on top of the forked table.
|
|
1188
|
-
*/
|
|
1189
|
-
createChildFDTable(childPid, options, callerPid) {
|
|
1190
|
-
// Fork parent's FD table if parent exists
|
|
1191
|
-
if (callerPid && this.fdTableManager.get(callerPid)) {
|
|
1192
|
-
const table = this.fdTableManager.fork(callerPid, childPid);
|
|
1193
|
-
// Apply FD overrides on top of the forked table
|
|
1194
|
-
const hasFdOverrides = options?.stdinFd !== undefined ||
|
|
1195
|
-
options?.stdoutFd !== undefined ||
|
|
1196
|
-
options?.stderrFd !== undefined;
|
|
1197
|
-
if (hasFdOverrides) {
|
|
1198
|
-
const callerTable = this.fdTableManager.get(callerPid);
|
|
1199
|
-
this.applyStdioOverride(table, callerTable, 0, options.stdinFd);
|
|
1200
|
-
this.applyStdioOverride(table, callerTable, 1, options.stdoutFd);
|
|
1201
|
-
this.applyStdioOverride(table, callerTable, 2, options.stderrFd);
|
|
1202
|
-
}
|
|
1203
|
-
// Close inherited pipe FDs above stdio that share a pipe with an
|
|
1204
|
-
// overridden stdio FD — prevents pipe deadlocks (close-on-exec for
|
|
1205
|
-
// counterpart pipe ends only, so tests that intentionally inherit pipe
|
|
1206
|
-
// FDs without overrides are not affected).
|
|
1207
|
-
if (hasFdOverrides) {
|
|
1208
|
-
const overridePipeIds = new Set();
|
|
1209
|
-
for (const fd of [0, 1, 2]) {
|
|
1210
|
-
const e = table.get(fd);
|
|
1211
|
-
if (e && this.pipeManager.isPipe(e.description.id)) {
|
|
1212
|
-
const pipeId = this.pipeManager.pipeIdFor(e.description.id);
|
|
1213
|
-
if (pipeId !== undefined)
|
|
1214
|
-
overridePipeIds.add(pipeId);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
if (overridePipeIds.size > 0) {
|
|
1218
|
-
const toClose = [];
|
|
1219
|
-
for (const entry of table) {
|
|
1220
|
-
if (entry.fd > 2 && this.pipeManager.isPipe(entry.description.id)) {
|
|
1221
|
-
const pid2 = this.pipeManager.pipeIdFor(entry.description.id);
|
|
1222
|
-
if (pid2 !== undefined && overridePipeIds.has(pid2)) {
|
|
1223
|
-
toClose.push(entry.fd);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
for (const fd of toClose) {
|
|
1228
|
-
table.close(fd);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
return table;
|
|
1233
|
-
}
|
|
1234
|
-
return this.fdTableManager.create(childPid);
|
|
1235
|
-
}
|
|
1236
|
-
/** Close inherited stdio FD and install an override from the caller's table. */
|
|
1237
|
-
applyStdioOverride(childTable, callerTable, targetFd, overrideFd) {
|
|
1238
|
-
if (overrideFd === undefined)
|
|
1239
|
-
return;
|
|
1240
|
-
if (overrideFd === 0xFFFFFFFF)
|
|
1241
|
-
return; // /dev/null sentinel — keep inherited
|
|
1242
|
-
const entry = callerTable.get(overrideFd);
|
|
1243
|
-
if (!entry)
|
|
1244
|
-
return;
|
|
1245
|
-
// Close the inherited FD and install the override
|
|
1246
|
-
const existing = childTable.get(targetFd);
|
|
1247
|
-
childTable.close(targetFd);
|
|
1248
|
-
if (existing && existing.description.refCount <= 0) {
|
|
1249
|
-
this.releaseDescriptionInode(existing.description);
|
|
1250
|
-
const descId = existing.description.id;
|
|
1251
|
-
if (this.pipeManager.isPipe(descId))
|
|
1252
|
-
this.pipeManager.close(descId);
|
|
1253
|
-
if (this.ptyManager.isPty(descId))
|
|
1254
|
-
this.ptyManager.close(descId);
|
|
1255
|
-
this.fileLockManager.releaseByDescription(descId);
|
|
1256
|
-
}
|
|
1257
|
-
childTable.openWith(entry.description, entry.filetype, targetFd);
|
|
1258
|
-
}
|
|
1259
|
-
/** Check if a stdio FD (0/1/2) in a process's table is a pipe or PTY. */
|
|
1260
|
-
isStdioPiped(table, fd) {
|
|
1261
|
-
const entry = table.get(fd);
|
|
1262
|
-
if (!entry)
|
|
1263
|
-
return false;
|
|
1264
|
-
return this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id);
|
|
1265
|
-
}
|
|
1266
|
-
/** Check if an FD in the given table refers to a PTY slave (terminal). */
|
|
1267
|
-
isFdPtySlave(table, fd) {
|
|
1268
|
-
const entry = table.get(fd);
|
|
1269
|
-
if (!entry)
|
|
1270
|
-
return false;
|
|
1271
|
-
return this.ptyManager.isSlave(entry.description.id);
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* Create a callback that forwards data through a piped stdio FD.
|
|
1275
|
-
* Needed for drivers (like Node) that emit output via callbacks rather
|
|
1276
|
-
* than kernel FD writes (like WasmVM does via WASI fd_write).
|
|
1277
|
-
*/
|
|
1278
|
-
createPipedOutputCallback(table, fd, pid) {
|
|
1279
|
-
const entry = table.get(fd);
|
|
1280
|
-
if (!entry)
|
|
1281
|
-
return undefined;
|
|
1282
|
-
const descId = entry.description.id;
|
|
1283
|
-
if (this.pipeManager.isPipe(descId)) {
|
|
1284
|
-
return (data) => {
|
|
1285
|
-
try {
|
|
1286
|
-
this.pipeManager.write(descId, data, pid);
|
|
1287
|
-
}
|
|
1288
|
-
catch { /* pipe closed */ }
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
if (this.ptyManager.isPty(descId)) {
|
|
1292
|
-
return (data) => {
|
|
1293
|
-
try {
|
|
1294
|
-
this.ptyManager.write(descId, data);
|
|
1295
|
-
}
|
|
1296
|
-
catch { /* pty closed */ }
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
return undefined;
|
|
1300
|
-
}
|
|
1301
|
-
/** Clean up all FDs for a process, closing pipe/PTY ends when last reference drops. */
|
|
1302
|
-
cleanupProcessFDs(pid) {
|
|
1303
|
-
const table = this.fdTableManager.get(pid);
|
|
1304
|
-
if (!table)
|
|
1305
|
-
return;
|
|
1306
|
-
// Collect descriptions before closing so we can check refCounts after.
|
|
1307
|
-
const descriptions = new Map();
|
|
1308
|
-
const managedDescs = [];
|
|
1309
|
-
for (const entry of table) {
|
|
1310
|
-
descriptions.set(entry.description.id, entry.description);
|
|
1311
|
-
const descId = entry.description.id;
|
|
1312
|
-
if (this.pipeManager.isPipe(descId)) {
|
|
1313
|
-
managedDescs.push({ id: descId, description: entry.description, type: "pipe" });
|
|
1314
|
-
}
|
|
1315
|
-
else if (this.ptyManager.isPty(descId)) {
|
|
1316
|
-
managedDescs.push({ id: descId, description: entry.description, type: "pty" });
|
|
1317
|
-
}
|
|
1318
|
-
else if (this.fileLockManager.hasLock(descId)) {
|
|
1319
|
-
managedDescs.push({ id: descId, description: entry.description, type: "lock" });
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
// Close all FDs and remove the table
|
|
1323
|
-
this.fdTableManager.remove(pid);
|
|
1324
|
-
// Flush buffered writes when the last shared reference closes.
|
|
1325
|
-
for (const description of descriptions.values()) {
|
|
1326
|
-
if (description.refCount <= 0) {
|
|
1327
|
-
this.releaseDescriptionInode(description);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
// Signal closure for managed descriptions whose last reference was dropped.
|
|
1331
|
-
for (const { id, description, type } of managedDescs) {
|
|
1332
|
-
if (description.refCount <= 0) {
|
|
1333
|
-
if (type === "pipe")
|
|
1334
|
-
this.pipeManager.close(id);
|
|
1335
|
-
else if (type === "pty")
|
|
1336
|
-
this.ptyManager.close(id);
|
|
1337
|
-
else if (type === "lock")
|
|
1338
|
-
this.fileLockManager.releaseByDescription(id);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
async vfsWrite(entry, data) {
|
|
1343
|
-
let content;
|
|
1344
|
-
try {
|
|
1345
|
-
content = await this.readDescriptionFile(entry.description);
|
|
1346
|
-
}
|
|
1347
|
-
catch {
|
|
1348
|
-
content = new Uint8Array(0);
|
|
1349
|
-
}
|
|
1350
|
-
// O_APPEND: every write seeks to end of file first (POSIX)
|
|
1351
|
-
const cursor = (entry.description.flags & O_APPEND)
|
|
1352
|
-
? content.length
|
|
1353
|
-
: Number(entry.description.cursor);
|
|
1354
|
-
const endPos = cursor + data.length;
|
|
1355
|
-
const newContent = new Uint8Array(Math.max(content.length, endPos));
|
|
1356
|
-
newContent.set(content);
|
|
1357
|
-
newContent.set(data, cursor);
|
|
1358
|
-
await this.writeDescriptionFile(entry.description, newContent);
|
|
1359
|
-
// Apply creation mode once the descriptor's newly created file is materialized.
|
|
1360
|
-
if (entry.description.creationMode !== undefined) {
|
|
1361
|
-
await this.vfs.chmod(entry.description.path, entry.description.creationMode);
|
|
1362
|
-
entry.description.creationMode = undefined;
|
|
1363
|
-
}
|
|
1364
|
-
entry.description.cursor = BigInt(endPos);
|
|
1365
|
-
return data.length;
|
|
1366
|
-
}
|
|
1367
|
-
releaseDescriptionInode(description) {
|
|
1368
|
-
// Flush buffered writes to durable storage when the last FD is closed.
|
|
1369
|
-
void this.vfs.fsync?.(description.path);
|
|
1370
|
-
}
|
|
1371
|
-
async readDescriptionFile(description) {
|
|
1372
|
-
return this.vfs.readFile(description.path);
|
|
1373
|
-
}
|
|
1374
|
-
async writeDescriptionFile(description, content) {
|
|
1375
|
-
await this.vfs.writeFile(description.path, content);
|
|
1376
|
-
}
|
|
1377
|
-
prepareOpenSync(path, flags) {
|
|
1378
|
-
const syncVfs = this.vfs;
|
|
1379
|
-
return syncVfs.prepareOpenSync?.(path, flags) ?? false;
|
|
1380
|
-
}
|
|
1381
|
-
async preadDescription(description, offset, length) {
|
|
1382
|
-
return this.vfs.pread(description.path, offset, length);
|
|
1383
|
-
}
|
|
1384
|
-
async pwriteDescription(description, offset, data) {
|
|
1385
|
-
await this.vfs.pwrite(description.path, offset, data);
|
|
1386
|
-
}
|
|
1387
|
-
async getDescriptionSize(description) {
|
|
1388
|
-
return (await this.statDescription(description)).size;
|
|
1389
|
-
}
|
|
1390
|
-
async statDescription(description) {
|
|
1391
|
-
return this.vfs.stat(description.path);
|
|
1392
|
-
}
|
|
1393
|
-
getTable(pid) {
|
|
1394
|
-
const table = this.fdTableManager.get(pid);
|
|
1395
|
-
if (!table)
|
|
1396
|
-
throw new KernelError("ESRCH", `no FD table for PID ${pid}`);
|
|
1397
|
-
return table;
|
|
1398
|
-
}
|
|
1399
|
-
assertNotDisposed() {
|
|
1400
|
-
if (this.disposed)
|
|
1401
|
-
throw new Error("Kernel is disposed");
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
function concatUint8(chunks) {
|
|
1405
|
-
if (chunks.length === 0)
|
|
1406
|
-
return "";
|
|
1407
|
-
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
1408
|
-
const buf = new Uint8Array(total);
|
|
1409
|
-
let offset = 0;
|
|
1410
|
-
for (const chunk of chunks) {
|
|
1411
|
-
buf.set(chunk, offset);
|
|
1412
|
-
offset += chunk.length;
|
|
1413
|
-
}
|
|
1414
|
-
return new TextDecoder().decode(buf);
|
|
1415
|
-
}
|