@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Unattended install for the local NVDA capture worker (Windows 11 ARM64 under QEMU).
|
|
4
|
+
|
|
5
|
+
Derived from the Autounattend.xml that UTM ships on utm-guest-tools.iso, which is
|
|
6
|
+
the proven ARM64 recipe: the LabConfig bypasses and the windowsPE ARM64 driver
|
|
7
|
+
paths come from there. What UTM's version does NOT do is complete the install
|
|
8
|
+
hands-off, so this adds:
|
|
9
|
+
|
|
10
|
+
* DiskConfiguration + ImageInstall - partition and install with no clicking
|
|
11
|
+
* LocalAccounts + AutoLogon - NVDA needs a logged-on interactive desktop,
|
|
12
|
+
so auto-logon is a REQUIREMENT here, not a
|
|
13
|
+
convenience: without it the worker is dead
|
|
14
|
+
after every reboot until a human logs in
|
|
15
|
+
* FirstLogonCommands - hand off to the worker bootstrap
|
|
16
|
+
|
|
17
|
+
Drive E: is the support ISO built by build-vm.sh (this file plus the
|
|
18
|
+
UTM guest-tools payload). Windows Setup picks autounattend.xml up automatically
|
|
19
|
+
from the root of removable media.
|
|
20
|
+
|
|
21
|
+
SECURITY: the account password below is in plain text, in a checked-in file. That is
|
|
22
|
+
acceptable ONLY because this is a disposable local VM behind QEMU user-mode
|
|
23
|
+
networking, reachable solely through the ports the launch script forwards to
|
|
24
|
+
127.0.0.1. Do not put this VM on a bridged network, and do not reuse the password.
|
|
25
|
+
|
|
26
|
+
NOTE: UTM's template sets EnableLUA=false (UAC off). We deliberately do NOT copy
|
|
27
|
+
that. With UAC off every process runs at high integrity, and the capture pipeline
|
|
28
|
+
depends on NVDA (nvda_noUIAccess.exe) and Edge sitting at the SAME integrity level;
|
|
29
|
+
leaving UAC at its default keeps that relationship the way the Proxmox worker has it,
|
|
30
|
+
rather than betting the pipeline on an untested configuration. Provisioning gets the
|
|
31
|
+
elevation it needs from a RunLevel Highest scheduled task instead, which elevates
|
|
32
|
+
without a prompt - and a prompt would be fatal, since UAC dialogs render on the
|
|
33
|
+
secure desktop where automation cannot reach them.
|
|
34
|
+
-->
|
|
35
|
+
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
|
36
|
+
|
|
37
|
+
<settings pass="windowsPE">
|
|
38
|
+
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
39
|
+
<SetupUILanguage>
|
|
40
|
+
<UILanguage>en-US</UILanguage>
|
|
41
|
+
</SetupUILanguage>
|
|
42
|
+
<InputLocale>en-US</InputLocale>
|
|
43
|
+
<SystemLocale>en-US</SystemLocale>
|
|
44
|
+
<UILanguage>en-US</UILanguage>
|
|
45
|
+
<UserLocale>en-US</UserLocale>
|
|
46
|
+
</component>
|
|
47
|
+
|
|
48
|
+
<component name="Microsoft-Windows-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
49
|
+
<Diagnostics>
|
|
50
|
+
<OptIn>false</OptIn>
|
|
51
|
+
</Diagnostics>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<!-- UEFI/GPT layout: ESP, MSR, then Windows across the remainder. -->
|
|
55
|
+
<DiskConfiguration>
|
|
56
|
+
<WillShowUI>OnError</WillShowUI>
|
|
57
|
+
<Disk wcm:action="add">
|
|
58
|
+
<DiskID>0</DiskID>
|
|
59
|
+
<WillWipeDisk>true</WillWipeDisk>
|
|
60
|
+
<CreatePartitions>
|
|
61
|
+
<CreatePartition wcm:action="add">
|
|
62
|
+
<Order>1</Order>
|
|
63
|
+
<Type>EFI</Type>
|
|
64
|
+
<Size>300</Size>
|
|
65
|
+
</CreatePartition>
|
|
66
|
+
<CreatePartition wcm:action="add">
|
|
67
|
+
<Order>2</Order>
|
|
68
|
+
<Type>MSR</Type>
|
|
69
|
+
<Size>16</Size>
|
|
70
|
+
</CreatePartition>
|
|
71
|
+
<CreatePartition wcm:action="add">
|
|
72
|
+
<Order>3</Order>
|
|
73
|
+
<Type>Primary</Type>
|
|
74
|
+
<Extend>true</Extend>
|
|
75
|
+
</CreatePartition>
|
|
76
|
+
</CreatePartitions>
|
|
77
|
+
<ModifyPartitions>
|
|
78
|
+
<ModifyPartition wcm:action="add">
|
|
79
|
+
<Order>1</Order>
|
|
80
|
+
<PartitionID>1</PartitionID>
|
|
81
|
+
<Format>FAT32</Format>
|
|
82
|
+
<Label>System</Label>
|
|
83
|
+
</ModifyPartition>
|
|
84
|
+
<ModifyPartition wcm:action="add">
|
|
85
|
+
<Order>2</Order>
|
|
86
|
+
<PartitionID>2</PartitionID>
|
|
87
|
+
</ModifyPartition>
|
|
88
|
+
<ModifyPartition wcm:action="add">
|
|
89
|
+
<Order>3</Order>
|
|
90
|
+
<PartitionID>3</PartitionID>
|
|
91
|
+
<Format>NTFS</Format>
|
|
92
|
+
<Label>Windows</Label>
|
|
93
|
+
<Letter>C</Letter>
|
|
94
|
+
</ModifyPartition>
|
|
95
|
+
</ModifyPartitions>
|
|
96
|
+
</Disk>
|
|
97
|
+
</DiskConfiguration>
|
|
98
|
+
|
|
99
|
+
<!-- Match /IMAGE/NAME to the WIM's NAME, which is "Windows 11 Professional".
|
|
100
|
+
"Windows 11 Pro" is its DESCRIPTION, and matching that leaves Setup sitting on
|
|
101
|
+
the edition picker. Keep this block byte-for-byte simple: substituting
|
|
102
|
+
/IMAGE/INDEX here, or adding a comment inside <InstallFrom>, made Setup reject
|
|
103
|
+
the whole answer file and revert to a fully interactive install. -->
|
|
104
|
+
<ImageInstall>
|
|
105
|
+
<OSImage>
|
|
106
|
+
<InstallFrom>
|
|
107
|
+
<MetaData wcm:action="add">
|
|
108
|
+
<Key>/IMAGE/NAME</Key>
|
|
109
|
+
<Value>Windows 11 Professional</Value>
|
|
110
|
+
</MetaData>
|
|
111
|
+
</InstallFrom>
|
|
112
|
+
<InstallTo>
|
|
113
|
+
<DiskID>0</DiskID>
|
|
114
|
+
<PartitionID>3</PartitionID>
|
|
115
|
+
</InstallTo>
|
|
116
|
+
<WillShowUI>OnError</WillShowUI>
|
|
117
|
+
</OSImage>
|
|
118
|
+
</ImageInstall>
|
|
119
|
+
|
|
120
|
+
<!-- Windows 11 refuses to install without TPM 2.0 and Secure Boot. Rather than
|
|
121
|
+
wiring up swtpm, take UTM's approach and tell Setup to skip the checks. -->
|
|
122
|
+
<RunSynchronous>
|
|
123
|
+
<RunSynchronousCommand wcm:action="add">
|
|
124
|
+
<Order>1</Order>
|
|
125
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassCPUCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
126
|
+
</RunSynchronousCommand>
|
|
127
|
+
<RunSynchronousCommand wcm:action="add">
|
|
128
|
+
<Order>2</Order>
|
|
129
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassRAMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
130
|
+
</RunSynchronousCommand>
|
|
131
|
+
<RunSynchronousCommand wcm:action="add">
|
|
132
|
+
<Order>3</Order>
|
|
133
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
134
|
+
</RunSynchronousCommand>
|
|
135
|
+
<RunSynchronousCommand wcm:action="add">
|
|
136
|
+
<Order>4</Order>
|
|
137
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassTPMCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
138
|
+
</RunSynchronousCommand>
|
|
139
|
+
<RunSynchronousCommand wcm:action="add">
|
|
140
|
+
<Order>5</Order>
|
|
141
|
+
<Path>reg add HKLM\System\Setup\LabConfig /v BypassDiskCheck /t REG_DWORD /d 0x00000001 /f</Path>
|
|
142
|
+
</RunSynchronousCommand>
|
|
143
|
+
</RunSynchronous>
|
|
144
|
+
|
|
145
|
+
<UserData>
|
|
146
|
+
<AcceptEula>true</AcceptEula>
|
|
147
|
+
<FullName>a11ign</FullName>
|
|
148
|
+
<Organization>a11ign</Organization>
|
|
149
|
+
<!-- Generic Windows 11 Pro KMS client key: selects the edition during an
|
|
150
|
+
unattended install. It does NOT activate, which is what we want - an
|
|
151
|
+
unactivated Windows is fully functional for capture, and leaving it
|
|
152
|
+
unactivated keeps the exported image portable between machines. -->
|
|
153
|
+
<ProductKey>
|
|
154
|
+
<Key>W269N-WFGWX-YVC9B-4J6C9-T83GX</Key>
|
|
155
|
+
<WillShowUI>Never</WillShowUI>
|
|
156
|
+
</ProductKey>
|
|
157
|
+
</UserData>
|
|
158
|
+
</component>
|
|
159
|
+
|
|
160
|
+
<!-- ARM64 virtio drivers, injected so the installed OS has storage, serial and
|
|
161
|
+
NETWORK working on first boot. Networking on first boot is what lets the
|
|
162
|
+
bootstrap fetch Node, Git and NVDA without any manual driver step.
|
|
163
|
+
Paths are UTM's, on the support ISO (drive E:). viogpu is deliberately left
|
|
164
|
+
out: UTM disables it because it causes a black screen. -->
|
|
165
|
+
<component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
166
|
+
<DriverPaths>
|
|
167
|
+
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
|
168
|
+
<Path>E:\Drivers\vioscsi\w10\ARM64</Path>
|
|
169
|
+
</PathAndCredentials>
|
|
170
|
+
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
|
|
171
|
+
<Path>E:\Drivers\viostor\w10\ARM64</Path>
|
|
172
|
+
</PathAndCredentials>
|
|
173
|
+
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
|
|
174
|
+
<Path>E:\Drivers\vioserial\w10\ARM64</Path>
|
|
175
|
+
</PathAndCredentials>
|
|
176
|
+
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
|
|
177
|
+
<Path>E:\Drivers\NetKVM\w10\ARM64</Path>
|
|
178
|
+
</PathAndCredentials>
|
|
179
|
+
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
|
|
180
|
+
<Path>E:\Drivers\Balloon\w10\ARM64</Path>
|
|
181
|
+
</PathAndCredentials>
|
|
182
|
+
</DriverPaths>
|
|
183
|
+
</component>
|
|
184
|
+
</settings>
|
|
185
|
+
|
|
186
|
+
<settings pass="specialize">
|
|
187
|
+
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
188
|
+
<ComputerName>A11Y-WORKER</ComputerName>
|
|
189
|
+
<TimeZone>GMT Standard Time</TimeZone>
|
|
190
|
+
</component>
|
|
191
|
+
<component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
192
|
+
<SkipAutoActivation>true</SkipAutoActivation>
|
|
193
|
+
</component>
|
|
194
|
+
<component name="Microsoft-Windows-SQMApi" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
195
|
+
<CEIPEnabled>0</CEIPEnabled>
|
|
196
|
+
</component>
|
|
197
|
+
</settings>
|
|
198
|
+
|
|
199
|
+
<settings pass="oobeSystem">
|
|
200
|
+
<component name="Microsoft-Windows-International-Core" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
201
|
+
<InputLocale>en-US</InputLocale>
|
|
202
|
+
<SystemLocale>en-US</SystemLocale>
|
|
203
|
+
<UILanguage>en-US</UILanguage>
|
|
204
|
+
<UserLocale>en-US</UserLocale>
|
|
205
|
+
</component>
|
|
206
|
+
|
|
207
|
+
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
|
208
|
+
<UserAccounts>
|
|
209
|
+
<LocalAccounts>
|
|
210
|
+
<LocalAccount wcm:action="add">
|
|
211
|
+
<Name>witness</Name>
|
|
212
|
+
<DisplayName>witness</DisplayName>
|
|
213
|
+
<Group>Administrators</Group>
|
|
214
|
+
<Password>
|
|
215
|
+
<Value>witness</Value>
|
|
216
|
+
<PlainText>true</PlainText>
|
|
217
|
+
</Password>
|
|
218
|
+
</LocalAccount>
|
|
219
|
+
</LocalAccounts>
|
|
220
|
+
</UserAccounts>
|
|
221
|
+
|
|
222
|
+
<!-- The whole reason this file exists. NVDA is a GUI application: it needs a
|
|
223
|
+
real logged-on desktop session. No auto-logon means no session after a
|
|
224
|
+
reboot, which means captures silently return nothing at all. -->
|
|
225
|
+
<AutoLogon>
|
|
226
|
+
<Enabled>true</Enabled>
|
|
227
|
+
<Username>witness</Username>
|
|
228
|
+
<Password>
|
|
229
|
+
<Value>witness</Value>
|
|
230
|
+
<PlainText>true</PlainText>
|
|
231
|
+
</Password>
|
|
232
|
+
<LogonCount>2147483647</LogonCount>
|
|
233
|
+
</AutoLogon>
|
|
234
|
+
|
|
235
|
+
<OOBE>
|
|
236
|
+
<HideEULAPage>true</HideEULAPage>
|
|
237
|
+
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
|
238
|
+
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
|
239
|
+
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
|
240
|
+
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
|
241
|
+
<ProtectYourPC>3</ProtectYourPC>
|
|
242
|
+
<VMModeOptimizations>
|
|
243
|
+
<SkipWinREInitialization>true</SkipWinREInitialization>
|
|
244
|
+
</VMModeOptimizations>
|
|
245
|
+
</OOBE>
|
|
246
|
+
|
|
247
|
+
<FirstLogonCommands>
|
|
248
|
+
<SynchronousCommand wcm:action="add">
|
|
249
|
+
<Order>1</Order>
|
|
250
|
+
<Description>Disable hibernation</Description>
|
|
251
|
+
<CommandLine>cmd /c POWERCFG -H OFF</CommandLine>
|
|
252
|
+
</SynchronousCommand>
|
|
253
|
+
<!-- Adds qemu-ga, which gives the host a command channel into this guest over
|
|
254
|
+
QMP before SSH exists - the difference between debugging this VM and
|
|
255
|
+
guessing at it. /S is the NSIS silent switch. -->
|
|
256
|
+
<SynchronousCommand wcm:action="add">
|
|
257
|
+
<Order>2</Order>
|
|
258
|
+
<Description>Install UTM/virtio guest tools (adds qemu-ga)</Description>
|
|
259
|
+
<CommandLine>cmd /c for %i in (D E F G) do if exist %i:\utm-guest-tools.exe start /wait %i:\utm-guest-tools.exe /S</CommandLine>
|
|
260
|
+
</SynchronousCommand>
|
|
261
|
+
<!-- Never disable the screensaver's lock and then leave the console locked:
|
|
262
|
+
a locked desktop makes NVDA silent. Belt and braces alongside the
|
|
263
|
+
provisioning script, which also turns the screensaver off. -->
|
|
264
|
+
<SynchronousCommand wcm:action="add">
|
|
265
|
+
<Order>3</Order>
|
|
266
|
+
<Description>Disable screensaver (it steals foreground mid-capture)</Description>
|
|
267
|
+
<CommandLine>reg add "HKCU\Control Panel\Desktop" /v ScreenSaveActive /t REG_SZ /d 0 /f</CommandLine>
|
|
268
|
+
</SynchronousCommand>
|
|
269
|
+
<!-- Provisioning needs elevation (firewall rules, HKLM Edge policy). A
|
|
270
|
+
RunLevel Highest scheduled task elevates with NO prompt; a UAC dialog here
|
|
271
|
+
would render on the secure desktop and hang the build forever. -->
|
|
272
|
+
<SynchronousCommand wcm:action="add">
|
|
273
|
+
<Order>4</Order>
|
|
274
|
+
<Description>Run the worker bootstrap elevated, without a UAC prompt</Description>
|
|
275
|
+
<CommandLine>cmd /c for %i in (D E F G) do if exist %i:\first-boot.cmd start /wait %i:\first-boot.cmd</CommandLine>
|
|
276
|
+
</SynchronousCommand>
|
|
277
|
+
</FirstLogonCommands>
|
|
278
|
+
</component>
|
|
279
|
+
</settings>
|
|
280
|
+
</unattend>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build a local NVDA capture worker VM on Apple Silicon, unattended.
|
|
3
|
+
#
|
|
4
|
+
# ./packages/worker-fleet/src/local-worker/build-vm.sh /path/to/Win11_ARM64.iso
|
|
5
|
+
#
|
|
6
|
+
# Produces a self-contained VM directory (default ~/a11y-worker-vm) holding the disk
|
|
7
|
+
# image, UEFI vars and a run script. That directory IS the portable artifact: copy it
|
|
8
|
+
# to another Apple Silicon Mac, run ./run.sh, done. See docs/local-worker-vm.md.
|
|
9
|
+
#
|
|
10
|
+
# Why QEMU and not UTM: `utmctl` has no `create` subcommand, so a UTM-based flow can
|
|
11
|
+
# only ever be "click through these GUI steps". QEMU is scriptable end to end, which is
|
|
12
|
+
# the difference between a reproducible artifact and a folk recipe.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
# architecture-audit.md §8: builds a local UTM/QEMU worker VM, which is deprecated -- "The UTM is
|
|
16
|
+
# deprecated, that was a testing thing." (repository owner, 2026-09-05). Capture on the bare-metal fleet
|
|
17
|
+
# instead: npm run fleet:status, npm run fleet:deploy.
|
|
18
|
+
echo "DEPRECATED: build-vm.sh builds a local UTM worker VM. UTM was a testing path and is not the fleet." >&2
|
|
19
|
+
echo "Capture on the bare-metal fleet instead: npm run fleet:status, npm run fleet:deploy." >&2
|
|
20
|
+
|
|
21
|
+
# #636: a warning on a path nobody watches is a warning nobody reads -- so this REFUSES, rather than
|
|
22
|
+
# merely printing the two lines above and continuing. The measurements and reasoning behind the local-VM
|
|
23
|
+
# work stay in docs/local-worker-vm.md as history; only the path that runs is fenced.
|
|
24
|
+
if [ "${A11Y_LOCAL_VM:-}" != "1" ]; then
|
|
25
|
+
echo "refusing: set A11Y_LOCAL_VM=1 to run this deprecated local-VM script anyway." >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
WIN_ISO="${1:-}"
|
|
30
|
+
VM_DIR="${A11Y_VM_DIR:-$HOME/a11y-worker-vm}"
|
|
31
|
+
DISK_GB="${A11Y_VM_DISK_GB:-64}"
|
|
32
|
+
RAM_MB="${A11Y_VM_RAM_MB:-8192}"
|
|
33
|
+
CPUS="${A11Y_VM_CPUS:-4}"
|
|
34
|
+
WORKER_PORT="${A11Y_PORT:-8765}"
|
|
35
|
+
SSH_PORT="${A11Y_VM_SSH_PORT:-2222}"
|
|
36
|
+
|
|
37
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
39
|
+
|
|
40
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
41
|
+
info() { echo "==> $*"; }
|
|
42
|
+
|
|
43
|
+
[ -n "$WIN_ISO" ] || die "usage: $0 <windows-11-arm64.iso> (build one with CrystalFetch, or packages/worker-fleet/src/local-worker/fetch-windows-iso.sh)"
|
|
44
|
+
[ -f "$WIN_ISO" ] || die "not found: $WIN_ISO"
|
|
45
|
+
command -v qemu-system-aarch64 >/dev/null || die "qemu missing: brew install qemu"
|
|
46
|
+
command -v qemu-img >/dev/null || die "qemu-img missing: brew install qemu"
|
|
47
|
+
command -v mkisofs >/dev/null || die "mkisofs missing: brew install cdrtools"
|
|
48
|
+
|
|
49
|
+
FW_CODE="$(ls /opt/homebrew/share/qemu/edk2-aarch64-code.fd /usr/local/share/qemu/edk2-aarch64-code.fd 2>/dev/null | head -1 || true)"
|
|
50
|
+
FW_VARS_SRC="$(ls /opt/homebrew/share/qemu/edk2-arm-vars.fd /usr/local/share/qemu/edk2-arm-vars.fd 2>/dev/null | head -1 || true)"
|
|
51
|
+
[ -n "$FW_CODE" ] || die "edk2-aarch64-code.fd not found (reinstall qemu)"
|
|
52
|
+
[ -n "$FW_VARS_SRC" ] || die "edk2-arm-vars.fd not found (reinstall qemu)"
|
|
53
|
+
|
|
54
|
+
mkdir -p "$VM_DIR"
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
info "Fetching UTM guest tools (ARM64 virtio drivers + qemu-ga)"
|
|
58
|
+
# These drivers are the reason this works at all: Windows 11 ARM64 has no inbox
|
|
59
|
+
# virtio-net driver, so without injecting NetKVM the installed OS has no network and
|
|
60
|
+
# cannot fetch Node/Git/NVDA. The ISO also carries qemu-ga, which gives the host a
|
|
61
|
+
# command channel into the guest over QMP before SSH exists.
|
|
62
|
+
GT_ISO="$VM_DIR/utm-guest-tools.iso"
|
|
63
|
+
if [ ! -f "$GT_ISO" ]; then
|
|
64
|
+
GT_URL="$(curl -sL https://api.github.com/repos/utmapp/qemu/releases/latest \
|
|
65
|
+
| grep -o 'https://[^"]*guest-tools[^"]*\.iso' | head -1 || true)"
|
|
66
|
+
[ -n "$GT_URL" ] || die "could not resolve the guest-tools ISO URL"
|
|
67
|
+
curl -L --fail -o "$GT_ISO" "$GT_URL"
|
|
68
|
+
fi
|
|
69
|
+
info "guest tools: $(du -h "$GT_ISO" | cut -f1)"
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
info "Building the support ISO (autounattend + drivers + bootstrap)"
|
|
73
|
+
# One removable medium carrying everything Setup and first logon need. Windows Setup
|
|
74
|
+
# picks autounattend.xml up automatically from the root of removable media.
|
|
75
|
+
STAGE="$(mktemp -d)"
|
|
76
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
77
|
+
MNT="$(mktemp -d)"
|
|
78
|
+
hdiutil attach -readonly -nobrowse -mountpoint "$MNT" "$GT_ISO" >/dev/null
|
|
79
|
+
cp -R "$MNT/Drivers" "$STAGE/"
|
|
80
|
+
cp "$MNT"/utm-guest-tools-*.exe "$STAGE/utm-guest-tools.exe"
|
|
81
|
+
hdiutil detach "$MNT" >/dev/null
|
|
82
|
+
rmdir "$MNT" 2>/dev/null || true
|
|
83
|
+
|
|
84
|
+
cp "$SCRIPT_DIR/autounattend.xml" "$STAGE/autounattend.xml"
|
|
85
|
+
cp "$SCRIPT_DIR/first-boot.cmd" "$STAGE/first-boot.cmd"
|
|
86
|
+
cp "$REPO_ROOT/scripts/bootstrap-windows-worker.ps1" "$STAGE/bootstrap-windows-worker.ps1"
|
|
87
|
+
|
|
88
|
+
SUPPORT_ISO="$VM_DIR/support.iso"
|
|
89
|
+
# -J (Joliet) so Windows sees real filenames rather than mangled 8.3 names.
|
|
90
|
+
mkisofs -quiet -J -R -V A11YSUPPORT -o "$SUPPORT_ISO" "$STAGE"
|
|
91
|
+
info "support ISO: $(du -h "$SUPPORT_ISO" | cut -f1)"
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
info "Creating disk and UEFI vars"
|
|
95
|
+
[ -f "$VM_DIR/disk.qcow2" ] || qemu-img create -f qcow2 "$VM_DIR/disk.qcow2" "${DISK_GB}G" >/dev/null
|
|
96
|
+
[ -f "$VM_DIR/efi-vars.fd" ] || cp "$FW_VARS_SRC" "$VM_DIR/efi-vars.fd"
|
|
97
|
+
# Symlink rather than copy: the Windows ISO is ~4 GB and only needed for --install.
|
|
98
|
+
# (When exporting the VM to another machine, the symlink is simply absent, which is
|
|
99
|
+
# fine -- normal boots do not use it.)
|
|
100
|
+
#
|
|
101
|
+
# Guard against the source ALREADY being $VM_DIR/windows.iso: `ln -sf x x` creates a
|
|
102
|
+
# symlink to itself and destroys the file. Passing the staged ISO back in is the obvious
|
|
103
|
+
# thing to do on a second run, so this must not eat it.
|
|
104
|
+
WIN_ISO_ABS="$(cd "$(dirname "$WIN_ISO")" && pwd)/$(basename "$WIN_ISO")"
|
|
105
|
+
if [ "$WIN_ISO_ABS" != "$VM_DIR/windows.iso" ]; then
|
|
106
|
+
ln -sf "$WIN_ISO_ABS" "$VM_DIR/windows.iso"
|
|
107
|
+
else
|
|
108
|
+
info "ISO is already staged at $VM_DIR/windows.iso; leaving it alone"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
info "Writing run.sh"
|
|
113
|
+
cat > "$VM_DIR/run.sh" <<RUNSH
|
|
114
|
+
#!/usr/bin/env bash
|
|
115
|
+
# Launch the a11ign NVDA worker VM. Generated by build-vm.sh.
|
|
116
|
+
# ./run.sh headless; worker on 127.0.0.1:${WORKER_PORT}, ssh on 2222
|
|
117
|
+
# ./run.sh --install first run: boot from the Windows ISO to install unattended
|
|
118
|
+
set -euo pipefail
|
|
119
|
+
VM_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
120
|
+
INSTALL=0
|
|
121
|
+
[ "\${1:-}" = "--install" ] && INSTALL=1
|
|
122
|
+
|
|
123
|
+
ARGS=(
|
|
124
|
+
-M virt,highmem=on
|
|
125
|
+
-accel hvf
|
|
126
|
+
-cpu host
|
|
127
|
+
-smp ${CPUS}
|
|
128
|
+
-m ${RAM_MB}
|
|
129
|
+
-drive if=pflash,format=raw,unit=0,readonly=on,file=${FW_CODE}
|
|
130
|
+
-drive if=pflash,format=raw,unit=1,file="\$VM_DIR/efi-vars.fd"
|
|
131
|
+
# NVMe rather than virtio-blk for the system disk: Windows 11 ARM64 has an inbox
|
|
132
|
+
# NVMe driver, so the disk is visible to Setup with no driver injection at all.
|
|
133
|
+
-drive file="\$VM_DIR/disk.qcow2",if=none,id=hd0,format=qcow2,cache=writeback,discard=unmap
|
|
134
|
+
-device nvme,drive=hd0,serial=a11yworker
|
|
135
|
+
# User-mode networking: the guest is NAT'd and unreachable except through these
|
|
136
|
+
# forwards, bound to loopback. That is what makes the plaintext password in
|
|
137
|
+
# autounattend.xml tolerable -- keep it that way, do not bridge this VM.
|
|
138
|
+
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:${WORKER_PORT}-:${WORKER_PORT},hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22
|
|
139
|
+
-device virtio-net-pci,netdev=net0
|
|
140
|
+
-device virtio-gpu-pci
|
|
141
|
+
-device qemu-xhci
|
|
142
|
+
-device usb-kbd
|
|
143
|
+
-device usb-tablet
|
|
144
|
+
-device virtio-rng-pci
|
|
145
|
+
# The monitor socket is how you get EYES on a headless install:
|
|
146
|
+
# echo "screendump /tmp/vm.ppm" | socat - unix:\$VM_DIR/monitor.sock
|
|
147
|
+
-monitor unix:"\$VM_DIR/monitor.sock",server,nowait
|
|
148
|
+
-qmp unix:"\$VM_DIR/qmp.sock",server,nowait
|
|
149
|
+
-display none
|
|
150
|
+
-vnc 127.0.0.1:1
|
|
151
|
+
-rtc base=localtime
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# CD-ROMs attach as USB storage, NOT with a bare "-drive media=cdrom". The aarch64
|
|
155
|
+
# "virt" machine has no IDE bus, so the bare form fails to start at all ("No 'ide' bus
|
|
156
|
+
# found"). virtio-blk would work for QEMU but Windows Setup cannot see a virtio disk
|
|
157
|
+
# without the driver it is trying to read FROM that disk; USB mass storage is inbox on
|
|
158
|
+
# Windows, so it sidesteps the chicken-and-egg entirely.
|
|
159
|
+
if [ \$INSTALL -eq 1 ]; then
|
|
160
|
+
ARGS+=(
|
|
161
|
+
-drive file="\$VM_DIR/windows.iso",if=none,id=cdwin,media=cdrom,readonly=on
|
|
162
|
+
-device usb-storage,drive=cdwin,removable=on,bootindex=0
|
|
163
|
+
-drive file="\$VM_DIR/support.iso",if=none,id=cdsup,media=cdrom,readonly=on
|
|
164
|
+
-device usb-storage,drive=cdsup,removable=on
|
|
165
|
+
)
|
|
166
|
+
else
|
|
167
|
+
# Keep the support ISO attached: provisioning may want the guest-tools installer,
|
|
168
|
+
# and an absent drive letter is a confusing failure mode.
|
|
169
|
+
ARGS+=(
|
|
170
|
+
-drive file="\$VM_DIR/support.iso",if=none,id=cdsup,media=cdrom,readonly=on
|
|
171
|
+
-device usb-storage,drive=cdsup,removable=on
|
|
172
|
+
)
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
exec qemu-system-aarch64 "\${ARGS[@]}"
|
|
176
|
+
RUNSH
|
|
177
|
+
chmod +x "$VM_DIR/run.sh"
|
|
178
|
+
|
|
179
|
+
# Screenshot helper. The VM runs headless, so this is the only way to see what an
|
|
180
|
+
# unattended install is actually doing -- and "it is silently sitting on a prompt" and
|
|
181
|
+
# "it is working fine" look identical without it.
|
|
182
|
+
cat > "$VM_DIR/shot.sh" <<'SHOTSH'
|
|
183
|
+
#!/usr/bin/env bash
|
|
184
|
+
# Screenshot the headless guest. Usage: ./shot.sh [outfile.png]
|
|
185
|
+
set -euo pipefail
|
|
186
|
+
D="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
187
|
+
OUT="${1:-$D/shot.png}"
|
|
188
|
+
command -v socat >/dev/null || { echo "needs socat: brew install socat" >&2; exit 1; }
|
|
189
|
+
[ -S "$D/monitor.sock" ] || { echo "no monitor.sock -- is the VM running?" >&2; exit 1; }
|
|
190
|
+
echo "screendump $D/.shot.ppm" | socat - unix-connect:"$D/monitor.sock" >/dev/null 2>&1
|
|
191
|
+
sips -s format png "$D/.shot.ppm" --out "$OUT" >/dev/null 2>&1
|
|
192
|
+
echo "$OUT (disk now $(du -h "$D/disk.qcow2" | cut -f1))"
|
|
193
|
+
SHOTSH
|
|
194
|
+
chmod +x "$VM_DIR/shot.sh"
|
|
195
|
+
|
|
196
|
+
cat <<EOF
|
|
197
|
+
|
|
198
|
+
--- VM prepared in $VM_DIR ---
|
|
199
|
+
|
|
200
|
+
1. Start the unattended install (takes ~20-40 min, no interaction):
|
|
201
|
+
$VM_DIR/run.sh --install
|
|
202
|
+
|
|
203
|
+
2. Watch it without a display (screendump needs socat: brew install socat):
|
|
204
|
+
echo "screendump $VM_DIR/shot.ppm" | socat - unix:$VM_DIR/monitor.sock
|
|
205
|
+
# then convert/view: sips -s format png $VM_DIR/shot.ppm --out $VM_DIR/shot.png
|
|
206
|
+
|
|
207
|
+
3. When it settles, the worker should answer on the host:
|
|
208
|
+
curl http://127.0.0.1:${WORKER_PORT}/health
|
|
209
|
+
ssh -p ${SSH_PORT} witness@127.0.0.1
|
|
210
|
+
|
|
211
|
+
4. Point the control plane at it:
|
|
212
|
+
A11Y_WORKER=http://127.0.0.1:${WORKER_PORT} npm run witness -- https://example.com --task "..."
|
|
213
|
+
|
|
214
|
+
Subsequent boots: $VM_DIR/run.sh (no --install)
|
|
215
|
+
|
|
216
|
+
To hand this to someone else, copy the whole $VM_DIR directory. Shut the guest down
|
|
217
|
+
cleanly first, and see docs/local-worker-vm.md for shrinking it.
|
|
218
|
+
EOF
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Clone the local NVDA worker VM into an additional, independent worker.
|
|
3
|
+
#
|
|
4
|
+
# ./packages/worker-fleet/src/local-worker/clone-worker.sh [new-name] # default: a11y-worker-2
|
|
5
|
+
#
|
|
6
|
+
# One worker serves one capture at a time by design (one desktop, one foreground window, one
|
|
7
|
+
# NVDA), so throughput scales by running more of them. On APFS the clone is copy-on-write, so
|
|
8
|
+
# it costs seconds and almost no disk until the two diverge.
|
|
9
|
+
#
|
|
10
|
+
# THE REASON THIS SCRIPT EXISTS: `utmctl clone` copies the MAC address. Two VMs with the same
|
|
11
|
+
# MAC on one vmnet share a single DHCP lease — the clone takes the original's IP and the
|
|
12
|
+
# original ends up with NO address at all. It boots perfectly and looks utterly dead from
|
|
13
|
+
# outside, which is a miserable thing to debug. So the clone gets a fresh MAC before it is
|
|
14
|
+
# ever started.
|
|
15
|
+
#
|
|
16
|
+
# Editing config.plist requires UTM to be quit: it caches configuration in memory and will
|
|
17
|
+
# write its cached copy back over the edit.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
# architecture-audit.md §8: clones a local UTM worker VM, which is deprecated -- "The UTM is deprecated,
|
|
21
|
+
# that was a testing thing." (repository owner, 2026-09-05). Capture on the bare-metal fleet instead:
|
|
22
|
+
# npm run fleet:status, npm run fleet:deploy.
|
|
23
|
+
echo "DEPRECATED: clone-worker.sh clones a local UTM worker VM. UTM was a testing path and is not the fleet." >&2
|
|
24
|
+
echo "Capture on the bare-metal fleet instead: npm run fleet:status, npm run fleet:deploy." >&2
|
|
25
|
+
|
|
26
|
+
# #636: a warning on a path nobody watches is a warning nobody reads -- so this REFUSES, rather than
|
|
27
|
+
# merely printing the two lines above and continuing. The measurements and reasoning behind the local-VM
|
|
28
|
+
# work stay in docs/local-worker-vm.md as history; only the path that runs is fenced.
|
|
29
|
+
if [ "${A11Y_LOCAL_VM:-}" != "1" ]; then
|
|
30
|
+
echo "refusing: set A11Y_LOCAL_VM=1 to run this deprecated local-VM script anyway." >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SOURCE_NAME="${A11Y_VM_NAME:-a11y-worker}"
|
|
35
|
+
NEW_NAME="${1:-${SOURCE_NAME}-2}"
|
|
36
|
+
DOCS="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents"
|
|
37
|
+
BOOT_TIMEOUT_S=300
|
|
38
|
+
PORT="${A11Y_PORT:-8765}"
|
|
39
|
+
|
|
40
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
41
|
+
say() { echo "==> $*"; }
|
|
42
|
+
|
|
43
|
+
command -v utmctl >/dev/null || die "utmctl not found (brew install --cask utm)"
|
|
44
|
+
pgrep -x UTM >/dev/null || { say "launching UTM (utmctl needs the app)"; open -a UTM; sleep 5; }
|
|
45
|
+
|
|
46
|
+
uuid_of() { utmctl list | awk -v n="$1" '$3 == n { print $1 }'; }
|
|
47
|
+
|
|
48
|
+
SOURCE_UUID="$(uuid_of "$SOURCE_NAME")"
|
|
49
|
+
[ -n "$SOURCE_UUID" ] || die "no VM named '$SOURCE_NAME'"
|
|
50
|
+
[ "$(echo "$SOURCE_UUID" | wc -l | tr -d ' ')" -eq 1 ] || die "several VMs named '$SOURCE_NAME'; see docs/local-worker-vm.md"
|
|
51
|
+
[ -z "$(uuid_of "$NEW_NAME")" ] || die "'$NEW_NAME' already exists. Pick another name, or remove it in UTM first."
|
|
52
|
+
|
|
53
|
+
# Clone a stopped VM. Copying a running disk gives a crash-consistent image — the clone would
|
|
54
|
+
# boot as though the power had been pulled, with the dirty-volume repair that implies.
|
|
55
|
+
if [ "$(utmctl status "$SOURCE_UUID")" != "stopped" ]; then
|
|
56
|
+
say "stopping '$SOURCE_NAME' cleanly first (a clone of a running disk boots dirty)"
|
|
57
|
+
utmctl stop "$SOURCE_UUID" --request >/dev/null
|
|
58
|
+
for _ in $(seq 1 40); do
|
|
59
|
+
[ "$(utmctl status "$SOURCE_UUID")" = "stopped" ] && break
|
|
60
|
+
sleep 3
|
|
61
|
+
done
|
|
62
|
+
[ "$(utmctl status "$SOURCE_UUID")" = "stopped" ] || die "'$SOURCE_NAME' would not shut down; stop it by hand and re-run"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
say "cloning '$SOURCE_NAME' -> '$NEW_NAME'"
|
|
66
|
+
utmctl clone "$SOURCE_UUID" --name "$NEW_NAME" >/dev/null
|
|
67
|
+
NEW_UUID="$(uuid_of "$NEW_NAME")"
|
|
68
|
+
[ -n "$NEW_UUID" ] || die "clone reported success but '$NEW_NAME' is not registered"
|
|
69
|
+
|
|
70
|
+
CONFIG="$DOCS/$NEW_NAME.utm/config.plist"
|
|
71
|
+
[ -f "$CONFIG" ] || die "cloned bundle has no config.plist at $CONFIG"
|
|
72
|
+
|
|
73
|
+
# Every VM must be stopped before UTM is quit, not just the one being cloned.
|
|
74
|
+
#
|
|
75
|
+
# Quitting UTM tries to SUSPEND anything still running, and suspend fails outright with an
|
|
76
|
+
# emulated NVMe disk — which ours is. The result is a modal dialog ("Failed to save suspend
|
|
77
|
+
# state ... Quitting UTM will kill all running VMs") that blocks the script and, if dismissed
|
|
78
|
+
# with OK, takes down every running worker. Found by running this script while a second worker
|
|
79
|
+
# was serving.
|
|
80
|
+
say "stopping every running VM before quitting UTM (suspend fails on NVMe, and quitting kills running VMs)"
|
|
81
|
+
for uuid in $(utmctl list | awk 'NR > 1 && $2 != "stopped" { print $1 }'); do
|
|
82
|
+
utmctl stop "$uuid" --request >/dev/null 2>&1 || true
|
|
83
|
+
done
|
|
84
|
+
for _ in $(seq 1 40); do pgrep -f QEMULauncher >/dev/null || break; sleep 3; done
|
|
85
|
+
for uuid in $(utmctl list | awk 'NR > 1 && $2 != "stopped" { print $1 }'); do
|
|
86
|
+
utmctl stop "$uuid" >/dev/null 2>&1 || true
|
|
87
|
+
done
|
|
88
|
+
for _ in $(seq 1 10); do pgrep -f QEMULauncher >/dev/null || break; sleep 2; done
|
|
89
|
+
pgrep -f QEMULauncher >/dev/null && die "a VM is still running; stop it by hand before re-running"
|
|
90
|
+
|
|
91
|
+
say "quitting UTM so the config edit is not overwritten from its cache"
|
|
92
|
+
osascript -e 'tell application "UTM" to quit' 2>/dev/null || true
|
|
93
|
+
for _ in $(seq 1 10); do pgrep -x UTM >/dev/null || break; sleep 2; done
|
|
94
|
+
|
|
95
|
+
# Locally administered, unicast: first octet 0x52 has bit 1 set (local) and bit 0 clear
|
|
96
|
+
# (unicast), so it cannot collide with a real vendor's allocation.
|
|
97
|
+
NEW_MAC="$(python3 -c "
|
|
98
|
+
import random
|
|
99
|
+
print(':'.join(['52'] + ['%02X' % random.randint(0, 255) for _ in range(5)]))")"
|
|
100
|
+
cp "$CONFIG" "$CONFIG.bak"
|
|
101
|
+
plutil -replace Network.0.MacAddress -string "$NEW_MAC" "$CONFIG" \
|
|
102
|
+
|| die "could not set the MAC; the original config is at $CONFIG.bak"
|
|
103
|
+
READBACK="$(plutil -extract Network.0.MacAddress raw "$CONFIG")"
|
|
104
|
+
[ "$READBACK" = "$NEW_MAC" ] || die "MAC did not take (read back '$READBACK'); config backed up at $CONFIG.bak"
|
|
105
|
+
say "new MAC $NEW_MAC (source keeps its own)"
|
|
106
|
+
|
|
107
|
+
open -a UTM
|
|
108
|
+
for _ in $(seq 1 15); do sleep 2; utmctl list >/dev/null 2>&1 && break; done
|
|
109
|
+
# #635: without this, a UTM that never becomes responsive falls straight through into the loop below,
|
|
110
|
+
# whose `utmctl list` would fail again, iterate zero names, and let the script finish having started
|
|
111
|
+
# nothing -- with no error anywhere. Refuse instead.
|
|
112
|
+
utmctl list >/dev/null 2>&1 || die "UTM did not become responsive after relaunch"
|
|
113
|
+
|
|
114
|
+
# Start the source first. Both guests carry the same Windows machine identity, and letting one
|
|
115
|
+
# settle before the other avoids two identical hostnames racing for the same DHCP server.
|
|
116
|
+
for name in $(utmctl list | awk -v n="$SOURCE_NAME" 'NR > 1 && $3 ~ "^"n { print $3 }' | sort -u); do
|
|
117
|
+
uuid="$(uuid_of "$name")"
|
|
118
|
+
say "starting '$name'"
|
|
119
|
+
utmctl start "$uuid" >/dev/null
|
|
120
|
+
waited=0
|
|
121
|
+
ip=""
|
|
122
|
+
while [ "$waited" -lt "$BOOT_TIMEOUT_S" ]; do
|
|
123
|
+
sleep 10; waited=$((waited + 10))
|
|
124
|
+
# Ignore link-local: a guest reports 169.254.x.x while DHCP is still pending, and that
|
|
125
|
+
# reads as a failure when it is just "not yet".
|
|
126
|
+
ip="$(utmctl ip-address "$uuid" 2>/dev/null | grep -oE '^[0-9.]+' | grep -v '^127' | grep -v '^169\.254' | head -1 || true)"
|
|
127
|
+
[ -n "$ip" ] && break
|
|
128
|
+
done
|
|
129
|
+
[ -n "$ip" ] || die "'$name' never got a DHCP lease in ${BOOT_TIMEOUT_S}s. Check its MAC is unique: plutil -extract Network.0.MacAddress raw '$DOCS/$name.utm/config.plist'"
|
|
130
|
+
health=""
|
|
131
|
+
while [ "$waited" -lt "$BOOT_TIMEOUT_S" ]; do
|
|
132
|
+
health="$(curl -s -m 5 "http://$ip:$PORT/health" 2>/dev/null || true)"
|
|
133
|
+
[ -n "$health" ] && break
|
|
134
|
+
sleep 10; waited=$((waited + 10))
|
|
135
|
+
done
|
|
136
|
+
say " $name $ip ${health:-NOT ANSWERING /health yet}"
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
echo
|
|
140
|
+
say "pool:"
|
|
141
|
+
./"$(dirname "$0")/worker-ctl.sh" pool 2>/dev/null || echo " (run npm run worker:ctl -- pool)"
|