@a11ign/screenreader-fleet 0.0.0-reserved.0 → 0.1.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 +173 -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 +78 -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
package/dist/local-vm.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand lifecycle for the local UTM worker VM.
|
|
3
|
+
*
|
|
4
|
+
* A Windows guest is never really idle -- Defender, Update and the search indexer keep it
|
|
5
|
+
* busy -- so leaving one running between captures costs the host real CPU. Cold start is
|
|
6
|
+
* cheap enough (12-15s to /health, measured) that starting it per run and shutting it down
|
|
7
|
+
* afterwards is the better trade.
|
|
8
|
+
*
|
|
9
|
+
* The default is to LEAVE THE VM AS FOUND, which needs no configuration to do the right
|
|
10
|
+
* thing: a VM that was stopped gets stopped again, a paused one gets re-paused, and one you
|
|
11
|
+
* had already started yourself is left alone. That last case matters -- a run must never
|
|
12
|
+
* shut down a VM somebody else is using.
|
|
13
|
+
*
|
|
14
|
+
* Everything UTM-specific lives in `local-worker/worker-ctl.sh` in this package (`json` emits the
|
|
15
|
+
* state this module reads). Nothing here knows about utmctl, bundles or bookmarks.
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
|
+
import { networkInterfaces } from "node:os";
|
|
20
|
+
import { availableHostMemoryMb, capacityReason, workersHostCanRun } from "./host-capacity.mjs";
|
|
21
|
+
import { fleetScriptPaths } from "./fleet-scripts.mjs";
|
|
22
|
+
import { inventoryWorkerUrls } from "./fleet-env.mjs";
|
|
23
|
+
import { warnUtmDeprecated } from "./utm-deprecated.mjs";
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
/** Where a hand-started worker on this machine has always lived. */
|
|
26
|
+
export const DEFAULT_WORKER = "http://localhost:8765";
|
|
27
|
+
// Relative to this module, not the process cwd: `npm run` happens to start in the repo
|
|
28
|
+
// root, but a direct `tsx src/cli.ts` from elsewhere would not.
|
|
29
|
+
const CTL = fleetScriptPaths().workerCtl;
|
|
30
|
+
const STATUS_TIMEOUT_MS = 30_000;
|
|
31
|
+
// Generous on purpose: `up` boots Windows, waits for auto-logon and then polls /health,
|
|
32
|
+
// and worker-ctl gives that 180s of its own before giving up.
|
|
33
|
+
const LIFECYCLE_TIMEOUT_MS = 300_000;
|
|
34
|
+
const AFTER_RUN_VALUES = ["restore", "stop", "pause", "leave"];
|
|
35
|
+
export function isAfterRun(v) {
|
|
36
|
+
return AFTER_RUN_VALUES.includes(v);
|
|
37
|
+
}
|
|
38
|
+
const IPV4_OCTETS = 4;
|
|
39
|
+
function ipv4ToInt(ip) {
|
|
40
|
+
const parts = ip.split(".").map(Number);
|
|
41
|
+
if (parts.length !== IPV4_OCTETS || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
42
|
+
return null;
|
|
43
|
+
return parts.reduce((acc, octet) => acc * 256 + octet, 0);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* This host's address on the same subnet as the guest -- the gateway end of UTM's shared
|
|
47
|
+
* network (bridge100 on macOS). Derived by matching interfaces against the guest's address
|
|
48
|
+
* rather than assuming the usual `x.y.z.1`, because guessing an address that silently does
|
|
49
|
+
* not answer is worse than reporting that we could not find one.
|
|
50
|
+
*/
|
|
51
|
+
function hostAddressFor(guestIp) {
|
|
52
|
+
const guest = ipv4ToInt(guestIp);
|
|
53
|
+
if (guest === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
for (const addresses of Object.values(networkInterfaces())) {
|
|
56
|
+
for (const a of addresses ?? []) {
|
|
57
|
+
if (a.family !== "IPv4" || a.internal)
|
|
58
|
+
continue;
|
|
59
|
+
const host = ipv4ToInt(a.address);
|
|
60
|
+
const mask = ipv4ToInt(a.netmask);
|
|
61
|
+
if (host === null || mask === null)
|
|
62
|
+
continue;
|
|
63
|
+
// Both sides go through the same int32 coercion, so addresses above 2^31 comparing
|
|
64
|
+
// as negative is harmless here.
|
|
65
|
+
if ((host & mask) === (guest & mask))
|
|
66
|
+
return a.address;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
async function ctl(args, timeoutMs, vmName) {
|
|
72
|
+
const env = vmName ? { ...process.env, A11Y_VM_NAME: vmName } : process.env;
|
|
73
|
+
const { stdout } = await execFileAsync(CTL, args, { timeout: timeoutMs, encoding: "utf8", env });
|
|
74
|
+
return stdout;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The registered local VM, or null when there is nothing to manage: not macOS, no UTM, no
|
|
78
|
+
* VM registered under that name, or duplicate registrations (worker-ctl refuses to guess
|
|
79
|
+
* between those rather than risk acting on the wrong one).
|
|
80
|
+
*/
|
|
81
|
+
export async function findLocalVm() {
|
|
82
|
+
if (process.platform !== "darwin")
|
|
83
|
+
return null;
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(await ctl(["json"], STATUS_TIMEOUT_MS));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Absence is the normal case for anyone without a local VM, so this is a debug
|
|
89
|
+
// breadcrumb rather than a warning -- but never a silent catch.
|
|
90
|
+
if (process.env.A11Y_DEBUG_VM) {
|
|
91
|
+
process.stderr.write(`local VM lookup failed (continuing without one): ${e.message}\n`);
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Which lifecycle action returns the VM to `stateBefore`. */
|
|
97
|
+
function restoreAction(stateBefore) {
|
|
98
|
+
if (stateBefore === "started")
|
|
99
|
+
return "leave";
|
|
100
|
+
if (stateBefore === "paused")
|
|
101
|
+
return "pause";
|
|
102
|
+
return "stop";
|
|
103
|
+
}
|
|
104
|
+
async function releaseVm(action, stateBefore) {
|
|
105
|
+
const resolved = action === "restore" ? restoreAction(stateBefore) : action;
|
|
106
|
+
if (resolved === "leave")
|
|
107
|
+
return;
|
|
108
|
+
// Another run may have started a capture while ours was finishing. Shutting the guest
|
|
109
|
+
// down underneath it would look exactly like a worker crash, so stand down instead.
|
|
110
|
+
const now = await findLocalVm();
|
|
111
|
+
if (now?.busy) {
|
|
112
|
+
process.stderr.write(`worker is busy with another capture; leaving the VM ${now.state}\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
process.stderr.write(`${resolved === "pause" ? "Pausing" : "Shutting down"} the local worker VM ...\n`);
|
|
116
|
+
await ctl([resolved], LIFECYCLE_TIMEOUT_MS);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Start (or resume) the local VM, returning its worker URL and the matching cleanup.
|
|
120
|
+
* Throws if the VM never becomes healthy -- there is no point judging a capture that could
|
|
121
|
+
* not happen.
|
|
122
|
+
*/
|
|
123
|
+
export async function acquireLocalWorker(vm, after) {
|
|
124
|
+
const stateBefore = vm.state;
|
|
125
|
+
if (stateBefore === "started" && vm.healthy) {
|
|
126
|
+
process.stderr.write(`Using the running local worker VM '${vm.name}' at ${vm.ip}\n`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
process.stderr.write(`Local worker VM '${vm.name}' is ${stateBefore}; bringing it up ...\n`);
|
|
130
|
+
process.stderr.write(await ctl(["up"], LIFECYCLE_TIMEOUT_MS));
|
|
131
|
+
}
|
|
132
|
+
const ready = await findLocalVm();
|
|
133
|
+
if (!ready?.healthy || !ready.ip) {
|
|
134
|
+
throw new Error(`Local worker VM '${vm.name}' did not become healthy. Check it directly: ${CTL} status`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
worker: `http://${ready.ip}:${ready.port}`,
|
|
138
|
+
source: "local-vm",
|
|
139
|
+
hostAddress: hostAddressFor(ready.ip),
|
|
140
|
+
release: () => releaseVm(after, stateBefore).catch((e) => {
|
|
141
|
+
// Report and move on. The run's verdict is the product; a VM left running is a
|
|
142
|
+
// resource leak the user can fix with one command, not a reason to fail the run.
|
|
143
|
+
process.stderr.write(`WARNING: could not release the local worker VM: ${e.message}\n`);
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decide what to capture against, in priority order:
|
|
149
|
+
*
|
|
150
|
+
* 1. A worker the caller named is used as-is. Naming one is a statement that you are
|
|
151
|
+
* managing it yourself, so the VM lifecycle is never touched.
|
|
152
|
+
* 2. Otherwise, `inventory.yml`'s bare-metal fleet — always-on boxes, so there is no VM
|
|
153
|
+
* lifecycle to run: the first declared worker is leased with a no-op release.
|
|
154
|
+
* 3. Otherwise, a registered local UTM VM is started on demand and released afterwards.
|
|
155
|
+
* 4. Otherwise, the historical default, so a hand-run worker on this machine still works.
|
|
156
|
+
*
|
|
157
|
+
* FOUND 2026-09-06 (`docs/backlog.md` §8): this used to go straight from (1) to (3), never
|
|
158
|
+
* reading the inventory at all -- so a checkout WITH a bare-metal fleet declared and a UTM guest
|
|
159
|
+
* still registered (the ordinary state of a Mac that used to run the deprecated pool) leased the
|
|
160
|
+
* VM by default, exactly the wrong turn CLAUDE.md already records costing a capture-path change:
|
|
161
|
+
* "a deprecated path that is still the first one documented is not deprecated". `resolveWorkerPool`
|
|
162
|
+
* (`fleet-env.mjs`) already had the corrected order for the POOL case; this brings the single-worker
|
|
163
|
+
* case into line, and `worker-precedence.test.ts` is what is supposed to keep them there.
|
|
164
|
+
*
|
|
165
|
+
* Step 2 costs nothing and throws nothing for anyone without an `inventory.yml` -- a public
|
|
166
|
+
* consumer who installs this package -- because `inventoryWorkerUrls()` already treats an absent
|
|
167
|
+
* or unparsable file as "no fleet declared here" and returns `[]`. So the fall-through to (3) and
|
|
168
|
+
* (4) is byte-for-byte what it was before this change for that consumer; only a checkout that
|
|
169
|
+
* DECLARES a fleet sees new behaviour, and it sees the behaviour `doctor` and `worker:code`
|
|
170
|
+
* already assumed it had.
|
|
171
|
+
*
|
|
172
|
+
* A11Y_LOCAL_VM=0 skips step 3 for anyone who wants the pre-inventory local-VM behaviour back --
|
|
173
|
+
* unchanged in what it does, moved because inventory now sits ahead of it.
|
|
174
|
+
*
|
|
175
|
+
* Shared by every entry point that needs a worker, so the priority order cannot drift
|
|
176
|
+
* between them.
|
|
177
|
+
*
|
|
178
|
+
* `deps` is the injection seam: real callers get the real inventory reader and the real (shells
|
|
179
|
+
* out to `utmctl`) VM lookup, and a test supplies fakes for both, so the precedence can be proven
|
|
180
|
+
* without a filesystem `inventory.yml` or a UTM install. Called as `deps.inventory?.() ?? inventoryWorkerUrls()`
|
|
181
|
+
* rather than resolved into a local first — deliberately, so `inventoryWorkerUrls(` and `findLocalVm(`
|
|
182
|
+
* both appear as real CALLS at the exact decision point, which is what lets `worker-precedence.test.ts`
|
|
183
|
+
* read this function's actual order rather than only the shape of its signature.
|
|
184
|
+
*/
|
|
185
|
+
export async function leaseWorker({ worker, after }, deps = {}) {
|
|
186
|
+
const release = async () => { };
|
|
187
|
+
// Strip a trailing slash once, here, so no caller has to remember that `${worker}/capture`
|
|
188
|
+
// would otherwise produce a double slash.
|
|
189
|
+
if (worker)
|
|
190
|
+
return { worker: worker.replace(/\/$/, ""), source: "explicit", release };
|
|
191
|
+
const fleet = deps.inventory ? deps.inventory() : inventoryWorkerUrls();
|
|
192
|
+
if (fleet.length)
|
|
193
|
+
return { worker: fleet[0].replace(/\/$/, ""), source: "inventory.yml", release };
|
|
194
|
+
if (process.env.A11Y_LOCAL_VM === "0")
|
|
195
|
+
return { worker: DEFAULT_WORKER, source: "default", release };
|
|
196
|
+
// Reached only with no explicit worker and no inventory.yml entry — architecture-audit.md §8's own
|
|
197
|
+
// finding, named exactly: this fallback IS the deprecated path being the CLI's default. Warned, not
|
|
198
|
+
// refused, because deleting it is a separate proposal and some machines still only have a UTM guest
|
|
199
|
+
// to reach.
|
|
200
|
+
const vm = deps.findLocalVm ? await deps.findLocalVm() : await findLocalVm();
|
|
201
|
+
if (!vm)
|
|
202
|
+
return { worker: DEFAULT_WORKER, source: "default", release };
|
|
203
|
+
// Only once a VM exists: the notice says this run "manages a local UTM worker VM", which is false
|
|
204
|
+
// on a machine with none, and the stranger running the published `npx a11ign` is that machine (#3219).
|
|
205
|
+
warnUtmDeprecated("this run (no worker named, no fleet configured)");
|
|
206
|
+
return acquireLocalWorker(vm, after);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Rewrite a base URL the GUEST has to fetch so it points at this host rather than at
|
|
210
|
+
* `localhost`, which from inside the guest means the guest.
|
|
211
|
+
*
|
|
212
|
+
* This is the trap in dataset capture: the pages are served by a plain HTTP server on the
|
|
213
|
+
* Mac, and the default base URL is `http://localhost:5050`. Left alone, every capture
|
|
214
|
+
* loads the guest's own empty port and the transcripts come back describing nothing --
|
|
215
|
+
* with no error, because a connection refused inside Edge is not a worker failure.
|
|
216
|
+
*/
|
|
217
|
+
/**
|
|
218
|
+
* This host's address as seen from a worker at `workerUrl`, when the two share a subnet.
|
|
219
|
+
* Returns undefined for a hostname, or for a worker somewhere we have no interface onto --
|
|
220
|
+
* in which case the caller must be told where to reach us rather than have it guessed.
|
|
221
|
+
*/
|
|
222
|
+
export function hostAddressForWorker(workerUrl) {
|
|
223
|
+
try {
|
|
224
|
+
const { hostname } = new URL(workerUrl);
|
|
225
|
+
return ipv4ToInt(hostname) === null ? undefined : hostAddressFor(hostname);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export function guestReachableUrl(baseUrl, lease) {
|
|
232
|
+
// Fall back to deriving it from the worker's own address.
|
|
233
|
+
//
|
|
234
|
+
// hostAddress used to be set only on the managed-VM path, so naming a worker explicitly --
|
|
235
|
+
// A11Y_WORKER, or any pool -- silently skipped the rewrite and every capture fetched the
|
|
236
|
+
// GUEST's localhost. Edge shows "localhost refused to connect", the title check rejects the
|
|
237
|
+
// capture, and three attempts are burned per page before it gives up. The worker being
|
|
238
|
+
// remote or explicit does not change the fact that it cannot reach our localhost.
|
|
239
|
+
const hostAddress = lease.hostAddress ?? hostAddressForWorker(lease.worker);
|
|
240
|
+
if (!hostAddress)
|
|
241
|
+
return baseUrl;
|
|
242
|
+
const parsed = new URL(baseUrl);
|
|
243
|
+
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1")
|
|
244
|
+
return baseUrl;
|
|
245
|
+
parsed.hostname = hostAddress;
|
|
246
|
+
return parsed.toString().replace(/\/$/, "");
|
|
247
|
+
}
|
|
248
|
+
/** Every local worker VM, whatever state each is in. */
|
|
249
|
+
async function findLocalPool() {
|
|
250
|
+
if (process.platform !== "darwin")
|
|
251
|
+
return [];
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(await ctl(["pool"], STATUS_TIMEOUT_MS));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** A11Y_MAX_WORKERS wins over the measurement: the operator may know something we cannot measure. */
|
|
260
|
+
function configuredWorkerLimit() {
|
|
261
|
+
const configured = Number(process.env.A11Y_MAX_WORKERS);
|
|
262
|
+
return Number.isFinite(configured) && configured > 0 ? configured : null;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Which registered VMs should actually be running for this run?
|
|
266
|
+
*
|
|
267
|
+
* The pool used to start all of them. Three guests on a 36 GB host made every capture 1.6x slower
|
|
268
|
+
* than one, and caused mute-NVDA failures, because the guests were swapped out from under NVDA — so
|
|
269
|
+
* "how many VMs exist" was the wrong question and "how much memory is there" is the right one.
|
|
270
|
+
*
|
|
271
|
+
* Already-running VMs are preferred over stopped ones: starting a guest in order to leave a running
|
|
272
|
+
* one idle is pure churn. Note that a VM someone else has already started is never stopped here — it
|
|
273
|
+
* is simply not used — because a run must not shut down a worker it did not start.
|
|
274
|
+
*/
|
|
275
|
+
function chooseRunnableWorkers(pool) {
|
|
276
|
+
const availableMb = availableHostMemoryMb();
|
|
277
|
+
const running = pool.filter((vm) => vm.state === "started");
|
|
278
|
+
const limit = configuredWorkerLimit()
|
|
279
|
+
?? workersHostCanRun({ availableMb, alreadyRunning: running.length });
|
|
280
|
+
if (limit >= pool.length)
|
|
281
|
+
return { chosen: pool, note: null };
|
|
282
|
+
const chosen = [...running, ...pool.filter((vm) => vm.state !== "started")].slice(0, limit);
|
|
283
|
+
return { chosen, note: capacityReason({ limit, wanted: pool.length, availableMb }) };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Lease every local worker VM: start what is not running, and put each one back afterwards.
|
|
287
|
+
*
|
|
288
|
+
* This exists because the pool path used to hand back a no-op release, so a pooled run left
|
|
289
|
+
* every VM running indefinitely — which is precisely the cost the single-worker lease was
|
|
290
|
+
* written to avoid, reintroduced the moment pooling became the normal way to run.
|
|
291
|
+
*
|
|
292
|
+
* Per-VM restore, not a blanket stop: a VM you had already started stays started, exactly as
|
|
293
|
+
* in the single-worker case. A long dataset run must not shut down a worker somebody else is
|
|
294
|
+
* using, and with a pool that is likelier, not less.
|
|
295
|
+
*/
|
|
296
|
+
export async function leaseWorkerPool(after) {
|
|
297
|
+
warnUtmDeprecated("the local UTM worker pool");
|
|
298
|
+
const pool = await findLocalPool();
|
|
299
|
+
if (pool.length < 2)
|
|
300
|
+
return null; // one VM is the single-worker path's job
|
|
301
|
+
const { chosen, note } = chooseRunnableWorkers(pool);
|
|
302
|
+
if (note)
|
|
303
|
+
process.stderr.write(note + "\n");
|
|
304
|
+
const chosenNames = new Set(chosen.map((vm) => vm.name));
|
|
305
|
+
const before = new Map(pool.map((vm) => [vm.name, vm.state]));
|
|
306
|
+
for (const vm of chosen) {
|
|
307
|
+
if (vm.state === "started" && vm.healthy)
|
|
308
|
+
continue;
|
|
309
|
+
process.stderr.write(`Local worker '${vm.name}' is ${vm.state}; bringing it up ...\n`);
|
|
310
|
+
try {
|
|
311
|
+
await ctl(["up"], LIFECYCLE_TIMEOUT_MS, vm.name);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
// One sick VM must not cancel the run. This threw and killed a resume outright while two other
|
|
315
|
+
// workers sat ready: a guest wedged in "stopping" failed `up`, the exception escaped the lease,
|
|
316
|
+
// and 1,000 cases went nowhere. Bringing up a pool is best-effort per member -- the readiness
|
|
317
|
+
// filter below is what decides who actually takes work, and it already fails loudly if nobody
|
|
318
|
+
// does.
|
|
319
|
+
process.stderr.write(`Local worker '${vm.name}' would not come up (${error.message.split("\n")[0]}); ` +
|
|
320
|
+
"continuing without it\n");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Only workers we CHOSE, even if a VM we declined is up and healthy: taking work to a guest the
|
|
324
|
+
// capacity check excluded would defeat the check, and it may belong to somebody else's run.
|
|
325
|
+
const ready = (await findLocalPool()).filter((vm) => vm.healthy && vm.ip && chosenNames.has(vm.name));
|
|
326
|
+
if (!ready.length)
|
|
327
|
+
throw new Error("no local worker became healthy; check: worker-ctl.sh pool");
|
|
328
|
+
const missing = chosen.length - ready.length;
|
|
329
|
+
if (missing > 0) {
|
|
330
|
+
// Never silent: a run that quietly used two of three workers looks like an unexplained slowdown.
|
|
331
|
+
process.stderr.write(`${missing} of ${chosen.length} local workers are unavailable; running on the rest\n`);
|
|
332
|
+
}
|
|
333
|
+
process.stderr.write(`Pool of ${ready.length}: ${ready.map((v) => v.name).join(", ")}\n`);
|
|
334
|
+
return {
|
|
335
|
+
workers: ready.map((vm) => `http://${vm.ip}:${vm.port}`),
|
|
336
|
+
hostAddress: hostAddressFor(ready[0].ip),
|
|
337
|
+
release: async () => {
|
|
338
|
+
for (const vm of ready) {
|
|
339
|
+
const action = after === "restore" ? restoreAction(before.get(vm.name) ?? "stopped") : after;
|
|
340
|
+
if (action === "leave")
|
|
341
|
+
continue;
|
|
342
|
+
try {
|
|
343
|
+
// Check per VM: another run may have picked this one up while ours was finishing.
|
|
344
|
+
const now = (await findLocalPool()).find((v) => v.name === vm.name);
|
|
345
|
+
if (now?.busy) {
|
|
346
|
+
process.stderr.write(`'${vm.name}' is busy with another capture; leaving it ${now.state}\n`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
process.stderr.write(`${action === "pause" ? "Pausing" : "Shutting down"} '${vm.name}' ...\n`);
|
|
350
|
+
await ctl([action], LIFECYCLE_TIMEOUT_MS, vm.name);
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
// One VM failing to stop must not strand the others.
|
|
354
|
+
process.stderr.write(`WARNING: could not ${action} '${vm.name}': ${e.message}\n`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=local-vm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-vm.js","sourceRoot":"","sources":["../src/local-vm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} workers
|
|
3
|
+
* @param {{ what: string, timeoutMs?: number, request?: import("./probe-outcome.mjs").ProbeRequest }} about
|
|
4
|
+
* `what` names the measurement, so a refusal says what was NOT measured; `timeoutMs` and `request` are injectable
|
|
5
|
+
* @returns {Promise<void>} resolves when every worker is free; throws naming the busy ones
|
|
6
|
+
*/
|
|
7
|
+
export function refuseIfBusy(workers: string[], { what, timeoutMs, request }: {
|
|
8
|
+
what: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
request?: import("./probe-outcome.mjs").ProbeRequest;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* A worker's `/health` `vitals`, or `null` when there are none to be had, and SAYS WHY through `warn`: a box that did
|
|
14
|
+
* not answer, one that refused, and one that answered without vitals are three different reasons for the same empty
|
|
15
|
+
* column in the report, and the run used to print none of them.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} worker
|
|
18
|
+
* @param {{ timeoutMs?: number, request?: import("./probe-outcome.mjs").ProbeRequest, warn?: (line: string) => void }} [options]
|
|
19
|
+
* @returns {Promise<any>}
|
|
20
|
+
*/
|
|
21
|
+
export function sampleVitals(worker: string, { timeoutMs, request, warn }?: {
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
request?: import("./probe-outcome.mjs").ProbeRequest;
|
|
24
|
+
warn?: (line: string) => void;
|
|
25
|
+
}): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* THE VITALS PROBE'S TIMEOUT, WITH ITS READING (#2683). `worker:compare` reads `vitals` before the rounds and again
|
|
28
|
+
* straight AFTER the last capture, so the second read is the LOADED case: a box that has just finished one can take
|
|
29
|
+
* up to about 10 s to answer (#2671, the reading beside `WORKER_PROBE_TIMEOUT_MS`), against 3.09 s for the slowest
|
|
30
|
+
* healthy first-after-idle answer. 20 s is that ceiling twice over, kept from before #2683 because it already clears
|
|
31
|
+
* both. A vitals sample that times out costs the run one column, so there is no reason to give it less.
|
|
32
|
+
*/
|
|
33
|
+
export const VITALS_TIMEOUT_MS: 20000;
|
|
34
|
+
//# sourceMappingURL=measure-guard.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure-guard.d.mts","sourceRoot":"","sources":["../src/measure-guard.mjs"],"names":[],"mappings":"AAsBA;;;;;GAKG;AACH,sCALW,MAAM,EAAE,gCACR;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,YAAY,CAAA;CAAE,GAExF,OAAO,CAAC,IAAI,CAAC,CAmBzB;AAWD;;;;;;;;GAQG;AACH,qCAJW,MAAM,iCACN;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,YAAY,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACzG,OAAO,CAAC,GAAG,CAAC,CASxB;AAzBD;;;;;;GAMG;AACH,gCAAiC,KAAM,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* REFUSE TO MEASURE A BOX THAT IS BUSY — because the rule alone does not work.
|
|
4
|
+
*
|
|
5
|
+
* "Do not measure while something else is running" is written in `capture-protocol-plan.md`, by the person
|
|
6
|
+
* who then broke it FOUR times in two days:
|
|
7
|
+
*
|
|
8
|
+
* a page-status audit against :5050 while `capture:check` held it with a different root -> 4 false 404s
|
|
9
|
+
* a transport probe against a box running a gate -> 12 consecutive 429s read as timings
|
|
10
|
+
* an idle-reap measurement that timed `keepAliveTimeout` instead of the capture case
|
|
11
|
+
* a capture-reap measurement against a busy fleet -> "responses" at 0.0-1.6 s, which are 429s
|
|
12
|
+
*
|
|
13
|
+
* Every one produced a NUMBER, which is what makes it dangerous: a measurement that fails loudly is
|
|
14
|
+
* harmless, and one that returns a plausible wrong figure gets believed. Three of the four were caught only
|
|
15
|
+
* because the number looked odd afterwards.
|
|
16
|
+
*
|
|
17
|
+
* So this is the repo's own housekeeping rule applied to measurement: anything a human has to remember is
|
|
18
|
+
* something that does not happen. A measurement script asks the box whether it is free FIRST, and refuses
|
|
19
|
+
* with the reason rather than sampling whatever a busy worker happens to say.
|
|
20
|
+
*/
|
|
21
|
+
import { describeProbe, probeHealth, WORKER_PROBE_TIMEOUT_MS } from "./probe-outcome.mjs";
|
|
22
|
+
/**
|
|
23
|
+
* @param {string[]} workers
|
|
24
|
+
* @param {{ what: string, timeoutMs?: number, request?: import("./probe-outcome.mjs").ProbeRequest }} about
|
|
25
|
+
* `what` names the measurement, so a refusal says what was NOT measured; `timeoutMs` and `request` are injectable
|
|
26
|
+
* @returns {Promise<void>} resolves when every worker is free; throws naming the busy ones
|
|
27
|
+
*/
|
|
28
|
+
export async function refuseIfBusy(workers, { what, timeoutMs = WORKER_PROBE_TIMEOUT_MS, request }) {
|
|
29
|
+
const states = await Promise.all(workers.map(async (worker) => {
|
|
30
|
+
const probe = await probeHealth(worker, { timeoutMs, request });
|
|
31
|
+
// `ready` is the right field, not `ok`: `ok` only ever meant "the HTTP server is answering", and a
|
|
32
|
+
// worker answered it while NVDA could not start. `ready` is false while a capture holds the box.
|
|
33
|
+
if (probe.outcome === "ready")
|
|
34
|
+
return { worker, free: true, why: "" };
|
|
35
|
+
// NOT ANSWERING IS NOT FREE, AND IT IS NOT "DOWN" EITHER. Measuring against a box that cannot answer /health
|
|
36
|
+
// produces exactly the kind of plausible-looking nonsense this guard exists to prevent, so it refuses; but
|
|
37
|
+
// what it says is the observed thing (#2683), one sentence per outcome from `describeProbe`.
|
|
38
|
+
return { worker, free: false, why: describeProbe(probe, { worker, timeoutMs }) };
|
|
39
|
+
}));
|
|
40
|
+
const busy = states.filter((s) => !s.free);
|
|
41
|
+
if (!busy.length)
|
|
42
|
+
return;
|
|
43
|
+
throw new Error(`REFUSING to measure ${what}: ${busy.length} of ${workers.length} worker(s) are not free `
|
|
44
|
+
+ `— ${busy.map((b) => b.why).join(" ")} A measurement taken against a busy box `
|
|
45
|
+
+ "samples its 429s, not its behaviour, and returns a number that looks real. Wait, or pass a worker "
|
|
46
|
+
+ "that is free.");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* THE VITALS PROBE'S TIMEOUT, WITH ITS READING (#2683). `worker:compare` reads `vitals` before the rounds and again
|
|
50
|
+
* straight AFTER the last capture, so the second read is the LOADED case: a box that has just finished one can take
|
|
51
|
+
* up to about 10 s to answer (#2671, the reading beside `WORKER_PROBE_TIMEOUT_MS`), against 3.09 s for the slowest
|
|
52
|
+
* healthy first-after-idle answer. 20 s is that ceiling twice over, kept from before #2683 because it already clears
|
|
53
|
+
* both. A vitals sample that times out costs the run one column, so there is no reason to give it less.
|
|
54
|
+
*/
|
|
55
|
+
export const VITALS_TIMEOUT_MS = 20_000;
|
|
56
|
+
/**
|
|
57
|
+
* A worker's `/health` `vitals`, or `null` when there are none to be had, and SAYS WHY through `warn`: a box that did
|
|
58
|
+
* not answer, one that refused, and one that answered without vitals are three different reasons for the same empty
|
|
59
|
+
* column in the report, and the run used to print none of them.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} worker
|
|
62
|
+
* @param {{ timeoutMs?: number, request?: import("./probe-outcome.mjs").ProbeRequest, warn?: (line: string) => void }} [options]
|
|
63
|
+
* @returns {Promise<any>}
|
|
64
|
+
*/
|
|
65
|
+
export async function sampleVitals(worker, { timeoutMs = VITALS_TIMEOUT_MS, request, warn = () => { } } = {}) {
|
|
66
|
+
const probe = await probeHealth(worker, { timeoutMs, request });
|
|
67
|
+
if (probe.outcome === "refused" || probe.outcome === "no-answer") {
|
|
68
|
+
warn(` no vitals sampled: ${describeProbe(probe, { worker, timeoutMs })}\n`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return probe.health?.vitals ?? null;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=measure-guard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure-guard.mjs","sourceRoot":"","sources":["../src/measure-guard.mjs"],"names":[],"mappings":"AAAA,YAAY;AACZ;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE1F;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,GAAG,uBAAuB,EAAE,OAAO,EAAE;IAChG,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QAC5D,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;QAChE,mGAAmG;QACnG,iGAAiG;QACjG,IAAI,KAAK,CAAC,OAAO,KAAK,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;QACtE,6GAA6G;QAC7G,2GAA2G;QAC3G,6FAA6F;QAC7F,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACnF,CAAC,CAAC,CAAC,CAAC;IACJ,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO;IACzB,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,KAAK,IAAI,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM,0BAA0B;UACtG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,0CAA0C;UAC/E,oGAAoG;UACpG,eAAe,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAM,EAAE,EAAE,SAAS,GAAG,iBAAiB,EAAE,OAAO,EAAE,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,EAAE,GAAG,EAAE;IACzG,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IAChE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC;QACjE,IAAI,CAAC,wBAAwB,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC;QAC9E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,EAAE,MAAM,IAAI,IAAI,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalise-fleet.d.mts","sourceRoot":"","sources":["../src/normalise-fleet.mjs"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Bring every local guest to the same baseline, elevated, and prove it took.
|
|
3
|
+
//
|
|
4
|
+
// npm run fleet:normalise
|
|
5
|
+
//
|
|
6
|
+
// Fleet CONSISTENCY is the point, more than any individual setting. Measured today:
|
|
7
|
+
// StartupBoostEnabled was 1 on two guests and 0 on a third, and Edge had auto-updated from 150 to 151
|
|
8
|
+
// on one before the others. Two guests with different browser behaviour producing into one corpus is
|
|
9
|
+
// the same class of problem as a mixed OS image -- the cache key cannot see it, and the evidence is
|
|
10
|
+
// quietly heterogeneous.
|
|
11
|
+
//
|
|
12
|
+
// Runs scripts/guest/normalise-fleet.cmd on each guest through guest-run.mjs, which is the only
|
|
13
|
+
// channel that can do elevated work on these VMs (see that file for why the obvious ones cannot).
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
18
|
+
import { warnUtmDeprecated } from "./utm-deprecated.mjs";
|
|
19
|
+
/**
|
|
20
|
+
* takes no flags: it brings every local guest to one baseline.
|
|
21
|
+
*
|
|
22
|
+
* An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
|
|
23
|
+
*/
|
|
24
|
+
refuseUnknownFlags([], { entry: import.meta.url, command: "npm run fleet:normalise" });
|
|
25
|
+
// A SIBLING in this package, resolved from this module. It was the cwd-relative path `scripts/guest-run.mjs`,
|
|
26
|
+
// which stopped existing when M8 moved the fleet tooling — and was only ever right when the cwd happened to be
|
|
27
|
+
// the repo root.
|
|
28
|
+
const GUEST_RUN = fileURLToPath(new URL("./guest-run.mjs", import.meta.url));
|
|
29
|
+
const run = promisify(execFile);
|
|
30
|
+
const UTMCTL = "/Applications/UTM.app/Contents/MacOS/utmctl";
|
|
31
|
+
// Only when RUN, never on import. Unguarded, importing this file SHELLED OUT to `utmctl list` and then
|
|
32
|
+
// drove DISM across every registered guest -- so `node -e "import('./normalise-fleet.mjs')"`, which
|
|
33
|
+
// CLAUDE.md makes the only real check that this file still loads, normalised the fleet as a side effect.
|
|
34
|
+
//
|
|
35
|
+
// This file has already been broken once by a path that nothing exercised: it read
|
|
36
|
+
// `scripts/guest/normalise-fleet.cmd`, which the packages/ restructure moved, and `npm run fleet:normalise`
|
|
37
|
+
// stayed broken while `doctor` printed it as the FIX for a failed consistency check. The import check is
|
|
38
|
+
// what catches that class, so it has to be safe to run.
|
|
39
|
+
async function main() {
|
|
40
|
+
warnUtmDeprecated("npm run fleet:normalise");
|
|
41
|
+
const { stdout } = await run(UTMCTL, ["list"]);
|
|
42
|
+
const vms = stdout.split("\n").slice(1)
|
|
43
|
+
.map((l) => l.trim().split(/\s+/))
|
|
44
|
+
.filter((c) => c.length >= 3 && c[2].startsWith("a11y-worker"))
|
|
45
|
+
.map((c) => ({ uuid: c[0], state: c[1], name: c[2] }));
|
|
46
|
+
if (!vms.length) {
|
|
47
|
+
process.stderr.write("no a11y-worker VMs registered\n");
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
// Resolved from THIS module, never the cwd. It read `scripts/guest/normalise-fleet.cmd` -- a path the
|
|
51
|
+
// packages/ restructure moved and nothing noticed, so `npm run fleet:normalise` has been broken, and
|
|
52
|
+
// doctor printed it as the FIX for a failed consistency check. A path spelled relative to the working
|
|
53
|
+
// directory is a path that breaks the moment anyone runs the command from somewhere else.
|
|
54
|
+
const script = fileURLToPath(new URL("./guest/normalise-fleet.cmd", import.meta.url));
|
|
55
|
+
const failures = [];
|
|
56
|
+
for (const vm of vms) {
|
|
57
|
+
// One at a time. Three guests doing DISM and service work simultaneously is exactly the disk
|
|
58
|
+
// contention this project spent a day diagnosing.
|
|
59
|
+
try {
|
|
60
|
+
const { stdout: out } = await run("node", [GUEST_RUN, vm.name, script, "--timeout=900"], { maxBuffer: 1 << 24 });
|
|
61
|
+
process.stdout.write(out);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
failures.push(vm.name);
|
|
65
|
+
process.stdout.write(`==> ${vm.name}: FAILED — ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(`\n${vms.length - failures.length}/${vms.length} guest(s) normalised` +
|
|
69
|
+
(failures.length ? `; failed: ${failures.join(", ")}\n` : "\n"));
|
|
70
|
+
process.stdout.write("Verify with: npm run doctor\n");
|
|
71
|
+
if (failures.length)
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href)
|
|
75
|
+
await main();
|
|
76
|
+
//# sourceMappingURL=normalise-fleet.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalise-fleet.mjs","sourceRoot":"","sources":["../src/normalise-fleet.mjs"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {"npx" | "npm"} name
|
|
3
|
+
* @returns {string[]}
|
|
4
|
+
*/
|
|
5
|
+
export function npmCliScriptCandidates(name: "npx" | "npm"): string[];
|
|
6
|
+
/**
|
|
7
|
+
* @param {"npx" | "npm"} name
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function resolveNpmCliScript(name: "npx" | "npm"): string;
|
|
11
|
+
/**
|
|
12
|
+
* @param {"npx" | "npm"} name
|
|
13
|
+
* @param {string[]} args
|
|
14
|
+
* @returns {{ command: string, args: string[] }}
|
|
15
|
+
*/
|
|
16
|
+
export function npmCliInvocation(name: "npx" | "npm", args: string[]): {
|
|
17
|
+
command: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* #2301: `pnpm <args>` WITHOUT SPAWNING A `.cmd`, for the same reason `npmCliInvocation` exists, and with the
|
|
22
|
+
* added problem that pnpm is not always ON `PATH` at all: this host and the Windows workers run it as
|
|
23
|
+
* `corepack pnpm` (`packageManager` in the root manifest pins the version), while a CI runner has the
|
|
24
|
+
* shim `pnpm/action-setup` puts there. Three ways to reach it, tried in this order, each ending in an argv
|
|
25
|
+
* that `execFileSync` can run with no shell:
|
|
26
|
+
*
|
|
27
|
+
* 1. `npm_execpath`, when this process was itself started BY pnpm (`pnpm run ...`, `pnpm exec ...`): pnpm
|
|
28
|
+
* says which script it is, so nothing is searched for and no other pnpm can be picked up by mistake.
|
|
29
|
+
* 2. a `pnpm` on `PATH`: the executable itself on POSIX; on Windows the shim is `pnpm.cmd`, so the
|
|
30
|
+
* `pnpm.cjs` beside it is run through `process.execPath` instead.
|
|
31
|
+
* 3. `corepack` on `PATH`, the same way: `corepack pnpm` on POSIX, `corepack.js` beside `node.exe`
|
|
32
|
+
* through `process.execPath` on Windows.
|
|
33
|
+
*
|
|
34
|
+
* Throws NAMING WHAT WAS TRIED, for the reason `resolveNpmCliScript` does.
|
|
35
|
+
* @param {string[]} args
|
|
36
|
+
* @returns {{ command: string, args: string[] }}
|
|
37
|
+
*/
|
|
38
|
+
export function pnpmCliInvocation(args: string[]): {
|
|
39
|
+
command: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=npm-cli-executable.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"npm-cli-executable.d.mts","sourceRoot":"","sources":["../src/npm-cli-executable.mjs"],"names":[],"mappings":"AAkCA;;;GAGG;AACH,6CAHW,KAAK,GAAG,KAAK,GACX,MAAM,EAAE,CAWpB;AAwBD;;;GAGG;AACH,0CAHW,KAAK,GAAG,KAAK,GACX,MAAM,CAUlB;AAED;;;;GAIG;AACH,uCAJW,KAAK,GAAG,KAAK,QACb,MAAM,EAAE,GACN;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAI/C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wCAHW,MAAM,EAAE,GACN;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAgB/C"}
|