@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.
Files changed (248) hide show
  1. package/README.md +5 -5
  2. package/dist/binary.d.ts +4 -0
  3. package/dist/binary.js +25 -0
  4. package/dist/bytes.d.ts +2 -0
  5. package/dist/bytes.js +6 -0
  6. package/dist/callbacks.d.ts +41 -0
  7. package/dist/callbacks.js +94 -0
  8. package/dist/cargo.d.ts +2 -0
  9. package/dist/cargo.js +142 -0
  10. package/dist/correlation.d.ts +10 -0
  11. package/dist/correlation.js +49 -0
  12. package/dist/descriptors.d.ts +34 -0
  13. package/dist/descriptors.js +37 -0
  14. package/dist/event-buffer.d.ts +90 -0
  15. package/dist/event-buffer.js +313 -0
  16. package/dist/ext.d.ts +7 -0
  17. package/dist/ext.js +13 -0
  18. package/dist/filesystem.d.ts +41 -0
  19. package/dist/filesystem.js +70 -0
  20. package/dist/frame-payload-codec.d.ts +8 -0
  21. package/dist/frame-payload-codec.js +14 -0
  22. package/dist/frame-rpc.d.ts +38 -0
  23. package/dist/frame-rpc.js +73 -0
  24. package/dist/frame-stream.d.ts +27 -0
  25. package/dist/frame-stream.js +99 -0
  26. package/dist/framing.d.ts +7 -0
  27. package/dist/framing.js +22 -0
  28. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  29. package/dist/generated/AcpLimitsConfig.js +2 -0
  30. package/dist/generated/CreateVmConfig.d.ts +19 -0
  31. package/dist/generated/FsPermissionRule.d.ts +6 -0
  32. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  33. package/dist/generated/FsPermissionRuleSet.js +1 -0
  34. package/dist/generated/FsPermissionScope.d.ts +3 -0
  35. package/dist/generated/FsPermissionScope.js +1 -0
  36. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  37. package/dist/generated/HttpLimitsConfig.js +2 -0
  38. package/dist/generated/JsModuleResolution.d.ts +1 -0
  39. package/dist/generated/JsModuleResolution.js +2 -0
  40. package/dist/generated/JsRuntimeConfig.d.ts +26 -0
  41. package/dist/generated/JsRuntimeConfig.js +1 -0
  42. package/dist/generated/JsRuntimeLimitsConfig.d.ts +7 -0
  43. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  44. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  45. package/dist/generated/JsRuntimePlatform.js +2 -0
  46. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  47. package/dist/generated/MountPluginDescriptor.js +2 -0
  48. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  49. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  50. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  51. package/dist/generated/PatternPermissionRule.js +1 -0
  52. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  53. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  54. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  55. package/dist/generated/PatternPermissionScope.js +1 -0
  56. package/dist/generated/PermissionMode.d.ts +1 -0
  57. package/dist/generated/PermissionMode.js +2 -0
  58. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  59. package/dist/generated/PermissionsPolicy.js +1 -0
  60. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  61. package/dist/generated/PluginLimitsConfig.js +2 -0
  62. package/dist/generated/PythonLimitsConfig.d.ts +5 -0
  63. package/dist/generated/PythonLimitsConfig.js +2 -0
  64. package/dist/generated/ResourceLimitsConfig.d.ts +22 -0
  65. package/dist/generated/ResourceLimitsConfig.js +2 -0
  66. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  67. package/dist/generated/RootFilesystemConfig.js +1 -0
  68. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  69. package/dist/generated/RootFilesystemEntry.js +1 -0
  70. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  71. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  72. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  73. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  74. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  75. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  76. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  77. package/dist/generated/RootFilesystemMode.js +2 -0
  78. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  79. package/dist/generated/ToolLimitsConfig.js +2 -0
  80. package/dist/generated/VmDnsConfig.d.ts +6 -0
  81. package/dist/generated/VmDnsConfig.js +2 -0
  82. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  83. package/dist/generated/VmLimitsConfig.js +1 -0
  84. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  85. package/dist/generated/VmListenPolicyConfig.js +2 -0
  86. package/dist/generated/WasmLimitsConfig.d.ts +5 -0
  87. package/dist/generated/WasmLimitsConfig.js +2 -0
  88. package/dist/generated-protocol.d.ts +1037 -0
  89. package/dist/generated-protocol.js +2887 -0
  90. package/dist/index.d.ts +24 -62
  91. package/dist/index.js +24 -53
  92. package/dist/json.d.ts +2 -0
  93. package/dist/json.js +20 -0
  94. package/dist/kernel-proxy.d.ts +149 -0
  95. package/dist/kernel-proxy.js +1733 -0
  96. package/dist/native-client.d.ts +41 -0
  97. package/dist/native-client.js +124 -0
  98. package/dist/node-runtime.d.ts +490 -0
  99. package/dist/node-runtime.js +585 -0
  100. package/dist/numbers.d.ts +1 -0
  101. package/dist/numbers.js +8 -0
  102. package/dist/ownership.d.ts +18 -0
  103. package/dist/ownership.js +77 -0
  104. package/dist/permissions.d.ts +29 -0
  105. package/dist/permissions.js +68 -0
  106. package/dist/process.d.ts +35 -0
  107. package/dist/process.js +125 -0
  108. package/dist/protocol-client.d.ts +46 -0
  109. package/dist/protocol-client.js +180 -0
  110. package/dist/protocol-frames.d.ts +68 -0
  111. package/dist/protocol-frames.js +139 -0
  112. package/dist/protocol-maps.d.ts +28 -0
  113. package/dist/protocol-maps.js +217 -0
  114. package/dist/protocol-schema.d.ts +10 -0
  115. package/dist/protocol-schema.js +11 -0
  116. package/dist/request-payloads.d.ts +137 -0
  117. package/dist/request-payloads.js +210 -0
  118. package/dist/response-payloads.d.ts +107 -0
  119. package/dist/response-payloads.js +161 -0
  120. package/dist/sidecar-client.d.ts +242 -0
  121. package/dist/sidecar-client.js +797 -0
  122. package/dist/state.d.ts +40 -0
  123. package/dist/state.js +44 -0
  124. package/dist/test-runtime.d.ts +526 -0
  125. package/dist/test-runtime.js +2119 -0
  126. package/dist/vm-config.d.ts +31 -0
  127. package/dist/vm-config.js +1 -0
  128. package/fixtures/alpine-defaults.json +520 -0
  129. package/fixtures/base-filesystem.json +528 -0
  130. package/package.json +193 -115
  131. package/LICENSE +0 -191
  132. package/dist/bridge-setup.d.ts +0 -6
  133. package/dist/bridge-setup.js +0 -9
  134. package/dist/esm-compiler.d.ts +0 -18
  135. package/dist/esm-compiler.js +0 -72
  136. package/dist/fs-helpers.d.ts +0 -23
  137. package/dist/fs-helpers.js +0 -41
  138. package/dist/generated/isolate-runtime.d.ts +0 -19
  139. package/dist/generated/isolate-runtime.js +0 -21
  140. package/dist/generated/polyfills.d.ts +0 -82
  141. package/dist/generated/polyfills.js +0 -82
  142. package/dist/isolate-runtime/apply-custom-global-policy.js +0 -53
  143. package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +0 -130
  144. package/dist/isolate-runtime/apply-timing-mitigation-off.js +0 -14
  145. package/dist/isolate-runtime/bridge-attach.js +0 -29
  146. package/dist/isolate-runtime/bridge-initial-globals.js +0 -385
  147. package/dist/isolate-runtime/eval-script-result.js +0 -8
  148. package/dist/isolate-runtime/global-exposure-helpers.js +0 -36
  149. package/dist/isolate-runtime/init-commonjs-module-globals.js +0 -28
  150. package/dist/isolate-runtime/override-process-cwd.js +0 -8
  151. package/dist/isolate-runtime/override-process-env.js +0 -8
  152. package/dist/isolate-runtime/require-setup.js +0 -4153
  153. package/dist/isolate-runtime/set-commonjs-file-globals.js +0 -36
  154. package/dist/isolate-runtime/set-stdin-data.js +0 -10
  155. package/dist/isolate-runtime/setup-dynamic-import.js +0 -123
  156. package/dist/isolate-runtime/setup-fs-facade.js +0 -87
  157. package/dist/kernel/command-registry.d.ts +0 -44
  158. package/dist/kernel/command-registry.js +0 -114
  159. package/dist/kernel/device-backend.d.ts +0 -14
  160. package/dist/kernel/device-backend.js +0 -251
  161. package/dist/kernel/device-layer.d.ts +0 -12
  162. package/dist/kernel/device-layer.js +0 -271
  163. package/dist/kernel/dns-cache.d.ts +0 -29
  164. package/dist/kernel/dns-cache.js +0 -52
  165. package/dist/kernel/fd-table.d.ts +0 -84
  166. package/dist/kernel/fd-table.js +0 -278
  167. package/dist/kernel/file-lock.d.ts +0 -34
  168. package/dist/kernel/file-lock.js +0 -122
  169. package/dist/kernel/host-adapter.d.ts +0 -50
  170. package/dist/kernel/host-adapter.js +0 -8
  171. package/dist/kernel/index.d.ts +0 -36
  172. package/dist/kernel/index.js +0 -34
  173. package/dist/kernel/kernel.d.ts +0 -9
  174. package/dist/kernel/kernel.js +0 -1415
  175. package/dist/kernel/mount-table.d.ts +0 -75
  176. package/dist/kernel/mount-table.js +0 -353
  177. package/dist/kernel/permissions.d.ts +0 -36
  178. package/dist/kernel/permissions.js +0 -150
  179. package/dist/kernel/pipe-manager.d.ts +0 -64
  180. package/dist/kernel/pipe-manager.js +0 -267
  181. package/dist/kernel/proc-backend.d.ts +0 -30
  182. package/dist/kernel/proc-backend.js +0 -428
  183. package/dist/kernel/proc-layer.d.ts +0 -11
  184. package/dist/kernel/proc-layer.js +0 -507
  185. package/dist/kernel/process-table.d.ts +0 -126
  186. package/dist/kernel/process-table.js +0 -651
  187. package/dist/kernel/pty.d.ts +0 -109
  188. package/dist/kernel/pty.js +0 -552
  189. package/dist/kernel/socket-table.d.ts +0 -312
  190. package/dist/kernel/socket-table.js +0 -1188
  191. package/dist/kernel/timer-table.d.ts +0 -54
  192. package/dist/kernel/timer-table.js +0 -108
  193. package/dist/kernel/types.d.ts +0 -541
  194. package/dist/kernel/types.js +0 -98
  195. package/dist/kernel/user.d.ts +0 -29
  196. package/dist/kernel/user.js +0 -35
  197. package/dist/kernel/vfs.d.ts +0 -82
  198. package/dist/kernel/vfs.js +0 -25
  199. package/dist/kernel/wait.d.ts +0 -45
  200. package/dist/kernel/wait.js +0 -112
  201. package/dist/kernel/wstatus.d.ts +0 -21
  202. package/dist/kernel/wstatus.js +0 -33
  203. package/dist/module-resolver.d.ts +0 -29
  204. package/dist/module-resolver.js +0 -314
  205. package/dist/package-bundler.d.ts +0 -41
  206. package/dist/package-bundler.js +0 -497
  207. package/dist/runtime-driver.d.ts +0 -66
  208. package/dist/shared/api-types.d.ts +0 -83
  209. package/dist/shared/bridge-contract.d.ts +0 -772
  210. package/dist/shared/bridge-contract.js +0 -169
  211. package/dist/shared/console-formatter.d.ts +0 -22
  212. package/dist/shared/console-formatter.js +0 -161
  213. package/dist/shared/constants.d.ts +0 -3
  214. package/dist/shared/constants.js +0 -3
  215. package/dist/shared/errors.d.ts +0 -16
  216. package/dist/shared/errors.js +0 -21
  217. package/dist/shared/esm-utils.d.ts +0 -28
  218. package/dist/shared/esm-utils.js +0 -97
  219. package/dist/shared/global-exposure.d.ts +0 -38
  220. package/dist/shared/global-exposure.js +0 -876
  221. package/dist/shared/in-memory-fs.d.ts +0 -16
  222. package/dist/shared/in-memory-fs.js +0 -115
  223. package/dist/shared/permissions.d.ts +0 -36
  224. package/dist/shared/permissions.js +0 -314
  225. package/dist/shared/require-setup.d.ts +0 -6
  226. package/dist/shared/require-setup.js +0 -9
  227. package/dist/test/block-store-conformance.d.ts +0 -34
  228. package/dist/test/block-store-conformance.js +0 -251
  229. package/dist/test/metadata-store-conformance.d.ts +0 -37
  230. package/dist/test/metadata-store-conformance.js +0 -646
  231. package/dist/test/vfs-conformance.d.ts +0 -65
  232. package/dist/test/vfs-conformance.js +0 -842
  233. package/dist/types.d.ts +0 -98
  234. package/dist/types.js +0 -6
  235. package/dist/vfs/chunked-vfs.d.ts +0 -66
  236. package/dist/vfs/chunked-vfs.js +0 -1290
  237. package/dist/vfs/host-block-store.d.ts +0 -19
  238. package/dist/vfs/host-block-store.js +0 -97
  239. package/dist/vfs/memory-block-store.d.ts +0 -16
  240. package/dist/vfs/memory-block-store.js +0 -45
  241. package/dist/vfs/memory-metadata.d.ts +0 -75
  242. package/dist/vfs/memory-metadata.js +0 -528
  243. package/dist/vfs/sqlite-metadata.d.ts +0 -91
  244. package/dist/vfs/sqlite-metadata.js +0 -582
  245. package/dist/vfs/types.d.ts +0 -210
  246. package/dist/vfs/types.js +0 -8
  247. /package/dist/{runtime-driver.js → generated/CreateVmConfig.js} +0 -0
  248. /package/dist/{shared/api-types.js → generated/FsPermissionRule.js} +0 -0
@@ -1,651 +0,0 @@
1
- /**
2
- * Process table.
3
- *
4
- * Universal process tracking across all runtimes. Owns PID allocation,
5
- * parent-child relationships, waitpid, and signal routing. A WasmVM
6
- * shell can waitpid on a Node child process.
7
- */
8
- import { KernelError, SIGCHLD, SIGALRM, SIGCONT, SIGSTOP, SIGTSTP, SIGKILL, SIGWINCH, WNOHANG, SA_RESETHAND, SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK, noopKernelLogger } from "./types.js";
9
- import { WaitQueue } from "./wait.js";
10
- import { encodeExitStatus, encodeSignalStatus } from "./wstatus.js";
11
- const ZOMBIE_TTL_MS = 60_000;
12
- export class ProcessTable {
13
- entries = new Map();
14
- nextPid = 1;
15
- waiters = new Map();
16
- zombieTimers = new Map();
17
- /** Pending alarm timers per PID: { timer, scheduledAt (ms epoch) }. */
18
- alarmTimers = new Map();
19
- log;
20
- /** Called when a process exits, before waiters are notified. */
21
- onProcessExit = null;
22
- /** Called when a zombie process is reaped (removed from the table). */
23
- onProcessReap = null;
24
- constructor(logger) {
25
- this.log = logger ?? noopKernelLogger;
26
- }
27
- /** Atomically allocate the next PID. */
28
- allocatePid() {
29
- return this.nextPid++;
30
- }
31
- /** Register a process with a pre-allocated PID. */
32
- register(pid, driver, command, args, ctx, driverProcess) {
33
- // Inherit pgid/sid/umask from parent, or default to own pid / 0o022
34
- const parent = ctx.ppid ? this.entries.get(ctx.ppid) : undefined;
35
- const pgid = parent?.pgid ?? pid;
36
- const sid = parent?.sid ?? pid;
37
- const umask = parent?.umask ?? 0o022;
38
- const entry = {
39
- pid,
40
- ppid: ctx.ppid,
41
- pgid,
42
- sid,
43
- driver,
44
- command,
45
- args,
46
- status: "running",
47
- exitCode: null,
48
- exitReason: null,
49
- termSignal: 0,
50
- startTime: Date.now(),
51
- exitTime: null,
52
- env: { ...ctx.env },
53
- cwd: ctx.cwd,
54
- umask,
55
- activeHandles: new Map(),
56
- handleLimit: 0,
57
- signalState: {
58
- handlers: new Map(),
59
- blockedSignals: new Set(),
60
- pendingSignals: new Set(),
61
- signalWaiters: new WaitQueue(),
62
- deliverySeq: 0,
63
- lastDeliveredSignal: null,
64
- lastDeliveredFlags: 0,
65
- },
66
- driverProcess,
67
- };
68
- this.entries.set(pid, entry);
69
- this.log.debug({ pid, ppid: ctx.ppid, pgid, sid, driver, command, args }, "process registered");
70
- // Wire up exit callback to mark process as exited
71
- driverProcess.onExit = (code) => {
72
- this.markExited(pid, code);
73
- };
74
- return entry;
75
- }
76
- get(pid) {
77
- return this.entries.get(pid);
78
- }
79
- /** Count pending zombie cleanup timers (test observability). */
80
- get zombieTimerCount() {
81
- return this.zombieTimers.size;
82
- }
83
- /** Count running (non-exited) processes. */
84
- runningCount() {
85
- let count = 0;
86
- for (const entry of this.entries.values()) {
87
- if (entry.status === "running")
88
- count++;
89
- }
90
- return count;
91
- }
92
- /** Mark a process as exited with the given code. Notifies waiters. */
93
- markExited(pid, exitCode) {
94
- const entry = this.entries.get(pid);
95
- if (!entry)
96
- return;
97
- if (entry.status === "exited")
98
- return;
99
- this.log.debug({
100
- pid, exitCode, command: entry.command,
101
- termSignal: entry.termSignal,
102
- reason: entry.termSignal > 0 ? "signal" : "normal",
103
- }, "process exited");
104
- entry.status = "exited";
105
- entry.exitCode = exitCode;
106
- entry.exitReason = entry.termSignal > 0 ? "signal" : "normal";
107
- entry.exitTime = Date.now();
108
- // Encode POSIX wstatus
109
- const wstatus = entry.termSignal > 0
110
- ? encodeSignalStatus(entry.termSignal)
111
- : encodeExitStatus(exitCode);
112
- // Cancel pending alarm
113
- this.cancelAlarm(pid);
114
- // Clear all active handles
115
- entry.activeHandles.clear();
116
- // Clean up process resources (FD table, pipe ends)
117
- this.onProcessExit?.(pid);
118
- // Deliver SIGCHLD to parent via signal handler system
119
- if (entry.ppid > 0) {
120
- const parent = this.entries.get(entry.ppid);
121
- if (parent && parent.status === "running") {
122
- this.deliverSignal(parent, SIGCHLD);
123
- }
124
- }
125
- // Notify waiters
126
- const waiters = this.waiters.get(pid);
127
- if (waiters) {
128
- for (const resolve of waiters) {
129
- resolve({ pid, status: wstatus, termSignal: entry.termSignal });
130
- }
131
- this.waiters.delete(pid);
132
- }
133
- // Schedule zombie cleanup (tracked for cancellation on dispose)
134
- const timer = setTimeout(() => {
135
- this.zombieTimers.delete(pid);
136
- this.reap(pid);
137
- }, ZOMBIE_TTL_MS);
138
- this.zombieTimers.set(pid, timer);
139
- }
140
- /**
141
- * Wait for a process to exit.
142
- * If already exited, resolves immediately. Otherwise blocks until exit.
143
- * With WNOHANG option, returns null immediately if process is still running.
144
- */
145
- waitpid(pid, options) {
146
- const entry = this.entries.get(pid);
147
- if (!entry) {
148
- return Promise.reject(new Error(`ESRCH: no such process ${pid}`));
149
- }
150
- if (entry.status === "exited") {
151
- const wstatus = entry.termSignal > 0
152
- ? encodeSignalStatus(entry.termSignal)
153
- : encodeExitStatus(entry.exitCode);
154
- return Promise.resolve({ pid, status: wstatus, termSignal: entry.termSignal });
155
- }
156
- // WNOHANG: return null immediately if process is still running
157
- if (options && (options & WNOHANG)) {
158
- return Promise.resolve(null);
159
- }
160
- return new Promise((resolve) => {
161
- let waiters = this.waiters.get(pid);
162
- if (!waiters) {
163
- waiters = [];
164
- this.waiters.set(pid, waiters);
165
- }
166
- waiters.push(resolve);
167
- });
168
- }
169
- /**
170
- * Send a signal to a process or process group.
171
- * If pid > 0, signal a single process.
172
- * If pid < 0, signal all processes in process group abs(pid).
173
- */
174
- kill(pid, signal) {
175
- // Validate signal range (POSIX: 0 = existence check, 1-64 = real signals)
176
- if (signal < 0 || signal > 64) {
177
- throw new KernelError("EINVAL", `invalid signal ${signal}`);
178
- }
179
- this.log.debug({ pid, signal }, "kill");
180
- if (pid < 0) {
181
- // Process group kill
182
- const pgid = -pid;
183
- let found = false;
184
- for (const entry of this.entries.values()) {
185
- if (entry.pgid === pgid && entry.status !== "exited") {
186
- found = true;
187
- if (signal !== 0) {
188
- this.deliverSignal(entry, signal);
189
- }
190
- }
191
- }
192
- if (!found)
193
- throw new KernelError("ESRCH", `no such process group ${pgid}`);
194
- return;
195
- }
196
- const entry = this.entries.get(pid);
197
- if (!entry)
198
- throw new KernelError("ESRCH", `no such process ${pid}`);
199
- if (entry.status === "exited")
200
- return;
201
- // Signal 0: existence check only — don't deliver
202
- if (signal === 0)
203
- return;
204
- this.deliverSignal(entry, signal);
205
- }
206
- /**
207
- * Deliver a signal to a process, respecting handlers, blocking, and coalescing.
208
- *
209
- * SIGKILL and SIGSTOP cannot be caught, blocked, or ignored (POSIX).
210
- * Blocked signals are queued in pendingSignals; standard signals (1-31) coalesce.
211
- * If a handler is registered, it is invoked with sa_mask temporarily blocked.
212
- */
213
- deliverSignal(entry, signal) {
214
- const { signalState } = entry;
215
- this.log.trace({ pid: entry.pid, signal, command: entry.command }, "deliver signal");
216
- // SIGKILL and SIGSTOP always use default action — cannot be caught/blocked/ignored
217
- if (signal === SIGKILL || signal === SIGSTOP) {
218
- this.applyDefaultAction(entry, signal);
219
- return;
220
- }
221
- // SIGCONT always resumes a stopped process, even if blocked or caught (POSIX)
222
- if (signal === SIGCONT) {
223
- this.cont(entry.pid);
224
- // If blocked, queue for handler delivery later; otherwise dispatch
225
- if (signalState.blockedSignals.has(signal)) {
226
- signalState.pendingSignals.add(signal);
227
- return;
228
- }
229
- this.dispatchSignal(entry, signal);
230
- return;
231
- }
232
- // If signal is blocked, queue it (standard signals 1-31 coalesce via Set)
233
- if (signalState.blockedSignals.has(signal)) {
234
- signalState.pendingSignals.add(signal);
235
- return;
236
- }
237
- this.dispatchSignal(entry, signal);
238
- }
239
- /**
240
- * Dispatch a signal to a process — check handler, then apply.
241
- * Called for unblocked signals and when delivering pending signals.
242
- */
243
- dispatchSignal(entry, signal) {
244
- const { signalState } = entry;
245
- const registration = signalState.handlers.get(signal);
246
- if (!registration) {
247
- // No handler registered — apply default action
248
- if (signal !== SIGCHLD) {
249
- this.recordSignalDelivery(signalState, signal, 0);
250
- }
251
- this.applyDefaultAction(entry, signal);
252
- return;
253
- }
254
- const { handler, mask, flags } = registration;
255
- if (handler === "ignore")
256
- return;
257
- if (handler === "default") {
258
- if (signal !== SIGCHLD) {
259
- this.recordSignalDelivery(signalState, signal, 0);
260
- }
261
- this.applyDefaultAction(entry, signal);
262
- return;
263
- }
264
- this.recordSignalDelivery(signalState, signal, flags);
265
- // User-defined handler: temporarily block sa_mask + the signal itself during execution
266
- const savedBlocked = new Set(signalState.blockedSignals);
267
- for (const s of mask)
268
- signalState.blockedSignals.add(s);
269
- signalState.blockedSignals.add(signal);
270
- try {
271
- handler(signal);
272
- }
273
- finally {
274
- // Restore previous blocked set
275
- signalState.blockedSignals = savedBlocked;
276
- }
277
- // Reset one-shot handlers before any pending re-delivery.
278
- if ((flags & SA_RESETHAND) !== 0) {
279
- signalState.handlers.set(signal, {
280
- handler: "default",
281
- mask: new Set(),
282
- flags: 0,
283
- });
284
- }
285
- // Deliver any signals that were pending and are now unblocked
286
- this.deliverPendingSignals(entry);
287
- }
288
- /** Wake signal-aware waiters after a signal has been dispatched. */
289
- recordSignalDelivery(signalState, signal, flags) {
290
- signalState.lastDeliveredSignal = signal;
291
- signalState.lastDeliveredFlags = flags;
292
- signalState.deliverySeq++;
293
- signalState.signalWaiters.wakeAll();
294
- }
295
- /** Apply the kernel default action for a signal. */
296
- applyDefaultAction(entry, signal) {
297
- if (signal === SIGTSTP || signal === SIGSTOP) {
298
- this.log.debug({ pid: entry.pid, signal, action: "stop" }, "signal default action");
299
- this.stop(entry.pid);
300
- entry.driverProcess.kill(signal);
301
- }
302
- else if (signal === SIGCONT) {
303
- this.log.debug({ pid: entry.pid, signal, action: "continue" }, "signal default action");
304
- this.cont(entry.pid);
305
- entry.driverProcess.kill(signal);
306
- }
307
- else if (signal === SIGCHLD || signal === SIGWINCH) {
308
- // Default action: ignore (POSIX — SIGCHLD and SIGWINCH don't terminate)
309
- return;
310
- }
311
- else {
312
- this.log.debug({ pid: entry.pid, signal, action: "terminate", command: entry.command }, "signal default action");
313
- entry.termSignal = signal;
314
- entry.driverProcess.kill(signal);
315
- }
316
- }
317
- /** Deliver pending signals that are no longer blocked (lowest signal number first). */
318
- deliverPendingSignals(entry) {
319
- const { signalState } = entry;
320
- if (signalState.pendingSignals.size === 0)
321
- return;
322
- // Deliver in ascending signal number order
323
- const pending = [...signalState.pendingSignals].sort((a, b) => a - b);
324
- for (const sig of pending) {
325
- // Check both: not blocked AND still pending (recursive delivery may have handled it)
326
- if (!signalState.blockedSignals.has(sig) && signalState.pendingSignals.has(sig)) {
327
- signalState.pendingSignals.delete(sig);
328
- this.dispatchSignal(entry, sig);
329
- if (entry.status === "exited")
330
- break;
331
- }
332
- }
333
- }
334
- /**
335
- * Schedule SIGALRM delivery after `seconds`. Returns previous alarm remaining (0 if none).
336
- * alarm(pid, 0) cancels any pending alarm. A new alarm replaces the previous one.
337
- */
338
- alarm(pid, seconds) {
339
- const entry = this.entries.get(pid);
340
- if (!entry)
341
- throw new KernelError("ESRCH", `no such process ${pid}`);
342
- // Calculate remaining time from any existing alarm
343
- let remaining = 0;
344
- const existing = this.alarmTimers.get(pid);
345
- if (existing) {
346
- const elapsed = (Date.now() - existing.scheduledAt) / 1000;
347
- remaining = Math.max(0, Math.ceil(existing.seconds - elapsed));
348
- clearTimeout(existing.timer);
349
- this.alarmTimers.delete(pid);
350
- }
351
- if (seconds === 0)
352
- return remaining;
353
- // Schedule new alarm
354
- const scheduledAt = Date.now();
355
- const timer = setTimeout(() => {
356
- this.alarmTimers.delete(pid);
357
- const e = this.entries.get(pid);
358
- if (!e || e.status !== "running")
359
- return;
360
- // Deliver through signal handler system
361
- this.deliverSignal(e, SIGALRM);
362
- }, seconds * 1000);
363
- this.alarmTimers.set(pid, { timer, scheduledAt, seconds });
364
- return remaining;
365
- }
366
- // -----------------------------------------------------------------------
367
- // Signal handlers (sigaction / sigprocmask)
368
- // -----------------------------------------------------------------------
369
- /**
370
- * Register a signal handler (POSIX sigaction).
371
- * Returns the previous handler for the signal, or undefined if none was set.
372
- * SIGKILL and SIGSTOP cannot be caught or ignored.
373
- */
374
- sigaction(pid, signal, handler) {
375
- const entry = this.entries.get(pid);
376
- if (!entry)
377
- throw new KernelError("ESRCH", `no such process ${pid}`);
378
- if (signal < 1 || signal > 64)
379
- throw new KernelError("EINVAL", `invalid signal ${signal}`);
380
- if (signal === SIGKILL || signal === SIGSTOP) {
381
- throw new KernelError("EINVAL", `cannot catch or ignore signal ${signal}`);
382
- }
383
- const prev = entry.signalState.handlers.get(signal);
384
- entry.signalState.handlers.set(signal, handler);
385
- return prev;
386
- }
387
- /**
388
- * Modify the blocked signal mask (POSIX sigprocmask).
389
- * Returns the previous blocked set.
390
- * SIGKILL and SIGSTOP cannot be blocked.
391
- */
392
- sigprocmask(pid, how, set) {
393
- const entry = this.entries.get(pid);
394
- if (!entry)
395
- throw new KernelError("ESRCH", `no such process ${pid}`);
396
- const { signalState } = entry;
397
- const prevBlocked = new Set(signalState.blockedSignals);
398
- // Filter out uncatchable signals
399
- const filtered = new Set(set);
400
- filtered.delete(SIGKILL);
401
- filtered.delete(SIGSTOP);
402
- if (how === SIG_BLOCK) {
403
- for (const s of filtered)
404
- signalState.blockedSignals.add(s);
405
- }
406
- else if (how === SIG_UNBLOCK) {
407
- for (const s of filtered)
408
- signalState.blockedSignals.delete(s);
409
- }
410
- else if (how === SIG_SETMASK) {
411
- signalState.blockedSignals = filtered;
412
- }
413
- else {
414
- throw new KernelError("EINVAL", `invalid sigprocmask how: ${how}`);
415
- }
416
- // Deliver any pending signals that are now unblocked
417
- this.deliverPendingSignals(entry);
418
- return prevBlocked;
419
- }
420
- /** Get the signal state for a process (read-only view). */
421
- getSignalState(pid) {
422
- const entry = this.entries.get(pid);
423
- if (!entry)
424
- throw new KernelError("ESRCH", `no such process ${pid}`);
425
- return entry.signalState;
426
- }
427
- /** Suspend a process (SIGTSTP/SIGSTOP). Sets status to 'stopped'. */
428
- stop(pid) {
429
- const entry = this.entries.get(pid);
430
- if (!entry)
431
- throw new KernelError("ESRCH", `no such process ${pid}`);
432
- if (entry.status !== "running")
433
- return;
434
- entry.status = "stopped";
435
- }
436
- /** Resume a stopped process (SIGCONT). Sets status back to 'running'. */
437
- cont(pid) {
438
- const entry = this.entries.get(pid);
439
- if (!entry)
440
- throw new KernelError("ESRCH", `no such process ${pid}`);
441
- if (entry.status !== "stopped")
442
- return;
443
- entry.status = "running";
444
- }
445
- /** Cancel a pending alarm for a process. */
446
- cancelAlarm(pid) {
447
- const existing = this.alarmTimers.get(pid);
448
- if (existing) {
449
- clearTimeout(existing.timer);
450
- this.alarmTimers.delete(pid);
451
- }
452
- }
453
- /** Set process group ID. Process can join existing group or create new one. */
454
- setpgid(pid, pgid) {
455
- const entry = this.entries.get(pid);
456
- if (!entry)
457
- throw new KernelError("ESRCH", `no such process ${pid}`);
458
- // pgid 0 means "use own PID as pgid"
459
- const targetPgid = pgid === 0 ? pid : pgid;
460
- // Can only join an existing group or create own group
461
- if (targetPgid !== pid) {
462
- let groupExists = false;
463
- for (const e of this.entries.values()) {
464
- if (e.pgid === targetPgid && e.status !== "exited") {
465
- // Reject cross-session group joining (POSIX)
466
- if (e.sid !== entry.sid) {
467
- throw new KernelError("EPERM", `cannot join process group in different session`);
468
- }
469
- groupExists = true;
470
- break;
471
- }
472
- }
473
- if (!groupExists)
474
- throw new KernelError("EPERM", `no such process group ${targetPgid}`);
475
- }
476
- entry.pgid = targetPgid;
477
- }
478
- /** Get process group ID. */
479
- getpgid(pid) {
480
- const entry = this.entries.get(pid);
481
- if (!entry)
482
- throw new KernelError("ESRCH", `no such process ${pid}`);
483
- return entry.pgid;
484
- }
485
- /** Create a new session. Process becomes session leader and process group leader. */
486
- setsid(pid) {
487
- const entry = this.entries.get(pid);
488
- if (!entry)
489
- throw new KernelError("ESRCH", `no such process ${pid}`);
490
- // Process must not already be a process group leader
491
- if (entry.pgid === pid) {
492
- throw new KernelError("EPERM", `process ${pid} is already a process group leader`);
493
- }
494
- entry.sid = pid;
495
- entry.pgid = pid;
496
- return pid;
497
- }
498
- /** Get session ID. */
499
- getsid(pid) {
500
- const entry = this.entries.get(pid);
501
- if (!entry)
502
- throw new KernelError("ESRCH", `no such process ${pid}`);
503
- return entry.sid;
504
- }
505
- /** Get the parent PID for a process. */
506
- getppid(pid) {
507
- const entry = this.entries.get(pid);
508
- if (!entry)
509
- throw new KernelError("ESRCH", `no such process ${pid}`);
510
- return entry.ppid;
511
- }
512
- /**
513
- * Send a signal to a process group, skipping session leaders.
514
- * Returns count of processes actually signaled.
515
- * Used for PTY-originated SIGINT where the session leader (shell)
516
- * cannot handle signals gracefully (WasmVM worker.terminate()).
517
- */
518
- killGroupExcludeLeaders(pgid, signal) {
519
- if (signal < 0 || signal > 64) {
520
- throw new KernelError("EINVAL", `invalid signal ${signal}`);
521
- }
522
- let count = 0;
523
- for (const entry of this.entries.values()) {
524
- if (entry.pgid === pgid && entry.status !== "exited") {
525
- if (entry.pid === entry.sid)
526
- continue; // Skip session leaders
527
- if (signal !== 0) {
528
- this.deliverSignal(entry, signal);
529
- }
530
- count++;
531
- }
532
- }
533
- return count;
534
- }
535
- /** Check if any running process belongs to the given process group. */
536
- hasProcessGroup(pgid) {
537
- for (const entry of this.entries.values()) {
538
- if (entry.pgid === pgid && entry.status !== "exited")
539
- return true;
540
- }
541
- return false;
542
- }
543
- /** Get a read-only view of process info for all processes. */
544
- listProcesses() {
545
- const result = new Map();
546
- for (const [pid, entry] of this.entries) {
547
- result.set(pid, {
548
- pid: entry.pid,
549
- ppid: entry.ppid,
550
- pgid: entry.pgid,
551
- sid: entry.sid,
552
- driver: entry.driver,
553
- command: entry.command,
554
- args: entry.args,
555
- cwd: entry.cwd,
556
- status: entry.status,
557
- exitCode: entry.exitCode,
558
- startTime: entry.startTime,
559
- exitTime: entry.exitTime,
560
- });
561
- }
562
- return result;
563
- }
564
- /** Remove a zombie process. */
565
- reap(pid) {
566
- const entry = this.entries.get(pid);
567
- if (entry?.status === "exited") {
568
- this.onProcessReap?.(pid);
569
- this.entries.delete(pid);
570
- }
571
- }
572
- // -----------------------------------------------------------------------
573
- // Handle tracking
574
- // -----------------------------------------------------------------------
575
- /** Register an active handle for a process. Throws EAGAIN if budget exceeded. */
576
- registerHandle(pid, id, description) {
577
- const entry = this.entries.get(pid);
578
- if (!entry)
579
- throw new KernelError("ESRCH", `no such process ${pid}`);
580
- if (entry.handleLimit > 0 && entry.activeHandles.size >= entry.handleLimit) {
581
- throw new KernelError("EAGAIN", `handle limit (${entry.handleLimit}) exceeded for process ${pid}`);
582
- }
583
- entry.activeHandles.set(id, description);
584
- }
585
- /** Unregister an active handle. Throws EBADF if handle not found. */
586
- unregisterHandle(pid, id) {
587
- const entry = this.entries.get(pid);
588
- if (!entry)
589
- throw new KernelError("ESRCH", `no such process ${pid}`);
590
- if (!entry.activeHandles.delete(id)) {
591
- throw new KernelError("EBADF", `no such handle ${id} for process ${pid}`);
592
- }
593
- }
594
- /** Set the maximum number of active handles for a process. 0 = unlimited. */
595
- setHandleLimit(pid, limit) {
596
- const entry = this.entries.get(pid);
597
- if (!entry)
598
- throw new KernelError("ESRCH", `no such process ${pid}`);
599
- entry.handleLimit = limit;
600
- }
601
- /** Get the active handles for a process (read-only copy). */
602
- getHandles(pid) {
603
- const entry = this.entries.get(pid);
604
- if (!entry)
605
- throw new KernelError("ESRCH", `no such process ${pid}`);
606
- return new Map(entry.activeHandles);
607
- }
608
- /** Terminate all running processes and clear pending timers. */
609
- async terminateAll() {
610
- // Clear all zombie cleanup timers to prevent post-dispose firings
611
- for (const timer of this.zombieTimers.values()) {
612
- clearTimeout(timer);
613
- }
614
- this.zombieTimers.clear();
615
- // Clear all pending alarm timers
616
- for (const { timer } of this.alarmTimers.values()) {
617
- clearTimeout(timer);
618
- }
619
- this.alarmTimers.clear();
620
- const running = [...this.entries.values()].filter((e) => e.status !== "exited");
621
- for (const entry of running) {
622
- try {
623
- entry.driverProcess.kill(15); // SIGTERM
624
- }
625
- catch {
626
- // Best effort
627
- }
628
- }
629
- // Wait briefly for graceful exits
630
- await Promise.allSettled(running.map((e) => Promise.race([
631
- e.driverProcess.wait(),
632
- new Promise((r) => setTimeout(r, 1000)),
633
- ])));
634
- // Escalate to SIGKILL for processes that survived SIGTERM
635
- const survivors = running.filter((e) => e.status !== "exited");
636
- for (const entry of survivors) {
637
- try {
638
- entry.driverProcess.kill(9); // SIGKILL
639
- }
640
- catch {
641
- // Best effort
642
- }
643
- }
644
- if (survivors.length > 0) {
645
- await Promise.allSettled(survivors.map((e) => Promise.race([
646
- e.driverProcess.wait(),
647
- new Promise((r) => setTimeout(r, 500)),
648
- ])));
649
- }
650
- }
651
- }