**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).
fleetdm.com
This is where the code for the public https://fleetdm.com website lives.
Bugs
To report a bug or make a suggestion for the website, create an issue in the fleet GitHub repository.
Testing locally
See https://fleetdm.com/handbook/engineering#test-fleetdm-com-locally
Deploying the website
To deploy changes to the website to production, merge changes to the main branch. If the changes affect the website's code, or touch any files that the website relies on to build content, such as the query library, osquery schema, docs, handbook, articles, etc., then the website will be redeployed.
Wondering how this works? This is implemented in a GitHub action in this repo. Check out the code there to see how it works! For help understanding what
sails runandnpm runcommands in there do, check the scripts inwebsite/package.jsonand inwebsite/scripts/.
Changing the database schema
To deploy new code to production that relies on changes to the database schema or other external systems (e.g. Stripe), first put the website in "maintenance mode" in Heroku. Then, make your changes in the database schema. Next, if you have a script to fix/migrate existing data, go ahead and run it now. (e.g. sails run fix-or-migrate-existing-data). Then, merge your changes and wait for the deploy to finish. Finally, switch off "maintenance mode" in Heroku.
Note that entering maintenance mode prevents visitors from using the website, so it should be used sparingly, and ideally at low-traffic times of day.
Warning: Doing an especially sensitive schema migration? There is a potential timing issue to consider, thanks to an infrastructure change that eliminated downtime during deploys by using Heroku's built-in support for hot-swapping. Read more in https://github.com/fleetdm/fleet/issues/6568#issuecomment-1211503881
Wiping the production database
I hope you know what you're doing. The "easiest" kind of database schema migration:
sails_datastores__default__url='REAL_DB_URI_HERE' sails run wipe
Then when you see the sailboat, hit CTRL+C to exit. All done!