@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,202 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Create the NVDA worker VM in UTM, fully from the CLI. No GUI clicking.
|
|
3
|
+
#
|
|
4
|
+
# ./packages/worker-fleet/src/local-worker/create-utm-vm.sh <windows-arm64.iso> [support.iso]
|
|
5
|
+
#
|
|
6
|
+
# Why UTM rather than plain QEMU: homebrew QEMU + HVF cannot boot Windows 11 ARM64 on
|
|
7
|
+
# Apple Silicon (open upstream bug, https://gitlab.com/qemu-project/qemu/-/issues/2893,
|
|
8
|
+
# reproduced here across five machine configurations). UTM ships a QEMU patched for
|
|
9
|
+
# Windows-on-ARM, but it is a dlopen'd framework, not an executable, so it cannot be
|
|
10
|
+
# driven from a shell.
|
|
11
|
+
#
|
|
12
|
+
# The way in is UTM's scripting interface, which DOES support creation even though
|
|
13
|
+
# `utmctl` has no `create` subcommand:
|
|
14
|
+
# make new virtual machine with properties {backend:qemu, configuration:{...}}
|
|
15
|
+
# (see /Applications/UTM.app/Contents/Resources/UTM.sdef -- the `make` command takes a
|
|
16
|
+
# `qemu configuration` record).
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
# architecture-audit.md §8: creates a local UTM worker VM, which is deprecated -- "The UTM is deprecated,
|
|
20
|
+
# that was a testing thing." (repository owner, 2026-09-05). Capture on the bare-metal fleet instead:
|
|
21
|
+
# npm run fleet:status, npm run fleet:deploy.
|
|
22
|
+
echo "DEPRECATED: create-utm-vm.sh creates a local UTM worker VM. UTM was a testing path and is not the fleet." >&2
|
|
23
|
+
echo "Capture on the bare-metal fleet instead: npm run fleet:status, npm run fleet:deploy." >&2
|
|
24
|
+
|
|
25
|
+
# #636: a warning on a path nobody watches is a warning nobody reads -- so this REFUSES, rather than
|
|
26
|
+
# merely printing the two lines above and continuing. The measurements and reasoning behind the local-VM
|
|
27
|
+
# work stay in docs/local-worker-vm.md as history; only the path that runs is fenced.
|
|
28
|
+
if [ "${A11Y_LOCAL_VM:-}" != "1" ]; then
|
|
29
|
+
echo "refusing: set A11Y_LOCAL_VM=1 to run this deprecated local-VM script anyway." >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
WIN_ISO="${1:-}"
|
|
34
|
+
SUPPORT_ISO="${2:-$HOME/a11y-worker-vm/support.iso}"
|
|
35
|
+
VM_NAME="${A11Y_VM_NAME:-a11y-worker}"
|
|
36
|
+
# 4 GB: the documented Windows 11 minimum, and measured sufficient. Driving Edge and NVDA is
|
|
37
|
+
# not memory-intensive. Verified by running the same 10 cases on 4 GB and 8 GB VMs -- 165s vs
|
|
38
|
+
# 167s, byte-identical evidence (62 phrases, 51 role words, 22 heading-levels), and ZERO
|
|
39
|
+
# pagefile use on either guest, so nothing is being paged to fake the result.
|
|
40
|
+
#
|
|
41
|
+
# Do not size this from Windows' "in use" figure: that includes the file cache, which grows to
|
|
42
|
+
# fill whatever it is given. An 8 GB guest reported 3.5 GB "in use" and needed less than half
|
|
43
|
+
# of it. Committed bytes is the number that means anything.
|
|
44
|
+
RAM_MB="${A11Y_VM_RAM_MB:-4096}"
|
|
45
|
+
CPUS="${A11Y_VM_CPUS:-4}"
|
|
46
|
+
DISK_MB="${A11Y_VM_DISK_MB:-65536}"
|
|
47
|
+
|
|
48
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
49
|
+
info() { echo "==> $*"; }
|
|
50
|
+
|
|
51
|
+
[ -n "$WIN_ISO" ] || die "usage: $0 <windows-arm64.iso> [support.iso]"
|
|
52
|
+
[ -f "$WIN_ISO" ] || die "not found: $WIN_ISO"
|
|
53
|
+
[ -f "$SUPPORT_ISO" ] || die "support ISO not found: $SUPPORT_ISO (run build-vm.sh first)"
|
|
54
|
+
[ -d /Applications/UTM.app ] || die "UTM not installed: brew install --cask utm"
|
|
55
|
+
|
|
56
|
+
DOCS="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents"
|
|
57
|
+
BUNDLE="$DOCS/$VM_NAME.utm"
|
|
58
|
+
[ -e "$BUNDLE" ] && die "$BUNDLE already exists. Remove it first: utmctl delete $VM_NAME"
|
|
59
|
+
|
|
60
|
+
# Refuse to create a SECOND registration with this name. Two registrations end up pointing
|
|
61
|
+
# at the same $VM_NAME.utm bundle, and then `utmctl delete` on EITHER of them removes that
|
|
62
|
+
# shared directory -- taking the other VM's disk and UEFI vars with it. That is not a
|
|
63
|
+
# name-resolution problem you can dodge by using UUIDs: it destroyed a fully provisioned
|
|
64
|
+
# worker here, and the only symptom afterwards is a start failing with
|
|
65
|
+
# 'The file "edk2-arm-vars.fd" doesn't exist'.
|
|
66
|
+
EXISTING_REG="$(utmctl list 2>/dev/null | awk -v n="$VM_NAME" '$3 == n { print $1 }' || true)"
|
|
67
|
+
if [ -n "$EXISTING_REG" ]; then
|
|
68
|
+
# Note we only get here when the bundle is ALREADY gone (checked just above), so this is
|
|
69
|
+
# an orphaned registration -- and `utmctl delete` on one of those fails with -2700 and
|
|
70
|
+
# leaves the entry behind. Editing UTM's registry is the route that works.
|
|
71
|
+
die "UTM already has a VM registered as '$VM_NAME', but its bundle is gone:
|
|
72
|
+
$EXISTING_REG
|
|
73
|
+
Clear the stale registration first -- see 'Never utmctl delete a VM whose name appears
|
|
74
|
+
twice' in docs/local-worker-vm.md for the PlistBuddy recipe. Do NOT use 'utmctl delete':
|
|
75
|
+
registrations sharing a name share one bundle, so it can destroy a working VM's disk.
|
|
76
|
+
Or pass A11Y_VM_NAME=<other> to build alongside the existing one."
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# UTM is sandboxed and will not follow a symlink out to /private/tmp, so give it real
|
|
80
|
+
# files in the user's home.
|
|
81
|
+
STAGE="$HOME/a11y-worker-vm"
|
|
82
|
+
mkdir -p "$STAGE"
|
|
83
|
+
if [ "$(cd "$(dirname "$WIN_ISO")" && pwd)/$(basename "$WIN_ISO")" != "$STAGE/windows.iso" ]; then
|
|
84
|
+
info "Staging the Windows ISO into $STAGE (UTM's sandbox cannot read /private/tmp)"
|
|
85
|
+
cp "$WIN_ISO" "$STAGE/windows.iso"
|
|
86
|
+
fi
|
|
87
|
+
[ -f "$STAGE/support.iso" ] || cp "$SUPPORT_ISO" "$STAGE/support.iso"
|
|
88
|
+
|
|
89
|
+
info "Launching UTM"
|
|
90
|
+
open -a UTM
|
|
91
|
+
for _ in $(seq 1 20); do pgrep -x UTM >/dev/null && break; sleep 1; done
|
|
92
|
+
pgrep -x UTM >/dev/null || die "UTM did not start"
|
|
93
|
+
sleep 3
|
|
94
|
+
|
|
95
|
+
info "Creating the VM via UTM's scripting interface"
|
|
96
|
+
# Three things here are load-bearing and were each found the hard way:
|
|
97
|
+
#
|
|
98
|
+
# 1. `displays` is NOT optional. Omit it and UTM builds the VM with no graphics adapter
|
|
99
|
+
# at all (`-vga none -nographic` in the launch line); Windows Setup cannot run
|
|
100
|
+
# without a framebuffer and the guest just sits there. virtio-ramfb is a plain UEFI
|
|
101
|
+
# framebuffer, which is what you want: Windows 11 ARM64 has no inbox virtio-gpu
|
|
102
|
+
# driver, and UTM disables the viogpu guest driver anyway (it causes a black screen).
|
|
103
|
+
#
|
|
104
|
+
# 2. DRIVE ORDER IS BOOT ORDER, and the SYSTEM DISK MUST COME FIRST. UTM assigns
|
|
105
|
+
# `bootindex` by position. Firmware skips the empty disk (no bootloader on it yet),
|
|
106
|
+
# boots the ISO, installs -- and then, crucially, Setup REBOOTS. If the ISO were
|
|
107
|
+
# first, that reboot starts the installer again from scratch, forever. With the disk
|
|
108
|
+
# first, the reboot finds the freshly written bootloader and Setup continues into its
|
|
109
|
+
# specialize/oobeSystem phases.
|
|
110
|
+
#
|
|
111
|
+
# Do not "fix" a boot failure by moving the ISO first: if firmware drops to the EDK2
|
|
112
|
+
# UEFI Shell with the disk first, the real cause is that the ISO has no valid UEFI
|
|
113
|
+
# El Torito record (see fetch-windows-iso.sh), not the ordering.
|
|
114
|
+
#
|
|
115
|
+
# 3. Do not try to drive that shell with `input keystroke`: it routes through the SPICE
|
|
116
|
+
# guest agent, which does not exist in UEFI, so the text silently never arrives
|
|
117
|
+
# (`input scan code` does reach the hardware, and is the only way to send keys before
|
|
118
|
+
# an OS is installed).
|
|
119
|
+
VM_ID="$(osascript <<APPLESCRIPT
|
|
120
|
+
tell application "UTM"
|
|
121
|
+
set vm to make new virtual machine with properties {backend:qemu, configuration:{name:"$VM_NAME", architecture:"aarch64", machine:"virt", memory:$RAM_MB, cpu cores:$CPUS, hypervisor:true, uefi:true, displays:{{hardware:"virtio-ramfb"}}, network interfaces:{{mode:shared}}, drives:{{guest size:$DISK_MB, interface:NVMe}, {source:POSIX file "$STAGE/windows.iso", interface:USB}, {source:POSIX file "$STAGE/support.iso", interface:USB}}}}
|
|
122
|
+
return id of vm
|
|
123
|
+
end tell
|
|
124
|
+
APPLESCRIPT
|
|
125
|
+
)"
|
|
126
|
+
[ -n "$VM_ID" ] || die "VM creation returned no id"
|
|
127
|
+
info "created $VM_NAME ($VM_ID)"
|
|
128
|
+
|
|
129
|
+
# UTM's scripting layer imports an ISO given as `source` by CONVERTING it to a qcow2
|
|
130
|
+
# and attaching it as a fixed Disk -- and `removable` is read-only in the sdef, so this
|
|
131
|
+
# cannot be expressed at creation time. Windows Setup will not read autounattend.xml off
|
|
132
|
+
# a fixed disk, and the windowsPE driver injection depends on that medium, so rewrite
|
|
133
|
+
# the two entries as real read-only CDs. UTM caches configs in memory, hence the quit.
|
|
134
|
+
#
|
|
135
|
+
# Keep such edits to values of keys UTM already wrote. UTM decodes config.plist with
|
|
136
|
+
# strict Swift Codable: one unexpected value and the VM silently DISAPPEARS from
|
|
137
|
+
# `utmctl list` with no error anywhere. (Learned by writing lowercase "linear" where UTM
|
|
138
|
+
# writes "Linear" for a display filter.) Anything structural should go through the
|
|
139
|
+
# scripting interface instead, which validates and fills defaults.
|
|
140
|
+
info "Rewriting the ISO drives as CD-ROMs (not expressible via scripting)"
|
|
141
|
+
osascript -e 'tell application "UTM" to quit' >/dev/null 2>&1 || true
|
|
142
|
+
for _ in $(seq 1 20); do pgrep -x UTM >/dev/null || break; sleep 1; done
|
|
143
|
+
# #635: the loop above exits EITHER because UTM quit OR because 20s ran out -- and the edit below only
|
|
144
|
+
# survives if UTM is actually gone (it caches config.plist in memory and would overwrite the edit on its
|
|
145
|
+
# own next flush). Refuse rather than silently proceeding into an edit that would be lost.
|
|
146
|
+
pgrep -x UTM >/dev/null && die "UTM did not quit; the config.plist edit below would be lost to its cache"
|
|
147
|
+
sleep 2
|
|
148
|
+
|
|
149
|
+
cp "$STAGE/windows.iso" "$BUNDLE/Data/windows.iso"
|
|
150
|
+
cp "$STAGE/support.iso" "$BUNDLE/Data/support.iso"
|
|
151
|
+
python3 - "$BUNDLE/config.plist" <<'PY'
|
|
152
|
+
import plistlib, sys
|
|
153
|
+
p = sys.argv[1]
|
|
154
|
+
d = plistlib.load(open(p, 'rb'))
|
|
155
|
+
for drive in d['Drive']:
|
|
156
|
+
name = drive.get('ImageName', '')
|
|
157
|
+
if name.startswith('windows'):
|
|
158
|
+
drive.update(ImageName='windows.iso', ImageType='CD', ReadOnly=True)
|
|
159
|
+
elif name.startswith('support'):
|
|
160
|
+
drive.update(ImageName='support.iso', ImageType='CD', ReadOnly=True)
|
|
161
|
+
plistlib.dump(d, open(p, 'wb'))
|
|
162
|
+
for drive in d['Drive']:
|
|
163
|
+
print(" %-12s %-6s %s" % (drive['ImageType'], drive['Interface'], drive['ImageName']))
|
|
164
|
+
PY
|
|
165
|
+
# Reclaim the qcow2 copies UTM made of both ISOs (~4.5 GB).
|
|
166
|
+
rm -f "$BUNDLE/Data/windows.qcow2" "$BUNDLE/Data/support.qcow2"
|
|
167
|
+
|
|
168
|
+
info "Starting the VM (Windows installs unattended from support.iso)"
|
|
169
|
+
open -a UTM
|
|
170
|
+
for _ in $(seq 1 20); do pgrep -x UTM >/dev/null && break; sleep 1; done
|
|
171
|
+
# #635: without this, a UTM that never relaunches falls straight through into `utmctl start`, which
|
|
172
|
+
# (per this file's own documented quirks) may report a misleading `unknown` state instead of a clear
|
|
173
|
+
# failure -- die here, with the actual cause, instead.
|
|
174
|
+
pgrep -x UTM >/dev/null || die "UTM did not relaunch before starting the VM"
|
|
175
|
+
sleep 3
|
|
176
|
+
# Operate by UUID, never by name. If two registrations ever share a name, `utmctl start
|
|
177
|
+
# <name>` silently picks the wrong one -- and `utmctl delete <name>` will remove the
|
|
178
|
+
# SHARED bundle directory, destroying the other VM's disk with it.
|
|
179
|
+
utmctl start "$VM_ID"
|
|
180
|
+
utmctl status "$VM_ID"
|
|
181
|
+
|
|
182
|
+
DISK="$(ls "$BUNDLE/Data"/*.qcow2 | head -1)"
|
|
183
|
+
cat <<EOF
|
|
184
|
+
|
|
185
|
+
--- $VM_NAME started ---
|
|
186
|
+
|
|
187
|
+
The install is unattended. There is no console screenshot from the CLI, so track it by
|
|
188
|
+
side effect -- Windows only writes to the system disk once Setup is really running:
|
|
189
|
+
|
|
190
|
+
watch: du -h "$DISK"
|
|
191
|
+
(WinPE decompresses boot.wim into RAM first, so expect a few minutes at 0 growth)
|
|
192
|
+
|
|
193
|
+
When it is up:
|
|
194
|
+
utmctl query ip $VM_ID # needs qemu-ga, installed at first logon
|
|
195
|
+
curl http://<guest-ip>:8765/health
|
|
196
|
+
A11Y_WORKER=http://<guest-ip>:8765 npm run witness -- https://example.com --task "..."
|
|
197
|
+
|
|
198
|
+
Guest commands without SSH (qemu-ga, via UTM):
|
|
199
|
+
utmctl exec $VM_ID --cmd "powershell -Command Get-Process node"
|
|
200
|
+
|
|
201
|
+
Troubleshooting the worker itself: docs/nvda-worker-runbook.md
|
|
202
|
+
EOF
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build an official Windows 11 ARM64 ISO on macOS, from the CLI.
|
|
3
|
+
#
|
|
4
|
+
# ./packages/worker-fleet/src/local-worker/fetch-windows-iso.sh [outdir]
|
|
5
|
+
#
|
|
6
|
+
# Microsoft's ARM64 ISO download is a session-token web flow that does not script
|
|
7
|
+
# cleanly, so this uses UUP dump: it fetches the same Unified Update Platform packages
|
|
8
|
+
# from Microsoft's update servers and assembles them locally. The bits are official;
|
|
9
|
+
# the assembly is local.
|
|
10
|
+
#
|
|
11
|
+
# The conversion needs five tools. Four are in homebrew-core. `chntpw` is NOT, and the
|
|
12
|
+
# only convenient macOS build is the universal one inside CrystalFetch.app -- which is
|
|
13
|
+
# signed against that bundle, so invoking it directly dies with SIGTRAP (exit 133)
|
|
14
|
+
# unless the bundle's OpenSSL.framework is on the framework path. Hence the shim below.
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# architecture-audit.md §8: builds an ISO for a local UTM worker VM, which is deprecated -- "The UTM is
|
|
18
|
+
# deprecated, that was a testing thing." (repository owner, 2026-09-05). Bare-metal boxes install via
|
|
19
|
+
# PXE/autounattend.xml instead — see packages/worker-fleet/src/provisioning/bare-metal/.
|
|
20
|
+
echo "DEPRECATED: fetch-windows-iso.sh feeds a local UTM worker VM build. UTM was a testing path and is not the fleet." >&2
|
|
21
|
+
echo "Capture on the bare-metal fleet instead: npm run fleet:status, npm run fleet:deploy." >&2
|
|
22
|
+
|
|
23
|
+
# #636: a warning on a path nobody watches is a warning nobody reads -- so this REFUSES, rather than
|
|
24
|
+
# merely printing the two lines above and continuing. The measurements and reasoning behind the local-VM
|
|
25
|
+
# work stay in docs/local-worker-vm.md as history; only the path that runs is fenced.
|
|
26
|
+
if [ "${A11Y_LOCAL_VM:-}" != "1" ]; then
|
|
27
|
+
echo "refusing: set A11Y_LOCAL_VM=1 to run this deprecated local-VM script anyway." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
OUT_DIR="${1:-$HOME/a11y-worker-vm/iso-build}"
|
|
32
|
+
EDITION="${A11Y_WIN_EDITION:-professional}"
|
|
33
|
+
LANG_CODE="${A11Y_WIN_LANG:-en-us}"
|
|
34
|
+
|
|
35
|
+
# Default to 23H2, NOT the newest build. Windows 11 24H2 replaced Setup with one that
|
|
36
|
+
# calls SetupPrep.exe, is far stricter about autounattend.xml, and frequently ignores it
|
|
37
|
+
# outright -- we saw the install boot fine and then stop dead on the language/region
|
|
38
|
+
# prompt, which is the symptom widely reported by others. 23H2 uses the classic Setup and
|
|
39
|
+
# honours the unattend file. Override with A11Y_WIN_VERSION if you want to retest 24H2.
|
|
40
|
+
WIN_VERSION="${A11Y_WIN_VERSION:-23H2}"
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
|
|
44
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
45
|
+
info() { echo "==> $*"; }
|
|
46
|
+
|
|
47
|
+
command -v brew >/dev/null || die "homebrew required"
|
|
48
|
+
|
|
49
|
+
info "Installing conversion tools"
|
|
50
|
+
for f in aria2 cabextract wimlib cdrtools; do
|
|
51
|
+
brew list --formula "$f" >/dev/null 2>&1 || brew install "$f"
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
CF_BIN="/Applications/CrystalFetch.app/Contents/MacOS/chntpw"
|
|
55
|
+
CF_FW="/Applications/CrystalFetch.app/Contents/Frameworks"
|
|
56
|
+
if [ ! -x "$CF_BIN" ]; then
|
|
57
|
+
info "chntpw not found; installing CrystalFetch (it bundles a universal build)"
|
|
58
|
+
brew install --cask crystalfetch
|
|
59
|
+
fi
|
|
60
|
+
[ -x "$CF_BIN" ] || die "chntpw unavailable and CrystalFetch install failed"
|
|
61
|
+
|
|
62
|
+
SHIM_DIR="$OUT_DIR/.tools"
|
|
63
|
+
mkdir -p "$SHIM_DIR"
|
|
64
|
+
cat > "$SHIM_DIR/chntpw" <<SHIM
|
|
65
|
+
#!/bin/bash
|
|
66
|
+
exec env DYLD_FRAMEWORK_PATH="$CF_FW" "$CF_BIN" "\$@"
|
|
67
|
+
SHIM
|
|
68
|
+
chmod +x "$SHIM_DIR/chntpw"
|
|
69
|
+
export PATH="$SHIM_DIR:$PATH"
|
|
70
|
+
|
|
71
|
+
for t in aria2c cabextract wimlib-imagex chntpw mkisofs; do
|
|
72
|
+
command -v "$t" >/dev/null || die "missing tool: $t"
|
|
73
|
+
done
|
|
74
|
+
info "toolchain ready"
|
|
75
|
+
|
|
76
|
+
info "Finding the newest RETAIL Windows 11 ARM64 build for $WIN_VERSION"
|
|
77
|
+
# The listing includes Insider Preview builds; filter to released versions so the worker
|
|
78
|
+
# is not sitting on a preview OS. Titles look like "Windows 11, version 23H2".
|
|
79
|
+
UUID="$(curl -sL --fail "https://api.uupdump.net/listid.php?search=windows%2011&sortByDate=1" \
|
|
80
|
+
| WANT="$WIN_VERSION" node -e '
|
|
81
|
+
let s=""; process.stdin.on("data",d=>s+=d).on("end",()=>{
|
|
82
|
+
const want = (process.env.WANT || "").toLowerCase();
|
|
83
|
+
const builds = JSON.parse(s)?.response?.builds || {};
|
|
84
|
+
const rows = Object.values(builds).filter(b =>
|
|
85
|
+
/arm64/i.test(b.arch || "") &&
|
|
86
|
+
/^Windows 11, version/i.test(b.title || "") &&
|
|
87
|
+
(b.title || "").toLowerCase().includes("version " + want));
|
|
88
|
+
if (!rows.length) { process.exit(1); }
|
|
89
|
+
console.log(rows[0].uuid);
|
|
90
|
+
});')"
|
|
91
|
+
[ -n "$UUID" ] || die "could not find a retail Windows 11 ARM64 $WIN_VERSION build"
|
|
92
|
+
info "build uuid: $UUID"
|
|
93
|
+
|
|
94
|
+
mkdir -p "$OUT_DIR"
|
|
95
|
+
cd "$OUT_DIR"
|
|
96
|
+
|
|
97
|
+
# Idempotence: if a finished ISO with a UEFI boot record is already here, stop. And if the
|
|
98
|
+
# converter's output is present, skip straight to the rebuild rather than re-downloading
|
|
99
|
+
# ~10 GB. Re-running this script should cost minutes, not an hour.
|
|
100
|
+
EXISTING="$(ls -t "$OUT_DIR"/*.ISO "$OUT_DIR"/*.iso 2>/dev/null | grep -vi support | head -1 || true)"
|
|
101
|
+
if [ -n "$EXISTING" ] && xorriso -indev "$EXISTING" -report_el_torito plain 2>/dev/null | grep -q UEFI; then
|
|
102
|
+
echo "ISO ready (already built): $EXISTING"
|
|
103
|
+
echo "Next: ./packages/worker-fleet/src/local-worker/create-utm-vm.sh \"$EXISTING\""
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
if [ -n "$EXISTING" ]; then
|
|
107
|
+
info "found an existing converter ISO; skipping download and going straight to the rebuild"
|
|
108
|
+
SKIP_DOWNLOAD=1
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
info "Fetching the UUP download+convert pack"
|
|
112
|
+
# autodl=2 is "download using aria2 and convert". autodl=3 is the virtual-editions
|
|
113
|
+
# flow and errors out unless you request extra editions.
|
|
114
|
+
curl -sL --fail -o pack.zip \
|
|
115
|
+
"https://uupdump.net/get.php?id=${UUID}&pack=${LANG_CODE}&edition=${EDITION}&autodl=2"
|
|
116
|
+
unzip -o -q pack.zip
|
|
117
|
+
chmod +x uup_download_macos.sh
|
|
118
|
+
|
|
119
|
+
# Soften aria2's fan-out before running it. The stock script uses -x16 -s16 across many
|
|
120
|
+
# hosts, which overwhelmed a resolver mid-download here: 42 x errorCode=19 ("Name
|
|
121
|
+
# resolution ... failed") and the run aborted twice with ~7 GB still to fetch. Fewer
|
|
122
|
+
# connections plus explicit resolvers and unlimited retries turns a DNS blip into a pause
|
|
123
|
+
# instead of a failure. Costs nothing on a healthy link; rescues a flaky one.
|
|
124
|
+
GENTLE='-x4 -s4 -j2 --async-dns-server=1.1.1.1,8.8.8.8,9.9.9.9 --max-tries=0 --retry-wait=5 --connect-timeout=30 --timeout=60'
|
|
125
|
+
sed -i '' "s/-x16 -s16 -j5/$GENTLE/; s/-x16 -s16 -j2/$GENTLE/" uup_download_macos.sh
|
|
126
|
+
grep -q 'async-dns-server' uup_download_macos.sh && info "aria2 settings softened for flaky links"
|
|
127
|
+
|
|
128
|
+
if [ "${SKIP_DOWNLOAD:-0}" = "1" ]; then
|
|
129
|
+
info "skipping download/convert (existing ISO found)"
|
|
130
|
+
else
|
|
131
|
+
info "Downloading (~10 GB) and converting. This is the slow part."
|
|
132
|
+
# aria2 resumes, so re-running after an interruption picks up where it left off.
|
|
133
|
+
./uup_download_macos.sh
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
ISO="$(ls -t "$OUT_DIR"/*.ISO "$OUT_DIR"/*.iso 2>/dev/null | grep -vi 'support\|fixed' | head -1 || true)"
|
|
137
|
+
[ -n "$ISO" ] || die "conversion finished but no ISO was produced (see the log in $OUT_DIR)"
|
|
138
|
+
info "converter produced: $ISO"
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# The converter's ISO is NOT UEFI-bootable. Its arm64 branch runs
|
|
142
|
+
# mkisofs -b efi/microsoft/boot/efisys.bin --no-emul-boot ...
|
|
143
|
+
# and `-b` registers a BIOS boot image (platform 0x00). A UEFI entry needs platform
|
|
144
|
+
# 0xEF, so firmware finds no bootable UEFI record, silently declines the disc, and drops
|
|
145
|
+
# to the EDK2 UEFI Shell -- which looks exactly like a hung VM. Confirm with:
|
|
146
|
+
# xorriso -indev <iso> -report_el_torito plain # want "Pltf: UEFI", not BIOS
|
|
147
|
+
#
|
|
148
|
+
# While rebuilding we also swap efisys.bin for efisys_noprompt.bin. The default image
|
|
149
|
+
# stops at "Press any key to boot from CD or DVD" and gives up if nobody presses one,
|
|
150
|
+
# which is fatal for an unattended install.
|
|
151
|
+
#
|
|
152
|
+
# NOTE: xorriso cannot do this job -- `xorriso -as mkisofs` rejects `-udf`, and UDF is
|
|
153
|
+
# required because install.wim exceeds 4 GB. cdrtools mkisofs supports both --udf and
|
|
154
|
+
# -eltorito-platform, so it is the builder here.
|
|
155
|
+
if xorriso -indev "$ISO" -report_el_torito plain 2>/dev/null | grep -q "UEFI"; then
|
|
156
|
+
info "ISO already has a UEFI El Torito record; leaving it alone"
|
|
157
|
+
else
|
|
158
|
+
info "Rebuilding the ISO with a UEFI El Torito record (the converter's is BIOS-only)"
|
|
159
|
+
STAGE_DIR="$OUT_DIR/isodir"
|
|
160
|
+
MNT="$OUT_DIR/mnt"
|
|
161
|
+
# ditto preserves the ISO's read-only bits, so a previous staging tree cannot be
|
|
162
|
+
# removed (or modified) without restoring write permission first -- otherwise a re-run
|
|
163
|
+
# dies on "rm: ...: Permission denied" and leaves a half-deleted tree behind.
|
|
164
|
+
[ -d "$STAGE_DIR" ] && chmod -R u+w "$STAGE_DIR" 2>/dev/null || true
|
|
165
|
+
rm -rf "$STAGE_DIR" "$MNT"; mkdir -p "$STAGE_DIR" "$MNT"
|
|
166
|
+
# Must copy via a macOS UDF mount: the converter passes `--hide "*"`, so every file
|
|
167
|
+
# lives ONLY in the UDF tree and an ISO9660 reader (including xorriso) sees an empty
|
|
168
|
+
# disc. boot.catalog is unreadable and is regenerated by mkisofs anyway.
|
|
169
|
+
hdiutil attach -readonly -nobrowse -mountpoint "$MNT" "$ISO" >/dev/null
|
|
170
|
+
ditto --norsrc --noextattr "$MNT" "$STAGE_DIR" 2>/dev/null || true
|
|
171
|
+
hdiutil detach "$MNT" >/dev/null
|
|
172
|
+
rmdir "$MNT" 2>/dev/null || true
|
|
173
|
+
# Same reason: the staged copy is read-only, and we need to write autounattend.xml into
|
|
174
|
+
# it and update boot.wim.
|
|
175
|
+
chmod -R u+w "$STAGE_DIR"
|
|
176
|
+
[ -f "$STAGE_DIR/sources/boot.wim" ] || die "extraction failed (no sources/boot.wim)"
|
|
177
|
+
|
|
178
|
+
UNATTEND="$SCRIPT_DIR/autounattend.xml"
|
|
179
|
+
|
|
180
|
+
# GUARD 1: language must match the media. The answer file's SetupUILanguage/UILanguage
|
|
181
|
+
# must name a language pack that actually exists in sources/. Ask for en-GB on an EN-US
|
|
182
|
+
# disc and Setup cannot load its UI language, silently abandons the whole answer file,
|
|
183
|
+
# and shows the interactive language prompt -- indistinguishable from "file not found".
|
|
184
|
+
# This one cost hours; it is two lines to check.
|
|
185
|
+
MEDIA_LANG="$(ls "$STAGE_DIR/sources" | grep -oiE '^[a-z]{2}-[a-z]{2}$' | head -1)"
|
|
186
|
+
[ -n "$MEDIA_LANG" ] || die "could not determine the media language from sources/"
|
|
187
|
+
WANTED_LANGS="$(grep -oE '<(SetupUILanguage>)?[[:space:]]*<?UILanguage>[a-zA-Z-]+' "$UNATTEND" \
|
|
188
|
+
| grep -oE '[a-z]{2}-[A-Z]{2}' | sort -u | tr '\n' ' ')"
|
|
189
|
+
for l in $WANTED_LANGS; do
|
|
190
|
+
if [ "$(echo "$l" | tr 'A-Z' 'a-z')" != "$(echo "$MEDIA_LANG" | tr 'A-Z' 'a-z')" ]; then
|
|
191
|
+
die "autounattend.xml asks for UI language '$l' but the media only has '$MEDIA_LANG'. Setup will ignore the answer file. Fix the locales in $UNATTEND."
|
|
192
|
+
fi
|
|
193
|
+
done
|
|
194
|
+
info "language check: unattend wants [$WANTED_LANGS], media has [$MEDIA_LANG]"
|
|
195
|
+
|
|
196
|
+
# GUARD 2: the image name must match the WIM's NAME (not its DESCRIPTION). Getting this
|
|
197
|
+
# wrong does not break the answer file -- Setup just stops on the edition picker.
|
|
198
|
+
if command -v wimlib-imagex >/dev/null; then
|
|
199
|
+
WIM_NAME="$(wimlib-imagex info "$STAGE_DIR/sources/install.wim" 2>/dev/null \
|
|
200
|
+
| awk -F': *' '/^Name:/{print $2; exit}')"
|
|
201
|
+
WANT_NAME="$(grep -A2 'IMAGE/NAME' "$UNATTEND" | grep -oE '<Value>[^<]+' | sed 's/<Value>//' | head -1)"
|
|
202
|
+
if [ -n "$WIM_NAME" ] && [ -n "$WANT_NAME" ] && [ "$WIM_NAME" != "$WANT_NAME" ]; then
|
|
203
|
+
die "autounattend.xml installs image '$WANT_NAME' but the WIM's Name is '$WIM_NAME' (its Description may differ). Fix /IMAGE/NAME in $UNATTEND."
|
|
204
|
+
fi
|
|
205
|
+
info "image check: unattend wants '$WANT_NAME', WIM Name is '$WIM_NAME'"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# autounattend.xml belongs on the ROOT OF THE INSTALL MEDIA, which is where Setup
|
|
209
|
+
# looks first. It is also on the support ISO; both is harmless and more robust.
|
|
210
|
+
cp "$UNATTEND" "$STAGE_DIR/autounattend.xml"
|
|
211
|
+
|
|
212
|
+
# Belt and braces: also inject it into boot.wim's Setup image at \Windows\Panther,
|
|
213
|
+
# which Setup reads explicitly. Removable-media discovery is not guaranteed when the
|
|
214
|
+
# disc is presented as USB mass storage rather than a real DVD, as it is here.
|
|
215
|
+
if command -v wimlib-imagex >/dev/null; then
|
|
216
|
+
chmod u+w "$STAGE_DIR/sources/boot.wim"
|
|
217
|
+
printf 'delete --force /Windows/Panther/unattend.xml\nadd "%s" /Windows/Panther/unattend.xml\n' \
|
|
218
|
+
"$UNATTEND" | wimlib-imagex update "$STAGE_DIR/sources/boot.wim" 2 >/dev/null 2>&1 \
|
|
219
|
+
&& info "injected unattend into boot.wim (index 2, Windows Setup)"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
FIXED="$OUT_DIR/windows-arm64-uefi.iso"
|
|
223
|
+
LABEL="$(basename "$ISO" | sed 's/\.[Ii][Ss][Oo]$//' | cut -c1-32)"
|
|
224
|
+
rm -f "$FIXED"
|
|
225
|
+
mkisofs -eltorito-platform efi -b "efi/microsoft/boot/efisys_noprompt.bin" \
|
|
226
|
+
--no-emul-boot --udf -iso-level 3 --hide "*" -V "$LABEL" -o "$FIXED" "$STAGE_DIR"
|
|
227
|
+
xorriso -indev "$FIXED" -report_el_torito plain 2>/dev/null | grep -q "UEFI" \
|
|
228
|
+
|| die "rebuild still has no UEFI El Torito record"
|
|
229
|
+
chmod -R u+w "$STAGE_DIR" 2>/dev/null || true
|
|
230
|
+
rm -rf "$STAGE_DIR"
|
|
231
|
+
ISO="$FIXED"
|
|
232
|
+
info "rebuilt: $ISO"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
echo
|
|
236
|
+
echo "ISO ready: $ISO"
|
|
237
|
+
echo "Next: ./packages/worker-fleet/src/local-worker/build-vm.sh \"$ISO\""
|
|
238
|
+
echo " ./packages/worker-fleet/src/local-worker/create-utm-vm.sh \"$ISO\""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem Runs from the support ISO at first logon (invoked by autounattend.xml).
|
|
3
|
+
rem
|
|
4
|
+
rem Its only job: get an ELEVATED shell without a UAC prompt, then run the worker
|
|
5
|
+
rem bootstrap. A UAC prompt here would be fatal -- it renders on the secure desktop,
|
|
6
|
+
rem which no automation can click and NVDA cannot read.
|
|
7
|
+
setlocal EnableDelayedExpansion
|
|
8
|
+
set "LOG=C:\a11y-first-boot.log"
|
|
9
|
+
echo [%DATE% %TIME%] first-boot starting from %~dp0 > "%LOG%"
|
|
10
|
+
|
|
11
|
+
rem The NetKVM driver was injected during windowsPE so the NIC exists, but DHCP may
|
|
12
|
+
rem not have finished by first logon. The bootstrap needs the network for winget.
|
|
13
|
+
set NET=0
|
|
14
|
+
for /L %%i in (1,1,60) do (
|
|
15
|
+
if !NET!==0 (
|
|
16
|
+
ping -n 1 -w 1000 8.8.8.8 >nul 2>&1 && set NET=1
|
|
17
|
+
if !NET!==0 timeout /t 2 /nobreak >nul
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
if !NET!==1 (echo [%DATE% %TIME%] network up >> "%LOG%") else (echo [%DATE% %TIME%] WARNING: no network after ~120s >> "%LOG%")
|
|
21
|
+
|
|
22
|
+
rem Copy off the read-only ISO so it survives the ISO being detached after install.
|
|
23
|
+
copy /y "%~dp0bootstrap-windows-worker.ps1" C:\bootstrap-windows-worker.ps1 >> "%LOG%" 2>&1
|
|
24
|
+
|
|
25
|
+
rem The operator's PUBLIC SSH key, if one was staged beside this file. This is what turns
|
|
26
|
+
rem "one console visit per box" into "none": bootstrap installs it into
|
|
27
|
+
rem administrators_authorized_keys with the ACL sshd insists on, and the machine is then
|
|
28
|
+
rem reachable by Ansible from the moment it finishes.
|
|
29
|
+
rem
|
|
30
|
+
rem Staged at ISO-build time rather than committed. A public key is not a secret, but it IS
|
|
31
|
+
rem specific to whoever runs this fleet, and a checked-in one would silently grant access to
|
|
32
|
+
rem whoever happened to be in the repo -- which is a worse default than asking.
|
|
33
|
+
rem
|
|
34
|
+
rem Copied to a FILE rather than exported as an environment variable. The fallback path below runs
|
|
35
|
+
rem the bootstrap through a scheduled task, which starts a fresh session and inherits nothing from
|
|
36
|
+
rem here -- so an exported variable would work on the elevated path and silently vanish on the other,
|
|
37
|
+
rem which is the kind of "works when I tested it" difference this project keeps paying for.
|
|
38
|
+
if exist "%~dp0operator-key.pub" (
|
|
39
|
+
if not exist "C:\ProgramData\a11y-witness" mkdir "C:\ProgramData\a11y-witness" >> "%LOG%" 2>&1
|
|
40
|
+
copy /y "%~dp0operator-key.pub" "C:\ProgramData\a11y-witness\operator-key.pub" >> "%LOG%" 2>&1
|
|
41
|
+
echo [%DATE% %TIME%] operator key staged from the install media >> "%LOG%"
|
|
42
|
+
) else (
|
|
43
|
+
echo [%DATE% %TIME%] no operator-key.pub beside this script; this box will need one console >> "%LOG%"
|
|
44
|
+
echo [%DATE% %TIME%] visit, or "ansible-playbook ssh-key.yml" once it is reachable >> "%LOG%"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
rem FirstLogonCommands usually run elevated, but that is not guaranteed. Try inline
|
|
48
|
+
rem first; fall back to a RunLevel Highest scheduled task, which elevates silently.
|
|
49
|
+
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
|
50
|
+
"$e=(New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator);" ^
|
|
51
|
+
"if ($e) { Write-Output 'already elevated; running inline'; & powershell -NoProfile -ExecutionPolicy Bypass -File C:\bootstrap-windows-worker.ps1 }" ^
|
|
52
|
+
"else { Write-Output 'not elevated; via RunLevel Highest task';" ^
|
|
53
|
+
" $a=New-ScheduledTaskAction -Execute 'powershell' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\bootstrap-windows-worker.ps1';" ^
|
|
54
|
+
" $p=New-ScheduledTaskPrincipal -UserId ([Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Highest;" ^
|
|
55
|
+
" Register-ScheduledTask -TaskName 'a11y-firstboot' -Action $a -Principal $p -Force | Out-Null;" ^
|
|
56
|
+
" Start-ScheduledTask -TaskName 'a11y-firstboot' }" >> "%LOG%" 2>&1
|
|
57
|
+
|
|
58
|
+
echo [%DATE% %TIME%] first-boot finished >> "%LOG%"
|