**Related issue:** Resolves #50330 Adds Paint.NET as a Windows Fleet-maintained app, from winget `dotPDN.PaintDotNet` (5.1.12, machine scope, x64). Found in a customer's ManageEngine ServiceDesk Plus Windows deployment catalog with no Fleet equivalent. ## Identity: winget's metadata is wrong here The winget locale manifest gives `PackageName: paint.net` (lowercase). The actual registry `DisplayName` is **`Paint.NET`**, read straight out of the MSI Property table: ``` ProductName Paint.NET Manufacturer dotPDN LLC UpgradeCode {04A40F40-A207-4B48-AED7-6AA532E43275} ALLUSERS 2 ``` There is no `ARPDISPLAYNAME` override and no `ARPSYSTEMCOMPONENT`, so `ProductName` is what lands in Add/Remove Programs. Taking the winget name at face value would have produced an exists query that silently never matches. `ALLUSERS=2` confirms it installs per-machine when run elevated, which is how Fleet runs it. ## This is a zip-wrapped installer Paint.NET publishes **only** `.zip` assets — there is no bare `.exe` or `.msi` on the vendor's GitHub releases. So this uses `installer_type: zip` with custom scripts, following the existing precedent of `agent-ransack`, `adobe-acrobat-pro`, `vnc-server`, and `vnc-viewer`. The install script extracts the archive and runs the nested installer with `/auto`, the vendor's silent switch per the manifest's `InstallerSwitches`. **Uninstall resolves the product from the UpgradeCode, not the ProductCode.** Paint.NET's ProductCode changes with every release, and the `.exe` and `.msi` variants register *different* ProductCodes. The UpgradeCode is stable — I verified it is identical across 5.1.10 and 5.1.12 — so `RelatedProducts` on it removes whichever variant is present. ## One thing reviewers may want to change The manifest offers six installers; three are x64/machine/zip and differ only by `NestedInstallerType` (`exe`, `wix`, `portable`). The ingester's selection loop takes the **first** match and breaks, so it picks the `.install.x64.exe` bootstrapper. The `.winmsi.x64.zip` variant is arguably the better FMA target — a plain MSI with predictable ARP behaviour — but there is no way to express "prefer this nested type" in the input today. Selecting it would need an ingester change, so I did not do it here. Worth a follow-up if we hit trouble with the bootstrapper. ## Verification - Zip SHA confirmed against a local download (`3cd861b5…c867`); archive contains exactly one file, `paint.net.5.1.12.install.x64.exe`. - Icon extracted from that installer's own 256px resource, not sourced from the web. - Icon map key is `"paint.net"`, the lowercased catalog name. The icon generator derives its key from the slug and produced `"paint dot net"`, which would never have matched at runtime — corrected by hand. # Checklist for submitter - [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 Paint.NET to the Windows software catalog. * Added support for installing, upgrading, detecting, and uninstalling Paint.NET. * Added Paint.NET branding and an icon to the software interface. * Included Paint.NET version 5.1.12 with verified download metadata and Productivity categorization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Allen Houchins <allenhouchins@mac.com>
80 lines
2.7 KiB
PowerShell
80 lines
2.7 KiB
PowerShell
# Paint.NET ships as a zip containing its installer .exe.
|
|
|
|
$zipFilePath = "${env:INSTALLER_PATH}"
|
|
|
|
$installTimeoutSeconds = 420
|
|
$registrationTimeoutSeconds = 120
|
|
|
|
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
|
|
# Same DisplayName and Publisher the catalog's exists query uses.
|
|
function Get-PaintDotNetEntry {
|
|
Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
|
|
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
|
|
Where-Object { $_.DisplayName -eq "Paint.NET" -and $_.Publisher -eq "dotPDN LLC" } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
try {
|
|
|
|
$extractPath = Join-Path $env:TEMP "PaintDotNetInstall"
|
|
if (Test-Path $extractPath) { Remove-Item -Path $extractPath -Recurse -Force }
|
|
Expand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force
|
|
|
|
$installer = Get-ChildItem -Path $extractPath -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if (-not $installer) {
|
|
Write-Host "Error: installer .exe not found under $extractPath"
|
|
Exit 1
|
|
}
|
|
|
|
# /auto is the vendor's silent switch. -Wait would also wait on descendants.
|
|
$process = Start-Process -FilePath $installer.FullName -ArgumentList "/auto" -PassThru
|
|
$null = $process.Handle # keeps .ExitCode readable after exit
|
|
|
|
$killed = $false
|
|
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
|
|
$null = $process.WaitForExit(30 * 1000)
|
|
$killed = $true
|
|
}
|
|
|
|
$exitCode = $null
|
|
if ($process.HasExited) {
|
|
$exitCode = $process.ExitCode
|
|
Write-Host "Install exit code: $exitCode"
|
|
}
|
|
|
|
# The installer can return before the ARP entry is written.
|
|
$elapsed = 0
|
|
while (-not (Get-PaintDotNetEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) {
|
|
Start-Sleep -Seconds 5
|
|
$elapsed += 5
|
|
Write-Host "Waiting for Paint.NET to register... ($elapsed seconds)"
|
|
}
|
|
|
|
Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue
|
|
|
|
Stop-Process -Name "paintdotnet" -Force -ErrorAction SilentlyContinue
|
|
|
|
$entry = Get-PaintDotNetEntry
|
|
if (-not $entry) {
|
|
Write-Host "Paint.NET did not register in Add/Remove Programs."
|
|
Exit 1
|
|
}
|
|
Write-Host "Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion)."
|
|
|
|
# Registration is the success signal; a killed process's code means nothing.
|
|
if ($killed -or $null -eq $exitCode) { Exit 0 }
|
|
|
|
# 3010/1641 = reboot required/initiated.
|
|
if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
|
|
|
|
Exit $exitCode
|
|
|
|
} catch {
|
|
Write-Host "Error: $_"
|
|
Exit 1
|
|
}
|