Files
fleet/ee/maintained-apps/inputs/winget
Allen Houchins 89acb97395 Add NVDA as a Windows Fleet-maintained app (#50450)
**Related issue:** Resolves #50125

Adds NVDA as a Windows Fleet-maintained app, from winget `NVAccess.NVDA`
(2026.1.1, NSIS/nullsoft, x86 launcher).

## Identity — read out of the shipped installer, not the manifest

I downloaded the 60 MB installer, extracted the NSIS payload, and read
the identity fields from `_buildVersion.pyc` and the PE headers. The
winget manifest is misleading in two ways:

| Field | winget says | Actually is | Source |
|---|---|---|---|
| Architecture | `x86` | **x64** app behind a 32-bit NSIS launcher stub
| `nvda_noUIAccess.exe` / `nvda_slave.exe` PE headers |
| Registry DisplayName | PackageName `NVDA` | **`NVDA 2026.1.1`** |
`source/installer.py` `getUninstallerRegInfo()`: `DisplayName=f"{name}
{version}"` |
| Publisher | `NV Access` | `NV Access` (matches) | `_buildVersion.pyc`:
`publisher = "NV Access"` |

Two consequences:

- Because NVDA itself is a **64-bit** process, it registers under the
native registry view, **not** `Wow6432Node` (the launcher's 32-bit-ness
is irrelevant). Both scripts check both views anyway, for legacy 32-bit
copies.
- DisplayName carries the version, so this needs `fuzzy_match_name:
true` → `name LIKE 'NVDA %'`. Publisher matches the locale manifest, so
no `program_publisher` override.

`installer_arch` stays `x86` because that's what the manifest declares
and the ingester matches on it.

## Version reconciles without a validator exception

DisplayVersion is the 4-part `2026.1.1.55980` (`version_detailed`)
against winget's `2026.1.1`:

- **Validator:** passes via the existing
`strings.HasPrefix(result.Version, appVersion+".")` branch in
`cmd/maintained-apps/validate/windows.go`. No new skip added —
deliberately, since existence-only skips make patch policies always
report "patched".
- **Patch policy:** `version_compare('2026.1.1.55980', '2026.1.1')` is
`> 0`, so an installed copy reads as newer, not outdated. No perpetual
false "update available".

## The install script can't trust the exit code

`source/gui/installerGui.py` `doInstall()` pops `winUser.MessageBox` /
`gui.messageBox` on **every** install failure path with **no `if silent`
guard**, and then falls through and exits **0**. Under SYSTEM in session
0 that means:

1. a failure **hangs forever** — nobody can click Retry/Cancel; and
2. if it were dismissed, a failed install would report **success**.

So `nvda_install.ps1` uses a watchdog plus an Add/Remove Programs
registration poll as the real success signal — the same shape as the
existing `azure_data_studio_install.ps1`. Timeouts are 420 + 120 + 30 =
570s, under the caller's 10-minute cap.

On timeout it kills only the launcher's `%TEMP%` children
(`nvda_noUIAccess` / `nvda_uiAccess`), **deliberately not `nvda.exe`** —
an installed NVDA runs as `nvda.exe`, and force-killing it would cut off
a signed-in user's screen reader with no warning.

## Uninstall

Vendor-documented `/S` (NVDA user guide, "Uninstalling NVDA"), plus
`_?=` last so the NSIS uninstaller runs in place instead of relaunching
from `%TEMP%` and returning immediately. NVDA writes **no**
`QuietUninstallString`, and its `UninstallString` is an **unquoted path
containing spaces** (`C:\Program Files\NVDA\uninstall.exe`), so the
parser handles that form. The directory comes from NVDA's `InstallDir`
value (not `InstallLocation`). Absence of the ARP entry is the success
signal, since NVDA removes it via `nvda_slave.exe unregisterInstall`.

## Reviewer notes

- **`installer_scope` is `""`, not `"machine"`.** NVDA genuinely
installs machine-wide (`%ProgramFiles%\NVDA` + HKLM), but the winget
manifest declares no `Scope`, so the ingester derives `""` and
`"machine"` panics with "failed to find installer". The one-line
ingester fix for this is designed in #48248 but isn't in `main`; I chose
not to change shared installer-selection code for a single-app addition.
Happy to land that fix here instead if preferred.
- **Upgrade caveat:** if NVDA is running for a signed-in user,
`--install-silent` refuses to overwrite its own running files by design
(`installer.py` `install()`). The script fails with an actionable
message rather than force-killing the screen reader.
- Installer URL is version-pinned
(`download.nvaccess.org/releases/2026.1.1/...`), not a "latest"
redirect. SHA verified against my own download of the file.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [ ] QA'd all new/changed functionality manually

`go test ./ee/maintained-apps/...` passes; prettier and `tsc --noEmit`
are clean. I have no Windows host or `pwsh`, so **the install/uninstall
scripts are unexercised** until FMA validation CI runs them on a Windows
runner. No changes file — consistent with other FMA additions (#50415,
#50348, #50352).
2026-08-03 12:40:44 -05:00
..
2026-06-07 12:21:33 -05:00
2026-06-15 12:06:24 -05:00
2026-06-07 08:26:36 -05:00
2026-05-28 21:32:29 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-05-26 15:37:28 -05:00
2026-05-28 11:06:13 -05:00
2026-05-28 11:06:13 -05:00
2026-06-12 22:25:19 -05:00
2026-06-12 22:25:19 -05:00
2026-05-29 20:38:57 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-05-28 11:06:13 -05:00
2026-05-28 11:06:13 -05:00
2026-05-28 11:06:13 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-12 22:25:19 -05:00
2026-06-12 22:25:19 -05:00
2026-06-15 12:06:24 -05:00
2026-05-28 22:19:11 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-07 08:26:36 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-06-15 12:06:24 -05:00
2026-05-28 11:06:13 -05:00
2026-06-15 12:06:24 -05:00
2026-05-28 11:06:13 -05:00
2026-06-15 12:06:24 -05:00
2026-05-28 11:06:13 -05:00
2026-06-12 22:25:19 -05:00
2026-06-15 12:06:24 -05:00
2026-06-12 22:25:19 -05:00
2026-05-28 11:06:13 -05:00
2026-05-27 10:13:37 -05:00
2026-06-12 22:25:19 -05:00
2026-05-28 11:16:13 -07:00
2026-05-28 11:06:13 -05:00
2026-06-07 12:14:02 -05:00
2026-06-16 08:36:17 -05:00
2026-05-28 11:06:13 -05:00
2026-05-28 11:06:13 -05:00
2026-06-12 22:25:19 -05:00
2026-06-16 08:36:17 -05:00
2026-05-28 22:22:18 -05:00
2026-06-07 12:14:02 -05:00
2026-06-16 08:36:17 -05:00
2026-05-31 23:21:18 -05:00
2026-06-12 22:25:19 -05:00