typescript-virtual-container 1.5.5 → 1.5.7

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 (64) hide show
  1. package/README.md +117 -35
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/index.d.ts +5 -1
  4. package/dist/SSHMimic/index.js +27 -3
  5. package/dist/SSHMimic/scp.d.ts +34 -0
  6. package/dist/SSHMimic/scp.js +285 -0
  7. package/dist/SSHMimic/sftp.d.ts +53 -3
  8. package/dist/SSHMimic/sftp.js +9 -3
  9. package/dist/VirtualFileSystem/binaryPack.d.ts +7 -0
  10. package/dist/VirtualFileSystem/binaryPack.js +37 -1
  11. package/dist/VirtualFileSystem/index.d.ts +7 -0
  12. package/dist/VirtualFileSystem/index.js +67 -27
  13. package/dist/VirtualFileSystem/internalTypes.d.ts +2 -0
  14. package/dist/VirtualFileSystem/path.d.ts +5 -0
  15. package/dist/VirtualFileSystem/path.js +24 -11
  16. package/dist/VirtualPackageManager/index.d.ts +4 -2
  17. package/dist/VirtualPackageManager/index.js +24 -4
  18. package/dist/VirtualShell/index.d.ts +4 -0
  19. package/dist/VirtualShell/index.js +1 -7
  20. package/dist/VirtualShell/shell.js +40 -10
  21. package/dist/VirtualShell/shellParser.js +1 -22
  22. package/dist/commands/awk.d.ts +6 -11
  23. package/dist/commands/awk.js +462 -109
  24. package/dist/commands/bzip2.d.ts +11 -0
  25. package/dist/commands/bzip2.js +91 -0
  26. package/dist/commands/exit.js +1 -1
  27. package/dist/commands/find.d.ts +2 -2
  28. package/dist/commands/find.js +209 -37
  29. package/dist/commands/helpers.d.ts +0 -20
  30. package/dist/commands/helpers.js +0 -97
  31. package/dist/commands/lsof.d.ts +6 -0
  32. package/dist/commands/lsof.js +30 -0
  33. package/dist/commands/perl.d.ts +6 -0
  34. package/dist/commands/perl.js +76 -0
  35. package/dist/commands/python.js +5 -2
  36. package/dist/commands/registry.js +19 -1
  37. package/dist/commands/runtime.js +65 -87
  38. package/dist/commands/sed.d.ts +2 -2
  39. package/dist/commands/sed.js +216 -34
  40. package/dist/commands/sh.js +42 -0
  41. package/dist/commands/strace.d.ts +6 -0
  42. package/dist/commands/strace.js +26 -0
  43. package/dist/commands/tar.d.ts +2 -1
  44. package/dist/commands/tar.js +138 -52
  45. package/dist/commands/test.js +2 -2
  46. package/dist/commands/zip.d.ts +11 -0
  47. package/dist/commands/zip.js +232 -0
  48. package/dist/modules/linuxRootfs.js +1 -4
  49. package/dist/modules/neofetch.js +2 -2
  50. package/dist/types/commands.d.ts +4 -0
  51. package/dist/utils/argv.d.ts +6 -0
  52. package/dist/utils/argv.js +32 -0
  53. package/dist/utils/expand.d.ts +5 -2
  54. package/dist/utils/expand.js +112 -45
  55. package/dist/utils/glob.d.ts +6 -0
  56. package/dist/utils/glob.js +34 -0
  57. package/dist/utils/tokenize.js +13 -13
  58. package/package.json +9 -7
  59. package/dist/self-standalone.d.ts +0 -1
  60. package/dist/self-standalone.js +0 -444
  61. package/dist/standalone-wo-sftp.d.ts +0 -1
  62. package/dist/standalone-wo-sftp.js +0 -30
  63. package/dist/standalone.d.ts +0 -1
  64. package/dist/standalone.js +0 -61
@@ -1,444 +0,0 @@
1
- import { readFile, unlink, writeFile } from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { basename } from "node:path";
4
- import { stdin, stdout } from "node:process";
5
- import { createInterface } from "node:readline";
6
- import { getCommandNames } from "./commands/registry";
7
- import { makeDefaultEnv, runCommand, userHome } from "./commands/runtime";
8
- import { spawnNanoEditorProcess } from "./modules/shellInteractive";
9
- import { resolvePath } from "./modules/shellRuntime";
10
- import { buildLoginBanner } from "./SSHMimic/loginBanner";
11
- import { buildPrompt } from "./SSHMimic/prompt";
12
- import { VirtualShell } from "./VirtualShell";
13
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
14
- const argv = process.argv.slice(2);
15
- // ── CLI args ──────────────────────────────────────────────────────────────────
16
- console.clear();
17
- function readUserArg() {
18
- for (let index = 0; index < argv.length; index += 1) {
19
- const current = argv[index];
20
- if (current === "--user") {
21
- const next = argv[index + 1];
22
- if (!next || next.startsWith("--")) {
23
- throw new Error("self-standalone: --user requires a value");
24
- }
25
- return next;
26
- }
27
- if (current?.startsWith("--user=")) {
28
- return current.slice("--user=".length) || "root";
29
- }
30
- }
31
- return "root";
32
- }
33
- const initialUser = readUserArg();
34
- const virtualShell = new VirtualShell(hostname, undefined, {
35
- mode: "fs",
36
- snapshotPath: ".vfs",
37
- });
38
- // ── VFS helpers ───────────────────────────────────────────────────────────────
39
- function readLastLogin(username) {
40
- const lastlogPath = `/home/${username}/.lastlog`;
41
- if (!virtualShell.vfs.exists(lastlogPath))
42
- return null;
43
- try {
44
- return JSON.parse(virtualShell.vfs.readFile(lastlogPath));
45
- }
46
- catch {
47
- return null;
48
- }
49
- }
50
- function writeLastLogin(username, from) {
51
- virtualShell.vfs.writeFile(`/home/${username}/.lastlog`, JSON.stringify({ at: new Date().toISOString(), from }));
52
- }
53
- async function flushVfs() {
54
- await virtualShell.vfs.stopAutoFlush();
55
- }
56
- function loadHistory(authUser) {
57
- const historyPath = `${userHome(authUser)}/.bash_history`;
58
- if (!virtualShell.vfs.exists(historyPath)) {
59
- virtualShell.vfs.writeFile(historyPath, "");
60
- return [];
61
- }
62
- return virtualShell.vfs
63
- .readFile(historyPath)
64
- .split("\n")
65
- .map((line) => line.trim())
66
- .filter((line) => line.length > 0);
67
- }
68
- function saveHistory(history, authUser) {
69
- const data = history.length > 0 ? `${history.join("\n")}\n` : "";
70
- virtualShell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
71
- }
72
- // ── Tab completion ────────────────────────────────────────────────────────────
73
- function listPathCompletions(vfs, cwd, prefix) {
74
- const slashIndex = prefix.lastIndexOf("/");
75
- const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
76
- const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
77
- const basePath = resolvePath(cwd, dirPart || ".");
78
- try {
79
- return vfs
80
- .list(basePath)
81
- .filter((e) => !e.startsWith(".") && e.startsWith(namePart))
82
- .map((e) => {
83
- const fullPath = path.posix.join(basePath, e);
84
- const st = vfs.stat(fullPath);
85
- return `${dirPart}${e}${st.type === "directory" ? "/" : ""}`;
86
- })
87
- .sort();
88
- }
89
- catch {
90
- return [];
91
- }
92
- }
93
- function makeCompleter(getState) {
94
- const commandNames = Array.from(new Set(getCommandNames())).sort();
95
- return (line, cb) => {
96
- const { cwd } = getState();
97
- // Extract the token under/before cursor (last whitespace-separated word)
98
- const token = line.split(/\s+/).at(-1) ?? "";
99
- const isFirstToken = line.trimStart() === token;
100
- const cmdHits = isFirstToken ? commandNames.filter((n) => n.startsWith(token)) : [];
101
- const pathHits = listPathCompletions(virtualShell.vfs, cwd, token);
102
- const hits = Array.from(new Set([...cmdHits, ...pathHits])).sort();
103
- cb(null, [hits, token]);
104
- };
105
- }
106
- // ── Hidden password input ─────────────────────────────────────────────────────
107
- function askHiddenQuestion(rl, promptText) {
108
- return new Promise((resolve) => {
109
- if (!stdin.isTTY || !stdout.isTTY) {
110
- rl.question(promptText, resolve);
111
- return;
112
- }
113
- const wasRawMode = Boolean(stdin.isRaw);
114
- let buffer = "";
115
- const cleanup = () => {
116
- stdin.off("data", onData);
117
- if (!wasRawMode)
118
- stdin.setRawMode(false);
119
- };
120
- const finish = (value) => {
121
- cleanup();
122
- stdout.write("\n");
123
- resolve(value);
124
- };
125
- const onData = (chunk) => {
126
- const input = chunk.toString("utf8");
127
- for (let i = 0; i < input.length; i += 1) {
128
- const ch = input[i];
129
- if (ch === "\r" || ch === "\n") {
130
- finish(buffer);
131
- return;
132
- }
133
- if (ch === "\u007f" || ch === "\b") {
134
- buffer = buffer.slice(0, -1);
135
- continue;
136
- }
137
- if (ch >= " ")
138
- buffer += ch;
139
- }
140
- };
141
- // Pause readline so it doesn't eat our raw keystrokes
142
- rl.pause();
143
- stdout.write(promptText);
144
- if (!wasRawMode)
145
- stdin.setRawMode(true);
146
- stdin.resume();
147
- stdin.on("data", onData);
148
- });
149
- }
150
- // ── Session state helper ──────────────────────────────────────────────────────
151
- function applySessionState(authUserState, cwdState, result, shellEnvState) {
152
- let authUser = authUserState;
153
- let cwd = cwdState;
154
- if (result.switchUser) {
155
- authUser = result.switchUser;
156
- cwd = result.nextCwd ?? userHome(authUser);
157
- shellEnvState.vars.USER = authUser;
158
- shellEnvState.vars.LOGNAME = authUser;
159
- shellEnvState.vars.HOME = userHome(authUser);
160
- shellEnvState.vars.PWD = cwd;
161
- }
162
- else if (result.nextCwd) {
163
- cwd = result.nextCwd;
164
- shellEnvState.vars.PWD = cwd;
165
- }
166
- return { authUser, cwd };
167
- }
168
- // ── Demo command ──────────────────────────────────────────────────────────────
169
- virtualShell.addCommand("demo", [], () => ({
170
- stdout: "This is a demo command. It does nothing useful.",
171
- exitCode: 0,
172
- }));
173
- // ── Main shell ────────────────────────────────────────────────────────────────
174
- async function runReadlineShell() {
175
- await virtualShell.ensureInitialized();
176
- const selectedUser = initialUser.trim() || "root";
177
- if (virtualShell.users.getPasswordHash(selectedUser) === null) {
178
- process.stderr.write(`self-standalone: user '${selectedUser}' does not exist\n`);
179
- process.exit(1);
180
- }
181
- // Ensure home dir and README.txt exist (mirrors SSHMimic/VirtualUserManager behaviour)
182
- const homePath = selectedUser === "root" ? "/root" : userHome(selectedUser);
183
- if (!virtualShell.vfs.exists(homePath)) {
184
- virtualShell.vfs.mkdir(homePath, selectedUser === "root" ? 0o700 : 0o755);
185
- }
186
- const readmePath = `${homePath}/README.txt`;
187
- if (!virtualShell.vfs.exists(readmePath)) {
188
- virtualShell.vfs.writeFile(readmePath, `Welcome to ${hostname}\n`);
189
- await virtualShell.vfs.stopAutoFlush();
190
- }
191
- const shellEnv = makeDefaultEnv(selectedUser, hostname);
192
- let authUser = selectedUser;
193
- let cwd = userHome(authUser);
194
- shellEnv.vars.PWD = cwd;
195
- const remoteAddress = "localhost";
196
- const terminalSize = { cols: stdout.columns ?? 80, rows: stdout.rows ?? 24 };
197
- let history = loadHistory(authUser);
198
- // completer reads cwd via closure — always current
199
- const rl = createInterface({
200
- input: stdin,
201
- output: stdout,
202
- terminal: true,
203
- completer: makeCompleter(() => ({ cwd })),
204
- });
205
- // Sync readline's internal history with our VFS history
206
- const rlWithHistory = rl;
207
- rlWithHistory.history = [...history].reverse();
208
- // ── nano editor ────────────────────────────────────────────────────────────
209
- async function startNanoEditor(targetPath, initialContent, tempPath) {
210
- if (virtualShell.vfs.exists(targetPath)) {
211
- await writeFile(tempPath, initialContent, "utf8");
212
- }
213
- rl.pause();
214
- const editor = spawnNanoEditorProcess(tempPath, terminalSize, {
215
- write: stdout.write.bind(stdout),
216
- exit: () => undefined,
217
- end: () => undefined,
218
- });
219
- const wasRawMode = Boolean(stdin.isRaw);
220
- const forwardInput = (chunk) => { editor.stdin.write(chunk); };
221
- stdin.resume();
222
- if (!wasRawMode)
223
- stdin.setRawMode(true);
224
- stdin.on("data", forwardInput);
225
- await new Promise((resolve) => {
226
- const cleanup = () => {
227
- stdin.off("data", forwardInput);
228
- if (!wasRawMode)
229
- stdin.setRawMode(false);
230
- rl.resume();
231
- };
232
- editor.on("error", (error) => {
233
- cleanup();
234
- stdout.write(`nano: ${error.message}\r\n`);
235
- resolve();
236
- });
237
- editor.on("close", async () => {
238
- cleanup();
239
- rl.write("", { ctrl: true, name: "u" });
240
- try {
241
- const updatedContent = await readFile(tempPath, "utf8");
242
- virtualShell.writeFileAsUser(authUser, targetPath, updatedContent);
243
- await flushVfs();
244
- }
245
- catch {
246
- // save skipped or temp file missing
247
- }
248
- await unlink(tempPath).catch(() => undefined);
249
- stdout.write("\r\n");
250
- resolve();
251
- });
252
- });
253
- }
254
- // ── challenge handlers ─────────────────────────────────────────────────────
255
- async function handleSudoChallenge(challenge) {
256
- if (challenge.onPassword) {
257
- let promptText = challenge.prompt;
258
- while (true) {
259
- const typed = await askHiddenQuestion(rl, promptText);
260
- const step = await challenge.onPassword(typed, virtualShell);
261
- if (step.result === null) {
262
- promptText = step.nextPrompt ?? promptText;
263
- continue;
264
- }
265
- await handleCommandResult(step.result);
266
- return;
267
- }
268
- }
269
- const password = await askHiddenQuestion(rl, challenge.prompt);
270
- if (!virtualShell.users.verifyPassword(challenge.username, password)) {
271
- process.stderr.write("Sorry, try again.\n");
272
- return;
273
- }
274
- if (!challenge.commandLine) {
275
- authUser = challenge.targetUser;
276
- cwd = userHome(authUser);
277
- shellEnv.vars.USER = authUser;
278
- shellEnv.vars.LOGNAME = authUser;
279
- shellEnv.vars.HOME = userHome(authUser);
280
- shellEnv.vars.PWD = cwd;
281
- return;
282
- }
283
- const runCwd = challenge.loginShell ? userHome(challenge.targetUser) : cwd;
284
- const nestedResult = await runCommand(challenge.commandLine, challenge.targetUser, hostname, "shell", runCwd, virtualShell, undefined, shellEnv);
285
- await handleCommandResult(nestedResult);
286
- }
287
- async function handlePasswordChallenge(challenge) {
288
- const first = await askHiddenQuestion(rl, challenge.prompt);
289
- if (challenge.confirmPrompt) {
290
- const second = await askHiddenQuestion(rl, challenge.confirmPrompt);
291
- if (second !== first) {
292
- process.stderr.write("passwords do not match\n");
293
- return;
294
- }
295
- }
296
- switch (challenge.action) {
297
- case "passwd":
298
- await virtualShell.users.setPassword(challenge.targetUsername, first);
299
- stdout.write("passwd: password updated successfully\n");
300
- break;
301
- case "adduser":
302
- if (!challenge.newUsername) {
303
- process.stderr.write("adduser: missing username\n");
304
- return;
305
- }
306
- await virtualShell.users.addUser(challenge.newUsername, first);
307
- stdout.write(`adduser: user '${challenge.newUsername}' created\n`);
308
- break;
309
- case "deluser":
310
- await virtualShell.users.deleteUser(challenge.targetUsername);
311
- stdout.write(`Removing user '${challenge.targetUsername}' ...\ndeluser: done.\n`);
312
- break;
313
- case "su":
314
- authUser = challenge.targetUsername;
315
- cwd = userHome(authUser);
316
- shellEnv.vars.USER = authUser;
317
- shellEnv.vars.LOGNAME = authUser;
318
- shellEnv.vars.HOME = userHome(authUser);
319
- shellEnv.vars.PWD = cwd;
320
- break;
321
- }
322
- }
323
- // handleCommandResult must be declared before the "line" handler
324
- async function handleCommandResult(result) {
325
- if (result.openEditor) {
326
- await startNanoEditor(result.openEditor.targetPath, result.openEditor.initialContent, result.openEditor.tempPath);
327
- return;
328
- }
329
- if (result.sudoChallenge) {
330
- await handleSudoChallenge(result.sudoChallenge);
331
- return;
332
- }
333
- if (result.passwordChallenge) {
334
- await handlePasswordChallenge(result.passwordChallenge);
335
- return;
336
- }
337
- if (result.clearScreen) {
338
- stdout.write("\u001b[2J\u001b[H");
339
- console.clear();
340
- }
341
- if (result.stdout) {
342
- stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}\n`);
343
- }
344
- if (result.stderr) {
345
- process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`);
346
- }
347
- const updated = applySessionState(authUser, cwd, result, shellEnv);
348
- authUser = updated.authUser;
349
- cwd = updated.cwd;
350
- if (result.closeSession) {
351
- await flushVfs();
352
- rl.close();
353
- process.exit(result.exitCode ?? 0);
354
- }
355
- }
356
- // ── Prompt helper ──────────────────────────────────────────────────────────
357
- const renderPrompt = () => {
358
- const cwdLabel = cwd === userHome(authUser) ? "~" : basename(cwd) || "/";
359
- return buildPrompt(authUser, hostname, cwdLabel);
360
- };
361
- const prompt = () => {
362
- rl.setPrompt(renderPrompt());
363
- rl.prompt();
364
- };
365
- // ── Auth (password gate) ───────────────────────────────────────────────────
366
- if (process.env.USER !== "root" && virtualShell.users.hasPassword(authUser)) {
367
- const password = await askHiddenQuestion(rl, `Password for ${authUser}: `);
368
- if (!virtualShell.users.verifyPassword(authUser, password)) {
369
- process.stderr.write("self-standalone: authentication failed\n");
370
- process.exit(1);
371
- }
372
- }
373
- // ── Login banner ───────────────────────────────────────────────────────────
374
- stdout.write(buildLoginBanner(hostname, virtualShell.properties, readLastLogin(authUser)));
375
- writeLastLogin(authUser, remoteAddress);
376
- await flushVfs();
377
- // ── Event-driven line handler (enables completer) ──────────────────────────
378
- //
379
- // Key insight: readline's completer only fires when readline itself owns
380
- // stdin (i.e. rl is not paused). We use the event-driven "line" pattern
381
- // instead of a while(true)+rl.once("line") loop so readline stays active
382
- // between commands. We pause only while awaiting async work, then resume
383
- // immediately before re-prompting so the next Tab press is caught.
384
- let busy = false;
385
- rl.on("line", async (inputLine) => {
386
- if (busy)
387
- return; // shouldn't happen but guard re-entrancy
388
- busy = true;
389
- rl.pause();
390
- const trimmed = inputLine.trim();
391
- if (trimmed.length > 0) {
392
- history.push(inputLine);
393
- if (history.length > 500)
394
- history = history.slice(history.length - 500);
395
- saveHistory(history, authUser);
396
- rlWithHistory.history = [...history].reverse();
397
- }
398
- const result = await runCommand(inputLine, authUser, hostname, "shell", cwd, virtualShell, undefined, shellEnv);
399
- await handleCommandResult(result);
400
- await flushVfs();
401
- busy = false;
402
- // Resume before prompt so readline can handle Tab on the next input
403
- rl.resume();
404
- prompt();
405
- });
406
- rl.on("SIGINT", () => {
407
- stdout.write("^C\n");
408
- rl.write("", { ctrl: true, name: "u" });
409
- prompt();
410
- });
411
- rl.on("close", () => {
412
- void flushVfs().then(() => {
413
- console.log("");
414
- process.exit(0);
415
- });
416
- });
417
- // Initial prompt — readline is already active, completer live from first keystroke
418
- prompt();
419
- }
420
- runReadlineShell().catch((error) => {
421
- console.error("Failed to start readline SSH emulation:", error);
422
- process.exit(1);
423
- });
424
- // ── Graceful shutdown (process-level) ────────────────────────────────────────
425
- let _shuttingDown = false;
426
- async function _gracefulShutdown(signal) {
427
- if (_shuttingDown)
428
- return;
429
- _shuttingDown = true;
430
- process.stdout.write(`\n[${signal}] Saving VFS...\n`);
431
- try {
432
- await virtualShell.vfs.stopAutoFlush();
433
- }
434
- catch { }
435
- process.exit(0);
436
- }
437
- process.on("SIGTERM", () => { void _gracefulShutdown("SIGTERM"); });
438
- process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
439
- process.on("uncaughtException", (error) => {
440
- console.error("Uncaught exception:", error);
441
- });
442
- process.on("unhandledRejection", (error, promise) => {
443
- console.error("Unhandled rejection at:", promise, "error:", error);
444
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,30 +0,0 @@
1
- import { SshMimic } from "./SSHMimic/index";
2
- import { VirtualShell } from "./VirtualShell";
3
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
4
- const virtualShell = new VirtualShell(hostname, undefined, {
5
- mode: "fs",
6
- snapshotPath: ".vfs",
7
- });
8
- virtualShell.addCommand("demo", [], () => {
9
- return {
10
- stdout: "This is a demo command. It does nothing useful.",
11
- exitCode: 0,
12
- };
13
- });
14
- new SshMimic({
15
- port: 2222,
16
- hostname,
17
- shell: virtualShell,
18
- })
19
- .start()
20
- .catch((error) => {
21
- console.error("Failed to start SSH Mimic:", error);
22
- process.exit(1);
23
- });
24
- process.on("uncaughtException", (error) => {
25
- console.log("Oh my god, something terrible happened: ", error);
26
- });
27
- process.on("unhandledRejection", (error, promise) => {
28
- console.log(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
29
- console.log(" The error was: ", error);
30
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,61 +0,0 @@
1
- import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
2
- const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
3
- const virtualShell = new VirtualShell(hostname, undefined, {
4
- mode: "fs",
5
- snapshotPath: ".vfs",
6
- });
7
- virtualShell.addCommand("demo", [], () => {
8
- return {
9
- stdout: "This is a demo command. It does nothing useful.",
10
- exitCode: 0,
11
- };
12
- });
13
- new VirtualSshServer({
14
- port: 2222,
15
- hostname,
16
- shell: virtualShell,
17
- })
18
- .start()
19
- .catch((error) => {
20
- console.error("Failed to start SSH Mimic:", error);
21
- process.exit(1);
22
- });
23
- new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
24
- .start()
25
- .catch((error) => {
26
- console.error("Failed to start SFTP Mimic:", error);
27
- process.exit(1);
28
- });
29
- // ── Graceful shutdown ─────────────────────────────────────────────────────────
30
- // On SIGINT / SIGTERM: flush the WAL journal to a full checkpoint before exit.
31
- // A kill -9 or OOM crash is unrecoverable here, but the WAL journal on disk
32
- // guarantees all writes since the last checkpoint are replayed on next start.
33
- let isShuttingDown = false;
34
- async function gracefulShutdown(signal) {
35
- if (isShuttingDown)
36
- return;
37
- isShuttingDown = true;
38
- console.log(`\n[${signal}] Flushing VFS checkpoint before exit...`);
39
- try {
40
- await virtualShell.vfs.stopAutoFlush();
41
- console.log("[shutdown] Checkpoint written. Goodbye.");
42
- }
43
- catch (err) {
44
- console.error("[shutdown] Flush failed:", err);
45
- }
46
- process.exit(0);
47
- }
48
- process.on("SIGINT", () => { void gracefulShutdown("SIGINT"); });
49
- process.on("SIGTERM", () => { void gracefulShutdown("SIGTERM"); });
50
- process.on("beforeExit", () => { void virtualShell.vfs.stopAutoFlush(); });
51
- process.on("uncaughtException", (error) => {
52
- console.debug("Oh my god, something terrible happened: ", error);
53
- });
54
- process.on("unhandledRejection", (error, promise) => {
55
- console.debug(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
56
- console.debug(" The error was: ", error);
57
- });
58
- setInterval(() => {
59
- const rss = process.memoryUsage().rss; // Just keep the event loop alive and prevent exit
60
- console.debug(`Current memory usage: ${Math.round(rss / 1024 / 1024)} MB`);
61
- }, 1000);