<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** #50020 # What this does Adds **Logitech Unifying Software** 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 Install and detection were already fine on the SYSTEM-context Windows runner — osquery found `Logitech Unifying Software 2.52` at `C:\Program Files\Common Files\LogiShrd\Unifying`. **Uninstall** was the failure: ``` 20:40:55 INFO msg="Executing uninstall script for app..." 20:40:57 INFO msg="Found app: 'Logitech Unifying Software 2.52' ... Version: 2.52.33" 20:40:57 ERROR msg="App still present after uninstall (expected no match for version '2.52.33' in programs)" ``` Two seconds start to finish — the uninstaller hadn't actually done anything yet. This is standard NSIS behavior: the uninstaller copies itself to `%TEMP%` and relaunches, so the process the script starts exits almost immediately while the real work happens in a detached child. The fix passes NSIS's `_?=<dir>` switch, which runs the uninstaller in place instead of relaunching, making it synchronous. It has to be the last argument and unquoted, so the script builds a single argument string rather than an array (PowerShell would quote an element containing spaces). A bounded poll on the ARP key follows as a backstop, and the script fails explicitly if the entry is still there. ## Notes - **Versioned ARP name.** The registry `DisplayName` is `Logitech Unifying Software 2.52`, so the input uses `fuzzy_match_name` and the exists query is `name LIKE 'Logitech Unifying Software %'`. The uninstall script matches the same prefix rather than an exact string. - Publisher `Logitech` confirmed against the winget locale manifest. - Installs under `C:\Program Files\Common Files`, so the validator's "no changes detected in `C:\Program Files`" line is an expected warning, not a failure. - 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 30384010810](https://github.com/fleetdm/fleet/actions/runs/30384010810) (`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 Logitech Unifying Software to the Windows software catalog, including the version 2.52.33 download, checksum, and install-detection metadata. * Implemented silent installation and a robust, registry-aware uninstall flow (with process lock handling and timeout behavior). * Added a dedicated Logitech Unifying Software icon to the software page UI. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
99 lines
4.0 KiB
PowerShell
99 lines
4.0 KiB
PowerShell
# The ARP DisplayName carries a version suffix ("Logitech Unifying Software 2.52"),
|
|
# so match on a prefix, plus the publisher to avoid other products sharing it.
|
|
$softwareName = "Logitech Unifying Software"
|
|
$softwareNameLike = "$softwareName*"
|
|
$softwarePublisher = "Logitech"
|
|
|
|
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
$exitCode = 0
|
|
$timeoutSeconds = 300
|
|
|
|
# Logs matching entries and their publishers to diagnose a name/publisher miss.
|
|
function Write-UnifyingCandidates {
|
|
Write-Host "Registry entries matching '$softwareNameLike':"
|
|
$found = Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
|
|
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
|
|
Where-Object { $_.DisplayName -like $softwareNameLike }
|
|
if (-not $found) { Write-Host " (none)" ; return }
|
|
foreach ($f in $found) {
|
|
Write-Host " DisplayName='$($f.DisplayName)' Publisher='$($f.Publisher)' Version='$($f.DisplayVersion)'"
|
|
}
|
|
}
|
|
|
|
function Get-UnifyingUninstallKey {
|
|
Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
|
|
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
|
|
Where-Object { $_.DisplayName -like $softwareNameLike -and $_.Publisher -eq $softwarePublisher } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
# Stop leftovers: they hold file locks, and -Wait would block on them.
|
|
foreach ($name in @("LogiUnify", "Unifying", "UnifyingUnInstaller", "DJCUHost")) {
|
|
Stop-Process -Name $name -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
try {
|
|
$key = Get-UnifyingUninstallKey
|
|
if (-not $key) {
|
|
Write-UnifyingCandidates
|
|
Write-Host "Uninstall entry not found for '$softwareName' with publisher '$softwarePublisher'."
|
|
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
|
|
$existingArgs = ""
|
|
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
|
|
$uninstallCommand = $Matches[1]; $existingArgs = $Matches[2]
|
|
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
|
|
$uninstallCommand = $Matches[1]; $existingArgs = $Matches[2]
|
|
} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
|
|
$uninstallCommand = $Matches[1]; $existingArgs = $Matches[2]
|
|
}
|
|
|
|
# This vendor NSIS uninstaller rejects the in-place "_?=<dir>" switch with
|
|
# exit code 10, so use plain /S and poll for removal at the end instead.
|
|
# Keep any registry arguments rather than dropping them.
|
|
$uninstallArgs = ("$existingArgs /S").Trim()
|
|
|
|
Write-Host "Uninstall command: $uninstallCommand"
|
|
Write-Host "Uninstall args: $uninstallArgs"
|
|
|
|
# No -NoNewWindow: a leftover child would hold this script's pipes open.
|
|
$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
|
|
}
|
|
|
|
# The uninstaller hands off, so wait for the ARP entry to disappear.
|
|
$elapsed = 0
|
|
while ((Get-UnifyingUninstallKey) -and ($elapsed -lt 240)) {
|
|
Start-Sleep -Seconds 5
|
|
$elapsed += 5
|
|
Write-Host "Waiting for the uninstall to finish... ($elapsed seconds)"
|
|
}
|
|
|
|
if (Get-UnifyingUninstallKey) {
|
|
Write-UnifyingCandidates
|
|
Write-Host "'$softwareName' is still registered after the uninstall."
|
|
Exit 1
|
|
}
|
|
|
|
Exit $exitCode
|