rightsize 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +28 -4
  2. package/dist/backend-docker/backend.d.ts +53 -1
  3. package/dist/backend-docker/backend.d.ts.map +1 -1
  4. package/dist/backend-docker/backend.js +106 -13
  5. package/dist/backend-docker/backend.js.map +1 -1
  6. package/dist/backend-docker/labels.d.ts +27 -5
  7. package/dist/backend-docker/labels.d.ts.map +1 -1
  8. package/dist/backend-docker/labels.js +33 -5
  9. package/dist/backend-docker/labels.js.map +1 -1
  10. package/dist/backend-msb/backend.d.ts +35 -6
  11. package/dist/backend-msb/backend.d.ts.map +1 -1
  12. package/dist/backend-msb/backend.js +69 -14
  13. package/dist/backend-msb/backend.js.map +1 -1
  14. package/dist/backend-msb/provider.d.ts.map +1 -1
  15. package/dist/backend-msb/provider.js +7 -11
  16. package/dist/backend-msb/provider.js.map +1 -1
  17. package/dist/backend-msb/provisioner.d.ts.map +1 -1
  18. package/dist/backend-msb/provisioner.js +1 -20
  19. package/dist/backend-msb/provisioner.js.map +1 -1
  20. package/dist/core/backend.d.ts +73 -0
  21. package/dist/core/backend.d.ts.map +1 -1
  22. package/dist/core/backends.d.ts +16 -2
  23. package/dist/core/backends.d.ts.map +1 -1
  24. package/dist/core/backends.js +39 -5
  25. package/dist/core/backends.js.map +1 -1
  26. package/dist/core/cache-dir.d.ts +17 -0
  27. package/dist/core/cache-dir.d.ts.map +1 -0
  28. package/dist/core/cache-dir.js +29 -0
  29. package/dist/core/cache-dir.js.map +1 -0
  30. package/dist/core/cleanup.d.ts +19 -4
  31. package/dist/core/cleanup.d.ts.map +1 -1
  32. package/dist/core/cleanup.js +14 -27
  33. package/dist/core/cleanup.js.map +1 -1
  34. package/dist/core/diagnostics.d.ts +33 -0
  35. package/dist/core/diagnostics.d.ts.map +1 -0
  36. package/dist/core/diagnostics.js +76 -0
  37. package/dist/core/diagnostics.js.map +1 -0
  38. package/dist/core/errors.d.ts +41 -0
  39. package/dist/core/errors.d.ts.map +1 -1
  40. package/dist/core/errors.js +56 -0
  41. package/dist/core/errors.js.map +1 -1
  42. package/dist/core/generic-container.d.ts +107 -1
  43. package/dist/core/generic-container.d.ts.map +1 -1
  44. package/dist/core/generic-container.js +417 -3
  45. package/dist/core/generic-container.js.map +1 -1
  46. package/dist/core/model.d.ts +22 -0
  47. package/dist/core/model.d.ts.map +1 -1
  48. package/dist/core/network.d.ts.map +1 -1
  49. package/dist/core/network.js +2 -0
  50. package/dist/core/network.js.map +1 -1
  51. package/dist/core/reaper/env.d.ts +14 -0
  52. package/dist/core/reaper/env.d.ts.map +1 -0
  53. package/dist/core/reaper/env.js +22 -0
  54. package/dist/core/reaper/env.js.map +1 -0
  55. package/dist/core/reaper/init.d.ts +39 -0
  56. package/dist/core/reaper/init.d.ts.map +1 -0
  57. package/dist/core/reaper/init.js +140 -0
  58. package/dist/core/reaper/init.js.map +1 -0
  59. package/dist/core/reaper/ledger.d.ts +32 -0
  60. package/dist/core/reaper/ledger.d.ts.map +1 -0
  61. package/dist/core/reaper/ledger.js +160 -0
  62. package/dist/core/reaper/ledger.js.map +1 -0
  63. package/dist/core/reaper/process-liveness.d.ts +39 -0
  64. package/dist/core/reaper/process-liveness.d.ts.map +1 -0
  65. package/dist/core/reaper/process-liveness.js +117 -0
  66. package/dist/core/reaper/process-liveness.js.map +1 -0
  67. package/dist/core/reaper/run-record.d.ts +48 -0
  68. package/dist/core/reaper/run-record.d.ts.map +1 -0
  69. package/dist/core/reaper/run-record.js +58 -0
  70. package/dist/core/reaper/run-record.js.map +1 -0
  71. package/dist/core/reaper/sweep.d.ts +23 -0
  72. package/dist/core/reaper/sweep.d.ts.map +1 -0
  73. package/dist/core/reaper/sweep.js +76 -0
  74. package/dist/core/reaper/sweep.js.map +1 -0
  75. package/dist/core/reaper/watchdog.d.ts +100 -0
  76. package/dist/core/reaper/watchdog.d.ts.map +1 -0
  77. package/dist/core/reaper/watchdog.js +412 -0
  78. package/dist/core/reaper/watchdog.js.map +1 -0
  79. package/dist/core/reuse/env.d.ts +12 -0
  80. package/dist/core/reuse/env.d.ts.map +1 -0
  81. package/dist/core/reuse/env.js +15 -0
  82. package/dist/core/reuse/env.js.map +1 -0
  83. package/dist/core/reuse/hash.d.ts +34 -0
  84. package/dist/core/reuse/hash.d.ts.map +1 -0
  85. package/dist/core/reuse/hash.js +52 -0
  86. package/dist/core/reuse/hash.js.map +1 -0
  87. package/dist/core/reuse/registry.d.ts +50 -0
  88. package/dist/core/reuse/registry.d.ts.map +1 -0
  89. package/dist/core/reuse/registry.js +75 -0
  90. package/dist/core/reuse/registry.js.map +1 -0
  91. package/dist/index.d.ts +5 -3
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -1
  94. package/dist/index.js.map +1 -1
  95. package/package.json +1 -1
  96. package/dist/backend-msb/reaper.d.ts +0 -10
  97. package/dist/backend-msb/reaper.d.ts.map +0 -1
  98. package/dist/backend-msb/reaper.js +0 -73
  99. package/dist/backend-msb/reaper.js.map +0 -1
@@ -0,0 +1,412 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import * as fsSync from "node:fs";
4
+ import * as fsp from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ export function watchdogDir(cacheDir) {
7
+ return path.join(cacheDir, "reaper");
8
+ }
9
+ /**
10
+ * `watchdog-<12 hex of SHA-256(content)>.<ext>` — the filename derives from the
11
+ * script's own bytes. The `reaper/` directory is shared with the sibling rightsize
12
+ * libraries (Kotlin, Rust) and with other versions of this package, each shipping
13
+ * its own script with its own argv contract; content-derived names make it
14
+ * impossible to execute a script whose contract this code doesn't match, and
15
+ * impossible for a write to clobber a script another process's `sh` is executing.
16
+ * Exported for tests.
17
+ */
18
+ export function watchdogScriptFilename(content) {
19
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
20
+ return process.platform === "win32" ? `watchdog-${hash}.js` : `watchdog-${hash}.sh`;
21
+ }
22
+ /**
23
+ * POSIX `sh` driver: blocks reading stdin until EOF (the invoking rightsize
24
+ * process holds the other end of the pipe; EOF means that process is gone,
25
+ * cleanly or via SIGKILL), then reaps every sandbox/network the run tracked
26
+ * and deletes its three ledger files. Written once under its content-derived
27
+ * name (see watchdogScriptFilename) — this file is never hand-edited, and
28
+ * each command prefix arrives via argv (word-split on spaces) rather than
29
+ * being baked into the script, so a unit test can substitute a stub recorder
30
+ * command without touching this file's content at all.
31
+ */
32
+ function posixScriptContent() {
33
+ return `#!/bin/sh
34
+ # rightsize reaper watchdog — generated file, named by its own content hash.
35
+ # argv: <sandboxesPath> <networksPath> <recordPath> <stopCmd> <removeCmd> <removeNetworkCmd>
36
+ # Each *Cmd is a single space-joined argv prefix (may be empty); the name
37
+ # being reaped is appended as the final argument.
38
+ set -u
39
+
40
+ SANDBOXES="$1"
41
+ NETWORKS="$2"
42
+ RECORD="$3"
43
+ STOP_CMD="$4"
44
+ REMOVE_CMD="$5"
45
+ REMOVE_NET_CMD="$6"
46
+
47
+ # Block here until the holder of the pipe's write end exits — cleanly or via
48
+ # SIGKILL, either way the OS closes that fd for us and this read hits EOF.
49
+ cat >/dev/null
50
+
51
+ # msb's shared state-database migration race (see isMsbStateDbError in the
52
+ # TypeScript backend) can fail a stop/rm exactly the same way it can fail a
53
+ # boot; retried once, same as the library's own in-process retry policy.
54
+ # Harmless no-op against docker's "docker rm -f" output, which never matches.
55
+ run_cmd() {
56
+ prefix="$1"
57
+ name="$2"
58
+ [ -z "$prefix" ] && return 0
59
+ # shellcheck disable=SC2086 -- intentional word-splitting of the prefix
60
+ out=$($prefix "$name" 2>&1)
61
+ if printf '%s' "$out" | grep -q "error: database error:"; then
62
+ # shellcheck disable=SC2086
63
+ $prefix "$name" >/dev/null 2>&1
64
+ fi
65
+ return 0
66
+ }
67
+
68
+ if [ -f "$SANDBOXES" ]; then
69
+ while IFS= read -r name || [ -n "$name" ]; do
70
+ [ -z "$name" ] && continue
71
+ run_cmd "$STOP_CMD" "$name"
72
+ run_cmd "$REMOVE_CMD" "$name"
73
+ done < "$SANDBOXES"
74
+ fi
75
+
76
+ if [ -f "$NETWORKS" ]; then
77
+ while IFS= read -r net || [ -n "$net" ]; do
78
+ [ -z "$net" ] && continue
79
+ run_cmd "$REMOVE_NET_CMD" "$net"
80
+ done < "$NETWORKS"
81
+ fi
82
+
83
+ rm -f "$SANDBOXES" "$NETWORKS" "$RECORD"
84
+ `;
85
+ }
86
+ /**
87
+ * Windows driver — a plain Node.js script, NOT PowerShell. Two things were
88
+ * proven directly (RIGHTSIZE_REAPER_DEBUG=1 evidence, see the investigation
89
+ * this replaces in git history) rather than assumed:
90
+ *
91
+ * 1. On Windows, a NON-detached child of this process is killed by the OS
92
+ * itself the instant this process is torn down (SIGKILL or otherwise),
93
+ * regardless of what the child is doing — confirmed with a minimal,
94
+ * PowerShell/msb-independent control experiment (a plain `node -e`
95
+ * heartbeat writer stopped heartbeating within a few hundred ms of the
96
+ * owner's kill). This is Windows Job Object semantics: Node/libuv ties a
97
+ * non-detached child to a job object that the OS closes (and, with it,
98
+ * terminates every member process) when the owner's own job handle
99
+ * closes. `detached: true` opts out of that job.
100
+ * 2. `detached: true` reliably lets an ORDINARY process survive — the same
101
+ * control experiment's detached variant kept heartbeating for the full
102
+ * 60s+ test window, well past the owner's death. But BOTH PowerShell
103
+ * hosts available on Windows (legacy `powershell` and `pwsh`) die within
104
+ * ~200ms of being spawned with `detached: true`, before running a
105
+ * single script line, independent of whether the owner has even been
106
+ * killed yet — `DETACHED_PROCESS` (what `detached: true` maps to on
107
+ * win32) appears fatal to launching either PowerShell host via `-File`,
108
+ * not a version-specific ConsoleHost quirk.
109
+ *
110
+ * Put together: the fix isn't a different PowerShell invocation shape, it's
111
+ * not using PowerShell at all. `node` itself is proven to survive
112
+ * `detached: true` on this exact platform, so the Windows watchdog is
113
+ * `node <this script>.js <args>`, `detached: true` — the SAME reap/cleanup
114
+ * sequence as the POSIX `sh` script, just polling for the owner's pid
115
+ * (`process.kill(pid, 0)` — the exact liveness primitive
116
+ * process-liveness.ts's cross-platform sweep already uses successfully on
117
+ * Windows) every 500ms instead of blocking on stdin EOF (which POSIX can
118
+ * do safely — see posixScriptContent's doc — but which this investigation
119
+ * separately found unreliable on Windows before settling on the job-object
120
+ * explanation above; moot once nothing here reads stdin at all).
121
+ *
122
+ * Emits timestamped checkpoint lines to `<RecordPath's directory>\
123
+ * watchdog-trace-<PID>.log` when `RIGHTSIZE_REAPER_DEBUG=1` is set in this
124
+ * process's OWN environment (inherited from whatever spawned it) — see
125
+ * watchdogTracePath.
126
+ */
127
+ function windowsScriptContent() {
128
+ return `// rightsize reaper watchdog — generated file, named by its own content hash.
129
+ // argv: <sandboxesPath> <networksPath> <recordPath> <stopCmd> <removeCmd> <removeNetworkCmd> <ownerPid>
130
+ // Each *Cmd is a single space-joined argv prefix (may be empty); the name
131
+ // being reaped is appended as the final argument.
132
+ "use strict";
133
+ const fs = require("fs");
134
+ const path = require("path");
135
+ const { spawnSync } = require("child_process");
136
+
137
+ const [sandboxesPath, networksPath, recordPath, stopCmd, removeCmd, removeNetCmd, ownerPidRaw] = process.argv.slice(2);
138
+ const ownerPid = Number(ownerPidRaw);
139
+
140
+ const debugOn = process.env.RIGHTSIZE_REAPER_DEBUG === "1";
141
+ const tracePath = path.join(path.dirname(recordPath), \`watchdog-trace-\${process.pid}.log\`);
142
+ function trace(msg) {
143
+ if (!debugOn) return;
144
+ try {
145
+ fs.appendFileSync(tracePath, \`[\${new Date().toISOString()}] \${msg}\\n\`);
146
+ } catch {
147
+ // best-effort diagnostics only
148
+ }
149
+ }
150
+
151
+ trace("started");
152
+ trace(\`params: ownerPid=[\${ownerPid}] stopCmd=[\${stopCmd}] removeCmd=[\${removeCmd}] removeNetCmd=[\${removeNetCmd}]\`);
153
+
154
+ // process.kill(pid, 0) sends no signal, just checks existence — the same
155
+ // primitive process-liveness.ts's isProcessAlive uses, cross-platform.
156
+ function ownerAlive() {
157
+ try {
158
+ process.kill(ownerPid, 0);
159
+ return true;
160
+ } catch (err) {
161
+ return err && err.code === "EPERM";
162
+ }
163
+ }
164
+
165
+ function splitPrefix(cmd) {
166
+ return cmd.length === 0 ? [] : cmd.split(" ");
167
+ }
168
+
169
+ // msb's shared state-database migration race (see isMsbStateDbError in the
170
+ // TypeScript backend) can fail a stop/rm exactly the same way it can fail a
171
+ // boot; retried once, same as the library's own in-process retry policy.
172
+ // Harmless no-op against docker's "docker rm -f" output, which never matches.
173
+ function runCmd(prefix, name) {
174
+ const words = splitPrefix(prefix);
175
+ if (words.length === 0) return;
176
+ const [exe, ...rest] = words;
177
+ const args = [...rest, name];
178
+ const run = () => {
179
+ const res = spawnSync(exe, args, { encoding: "utf8" });
180
+ return (res.stdout || "") + (res.stderr || "");
181
+ };
182
+ const out = run();
183
+ if (out.includes("error: database error:")) {
184
+ run();
185
+ }
186
+ }
187
+
188
+ function reapLines(filePath, cmds) {
189
+ if (!fs.existsSync(filePath)) return;
190
+ const names = fs.readFileSync(filePath, "utf8").split("\\n").filter((l) => l.length > 0);
191
+ for (const name of names) {
192
+ for (const cmd of cmds) {
193
+ if (cmd === undefined) continue;
194
+ trace(\`\${cmd === stopCmd ? "stop" : cmd === removeCmd ? "remove" : "remove-net"} \${name}\`);
195
+ runCmd(cmd, name);
196
+ }
197
+ }
198
+ }
199
+
200
+ function poll() {
201
+ const pollCountKey = "__rzPollCount";
202
+ poll[pollCountKey] = (poll[pollCountKey] || 0) + 1;
203
+ const n = poll[pollCountKey];
204
+ if (n === 1 || n % 20 === 0) {
205
+ trace(\`poll #\${n}: ownerAlive(\${ownerPid}) -> \${ownerAlive()}\`);
206
+ }
207
+ if (ownerAlive()) {
208
+ setTimeout(poll, 500);
209
+ return;
210
+ }
211
+ trace("owner gone");
212
+ reapLines(sandboxesPath, [stopCmd, removeCmd]);
213
+ reapLines(networksPath, [removeNetCmd]);
214
+ trace("removing ledger files");
215
+ for (const p of [sandboxesPath, networksPath, recordPath]) {
216
+ try {
217
+ fs.unlinkSync(p);
218
+ } catch {
219
+ // already gone — fine
220
+ }
221
+ }
222
+ trace("done");
223
+ }
224
+
225
+ poll();
226
+ `;
227
+ }
228
+ /** Writes the platform-appropriate watchdog script under `<cacheDir>/reaper/` (skipped when its content-named file already exists) and returns its path. */
229
+ export async function ensureWatchdogScript(cacheDir) {
230
+ const dir = watchdogDir(cacheDir);
231
+ await fsp.mkdir(dir, { recursive: true });
232
+ const content = process.platform === "win32" ? windowsScriptContent() : posixScriptContent();
233
+ const scriptPath = path.join(dir, watchdogScriptFilename(content));
234
+ try {
235
+ await fsp.access(scriptPath);
236
+ return scriptPath; // the filename encodes the content, so presence means identity
237
+ }
238
+ catch {
239
+ // absent — write below
240
+ }
241
+ const tmpPath = `${scriptPath}.${process.pid}.tmp`;
242
+ await fsp.writeFile(tmpPath, content);
243
+ if (process.platform !== "win32") {
244
+ await fsp.chmod(tmpPath, 0o755);
245
+ }
246
+ try {
247
+ await fsp.rename(tmpPath, scriptPath);
248
+ }
249
+ catch (err) {
250
+ // A concurrent writer beat us to it with identical bytes; otherwise re-throw.
251
+ await fsp.rm(tmpPath, { force: true });
252
+ try {
253
+ await fsp.access(scriptPath);
254
+ }
255
+ catch {
256
+ throw err;
257
+ }
258
+ }
259
+ return scriptPath;
260
+ }
261
+ function joinPrefix(prefix) {
262
+ return prefix.join(" ");
263
+ }
264
+ /**
265
+ * `RIGHTSIZE_REAPER_DEBUG=1` turns on the watchdog spawn diagnostics below —
266
+ * off by default (production spawns never pay for the extra file opens or
267
+ * the diagnostics IO). Meant to be set for a single CI debug run, not left
268
+ * on permanently, though the affordance itself is cheap enough to ship.
269
+ */
270
+ function reaperDebugEnabled() {
271
+ return process.env["RIGHTSIZE_REAPER_DEBUG"] === "1";
272
+ }
273
+ /** The run id a ledger path was generated for — `runs/<run-id>.sandboxes` etc. */
274
+ function runIdFromSandboxesPath(sandboxesPathValue) {
275
+ return path.basename(sandboxesPathValue).replace(/\.sandboxes$/, "");
276
+ }
277
+ /**
278
+ * Paths the debug affordance below writes to when `RIGHTSIZE_REAPER_DEBUG=1`
279
+ * — the watchdog's own stdout/stderr (normally `"ignore"`d) plus a meta log
280
+ * of spawn/error/exit events, all under the same `reaper/` directory the
281
+ * watchdog script itself lives in. Exported so a test can locate them
282
+ * without duplicating the naming scheme.
283
+ */
284
+ export function watchdogDebugPaths(cacheDir, runId) {
285
+ const dir = watchdogDir(cacheDir);
286
+ return {
287
+ out: path.join(dir, `watchdog-${runId}.debug.out.log`),
288
+ err: path.join(dir, `watchdog-${runId}.debug.err.log`),
289
+ meta: path.join(dir, `watchdog-${runId}.debug.meta.log`),
290
+ };
291
+ }
292
+ /**
293
+ * Where the Windows script's own in-script trace (see windowsScriptContent's
294
+ * doc) lands: same directory as the run record, named by the watchdog
295
+ * process's own pid — the one piece of the script's identity a caller can
296
+ * know ahead of the script writing anything itself. POSIX has no equivalent
297
+ * (its script has no such tracing; its debug story is the stdout/stderr
298
+ * capture above, which is enough there since `sh -x` isn't needed to tell
299
+ * the difference between "never started" and "died partway").
300
+ */
301
+ export function watchdogTracePath(recordPathValue, pid) {
302
+ return path.join(path.dirname(recordPathValue), `watchdog-trace-${pid}.log`);
303
+ }
304
+ function appendDebugLine(filePath, line) {
305
+ try {
306
+ fsSync.appendFileSync(filePath, `[${new Date().toISOString()}] ${line}\n`);
307
+ }
308
+ catch {
309
+ // Best-effort diagnostics only — never let a debug-log write failure
310
+ // affect the watchdog spawn itself.
311
+ }
312
+ }
313
+ /**
314
+ * Spawns the watchdog detached, unref()'d so it never keeps this process's
315
+ * event loop alive.
316
+ *
317
+ * On POSIX, its stdin is a pipe whose write end ONLY this process holds —
318
+ * that write end is `handle.process.stdin`, returned so the caller can
319
+ * keep a live reference for the rest of the process's life (letting it get
320
+ * garbage-collected or explicitly closing it would arm the watchdog
321
+ * early). `detached: true` there just means the standard POSIX
322
+ * survive-your-parent behavior (setsid); this has always worked and this
323
+ * investigation never found a POSIX failure.
324
+ *
325
+ * On Windows, `detached: true` is load-bearing for a completely different
326
+ * reason, proven with a minimal control experiment (a plain `node -e`
327
+ * heartbeat writer — see git history): a NON-detached Windows child is
328
+ * killed by the OS itself the instant this process is torn down, via
329
+ * Windows Job Object semantics (Node/libuv ties a non-detached child to a
330
+ * job object that closes, killing every member, when the owner's own job
331
+ * handle closes) — regardless of what the child is doing. `detached: true`
332
+ * escapes that job and reliably survives (confirmed: 60s+ of continued
333
+ * heartbeats past the owner's death). PowerShell was the original design
334
+ * for the Windows script, but BOTH available hosts (legacy `powershell`
335
+ * and `pwsh`) die within ~200ms of being spawned with `detached: true`,
336
+ * before running a single line — so the Windows watchdog is `node` running
337
+ * a small generated `.js` file instead (see windowsScriptContent), since
338
+ * plain `node` IS proven to survive `detached: true` here. Its stdin is
339
+ * `"ignore"` — it polls for this process's death rather than reading
340
+ * anything, so there is no pipe to leak into any later child this process
341
+ * spawns either. All other stdio is `"ignore"` (or, under
342
+ * `RIGHTSIZE_REAPER_DEBUG`, captured to files) so CI runners never hang on
343
+ * an inherited handle.
344
+ */
345
+ export async function spawnWatchdog(opts) {
346
+ const scriptPath = await ensureWatchdogScript(opts.cacheDir);
347
+ const interpreter = process.platform === "win32" ? process.execPath : "sh";
348
+ const baseArgs = [scriptPath];
349
+ const argv = [
350
+ ...baseArgs,
351
+ opts.sandboxesPath,
352
+ opts.networksPath,
353
+ opts.recordPath,
354
+ joinPrefix(opts.killCommand.stop),
355
+ joinPrefix(opts.killCommand.remove),
356
+ joinPrefix(opts.killCommand.removeNetwork),
357
+ ...(process.platform === "win32" ? [String(opts.ownerPid)] : []),
358
+ ];
359
+ const debug = reaperDebugEnabled();
360
+ const runId = runIdFromSandboxesPath(opts.sandboxesPath);
361
+ const debugPaths = debug ? watchdogDebugPaths(opts.cacheDir, runId) : undefined;
362
+ if (debugPaths !== undefined) {
363
+ fsSync.mkdirSync(watchdogDir(opts.cacheDir), { recursive: true });
364
+ }
365
+ // Normally "ignore" (CI runners must never hang on an inherited handle);
366
+ // under RIGHTSIZE_REAPER_DEBUG=1 the watchdog's own stdout/stderr are
367
+ // instead captured to files so a failing run can show what the script
368
+ // itself printed rather than nothing at all. Stdin is "pipe" only on
369
+ // POSIX — see this function's doc.
370
+ const stdin = process.platform === "win32" ? "ignore" : "pipe";
371
+ const stdio = debugPaths === undefined
372
+ ? [stdin, "ignore", "ignore"]
373
+ : [stdin, fsSync.openSync(debugPaths.out, "a"), fsSync.openSync(debugPaths.err, "a")];
374
+ const child = spawn(interpreter, argv, {
375
+ detached: true,
376
+ windowsHide: true,
377
+ stdio,
378
+ });
379
+ if (debugPaths !== undefined) {
380
+ appendDebugLine(debugPaths.meta, `spawn interpreter=${interpreter} pid=${child.pid ?? "unknown"} argv=${JSON.stringify(argv)}`);
381
+ child.once("error", (err) => {
382
+ appendDebugLine(debugPaths.meta, `spawn error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
383
+ });
384
+ child.once("exit", (code, signal) => {
385
+ appendDebugLine(debugPaths.meta, `exit code=${code ?? "null"} signal=${signal ?? "null"}`);
386
+ });
387
+ }
388
+ else {
389
+ // Always listen for 'error', debug or not: an unlistened 'error' event
390
+ // on a ChildProcess is an uncaught exception in this process, and a
391
+ // failed watchdog spawn (e.g. the interpreter missing from PATH) must
392
+ // stay best-effort like every other reaper failure mode, never crash
393
+ // the host application.
394
+ child.once("error", () => { });
395
+ }
396
+ child.unref();
397
+ // child.stdin is typed as the generic Writable interface, but for a
398
+ // "pipe" stdio entry Node always backs it with a net.Socket, which DOES
399
+ // expose unref() — without this, the held write end would keep this
400
+ // process's event loop alive even though child.unref() already detached
401
+ // the child itself. A no-op on Windows, where stdin is "ignore" and
402
+ // child.stdin is null.
403
+ const stdinSocket = child.stdin;
404
+ stdinSocket?.unref?.();
405
+ return {
406
+ process: child,
407
+ closeForTests: () => {
408
+ child.stdin?.end();
409
+ },
410
+ };
411
+ }
412
+ //# sourceMappingURL=watchdog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../../../src/core/reaper/watchdog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The second half of reuse's double opt-in (see `GenericContainer.withReuse`
3
+ * for the first half, the API marker): exactly `"true"` or `"1"`, matching
4
+ * the feature spec's own wording. Any other value — including a
5
+ * near-miss like `"True"` or `"yes"` — is treated as disabled, the same
6
+ * strict-match convention as the rest of this library's boolean-shaped env
7
+ * vars (unlike `RIGHTSIZE_REAPER`, whose unrecognized values fall back to
8
+ * its safe default rather than "off": reuse's safe default IS off, so an
9
+ * unrecognized value staying off needs no special-casing here at all).
10
+ */
11
+ export declare function reuseEnabled(env: Record<string, string | undefined>): boolean;
12
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/core/reuse/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,OAAO,CAG7E"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The second half of reuse's double opt-in (see `GenericContainer.withReuse`
3
+ * for the first half, the API marker): exactly `"true"` or `"1"`, matching
4
+ * the feature spec's own wording. Any other value — including a
5
+ * near-miss like `"True"` or `"yes"` — is treated as disabled, the same
6
+ * strict-match convention as the rest of this library's boolean-shaped env
7
+ * vars (unlike `RIGHTSIZE_REAPER`, whose unrecognized values fall back to
8
+ * its safe default rather than "off": reuse's safe default IS off, so an
9
+ * unrecognized value staying off needs no special-casing here at all).
10
+ */
11
+ export function reuseEnabled(env) {
12
+ const raw = env["RIGHTSIZE_REUSE"];
13
+ return raw === "true" || raw === "1";
14
+ }
15
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/core/reuse/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,GAAuC;IAClE,MAAM,GAAG,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,OAAO,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC;AACvC,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The reuse-relevant subset of a container's builder state — everything
3
+ * that busts identity if it changes, and nothing else (host ports, the
4
+ * process's `RunId`, network topology, and `readOnly` are all deliberately
5
+ * absent). This is the cross-language contract: a Kotlin or Rust rightsize
6
+ * process hashing the "same" logical spec must reach the identical digest,
7
+ * so every field here, and the canonical form `reuseHash` builds from it,
8
+ * is part of the wire format, not an implementation detail.
9
+ */
10
+ export interface ReuseIdentitySpec {
11
+ readonly image: string;
12
+ /** Array of pairs, not a Map — mirrors `ContainerSpec.env`. Order does not affect the hash (canonicalized by sorting on key). */
13
+ readonly env: ReadonlyArray<readonly [string, string]>;
14
+ /** `undefined` and `[]` hash identically — both mean "no command override". */
15
+ readonly command: ReadonlyArray<string> | undefined;
16
+ /** Order does not affect the hash (canonicalized by sorting ascending). */
17
+ readonly exposedPorts: ReadonlyArray<number>;
18
+ readonly memoryLimitMb: number | undefined;
19
+ /** Order does not affect the hash (canonicalized by sorting on `guestPath`); content is hashed from `hostPath` at call time. */
20
+ readonly copies: ReadonlyArray<{
21
+ readonly guestPath: string;
22
+ readonly hostPath: string;
23
+ }>;
24
+ }
25
+ /**
26
+ * sha256 over the canonical JSON serialization of `spec`'s reuse-relevant
27
+ * fields, as a lowercase hex digest — identical across every rightsize
28
+ * language implementation for the same logical spec (see the feature spec's
29
+ * pinned contract vector, asserted against this function in `hash.test.ts`).
30
+ */
31
+ export declare function reuseHash(spec: ReuseIdentitySpec): Promise<string>;
32
+ /** `rz-reuse-<first 12 hex chars of hash>` — the reuse sandbox naming convention (addendum). */
33
+ export declare function reuseName(hash: string): string;
34
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/core/reuse/hash.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iIAAiI;IACjI,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,gIAAgI;IAChI,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3F;AA0DD;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxE;AAED,gGAAgG;AAChG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
@@ -0,0 +1,52 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fsp from "node:fs/promises";
3
+ function compareStrings(a, b) {
4
+ return a < b ? -1 : a > b ? 1 : 0;
5
+ }
6
+ async function hashFileContent(hostPath) {
7
+ const content = await fsp.readFile(hostPath);
8
+ return createHash("sha256").update(content).digest("hex");
9
+ }
10
+ /**
11
+ * Builds the canonical JSON serialization the spec pins: a fixed key order
12
+ * (`image, env, command, exposedPorts, memoryLimitMb, copies`), env as an
13
+ * object with keys inserted in sorted order, `command` normalized to `[]`
14
+ * when unset, ports sorted ascending, `memoryLimitMb` normalized to `null`
15
+ * when unset, and copies sorted by `guestPath` with each entry's content
16
+ * read from `hostPath` and hashed. No whitespace — plain `JSON.stringify`
17
+ * already produces none.
18
+ */
19
+ async function canonicalize(spec) {
20
+ const env = {};
21
+ for (const [key, value] of [...spec.env].sort(([a], [b]) => compareStrings(a, b))) {
22
+ env[key] = value;
23
+ }
24
+ const copies = await Promise.all(spec.copies.map(async (copy) => ({
25
+ guestPath: copy.guestPath,
26
+ sha256: await hashFileContent(copy.hostPath),
27
+ })));
28
+ copies.sort((a, b) => compareStrings(a.guestPath, b.guestPath));
29
+ return {
30
+ image: spec.image,
31
+ env,
32
+ command: spec.command ?? [],
33
+ exposedPorts: [...spec.exposedPorts].sort((a, b) => a - b),
34
+ memoryLimitMb: spec.memoryLimitMb ?? null,
35
+ copies,
36
+ };
37
+ }
38
+ /**
39
+ * sha256 over the canonical JSON serialization of `spec`'s reuse-relevant
40
+ * fields, as a lowercase hex digest — identical across every rightsize
41
+ * language implementation for the same logical spec (see the feature spec's
42
+ * pinned contract vector, asserted against this function in `hash.test.ts`).
43
+ */
44
+ export async function reuseHash(spec) {
45
+ const canonical = await canonicalize(spec);
46
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
47
+ }
48
+ /** `rz-reuse-<first 12 hex chars of hash>` — the reuse sandbox naming convention (addendum). */
49
+ export function reuseName(hash) {
50
+ return `rz-reuse-${hash.slice(0, 12)}`;
51
+ }
52
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../../src/core/reuse/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;AAsCxC,SAAS,cAAc,CAAC,CAAS,EAAE,CAAS;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,QAAgB;IAC7C,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC7C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,YAAY,CAAC,IAAuB;IACjD,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAC/B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;KAC7C,CAAC,CAAC,CACJ,CAAC;IACF,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhE,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG;QACH,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC3B,YAAY,EAAE,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;QAC1D,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI;QACzC,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAuB;IACrD,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,YAAY,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACzC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One `reuse/<hash>.json` record: written atomically after a reused
3
+ * container's first fresh boot passes its wait strategy, and read by every
4
+ * later `start()` (in this process or a later one) that computes the same
5
+ * `hash` to decide whether to adopt instead of create. Every field here is
6
+ * part of the cross-language contract — a Kotlin or Rust process must be
7
+ * able to parse exactly this shape.
8
+ */
9
+ export interface ReuseRegistryEntry {
10
+ readonly name: string;
11
+ readonly image: string;
12
+ /** `{"<guestPort>": <hostPort>, ...}` — JSON object keys are always strings, so the guest port is stringified. */
13
+ readonly ports: Record<string, number>;
14
+ readonly createdIso: string;
15
+ /** The backend name (e.g. `"microsandbox"`, `"docker"`) that created this sandbox — informational; adopt always re-verifies liveness through the CURRENTLY active backend regardless of this value. */
16
+ readonly backend: string;
17
+ }
18
+ /** `<cacheDir>/reuse` — the directory every reuse registry file lives under. */
19
+ export declare function reuseDir(cacheDir: string): string;
20
+ /** `reuse/<hash>.json`. */
21
+ export declare function reusePath(cacheDir: string, hash: string): string;
22
+ /** The three outcomes reading a registry file can settle to — corrupt is deliberately distinct from missing, since the adopt path best-effort-cleans up a stale SANDBOX only for the former (see the reuse spec's stale/corrupt-registry fallback). */
23
+ export type RegistryReadResult = {
24
+ readonly kind: "missing";
25
+ } | {
26
+ readonly kind: "corrupt";
27
+ } | {
28
+ readonly kind: "found";
29
+ readonly entry: ReuseRegistryEntry;
30
+ };
31
+ /**
32
+ * Reads and parses `reuse/<hash>.json`. `"missing"` means the file does not
33
+ * exist at all (the common "never created yet" case); `"corrupt"` means it
34
+ * exists but isn't a well-shaped `ReuseRegistryEntry` — malformed JSON or a
35
+ * missing/mistyped required field — distinct from `"missing"` because the
36
+ * adopt path's fallback behavior differs (see `hash.ts`'s sibling module doc
37
+ * and the reuse spec).
38
+ */
39
+ export declare function readRegistry(cacheDir: string, hash: string): Promise<RegistryReadResult>;
40
+ /**
41
+ * Atomically writes `reuse/<hash>.json` (tmp file + rename, same protocol as
42
+ * the reaping ledger's `writeRunRecord`) — called once, after a fresh reuse
43
+ * container's wait strategy has confirmed readiness. A concurrent reader
44
+ * only ever observes either the previous complete file or this one, never a
45
+ * partial write.
46
+ */
47
+ export declare function writeRegistryAtomic(cacheDir: string, hash: string, entry: ReuseRegistryEntry): Promise<void>;
48
+ /** Best-effort delete of `reuse/<hash>.json`. A file already gone is not an error. */
49
+ export declare function removeRegistry(cacheDir: string, hash: string): Promise<void>;
50
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/core/reuse/registry.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kHAAkH;IAClH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uMAAuM;IACvM,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,2BAA2B;AAC3B,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhE;AAsBD,uPAAuP;AACvP,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAEnE;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiB9F;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlH;AAED,sFAAsF;AACtF,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElF"}