Files
Allen Houchins b18b0c2a05 Add Windows FMAs (letter D): 12 apps (#49086)
**Related issue:** N/A — part of the Windows Fleet-maintained apps
catalog expansion (letter D batch; follows #48872, #48881, #48950,
#48969).

Adds twelve new Windows Fleet-maintained apps:

| App | winget package | Installer | Notes |
|-----|----------------|-----------|-------|
| DataSpell | `JetBrains.DataSpell` | NSIS (install4j), machine, x64 |
Mirrors the DataGrip FMA pattern: `fuzzy_match_name` +
`use_display_version_for_patch` (registry version is a JetBrains build
number; marketing version parsed from the name). |
| dnGrep | `dnGrep.dnGrep` | MSI, machine, x64 | Versioned+arch ARP name
("dnGrep 5.0.9 (x64)") → fuzzy match. |
| Draftable Desktop | `Draftable.Draftable` | MSI, machine, x64 | Uses
the machine-scope `DraftableDesktopSystem` MSI. **Caveat:** hard winget
dependency on .NET 10 Desktop Runtime — installs fine but won't launch
without it (same class as the BleachBit VCRedist dependency). |
| dRofus | `dRofus.dRofus` | MSI, machine, x64 | Versioned ARP name
("dRofus 2.18") → fuzzy match. |
| Devolutions Launcher | `Devolutions.Launcher` | MSI, machine, x64 |
Distinct ARP identity from the existing Remote Desktop Manager FMA. |
| Devolutions Workspace | `Devolutions.Workspace` | MSI, machine, x64 |
Product renamed: ARP DisplayName is **"Devolutions Password Manager"**
(set as `unique_identifier`). `program_publisher` overridden to
"Devolutions Inc." (capital I) — verified via msiinfo; the
locale-derived lowercase would not match. |
| Delinea Connection Manager | `Delinea.DelineaConnectionManager` | MSI,
machine, x64 | Dual-purpose MSI defaults to **per-user** (ALLUSERS=2 +
MSIINSTALLPERUSER=1); custom install script forces `ALLUSERS=1
MSIINSTALLPERUSER=""`. `program_publisher` = "Delinea Inc.." (double
period, as stored in the MSI). Unversioned URL → `ignore_hash`. |
| DAX Studio | `DaxStudio.DaxStudio` | Inno, machine, x64 | `/ALLUSERS`
for machine scope; versioned ARP name → fuzzy. |
| DevPod | `LoftLabs.DevPod` | MSI, machine, x64 | Uses the WiX MSI
variant (the manifest also has an NSIS per-user one). |
| Directory Opus | `GPSoftware.DirectoryOpus` | Inno, machine, x64 |
Standard Inno silent install/uninstall. |
| DYMO ID | `DYMO.DYMOID` | InstallShield→MSI, machine, x86 | `/S /V"/qn
/norestart"`; uninstall via msiexec by ProductCode. |
| digiSeal Reader | `secrypt.digiSealreader` | EXE (self-extracting),
machine, x86 | `installer_scope: ""` (manifest declares no scope);
`-silent` install, shipped uninstaller with `-silent`. Unversioned URL →
`ignore_hash`. |

Considered but **not** added (recorded in the workstream tracker):
- **Datadog Agent** (`Datadog.Agent`): winget PackageVersion is
`7.81.0.1` but the MSI and registry report `7.81.0.0`, so the patch
policy would flag every install as perpetually outdated.
`use_display_version_for_patch` can't fix it (the manifest has no
`AppsAndFeaturesEntries`); it needs a version-normalizing ingester ref
(like `onepassword_version_shortener`). Deferred pending that helper
(task spawned).
- **Dell Display and Peripheral Manager**
(`Dell.DisplayAndPeripheralManager`, covers both "Dell Display Manager"
and "Dell Peripheral Manager"): the winget-pinned host `dl.dell.com`
returns **403** to non-browser User-Agents, and Fleet's downloader sends
`Go-http-client` (same failure that dropped Crestron AirMedia in letter
C). The `downloads.dell.com` mirror serves the identical path to any UA,
but there's no input field to override the manifest URL. Deferred
pending a downloader User-Agent fix (task spawned — would also unblock
Crestron).
- **Dell EMC System Update** (`Dell.SystemUpdate`): registers its ARP
entry under **HKCU** (per-user) under a SYSTEM install; legacy product
Dell steers users away from (toward Dell Command Update, already an
FMA); firmware/driver DUP bootstrapper.
- **Devolutions Remote Desktop Manager Agent**
(`Devolutions.RemoteDesktopManagerAgent`): legacy/superseded by
"Devolutions Agent" (2026.1); winget manifest frozen at 2025.2.28.0
since Sept 2025; vendor docs page 404s.
- **Dedoose** (`Dedoose.Dedoose`): per-user-only scope, x86,
latest-pointer URL (CloudShow class).
- **Defraggler** (`Piriform.Defraggler`): abandoned (final release 2020,
no winget commits since 2023); legacy defrag tool with an unverified ARP
publisher string.
- **DiRoots ProSheets** (`DiRoots.ProSheets`): validated install but its
Advanced Installer bootstrapper uninstall hung to the timeout and it
drags bundled PDF24 Creator entries into inventory (dropped at
validation).
- **DroidCam Client** (`dev47apps.DroidCam`): NSIS `/S` install hung
headless to the timeout (inline vc_redist); 3DxWare/Citrix headless-hang
class (dropped at validation).

Identities verified per app (msiinfo Property tables; installer
PE/version resources; winget AppsAndFeaturesEntries; uninstall-database
corroboration). Two publisher-casing mismatches (Devolutions Workspace,
Delinea) were caught by direct MSI inspection and fixed with
`program_publisher` before they could silently break the exists queries.
SHAs verified against manifests for pinned URLs; `ignore_hash` used only
where the manifest is demonstrably actively maintained. Icons via
`tools/software/icons/generate-icons.sh` (the pre-existing DataSpell
icon component is reused untouched).

# 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

- [ ] QA'd all new/changed functionality manually (relying on the FMA CI
validator for Windows install/uninstall validation)
2026-07-10 08:54:58 -05:00

94 lines
2.8 KiB
PowerShell

# Locates DataSpell's NSIS uninstaller from the registry and runs it silently.
# JetBrains NSIS installers embed the version in DisplayName (e.g.
# "DataSpell 2026.1.2"), so we match by prefix and require the JetBrains
# publisher to avoid collisions with other JetBrains IDEs.
$softwareNameLike = "DataSpell*"
$publisherLike = "*JetBrains*"
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
$exitCode = 0
try {
[array]$uninstallKeys = Get-ChildItem `
-Path $paths `
-ErrorAction SilentlyContinue |
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
$selected = $null
foreach ($key in $uninstallKeys) {
if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike -and $key.Publisher -like $publisherLike) {
$selected = $key
break
}
}
if (-not $selected -or -not $selected.UninstallString) {
Write-Host "Uninstall entry not found for $softwareNameLike"
Exit 1
}
# Best-effort: stop the IDE so the uninstaller doesn't fail on locked files.
Stop-Process -Name "dataspell64" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "dataspell" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "fsnotifier" -Force -ErrorAction SilentlyContinue
$uninstallCommand = $selected.UninstallString
# Split the uninstall string into exe + args. Handle both quoted and unquoted
# exe paths.
$exePath = ""
$existingArgs = ""
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
# Quoted path: "C:\Path With Spaces\uninst.exe" [args]
$exePath = $matches[1]
$existingArgs = $matches[2].Trim()
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
# Unquoted path that may contain spaces: capture through the .exe.
# JetBrains stores e.g.
# C:\Program Files\JetBrains\DataSpell 2026.1.2\bin\Uninstall.exe
$exePath = $matches[1]
$existingArgs = $matches[2].Trim()
} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
# Fallback: no .exe found, split on first whitespace.
$exePath = $matches[1]
$existingArgs = $matches[2].Trim()
} else {
Throw "Could not parse uninstall string: $uninstallCommand"
}
# NSIS uninstallers require /S for silent uninstall.
if ($existingArgs -notmatch '\b/S\b') {
$existingArgs = ("$existingArgs /S").Trim()
}
Write-Host "Selected entry DisplayName: $($selected.DisplayName)"
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"
$processOptions = @{
FilePath = $exePath
PassThru = $true
Wait = $true
}
if ($existingArgs -ne '') {
$processOptions.ArgumentList = $existingArgs
}
$process = Start-Process @processOptions
$exitCode = $process.ExitCode
Write-Host "Uninstall exit code: $exitCode"
Exit $exitCode
} catch {
Write-Host "Error: $_"
Exit 1
}