@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,275 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Build a Windows 11 image with Defender and unused components removed, keeping everything a capture
|
|
4
|
+
guest needs. Modelled on nano11builder's technique; none of its choices.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Why this exists, in one number: Defender's MsMpEng is ~259 MB resident on a guest that commits
|
|
8
|
+
~1,859 MB, and it is the ONLY remaining item that cannot be dealt with on a running system. Measured
|
|
9
|
+
on this fleet, as NT AUTHORITY\SYSTEM, all three routes fail:
|
|
10
|
+
|
|
11
|
+
Set-MpPreference -DisableRealtimeMonitoring accepted, then silently reverted
|
|
12
|
+
WinDefend\Start = 4 ERROR: Access is denied
|
|
13
|
+
Policies\...\DisableAntiSpyware = 1 writes fine, and Windows ignores it while TP is on
|
|
14
|
+
|
|
15
|
+
Tamper Protection is a RUNTIME guard, so offline is not a workaround, it is the supported way: with
|
|
16
|
+
nothing booted there is no driver defending the hive, and it is just a file.
|
|
17
|
+
|
|
18
|
+
IMPORTANT, and it changes what this script can do: `Get-WindowsPackage -Online` on this ARM64 image
|
|
19
|
+
returns NO Defender package. tiny11 removes `Windows-Defender-Client-Package~31bf3856ad364e35~` on
|
|
20
|
+
x64 22621; there is no equivalent here, so DISM package REMOVAL cannot take Defender out offline
|
|
21
|
+
either. The offline SYSTEM-hive service disable below is the mechanism that actually works. The
|
|
22
|
+
feature-package loop still tries, because a future x64 image will have the package and removing it is
|
|
23
|
+
strictly better than disabling it.
|
|
24
|
+
|
|
25
|
+
Everything else that could be trimmed already has been, from the running guest, by
|
|
26
|
+
packages/nvda-worker/src/windows-trim.mjs. That yielded ~60 MB, because this ARM64 image ships with exactly
|
|
27
|
+
three provisioned Appx packages (Edge, DevHome, CrossDevice) and none of nano11's targets.
|
|
28
|
+
|
|
29
|
+
.NOTES
|
|
30
|
+
WHERE THIS RUNS
|
|
31
|
+
It needs DISM, so it runs on Windows, elevated -- in practice on one of the capture guests, driven
|
|
32
|
+
through `utmctl exec`, which runs as SYSTEM. It cannot run on the Mac.
|
|
33
|
+
|
|
34
|
+
WHAT IT DELIBERATELY KEEPS, AND WHY THAT MATTERS MORE THAN WHAT IT REMOVES
|
|
35
|
+
nano11builder and tiny11builder both delete Microsoft Edge and the LanguageFeatures Speech and
|
|
36
|
+
TextToSpeech packages. Those are the browser we capture through and the `oneCore` synth NVDA is
|
|
37
|
+
configured to use. An image without them does not fail loudly -- it produces empty transcripts that
|
|
38
|
+
look exactly like the NVDA mute faults this project has already spent days chasing. So the keep-list
|
|
39
|
+
is asserted before the image is committed, and the build FAILS rather than shipping a silent guest.
|
|
40
|
+
|
|
41
|
+
ARCHITECTURE
|
|
42
|
+
nano11builder hardcodes `amd64~~10.0.22621.1265` into package names, which is why it is x64-only.
|
|
43
|
+
These guests are ARM64. Every package name here is queried from the mounted image instead.
|
|
44
|
+
|
|
45
|
+
NO /ResetBase
|
|
46
|
+
tiny11's core variant runs `/Cleanup-Image /StartComponentCleanup /ResetBase` and deletes WinSxS
|
|
47
|
+
down to an allow-list. That is most of its disk saving and NONE of its memory saving -- WinSxS is a
|
|
48
|
+
component store on disk, not resident memory -- and it leaves an image that can never take a driver,
|
|
49
|
+
feature or update again. Our constraint is RAM. Skipped by default; -ResetBase if you ever need disk.
|
|
50
|
+
#>
|
|
51
|
+
[CmdletBinding()]
|
|
52
|
+
param(
|
|
53
|
+
# Drive letter of a mounted Windows 11 ISO, e.g. 'E'.
|
|
54
|
+
[Parameter(Mandatory)][ValidatePattern('^[A-Za-z]$')][string] $SourceDrive,
|
|
55
|
+
[string] $WorkDir = 'C:\lean11',
|
|
56
|
+
[string] $OutputIso = 'C:\lean11.iso',
|
|
57
|
+
[int] $ImageIndex = 0, # 0 = pick the Pro edition automatically
|
|
58
|
+
[switch] $ResetBase, # see NOTES: disk only, and makes the image unserviceable
|
|
59
|
+
[switch] $KeepMounted # leave the image mounted for inspection instead of committing
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
$ErrorActionPreference = 'Stop'
|
|
63
|
+
function Step($m) { Write-Host "`n==> $m" -ForegroundColor Cyan }
|
|
64
|
+
function OK($m) { Write-Host " OK $m" -ForegroundColor Green }
|
|
65
|
+
function Warn($m) { Write-Host " WARN $m" -ForegroundColor Yellow }
|
|
66
|
+
|
|
67
|
+
# --- what must survive, and what may go -------------------------------------
|
|
68
|
+
|
|
69
|
+
# Asserted present after every removal. The build fails if any of these vanish, because a guest that
|
|
70
|
+
# cannot run Edge or speak is indistinguishable from the mute faults we already chase.
|
|
71
|
+
$MustKeepPatterns = @(
|
|
72
|
+
'LanguageFeatures-Speech', # NVDA's oneCore synth
|
|
73
|
+
'LanguageFeatures-TextToSpeech'
|
|
74
|
+
)
|
|
75
|
+
$MustKeepPaths = @(
|
|
76
|
+
'Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Feature packages safe to remove. Speech, TextToSpeech and anything Edge are absent BY DESIGN.
|
|
80
|
+
$RemovableFeaturePatterns = @(
|
|
81
|
+
'Microsoft-Windows-InternetExplorer-Optional-Package',
|
|
82
|
+
'Microsoft-Windows-MediaPlayer-Package',
|
|
83
|
+
'Microsoft-Windows-WordPad-FoD-Package',
|
|
84
|
+
'Microsoft-Windows-StepsRecorder-Package',
|
|
85
|
+
'Microsoft-Windows-TabletPCMath-Package',
|
|
86
|
+
'Microsoft-Windows-Wallpaper-Content-Extended-FoD-Package',
|
|
87
|
+
'Microsoft-Windows-LanguageFeatures-Handwriting',
|
|
88
|
+
'Microsoft-Windows-LanguageFeatures-OCR',
|
|
89
|
+
# The reason this script exists. Removable offline only.
|
|
90
|
+
'Windows-Defender-Client-Package'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Provisioned Appx. Kept in sync with packages/nvda-worker/src/windows-trim.mjs, which has the unit tests
|
|
94
|
+
# proving Edge and the speech stack can never appear in a removal set.
|
|
95
|
+
$RemovableAppxPrefixes = @(
|
|
96
|
+
'Clipchamp.Clipchamp', 'Microsoft.BingNews', 'Microsoft.BingWeather', 'Microsoft.GamingApp',
|
|
97
|
+
'Microsoft.GetHelp', 'Microsoft.Getstarted', 'Microsoft.MicrosoftOfficeHub',
|
|
98
|
+
'Microsoft.MicrosoftSolitaireCollection', 'Microsoft.People', 'Microsoft.PowerAutomateDesktop',
|
|
99
|
+
'Microsoft.Todos', 'Microsoft.WindowsAlarms', 'microsoft.windowscommunicationsapps',
|
|
100
|
+
'Microsoft.WindowsFeedbackHub', 'Microsoft.WindowsMaps', 'Microsoft.WindowsSoundRecorder',
|
|
101
|
+
'Microsoft.Xbox.TCUI', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay',
|
|
102
|
+
'Microsoft.YourPhone', 'Microsoft.ZuneMusic', 'Microsoft.ZuneVideo',
|
|
103
|
+
'MicrosoftCorporationII.MicrosoftFamily', 'MicrosoftCorporationII.QuickAssist',
|
|
104
|
+
'MicrosoftTeams', 'MSTeams', 'Microsoft.Windows.Copilot', 'Microsoft.Copilot',
|
|
105
|
+
'Microsoft.OutlookForWindows', 'Microsoft.549981C3F5F10',
|
|
106
|
+
'Microsoft.Windows.DevHome', 'MicrosoftWindows.CrossDevice'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Anything matching these is never removed, whatever the lists above say. Deliberately blunter than it
|
|
110
|
+
# needs to be: leaving a few MB of Xbox speech-to-text captioning behind is the right price for a rule
|
|
111
|
+
# that cannot take out NVDA's synth.
|
|
112
|
+
$NeverRemovePatterns = @(
|
|
113
|
+
'edge', 'webview', 'speech', 'texttospeech', 'onecore', 'narrator', 'accessib',
|
|
114
|
+
'uiautomation', 'dotnet', 'netfx', 'vclibs', 'ui.xaml', 'runtime', 'servicingstack'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
function Test-NeverRemove([string] $name) {
|
|
118
|
+
foreach ($p in $NeverRemovePatterns) { if ($name -match [regex]::Escape($p)) { return $true } }
|
|
119
|
+
return $false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
123
|
+
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
124
|
+
throw 'Must run elevated. DISM offline servicing needs it, as does every removal below.'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$mount = Join-Path $WorkDir 'mount'
|
|
128
|
+
$src = "${SourceDrive}:"
|
|
129
|
+
if (-not (Test-Path "$src\sources")) { throw "No \sources on ${src} -- is the ISO mounted?" }
|
|
130
|
+
|
|
131
|
+
# --- copy and mount ----------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
Step "Copying $src to $WorkDir"
|
|
134
|
+
New-Item -ItemType Directory -Force -Path $WorkDir, $mount | Out-Null
|
|
135
|
+
Copy-Item -Path "$src\*" -Destination $WorkDir -Recurse -Force
|
|
136
|
+
$wim = Join-Path $WorkDir 'sources\install.wim'
|
|
137
|
+
if (-not (Test-Path $wim)) {
|
|
138
|
+
$esd = Join-Path $WorkDir 'sources\install.esd'
|
|
139
|
+
if (-not (Test-Path $esd)) { throw 'Neither install.wim nor install.esd found in sources.' }
|
|
140
|
+
Step 'Converting install.esd to install.wim'
|
|
141
|
+
$idx = if ($ImageIndex -gt 0) { $ImageIndex } else {
|
|
142
|
+
(Get-WindowsImage -ImagePath $esd | Where-Object ImageName -match 'Pro' |
|
|
143
|
+
Select-Object -First 1).ImageIndex
|
|
144
|
+
}
|
|
145
|
+
& dism /English /Export-Image /SourceImageFile:$esd /SourceIndex:$idx `
|
|
146
|
+
/DestinationImageFile:$wim /Compress:max /CheckIntegrity
|
|
147
|
+
Remove-Item $esd -Force
|
|
148
|
+
$ImageIndex = 1
|
|
149
|
+
}
|
|
150
|
+
Set-ItemProperty $wim -Name IsReadOnly -Value $false
|
|
151
|
+
|
|
152
|
+
if ($ImageIndex -le 0) {
|
|
153
|
+
$ImageIndex = (Get-WindowsImage -ImagePath $wim | Where-Object ImageName -match 'Pro' |
|
|
154
|
+
Select-Object -First 1).ImageIndex
|
|
155
|
+
if (-not $ImageIndex) { throw 'No Pro edition found; pass -ImageIndex explicitly.' }
|
|
156
|
+
}
|
|
157
|
+
OK "using image index $ImageIndex"
|
|
158
|
+
|
|
159
|
+
Step 'Mounting the image'
|
|
160
|
+
& dism /English /Mount-Image /ImageFile:$wim /Index:$ImageIndex /MountDir:$mount
|
|
161
|
+
if ($LASTEXITCODE -ne 0) { throw 'Mount-Image failed.' }
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
# --- removals, all queried from the image, never hardcoded ------------------
|
|
165
|
+
|
|
166
|
+
Step 'Removing provisioned apps'
|
|
167
|
+
$appx = Get-AppxProvisionedPackage -Path $mount
|
|
168
|
+
foreach ($pkg in $appx) {
|
|
169
|
+
$name = $pkg.PackageName
|
|
170
|
+
if (Test-NeverRemove $name) { continue }
|
|
171
|
+
if (-not ($RemovableAppxPrefixes | Where-Object { $name -like "$_*" })) { continue }
|
|
172
|
+
try {
|
|
173
|
+
Remove-AppxProvisionedPackage -Path $mount -PackageName $name -ErrorAction Stop | Out-Null
|
|
174
|
+
OK "removed appx $($name.Split('_')[0])"
|
|
175
|
+
} catch { Warn "could not remove $name : $($_.Exception.Message)" }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Step 'Removing feature packages (including Defender, which is the point)'
|
|
179
|
+
$packages = Get-WindowsPackage -Path $mount
|
|
180
|
+
foreach ($pattern in $RemovableFeaturePatterns) {
|
|
181
|
+
foreach ($pkg in ($packages | Where-Object { $_.PackageName -like "$pattern*" })) {
|
|
182
|
+
if (Test-NeverRemove $pkg.PackageName) { Warn "kept $($pkg.PackageName) (keep-list)"; continue }
|
|
183
|
+
try {
|
|
184
|
+
Remove-WindowsPackage -Path $mount -PackageName $pkg.PackageName -ErrorAction Stop | Out-Null
|
|
185
|
+
OK "removed $($pkg.PackageName)"
|
|
186
|
+
} catch { Warn "could not remove $($pkg.PackageName): $($_.Exception.Message)" }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Defender, disabled in the offline hives. Tamper Protection is a runtime guard and there is no
|
|
191
|
+
# runtime here, which is the whole reason this step is in an image build and not in windows-trim.mjs.
|
|
192
|
+
#
|
|
193
|
+
# ORDER AND HIVE BOTH MATTER, and getting either wrong makes the whole step a no-op.
|
|
194
|
+
# Tamper Protection is itself a registry value, and it lives in SOFTWARE, not SYSTEM:
|
|
195
|
+
# `Microsoft\Windows Defender\Features\TamperProtection`. Disabling the services in SYSTEM while
|
|
196
|
+
# leaving that flag armed means the guest boots with Tamper Protection active and reverts them --
|
|
197
|
+
# which is exactly what happens on a live system, and would make this build look like it did nothing.
|
|
198
|
+
# Clear the flag first, then the services.
|
|
199
|
+
Step 'Disarming Tamper Protection in the offline SOFTWARE hive'
|
|
200
|
+
& reg load HKLM\zSOFTWARE "$mount\Windows\System32\config\SOFTWARE" | Out-Null
|
|
201
|
+
try {
|
|
202
|
+
$features = 'HKLM:\zSOFTWARE\Microsoft\Windows Defender\Features'
|
|
203
|
+
New-Item -Path $features -Force | Out-Null
|
|
204
|
+
Set-ItemProperty $features -Name TamperProtection -Value 0 -Type DWord
|
|
205
|
+
Set-ItemProperty $features -Name TamperProtectionSource -Value 0 -Type DWord -ErrorAction SilentlyContinue
|
|
206
|
+
OK 'TamperProtection = 0'
|
|
207
|
+
$policy = 'HKLM:\zSOFTWARE\Policies\Microsoft\Windows Defender'
|
|
208
|
+
New-Item -Path $policy -Force | Out-Null
|
|
209
|
+
Set-ItemProperty $policy -Name DisableAntiSpyware -Value 1 -Type DWord
|
|
210
|
+
Set-ItemProperty $policy -Name DisableAntiVirus -Value 1 -Type DWord
|
|
211
|
+
New-Item -Path "$policy\Real-Time Protection" -Force | Out-Null
|
|
212
|
+
Set-ItemProperty "$policy\Real-Time Protection" -Name DisableRealtimeMonitoring -Value 1 -Type DWord
|
|
213
|
+
OK 'DisableAntiSpyware / DisableRealtimeMonitoring policies set'
|
|
214
|
+
} finally {
|
|
215
|
+
# The GC call is not superstition: PowerShell keeps hive handles open and `reg unload` fails with
|
|
216
|
+
# "Access is denied" while they live, leaving the image mounted with a loaded hive.
|
|
217
|
+
[gc]::Collect(); [gc]::WaitForPendingFinalizers()
|
|
218
|
+
& reg unload HKLM\zSOFTWARE | Out-Null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Step 'Disabling Defender services in the offline SYSTEM hive'
|
|
222
|
+
& reg load HKLM\zSYSTEM "$mount\Windows\System32\config\SYSTEM" | Out-Null
|
|
223
|
+
try {
|
|
224
|
+
foreach ($svc in 'WinDefend', 'WdNisSvc', 'WdNisDrv', 'WdFilter', 'WdBoot', 'Sense') {
|
|
225
|
+
$key = "HKLM:\zSYSTEM\ControlSet001\Services\$svc"
|
|
226
|
+
if (Test-Path $key) { Set-ItemProperty $key -Name Start -Value 4; OK "$svc disabled" }
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
[gc]::Collect(); [gc]::WaitForPendingFinalizers()
|
|
230
|
+
& reg unload HKLM\zSYSTEM | Out-Null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# --- the assertions that make this safe to ship -----------------------------
|
|
234
|
+
|
|
235
|
+
Step 'Verifying the image can still capture'
|
|
236
|
+
$after = Get-WindowsPackage -Path $mount
|
|
237
|
+
foreach ($needed in $MustKeepPatterns) {
|
|
238
|
+
if (-not ($after | Where-Object { $_.PackageName -like "*$needed*" })) {
|
|
239
|
+
throw "FATAL: $needed is missing from the image. NVDA would be silent. Refusing to commit."
|
|
240
|
+
}
|
|
241
|
+
OK "$needed present"
|
|
242
|
+
}
|
|
243
|
+
foreach ($path in $MustKeepPaths) {
|
|
244
|
+
if (-not (Test-Path (Join-Path $mount $path))) {
|
|
245
|
+
throw "FATAL: $path is missing. There would be no browser to capture through. Refusing to commit."
|
|
246
|
+
}
|
|
247
|
+
OK "$path present"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if ($ResetBase) {
|
|
251
|
+
Warn 'ResetBase: disk only, and the image can never be serviced again.'
|
|
252
|
+
& dism /English /Image:$mount /Cleanup-Image /StartComponentCleanup /ResetBase
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
Step 'Build failed -- discarding the mount so a broken image is never written'
|
|
256
|
+
& dism /English /Unmount-Image /MountDir:$mount /Discard | Out-Null
|
|
257
|
+
throw
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if ($KeepMounted) { OK "left mounted at $mount for inspection"; return }
|
|
261
|
+
|
|
262
|
+
Step 'Committing and unmounting'
|
|
263
|
+
& dism /English /Unmount-Image /MountDir:$mount /Commit
|
|
264
|
+
if ($LASTEXITCODE -ne 0) { throw 'Unmount /Commit failed.' }
|
|
265
|
+
|
|
266
|
+
Step 'Building the ISO'
|
|
267
|
+
$oscdimg = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit' `
|
|
268
|
+
-Recurse -Filter oscdimg.exe -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
269
|
+
if (-not $oscdimg) {
|
|
270
|
+
Warn "oscdimg.exe not found (install the Windows ADK). The serviced tree is at $WorkDir."
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
$boot = "$WorkDir\efi\microsoft\boot\efisys.bin"
|
|
274
|
+
& $oscdimg.FullName -m -o -u2 -udfver102 -bootdata:"1#pEF,e,b$boot" $WorkDir $OutputIso
|
|
275
|
+
OK "wrote $OutputIso"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Diagnose a broken NVDA capture worker. Read-only: changes nothing.
|
|
2
|
+
#
|
|
3
|
+
# Checks every layer in the order that failures actually cascade, and prints a
|
|
4
|
+
# VERDICT per layer rather than raw dumps, so the first FAIL is the thing to fix.
|
|
5
|
+
# Exits non-zero if any check failed. Copy it over and run it with -File:
|
|
6
|
+
#
|
|
7
|
+
# scp packages/worker-fleet/src/provisioning/diagnose-nvda-worker.ps1 user@host:C:/Users/user/
|
|
8
|
+
# ssh user@host "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Users\user\diagnose-nvda-worker.ps1"
|
|
9
|
+
#
|
|
10
|
+
# Do NOT pipe this to `powershell -Command -`. That mode silently truncated this
|
|
11
|
+
# script mid-run (the summary and exit code never executed), and a leading `<# #>`
|
|
12
|
+
# block comment suppresses its output entirely. Reserve stdin piping for short
|
|
13
|
+
# ad-hoc snippets; use -File for anything real.
|
|
14
|
+
#
|
|
15
|
+
# Every check here corresponds to a real outage. The mapping from error text to
|
|
16
|
+
# cause is in docs/nvda-worker-runbook.md; this script applies it automatically.
|
|
17
|
+
#
|
|
18
|
+
# Config comes from the environment rather than param(), so the same file works in
|
|
19
|
+
# either invocation mode:
|
|
20
|
+
# A11Y_REPO_PATH (default %USERPROFILE%\a11y-witness)
|
|
21
|
+
# A11Y_PORT (default 8765)
|
|
22
|
+
|
|
23
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
24
|
+
|
|
25
|
+
$RepoPath = if ($env:A11Y_REPO_PATH) { $env:A11Y_REPO_PATH } else { Join-Path $env:USERPROFILE 'a11y-witness' }
|
|
26
|
+
$Port = if ($env:A11Y_PORT) { [int] $env:A11Y_PORT } else { 8765 }
|
|
27
|
+
$fails = @()
|
|
28
|
+
|
|
29
|
+
function Section($t) { Write-Host "`n== $t ==" -ForegroundColor Cyan }
|
|
30
|
+
function Pass($m) { Write-Host " PASS $m" -ForegroundColor Green }
|
|
31
|
+
function Fail($m, $fix) {
|
|
32
|
+
Write-Host " FAIL $m" -ForegroundColor Red
|
|
33
|
+
Write-Host " fix: $fix" -ForegroundColor Yellow
|
|
34
|
+
$script:fails += $m
|
|
35
|
+
}
|
|
36
|
+
function Info($m) { Write-Host " info $m" -ForegroundColor DarkGray }
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
Section 'Layer 1: interactive desktop session'
|
|
40
|
+
# Without a logged-on console session NVDA runs but announces NOTHING, and every
|
|
41
|
+
# capture comes back empty with no error -- the most confusing failure mode there is.
|
|
42
|
+
$sessions = query session 2>$null
|
|
43
|
+
Info (($sessions | Out-String).Trim())
|
|
44
|
+
if ($sessions | Select-String '^\s*>?\s*console\s+\S+\s+\d+\s+Active') {
|
|
45
|
+
Pass 'console session Active'
|
|
46
|
+
} else {
|
|
47
|
+
Fail 'no Active console session' 'log in at the VM console (not SSH); enable auto-logon so reboots recover'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# A modal dialog freezes the whole interactive session invisibly: captures return 0
|
|
51
|
+
# phrases even on a known-good page, with no error anywhere.
|
|
52
|
+
$dialogs = Get-Process | Where-Object { $_.MainWindowTitle -and $_.SessionId -ne 0 } |
|
|
53
|
+
Select-Object Name, MainWindowTitle
|
|
54
|
+
if ($dialogs) { Info "windows on the desktop: $((($dialogs | ForEach-Object { "$($_.Name):$($_.MainWindowTitle)" }) -join ' | '))" }
|
|
55
|
+
if (Get-Process LogonUI) { Fail 'LogonUI is running: the desktop is LOCKED' 'unlock the console; disable the screensaver/lock' }
|
|
56
|
+
else { Pass 'desktop not locked' }
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
Section 'Layer 2: worker process and port'
|
|
60
|
+
$node = Get-Process node
|
|
61
|
+
if ($node) { Pass "node running (pid $($node.Id -join ','), session $($node.SessionId -join ','))" }
|
|
62
|
+
else { Fail 'no node process' "Start-ScheduledTask -TaskName a11ysrv" }
|
|
63
|
+
|
|
64
|
+
if (Get-NetTCPConnection -LocalPort $Port -State Listen) { Pass "listening on $Port" }
|
|
65
|
+
else { Fail "nothing listening on $Port" "Start-ScheduledTask -TaskName a11ysrv; then check $RepoPath\server.log" }
|
|
66
|
+
|
|
67
|
+
$task = Get-ScheduledTask -TaskName 'a11ysrv'
|
|
68
|
+
if ($task) {
|
|
69
|
+
$trigCount = ($task.Triggers | Measure-Object).Count
|
|
70
|
+
if ($task.Principal.LogonType -ne 'Interactive') {
|
|
71
|
+
Fail "task LogonType is $($task.Principal.LogonType), not Interactive" 're-register with -LogonType Interactive, or NVDA gets no desktop'
|
|
72
|
+
} else { Pass 'task LogonType Interactive' }
|
|
73
|
+
if ($trigCount -eq 0) {
|
|
74
|
+
Fail 'task has NO trigger: it will not restart after a reboot' 'add: Set-ScheduledTask -TaskName a11ysrv -Trigger (New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME)'
|
|
75
|
+
} else { Pass "task has $trigCount trigger(s)" }
|
|
76
|
+
} else { Fail 'scheduled task a11ysrv is not registered' 'run scripts/provision-nvda-worker.ps1' }
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
Section 'Layer 3: guidepup / NVDA version pairing'
|
|
80
|
+
$gpJson = Join-Path $RepoPath 'node_modules\@guidepup\guidepup\package.json'
|
|
81
|
+
if (Test-Path $gpJson) {
|
|
82
|
+
$gp = (Get-Content $gpJson -Raw | ConvertFrom-Json).version
|
|
83
|
+
Info "@guidepup/guidepup $gp"
|
|
84
|
+
if ([version]($gp -replace '-.*$') -lt [version]'0.29.0') {
|
|
85
|
+
Fail "guidepup $gp cannot drive NVDA 2026.x" 'bump to >=0.29.2; symptom is "NVDA not installed" thrown from NVDAClient.connect'
|
|
86
|
+
} else { Pass "guidepup $gp speaks NVDA 2026 core Remote Access" }
|
|
87
|
+
} else { Fail '@guidepup/guidepup not installed' 'corepack pnpm install --frozen-lockfile' }
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
Section 'Layer 4: NVDA install integrity'
|
|
91
|
+
# guidepup >=0.29 resolves the install from this cache path, NOT the old
|
|
92
|
+
# HKCU\Software\Guidepup\Nvda registry pointer.
|
|
93
|
+
$cacheRoot = if ($env:GUIDEPUP_SCREEN_READERS_PATH) { $env:GUIDEPUP_SCREEN_READERS_PATH } else { Join-Path $env:LOCALAPPDATA 'guidepup' }
|
|
94
|
+
Info "cache root: $cacheRoot"
|
|
95
|
+
$nvdaExe = Get-ChildItem (Join-Path $cacheRoot 'nvda') -Recurse -Filter 'nvda.exe' |
|
|
96
|
+
Sort-Object FullName -Descending | Select-Object -First 1
|
|
97
|
+
|
|
98
|
+
if (-not $nvdaExe) {
|
|
99
|
+
Fail "no nvda.exe under $cacheRoot" 'run from the repo: npx @guidepup/setup install nvda (symptom: "NVDA is not supported")'
|
|
100
|
+
} else {
|
|
101
|
+
$dir = Split-Path $nvdaExe.FullName
|
|
102
|
+
$count = (Get-ChildItem $dir -Recurse -File).Count
|
|
103
|
+
$mb = [Math]::Round(((Get-ChildItem $dir -Recurse -File | Measure-Object Length -Sum).Sum / 1MB), 1)
|
|
104
|
+
Info "$dir -- $count files, $mb MB"
|
|
105
|
+
# A stub install (temp cleanup deleted the payload) launches and dies instantly:
|
|
106
|
+
# "Timed out waiting for NVDA to be running", and no nvda.log is ever written.
|
|
107
|
+
if ($count -lt 500) {
|
|
108
|
+
Fail "install looks GUTTED ($count files, expected ~1700+)" 'delete that directory and re-run npx @guidepup/setup install nvda'
|
|
109
|
+
} else { Pass "install intact ($count files, $mb MB)" }
|
|
110
|
+
foreach ($f in @('library.zip', 'nvda_slave.exe', 'nvda.exe')) {
|
|
111
|
+
if (Test-Path (Join-Path $dir $f)) { Pass " $f present" }
|
|
112
|
+
else { Fail " $f MISSING" 'the install is incomplete; reinstall NVDA' }
|
|
113
|
+
}
|
|
114
|
+
if (Test-Path (Join-Path $dir 'userConfig\remoteAccess\localRelay\NvdaRemoteRelay.pem')) {
|
|
115
|
+
Pass ' Remote Access relay cert present (NVDA 2026 core)'
|
|
116
|
+
} elseif (Test-Path (Join-Path $dir 'userConfig\addons\remote\globalPlugins\remoteClient\server.pem')) {
|
|
117
|
+
Pass ' legacy NVDA Remote add-on cert present'
|
|
118
|
+
} else {
|
|
119
|
+
Fail ' no speech-channel certificate' 'reinstall NVDA; without it connect() throws "NVDA not installed"'
|
|
120
|
+
}
|
|
121
|
+
# The single most damaging silent misconfiguration: probes record the Speech
|
|
122
|
+
# Viewer window instead of the page's response, and capture-check still passes.
|
|
123
|
+
if (Get-ChildItem $dir -Recurse -Filter 'nvda.ini' | Select-String 'showSpeechViewerAtStartup = True') {
|
|
124
|
+
Fail ' Speech Viewer is ENABLED: interaction probes will record "NVDA Speech Viewer"' 'set showSpeechViewerAtStartup = False in the install userConfig\nvda.ini'
|
|
125
|
+
} else { Pass ' Speech Viewer disabled' }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
Section 'Layer 5: focus and dialog hygiene'
|
|
130
|
+
$flt = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ForegroundLockTimeout).ForegroundLockTimeout
|
|
131
|
+
if ($flt -eq 0) { Pass 'ForegroundLockTimeout = 0' } else { Fail "ForegroundLockTimeout = $flt" 'npx @guidepup/setup setup' }
|
|
132
|
+
|
|
133
|
+
$ss = (Get-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive).ScreenSaveActive
|
|
134
|
+
if ($ss -eq '0') { Pass 'screensaver disabled' } else { Fail "screensaver enabled ($ss)" 'set ScreenSaveActive=0; it steals foreground mid-capture' }
|
|
135
|
+
|
|
136
|
+
$notify = (Get-NetFirewallProfile).NotifyOnListen | Sort-Object -Unique
|
|
137
|
+
if ($notify -contains 'True') { Fail "firewall NotifyOnListen is True ($($notify -join ','))" 'Set-NetFirewallProfile -Profile Domain,Private,Public -NotifyOnListen False -- the allow-app dialog is unclickable on a VM' }
|
|
138
|
+
else { Pass 'firewall NotifyOnListen False (no allow-app dialogs)' }
|
|
139
|
+
|
|
140
|
+
$edge = Get-ItemProperty 'HKLM:\SOFTWARE\Policies\Microsoft\Edge'
|
|
141
|
+
if ($edge.HideFirstRunExperience -eq 1) { Pass 'Edge HideFirstRunExperience = 1' }
|
|
142
|
+
else { Fail 'Edge first-run experience not suppressed' 'set HKLM\SOFTWARE\Policies\Microsoft\Edge HideFirstRunExperience=1, BrowserSignin=0' }
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
Section 'Layer 6: last capture outcome'
|
|
146
|
+
$log = Join-Path $RepoPath 'server.log'
|
|
147
|
+
if (Test-Path $log) {
|
|
148
|
+
Get-Content $log -Tail 12 | ForEach-Object { Info $_ }
|
|
149
|
+
$body = Get-Content $log -Raw
|
|
150
|
+
# Map the exact error strings to their real causes -- each of these was, at least
|
|
151
|
+
# once, misread as something else.
|
|
152
|
+
if ($body -match 'Timed out waiting for NVDA to be running') {
|
|
153
|
+
Fail 'log: NVDA start timed out' 'NVDA install is gutted/stub (Layer 4), or no interactive desktop (Layer 1)'
|
|
154
|
+
}
|
|
155
|
+
if ($body -match 'NVDA not installed') {
|
|
156
|
+
Fail 'log: "NVDA not installed"' 'MISLEADING: usually thrown by NVDAClient.connect when the speech-channel cert is missing or guidepup is too old (Layers 3-4), NOT a missing install'
|
|
157
|
+
}
|
|
158
|
+
if ($body -match 'NVDA is not supported') {
|
|
159
|
+
Fail 'log: "NVDA is not supported"' 'guidepup resolved no install at its cache path (Layer 4): npx @guidepup/setup install nvda'
|
|
160
|
+
}
|
|
161
|
+
if ($body -match 'afterStart\.lastSpoken=""' -and $body -match '-> 0 phrases') {
|
|
162
|
+
Fail 'log: 0 phrases and NVDA silent right after start' 'no interactive desktop, or a modal dialog is blocking the session (Layer 1)'
|
|
163
|
+
}
|
|
164
|
+
} else { Info "no server.log at $log yet" }
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
Write-Host ''
|
|
168
|
+
if ($fails.Count -eq 0) {
|
|
169
|
+
Write-Host 'ALL CHECKS PASSED -- if capture still fails, run a real capture and inspect' -ForegroundColor Green
|
|
170
|
+
Write-Host 'diagnostics[].afterStart.lastSpoken plus interaction.formChanges in the response.' -ForegroundColor Green
|
|
171
|
+
} else {
|
|
172
|
+
Write-Host "$($fails.Count) CHECK(S) FAILED -- fix the FIRST one; later failures are usually downstream." -ForegroundColor Red
|
|
173
|
+
exit 1
|
|
174
|
+
}
|