Skip to content

Add Gpg4win as a Windows FMA - #50026

Merged
allenhouchins merged 5 commits into
mainfrom
add-gpg4win-windows-fma
Jul 31, 2026
Merged

Add Gpg4win as a Windows FMA#50026
allenhouchins merged 5 commits into
mainfrom
add-gpg4win-windows-fma

Conversation

@kitzy

@kitzy kitzy commented Jul 28, 2026

Copy link
Copy Markdown
Member

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:

20:30:53  INFO  msg="Executing install script..." app=Gpg4win
20:40:53  ERROR msg="Error executing install script: exit status 1"   # exactly 10:00 later
20:40:53  INFO  msg="New application detected at: C:\Program Files\Gpg4win"

Ten minutes on the nose is the validator's executeScript timeout. Start-Process -Wait waits for the process and all of its descendants, and Gpg4win leaves gpg-agent, dirmngr, keyboxd and scdaemon resident (plus Kleopatra), so -Wait never returns. The same run left gpg4win-5.0.2.exe locked 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

  • Versioned ARP name. The registry DisplayName is Gpg4win (5.0.2), so the input uses fuzzy_match_name and the exists query is name LIKE 'Gpg4win %'. The uninstall script matches the same prefix.
  • x86-only installer. Publisher The Gpg4win Project.
  • Ships a new catalog icon and website asset.

Checklist for submitter

  • 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.
  • Timeouts are implemented and retries are limited to avoid infinite loops

Testing

  • FMA CI validator (install → detect → uninstall) passes on the SYSTEM-context Windows runner — run 30384125610 (All checks passed)
  • Generated output verified locally: manifest SHA matches the winget manifest, exists/patched queries reviewed for name + publisher correctness, apps.json is valid JSON with a description filled in.
  • QA'd all new/changed functionality manually

Summary by CodeRabbit

  • New Features
    • Added Gpg4win as a supported Windows application.
    • Added Gpg4win version 5.0.2 with Security categorization.
    • Added a Gpg4win icon to the software interface.
    • Introduced silent install and uninstall support with process cleanup, timeouts, and registry-based verification to confirm install/removal outcomes.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 00:07
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 68.03%. Comparing base (b64fdaa) to head (3ef175e).
⚠️ Report is 32 commits behind head on main.

Files with missing lines Patch % Lines
...nd/pages/SoftwarePage/components/icons/Gpg4Win.tsx 50.00% 1 Missing ⚠️
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     
Flag Coverage Δ
frontend 60.88% <50.00%> (+0.47%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in outputs/apps.json.
  • Adds a new software catalog icon component and maps gpg4win to 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.

Comment on lines +34 to +41
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"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +38 to +55
# 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"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 00:30
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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) ===

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Comment on lines +13 to +22
$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
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 01:02
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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) ===

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 15:18
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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
 }

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.

@kitzy
kitzy marked this pull request as ready for review July 28, 2026 16:16
@kitzy
kitzy requested a review from a team as a code owner July 28, 2026 16:16
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds 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

  • fleetdm/fleet#49923: Updates the frontend software icon registry with additional application mappings.
  • fleetdm/fleet#50016: Extends the frontend software icon registry with application-specific mappings.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: adding Gpg4win as a Windows Fleet-maintained app.
Description check ✅ Passed The description explains the change, failure cause, implementation, related issue, validation, testing, and manual QA.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-gpg4win-windows-fma

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx (1)

6-10: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Replace the large embedded PNG with a bundled asset.

Gpg4Win.tsx is roughly 18 MB because of the data: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

📥 Commits

Reviewing files that changed from the base of the PR and between b64fdaa and f849057.

⛔ Files ignored due to path filters (1)
  • website/assets/images/app-icon-gpg4win-60x60@2x.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • ee/maintained-apps/inputs/winget/gpg4win.json
  • ee/maintained-apps/inputs/winget/scripts/gpg4win_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/gpg4win_uninstall.ps1
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/gpg4win/windows.json
  • frontend/pages/SoftwarePage/components/icons/Gpg4Win.tsx
  • frontend/pages/SoftwarePage/components/icons/index.ts

Comment on lines +129 to +135
if (Get-Gpg4winUninstallKey) {
Write-Gpg4winCandidates
Write-Host "'$softwareName' is still registered after the uninstall."
Exit 1
}

Exit $exitCode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
"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.

Comment on lines +6 to +10
<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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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 || true

Repository: 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()}")
PY

Repository: 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])
PY

Repository: 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.

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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
 }

@allenhouchins allenhouchins changed the title Add Gpg4win Windows FMA Add Gpg4win as a Windows FMA Jul 31, 2026
@allenhouchins
allenhouchins merged commit 43bff98 into main Jul 31, 2026
36 checks passed
@allenhouchins
allenhouchins deleted the add-gpg4win-windows-fma branch July 31, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants