diff --git a/ee/maintained-apps/inputs/winget/gnupg.json b/ee/maintained-apps/inputs/winget/gnupg.json new file mode 100644 index 0000000000..6a3d4edc7c --- /dev/null +++ b/ee/maintained-apps/inputs/winget/gnupg.json @@ -0,0 +1,13 @@ +{ + "name": "GNU Privacy Guard", + "slug": "gnupg/windows", + "package_identifier": "GnuPG.GnuPG", + "unique_identifier": "GNU Privacy Guard", + "program_publisher": "The GnuPG Project", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/gnupg_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/gnupg_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "", + "default_categories": ["Security"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/gnupg_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/gnupg_install.ps1 new file mode 100644 index 0000000000..f036f34d79 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/gnupg_install.ps1 @@ -0,0 +1,77 @@ +# 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 its window lets it run through to the section that writes the +# Add/Remove Programs entry; killing it instead would leave a partial install. +$daemons = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf", "gpa", "launch-gpa") +$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\*' + +function Test-GnuPGRegistered { + $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | + Where-Object { $_.DisplayName -like "GNU Privacy Guard*" } | + 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 } + + $children = @(Get-Process -Name $daemons -ErrorAction SilentlyContinue | + Select-Object -ExpandProperty Name -Unique) + $windowTitle = "" + try { $windowTitle = $process.MainWindowTitle } catch { } + + Write-Host "Installing... ($elapsed seconds, registered: $(Test-GnuPGRegistered), 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() + } +} + +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 daemons; they hold file locks the uninstall needs released. +foreach ($name in $daemons) { + Stop-Process -Name $name -Force -ErrorAction SilentlyContinue +} + +# Registration is the success signal: a killed installer's exit code says nothing. +if (-not (Test-GnuPGRegistered)) { + Write-Host "GnuPG did not register in Add/Remove Programs." + Exit 1 +} + +Write-Host "GnuPG is registered in Add/Remove Programs." +Exit 0 + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/gnupg_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/gnupg_uninstall.ps1 new file mode 100644 index 0000000000..dfb1afe13b --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/gnupg_uninstall.ps1 @@ -0,0 +1,89 @@ +$softwareName = "GNU Privacy Guard" +$softwarePublisher = "The GnuPG Project" + +# The daemons hold file locks and would block -Wait, so stop them first and wait +# only on the uninstaller process. +$daemons = @("gpg-agent", "dirmngr", "keyboxd", "scdaemon", "gpg-connect-agent", "gpgconf") +$timeoutSeconds = 300 + +$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\*' +$userKey32on64 = 'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' +$exitCode = 0 + +function Get-GnuPGUninstallKey { + Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey, $userKey32on64) -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } | + Where-Object { $_.DisplayName -like "$softwareName*" -and $_.Publisher -eq $softwarePublisher } | + Select-Object -First 1 +} + +foreach ($daemon in $daemons) { + Stop-Process -Name $daemon -Force -ErrorAction SilentlyContinue +} + +try { + $key = Get-GnuPGUninstallKey + if (-not $key) { + Write-Host "Uninstall entry not found for '$softwareName'." + Exit 0 + } + + $uninstallString = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString } + Write-Host "Uninstall string: $uninstallString" + + # Handles quoted paths, unquoted paths with 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 relaunch from %TEMP% and detach by default; "_?=" + # runs in place so this stays synchronous. Must be the last argument. + $installDir = Split-Path -Parent $uninstallCommand + $uninstallArgs = "/S _?=$installDir" + + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $process = Start-Process -FilePath $uninstallCommand -ArgumentList $uninstallArgs -PassThru + # Keeps .ExitCode readable after the process ends. + $null = $process.Handle + + if (-not $process.WaitForExit($timeoutSeconds * 1000)) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + Write-Host "Uninstall timed out after $timeoutSeconds seconds" + Exit 1603 + } + + $exitCode = $process.ExitCode + Write-Host "Uninstall exit code: $exitCode" +} catch { + Write-Host "Error: $_" + Exit 1 +} + +# Stop anything restarted, then wait for the ARP entry to clear. +foreach ($daemon in $daemons) { + Stop-Process -Name $daemon -Force -ErrorAction SilentlyContinue +} + +$elapsed = 0 +while ((Get-GnuPGUninstallKey) -and ($elapsed -lt 120)) { + Start-Sleep -Seconds 5 + $elapsed += 5 + Write-Host "Waiting for the uninstall to finish... ($elapsed seconds)" +} + +if (Get-GnuPGUninstallKey) { + Write-Host "'$softwareName' is still registered after the uninstall." + Exit 1 +} + +Exit $exitCode diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index bd2065b21d..64e6992784 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -3753,6 +3753,13 @@ "unique_identifier": "com.GeorgSeifert.Glyphs3", "description": "Glyphs is a font editor." }, + { + "name": "GNU Privacy Guard", + "slug": "gnupg/windows", + "platform": "windows", + "unique_identifier": "GNU Privacy Guard", + "description": "GNU Privacy Guard is an implementation of the OpenPGP standard for encrypting and signing data and communications." + }, { "name": "Go", "slug": "go/windows", diff --git a/ee/maintained-apps/outputs/gnupg/windows.json b/ee/maintained-apps/outputs/gnupg/windows.json new file mode 100644 index 0000000000..df50bb0792 --- /dev/null +++ b/ee/maintained-apps/outputs/gnupg/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "2.5.21", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'GNU Privacy Guard' AND publisher = 'The GnuPG Project';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'GNU Privacy Guard' AND publisher = 'The GnuPG Project' AND version_compare(version, '2.5.21') < 0);" + }, + "installer_url": "https://gnupg.org/ftp/gcrypt/binary/gnupg-w32-2.5.21_20260702.exe", + "install_script_ref": "8c2ff75e", + "uninstall_script_ref": "d1a2230f", + "sha256": "6246c925a73167253444afc24a0deb83a3f43b7d636af84d6aaf48a98a62f024", + "default_categories": [ + "Security" + ] + } + ], + "refs": { + "8c2ff75e": "# 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 installer stalls on a modal dialog with no interactive desktop and never\n# exits. Closing its window lets it run through to the section that writes the\n# Add/Remove Programs entry; killing it instead would leave a partial install.\n$daemons = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\", \"gpa\", \"launch-gpa\")\n$installTimeoutSeconds = 420\n$pollSeconds = 10\n$graceSeconds = 30\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n# Uninstall info is written with SHCTX, so it can land per-user.\n$userKey = 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\nfunction Test-GnuPGRegistered {\n $null -ne (Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"GNU Privacy Guard*\" } |\n Select-Object -First 1)\n}\n\ntry {\n\n$process = Start-Process -FilePath \"$exeFilePath\" -ArgumentList \"/S\" -PassThru\n# Keeps .ExitCode readable after the process ends.\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 $children = @(Get-Process -Name $daemons -ErrorAction SilentlyContinue |\n Select-Object -ExpandProperty Name -Unique)\n $windowTitle = \"\"\n try { $windowTitle = $process.MainWindowTitle } catch { }\n\n Write-Host \"Installing... ($elapsed seconds, registered: $(Test-GnuPGRegistered), window: '$windowTitle', children: $($children -join ', '))\"\n\n if ($elapsed -ge $graceSeconds -and $process.MainWindowHandle -ne [IntPtr]::Zero) {\n Write-Host \"Installer is showing a window ('$windowTitle'); closing it so the install can continue.\"\n $null = $process.CloseMainWindow()\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 daemons; they hold file locks the uninstall needs released.\nforeach ($name in $daemons) {\n Stop-Process -Name $name -Force -ErrorAction SilentlyContinue\n}\n\n# Registration is the success signal: a killed installer's exit code says nothing.\nif (-not (Test-GnuPGRegistered)) {\n Write-Host \"GnuPG did not register in Add/Remove Programs.\"\n Exit 1\n}\n\nWrite-Host \"GnuPG is registered in Add/Remove Programs.\"\nExit 0\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "d1a2230f": "$softwareName = \"GNU Privacy Guard\"\n$softwarePublisher = \"The GnuPG Project\"\n\n# The daemons hold file locks and would block -Wait, so stop them first and wait\n# only on the uninstaller process.\n$daemons = @(\"gpg-agent\", \"dirmngr\", \"keyboxd\", \"scdaemon\", \"gpg-connect-agent\", \"gpgconf\")\n$timeoutSeconds = 300\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n# Uninstall info is written with SHCTX, so it can land per-user.\n$userKey = 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$userKey32on64 = 'HKCU:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$exitCode = 0\n\nfunction Get-GnuPGUninstallKey {\n Get-ChildItem -Path @($machineKey, $machineKey32on64, $userKey, $userKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -like \"$softwareName*\" -and $_.Publisher -eq $softwarePublisher } |\n Select-Object -First 1\n}\n\nforeach ($daemon in $daemons) {\n Stop-Process -Name $daemon -Force -ErrorAction SilentlyContinue\n}\n\ntry {\n $key = Get-GnuPGUninstallKey\n if (-not $key) {\n Write-Host \"Uninstall entry not found for '$softwareName'.\"\n Exit 0\n }\n\n $uninstallString = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }\n Write-Host \"Uninstall string: $uninstallString\"\n\n # Handles quoted paths, unquoted paths with 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 relaunch from %TEMP% and detach by default; \"_?=\"\n # runs in place so this stays synchronous. Must be the last argument.\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 # Keeps .ExitCode readable after the process ends.\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 restarted, then wait for the ARP entry to clear.\nforeach ($daemon in $daemons) {\n Stop-Process -Name $daemon -Force -ErrorAction SilentlyContinue\n}\n\n$elapsed = 0\nwhile ((Get-GnuPGUninstallKey) -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-GnuPGUninstallKey) {\n Write-Host \"'$softwareName' is still registered after the uninstall.\"\n Exit 1\n}\n\nExit $exitCode\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/Gnupg.tsx b/frontend/pages/SoftwarePage/components/icons/Gnupg.tsx new file mode 100644 index 0000000000..4f2ae6ae7a --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Gnupg.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const Gnupg = (props: SVGProps) => ( + + + +); +export default Gnupg; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index a4628d4b4c..3a7b4ce0e4 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -440,6 +440,7 @@ import Gitify from "./Gitify"; import GitKraken from "./GitKraken"; import GitupApp from "./GitupApp"; import Glyphs from "./Glyphs"; +import Gnupg from "./Gnupg"; import Go from "./Go"; import Go2Shell from "./Go2Shell"; import GoanywhereOpenpgpStudio from "./GoanywhereOpenpgpStudio"; @@ -1585,6 +1586,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { gitkraken: GitKraken, gitup: GitupApp, glyphs: Glyphs, + "gnu privacy guard": Gnupg, go: Go, go2shell: Go2Shell, "goanywhere openpgp studio": GoanywhereOpenpgpStudio, diff --git a/website/assets/images/app-icon-gnupg-60x60@2x.png b/website/assets/images/app-icon-gnupg-60x60@2x.png new file mode 100644 index 0000000000..810a898128 Binary files /dev/null and b/website/assets/images/app-icon-gnupg-60x60@2x.png differ