Add Gpg4win as a Windows FMA - #50026
Conversation
Gpg4win bundles GnuPG, so the install leaves gpg-agent, dirmngr, keyboxd and scdaemon resident. Start-Process -Wait waits for descendants as well as the process itself, so the install script never returned and the validator killed it at its 10-minute cap. Wait on the installer process only, poll for the ARP entry, then stop the leftovers -- which also unlocks the files the uninstaller needs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #50026 +/- ##
==========================================
+ Coverage 67.97% 68.03% +0.06%
==========================================
Files 3922 3930 +8
Lines 250032 250268 +236
Branches 13334 13270 -64
==========================================
+ Hits 169949 170273 +324
+ Misses 64781 64691 -90
- Partials 15302 15304 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds Gpg4win as a Windows Fleet-maintained app (FMA), including install/uninstall scripts designed to avoid validator timeouts caused by lingering child processes, and adds a catalog icon + app metadata so it appears correctly in Fleet’s UI and FMA library.
Changes:
- Adds Winget input (
gpg4win.json) and PowerShell install/uninstall scripts for Gpg4win. - Adds generated FMA output manifest (
outputs/gpg4win/windows.json) and registers the app inoutputs/apps.json. - Adds a new software catalog icon component and maps
gpg4winto it.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/pages/SoftwarePage/components/icons/index.ts | Registers the Gpg4win icon in the software-name → icon map. |
| frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx | Adds the Gpg4win icon component. |
| ee/maintained-apps/inputs/winget/gpg4win.json | Adds Winget input metadata for the new Windows FMA. |
| ee/maintained-apps/inputs/winget/scripts/gpg4win_install.ps1 | Adds install script with bounded wait and post-install registration polling. |
| ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1 | Adds uninstall script with defensive UninstallString parsing and ARP polling. |
| ee/maintained-apps/outputs/gpg4win/windows.json | Adds the generated output manifest embedding scripts and queries. |
| ee/maintained-apps/outputs/apps.json | Adds the app entry to the maintained-apps catalog list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) { | ||
| Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it." | ||
| Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue | ||
| Start-Sleep -Seconds 2 | ||
| } | ||
|
|
||
| $exitCode = $process.ExitCode | ||
| Write-Host "Install exit code: $exitCode" |
There was a problem hiding this comment.
This is already handled — the finding looks to predate the current revision. .ExitCode is only read inside the else branch of if (-not $process.HasExited), so it's never touched while the process is alive; the timeout path logs and falls through to the registration check instead. Registration, not the exit code, is the success signal on that path, since a killed installer's code says nothing.
| # Parse the executable path, handling quoted paths, unquoted paths containing | ||
| # spaces, and bare tokens. | ||
| $uninstallCommand = $uninstallString | ||
| if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') { | ||
| $uninstallCommand = $Matches[1] | ||
| } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') { | ||
| $uninstallCommand = $Matches[1] | ||
| } elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') { | ||
| $uninstallCommand = $Matches[1] | ||
| } | ||
|
|
||
| # NSIS uninstallers copy themselves to %TEMP% and relaunch by default, so the | ||
| # process we start exits immediately while the real uninstall runs detached. | ||
| # "_?=<dir>" runs it in place instead, which makes it synchronous. It must be | ||
| # the last argument and must not be quoted, so pass one argument string rather | ||
| # than an array (PowerShell would quote an element containing spaces). | ||
| $installDir = Split-Path -Parent $uninstallCommand | ||
| $uninstallArgs = "/S _?=$installDir" |
There was a problem hiding this comment.
The quoting concern doesn't reproduce: this exact form installs and uninstalls Gpg4win under C:\Program Files\Gpg4win in CI (run 30372747618), a path with a space. NSIS requires _?= to be the final argument, and the single-string form is what's been validated for this installer, so I'd rather not swap it for an unvalidated one — the misleading part of the comment is fixed.
See also the reply on #50025: the repo uses both the array and quoted forms, and some NSIS uninstallers reject _?= entirely (DBeaver exit 2, Logitech Unifying exit 10).
| ], | ||
| "refs": { | ||
| "1c9be45b": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# Gpg4win bundles GnuPG, so the install starts the same resident daemons\n# (gpg-agent, dirmngr, keyboxd, scdaemon) and can leave Kleopatra running.\n# PowerShell's \"Start-Process -Wait\" waits for the process *and all of its\n# descendants*, so those keep the install script blocked indefinitely. Start\n# without -Wait, wait on the installer process itself with a timeout, then stop\n# the leftovers -- the same approach ollama_install.ps1 uses.\n$leftovers = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\", \"kleopatra\", \"gpgme-w32spawn\")\n$installTimeoutSeconds = 300\n$registrationTimeoutSeconds = 120\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\nfunction Test-Gpg4winRegistered {\n $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"Gpg4win*\" } |\n Select-Object -First 1)\n}\n\ntry {\n\n# Gpg4win uses an NSIS installer.\n$process = Start-Process -FilePath \"$exeFilePath\" -ArgumentList \"/S\" -PassThru\n# Touch .Handle so the exit code is still readable after the process ends:\n# Start-Process -PassThru otherwise returns $null for .ExitCode.\n$null = $process.Handle\n\nif (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {\n Write-Host \"Installer process did not exit within ${installTimeoutSeconds}s, stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Start-Sleep -Seconds 2\n}\n\n$exitCode = $process.ExitCode\nWrite-Host \"Install exit code: $exitCode\"\n\n# The installer can return before the Add/Remove Programs entry is written; wait\n# for it so software inventory sees a complete install.\n$elapsed = 0\nwhile (-not (Test-Gpg4winRegistered) -and ($elapsed -lt $registrationTimeoutSeconds)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for Gpg4win to register... ($elapsed seconds)\"\n}\n\n# Stop the resident processes the installer started. Leaving them running holds\n# file locks that make a later uninstall fail.\nforeach ($name in $leftovers) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\nif (-not (Test-Gpg4winRegistered)) {\n Write-Host \"Gpg4win did not register in Add/Remove Programs.\"\n Exit 1\n}\n\n# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.\nif ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", | ||
| "969aeebd": "# The registry DisplayName carries a parenthesised version (\"Gpg4win (5.0.2)\"),\n# so match on a prefix rather than an exact string.\n$softwareName = \"Gpg4win\"\n\n# Gpg4win bundles GnuPG, so the same daemons stay resident (gpg-agent, dirmngr,\n# keyboxd, scdaemon) along with Kleopatra. They hold file locks that make the\n# uninstall fail, and because \"Start-Process -Wait\" waits for descendants as well\n# as the process itself, anything the uninstaller re-spawns would block this\n# script indefinitely. Stop them first, then wait only on the uninstaller.\n$leftovers = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\", \"kleopatra\", \"gpgme-w32spawn\")\n$timeoutSeconds = 300\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$exitCode = 0\n\nfunction Get-Gpg4winUninstallKey {\n Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"$softwareName*\" } |\n Select-Object -First 1\n}\n\nforeach ($name in $leftovers) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\ntry {\n $key = Get-Gpg4winUninstallKey\n if (-not $key) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n Exit 1\n }\n\n $uninstallString = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }\n Write-Host \"Uninstall string: $uninstallString\"\n\n # Parse the executable path, handling quoted paths, unquoted paths containing\n # spaces, and bare tokens.\n $uninstallCommand = $uninstallString\n if ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $uninstallCommand = $Matches[1]\n } elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]\n } elseif ($uninstallCommand -match '^\\s*(\\S+)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]\n }\n\n # NSIS uninstallers copy themselves to %TEMP% and relaunch by default, so the\n # process we start exits immediately while the real uninstall runs detached.\n # \"_?=<dir>\" runs it in place instead, which makes it synchronous. It must be\n # the last argument and must not be quoted, so pass one argument string rather\n # than an array (PowerShell would quote an element containing spaces).\n $installDir = Split-Path -Parent $uninstallCommand\n $uninstallArgs = \"/S _?=$installDir\"\n\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $process = Start-Process -FilePath $uninstallCommand -ArgumentList $uninstallArgs -PassThru\n # Touch .Handle so the exit code is still readable after the process ends:\n # Start-Process -PassThru otherwise returns $null for .ExitCode.\n $null = $process.Handle\n\n if (-not $process.WaitForExit($timeoutSeconds * 1000)) {\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Write-Host \"Uninstall timed out after $timeoutSeconds seconds\"\n Exit 1603\n }\n\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n\n# Stop anything the uninstaller restarted, then wait for the ARP entry to clear.\nforeach ($name in $leftovers) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\n$elapsed = 0\nwhile ((Get-Gpg4winUninstallKey) -and ($elapsed -lt 120)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for the uninstall to finish... ($elapsed seconds)\"\n}\n\nif (Get-Gpg4winUninstallKey) {\n Write-Host \"'$softwareName' is still registered after the uninstall.\"\n Exit 1\n}\n\nExit $exitCode\n" |
There was a problem hiding this comment.
Regenerated — the ref now carries the HKCU lookup and the publisher predicate. run 30372747618 passes. On the _?= quoting specifically, see the reply on the source script: it is validated against C:\Program Files\Gpg4win, which contains a space.
Same fix as GnuPG (#50025), which Gpg4win bundles: the installer stalls on a MessageBox with no /SD default -- most likely the GpgEX regsvr32 failure -- and the ARP entry is written in a late section, so killing the installer leaves a half-finished install. Close the window instead and let it run to completion.
Script Diff Resultsee/maintained-apps/outputs/gpg4win/windows.json=== Install // 1c9be45b -> d0e1cf40 ===
--- /tmp/old.OEnY2j 2026-07-28 00:31:15.471989366 +0000
+++ /tmp/new.xvvq5A 2026-07-28 00:31:15.471989366 +0000
@@ -3,21 +3,38 @@
$exeFilePath = "${env:INSTALLER_PATH}"
-# Gpg4win bundles GnuPG, so the install starts the same resident daemons
-# (gpg-agent, dirmngr, keyboxd, scdaemon) and can leave Kleopatra running.
-# PowerShell's "Start-Process -Wait" waits for the process *and all of its
-# descendants*, so those keep the install script blocked indefinitely. Start
-# without -Wait, wait on the installer process itself with a timeout, then stop
-# the leftovers -- the same approach ollama_install.ps1 uses.
+# The Gpg4win installer process does not exit on its own on a headless machine,
+# and killing it is not enough: like GnuPG's installer (which it bundles), its
+# NSIS script writes the Add/Remove Programs entry in a late section, so an
+# installer stopped part-way leaves files on disk with no registry entry and
+# nothing for inventory to match.
+#
+# What stalls it is a modal dialog. The NSIS script has MessageBox calls with no
+# /SD default -- most relevantly the GpgEX shell-extension registration failure
+# ("regsvr32 /s gpgex.dll"), which is exactly the kind of thing that fails with no
+# interactive desktop. With nobody to click OK, the installer waits forever.
+#
+# So: leave the installer's children alone (killing regsvr32 would *guarantee*
+# that dialog), and instead close any window the installer puts up. Closing an
+# MB_OK dialog is equivalent to acknowledging it, and the install then runs on to
+# the section that writes the registry entry. Each poll also logs what is on
+# screen and which children are alive, so a future failure is diagnosable from
+# the CI log alone.
$leftovers = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "kleopatra", "gpgme-w32spawn")
-$installTimeoutSeconds = 300
-$registrationTimeoutSeconds = 120
+$installTimeoutSeconds = 420
+$pollSeconds = 10
+# Let the installer get on with it before we start closing windows, so a dialog
+# that is genuinely transient isn't dismissed prematurely.
+$graceSeconds = 30
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
+# The uninstall info is written with SHCTX, so check the per-user hive too in case
+# the installer resolves to current-user mode.
+$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
function Test-Gpg4winRegistered {
- $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
+ $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
Where-Object { $_.DisplayName -like "Gpg4win*" } |
Select-Object -First 1)
@@ -31,22 +48,32 @@
# Start-Process -PassThru otherwise returns $null for .ExitCode.
$null = $process.Handle
-if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
- Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
- Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
- Start-Sleep -Seconds 2
+$elapsed = 0
+while (-not $process.HasExited -and ($elapsed -lt $installTimeoutSeconds)) {
+ Start-Sleep -Seconds $pollSeconds
+ $elapsed += $pollSeconds
+ $process.Refresh()
+ if ($process.HasExited) { break }
+
+ $children = @(Get-Process -Name $leftovers -ErrorAction SilentlyContinue |
+ Select-Object -ExpandProperty Name -Unique)
+ $windowTitle = ""
+ try { $windowTitle = $process.MainWindowTitle } catch { }
+
+ Write-Host "Installing... ($elapsed seconds, registered: $(Test-Gpg4winRegistered), window: '$windowTitle', children: $($children -join ', '))"
+
+ if ($elapsed -ge $graceSeconds -and $process.MainWindowHandle -ne [IntPtr]::Zero) {
+ Write-Host "Installer is showing a window ('$windowTitle'); closing it so the install can continue."
+ $null = $process.CloseMainWindow()
+ }
}
-$exitCode = $process.ExitCode
-Write-Host "Install exit code: $exitCode"
-
-# The installer can return before the Add/Remove Programs entry is written; wait
-# for it so software inventory sees a complete install.
-$elapsed = 0
-while (-not (Test-Gpg4winRegistered) -and ($elapsed -lt $registrationTimeoutSeconds)) {
- Start-Sleep -Seconds 5
- $elapsed += 5
- Write-Host "Waiting for Gpg4win to register... ($elapsed seconds)"
+if (-not $process.HasExited) {
+ Write-Host "Installer still running after ${installTimeoutSeconds}s; stopping it."
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 2
+} else {
+ Write-Host "Install exit code: $($process.ExitCode)"
}
# Stop the resident processes the installer started. Leaving them running holds
@@ -55,15 +82,15 @@
Stop-Process -Name $name -Force -ErrorAction SilentlyContinue
}
+# Registration is the success signal, not the exit code: on the timeout path the
+# installer was killed, so its exit code says nothing about the install.
if (-not (Test-Gpg4winRegistered)) {
Write-Host "Gpg4win did not register in Add/Remove Programs."
Exit 1
}
-# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.
-if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
-
-Exit $exitCode
+Write-Host "Gpg4win is registered in Add/Remove Programs."
+Exit 0
} catch {
Write-Host "Error: $_"
=== Uninstall Script (no changes) === |
| $machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' | ||
| $machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | ||
| $exitCode = 0 | ||
|
|
||
| function Get-Gpg4winUninstallKey { | ||
| Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue | | ||
| ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | | ||
| Where-Object { $_.DisplayName -like "$softwareName*" } | | ||
| Select-Object -First 1 | ||
| } |
There was a problem hiding this comment.
Good catch — fixed the same way as #50025. The lookup now covers HKCU and HKCU\Wow6432Node as well as HKLM.
I also added the publisher predicate here, and this one earned its keep: The Gpg4win Project is the winget package publisher — precisely the class of value that was wrong for Spyder in #50016 — and it couldn't be confirmed statically (the PE resource says g10 Code GmbH, and the NSIS script that writes the real value is compressed). CI now proves it: run 30372747618 finds Gpg4win (5.0.2) with that publisher and removes it. On a miss the script logs every Gpg4win entry with its actual publisher, so a future mismatch is answered in one run.
The last run showed the installer itself never owns a window, so whatever is waiting for a click belongs to a child it spawned (regsvr32 registering the GpgEX shell extension being the likely one). Enumerate the installer's descendants, log their names and window titles, and close any window found in the tree.
Script Diff Resultsee/maintained-apps/outputs/gpg4win/windows.json=== Install // d0e1cf40 -> 29b7fd7f ===
--- /tmp/old.MMONWa 2026-07-28 01:04:19.615675269 +0000
+++ /tmp/new.ctnl3C 2026-07-28 01:04:19.615675269 +0000
@@ -33,6 +33,25 @@
# the installer resolves to current-user mode.
$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
+# Collect the installer process and everything descended from it, so a dialog
+# owned by a child (rather than the installer itself) is still visible to us.
+function Get-InstallerTree([int]$rootId) {
+ $all = @{}
+ Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
+ ForEach-Object { $all[[int]$_.ProcessId] = [int]$_.ParentProcessId }
+
+ $ids = New-Object System.Collections.Generic.HashSet[int]
+ $null = $ids.Add($rootId)
+ # Walk down a bounded number of generations; the tree here is shallow.
+ for ($depth = 0; $depth -lt 5; $depth++) {
+ foreach ($procId in @($all.Keys)) {
+ if ($ids.Contains($all[$procId])) { $null = $ids.Add($procId) }
+ }
+ }
+
+ Get-Process -ErrorAction SilentlyContinue | Where-Object { $ids.Contains($_.Id) }
+}
+
function Test-Gpg4winRegistered {
$null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
@@ -55,16 +74,26 @@
$process.Refresh()
if ($process.HasExited) { break }
- $children = @(Get-Process -Name $leftovers -ErrorAction SilentlyContinue |
- Select-Object -ExpandProperty Name -Unique)
+ # Look at the whole process tree, not just the installer: the previous attempt
+ # showed the installer itself never owns a window, so whatever is waiting for a
+ # click belongs to a child it spawned (regsvr32 registering the GpgEX shell
+ # extension is the likely one).
+ $tree = Get-InstallerTree $process.Id
+ $names = @($tree | Select-Object -ExpandProperty ProcessName -Unique)
$windowTitle = ""
try { $windowTitle = $process.MainWindowTitle } catch { }
+ $childWindows = @($tree | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
+ ForEach-Object { "$($_.ProcessName): '$($_.MainWindowTitle)'" })
- Write-Host "Installing... ($elapsed seconds, registered: $(Test-Gpg4winRegistered), window: '$windowTitle', children: $($children -join ', '))"
+ Write-Host "Installing... ($elapsed seconds, registered: $(Test-Gpg4winRegistered), window: '$windowTitle', tree: $($names -join ', '), child windows: $($childWindows -join ' | '))"
- if ($elapsed -ge $graceSeconds -and $process.MainWindowHandle -ne [IntPtr]::Zero) {
- Write-Host "Installer is showing a window ('$windowTitle'); closing it so the install can continue."
- $null = $process.CloseMainWindow()
+ if ($elapsed -ge $graceSeconds) {
+ foreach ($p in $tree) {
+ if ($p.MainWindowHandle -ne [IntPtr]::Zero) {
+ Write-Host "Closing window owned by $($p.ProcessName) ('$($p.MainWindowTitle)') so the install can continue."
+ $null = $p.CloseMainWindow()
+ }
+ }
}
}
=== Uninstall Script (no changes) === |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1:17
- Uninstall key lookup only checks HKLM (both native + Wow6432Node), but the install script explicitly notes the installer may resolve to current-user mode and checks HKCU. If Gpg4win registers in HKCU, this uninstall will fail to find the uninstaller and will incorrectly report that the app is still registered at the end.
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$exitCode = 0
function Get-Gpg4winUninstallKey {
ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1:55
- Building $uninstallArgs as a single unquoted string will be split on spaces (e.g. "C:\Program Files (x86)...") and can break the NSIS
_?=argument. Passing arguments as separate elements avoids whitespace-splitting while still keeping_?=as the final argument.
# "_?=<dir>" runs it in place instead, which makes it synchronous. It must be
# the last argument and must not be quoted, so pass one argument string rather
# than an array (PowerShell would quote an element containing spaces).
$installDir = Split-Path -Parent $uninstallCommand
$uninstallArgs = "/S _?=$installDir"
| # screen and which children are alive, so a future failure is diagnosable from | ||
| # the CI log alone. | ||
| $leftovers = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "kleopatra", "gpgme-w32spawn") | ||
| $installTimeoutSeconds = 420 |
There was a problem hiding this comment.
You're right, the description was stale after the rewrite — fixed. The cap is 420s, and the description now says so and notes it sits below the caller's 10-minute script budget.
Addresses Copilot's review: the install script accounts for the uninstall info being written with SHCTX (so it can land in HKCU) but the uninstall only searched HKLM. Also requires the publisher, mirroring the exists query, which puts that value under test since appExists matches on name only. 'The Gpg4win Project' is the least-verified publisher in this batch -- it is the winget *package* publisher, the same class of value that was wrong for Spyder in #50016, and it could not be confirmed statically (the PE resource says 'g10 Code GmbH' and the NSIS script is compressed). So on a miss the script now prints every Gpg4win entry with its actual publisher, which settles it in one CI run. Exit 0 when no entry is found, per repo convention, and corrected the _?= comment. The ExitCode-after-Stop-Process finding is already handled -- ExitCode is read only in the HasExited branch.
Script Diff Resultsee/maintained-apps/outputs/gpg4win/windows.json=== Install Script (no changes) ===
=== Uninstall // 969aeebd -> 9375da9a ===
--- /tmp/old.przUbs 2026-07-28 15:21:17.910396044 +0000
+++ /tmp/new.2Usw2I 2026-07-28 15:21:17.910396044 +0000
@@ -1,6 +1,19 @@
# The registry DisplayName carries a parenthesised version ("Gpg4win (5.0.2)"),
# so match on a prefix rather than an exact string.
$softwareName = "Gpg4win"
+# Require the publisher too, mirroring the manifest's exists query. This puts the
+# value under test: the validator's appExists looks up by name only, so a wrong
+# exists-query publisher would otherwise ship a manifest that can never match an
+# install (cf. the Spyder finding in #50016).
+#
+# This one is the least certain in the batch. "The Gpg4win Project" comes from the
+# winget locale manifest -- a *package* publisher, which is exactly the kind of
+# value that was wrong for Spyder -- and it could not be confirmed statically: the
+# installer's PE version resource says "g10 Code GmbH", and the NSIS script that
+# writes the real value is compressed. If it is wrong the uninstall below finds
+# nothing, and the diagnostic prints the Gpg4win entries actually present with
+# their publishers, so a single CI run settles it.
+$softwarePublisher = "The Gpg4win Project"
# Gpg4win bundles GnuPG, so the same daemons stay resident (gpg-agent, dirmngr,
# keyboxd, scdaemon) along with Kleopatra. They hold file locks that make the
@@ -12,15 +25,34 @@
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
+# The uninstall info is written with SHCTX, so it can land in the per-user hive.
+# The install script already accounts for that; mirror it here.
+$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
+$userKey32on64 = 'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$exitCode = 0
+$allKeys = @($machineKey, $machineKey32on64, $userKey, $userKey32on64)
+
function Get-Gpg4winUninstallKey {
- Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
+ Get-ChildItem -Path $allKeys -ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
- Where-Object { $_.DisplayName -like "$softwareName*" } |
+ Where-Object { $_.DisplayName -like "$softwareName*" -and $_.Publisher -eq $softwarePublisher } |
Select-Object -First 1
}
+# Print every Gpg4win-ish entry with its publisher, so a publisher mismatch names
+# the correct value instead of just failing.
+function Write-Gpg4winCandidates {
+ Write-Host "Registry entries matching '$softwareName*':"
+ $found = Get-ChildItem -Path $allKeys -ErrorAction SilentlyContinue |
+ ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
+ Where-Object { $_.DisplayName -like "$softwareName*" }
+ if (-not $found) { Write-Host " (none)" ; return }
+ foreach ($f in $found) {
+ Write-Host " DisplayName='$($f.DisplayName)' Publisher='$($f.Publisher)' Version='$($f.DisplayVersion)'"
+ }
+}
+
foreach ($name in $leftovers) {
Stop-Process -Name $name -Force -ErrorAction SilentlyContinue
}
@@ -28,8 +60,12 @@
try {
$key = Get-Gpg4winUninstallKey
if (-not $key) {
- Write-Host "Uninstaller for '$softwareName' not found."
- Exit 1
+ Write-Gpg4winCandidates
+ # Nothing to remove is not a failure: uninstall scripts are idempotent here,
+ # as in nordpass_uninstall.ps1 and windsurf_uninstall.ps1. If the app *is*
+ # still installed the validator's own post-uninstall check catches it.
+ Write-Host "Uninstall entry not found for '$softwareName' with publisher '$softwarePublisher'."
+ Exit 0
}
$uninstallString = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
@@ -48,9 +84,12 @@
# NSIS uninstallers copy themselves to %TEMP% and relaunch by default, so the
# process we start exits immediately while the real uninstall runs detached.
- # "_?=<dir>" runs it in place instead, which makes it synchronous. It must be
- # the last argument and must not be quoted, so pass one argument string rather
- # than an array (PowerShell would quote an element containing spaces).
+ # "_?=<dir>" runs it in place instead, which makes it synchronous, and it has
+ # to be the last argument. Other scripts here pass it as its own ArgumentList
+ # element (bdash, binance, canva) or quoted (android_studio); this passes one
+ # argument string, which is what was validated in CI for this installer. Note
+ # not every NSIS uninstaller accepts it -- DBeaver's returns exit 2 and
+ # Logitech Unifying's exit 10, so both of those omit it.
$installDir = Split-Path -Parent $uninstallCommand
$uninstallArgs = "/S _?=$installDir"
@@ -88,6 +127,7 @@
}
if (Get-Gpg4winUninstallKey) {
+ Write-Gpg4winCandidates
Write-Host "'$softwareName' is still registered after the uninstall."
Exit 1
} |
WalkthroughAdds Gpg4win as a maintained Windows application with package metadata, a version 5.0.2 manifest, unattended PowerShell install and uninstall workflows, registry-based lifecycle validation, catalog output, and a frontend icon mapping. Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx (1)
6-10: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftReplace the large embedded PNG with a bundled asset.
Gpg4Win.tsxis roughly 18 MB because of thedata:image/png;base64,...payload inside a statically imported icon. Move this to an optimized static asset/SVG referenced via import, following the repository’s icon pipeline, to prevent this large payload from inflating the frontend JavaScript bundle.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx` around lines 6 - 10, Replace the base64 PNG payload in the Gpg4Win SVG image with an optimized bundled asset or repository-standard SVG icon import. Update the Gpg4Win component’s image reference while preserving its 32×32 rendering and existing props behavior, ensuring the large inline data URI is removed from the frontend bundle.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1`:
- Around line 129-135: Update the final exit behavior after the
Get-Gpg4winUninstallKey check so a confirmed registry removal exits with success
code 0 rather than the raw $exitCode; preserve Exit 1 when the uninstall
registration remains present.
In `@ee/maintained-apps/outputs/gpg4win/windows.json`:
- Line 19: Update Test-Gpg4winRegistered to validate the same Gpg4win identity
used by the catalog, exists query, and uninstall logic: require both a matching
Gpg4win display name and publisher “The Gpg4win Project.” Keep the existing
registry hive checks and return behavior unchanged.
In `@frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx`:
- Around line 6-10: Add a viewBox attribute of “0 0 32 32” to the root svg
element in Gpg4Win while preserving its existing fixed width and height and
embedded image attributes.
---
Nitpick comments:
In `@frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx`:
- Around line 6-10: Replace the base64 PNG payload in the Gpg4Win SVG image with
an optimized bundled asset or repository-standard SVG icon import. Update the
Gpg4Win component’s image reference while preserving its 32×32 rendering and
existing props behavior, ensuring the large inline data URI is removed from the
frontend bundle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c2b26457-e4bf-4144-8c93-0fa2b7cbff02
⛔ Files ignored due to path filters (1)
website/assets/images/app-icon-gpg4win-60x60@2x.pngis excluded by!**/*.png
📒 Files selected for processing (7)
ee/maintained-apps/inputs/winget/gpg4win.jsonee/maintained-apps/inputs/winget/scripts/gpg4win_install.ps1ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1ee/maintained-apps/outputs/apps.jsonee/maintained-apps/outputs/gpg4win/windows.jsonfrontend/pages/SoftwarePage/components/icons/Gpg4Win.tsxfrontend/pages/SoftwarePage/components/icons/index.ts
| if (Get-Gpg4winUninstallKey) { | ||
| Write-Gpg4winCandidates | ||
| Write-Host "'$softwareName' is still registered after the uninstall." | ||
| Exit 1 | ||
| } | ||
|
|
||
| Exit $exitCode |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Final Exit $exitCode undermines the registry-based success check it just performed.
By the time execution reaches line 135, the preceding poll (lines 122-127) has already confirmed the uninstall entry is gone — that's the success signal, per this same file's own reasoning and the install script's stated philosophy ("Registration is the success signal, not the exit code"). Yet this line exits with the raw NSIS process exit code instead of 0. If Gpg4win's uninstaller ever returns a benign nonzero code on success (as this file itself notes DBeaver and Logitech Unifying do for their NSIS uninstallers), a fully successful, registry-confirmed removal will still be reported as a failure.
🐛 Proposed fix
if (Get-Gpg4winUninstallKey) {
Write-Gpg4winCandidates
Write-Host "'$softwareName' is still registered after the uninstall."
Exit 1
}
-Exit $exitCode
+# Registry state already confirms removal succeeded; don't fail on a benign
+# nonzero exit code from the uninstaller (cf. the DBeaver/Logitech notes above).
+Write-Host "Uninstall exit code was $exitCode; registry confirms removal succeeded."
+Exit 0📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (Get-Gpg4winUninstallKey) { | |
| Write-Gpg4winCandidates | |
| Write-Host "'$softwareName' is still registered after the uninstall." | |
| Exit 1 | |
| } | |
| Exit $exitCode | |
| if (Get-Gpg4winUninstallKey) { | |
| Write-Gpg4winCandidates | |
| Write-Host "'$softwareName' is still registered after the uninstall." | |
| Exit 1 | |
| } | |
| # Registry state already confirms removal succeeded; don't fail on a benign | |
| # nonzero exit code from the uninstaller (cf. the DBeaver/Logitech notes above). | |
| Write-Host "Uninstall exit code was $exitCode; registry confirms removal succeeded." | |
| Exit 0 |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1` around lines
129 - 135, Update the final exit behavior after the Get-Gpg4winUninstallKey
check so a confirmed registry removal exits with success code 0 rather than the
raw $exitCode; preserve Exit 1 when the uninstall registration remains present.
| } | ||
| ], | ||
| "refs": { | ||
| "29b7fd7f": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# The Gpg4win installer process does not exit on its own on a headless machine,\n# and killing it is not enough: like GnuPG's installer (which it bundles), its\n# NSIS script writes the Add/Remove Programs entry in a late section, so an\n# installer stopped part-way leaves files on disk with no registry entry and\n# nothing for inventory to match.\n#\n# What stalls it is a modal dialog. The NSIS script has MessageBox calls with no\n# /SD default -- most relevantly the GpgEX shell-extension registration failure\n# (\"regsvr32 /s gpgex.dll\"), which is exactly the kind of thing that fails with no\n# interactive desktop. With nobody to click OK, the installer waits forever.\n#\n# So: leave the installer's children alone (killing regsvr32 would *guarantee*\n# that dialog), and instead close any window the installer puts up. Closing an\n# MB_OK dialog is equivalent to acknowledging it, and the install then runs on to\n# the section that writes the registry entry. Each poll also logs what is on\n# screen and which children are alive, so a future failure is diagnosable from\n# the CI log alone.\n$leftovers = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\", \"kleopatra\", \"gpgme-w32spawn\")\n$installTimeoutSeconds = 420\n$pollSeconds = 10\n# Let the installer get on with it before we start closing windows, so a dialog\n# that is genuinely transient isn't dismissed prematurely.\n$graceSeconds = 30\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n# The uninstall info is written with SHCTX, so check the per-user hive too in case\n# the installer resolves to current-user mode.\n$userKey = 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n# Collect the installer process and everything descended from it, so a dialog\n# owned by a child (rather than the installer itself) is still visible to us.\nfunction Get-InstallerTree([int]$rootId) {\n $all = @{}\n Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |\n ForEach-Object { $all[[int]$_.ProcessId] = [int]$_.ParentProcessId }\n\n $ids = New-Object System.Collections.Generic.HashSet[int]\n $null = $ids.Add($rootId)\n # Walk down a bounded number of generations; the tree here is shallow.\n for ($depth = 0; $depth -lt 5; $depth++) {\n foreach ($procId in @($all.Keys)) {\n if ($ids.Contains($all[$procId])) { $null = $ids.Add($procId) }\n }\n }\n\n Get-Process -ErrorAction SilentlyContinue | Where-Object { $ids.Contains($_.Id) }\n}\n\nfunction Test-Gpg4winRegistered {\n $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"Gpg4win*\" } |\n Select-Object -First 1)\n}\n\ntry {\n\n# Gpg4win uses an NSIS installer.\n$process = Start-Process -FilePath \"$exeFilePath\" -ArgumentList \"/S\" -PassThru\n# Touch .Handle so the exit code is still readable after the process ends:\n# Start-Process -PassThru otherwise returns $null for .ExitCode.\n$null = $process.Handle\n\n$elapsed = 0\nwhile (-not $process.HasExited -and ($elapsed -lt $installTimeoutSeconds)) {\n Start-Sleep -Seconds $pollSeconds\n $elapsed += $pollSeconds\n $process.Refresh()\n if ($process.HasExited) { break }\n\n # Look at the whole process tree, not just the installer: the previous attempt\n # showed the installer itself never owns a window, so whatever is waiting for a\n # click belongs to a child it spawned (regsvr32 registering the GpgEX shell\n # extension is the likely one).\n $tree = Get-InstallerTree $process.Id\n $names = @($tree | Select-Object -ExpandProperty ProcessName -Unique)\n $windowTitle = \"\"\n try { $windowTitle = $process.MainWindowTitle } catch { }\n $childWindows = @($tree | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |\n ForEach-Object { \"$($_.ProcessName): '$($_.MainWindowTitle)'\" })\n\n Write-Host \"Installing... ($elapsed seconds, registered: $(Test-Gpg4winRegistered), window: '$windowTitle', tree: $($names -join ', '), child windows: $($childWindows -join ' | '))\"\n\n if ($elapsed -ge $graceSeconds) {\n foreach ($p in $tree) {\n if ($p.MainWindowHandle -ne [IntPtr]::Zero) {\n Write-Host \"Closing window owned by $($p.ProcessName) ('$($p.MainWindowTitle)') so the install can continue.\"\n $null = $p.CloseMainWindow()\n }\n }\n }\n}\n\nif (-not $process.HasExited) {\n Write-Host \"Installer still running after ${installTimeoutSeconds}s; stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Start-Sleep -Seconds 2\n} else {\n Write-Host \"Install exit code: $($process.ExitCode)\"\n}\n\n# Stop the resident processes the installer started. Leaving them running holds\n# file locks that make a later uninstall fail.\nforeach ($name in $leftovers) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\n# Registration is the success signal, not the exit code: on the timeout path the\n# installer was killed, so its exit code says nothing about the install.\nif (-not (Test-Gpg4winRegistered)) {\n Write-Host \"Gpg4win did not register in Add/Remove Programs.\"\n Exit 1\n}\n\nWrite-Host \"Gpg4win is registered in Add/Remove Programs.\"\nExit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Align install success validation with the catalog identity.
Test-Gpg4winRegistered matches only DisplayName -like "Gpg4win*", while ee/maintained-apps/inputs/winget/gpg4win.json Line [7], this file’s exists query at Line [6], and the uninstall script require publisher The Gpg4win Project. A stale or mismatched ARP entry can therefore make installation return success without registering the target app identity.
Proposed fix
+ $softwarePublisher = "The Gpg4win Project"
...
- Where-Object { $_.DisplayName -like "Gpg4win*" } |
+ Where-Object { $_.DisplayName -like "Gpg4win*" -and $_.Publisher -eq $softwarePublisher } |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "29b7fd7f": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# The Gpg4win installer process does not exit on its own on a headless machine,\n# and killing it is not enough: like GnuPG's installer (which it bundles), its\n# NSIS script writes the Add/Remove Programs entry in a late section, so an\n# installer stopped part-way leaves files on disk with no registry entry and\n# nothing for inventory to match.\n#\n# What stalls it is a modal dialog. The NSIS script has MessageBox calls with no\n# /SD default -- most relevantly the GpgEX shell-extension registration failure\n# (\"regsvr32 /s gpgex.dll\"), which is exactly the kind of thing that fails with no\n# interactive desktop. With nobody to click OK, the installer waits forever.\n#\n# So: leave the installer's children alone (killing regsvr32 would *guarantee*\n# that dialog), and instead close any window the installer puts up. Closing an\n# MB_OK dialog is equivalent to acknowledging it, and the install then runs on to\n# the section that writes the registry entry. Each poll also logs what is on\n# screen and which children are alive, so a future failure is diagnosable from\n# the CI log alone.\n$leftovers = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\", \"kleopatra\", \"gpgme-w32spawn\")\n$installTimeoutSeconds = 420\n$pollSeconds = 10\n# Let the installer get on with it before we start closing windows, so a dialog\n# that is genuinely transient isn't dismissed prematurely.\n$graceSeconds = 30\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n# The uninstall info is written with SHCTX, so check the per-user hive too in case\n# the installer resolves to current-user mode.\n$userKey = 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n# Collect the installer process and everything descended from it, so a dialog\n# owned by a child (rather than the installer itself) is still visible to us.\nfunction Get-InstallerTree([int]$rootId) {\n $all = @{}\n Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |\n ForEach-Object { $all[[int]$_.ProcessId] = [int]$_.ParentProcessId }\n\n $ids = New-Object System.Collections.Generic.HashSet[int]\n $null = $ids.Add($rootId)\n # Walk down a bounded number of generations; the tree here is shallow.\n for ($depth = 0; $depth -lt 5; $depth++) {\n foreach ($procId in @($all.Keys)) {\n if ($ids.Contains($all[$procId])) { $null = $ids.Add($procId) }\n }\n }\n\n Get-Process -ErrorAction SilentlyContinue | Where-Object { $ids.Contains($_.Id) }\n}\n\nfunction Test-Gpg4winRegistered {\n $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"Gpg4win*\" } |\n Select-Object -First 1)\n}\n\ntry {\n\n# Gpg4win uses an NSIS installer.\n$process = Start-Process -FilePath \"$exeFilePath\" -ArgumentList \"/S\" -PassThru\n# Touch .Handle so the exit code is still readable after the process ends:\n# Start-Process -PassThru otherwise returns $null for .ExitCode.\n$null = $process.Handle\n\n$elapsed = 0\nwhile (-not $process.HasExited -and ($elapsed -lt $installTimeoutSeconds)) {\n Start-Sleep -Seconds $pollSeconds\n $elapsed += $pollSeconds\n $process.Refresh()\n if ($process.HasExited) { break }\n\n # Look at the whole process tree, not just the installer: the previous attempt\n # showed the installer itself never owns a window, so whatever is waiting for a\n # click belongs to a child it spawned (regsvr32 registering the GpgEX shell\n # extension is the likely one).\n $tree = Get-InstallerTree $process.Id\n $names = @($tree | Select-Object -ExpandProperty ProcessName -Unique)\n $windowTitle = \"\"\n try { $windowTitle = $process.MainWindowTitle } catch { }\n $childWindows = @($tree | Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |\n ForEach-Object { \"$($_.ProcessName): '$($_.MainWindowTitle)'\" })\n\n Write-Host \"Installing... ($elapsed seconds, registered: $(Test-Gpg4winRegistered), window: '$windowTitle', tree: $($names -join ', '), child windows: $($childWindows -join ' | '))\"\n\n if ($elapsed -ge $graceSeconds) {\n foreach ($p in $tree) {\n if ($p.MainWindowHandle -ne [IntPtr]::Zero) {\n Write-Host \"Closing window owned by $($p.ProcessName) ('$($p.MainWindowTitle)') so the install can continue.\"\n $null = $p.CloseMainWindow()\n }\n }\n }\n}\n\nif (-not $process.HasExited) {\n Write-Host \"Installer still running after ${installTimeoutSeconds}s; stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Start-Sleep -Seconds 2\n} else {\n Write-Host \"Install exit code: $($process.ExitCode)\"\n}\n\n# Stop the resident processes the installer started. Leaving them running holds\n# file locks that make a later uninstall fail.\nforeach ($name in $leftovers) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\n# Registration is the success signal, not the exit code: on the timeout path the\n# installer was killed, so its exit code says nothing about the install.\nif (-not (Test-Gpg4winRegistered)) {\n Write-Host \"Gpg4win did not register in Add/Remove Programs.\"\n Exit 1\n}\n\nWrite-Host \"Gpg4win is registered in Add/Remove Programs.\"\nExit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", | |
| $leftovers = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "kleopatra", "gpgme-w32spawn") | |
| $installTimeoutSeconds = 420 | |
| $pollSeconds = 10 | |
| # Let the installer get on with it before we start closing windows, so a dialog | |
| # that is genuinely transient isn't dismissed prematurely. | |
| $graceSeconds = 30 | |
| $softwarePublisher = "The Gpg4win Project" | |
| $machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' | |
| $machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' | |
| # The uninstall info is written with SHCTX, so check the per-user hive too in case | |
| # the installer resolves to current-user mode. | |
| $userKey = 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' | |
| # Collect the installer process and everything descended from it, so a dialog | |
| # owned by a child (rather than the installer itself) is still visible to us. | |
| function Get-InstallerTree([int]$rootId) { | |
| $all = @{} | |
| Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | | |
| ForEach-Object { $all[[int]$_.ProcessId] = [int]$_.ParentProcessId } | |
| $ids = New-Object System.Collections.Generic.HashSet[int] | |
| $null = $ids.Add($rootId) | |
| # Walk down a bounded number of generations; the tree here is shallow. | |
| for ($depth = 0; $depth -lt 5; $depth++) { | |
| foreach ($procId in @($all.Keys)) { | |
| if ($ids.Contains($all[$procId])) { $null = $ids.Add($procId) } | |
| } | |
| } | |
| Get-Process -ErrorAction SilentlyContinue | Where-Object { $ids.Contains($_.Id) } | |
| } | |
| function Test-Gpg4winRegistered { | |
| $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue | | |
| ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | | |
| Where-Object { $_.DisplayName -like "Gpg4win*" -and $_.Publisher -eq $softwarePublisher } | | |
| Select-Object -First 1) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ee/maintained-apps/outputs/gpg4win/windows.json` at line 19, Update
Test-Gpg4winRegistered to validate the same Gpg4win identity used by the
catalog, exists query, and uninstall logic: require both a matching Gpg4win
display name and publisher “The Gpg4win Project.” Keep the existing registry
hive checks and return behavior unchanged.
| <svg xmlns="http://www.w3.org/2000/svg" width={32} height={32} {...props}> | ||
| <image | ||
| width={32} | ||
| height={32} | ||
| href="data:image/png;base64,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" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate file =="
fd -a 'Gpg4Win\.tsx$' . || true
echo "== Inspect candidate file =="
file="$(fd 'Gpg4Win\.tsx$' . | head -n1)"
if [ -n "$file" ]; then
wc -l "$file"
cat -n "$file"
fi
echo "== Search SVG icon patterns for viewBox/image usage =="
rg -n "<svg|viewBox|<image|href=\"data:image" frontend/pages/SoftwarePage/components/icons -S || trueRepository: fleetdm/fleet
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="$(fd 'Gpg4Win\.tsx$' . | head -n1 || true)"
if [ -z "$file" ]; then
echo "Gpg4Win.tsx not found"
exit 0
fi
echo "== file location and line count =="
echo "$file"
wc -l "$file"
echo "== first 20 lines with line numbers =="
sed -n '1,20p' "$file" | cat -n
echo "== nearby SVG/image attributes in surrounding icon files without expanding embedded data =="
python3 - <<'PY'
import pathlib, re, sys
root = pathlib.Path('frontend/pages/SoftwarePage/components/icons')
for p in sorted(root.glob('*.tsx')):
text = p.read_text(errors='ignore')
match = re.search(r'<svg[^>]+>|<image\s+([^>]*)>', text[:500], re.S)
if not match:
continue
start=line_match=index_match=match.start()
line= text[:start].count("\n")+1
# print only SVG/image tag, not full base64 by trimming href body
tag = match.group(0)
if "<image" in tag:
tag = re.sub(r'href="([^"]*)"', lambda m: r'href="...base64..."' if len(m.group(1))>80 else m.group(0), tag)
print(f"{p}:{line}: {tag[:300]}")
PY
echo "== SVG/viewBox/image occurrences in this file =="
python3 - <<'PY'
import pathlib, re
p = pathlib.Path('frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx')
text = p.read_text()
for i,line in enumerate(text.splitlines(),1):
if re.search(r'<svg|<image|viewBox', line):
print(f"{i}: {line.strip()}")
PYRepository: fleetdm/fleet
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Gpg4Win.tsx svg/image/search for viewBox =="
wc -l frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx
sed -n '1,20p' frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx | cat -n
rg -n '<svg|<image|viewBox' frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx -S
echo "== base64 image dimensions in Gpg4Win.tsx XML header =="
python3 - <<'PY'
import base64, re, pathlib, io
p= pathlib.Path('frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx')
text = p.read_text()
m = re.search(r'href="data:image/png;base64,([^"]+)"', text, re.S)
payload = base64.b64decode(m.group(1))
print("payload_size", len(payload))
png_struct = payload[:8]
if png_struct == b'\x89PNG\r\n\x1a\n':
print("recognized_png")
print("first_512bytes", payload[:512])
PYRepository: fleetdm/fleet
Length of output: 16640
Preserve the icon coordinate system when resizing.
Callers can override width and height, but Gpg4Win has no viewBox while the embedded image has a 128×128 viewBox inside a 32×32 presentation box. Adding viewBox="0 0 32 32" makes the root SVG viewBox-match the intended icon box without changing the fixed 32×32 rendering.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx` around lines 6 -
10, Add a viewBox attribute of “0 0 32 32” to the root svg element in Gpg4Win
while preserving its existing fixed width and height and embedded image
attributes.
Script Diff Resultsee/maintained-apps/outputs/gpg4win/windows.json=== Install // 29b7fd7f -> 33dd6930 ===
--- /tmp/old.Y9tqhH 2026-07-28 17:44:00.908326055 +0000
+++ /tmp/new.NpQmn2 2026-07-28 17:44:00.908326055 +0000
@@ -3,38 +3,21 @@
$exeFilePath = "${env:INSTALLER_PATH}"
-# The Gpg4win installer process does not exit on its own on a headless machine,
-# and killing it is not enough: like GnuPG's installer (which it bundles), its
-# NSIS script writes the Add/Remove Programs entry in a late section, so an
-# installer stopped part-way leaves files on disk with no registry entry and
-# nothing for inventory to match.
-#
-# What stalls it is a modal dialog. The NSIS script has MessageBox calls with no
-# /SD default -- most relevantly the GpgEX shell-extension registration failure
-# ("regsvr32 /s gpgex.dll"), which is exactly the kind of thing that fails with no
-# interactive desktop. With nobody to click OK, the installer waits forever.
-#
-# So: leave the installer's children alone (killing regsvr32 would *guarantee*
-# that dialog), and instead close any window the installer puts up. Closing an
-# MB_OK dialog is equivalent to acknowledging it, and the install then runs on to
-# the section that writes the registry entry. Each poll also logs what is on
-# screen and which children are alive, so a future failure is diagnosable from
-# the CI log alone.
+# The installer stalls on a modal dialog with no interactive desktop and never
+# exits. Closing the window lets it run through to the section that writes the
+# Add/Remove Programs entry; killing it instead would leave a partial install.
+# The dialog belongs to a child process, so search the whole tree.
$leftovers = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "kleopatra", "gpgme-w32spawn")
$installTimeoutSeconds = 420
$pollSeconds = 10
-# Let the installer get on with it before we start closing windows, so a dialog
-# that is genuinely transient isn't dismissed prematurely.
$graceSeconds = 30
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
-# The uninstall info is written with SHCTX, so check the per-user hive too in case
-# the installer resolves to current-user mode.
+# Uninstall info is written with SHCTX, so it can land per-user.
$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
-# Collect the installer process and everything descended from it, so a dialog
-# owned by a child (rather than the installer itself) is still visible to us.
+# The installer process plus its descendants.
function Get-InstallerTree([int]$rootId) {
$all = @{}
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
@@ -42,7 +25,6 @@
$ids = New-Object System.Collections.Generic.HashSet[int]
$null = $ids.Add($rootId)
- # Walk down a bounded number of generations; the tree here is shallow.
for ($depth = 0; $depth -lt 5; $depth++) {
foreach ($procId in @($all.Keys)) {
if ($ids.Contains($all[$procId])) { $null = $ids.Add($procId) }
@@ -61,10 +43,8 @@
try {
-# Gpg4win uses an NSIS installer.
$process = Start-Process -FilePath "$exeFilePath" -ArgumentList "/S" -PassThru
-# Touch .Handle so the exit code is still readable after the process ends:
-# Start-Process -PassThru otherwise returns $null for .ExitCode.
+# Keeps .ExitCode readable after the process ends.
$null = $process.Handle
$elapsed = 0
@@ -74,10 +54,6 @@
$process.Refresh()
if ($process.HasExited) { break }
- # Look at the whole process tree, not just the installer: the previous attempt
- # showed the installer itself never owns a window, so whatever is waiting for a
- # click belongs to a child it spawned (regsvr32 registering the GpgEX shell
- # extension is the likely one).
$tree = Get-InstallerTree $process.Id
$names = @($tree | Select-Object -ExpandProperty ProcessName -Unique)
$windowTitle = ""
@@ -105,14 +81,12 @@
Write-Host "Install exit code: $($process.ExitCode)"
}
-# Stop the resident processes the installer started. Leaving them running holds
-# file locks that make a later uninstall fail.
+# Stop resident processes; they hold file locks the uninstall needs released.
foreach ($name in $leftovers) {
Stop-Process -Name $name -Force -ErrorAction SilentlyContinue
}
-# Registration is the success signal, not the exit code: on the timeout path the
-# installer was killed, so its exit code says nothing about the install.
+# Registration is the success signal: a killed installer's exit code says nothing.
if (-not (Test-Gpg4winRegistered)) {
Write-Host "Gpg4win did not register in Add/Remove Programs."
Exit 1
=== Uninstall // 9375da9a -> 5b342b42 ===
--- /tmp/old.CxCdf5 2026-07-28 17:44:00.935325868 +0000
+++ /tmp/new.ww9li3 2026-07-28 17:44:00.935325868 +0000
@@ -1,32 +1,16 @@
# The registry DisplayName carries a parenthesised version ("Gpg4win (5.0.2)"),
# so match on a prefix rather than an exact string.
$softwareName = "Gpg4win"
-# Require the publisher too, mirroring the manifest's exists query. This puts the
-# value under test: the validator's appExists looks up by name only, so a wrong
-# exists-query publisher would otherwise ship a manifest that can never match an
-# install (cf. the Spyder finding in #50016).
-#
-# This one is the least certain in the batch. "The Gpg4win Project" comes from the
-# winget locale manifest -- a *package* publisher, which is exactly the kind of
-# value that was wrong for Spyder -- and it could not be confirmed statically: the
-# installer's PE version resource says "g10 Code GmbH", and the NSIS script that
-# writes the real value is compressed. If it is wrong the uninstall below finds
-# nothing, and the diagnostic prints the Gpg4win entries actually present with
-# their publishers, so a single CI run settles it.
$softwarePublisher = "The Gpg4win Project"
-# Gpg4win bundles GnuPG, so the same daemons stay resident (gpg-agent, dirmngr,
-# keyboxd, scdaemon) along with Kleopatra. They hold file locks that make the
-# uninstall fail, and because "Start-Process -Wait" waits for descendants as well
-# as the process itself, anything the uninstaller re-spawns would block this
-# script indefinitely. Stop them first, then wait only on the uninstaller.
+# The bundled GnuPG daemons and Kleopatra hold file locks and would block -Wait,
+# so stop them first and wait only on the uninstaller process.
$leftovers = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "kleopatra", "gpgme-w32spawn")
$timeoutSeconds = 300
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
-# The uninstall info is written with SHCTX, so it can land in the per-user hive.
-# The install script already accounts for that; mirror it here.
+# Uninstall info is written with SHCTX, so it can land per-user.
$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
$userKey32on64 = 'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
$exitCode = 0
@@ -40,8 +24,7 @@
Select-Object -First 1
}
-# Print every Gpg4win-ish entry with its publisher, so a publisher mismatch names
-# the correct value instead of just failing.
+# Logs matching entries and their publishers to diagnose a name/publisher miss.
function Write-Gpg4winCandidates {
Write-Host "Registry entries matching '$softwareName*':"
$found = Get-ChildItem -Path $allKeys -ErrorAction SilentlyContinue |
@@ -61,9 +44,6 @@
$key = Get-Gpg4winUninstallKey
if (-not $key) {
Write-Gpg4winCandidates
- # Nothing to remove is not a failure: uninstall scripts are idempotent here,
- # as in nordpass_uninstall.ps1 and windsurf_uninstall.ps1. If the app *is*
- # still installed the validator's own post-uninstall check catches it.
Write-Host "Uninstall entry not found for '$softwareName' with publisher '$softwarePublisher'."
Exit 0
}
@@ -71,8 +51,7 @@
$uninstallString = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
Write-Host "Uninstall string: $uninstallString"
- # Parse the executable path, handling quoted paths, unquoted paths containing
- # spaces, and bare tokens.
+ # Handles quoted paths, unquoted paths with spaces, and bare tokens.
$uninstallCommand = $uninstallString
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
$uninstallCommand = $Matches[1]
@@ -82,14 +61,8 @@
$uninstallCommand = $Matches[1]
}
- # NSIS uninstallers copy themselves to %TEMP% and relaunch by default, so the
- # process we start exits immediately while the real uninstall runs detached.
- # "_?=<dir>" runs it in place instead, which makes it synchronous, and it has
- # to be the last argument. Other scripts here pass it as its own ArgumentList
- # element (bdash, binance, canva) or quoted (android_studio); this passes one
- # argument string, which is what was validated in CI for this installer. Note
- # not every NSIS uninstaller accepts it -- DBeaver's returns exit 2 and
- # Logitech Unifying's exit 10, so both of those omit it.
+ # NSIS uninstallers relaunch from %TEMP% and detach by default; "_?=<dir>"
+ # runs in place so this stays synchronous. Must be the last argument.
$installDir = Split-Path -Parent $uninstallCommand
$uninstallArgs = "/S _?=$installDir"
@@ -97,8 +70,7 @@
Write-Host "Uninstall args: $uninstallArgs"
$process = Start-Process -FilePath $uninstallCommand -ArgumentList $uninstallArgs -PassThru
- # Touch .Handle so the exit code is still readable after the process ends:
- # Start-Process -PassThru otherwise returns $null for .ExitCode.
+ # Keeps .ExitCode readable after the process ends.
$null = $process.Handle
if (-not $process.WaitForExit($timeoutSeconds * 1000)) {
@@ -114,7 +86,7 @@
Exit 1
}
-# Stop anything the uninstaller restarted, then wait for the ARP entry to clear.
+# Stop anything restarted, then wait for the ARP entry to clear.
foreach ($name in $leftovers) {
Stop-Process -Name $name -Force -ErrorAction SilentlyContinue
} |
Related issue: #50020
What this does
Adds Gpg4win as a Windows Fleet-maintained app. One of the 11 apps split out of #48501 that failed the FMA validator; #50016 shipped the 6 that passed.
Why it was failing
Same root cause as GNU Privacy Guard (#50025) — Gpg4win bundles GnuPG. The install worked; the script never returned:
Ten minutes on the nose is the validator's
executeScripttimeout.Start-Process -Waitwaits for the process and all of its descendants, and Gpg4win leavesgpg-agent,dirmngr,keyboxdandscdaemonresident (plus Kleopatra), so-Waitnever returns. The same run leftgpg4win-5.0.2.exelocked in the validator's temp dir, confirming a live child process.The install script now follows the pattern already established by
ollama_install.ps1: start with-PassThru(no-Wait), wait on the installer process alone with a 7-minute cap (below the caller's 10-minute script budget), poll for the Add/Remove Programs entry, then stop the leftovers.The uninstall script stops those processes up front (they hold file locks that make the uninstall fail), uses NSIS's
_?=<dir>switch so the uninstaller runs in place rather than relaunching itself detached from%TEMP%, and polls the ARP key to confirm removal.Notes
DisplayNameisGpg4win (5.0.2), so the input usesfuzzy_match_nameand the exists query isname LIKE 'Gpg4win %'. The uninstall script matches the same prefix.The Gpg4win Project.Checklist for submitter
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.Testing
All checks passed)apps.jsonis valid JSON with a description filled in.Summary by CodeRabbit