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.
- package/README.md +28 -4
- package/dist/backend-docker/backend.d.ts +53 -1
- package/dist/backend-docker/backend.d.ts.map +1 -1
- package/dist/backend-docker/backend.js +106 -13
- package/dist/backend-docker/backend.js.map +1 -1
- package/dist/backend-docker/labels.d.ts +27 -5
- package/dist/backend-docker/labels.d.ts.map +1 -1
- package/dist/backend-docker/labels.js +33 -5
- package/dist/backend-docker/labels.js.map +1 -1
- package/dist/backend-msb/backend.d.ts +35 -6
- package/dist/backend-msb/backend.d.ts.map +1 -1
- package/dist/backend-msb/backend.js +69 -14
- package/dist/backend-msb/backend.js.map +1 -1
- package/dist/backend-msb/provider.d.ts.map +1 -1
- package/dist/backend-msb/provider.js +7 -11
- package/dist/backend-msb/provider.js.map +1 -1
- package/dist/backend-msb/provisioner.d.ts.map +1 -1
- package/dist/backend-msb/provisioner.js +1 -20
- package/dist/backend-msb/provisioner.js.map +1 -1
- package/dist/core/backend.d.ts +73 -0
- package/dist/core/backend.d.ts.map +1 -1
- package/dist/core/backends.d.ts +16 -2
- package/dist/core/backends.d.ts.map +1 -1
- package/dist/core/backends.js +39 -5
- package/dist/core/backends.js.map +1 -1
- package/dist/core/cache-dir.d.ts +17 -0
- package/dist/core/cache-dir.d.ts.map +1 -0
- package/dist/core/cache-dir.js +29 -0
- package/dist/core/cache-dir.js.map +1 -0
- package/dist/core/cleanup.d.ts +19 -4
- package/dist/core/cleanup.d.ts.map +1 -1
- package/dist/core/cleanup.js +14 -27
- package/dist/core/cleanup.js.map +1 -1
- package/dist/core/diagnostics.d.ts +33 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +76 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/errors.d.ts +41 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +56 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/generic-container.d.ts +107 -1
- package/dist/core/generic-container.d.ts.map +1 -1
- package/dist/core/generic-container.js +417 -3
- package/dist/core/generic-container.js.map +1 -1
- package/dist/core/model.d.ts +22 -0
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/network.d.ts.map +1 -1
- package/dist/core/network.js +2 -0
- package/dist/core/network.js.map +1 -1
- package/dist/core/reaper/env.d.ts +14 -0
- package/dist/core/reaper/env.d.ts.map +1 -0
- package/dist/core/reaper/env.js +22 -0
- package/dist/core/reaper/env.js.map +1 -0
- package/dist/core/reaper/init.d.ts +39 -0
- package/dist/core/reaper/init.d.ts.map +1 -0
- package/dist/core/reaper/init.js +140 -0
- package/dist/core/reaper/init.js.map +1 -0
- package/dist/core/reaper/ledger.d.ts +32 -0
- package/dist/core/reaper/ledger.d.ts.map +1 -0
- package/dist/core/reaper/ledger.js +160 -0
- package/dist/core/reaper/ledger.js.map +1 -0
- package/dist/core/reaper/process-liveness.d.ts +39 -0
- package/dist/core/reaper/process-liveness.d.ts.map +1 -0
- package/dist/core/reaper/process-liveness.js +117 -0
- package/dist/core/reaper/process-liveness.js.map +1 -0
- package/dist/core/reaper/run-record.d.ts +48 -0
- package/dist/core/reaper/run-record.d.ts.map +1 -0
- package/dist/core/reaper/run-record.js +58 -0
- package/dist/core/reaper/run-record.js.map +1 -0
- package/dist/core/reaper/sweep.d.ts +23 -0
- package/dist/core/reaper/sweep.d.ts.map +1 -0
- package/dist/core/reaper/sweep.js +76 -0
- package/dist/core/reaper/sweep.js.map +1 -0
- package/dist/core/reaper/watchdog.d.ts +100 -0
- package/dist/core/reaper/watchdog.d.ts.map +1 -0
- package/dist/core/reaper/watchdog.js +412 -0
- package/dist/core/reaper/watchdog.js.map +1 -0
- package/dist/core/reuse/env.d.ts +12 -0
- package/dist/core/reuse/env.d.ts.map +1 -0
- package/dist/core/reuse/env.js +15 -0
- package/dist/core/reuse/env.js.map +1 -0
- package/dist/core/reuse/hash.d.ts +34 -0
- package/dist/core/reuse/hash.d.ts.map +1 -0
- package/dist/core/reuse/hash.js +52 -0
- package/dist/core/reuse/hash.js.map +1 -0
- package/dist/core/reuse/registry.d.ts +50 -0
- package/dist/core/reuse/registry.d.ts.map +1 -0
- package/dist/core/reuse/registry.js +75 -0
- package/dist/core/reuse/registry.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/backend-msb/reaper.d.ts +0 -10
- package/dist/backend-msb/reaper.d.ts.map +0 -1
- package/dist/backend-msb/reaper.js +0 -73
- 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"}
|