Add Paint.NET as a Windows Fleet-maintained app (#50340)
**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>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Paint.NET",
|
||||
"slug": "paint-dot-net/windows",
|
||||
"package_identifier": "dotPDN.PaintDotNet",
|
||||
"unique_identifier": "Paint.NET",
|
||||
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/paint_dot_net_install.ps1",
|
||||
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/paint_dot_net_uninstall.ps1",
|
||||
"installer_arch": "x64",
|
||||
"installer_type": "zip",
|
||||
"installer_scope": "machine",
|
||||
"default_categories": ["Productivity"]
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# The ProductCode changes every release and differs between the .exe and .msi
|
||||
# variants; the UpgradeCode is stable across both.
|
||||
$upgradeCode = '{04A40F40-A207-4B48-AED7-6AA532E43275}'
|
||||
$timeoutSeconds = 300
|
||||
$successCodes = @(0, 3010, 1641)
|
||||
|
||||
try {
|
||||
$inst = New-Object -ComObject "WindowsInstaller.Installer"
|
||||
# An empty list means nothing to remove; a failed query means we don't know.
|
||||
$productCodes = @()
|
||||
try {
|
||||
$productCodes = @($inst.RelatedProducts($upgradeCode))
|
||||
} catch {
|
||||
Write-Host "Could not query related products for upgrade code $upgradeCode. Error: $_"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
if ($productCodes.Count -eq 0) { Write-Host "No installed product found for upgrade code $upgradeCode."; Exit 0 }
|
||||
|
||||
foreach ($productCode in $productCodes) {
|
||||
$process = Start-Process msiexec -ArgumentList @("/quiet", "/x", $productCode, "/norestart") -PassThru
|
||||
if (-not $process.WaitForExit($timeoutSeconds * 1000)) {
|
||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Uninstall for $productCode timed out."
|
||||
Exit 1603
|
||||
}
|
||||
Write-Host "Uninstall for $productCode exited $($process.ExitCode)"
|
||||
if ($successCodes -notcontains $process.ExitCode) { Exit $process.ExitCode }
|
||||
}
|
||||
} catch { Write-Host "Error: $_"; Exit 1 }
|
||||
|
||||
Exit 0
|
||||
@@ -6392,6 +6392,13 @@
|
||||
"unique_identifier": "com.charlessoft.pacifist",
|
||||
"description": "Pacifist is an extract files and folders from package files, disk images, and archives."
|
||||
},
|
||||
{
|
||||
"name": "Paint.NET",
|
||||
"slug": "paint-dot-net/windows",
|
||||
"platform": "windows",
|
||||
"unique_identifier": "Paint.NET",
|
||||
"description": "Paint.NET is an image and photo editor with support for layers, effects, and a range of image formats."
|
||||
},
|
||||
{
|
||||
"name": "Pale Moon",
|
||||
"slug": "pale-moon/darwin",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "5.1.12",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM programs WHERE name = 'Paint.NET' AND publisher = 'dotPDN LLC';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Paint.NET' AND publisher = 'dotPDN LLC' AND version_compare(version, '5.1.12') < 0);"
|
||||
},
|
||||
"installer_url": "https://github.com/paintdotnet/release/releases/download/v5.1.12/paint.net.5.1.12.install.x64.zip",
|
||||
"install_script_ref": "1dd07f15",
|
||||
"uninstall_script_ref": "a23d064d",
|
||||
"sha256": "3cd861b5af3f85bd28666d4a9017d03de237c08ad0103ee9d92b2cd921f8c867",
|
||||
"default_categories": [
|
||||
"Productivity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"1dd07f15": "# Paint.NET ships as a zip containing its installer .exe.\n\n$zipFilePath = \"${env:INSTALLER_PATH}\"\n\n$installTimeoutSeconds = 420\n$registrationTimeoutSeconds = 120\n\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\n# Same DisplayName and Publisher the catalog's exists query uses.\nfunction Get-PaintDotNetEntry {\n Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -eq \"Paint.NET\" -and $_.Publisher -eq \"dotPDN LLC\" } |\n Select-Object -First 1\n}\n\ntry {\n\n$extractPath = Join-Path $env:TEMP \"PaintDotNetInstall\"\nif (Test-Path $extractPath) { Remove-Item -Path $extractPath -Recurse -Force }\nExpand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force\n\n$installer = Get-ChildItem -Path $extractPath -Filter \"*.exe\" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1\nif (-not $installer) {\n Write-Host \"Error: installer .exe not found under $extractPath\"\n Exit 1\n}\n\n# /auto is the vendor's silent switch. -Wait would also wait on descendants.\n$process = Start-Process -FilePath $installer.FullName -ArgumentList \"/auto\" -PassThru\n$null = $process.Handle # keeps .ExitCode readable after exit\n\n$killed = $false\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 $null = $process.WaitForExit(30 * 1000)\n $killed = $true\n}\n\n$exitCode = $null\nif ($process.HasExited) {\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n}\n\n# The installer can return before the ARP entry is written.\n$elapsed = 0\nwhile (-not (Get-PaintDotNetEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for Paint.NET to register... ($elapsed seconds)\"\n}\n\nRemove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue\n\nStop-Process -Name \"paintdotnet\" -Force -ErrorAction SilentlyContinue\n\n$entry = Get-PaintDotNetEntry\nif (-not $entry) {\n Write-Host \"Paint.NET did not register in Add/Remove Programs.\"\n Exit 1\n}\nWrite-Host \"Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion).\"\n\n# Registration is the success signal; a killed process's code means nothing.\nif ($killed -or $null -eq $exitCode) { Exit 0 }\n\n# 3010/1641 = reboot required/initiated.\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",
|
||||
"a23d064d": "# The ProductCode changes every release and differs between the .exe and .msi\n# variants; the UpgradeCode is stable across both.\n$upgradeCode = '{04A40F40-A207-4B48-AED7-6AA532E43275}'\n$timeoutSeconds = 300\n$successCodes = @(0, 3010, 1641)\n\ntry {\n $inst = New-Object -ComObject \"WindowsInstaller.Installer\"\n # An empty list means nothing to remove; a failed query means we don't know.\n $productCodes = @()\n try {\n $productCodes = @($inst.RelatedProducts($upgradeCode))\n } catch {\n Write-Host \"Could not query related products for upgrade code $upgradeCode. Error: $_\"\n Exit 1\n }\n\n if ($productCodes.Count -eq 0) { Write-Host \"No installed product found for upgrade code $upgradeCode.\"; Exit 0 }\n\n foreach ($productCode in $productCodes) {\n $process = Start-Process msiexec -ArgumentList @(\"/quiet\", \"/x\", $productCode, \"/norestart\") -PassThru\n if (-not $process.WaitForExit($timeoutSeconds * 1000)) {\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n Write-Host \"Uninstall for $productCode timed out.\"\n Exit 1603\n }\n Write-Host \"Uninstall for $productCode exited $($process.ExitCode)\"\n if ($successCodes -notcontains $process.ExitCode) { Exit $process.ExitCode }\n }\n} catch { Write-Host \"Error: $_\"; Exit 1 }\n\nExit 0\n"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -752,6 +752,7 @@ import OrigamiStudio from "./OrigamiStudio";
|
||||
import P4V from "./P4V";
|
||||
import Pacifist from "./Pacifist";
|
||||
import Package from "./Package";
|
||||
import PaintDotNet from "./PaintDotNet";
|
||||
import PaleMoon from "./PaleMoon";
|
||||
import Paletro from "./Paletro";
|
||||
import ParallelsDesktop from "./ParallelsDesktop";
|
||||
@@ -1915,6 +1916,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
|
||||
p4v: P4V,
|
||||
pacifist: Pacifist,
|
||||
package: Package,
|
||||
"paint.net": PaintDotNet,
|
||||
"pale moon": PaleMoon,
|
||||
paletro: Paletro,
|
||||
"parallels desktop": ParallelsDesktop,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Reference in New Issue
Block a user