<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **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`](ee/maintained-apps/inputs/winget/scripts/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 - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] FMA CI validator (install → detect → uninstall) **passes** on the SYSTEM-context Windows runner — [run 30384125610](https://github.com/fleetdm/fleet/actions/runs/30384125610) (`All checks passed`) - [x] 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. - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
102 lines
3.8 KiB
PowerShell
102 lines
3.8 KiB
PowerShell
# Learn more about .exe install scripts:
|
|
# http://fleetdm.com/learn-more-about/exe-install-scripts
|
|
|
|
$exeFilePath = "${env:INSTALLER_PATH}"
|
|
|
|
# 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
|
|
$graceSeconds = 30
|
|
|
|
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
# Uninstall info is written with SHCTX, so it can land per-user.
|
|
$userKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
|
|
# The installer process plus its descendants.
|
|
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)
|
|
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*" } |
|
|
Select-Object -First 1)
|
|
}
|
|
|
|
try {
|
|
|
|
$process = Start-Process -FilePath "$exeFilePath" -ArgumentList "/S" -PassThru
|
|
# Keeps .ExitCode readable after the process ends.
|
|
$null = $process.Handle
|
|
|
|
$elapsed = 0
|
|
while (-not $process.HasExited -and ($elapsed -lt $installTimeoutSeconds)) {
|
|
Start-Sleep -Seconds $pollSeconds
|
|
$elapsed += $pollSeconds
|
|
$process.Refresh()
|
|
if ($process.HasExited) { break }
|
|
|
|
$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', tree: $($names -join ', '), child windows: $($childWindows -join ' | '))"
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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: a killed installer's exit code says nothing.
|
|
if (-not (Test-Gpg4winRegistered)) {
|
|
Write-Host "Gpg4win did not register in Add/Remove Programs."
|
|
Exit 1
|
|
}
|
|
|
|
Write-Host "Gpg4win is registered in Add/Remove Programs."
|
|
Exit 0
|
|
|
|
} catch {
|
|
Write-Host "Error: $_"
|
|
Exit 1
|
|
}
|