@a11ign/screenreader-fleet 0.0.0-reserved.0 → 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/LICENSE +661 -0
- package/README.md +94 -2
- package/dist/capture-client.d.mts +49 -0
- package/dist/capture-client.d.mts.map +1 -0
- package/dist/capture-client.mjs +352 -0
- package/dist/capture-client.mjs.map +1 -0
- package/dist/check-worker-code.d.mts +34 -0
- package/dist/check-worker-code.d.mts.map +1 -0
- package/dist/check-worker-code.mjs +142 -0
- package/dist/check-worker-code.mjs.map +1 -0
- package/dist/cli-flags.d.mts +71 -0
- package/dist/cli-flags.d.mts.map +1 -0
- package/dist/cli-flags.mjs +207 -0
- package/dist/cli-flags.mjs.map +1 -0
- package/dist/code-drift.d.mts +140 -0
- package/dist/code-drift.d.mts.map +1 -0
- package/dist/code-drift.mjs +284 -0
- package/dist/code-drift.mjs.map +1 -0
- package/dist/command-line-census.d.mts +33 -0
- package/dist/command-line-census.d.mts.map +1 -0
- package/dist/command-line-census.mjs +96 -0
- package/dist/command-line-census.mjs.map +1 -0
- package/dist/compare-workers.d.mts +3 -0
- package/dist/compare-workers.d.mts.map +1 -0
- package/dist/compare-workers.mjs +332 -0
- package/dist/compare-workers.mjs.map +1 -0
- package/dist/control-plane-isolation.d.mts +45 -0
- package/dist/control-plane-isolation.d.mts.map +1 -0
- package/dist/control-plane-isolation.mjs +67 -0
- package/dist/control-plane-isolation.mjs.map +1 -0
- package/dist/deploy-worker.d.mts +3 -0
- package/dist/deploy-worker.d.mts.map +1 -0
- package/dist/deploy-worker.mjs +333 -0
- package/dist/deploy-worker.mjs.map +1 -0
- package/dist/doctor.d.mts +216 -0
- package/dist/doctor.d.mts.map +1 -0
- package/dist/doctor.mjs +962 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/fleet-consistency.d.mts +235 -0
- package/dist/fleet-consistency.d.mts.map +1 -0
- package/dist/fleet-consistency.mjs +436 -0
- package/dist/fleet-consistency.mjs.map +1 -0
- package/dist/fleet-env.d.mts +228 -0
- package/dist/fleet-env.d.mts.map +1 -0
- package/dist/fleet-env.mjs +509 -0
- package/dist/fleet-env.mjs.map +1 -0
- package/dist/fleet-scripts.d.mts +11 -0
- package/dist/fleet-scripts.d.mts.map +1 -0
- package/dist/fleet-scripts.mjs +41 -0
- package/dist/fleet-scripts.mjs.map +1 -0
- package/dist/git-safe-env.d.mts +10 -0
- package/dist/git-safe-env.d.mts.map +1 -0
- package/dist/git-safe-env.mjs +44 -0
- package/dist/git-safe-env.mjs.map +1 -0
- package/dist/guest-run.d.mts +26 -0
- package/dist/guest-run.d.mts.map +1 -0
- package/dist/guest-run.mjs +164 -0
- package/dist/guest-run.mjs.map +1 -0
- package/dist/host-address.d.mts +33 -0
- package/dist/host-address.d.mts.map +1 -0
- package/dist/host-address.mjs +105 -0
- package/dist/host-address.mjs.map +1 -0
- package/dist/host-capacity.d.mts +64 -0
- package/dist/host-capacity.d.mts.map +1 -0
- package/dist/host-capacity.mjs +152 -0
- package/dist/host-capacity.mjs.map +1 -0
- package/dist/host-metrics.d.mts +116 -0
- package/dist/host-metrics.d.mts.map +1 -0
- package/dist/host-metrics.mjs +201 -0
- package/dist/host-metrics.mjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-vm.d.ts +125 -0
- package/dist/local-vm.d.ts.map +1 -0
- package/dist/local-vm.js +360 -0
- package/dist/local-vm.js.map +1 -0
- package/dist/measure-guard.d.mts +34 -0
- package/dist/measure-guard.d.mts.map +1 -0
- package/dist/measure-guard.mjs +73 -0
- package/dist/measure-guard.mjs.map +1 -0
- package/dist/normalise-fleet.d.mts +2 -0
- package/dist/normalise-fleet.d.mts.map +1 -0
- package/dist/normalise-fleet.mjs +76 -0
- package/dist/normalise-fleet.mjs.map +1 -0
- package/dist/npm-cli-executable.d.mts +42 -0
- package/dist/npm-cli-executable.d.mts.map +1 -0
- package/dist/npm-cli-executable.mjs +159 -0
- package/dist/npm-cli-executable.mjs.map +1 -0
- package/dist/probe-outcome.d.mts +89 -0
- package/dist/probe-outcome.d.mts.map +1 -0
- package/dist/probe-outcome.mjs +104 -0
- package/dist/probe-outcome.mjs.map +1 -0
- package/dist/protocol-guard.d.mts +34 -0
- package/dist/protocol-guard.d.mts.map +1 -0
- package/dist/protocol-guard.mjs +121 -0
- package/dist/protocol-guard.mjs.map +1 -0
- package/dist/source-walk.d.mts +12 -0
- package/dist/source-walk.d.mts.map +1 -0
- package/dist/source-walk.mjs +56 -0
- package/dist/source-walk.mjs.map +1 -0
- package/dist/transient-fault.d.mts +6 -0
- package/dist/transient-fault.d.mts.map +1 -0
- package/dist/transient-fault.mjs +86 -0
- package/dist/transient-fault.mjs.map +1 -0
- package/dist/utm-deprecated.d.mts +6 -0
- package/dist/utm-deprecated.d.mts.map +1 -0
- package/dist/utm-deprecated.mjs +23 -0
- package/dist/utm-deprecated.mjs.map +1 -0
- package/dist/worker-code-check.d.mts +29 -0
- package/dist/worker-code-check.d.mts.map +1 -0
- package/dist/worker-code-check.mjs +85 -0
- package/dist/worker-code-check.mjs.map +1 -0
- package/dist/worker-health.d.mts +56 -0
- package/dist/worker-health.d.mts.map +1 -0
- package/dist/worker-health.mjs +73 -0
- package/dist/worker-health.mjs.map +1 -0
- package/dist/worker-http.d.mts +103 -0
- package/dist/worker-http.d.mts.map +1 -0
- package/dist/worker-http.mjs +277 -0
- package/dist/worker-http.mjs.map +1 -0
- package/dist/worker-stats.d.mts +66 -0
- package/dist/worker-stats.d.mts.map +1 -0
- package/dist/worker-stats.mjs +143 -0
- package/dist/worker-stats.mjs.map +1 -0
- package/package.json +96 -4
- package/src/local-worker/autounattend.xml +280 -0
- package/src/local-worker/build-vm.sh +218 -0
- package/src/local-worker/clone-worker.sh +141 -0
- package/src/local-worker/create-utm-vm.sh +202 -0
- package/src/local-worker/fetch-windows-iso.sh +238 -0
- package/src/local-worker/first-boot.cmd +58 -0
- package/src/local-worker/worker-ctl.sh +442 -0
- package/src/provisioning/README.md +28 -0
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
- package/src/provisioning/bare-metal/README.md +213 -0
- package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
- package/src/provisioning/bare-metal/autounattend.xml +428 -0
- package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
- package/src/provisioning/bootstrap-control-plane.sh +463 -0
- package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
- package/src/provisioning/build-lean-worker-image.ps1 +275 -0
- package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
- package/src/provisioning/provision-nvda-worker.ps1 +827 -0
- package/src/provisioning/set-display-mode.ps1 +411 -0
- package/src/provisioning/stamp-provision-revision.ps1 +184 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-safe-env.mjs","sourceRoot":"","sources":["../src/git-safe-env.mjs"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,EAAE;AACF,yGAAyG;AACzG,iGAAiG;AACjG,qHAAqH;AACrH,qCAAqC;AACrC,EAAE;AACF,0FAA0F;AAC1F,uGAAuG;AACvG,2GAA2G;AAC3G,kGAAkG;AAClG,uGAAuG;AACvG,uGAAuG;AACvG,EAAE;AACF,0FAA0F;AAC1F,uGAAuG;AACvG,oGAAoG;AACpG,kBAAkB;AAElB,gCAAgC;AAChC,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,yBAAyB;IACzB,sBAAsB;IACtB,kCAAkC;IAClC,gBAAgB;CACjB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAK,GAAG,EAAE;IACtC,iDAAiD;IACjD,MAAM,QAAQ,GAAG,EAAE,CAAC;IACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACrD,CAAC;IACD,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cmd line that registers and starts the elevated task.
|
|
3
|
+
*
|
|
4
|
+
* Built as a single string on purpose -- see trap 1 above. Exported so the quoting is testable without
|
|
5
|
+
* a guest, because it is the part that silently produces wrong answers rather than errors.
|
|
6
|
+
*/
|
|
7
|
+
/** @param {{ scriptPath: string, taskName?: string }} where */
|
|
8
|
+
export function scheduleCommand({ scriptPath, taskName }: {
|
|
9
|
+
scriptPath: string;
|
|
10
|
+
taskName?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a caller's script so it always terminates with the sentinel.
|
|
14
|
+
*
|
|
15
|
+
* Without this there is no way to distinguish "still running" from "died on line one" -- which is
|
|
16
|
+
* exactly how a trim that crashed immediately looked identical to a trim in progress, for three boots.
|
|
17
|
+
*/
|
|
18
|
+
/** @param {string} body @param {string} outputFile */
|
|
19
|
+
export function wrapScript(body: string, outputFile: string): string;
|
|
20
|
+
/** Did the run finish? Exported because "no sentinel" and "no file" are different failures. */
|
|
21
|
+
/** @param {string} output */
|
|
22
|
+
/** @param {string | null} output */
|
|
23
|
+
export function isComplete(output: string | null): boolean;
|
|
24
|
+
/** Written by the wrapper as its last act. Absent means still running, or it died. */
|
|
25
|
+
export const DONE_SENTINEL: "---GUEST-RUN-DONE---";
|
|
26
|
+
//# sourceMappingURL=guest-run.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-run.d.mts","sourceRoot":"","sources":["../src/guest-run.mjs"],"names":[],"mappings":"AAuDA;;;;;GAKG;AACH,+DAA+D;AAC/D,0DADY;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,UAIpD;AAED;;;;;GAKG;AACH,sDAAsD;AACtD,iCADY,MAAM,cAAe,MAAM,UAWtC;AAED,+FAA+F;AAC/F,6BAA6B;AAC7B,oCAAoC;AACpC,mCADY,MAAM,GAAG,IAAI,WAGxB;AAxCD,sFAAsF;AACtF,4BAA6B,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Run a script on a guest, elevated, and get its output back.
|
|
3
|
+
//
|
|
4
|
+
// node packages/worker-fleet/src/guest-run.mjs <vm-name> <local-script.cmd> [--timeout=600]
|
|
5
|
+
//
|
|
6
|
+
// This exists because there was no reliable way to do it, and the workarounds each failed differently:
|
|
7
|
+
//
|
|
8
|
+
// utmctl exec runs as NT AUTHORITY\SYSTEM -- fully elevated -- but returns NO OUTPUT and exit
|
|
9
|
+
// code 0 whether or not the command ran. Fine for firing something, useless for
|
|
10
|
+
// knowing what happened.
|
|
11
|
+
// the worker's HTTP unelevated. The worker task is RunLevel Limited on purpose (NVDA does not need
|
|
12
|
+
// elevation), so DISM, sc.exe and Defender's registry keys are all closed to it.
|
|
13
|
+
// Register-ScheduledTask from the worker "Access is denied" -- registering an elevated task itself
|
|
14
|
+
// needs elevation.
|
|
15
|
+
// ssh / WinRM / RDP not installed; ports 22, 5985, 5986, 3389 and 445 are all closed.
|
|
16
|
+
//
|
|
17
|
+
// The combination that works: `utmctl exec` (SYSTEM) registers a scheduled task at RunLevel HIGHEST,
|
|
18
|
+
// starts it, and the task writes its output to a file we pull back. Elevation from the exec channel,
|
|
19
|
+
// results through the file channel, neither depending on the other.
|
|
20
|
+
//
|
|
21
|
+
// TWO TRAPS, both of which produced wrong answers today and are handled here:
|
|
22
|
+
//
|
|
23
|
+
// 1. `utmctl exec ... -- a b c` passes a, b and c as SEPARATE argv entries. A cmd line containing
|
|
24
|
+
// parentheses, `&` or quotes gets split across them and cmd receives fragments. A `reg query`
|
|
25
|
+
// written that way reported "key not found" for a key that certainly existed, and nearly led to
|
|
26
|
+
// the conclusion that a real configuration drift was a false alarm. The command is passed as ONE
|
|
27
|
+
// argument here, always.
|
|
28
|
+
// 2. A detached child dies with the exec that spawned it. `start /b` looked like it worked -- exit 0,
|
|
29
|
+
// no output -- and the child never ran. A scheduled task survives because the Task Scheduler
|
|
30
|
+
// service owns it.
|
|
31
|
+
import { execFile } from "node:child_process";
|
|
32
|
+
import { createReadStream, readFileSync, existsSync } from "node:fs";
|
|
33
|
+
import { pathToFileURL } from "node:url";
|
|
34
|
+
import { basename, resolve } from "node:path";
|
|
35
|
+
import { promisify } from "node:util";
|
|
36
|
+
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
37
|
+
import { warnUtmDeprecated } from "./utm-deprecated.mjs";
|
|
38
|
+
/**
|
|
39
|
+
* takes a VM name and a script POSITIONALLY, which this guard does not touch, plus `--timeout=`;
|
|
40
|
+
* a mistyped timeout silently falls back to 600s on an operation that may legitimately need longer.
|
|
41
|
+
*
|
|
42
|
+
* An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
|
|
43
|
+
*/
|
|
44
|
+
refuseUnknownFlags(["--timeout="], { entry: import.meta.url, command: "npm run guest:run" });
|
|
45
|
+
const run = promisify(execFile);
|
|
46
|
+
const UTMCTL = "/Applications/UTM.app/Contents/MacOS/utmctl";
|
|
47
|
+
const GUEST_DIR = "C:\\Users\\witness\\a11y-witness";
|
|
48
|
+
const TASK_NAME = "a11yguestrun";
|
|
49
|
+
/** Written by the wrapper as its last act. Absent means still running, or it died. */
|
|
50
|
+
export const DONE_SENTINEL = "---GUEST-RUN-DONE---";
|
|
51
|
+
const POLL_MS = 5_000;
|
|
52
|
+
/**
|
|
53
|
+
* The cmd line that registers and starts the elevated task.
|
|
54
|
+
*
|
|
55
|
+
* Built as a single string on purpose -- see trap 1 above. Exported so the quoting is testable without
|
|
56
|
+
* a guest, because it is the part that silently produces wrong answers rather than errors.
|
|
57
|
+
*/
|
|
58
|
+
/** @param {{ scriptPath: string, taskName?: string }} where */
|
|
59
|
+
export function scheduleCommand({ scriptPath, taskName = TASK_NAME }) {
|
|
60
|
+
return `schtasks /create /tn ${taskName} /tr "${scriptPath}" /sc once /st 00:00 ` +
|
|
61
|
+
`/ru SYSTEM /rl HIGHEST /f >nul 2>&1 & schtasks /run /tn ${taskName} >nul 2>&1`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Wrap a caller's script so it always terminates with the sentinel.
|
|
65
|
+
*
|
|
66
|
+
* Without this there is no way to distinguish "still running" from "died on line one" -- which is
|
|
67
|
+
* exactly how a trim that crashed immediately looked identical to a trim in progress, for three boots.
|
|
68
|
+
*/
|
|
69
|
+
/** @param {string} body @param {string} outputFile */
|
|
70
|
+
export function wrapScript(body, outputFile) {
|
|
71
|
+
const lines = [
|
|
72
|
+
"@echo off",
|
|
73
|
+
`cd /d ${GUEST_DIR}`,
|
|
74
|
+
`set OUT=${outputFile}`,
|
|
75
|
+
`> ${outputFile} echo === guest-run ===`,
|
|
76
|
+
...body.split(/\r?\n/).filter((/** @type {string} */ l) => !/^@echo off\s*$/i.test(l)).map((/** @type {string} */ l) => l.trimEnd()),
|
|
77
|
+
`>> ${outputFile} echo ${DONE_SENTINEL}`,
|
|
78
|
+
];
|
|
79
|
+
return lines.join("\r\n") + "\r\n";
|
|
80
|
+
}
|
|
81
|
+
/** Did the run finish? Exported because "no sentinel" and "no file" are different failures. */
|
|
82
|
+
/** @param {string} output */
|
|
83
|
+
/** @param {string | null} output */
|
|
84
|
+
export function isComplete(output) {
|
|
85
|
+
return typeof output === "string" && output.includes(DONE_SENTINEL);
|
|
86
|
+
}
|
|
87
|
+
/** @param {string} vmName */
|
|
88
|
+
async function uuidFor(vmName) {
|
|
89
|
+
const { stdout } = await run(UTMCTL, ["list"]);
|
|
90
|
+
const line = stdout.split("\n").find((l) => l.trim().endsWith(` ${vmName}`) || l.trim().endsWith(`\t${vmName}`));
|
|
91
|
+
const uuid = line?.trim().split(/\s+/)[0];
|
|
92
|
+
if (!uuid)
|
|
93
|
+
throw new Error(`no VM named '${vmName}'`);
|
|
94
|
+
return uuid;
|
|
95
|
+
}
|
|
96
|
+
/** @param {string} uuid @param {string} guestPath @param {string} localPath @returns {Promise<void>} */
|
|
97
|
+
function push(uuid, guestPath, localPath) {
|
|
98
|
+
return new Promise((done, fail) => {
|
|
99
|
+
const child = execFile(UTMCTL, ["file", "push", uuid, guestPath], (error) => error ? fail(new Error(`push failed: ${ /** @type {Error} */(error).message}`)) : done());
|
|
100
|
+
if (child.stdin)
|
|
101
|
+
createReadStream(localPath).pipe(child.stdin);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** @param {string} uuid @param {string} guestPath */
|
|
105
|
+
async function pull(uuid, guestPath) {
|
|
106
|
+
try {
|
|
107
|
+
const { stdout } = await run(UTMCTL, ["file", "pull", uuid, guestPath], { maxBuffer: 1 << 24 });
|
|
108
|
+
return stdout;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null; // not written yet
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function main() {
|
|
115
|
+
warnUtmDeprecated("node packages/worker-fleet/src/guest-run.mjs");
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
const [vmName, scriptFile] = args.filter((a) => !a.startsWith("--"));
|
|
118
|
+
const timeoutS = Number(args.find((a) => a.startsWith("--timeout="))?.split("=")[1] ?? 600);
|
|
119
|
+
if (!vmName || !scriptFile) {
|
|
120
|
+
process.stderr.write("usage: node packages/worker-fleet/src/guest-run.mjs <vm-name> <script.cmd> [--timeout=600]\n");
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
const local = resolve(scriptFile);
|
|
124
|
+
if (!existsSync(local))
|
|
125
|
+
throw new Error(`no such script: ${local}`);
|
|
126
|
+
const name = basename(scriptFile).replace(/\.[^.]+$/, "");
|
|
127
|
+
const guestScript = `${GUEST_DIR}\\${name}.cmd`;
|
|
128
|
+
const guestOutput = `${GUEST_DIR}\\.${name}.out`;
|
|
129
|
+
const wrapped = resolve(process.env.TMPDIR ?? "/tmp", `${name}.wrapped.cmd`);
|
|
130
|
+
const { writeFileSync } = await import("node:fs");
|
|
131
|
+
writeFileSync(wrapped, wrapScript(readFileSync(local, "utf8"), guestOutput), "utf8");
|
|
132
|
+
const uuid = await uuidFor(vmName);
|
|
133
|
+
process.stdout.write(`==> ${vmName} (${uuid})\n`);
|
|
134
|
+
await push(uuid, guestScript, wrapped);
|
|
135
|
+
process.stdout.write(` pushed ${name}.cmd\n`);
|
|
136
|
+
// One argument. See trap 1.
|
|
137
|
+
await run(UTMCTL, ["exec", uuid, "--cmd", "cmd.exe", "--", "/c",
|
|
138
|
+
scheduleCommand({ scriptPath: guestScript })]);
|
|
139
|
+
process.stdout.write(" running as SYSTEM at RunLevel HIGHEST\n");
|
|
140
|
+
const deadline = Date.now() + timeoutS * 1000;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
143
|
+
const output = await pull(uuid, guestOutput);
|
|
144
|
+
if (isComplete(output)) {
|
|
145
|
+
process.stdout.write((output ?? "").replace(DONE_SENTINEL, "").trimEnd() + "\n");
|
|
146
|
+
process.stdout.write(" done\n");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`no ${DONE_SENTINEL} within ${timeoutS}s — the script may still be running; ` +
|
|
151
|
+
`pull ${guestOutput} to see how far it got`);
|
|
152
|
+
}
|
|
153
|
+
// `import.meta.url === pathToFileURL(process.argv[1])`, not `endsWith("guest-run.mjs")`. The old form
|
|
154
|
+
// worked but matched on a SUFFIX, so any entry point whose path happened to end that way -- a
|
|
155
|
+
// `my-guest-run.mjs`, a copy under another directory -- would have run this file's main instead of its own.
|
|
156
|
+
// Every other entry point in this repo now uses the exact comparison; one idiom, so there is one thing to
|
|
157
|
+
// get right.
|
|
158
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
159
|
+
main().catch((error) => {
|
|
160
|
+
process.stderr.write(`guest-run failed: ${error.message}\n`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=guest-run.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-run.mjs","sourceRoot":"","sources":["../src/guest-run.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Dotted quad to a comparable integer, or null if it is not one (a hostname, IPv6, nonsense). */
|
|
2
|
+
/** @param {string} ip */
|
|
3
|
+
export function ipv4ToInt(ip: string): number | null;
|
|
4
|
+
/**
|
|
5
|
+
* This host's address on the same subnet as `guestIp`, or undefined if we have no interface onto it.
|
|
6
|
+
*
|
|
7
|
+
* Undefined rather than a guess: a caller that is told "we do not know" can ask to be told, whereas
|
|
8
|
+
* a caller handed a plausible-but-dead address discovers it as a timeout somewhere else entirely.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} guestIp
|
|
11
|
+
* @returns {string | undefined}
|
|
12
|
+
*/
|
|
13
|
+
export function hostAddressFor(guestIp: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* This host's address as seen from a worker at `workerUrl`, when the two share a subnet.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} workerUrl
|
|
18
|
+
* @returns {string | undefined}
|
|
19
|
+
*/
|
|
20
|
+
export function hostAddressForWorker(workerUrl: string): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The base URL a worker should use to fetch host-served pages.
|
|
23
|
+
*
|
|
24
|
+
* Throws rather than returning something unusable. The four call sites all feed this straight into a
|
|
25
|
+
* capture request, and a wrong page URL does not present as a page-server fault — it presents as
|
|
26
|
+
* captures that read an error page, which is evidence rot rather than an outage.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} workerUrl
|
|
29
|
+
* @param {number | string} [port]
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
export function hostPagesBase(workerUrl: string, port?: number | string): string;
|
|
33
|
+
//# sourceMappingURL=host-address.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-address.d.mts","sourceRoot":"","sources":["../src/host-address.mjs"],"names":[],"mappings":"AAiCA,kGAAkG;AAClG,yBAAyB;AACzB,8BADY,MAAM,iBAKjB;AAED;;;;;;;;GAQG;AACH,wCAHW,MAAM,GACJ,MAAM,GAAG,SAAS,CAiB9B;AAED;;;;;GAKG;AACH,gDAHW,MAAM,GACJ,MAAM,GAAG,SAAS,CAS9B;AAED;;;;;;;;;;GAUG;AACH,yCAJW,MAAM,SACN,MAAM,GAAG,MAAM,GACb,MAAM,CAWlB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* This host's address as a WORKER sees it — one definition, because there were five.
|
|
4
|
+
*
|
|
5
|
+
* A worker cannot reach the host's `localhost`, so anything the host serves to a worker (the dataset
|
|
6
|
+
* page server, above all) must be addressed by the host's LAN IP. `local-vm.ts` worked this out
|
|
7
|
+
* correctly and said why:
|
|
8
|
+
*
|
|
9
|
+
* > Derived by matching interfaces against the guest's address rather than assuming the usual
|
|
10
|
+
* > `x.y.z.1`, because guessing an address that silently does not answer is worse than reporting
|
|
11
|
+
* > that we could not find one.
|
|
12
|
+
*
|
|
13
|
+
* Four harnesses then reimplemented it inline as exactly the assumption that comment rejects:
|
|
14
|
+
*
|
|
15
|
+
* new URL(worker).hostname.replace(/\.\d+$/, ".1")
|
|
16
|
+
*
|
|
17
|
+
* That is right for UTM, where the Mac really is the `.1` gateway of the shared network, and wrong
|
|
18
|
+
* for every other network. Measured: against a bare-metal worker on the fleet's own LAN it produced
|
|
19
|
+
* the ROUTER's address instead — so `evidence:check` refused to run because the pages
|
|
20
|
+
* were not being served. On a fleet of bare-metal boxes it fails every time.
|
|
21
|
+
*
|
|
22
|
+
* The failure mode is why this matters more than tidiness. `guestReachableUrl`'s own note records
|
|
23
|
+
* what happened last time this was wrong: the guest fetched its own localhost, Edge showed
|
|
24
|
+
* "localhost refused to connect", the title check rejected the capture, and three attempts were
|
|
25
|
+
* burned per page before giving up — a page-server problem wearing a capture problem's costume.
|
|
26
|
+
*
|
|
27
|
+
* Plain `.mjs` on purpose: `capture-check.mjs` runs under bare node on the guest (see
|
|
28
|
+
* `run-capture-check.cmd`) and cannot import TypeScript.
|
|
29
|
+
*/
|
|
30
|
+
import { networkInterfaces } from "node:os";
|
|
31
|
+
const IPV4_OCTETS = 4;
|
|
32
|
+
/** Dotted quad to a comparable integer, or null if it is not one (a hostname, IPv6, nonsense). */
|
|
33
|
+
/** @param {string} ip */
|
|
34
|
+
export function ipv4ToInt(ip) {
|
|
35
|
+
const parts = String(ip).split(".").map(Number);
|
|
36
|
+
if (parts.length !== IPV4_OCTETS || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
37
|
+
return null;
|
|
38
|
+
return parts.reduce((acc, octet) => acc * 256 + octet, 0);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* This host's address on the same subnet as `guestIp`, or undefined if we have no interface onto it.
|
|
42
|
+
*
|
|
43
|
+
* Undefined rather than a guess: a caller that is told "we do not know" can ask to be told, whereas
|
|
44
|
+
* a caller handed a plausible-but-dead address discovers it as a timeout somewhere else entirely.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} guestIp
|
|
47
|
+
* @returns {string | undefined}
|
|
48
|
+
*/
|
|
49
|
+
export function hostAddressFor(guestIp) {
|
|
50
|
+
const guest = ipv4ToInt(guestIp);
|
|
51
|
+
if (guest === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
for (const addresses of Object.values(networkInterfaces())) {
|
|
54
|
+
for (const a of addresses ?? []) {
|
|
55
|
+
if (a.family !== "IPv4" || a.internal)
|
|
56
|
+
continue;
|
|
57
|
+
const host = ipv4ToInt(a.address);
|
|
58
|
+
const mask = ipv4ToInt(a.netmask);
|
|
59
|
+
if (host === null || mask === null)
|
|
60
|
+
continue;
|
|
61
|
+
// Both sides go through the same coercion, so addresses above 2^31 comparing as negative
|
|
62
|
+
// is harmless here.
|
|
63
|
+
if ((host & mask) === (guest & mask))
|
|
64
|
+
return a.address;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* This host's address as seen from a worker at `workerUrl`, when the two share a subnet.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} workerUrl
|
|
73
|
+
* @returns {string | undefined}
|
|
74
|
+
*/
|
|
75
|
+
export function hostAddressForWorker(workerUrl) {
|
|
76
|
+
try {
|
|
77
|
+
const { hostname } = new URL(workerUrl);
|
|
78
|
+
return ipv4ToInt(hostname) === null ? undefined : hostAddressFor(hostname);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The base URL a worker should use to fetch host-served pages.
|
|
86
|
+
*
|
|
87
|
+
* Throws rather than returning something unusable. The four call sites all feed this straight into a
|
|
88
|
+
* capture request, and a wrong page URL does not present as a page-server fault — it presents as
|
|
89
|
+
* captures that read an error page, which is evidence rot rather than an outage.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} workerUrl
|
|
92
|
+
* @param {number | string} [port]
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function hostPagesBase(workerUrl, port = process.env.DATASET_PAGES_PORT || 5050) {
|
|
96
|
+
if (process.env.DATASET_BASE_URL)
|
|
97
|
+
return process.env.DATASET_BASE_URL.replace(/\/$/, "");
|
|
98
|
+
const address = hostAddressForWorker(workerUrl);
|
|
99
|
+
if (!address) {
|
|
100
|
+
throw new Error(`Cannot work out this host's address as seen from ${workerUrl}: no local interface shares its subnet. `
|
|
101
|
+
+ "Set DATASET_BASE_URL to the URL the worker should fetch dataset pages from.");
|
|
102
|
+
}
|
|
103
|
+
return `http://${address}:${port}`;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=host-address.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-address.mjs","sourceRoot":"","sources":["../src/host-address.mjs"],"names":[],"mappings":"AAAA,YAAY;AACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB,kGAAkG;AAClG,yBAAyB;AACzB,MAAM,UAAU,SAAS,CAAC,EAAE;IAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7G,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,OAAO;IACpC,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACrC,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,EAAE,CAAC;QAC3D,KAAK,MAAM,CAAC,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ;gBAAE,SAAS;YAChD,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YAClC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC7C,yFAAyF;YACzF,oBAAoB;YACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;gBAAE,OAAO,CAAC,CAAC,OAAO,CAAC;QACzD,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAS;IAC5C,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,OAAO,SAAS,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI;IACpF,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzF,MAAM,OAAO,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,oDAAoD,SAAS,0CAA0C;cACrG,6EAA6E,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Available memory on macOS, in MB, or null if it cannot be read.
|
|
3
|
+
*
|
|
4
|
+
* `os.freemem()` is useless here: it reported 402 MB on a host that comfortably had ~12 GB to give,
|
|
5
|
+
* because macOS counts compressed and inactive pages as used. The pages that can actually be handed
|
|
6
|
+
* out without swapping are free + inactive + speculative + purgeable, which is what `vm_stat` reports.
|
|
7
|
+
*
|
|
8
|
+
* Returns null rather than throwing on any surprise — a capacity check that cannot read the host must
|
|
9
|
+
* not stop a run. This codebase already applies that rule to foregroundLockTimeout(): a broken
|
|
10
|
+
* diagnostic taking the pool offline is worse than the fault it looks for.
|
|
11
|
+
*/
|
|
12
|
+
export function availableHostMemoryMb(): number | null;
|
|
13
|
+
/** Physical RAM in MB. Unlike `os.freemem()` this cannot lie — it is a property of the machine. */
|
|
14
|
+
export function totalHostMemoryMb(): number;
|
|
15
|
+
/**
|
|
16
|
+
* The most workers this machine can EVER hold, from physical RAM alone.
|
|
17
|
+
*
|
|
18
|
+
* This exists because the dynamic estimate below is computed from `vm_stat`, and `vm_stat` is
|
|
19
|
+
* distorted by exactly the situation it needs to detect. Once guests are swapped out, their pages are
|
|
20
|
+
* counted as compressed/inactive — which `availableHostMemoryMb` reports as *available* — so a host
|
|
21
|
+
* three guests deep in swap advertised 13.7 GB free while two of the three could not answer an HTTP
|
|
22
|
+
* health check within 75 seconds. Total RAM is immune to that feedback loop.
|
|
23
|
+
*
|
|
24
|
+
* @returns {number} workers, from physical memory
|
|
25
|
+
*/
|
|
26
|
+
export function workerCeilingFromTotalRam(totalMb?: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* How many workers may be RUNNING at once, given what the host has spare.
|
|
29
|
+
*
|
|
30
|
+
* **A running worker is not automatically an affordable one.** This used to return
|
|
31
|
+
* `alreadyRunning + canStart` on the reasoning that guests already up had "paid for" their memory and
|
|
32
|
+
* `availableMb` was what remained after them. That is true of a healthy host and false of the one case
|
|
33
|
+
* that matters: when the host is in swap, the running guests are being paid for out of disk, and adding
|
|
34
|
+
* their count back ratifies the very over-commitment that is breaking the run. The result could never
|
|
35
|
+
* be lower than the number of VMs already up, so the cap was structurally unable to hold back a pool
|
|
36
|
+
* somebody had already started — which is how three guests came to share a 36 GB Mac, drive 6.6 GB of
|
|
37
|
+
* swap, and black out two of the three workers mid-run.
|
|
38
|
+
*
|
|
39
|
+
* So the answer is the lower of the dynamic estimate and the physical-RAM ceiling, and it is allowed to
|
|
40
|
+
* come out below `alreadyRunning`. The run then dispatches to fewer workers than are up, which is the
|
|
41
|
+
* conservative direction: the extra guest wastes memory but no longer takes work.
|
|
42
|
+
*
|
|
43
|
+
* Never returns less than one: a run with no workers is a worse outcome than a slow one.
|
|
44
|
+
*
|
|
45
|
+
* @param {{ availableMb: number | null, alreadyRunning: number, totalMb?: number }} host
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
export function workersHostCanRun({ availableMb, alreadyRunning, totalMb }: {
|
|
49
|
+
availableMb: number | null;
|
|
50
|
+
alreadyRunning: number;
|
|
51
|
+
totalMb?: number;
|
|
52
|
+
}): number;
|
|
53
|
+
/**
|
|
54
|
+
* Why the pool was capped, in words a human can act on. Null when nothing was held back.
|
|
55
|
+
*
|
|
56
|
+
* @param {{ limit: number, wanted: number, availableMb: number | null }} cap
|
|
57
|
+
* @returns {string | null}
|
|
58
|
+
*/
|
|
59
|
+
export function capacityReason({ limit, wanted, availableMb }: {
|
|
60
|
+
limit: number;
|
|
61
|
+
wanted: number;
|
|
62
|
+
availableMb: number | null;
|
|
63
|
+
}): string | null;
|
|
64
|
+
//# sourceMappingURL=host-capacity.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-capacity.d.mts","sourceRoot":"","sources":["../src/host-capacity.mjs"],"names":[],"mappings":"AAkEA;;;;;;;;;;GAUG;AACH,uDAcC;AAED,mGAAmG;AACnG,4CAEC;AAED;;;;;;;;;;GAUG;AACH,6DAFa,MAAM,CAIlB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,4EAHW;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACtE,MAAM,CAOlB;AAED;;;;;GAKG;AACH,+DAHW;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3D,MAAM,GAAG,IAAI,CASzB"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* How many worker VMs will actually fit on this Mac?
|
|
4
|
+
*
|
|
5
|
+
* The pool used to start every VM it found. On a 36 GB host that meant three guests, and measurement
|
|
6
|
+
* says three do not fit: with all three up a capture took 44.5 s, and with one up it took 27.4 s of
|
|
7
|
+
* the same page on the same worker — a 1.6x penalty paid by every capture in the run. The guests were
|
|
8
|
+
* being swapped out from under NVDA, which also produced "NVDA is running but not speaking" failures
|
|
9
|
+
* and blackouts on /health. More workers were making the run slower AND less reliable.
|
|
10
|
+
*
|
|
11
|
+
* So capacity is a property of the host, not a count of the VMs that happen to be registered.
|
|
12
|
+
*
|
|
13
|
+
* `.mjs` rather than `.ts` because `npm run doctor` runs under plain node — it is the first thing an
|
|
14
|
+
* agent runs and must not depend on a transpiler — while the pool lease is TypeScript. Same reason
|
|
15
|
+
* capture-decisions.mjs is .mjs.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { totalmem } from "node:os";
|
|
19
|
+
/**
|
|
20
|
+
* What one worker VM costs the host.
|
|
21
|
+
*
|
|
22
|
+
* Measured with `top -o mem`, which agrees with phys_footprint. Host cost tracks the guest's CONFIGURED
|
|
23
|
+
* RAM at ~1.8x, not its usage:
|
|
24
|
+
*
|
|
25
|
+
* 4096 MB configured -> 8,048-8,127 MB host
|
|
26
|
+
* 3072 MB configured -> 5,494 MB host <- what the guests now run at
|
|
27
|
+
* 2560 MB configured -> 4,952 MB host <- measured, and too small: the guest pages
|
|
28
|
+
*
|
|
29
|
+
* 3072 MB is the setting because it is the smallest that does not page. The guest commits ~1,859 MB, so
|
|
30
|
+
* 2560 leaves too little above it: capture phases went from a 12.1 s median (IQR 0.4, 0 recoveries in
|
|
31
|
+
* 10) to 36.6 s (IQR 38, 4 recoveries in 10). Less RAM stopped being cheaper the moment Windows started
|
|
32
|
+
* swapping, and four cramped workers measured worse than three comfortable ones.
|
|
33
|
+
*
|
|
34
|
+
* The ~1.8x multiplier is QEMU's own overhead on top of guest RAM that Windows dirties and never gives
|
|
35
|
+
* back (there is no balloon driver). That is why the CONFIGURED size is the lever and the guest's usage
|
|
36
|
+
* is not: Windows expands to fill whatever ceiling it is given.
|
|
37
|
+
*
|
|
38
|
+
* This constant has been wrong twice, in the same direction. 7,600 was an underestimate from a short
|
|
39
|
+
* sample; 8,100 was right for 4096 MB guests and became wrong the moment they were re-sized. Re-measure
|
|
40
|
+
* it whenever guest RAM changes -- it is a property of the configuration, not of the software.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately the high end of the range. Under-committing costs a little parallelism; over-committing
|
|
43
|
+
* costs correctness, because a swapped-out guest fails captures rather than merely slowing them.
|
|
44
|
+
*/
|
|
45
|
+
const MEMORY_PER_WORKER_MB = 5_600;
|
|
46
|
+
/**
|
|
47
|
+
* What the host's own software needs, beyond the run.
|
|
48
|
+
*
|
|
49
|
+
* Measured on this Mac while it was doing nothing unusual: Wispr Flow 2.3 GB, tessl 2.0 GB, stable
|
|
50
|
+
* 1.3 GB, mds_stores 1.1 GB, WindowServer 1.1 GB, Codex 0.9 GB and the rest — about 11 GB before a
|
|
51
|
+
* single guest starts. This module only ever runs on macOS (see availableHostMemoryMb), so it is
|
|
52
|
+
* always somebody's desktop, never a dedicated hypervisor.
|
|
53
|
+
*/
|
|
54
|
+
const HOST_APPS_RESERVE_MB = 12_000;
|
|
55
|
+
/**
|
|
56
|
+
* Memory left for the host itself.
|
|
57
|
+
*
|
|
58
|
+
* The Mac is somebody's desktop, not a dedicated hypervisor — Chrome, Spotify and the editor were all
|
|
59
|
+
* resident during the measurements above. Taking the last of the available memory pushes the HOST into
|
|
60
|
+
* swap, which slows every guest at once.
|
|
61
|
+
*/
|
|
62
|
+
const HOST_HEADROOM_MB = 3_000;
|
|
63
|
+
/**
|
|
64
|
+
* Available memory on macOS, in MB, or null if it cannot be read.
|
|
65
|
+
*
|
|
66
|
+
* `os.freemem()` is useless here: it reported 402 MB on a host that comfortably had ~12 GB to give,
|
|
67
|
+
* because macOS counts compressed and inactive pages as used. The pages that can actually be handed
|
|
68
|
+
* out without swapping are free + inactive + speculative + purgeable, which is what `vm_stat` reports.
|
|
69
|
+
*
|
|
70
|
+
* Returns null rather than throwing on any surprise — a capacity check that cannot read the host must
|
|
71
|
+
* not stop a run. This codebase already applies that rule to foregroundLockTimeout(): a broken
|
|
72
|
+
* diagnostic taking the pool offline is worse than the fault it looks for.
|
|
73
|
+
*/
|
|
74
|
+
export function availableHostMemoryMb() {
|
|
75
|
+
if (process.platform !== "darwin")
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
const output = execFileSync("vm_stat", { encoding: "utf8", timeout: 5_000 });
|
|
79
|
+
const pageSize = Number(/page size of (\d+) bytes/.exec(output)?.[1]);
|
|
80
|
+
if (!Number.isFinite(pageSize))
|
|
81
|
+
return null;
|
|
82
|
+
/** @param {string} label */
|
|
83
|
+
const pagesFor = (label) => Number(new RegExp(`Pages ${label}:\\s+(\\d+)`).exec(output)?.[1] ?? 0);
|
|
84
|
+
const pages = pagesFor("free") + pagesFor("inactive") + pagesFor("speculative") + pagesFor("purgeable");
|
|
85
|
+
return Math.round((pages * pageSize) / (1024 * 1024));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Physical RAM in MB. Unlike `os.freemem()` this cannot lie — it is a property of the machine. */
|
|
92
|
+
export function totalHostMemoryMb() {
|
|
93
|
+
return Math.round(totalmem() / (1024 * 1024));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The most workers this machine can EVER hold, from physical RAM alone.
|
|
97
|
+
*
|
|
98
|
+
* This exists because the dynamic estimate below is computed from `vm_stat`, and `vm_stat` is
|
|
99
|
+
* distorted by exactly the situation it needs to detect. Once guests are swapped out, their pages are
|
|
100
|
+
* counted as compressed/inactive — which `availableHostMemoryMb` reports as *available* — so a host
|
|
101
|
+
* three guests deep in swap advertised 13.7 GB free while two of the three could not answer an HTTP
|
|
102
|
+
* health check within 75 seconds. Total RAM is immune to that feedback loop.
|
|
103
|
+
*
|
|
104
|
+
* @returns {number} workers, from physical memory
|
|
105
|
+
*/
|
|
106
|
+
export function workerCeilingFromTotalRam(totalMb = totalHostMemoryMb()) {
|
|
107
|
+
return Math.floor((totalMb - HOST_APPS_RESERVE_MB - HOST_HEADROOM_MB) / MEMORY_PER_WORKER_MB);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* How many workers may be RUNNING at once, given what the host has spare.
|
|
111
|
+
*
|
|
112
|
+
* **A running worker is not automatically an affordable one.** This used to return
|
|
113
|
+
* `alreadyRunning + canStart` on the reasoning that guests already up had "paid for" their memory and
|
|
114
|
+
* `availableMb` was what remained after them. That is true of a healthy host and false of the one case
|
|
115
|
+
* that matters: when the host is in swap, the running guests are being paid for out of disk, and adding
|
|
116
|
+
* their count back ratifies the very over-commitment that is breaking the run. The result could never
|
|
117
|
+
* be lower than the number of VMs already up, so the cap was structurally unable to hold back a pool
|
|
118
|
+
* somebody had already started — which is how three guests came to share a 36 GB Mac, drive 6.6 GB of
|
|
119
|
+
* swap, and black out two of the three workers mid-run.
|
|
120
|
+
*
|
|
121
|
+
* So the answer is the lower of the dynamic estimate and the physical-RAM ceiling, and it is allowed to
|
|
122
|
+
* come out below `alreadyRunning`. The run then dispatches to fewer workers than are up, which is the
|
|
123
|
+
* conservative direction: the extra guest wastes memory but no longer takes work.
|
|
124
|
+
*
|
|
125
|
+
* Never returns less than one: a run with no workers is a worse outcome than a slow one.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ availableMb: number | null, alreadyRunning: number, totalMb?: number }} host
|
|
128
|
+
* @returns {number}
|
|
129
|
+
*/
|
|
130
|
+
export function workersHostCanRun({ availableMb, alreadyRunning, totalMb = totalHostMemoryMb() }) {
|
|
131
|
+
if (availableMb === null)
|
|
132
|
+
return Number.POSITIVE_INFINITY; // unreadable: do not constrain the run
|
|
133
|
+
const spareMb = availableMb - HOST_HEADROOM_MB;
|
|
134
|
+
const dynamic = alreadyRunning + Math.max(0, Math.floor(spareMb / MEMORY_PER_WORKER_MB));
|
|
135
|
+
return Math.max(1, Math.min(dynamic, workerCeilingFromTotalRam(totalMb)));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Why the pool was capped, in words a human can act on. Null when nothing was held back.
|
|
139
|
+
*
|
|
140
|
+
* @param {{ limit: number, wanted: number, availableMb: number | null }} cap
|
|
141
|
+
* @returns {string | null}
|
|
142
|
+
*/
|
|
143
|
+
export function capacityReason({ limit, wanted, availableMb }) {
|
|
144
|
+
if (!Number.isFinite(limit) || limit >= wanted)
|
|
145
|
+
return null;
|
|
146
|
+
return `host has ${totalHostMemoryMb()} MB of RAM and ~${availableMb} MB available, and each worker ` +
|
|
147
|
+
`costs ~${MEMORY_PER_WORKER_MB} MB, so ${limit} of ${wanted} local workers will be used. ` +
|
|
148
|
+
"Measured: an over-committed host made every capture 1.6x slower, caused mute-NVDA failures, and " +
|
|
149
|
+
"once starved two of three guests until they stopped answering /health at all. " +
|
|
150
|
+
"Override with A11Y_MAX_WORKERS if you know better.";
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=host-capacity.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-capacity.mjs","sourceRoot":"","sources":["../src/host-capacity.mjs"],"names":[],"mappings":"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"}
|