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
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/core/network.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/core/network.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** `RIGHTSIZE_REAPER`'s three modes: both layers, sweep only, or fully disabled. */
2
+ export type ReaperMode = "on" | "sweep" | "off";
3
+ /**
4
+ * Parses `RIGHTSIZE_REAPER`. Unset or unrecognized values both mean `"on"` —
5
+ * the same "unknown values are treated as the safe default" convention the
6
+ * rest of this library's env vars follow, rather than failing a whole
7
+ * process over a typo'd opt-out.
8
+ */
9
+ export declare function reaperMode(env: Record<string, string | undefined>): ReaperMode;
10
+ /** `sweep` and `on` both run the init-time sweep; only `off` turns it off. */
11
+ export declare function sweepEnabled(mode: ReaperMode): boolean;
12
+ /** Only `on` (the default) spawns the per-run watchdog process. */
13
+ export declare function watchdogEnabled(mode: ReaperMode): boolean;
14
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/env.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,UAAU,CAM9E;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEtD;AAED,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEzD"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Parses `RIGHTSIZE_REAPER`. Unset or unrecognized values both mean `"on"` —
3
+ * the same "unknown values are treated as the safe default" convention the
4
+ * rest of this library's env vars follow, rather than failing a whole
5
+ * process over a typo'd opt-out.
6
+ */
7
+ export function reaperMode(env) {
8
+ const raw = env["RIGHTSIZE_REAPER"];
9
+ if (raw === "sweep" || raw === "off") {
10
+ return raw;
11
+ }
12
+ return "on";
13
+ }
14
+ /** `sweep` and `on` both run the init-time sweep; only `off` turns it off. */
15
+ export function sweepEnabled(mode) {
16
+ return mode !== "off";
17
+ }
18
+ /** Only `on` (the default) spawns the per-run watchdog process. */
19
+ export function watchdogEnabled(mode) {
20
+ return mode === "on";
21
+ }
22
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/core/reaper/env.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,GAAuC;IAChE,MAAM,GAAG,GAAG,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACpC,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,KAAK,EAAE,CAAC;QACrC,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,OAAO,IAAI,KAAK,KAAK,CAAC;AACxB,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,eAAe,CAAC,IAAgB;IAC9C,OAAO,IAAI,KAAK,IAAI,CAAC;AACvB,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { SandboxBackend } from "../backend.js";
2
+ import { type WatchdogHandle } from "./watchdog.js";
3
+ /**
4
+ * Runs the full reaper bring-up exactly once per process: writes this run's
5
+ * record, sweeps every OTHER dead run whose backend matches, and (unless
6
+ * disabled) spawns the watchdog. Best-effort end to end — any failure here
7
+ * is swallowed so a broken cache dir or a sweep hiccup never prevents the
8
+ * backend from being usable; `active` simply never gets set, and every
9
+ * ledger call for the rest of this process's life is a no-op.
10
+ */
11
+ export declare function ensureReaperInitialized(backend: SandboxBackend): Promise<void>;
12
+ /** Appends a sandbox name to this process's ledger — a no-op unless the reaper has actually initialized (or is disabled). */
13
+ export declare function trackSandbox(name: string): Promise<void>;
14
+ /** Removes a sandbox name from this process's ledger, pruning the run's ledger files if nothing is left tracked. */
15
+ export declare function untrackSandbox(name: string): Promise<void>;
16
+ /** Appends a network id to this process's ledger. */
17
+ export declare function trackNetwork(networkId: string): Promise<void>;
18
+ /** Removes a network id from this process's ledger, pruning the run's ledger files if nothing is left tracked. */
19
+ export declare function untrackNetwork(networkId: string): Promise<void>;
20
+ /**
21
+ * The run-record cleanup rule's other trigger (see `ledger.ts`'s
22
+ * `pruneIfEmpty` for the first one): called once, after the active
23
+ * backend's own `close()` has finished its own-run cleanup (`Backends`'
24
+ * `beforeExit` hook awaits `close()`, then calls this). `close()` already
25
+ * removed every sandbox this run still had running (via its own run-id-
26
+ * label/`startedNames` scan), so whatever `.sandboxes`/`.networks` still
27
+ * list at this point is stale — deleted unconditionally rather than only
28
+ * when empty, since a later container start recreates the files from
29
+ * scratch anyway. No-op if the reaper never initialized (or already ran
30
+ * this) for this process.
31
+ */
32
+ export declare function notifyBackendClosed(): Promise<void>;
33
+ /** Test seam: whether the reaper has actually initialized in this process. Never call from library code. */
34
+ export declare function _isActiveForTests(): boolean;
35
+ /** Test seam: the held watchdog handle, if one was spawned — so a test can close its pipe rather than leaking a blocked child process. Never call from library code. */
36
+ export declare function _heldWatchdogForTests(): WatchdogHandle | undefined;
37
+ /** Test seam: forces the next `ensureReaperInitialized` call to redo the full bring-up. Never call from library code. */
38
+ export declare function _resetReaperForTests(): void;
39
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/init.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAapD,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AA8EnE;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9E;AAED,6HAA6H;AAC7H,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9D;AAED,oHAAoH;AACpH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhE;AAED,qDAAqD;AACrD,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnE;AAED,kHAAkH;AAClH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOzD;AAED,4GAA4G;AAC5G,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,wKAAwK;AACxK,wBAAgB,qBAAqB,IAAI,cAAc,GAAG,SAAS,CAElE;AAED,yHAAyH;AACzH,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C"}
@@ -0,0 +1,140 @@
1
+ import { cacheDir } from "../cache-dir.js";
2
+ import { RunId } from "../run-id.js";
3
+ import { writeRunRecord, appendSandboxName, removeSandboxName, appendNetworkId, removeNetworkId, deleteRunRecordFiles, } from "./ledger.js";
4
+ import { recordPath, sandboxesPath, networksPath } from "./run-record.js";
5
+ import { realProcessTimeSource, THIS_PROCESS_STARTED_ISO } from "./process-liveness.js";
6
+ import { sweepOnce } from "./sweep.js";
7
+ import { reaperMode, sweepEnabled, watchdogEnabled } from "./env.js";
8
+ import { spawnWatchdog } from "./watchdog.js";
9
+ let active;
10
+ let initPromise;
11
+ let heldWatchdog;
12
+ function backendKindOf(backend) {
13
+ // The only two backends this library ships. A third backend module would
14
+ // need a third LedgerBackendKind value and an update here.
15
+ return backend.name === "microsandbox" ? "msb" : "docker";
16
+ }
17
+ async function doInit(backend) {
18
+ const mode = reaperMode(process.env);
19
+ if (!sweepEnabled(mode)) {
20
+ // "off": no run record, no ledger tracking, no sweep, no watchdog —
21
+ // this process's containers are simply never listed anywhere for a
22
+ // later sweep to find.
23
+ return;
24
+ }
25
+ const dir = cacheDir();
26
+ const runId = RunId.value;
27
+ const backendKind = backendKindOf(backend);
28
+ const killCommand = await backend.reaperKillCommand();
29
+ // msb's `remove` prefix is always `[msbPath, "rm"]` (see
30
+ // MsbCliBackend.reaperKillCommand) — reusing it here avoids a second SPI
31
+ // method whose only job would be exposing the same path.
32
+ const msbPath = backendKind === "msb" ? killCommand.remove[0] : undefined;
33
+ const record = msbPath === undefined
34
+ ? { pid: process.pid, startedIso: THIS_PROCESS_STARTED_ISO, backend: backendKind }
35
+ : { pid: process.pid, startedIso: THIS_PROCESS_STARTED_ISO, backend: backendKind, msbPath };
36
+ // Written BEFORE this process's first sandbox is created — the caller
37
+ // (Backends.reaperReady(), awaited by GenericContainer.start() before its
38
+ // first backend.create()) guarantees that ordering.
39
+ await writeRunRecord(dir, runId, record);
40
+ active = { cacheDir: dir, runId };
41
+ await sweepOnce({
42
+ cacheDir: dir,
43
+ thisRunId: runId,
44
+ backendKind,
45
+ removeByName: (name) => backend.removeByName(name),
46
+ removeNetwork: (networkId) => backend.removeNetwork(networkId),
47
+ timeSource: realProcessTimeSource,
48
+ });
49
+ if (watchdogEnabled(mode)) {
50
+ heldWatchdog = await spawnWatchdog({
51
+ cacheDir: dir,
52
+ sandboxesPath: sandboxesPath(dir, runId),
53
+ networksPath: networksPath(dir, runId),
54
+ recordPath: recordPath(dir, runId),
55
+ killCommand,
56
+ // The Windows script polls for this pid instead of blocking on
57
+ // stdin EOF; see watchdog.ts's windowsScriptContent doc.
58
+ ownerPid: process.pid,
59
+ });
60
+ }
61
+ }
62
+ /**
63
+ * Runs the full reaper bring-up exactly once per process: writes this run's
64
+ * record, sweeps every OTHER dead run whose backend matches, and (unless
65
+ * disabled) spawns the watchdog. Best-effort end to end — any failure here
66
+ * is swallowed so a broken cache dir or a sweep hiccup never prevents the
67
+ * backend from being usable; `active` simply never gets set, and every
68
+ * ledger call for the rest of this process's life is a no-op.
69
+ */
70
+ export function ensureReaperInitialized(backend) {
71
+ if (initPromise === undefined) {
72
+ initPromise = doInit(backend).catch(() => {
73
+ // Swallowed — see the doc above.
74
+ });
75
+ }
76
+ return initPromise;
77
+ }
78
+ /** Appends a sandbox name to this process's ledger — a no-op unless the reaper has actually initialized (or is disabled). */
79
+ export async function trackSandbox(name) {
80
+ if (active === undefined) {
81
+ return;
82
+ }
83
+ await appendSandboxName(active.cacheDir, active.runId, name);
84
+ }
85
+ /** Removes a sandbox name from this process's ledger, pruning the run's ledger files if nothing is left tracked. */
86
+ export async function untrackSandbox(name) {
87
+ if (active === undefined) {
88
+ return;
89
+ }
90
+ await removeSandboxName(active.cacheDir, active.runId, name);
91
+ }
92
+ /** Appends a network id to this process's ledger. */
93
+ export async function trackNetwork(networkId) {
94
+ if (active === undefined) {
95
+ return;
96
+ }
97
+ await appendNetworkId(active.cacheDir, active.runId, networkId);
98
+ }
99
+ /** Removes a network id from this process's ledger, pruning the run's ledger files if nothing is left tracked. */
100
+ export async function untrackNetwork(networkId) {
101
+ if (active === undefined) {
102
+ return;
103
+ }
104
+ await removeNetworkId(active.cacheDir, active.runId, networkId);
105
+ }
106
+ /**
107
+ * The run-record cleanup rule's other trigger (see `ledger.ts`'s
108
+ * `pruneIfEmpty` for the first one): called once, after the active
109
+ * backend's own `close()` has finished its own-run cleanup (`Backends`'
110
+ * `beforeExit` hook awaits `close()`, then calls this). `close()` already
111
+ * removed every sandbox this run still had running (via its own run-id-
112
+ * label/`startedNames` scan), so whatever `.sandboxes`/`.networks` still
113
+ * list at this point is stale — deleted unconditionally rather than only
114
+ * when empty, since a later container start recreates the files from
115
+ * scratch anyway. No-op if the reaper never initialized (or already ran
116
+ * this) for this process.
117
+ */
118
+ export async function notifyBackendClosed() {
119
+ if (active === undefined) {
120
+ return;
121
+ }
122
+ const { cacheDir, runId } = active;
123
+ active = undefined;
124
+ await deleteRunRecordFiles(cacheDir, runId);
125
+ }
126
+ /** Test seam: whether the reaper has actually initialized in this process. Never call from library code. */
127
+ export function _isActiveForTests() {
128
+ return active !== undefined;
129
+ }
130
+ /** Test seam: the held watchdog handle, if one was spawned — so a test can close its pipe rather than leaking a blocked child process. Never call from library code. */
131
+ export function _heldWatchdogForTests() {
132
+ return heldWatchdog;
133
+ }
134
+ /** Test seam: forces the next `ensureReaperInitialized` call to redo the full bring-up. Never call from library code. */
135
+ export function _resetReaperForTests() {
136
+ active = undefined;
137
+ initPromise = undefined;
138
+ heldWatchdog = undefined;
139
+ }
140
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/core/reaper/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAA0C,MAAM,iBAAiB,CAAC;AAClH,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,EAAE,aAAa,EAAuB,MAAM,eAAe,CAAC;AAgBnE,IAAI,MAAgC,CAAC;AACrC,IAAI,WAAsC,CAAC;AAC3C,IAAI,YAAwC,CAAC;AAE7C,SAAS,aAAa,CAAC,OAAuB;IAC5C,yEAAyE;IACzE,2DAA2D;IAC3D,OAAO,OAAO,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC5D,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,OAAuB;IAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,oEAAoE;QACpE,mEAAmE;QACnE,uBAAuB;QACvB,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1B,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,iBAAiB,EAAE,CAAC;IACtD,yDAAyD;IACzD,yEAAyE;IACzE,yDAAyD;IACzD,MAAM,OAAO,GAAG,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1E,MAAM,MAAM,GACV,OAAO,KAAK,SAAS;QACnB,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE,wBAAwB,EAAE,OAAO,EAAE,WAAW,EAAE;QAClF,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE,wBAAwB,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAEhG,sEAAsE;IACtE,0EAA0E;IAC1E,oDAAoD;IACpD,MAAM,cAAc,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAElC,MAAM,SAAS,CAAC;QACd,QAAQ,EAAE,GAAG;QACb,SAAS,EAAE,KAAK;QAChB,WAAW;QACX,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC;QAClD,aAAa,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC;QAC9D,UAAU,EAAE,qBAAqB;KAClC,CAAC,CAAC;IAEH,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,YAAY,GAAG,MAAM,aAAa,CAAC;YACjC,QAAQ,EAAE,GAAG;YACb,aAAa,EAAE,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC;YACxC,YAAY,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC;YACtC,UAAU,EAAE,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC;YAClC,WAAW;YACX,+DAA+D;YAC/D,yDAAyD;YACzD,QAAQ,EAAE,OAAO,CAAC,GAAG;SACtB,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAuB;IAC7D,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACvC,iCAAiC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,6HAA6H;AAC7H,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY;IAC7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,iBAAiB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,oHAAoH;AACpH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,iBAAiB,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,SAAiB;IAClD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAClE,CAAC;AAED,kHAAkH;AAClH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB;IACpD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;IACT,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IACnC,MAAM,GAAG,SAAS,CAAC;IACnB,MAAM,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,iBAAiB;IAC/B,OAAO,MAAM,KAAK,SAAS,CAAC;AAC9B,CAAC;AAED,wKAAwK;AACxK,MAAM,UAAU,qBAAqB;IACnC,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,yHAAyH;AACzH,MAAM,UAAU,oBAAoB;IAClC,MAAM,GAAG,SAAS,CAAC;IACnB,WAAW,GAAG,SAAS,CAAC;IACxB,YAAY,GAAG,SAAS,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { parseRunRecord, type RunRecord } from "./run-record.js";
2
+ /** Best-effort delete of all three of a run's ledger files. Never throws — a file already gone is not an error. */
3
+ export declare function deleteRunRecordFiles(cacheDir: string, runId: string): Promise<void>;
4
+ /** Atomically writes `runs/<run-id>.json` (tmp file + rename) — called once, before this run's first sandbox is created. */
5
+ export declare function writeRunRecord(cacheDir: string, runId: string, record: RunRecord): Promise<void>;
6
+ /** Appends a sandbox name to `runs/<run-id>.sandboxes` — called BEFORE the backend's `create()` call. */
7
+ export declare function appendSandboxName(cacheDir: string, runId: string, name: string): Promise<void>;
8
+ /** Removes a sandbox name from `runs/<run-id>.sandboxes` — called AFTER a successful stop/remove; prunes the run's ledger files if this was the last thing tracked. */
9
+ export declare function removeSandboxName(cacheDir: string, runId: string, name: string): Promise<void>;
10
+ /** Appends a network id to `runs/<run-id>.networks` — same protocol as `appendSandboxName`. */
11
+ export declare function appendNetworkId(cacheDir: string, runId: string, networkId: string): Promise<void>;
12
+ /** Removes a network id from `runs/<run-id>.networks` — same protocol as `removeSandboxName`. */
13
+ export declare function removeNetworkId(cacheDir: string, runId: string, networkId: string): Promise<void>;
14
+ /**
15
+ * Reads every sandbox name currently tracked for a run. Serialized behind the
16
+ * same lock as the writers: writeLines truncates before it writes, so an
17
+ * unserialized read racing a rewrite could observe a torn (empty) file. The
18
+ * cross-process readers (a sweep judging a DEAD run, the watchdog after its
19
+ * owner died) need no lock — their run's writer no longer exists.
20
+ */
21
+ export declare function readSandboxNames(cacheDir: string, runId: string): Promise<string[]>;
22
+ /** Reads every network id currently tracked for a run — used by the sweep to know what to reap. */
23
+ export declare function readNetworkIds(cacheDir: string, runId: string): Promise<string[]>;
24
+ /** Every other run's `<run-id>.json` filename (without extension) currently under `runs/` — the sweep's iteration set. Missing `runs/` directory yields an empty list. */
25
+ export declare function listRunIds(cacheDir: string): Promise<string[]>;
26
+ /** Reads and parses one run's record, plus the file's mtime (used to age an unparseable record). `undefined` body means the file vanished (a concurrent sweep or the run's own clean shutdown already removed it). */
27
+ export declare function readRunRecordRaw(cacheDir: string, runId: string): Promise<{
28
+ text: string;
29
+ mtimeMs: number;
30
+ } | undefined>;
31
+ export { parseRunRecord };
32
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/ledger.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,cAAc,EAEd,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAqDzB,mHAAmH;AACnH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzF;AA4BD,4HAA4H;AAC5H,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAStG;AAED,yGAAyG;AACzG,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMpG;AAED,uKAAuK;AACvK,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOpG;AAED,+FAA+F;AAC/F,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvG;AAED,iGAAiG;AACjG,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvG;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEzF;AAED,mGAAmG;AACnG,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEvF;AAED,0KAA0K;AAC1K,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQpE;AAED,sNAAsN;AACtN,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC,CAQxD;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,160 @@
1
+ import * as fsp from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { recordPath, runsDir, sandboxesPath, networksPath, parseRunRecord, serializeRunRecord, } from "./run-record.js";
4
+ /**
5
+ * Every write this module makes is guarded by a single process-wide chain —
6
+ * this process only ever owns its own ledger files, so one chain (not one
7
+ * per file) is enough to make append/remove/prune read-modify-write safe
8
+ * against concurrent callers within THIS process. Cross-process safety is a
9
+ * non-goal by design: the feature spec only requires "atomic enough for
10
+ * single-process use," since two different processes never share a run id.
11
+ */
12
+ let chain = Promise.resolve();
13
+ function withLock(fn) {
14
+ const result = chain.then(fn, fn);
15
+ chain = result.then(() => undefined, () => undefined);
16
+ return result;
17
+ }
18
+ async function readLines(filePath) {
19
+ let text;
20
+ try {
21
+ text = await fsp.readFile(filePath, "utf8");
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ return text.split("\n").filter((line) => line.length > 0);
27
+ }
28
+ // A plain truncating write is safe here ONLY because every reader of a live
29
+ // run's file goes through the same lock as its writers (see readSandboxNames/
30
+ // readNetworkIds below); the sole lock-free readers — the watchdog process and
31
+ // another process's sweep — read a run's files strictly after its owning
32
+ // process is dead, when no writer can exist. A tmp+rename swap is deliberately
33
+ // NOT used: on Windows, renaming over a file a reader has open fails EPERM,
34
+ // which would turn a benign read race into a lost ledger update.
35
+ async function writeLines(filePath, lines) {
36
+ if (lines.length === 0) {
37
+ await fsp.unlink(filePath).catch(() => { });
38
+ return;
39
+ }
40
+ await fsp.writeFile(filePath, lines.join("\n") + "\n");
41
+ }
42
+ function removeFirstOccurrence(lines, value) {
43
+ const idx = lines.indexOf(value);
44
+ if (idx === -1) {
45
+ return [...lines];
46
+ }
47
+ return [...lines.slice(0, idx), ...lines.slice(idx + 1)];
48
+ }
49
+ /** Best-effort delete of all three of a run's ledger files. Never throws — a file already gone is not an error. */
50
+ export async function deleteRunRecordFiles(cacheDir, runId) {
51
+ await Promise.all([recordPath(cacheDir, runId), sandboxesPath(cacheDir, runId), networksPath(cacheDir, runId)].map((p) => fsp.unlink(p).catch(() => { })));
52
+ }
53
+ async function isEmptyOrMissing(filePath) {
54
+ try {
55
+ const text = await fsp.readFile(filePath, "utf8");
56
+ return text.trim().length === 0;
57
+ }
58
+ catch {
59
+ return true;
60
+ }
61
+ }
62
+ /**
63
+ * The run-record cleanup rule: once BOTH `.sandboxes` and `.networks` are
64
+ * empty (or missing), this run has nothing left in flight, so its ledger
65
+ * files are deleted — a later container start recreates them from scratch.
66
+ * Called after every removal, never after an append (an append can only
67
+ * grow a file, never trigger this).
68
+ */
69
+ async function pruneIfEmpty(cacheDir, runId) {
70
+ const [sandboxesEmpty, networksEmpty] = await Promise.all([
71
+ isEmptyOrMissing(sandboxesPath(cacheDir, runId)),
72
+ isEmptyOrMissing(networksPath(cacheDir, runId)),
73
+ ]);
74
+ if (sandboxesEmpty && networksEmpty) {
75
+ await deleteRunRecordFiles(cacheDir, runId);
76
+ }
77
+ }
78
+ /** Atomically writes `runs/<run-id>.json` (tmp file + rename) — called once, before this run's first sandbox is created. */
79
+ export async function writeRunRecord(cacheDir, runId, record) {
80
+ return withLock(async () => {
81
+ const dir = runsDir(cacheDir);
82
+ await fsp.mkdir(dir, { recursive: true });
83
+ const target = recordPath(cacheDir, runId);
84
+ const tmp = path.join(dir, `.${runId}.json.tmp-${process.pid}-${Date.now()}`);
85
+ await fsp.writeFile(tmp, serializeRunRecord(record));
86
+ await fsp.rename(tmp, target);
87
+ });
88
+ }
89
+ /** Appends a sandbox name to `runs/<run-id>.sandboxes` — called BEFORE the backend's `create()` call. */
90
+ export async function appendSandboxName(cacheDir, runId, name) {
91
+ return withLock(async () => {
92
+ const filePath = sandboxesPath(cacheDir, runId);
93
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
94
+ await fsp.appendFile(filePath, name + "\n");
95
+ });
96
+ }
97
+ /** Removes a sandbox name from `runs/<run-id>.sandboxes` — called AFTER a successful stop/remove; prunes the run's ledger files if this was the last thing tracked. */
98
+ export async function removeSandboxName(cacheDir, runId, name) {
99
+ return withLock(async () => {
100
+ const filePath = sandboxesPath(cacheDir, runId);
101
+ const lines = removeFirstOccurrence(await readLines(filePath), name);
102
+ await writeLines(filePath, lines);
103
+ await pruneIfEmpty(cacheDir, runId);
104
+ });
105
+ }
106
+ /** Appends a network id to `runs/<run-id>.networks` — same protocol as `appendSandboxName`. */
107
+ export async function appendNetworkId(cacheDir, runId, networkId) {
108
+ return withLock(async () => {
109
+ const filePath = networksPath(cacheDir, runId);
110
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
111
+ await fsp.appendFile(filePath, networkId + "\n");
112
+ });
113
+ }
114
+ /** Removes a network id from `runs/<run-id>.networks` — same protocol as `removeSandboxName`. */
115
+ export async function removeNetworkId(cacheDir, runId, networkId) {
116
+ return withLock(async () => {
117
+ const filePath = networksPath(cacheDir, runId);
118
+ const lines = removeFirstOccurrence(await readLines(filePath), networkId);
119
+ await writeLines(filePath, lines);
120
+ await pruneIfEmpty(cacheDir, runId);
121
+ });
122
+ }
123
+ /**
124
+ * Reads every sandbox name currently tracked for a run. Serialized behind the
125
+ * same lock as the writers: writeLines truncates before it writes, so an
126
+ * unserialized read racing a rewrite could observe a torn (empty) file. The
127
+ * cross-process readers (a sweep judging a DEAD run, the watchdog after its
128
+ * owner died) need no lock — their run's writer no longer exists.
129
+ */
130
+ export async function readSandboxNames(cacheDir, runId) {
131
+ return withLock(() => readLines(sandboxesPath(cacheDir, runId)));
132
+ }
133
+ /** Reads every network id currently tracked for a run — used by the sweep to know what to reap. */
134
+ export async function readNetworkIds(cacheDir, runId) {
135
+ return withLock(() => readLines(networksPath(cacheDir, runId)));
136
+ }
137
+ /** Every other run's `<run-id>.json` filename (without extension) currently under `runs/` — the sweep's iteration set. Missing `runs/` directory yields an empty list. */
138
+ export async function listRunIds(cacheDir) {
139
+ let entries;
140
+ try {
141
+ entries = await fsp.readdir(runsDir(cacheDir));
142
+ }
143
+ catch {
144
+ return [];
145
+ }
146
+ return entries.filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length));
147
+ }
148
+ /** Reads and parses one run's record, plus the file's mtime (used to age an unparseable record). `undefined` body means the file vanished (a concurrent sweep or the run's own clean shutdown already removed it). */
149
+ export async function readRunRecordRaw(cacheDir, runId) {
150
+ const filePath = recordPath(cacheDir, runId);
151
+ try {
152
+ const [text, stat] = await Promise.all([fsp.readFile(filePath, "utf8"), fsp.stat(filePath)]);
153
+ return { text, mtimeMs: stat.mtimeMs };
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ export { parseRunRecord };
160
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../../src/core/reaper/ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The two operations a sweep needs about ANY pid on this machine — not
3
+ * necessarily this process's own. Abstracted behind an interface so unit
4
+ * tests can fabricate arbitrary (pid, start-time) combinations without
5
+ * shelling out to the real OS, matching the feature spec's testing
6
+ * requirement ("same-pid-different-start-time counts as dead" — a scenario
7
+ * that's essentially impossible to provoke against a real process on
8
+ * demand).
9
+ */
10
+ export interface ProcessTimeSource {
11
+ /** Whether a process with this pid currently exists. */
12
+ isAlive(pid: number): boolean;
13
+ /** That process's start time, ISO-8601, or `undefined` if it can't be determined (including "no longer exists"). */
14
+ startedIso(pid: number): Promise<string | undefined>;
15
+ }
16
+ /** `process.kill(pid, 0)`: no signal sent, just existence-checked. EPERM means "exists but not ours" — still alive. */
17
+ export declare function isProcessAlive(pid: number): boolean;
18
+ /** Test-only access to the pure `ps -o etime=` duration parser, exercised against fabricated strings without shelling out. */
19
+ export declare function _parseEtimeMsForTests(text: string): number | undefined;
20
+ /** The real, OS-backed `ProcessTimeSource` — production sweeps and the watchdog's liveness checks use this. */
21
+ export declare const realProcessTimeSource: ProcessTimeSource;
22
+ /** Cross-process liveness protocol: a run is ALIVE iff its pid exists AND its actual start time matches the recorded one within 2 seconds — this window is what defeats PID reuse. */
23
+ export declare const LIVENESS_TOLERANCE_MS = 2000;
24
+ /**
25
+ * Judges a `(pid, startedIso)` pair against a time source. `undefined` from
26
+ * `startedIso()` (pid vanished between the alive-check and the start-time
27
+ * probe, or the platform command failed) is treated as dead — a run this
28
+ * module can't positively confirm alive is reaped, matching the reaper's
29
+ * bias toward cleaning up rather than leaking.
30
+ */
31
+ export declare function isRecordAlive(source: ProcessTimeSource, pid: number, recordedStartedIso: string): Promise<boolean>;
32
+ /**
33
+ * This process's own start time, ISO-8601 — computed once at module load
34
+ * from `process.uptime()` (Node has no direct "process start instant" API).
35
+ * Close enough to true process start for the ±2s liveness tolerance even if
36
+ * this module is first imported a little into the process's life.
37
+ */
38
+ export declare const THIS_PROCESS_STARTED_ISO: string;
39
+ //# sourceMappingURL=process-liveness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-liveness.d.ts","sourceRoot":"","sources":["../../../src/core/reaper/process-liveness.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,oHAAoH;IACpH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACtD;AAED,uHAAuH;AACvH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD;AAmDD,8HAA8H;AAC9H,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEtE;AA6BD,+GAA+G;AAC/G,eAAO,MAAM,qBAAqB,EAAE,iBAInC,CAAC;AAEF,sLAAsL;AACtL,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,iBAAiB,EACzB,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAAiF,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { spawn } from "node:child_process";
2
+ /** `process.kill(pid, 0)`: no signal sent, just existence-checked. EPERM means "exists but not ours" — still alive. */
3
+ export function isProcessAlive(pid) {
4
+ try {
5
+ process.kill(pid, 0);
6
+ return true;
7
+ }
8
+ catch (err) {
9
+ return err.code === "EPERM";
10
+ }
11
+ }
12
+ function collectStdout(cmd, args) {
13
+ return new Promise((resolveOut) => {
14
+ let out = "";
15
+ let settled = false;
16
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
17
+ child.stdout.setEncoding("utf8");
18
+ child.stdout.on("data", (chunk) => {
19
+ out += chunk;
20
+ });
21
+ child.once("error", () => {
22
+ if (!settled) {
23
+ settled = true;
24
+ resolveOut(undefined);
25
+ }
26
+ });
27
+ child.once("close", (code) => {
28
+ if (settled) {
29
+ return;
30
+ }
31
+ settled = true;
32
+ resolveOut(code === 0 ? out : undefined);
33
+ });
34
+ });
35
+ }
36
+ const ETIME_PATTERN = /^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/;
37
+ /**
38
+ * `ps -p <pid> -o etime=` prints a DURATION — `[[dd-]hh:]mm:ss` — not a wall-
39
+ * clock timestamp, so unlike `lstart` (a bare, timezone-less string that
40
+ * different `Date` parsers and even different processes on the SAME host
41
+ * can disagree about — confirmed empirically: a `bun test` worker's own
42
+ * `Intl`/`Date` machinery reported its local timezone as UTC while a `ps`
43
+ * child it spawned still printed the OS's real local time, a multi-hour
44
+ * mismatch with no env var involved to explain it) there is no timezone to
45
+ * get wrong here at all: `Date.now() - etimeMs` is unambiguous everywhere.
46
+ */
47
+ function parseEtimeMs(text) {
48
+ const trimmed = text.trim();
49
+ const m = ETIME_PATTERN.exec(trimmed);
50
+ if (m === null) {
51
+ return undefined;
52
+ }
53
+ const [, days, hours, minutes, seconds] = m;
54
+ const totalSeconds = (Number(days ?? 0) * 24 * 60 + Number(hours ?? 0) * 60 + Number(minutes)) * 60 + Number(seconds);
55
+ return Number.isFinite(totalSeconds) ? totalSeconds * 1000 : undefined;
56
+ }
57
+ /** Test-only access to the pure `ps -o etime=` duration parser, exercised against fabricated strings without shelling out. */
58
+ export function _parseEtimeMsForTests(text) {
59
+ return parseEtimeMs(text);
60
+ }
61
+ async function posixStartedIso(pid) {
62
+ const before = Date.now();
63
+ const out = await collectStdout("ps", ["-p", String(pid), "-o", "etime="]);
64
+ if (out === undefined) {
65
+ return undefined;
66
+ }
67
+ const etimeMs = parseEtimeMs(out);
68
+ return etimeMs === undefined ? undefined : new Date(before - etimeMs).toISOString();
69
+ }
70
+ /** PowerShell prints the process's start time already converted to UTC, in round-trippable ISO-8601 — no ad hoc parsing needed. */
71
+ async function windowsStartedIso(pid) {
72
+ const script = `$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue; ` +
73
+ `if ($p) { $p.StartTime.ToUniversalTime().ToString('o') }`;
74
+ const out = await collectStdout("powershell", ["-NoProfile", "-NonInteractive", "-Command", script]);
75
+ if (out === undefined) {
76
+ return undefined;
77
+ }
78
+ const trimmed = out.trim();
79
+ if (trimmed.length === 0) {
80
+ return undefined;
81
+ }
82
+ const parsed = new Date(trimmed);
83
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
84
+ }
85
+ /** The real, OS-backed `ProcessTimeSource` — production sweeps and the watchdog's liveness checks use this. */
86
+ export const realProcessTimeSource = {
87
+ isAlive: isProcessAlive,
88
+ startedIso: (pid) => process.platform === "win32" ? windowsStartedIso(pid) : posixStartedIso(pid),
89
+ };
90
+ /** Cross-process liveness protocol: a run is ALIVE iff its pid exists AND its actual start time matches the recorded one within 2 seconds — this window is what defeats PID reuse. */
91
+ export const LIVENESS_TOLERANCE_MS = 2000;
92
+ /**
93
+ * Judges a `(pid, startedIso)` pair against a time source. `undefined` from
94
+ * `startedIso()` (pid vanished between the alive-check and the start-time
95
+ * probe, or the platform command failed) is treated as dead — a run this
96
+ * module can't positively confirm alive is reaped, matching the reaper's
97
+ * bias toward cleaning up rather than leaking.
98
+ */
99
+ export async function isRecordAlive(source, pid, recordedStartedIso) {
100
+ if (!source.isAlive(pid)) {
101
+ return false;
102
+ }
103
+ const actual = await source.startedIso(pid);
104
+ if (actual === undefined) {
105
+ return false;
106
+ }
107
+ const diff = Math.abs(Date.parse(actual) - Date.parse(recordedStartedIso));
108
+ return Number.isFinite(diff) && diff <= LIVENESS_TOLERANCE_MS;
109
+ }
110
+ /**
111
+ * This process's own start time, ISO-8601 — computed once at module load
112
+ * from `process.uptime()` (Node has no direct "process start instant" API).
113
+ * Close enough to true process start for the ±2s liveness tolerance even if
114
+ * this module is first imported a little into the process's life.
115
+ */
116
+ export const THIS_PROCESS_STARTED_ISO = new Date(Date.now() - Math.round(process.uptime() * 1000)).toISOString();
117
+ //# sourceMappingURL=process-liveness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-liveness.js","sourceRoot":"","sources":["../../../src/core/reaper/process-liveness.ts"],"names":[],"mappings":"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"}