Add multiple Windows FMAs - #46794
Conversation
Add Winget input manifests for multiple maintained apps (Amazon DCV client/server, Dell Command Update, Dell Display Manager, Lenovo Dock Manager, Microsoft Remote Help, Nessus Agent, Plantronics Hub, Power Automate, PowerToys, and RStudio). Include corresponding install/uninstall PowerShell helper scripts under ee/maintained-apps/inputs/winget/scripts and add Windows output metadata (versions, installer URLs, checksums, installer/uninstaller refs) under ee/maintained-apps/outputs/*/windows.json. Also update ee/maintained-apps/outputs/apps.json to reflect the new entries. These additions enable automated install/uninstall and fleet management for the new Winget-backed apps.
Add React SVG icon components for multiple apps (DellCommandUpdate, DellDisplayManager, LenovoDockManager, MicrosoftRemoteHelp, NessusAgent, PlantronicsHub, PowerAutomate, Powertoys, Rstudio) and update icons index to export them. Also add 60x60@2x PNG app icon assets under website/assets/images. These resources are used by the SoftwarePage to display app icons.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #46794 +/- ##
========================================
Coverage 66.91% 66.91%
========================================
Files 2837 2848 +11
Lines 225259 225354 +95
Branches 11655 11536 -119
========================================
+ Hits 150733 150805 +72
- Misses 60830 60853 +23
Partials 13696 13696
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Remove hard-coded/versioned unique_identifiers for Dell Command Update and Dell Display Manager and add fuzzy matching. Inputs now use generic unique_identifier values and fuzzy_match_name (pattern for Command Update, boolean for Display Manager). Outputs/apps.json updated to match the new identifiers, and windows query checks replaced with LIKE patterns to more robustly detect installed variants and correctly evaluate patched status.
Add robustness to Windows install/uninstall scripts: - lenovo_dock_manager_install.ps1: add 180s timeout and registry-based install detection for Inno Setup (DockManager_is1). Wait for registration, then stop lingering dockmgr/installer processes so the installer doesn't hang; add /NOCANCEL and /SP- flags. - microsoft_remote_help_uninstall.ps1, plantronics_hub_uninstall.ps1, powertoys_uninstall.ps1: collect all matching registry entries and prefer WiX Burn bundle entries (QuietUninstallString) over child MSIs. Handle entries without uninstall strings, stop app processes before uninstall, and normalize uninstall arguments: convert MSI /i to /x and enforce /qn /norestart for MSI uninstalls; use /uninstall /quiet /norestart for bootstrapper uninstalls. - Update outputs JSON refs to point at the new/updated script implementations for Lenovo Dock Manager, Microsoft Remote Help, Plantronics Hub, and PowerToys.
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install // c24b16c0 -> 82f10c96 ===
--- /tmp/old.g1fDG8 2026-06-04 04:30:54.762784642 +0000
+++ /tmp/new.cNhtcm 2026-06-04 04:30:54.762784642 +0000
@@ -1,18 +1,56 @@
$exeFilePath = "${env:INSTALLER_PATH}"
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
+$timeoutSeconds = 180
+
+# Inno Setup writes its uninstall registry key (DockManager_is1) before running
+# its [Run] section. That post-install step launches the Dock Manager app/service,
+# which keeps the installer process alive indefinitely under the SYSTEM context.
+# So we wait for the install to register itself, then stop any lingering installer
+# or app processes instead of blocking on them.
+$regPaths = @(
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\DockManager_is1',
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\DockManager_is1'
+)
+
+function Test-Installed {
+ foreach ($p in $regPaths) { if (Test-Path $p) { return $true } }
+ return $false
+}
try {
$processOptions = @{
FilePath = "$exeFilePath"
- ArgumentList = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"
+ ArgumentList = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCANCEL /SP-"
PassThru = $true
- Wait = $true
}
$process = Start-Process @processOptions
-$exitCode = $process.ExitCode
+$elapsed = 0
+while ($elapsed -lt $timeoutSeconds) {
+ if ($process.HasExited) { break }
+ if (Test-Installed) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+}
+
+# If the installer is still running (blocked on its post-install [Run] step),
+# stop the launched app and then the installer so the script can return.
+if (-not $process.HasExited) {
+ Stop-Process -Name "dockmgr" -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 3
+ if (-not $process.HasExited) {
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ }
+}
+
+if (Test-Installed) {
+ Write-Host "Dock Manager installed successfully."
+ Exit 0
+}
+
+$exitCode = $process.ExitCode
Write-Host "Install exit code: $exitCode"
if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }
Exit $exitCode
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.jxdAL8 2026-06-04 04:30:54.809785288 +0000
+++ /tmp/new.AwXyvs 2026-06-04 04:30:54.810785302 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.vQd1GW 2026-06-04 04:30:55.060788734 +0000
+++ /tmp/new.uzKAK2 2026-06-04 04:30:55.061788748 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.3kOyqA 2026-06-04 04:30:55.316792250 +0000
+++ /tmp/new.kriUNm 2026-06-04 04:30:55.316792250 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
Enhance the PowerShell uninstall script to avoid hangs and race conditions. Adds a timeout and Test-StillInstalled helper to poll uninstall registry entries instead of blocking on the Inno uninstaller process, stops Dock Manager services/processes before running the uninstaller, and kills lingering uninstaller processes if the removal doesn't complete within the timeout. Also updates windows.json to point to the new uninstall script ref and replaces the embedded uninstall script in the refs map.
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install Script (no changes) ===
=== Uninstall // c60856a2 -> 00a89103 ===
--- /tmp/old.R2PrdN 2026-06-04 04:57:01.952121983 +0000
+++ /tmp/new.mkm8k8 2026-06-04 04:57:01.952121983 +0000
@@ -9,6 +9,17 @@
)
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
+$timeoutSeconds = 180
+
+function Test-StillInstalled {
+ foreach ($p in $paths) {
+ $m = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
+ }
+ if ($m) { return $true }
+ }
+ return $false
+}
$entry = $null
foreach ($p in $paths) {
@@ -23,7 +34,16 @@
Exit 0
}
-Stop-Process -Name "DockManager" -Force -ErrorAction SilentlyContinue
+# Stop the Dock Manager service and processes first. The Inno uninstaller
+# relaunches itself to a temp copy and the parent waits on it; that temp
+# uninstaller blocks indefinitely waiting on the running service/app under the
+# SYSTEM context, so we shut them down before uninstalling.
+Get-Service -ErrorAction SilentlyContinue | Where-Object {
+ $_.Name -like "*DockMgr*" -or $_.Name -like "*DockManager*" -or $_.DisplayName -like "*Dock Manager*"
+} | Stop-Service -Force -ErrorAction SilentlyContinue
+Stop-Process -Name "dockmgr" -Force -ErrorAction SilentlyContinue
+Stop-Process -Name "dockmgr.svc" -Force -ErrorAction SilentlyContinue
+Stop-Process -Name "dockmgr.schd" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -47,18 +67,32 @@
Write-Host "Uninstall args: $existingArgs"
try {
- $processOptions = @{
- FilePath = $exePath
- ArgumentList = $existingArgs
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
+ $process = Start-Process -FilePath $exePath -ArgumentList $existingArgs -NoNewWindow -PassThru
+
+ # The Inno uninstaller removes its registry entry when it completes. Poll for
+ # that instead of blocking on the process, which relaunches to a temp copy.
+ $elapsed = 0
+ while ($elapsed -lt $timeoutSeconds) {
+ if (-not (Test-StillInstalled)) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
}
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
- if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }
- Exit $exitCode
+
+ if (Test-StillInstalled) {
+ # Still present after the timeout: stop any lingering uninstaller processes.
+ Stop-Process -Name "unins000" -Force -ErrorAction SilentlyContinue
+ if (-not $process.HasExited) {
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ if (-not (Test-StillInstalled)) {
+ Write-Host "Dock Manager uninstalled successfully."
+ Exit 0
+ }
+
+ Write-Host "Dock Manager still present after uninstall attempt."
+ Exit 1
} catch {
Write-Host "Error running uninstaller: $_"
Exit 1ee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.hp33jA 2026-06-04 04:57:02.005122958 +0000
+++ /tmp/new.QTouBz 2026-06-04 04:57:02.005122958 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.TgnRDu 2026-06-04 04:57:02.238127243 +0000
+++ /tmp/new.I3qHXf 2026-06-04 04:57:02.239127262 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.oadNPt 2026-06-04 04:57:02.472131547 +0000
+++ /tmp/new.2tqyyR 2026-06-04 04:57:02.472131547 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
Increase uninstall timeout and harden Inno uninstaller handling: raise timeout to 300s, stop Dock Manager services/processes via Win32_Service and explicit names, extract and validate the unins000.exe path, run it with /VERYSILENT /NORESTART, poll the registry for real completion (longer interval), and kill lingering uninstaller/temp processes if needed. Update outputs/windows.json to point to the new uninstall script ref. Also refresh the frontend SVG icon and the raster app icon asset.
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install Script (no changes) ===
=== Uninstall // 00a89103 -> 322b80b3 ===
--- /tmp/old.fB6lBM 2026-06-04 05:21:01.072407669 +0000
+++ /tmp/new.VpRmQ4 2026-06-04 05:21:01.073407688 +0000
@@ -9,7 +9,10 @@
)
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$timeoutSeconds = 180
+# Give the uninstaller close to the full validation window; with the components
+# stopped it completes well under this, but it legitimately needs more than a
+# couple of minutes.
+$timeoutSeconds = 300
function Test-StillInstalled {
foreach ($p in $paths) {
@@ -29,63 +32,63 @@
if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $entry -or -not $entry.UninstallString) {
Write-Host "Uninstall entry not found"
Exit 0
}
-# Stop the Dock Manager service and processes first. The Inno uninstaller
-# relaunches itself to a temp copy and the parent waits on it; that temp
-# uninstaller blocks indefinitely waiting on the running service/app under the
-# SYSTEM context, so we shut them down before uninstalling.
-Get-Service -ErrorAction SilentlyContinue | Where-Object {
- $_.Name -like "*DockMgr*" -or $_.Name -like "*DockManager*" -or $_.DisplayName -like "*Dock Manager*"
-} | Stop-Service -Force -ErrorAction SilentlyContinue
-Stop-Process -Name "dockmgr" -Force -ErrorAction SilentlyContinue
-Stop-Process -Name "dockmgr.svc" -Force -ErrorAction SilentlyContinue
-Stop-Process -Name "dockmgr.schd" -Force -ErrorAction SilentlyContinue
-
-$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
-
-$exePath = ""
-$existingArgs = ""
-if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
- $exePath = $matches[1]; $existingArgs = $matches[2].Trim()
-} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
- $exePath = $matches[1]; $existingArgs = $matches[2].Trim()
-} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
- $exePath = $matches[1]; $existingArgs = $matches[2].Trim()
-} else {
- Throw "Could not parse uninstall string: $uninstallCommand"
+# Close the Dock Manager components first (the service plus the dockmgr,
+# dockmgr.schd and dockmgr.svc processes). The Inno uninstaller relaunches to a
+# temp copy and the parent waits on it, and that temp uninstaller blocks waiting
+# on the running service/app under the SYSTEM context. Uses only built-in
+# PowerShell cmdlets (no external toolkit required).
+Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object {
+ $_.PathName -like '*dockmgr*' -or $_.PathName -like '*Dock Manager*'
+} | ForEach-Object { Stop-Service -Name $_.Name -Force -ErrorAction SilentlyContinue }
+foreach ($n in @('dockmgr', 'dockmgr.schd', 'dockmgr.svc')) {
+ Stop-Process -Name $n -Force -ErrorAction SilentlyContinue
}
-if ($existingArgs -notmatch '(?i)/VERYSILENT') { $existingArgs = ("$existingArgs /VERYSILENT").Trim() }
-if ($existingArgs -notmatch '(?i)/SUPPRESSMSGBOXES') { $existingArgs = ("$existingArgs /SUPPRESSMSGBOXES").Trim() }
-if ($existingArgs -notmatch '(?i)/NORESTART') { $existingArgs = ("$existingArgs /NORESTART").Trim() }
+# Inno's UninstallString is the bare unins000.exe path (quoted, no args). Run it
+# with /VERYSILENT /NORESTART (not /SILENT, which renders a progress window that
+# stalls in the SYSTEM/session-0 context).
+$uninstPath = ($entry.UninstallString -replace '"', '').Trim()
+
+if (-not (Test-Path -LiteralPath $uninstPath)) {
+ Write-Host "Uninstaller not found at: $uninstPath"
+ Exit 1
+}
-Write-Host "Uninstall command: $exePath"
-Write-Host "Uninstall args: $existingArgs"
+Write-Host "Uninstall command: $uninstPath"
+Write-Host "Uninstall args: /VERYSILENT /NORESTART"
try {
- $process = Start-Process -FilePath $exePath -ArgumentList $existingArgs -NoNewWindow -PassThru
+ Start-Process -FilePath $uninstPath -ArgumentList "/VERYSILENT /NORESTART" | Out-Null
- # The Inno uninstaller removes its registry entry when it completes. Poll for
- # that instead of blocking on the process, which relaunches to a temp copy.
+ # The original unins000.exe relaunches to a temp copy and may return before the
+ # actual removal finishes, so poll the registry (the true completion signal)
+ # rather than waiting on a single process. The uninstaller removes its entry
+ # when it completes.
$elapsed = 0
while ($elapsed -lt $timeoutSeconds) {
if (-not (Test-StillInstalled)) { break }
- Start-Sleep -Seconds 3
- $elapsed += 3
+ Start-Sleep -Seconds 5
+ $elapsed += 5
}
+ Start-Sleep -Seconds 5
- if (Test-StillInstalled) {
- # Still present after the timeout: stop any lingering uninstaller processes.
- Stop-Process -Name "unins000" -Force -ErrorAction SilentlyContinue
- if (-not $process.HasExited) {
- Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
- }
+ if (-not (Test-StillInstalled)) {
+ Write-Host "Dock Manager uninstalled successfully."
+ Exit 0
}
+ # Still present after the timeout: stop any lingering uninstaller (original
+ # plus the temp _iu*.tmp copy) and re-check.
+ Stop-Process -Name "unins000" -Force -ErrorAction SilentlyContinue
+ Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "_iu*" } |
+ Stop-Process -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 5
+
if (-not (Test-StillInstalled)) {
Write-Host "Dock Manager uninstalled successfully."
Exit 0ee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.EXpgS3 2026-06-04 05:21:01.126408699 +0000
+++ /tmp/new.mobgi4 2026-06-04 05:21:01.126408699 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.4DU6ia 2026-06-04 05:21:01.374413429 +0000
+++ /tmp/new.xexG74 2026-06-04 05:21:01.374413429 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.vBPryJ 2026-06-04 05:21:01.628418273 +0000
+++ /tmp/new.nGKxVL 2026-06-04 05:21:01.628418273 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
Refactor and harden the Lenovo Dock Manager uninstall flow. Key changes: rename timeoutSeconds to waitSeconds and reduce to 220s so the script can emit diagnostics before the validator kills it; stop dockmgr-related services with logged messages; only call Stop-Process when processes exist; run the Inno uninstaller with /VERYSILENT /SUPPRESSMSGBOXES /NORESTART and write a log to %TEMP%; start the uninstaller with -PassThru and WaitForExit, then tail the Inno log for diagnostics; keep registry-based completion check and still kill lingering unins000/_iu* processes if needed. Also update windows.json to reference the new uninstall script id (12145776).
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install Script (no changes) ===
=== Uninstall // 322b80b3 -> 12145776 ===
--- /tmp/old.jni4eo 2026-06-04 12:54:27.424530821 +0000
+++ /tmp/new.qsIiMH 2026-06-04 12:54:27.425530823 +0000
@@ -9,10 +9,9 @@
)
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-# Give the uninstaller close to the full validation window; with the components
-# stopped it completes well under this, but it legitimately needs more than a
-# couple of minutes.
-$timeoutSeconds = 300
+# Bounded under the validator's ~300s cap so the script can print diagnostics
+# and clean up before it is force-killed.
+$waitSeconds = 220
function Test-StillInstalled {
foreach ($p in $paths) {
@@ -37,21 +36,21 @@
Exit 0
}
-# Close the Dock Manager components first (the service plus the dockmgr,
-# dockmgr.schd and dockmgr.svc processes). The Inno uninstaller relaunches to a
-# temp copy and the parent waits on it, and that temp uninstaller blocks waiting
-# on the running service/app under the SYSTEM context. Uses only built-in
-# PowerShell cmdlets (no external toolkit required).
-Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object {
+# Close the Dock Manager components first: the service plus the dockmgr,
+# dockmgr.schd and dockmgr.svc processes. Uses only built-in PowerShell cmdlets.
+$svcs = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object {
$_.PathName -like '*dockmgr*' -or $_.PathName -like '*Dock Manager*'
-} | ForEach-Object { Stop-Service -Name $_.Name -Force -ErrorAction SilentlyContinue }
+}
+foreach ($s in $svcs) {
+ Write-Host "Stopping service: $($s.Name) ($($s.State)) -> $($s.PathName)"
+ Stop-Service -Name $s.Name -Force -ErrorAction SilentlyContinue
+}
foreach ($n in @('dockmgr', 'dockmgr.schd', 'dockmgr.svc')) {
- Stop-Process -Name $n -Force -ErrorAction SilentlyContinue
+ $p = Get-Process -Name $n -ErrorAction SilentlyContinue
+ if ($p) { Write-Host "Stopping process: $n"; Stop-Process -Name $n -Force -ErrorAction SilentlyContinue }
}
-# Inno's UninstallString is the bare unins000.exe path (quoted, no args). Run it
-# with /VERYSILENT /NORESTART (not /SILENT, which renders a progress window that
-# stalls in the SYSTEM/session-0 context).
+# Inno's UninstallString is the bare unins000.exe path (quoted, no args).
$uninstPath = ($entry.UninstallString -replace '"', '').Trim()
if (-not (Test-Path -LiteralPath $uninstPath)) {
@@ -59,31 +58,43 @@
Exit 1
}
+$logFile = Join-Path $env:TEMP "LenovoDockManager-Uninstall.log"
+Remove-Item -LiteralPath $logFile -Force -ErrorAction SilentlyContinue
+
Write-Host "Uninstall command: $uninstPath"
-Write-Host "Uninstall args: /VERYSILENT /NORESTART"
+Write-Host "Uninstall args: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG=`"$logFile`""
try {
- Start-Process -FilePath $uninstPath -ArgumentList "/VERYSILENT /NORESTART" | Out-Null
+ $process = Start-Process -FilePath $uninstPath `
+ -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG=`"$logFile`"" `
+ -PassThru
+
+ $exited = $process.WaitForExit($waitSeconds * 1000)
+ if ($exited) {
+ Write-Host "Uninstaller process exited. ExitCode: $($process.ExitCode)"
+ } else {
+ Write-Host "Uninstaller process still running after $waitSeconds s."
+ }
- # The original unins000.exe relaunches to a temp copy and may return before the
- # actual removal finishes, so poll the registry (the true completion signal)
- # rather than waiting on a single process. The uninstaller removes its entry
- # when it completes.
- $elapsed = 0
- while ($elapsed -lt $timeoutSeconds) {
- if (-not (Test-StillInstalled)) { break }
- Start-Sleep -Seconds 5
- $elapsed += 5
+ # The original unins000.exe relaunches to a temp copy; give it a moment to
+ # finish removal, then re-check the registry (the true completion signal).
+ Start-Sleep -Seconds 10
+
+ # Surface the Inno uninstall log so we can see where it stopped if it failed.
+ if (Test-Path -LiteralPath $logFile) {
+ Write-Host "----- Inno uninstall log (tail) -----"
+ Get-Content -LiteralPath $logFile -Tail 40 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
+ Write-Host "-------------------------------------"
+ } else {
+ Write-Host "No Inno uninstall log was produced at $logFile"
}
- Start-Sleep -Seconds 5
if (-not (Test-StillInstalled)) {
Write-Host "Dock Manager uninstalled successfully."
Exit 0
}
- # Still present after the timeout: stop any lingering uninstaller (original
- # plus the temp _iu*.tmp copy) and re-check.
+ # Stop any lingering uninstaller (original plus the temp _iu*.tmp copy).
Stop-Process -Name "unins000" -Force -ErrorAction SilentlyContinue
Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "_iu*" } |
Stop-Process -Force -ErrorAction SilentlyContinueee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.nrM4oA 2026-06-04 12:54:27.478530882 +0000
+++ /tmp/new.hmtIVy 2026-06-04 12:54:27.478530882 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.J2wVUl 2026-06-04 12:54:27.727531163 +0000
+++ /tmp/new.qrVPl3 2026-06-04 12:54:27.727531163 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.HmBwkU 2026-06-04 12:54:27.977531465 +0000
+++ /tmp/new.wkQCI0 2026-06-04 12:54:27.977531465 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
Improve the uninstall flow for Lenovo Dock Manager by hardening how running components are torn down before running Inno's uninstaller. Changes: - ee/maintained-apps/inputs/winget/scripts/lenovo_dock_manager_uninstall.ps1: replace Stop-Service/Stop-Process calls with disabling the service (sc.exe config + sc.exe stop), disable matching scheduled tasks, and force-kill processes via taskkill; add a short sleep to allow termination. The script now uses built-in Windows tools to prevent the service/scheduler from restarting and locking files during uninstallation. - ee/maintained-apps/outputs/lenovo-dock-manager/windows.json: update uninstall_script_ref from "12145776" to "cddefe38" and replace the referenced script content with the improved uninstall script. Reason: the scheduler and service could auto-restart and lock files, causing the Inno uninstaller to block. Disabling the service and scheduled task and using taskkill prevents restarts and ensures reliable removal.
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install Script (no changes) ===
=== Uninstall // 12145776 -> cddefe38 ===
--- /tmp/old.1p4gEH 2026-06-04 13:19:51.057043788 +0000
+++ /tmp/new.uja06H 2026-06-04 13:19:51.058043787 +0000
@@ -36,19 +36,33 @@
Exit 0
}
-# Close the Dock Manager components first: the service plus the dockmgr,
-# dockmgr.schd and dockmgr.svc processes. Uses only built-in PowerShell cmdlets.
+# Tear down the Dock Manager components before uninstalling. A soft Stop-Service
+# is not enough: the scheduler component (dockmgr.schd) and service recovery
+# restart the service within seconds, and the running service then locks files
+# so the Inno uninstaller's second phase blocks immediately. So we DISABLE the
+# service (so it cannot restart), disable the scheduled task, and force-kill all
+# components. Uses only built-in Windows tools (sc.exe, taskkill, *-ScheduledTask).
$svcs = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue | Where-Object {
$_.PathName -like '*dockmgr*' -or $_.PathName -like '*Dock Manager*'
}
foreach ($s in $svcs) {
- Write-Host "Stopping service: $($s.Name) ($($s.State)) -> $($s.PathName)"
- Stop-Service -Name $s.Name -Force -ErrorAction SilentlyContinue
+ Write-Host "Disabling + stopping service: $($s.Name) ($($s.State)) -> $($s.PathName)"
+ & sc.exe config "$($s.Name)" start= disabled | Out-Null
+ & sc.exe stop "$($s.Name)" | Out-Null
}
-foreach ($n in @('dockmgr', 'dockmgr.schd', 'dockmgr.svc')) {
- $p = Get-Process -Name $n -ErrorAction SilentlyContinue
- if ($p) { Write-Host "Stopping process: $n"; Stop-Process -Name $n -Force -ErrorAction SilentlyContinue }
+
+Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object {
+ $_.TaskName -like '*Dock*Manager*' -or $_.TaskName -like '*dockmgr*'
+} | ForEach-Object {
+ Write-Host "Disabling scheduled task: $($_.TaskName)"
+ Disable-ScheduledTask -TaskName $_.TaskName -TaskPath $_.TaskPath -ErrorAction SilentlyContinue | Out-Null
+}
+
+foreach ($n in @('dockmgr', 'dockmgr.svc', 'dockmgr.schd')) {
+ Write-Host "Killing process: $n.exe"
+ & taskkill.exe /F /IM "$n.exe" /T 2>$null | Out-Null
}
+Start-Sleep -Seconds 3
# Inno's UninstallString is the bare unins000.exe path (quoted, no args).
$uninstPath = ($entry.UninstallString -replace '"', '').Trim()ee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.2rgpEk 2026-06-04 13:19:51.115043697 +0000
+++ /tmp/new.YhZVSr 2026-06-04 13:19:51.115043697 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.sD80d2 2026-06-04 13:19:51.354043321 +0000
+++ /tmp/new.hEktz1 2026-06-04 13:19:51.354043321 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.pQSPL1 2026-06-04 13:19:51.599042936 +0000
+++ /tmp/new.CC88E3 2026-06-04 13:19:51.599042936 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
Add a small C# Win32 helper (via Add-Type) that enumerates Inno uninstaller dialogs (#32770) belonging to unins000/_iu* processes and sends BM_CLICK to their Yes/OK buttons so the custom 'keep user data' prompt does not block uninstalls run as SYSTEM. Replace a single WaitForExit call with a polling loop that invokes the dismisser while waiting; log a warning if the helper fails to load. Update windows.json to point at the new uninstall script ref.
Script Diff Resultsee/maintained-apps/outputs/amazon-dcv-client/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/amazon-dcv-server/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-command-update/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/dell-display-manager/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/lenovo-dock-manager/windows.json=== Install Script (no changes) ===
=== Uninstall // cddefe38 -> 021699c4 ===
--- /tmp/old.nOauld 2026-06-04 13:51:00.576254009 +0000
+++ /tmp/new.38XQhg 2026-06-04 13:51:00.576254009 +0000
@@ -75,6 +75,71 @@
$logFile = Join-Path $env:TEMP "LenovoDockManager-Uninstall.log"
Remove-Item -LiteralPath $logFile -Force -ErrorAction SilentlyContinue
+# The Inno uninstaller pops a CUSTOM Yes/No dialog ("Do you want to keep local
+# user data and settings?") that /VERYSILENT /SUPPRESSMSGBOXES does NOT dismiss:
+# those flags only suppress message boxes created via Inno's SuppressibleMsgBox()
+# helper, and Lenovo coded this one with the plain (non-suppressible) MsgBox().
+# There is no command-line flag that will get past it, so we dismiss it ourselves
+# while the uninstaller runs by clicking the dialog's button via the Win32 API.
+# SendMessage(BM_CLICK) works even in session 0 with no interactive desktop
+# (where SendKeys / AppActivate / SetForegroundWindow do not), which is how
+# Fleet's orbit agent runs uninstall scripts (as SYSTEM). We only ever click
+# "Yes"/"OK" -- never "No"/"Cancel" -- so we can never abort the uninstall; for
+# the keep-data prompt, "Yes" just proceeds (the program and its registry entry
+# are still removed, which is what Test-StillInstalled verifies).
+$dismisserLoaded = $false
+try {
+ Add-Type -Language CSharp @"
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+public static class InnoDlg {
+ [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr p);
+ [DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr h, EnumWindowsProc cb, IntPtr p);
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder s, int n);
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
+ [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
+ [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, uint msg, IntPtr wp, IntPtr lp);
+ delegate bool EnumWindowsProc(IntPtr h, IntPtr p);
+ const uint BM_CLICK = 0x00F5;
+
+ static string ClassOf(IntPtr h) { var sb = new StringBuilder(256); GetClassName(h, sb, sb.Capacity); return sb.ToString(); }
+ static string TextOf(IntPtr h) { var sb = new StringBuilder(512); GetWindowText(h, sb, sb.Capacity); return sb.ToString(); }
+
+ // Finds standard dialog windows (#32770) owned by the Inno uninstaller -- the
+ // original "unins000" or its relaunched temporary "_iu*.tmp" second phase --
+ // and clicks their Yes/OK button. Returns the number of such dialogs found.
+ public static int DismissOnce() {
+ int found = 0;
+ EnumWindows((h, p) => {
+ if (ClassOf(h) != "#32770") return true;
+ uint pid; GetWindowThreadProcessId(h, out pid);
+ string pname = "";
+ try { pname = Process.GetProcessById((int)pid).ProcessName.ToLowerInvariant(); } catch {}
+ if (!(pname.StartsWith("unins") || pname.StartsWith("_iu"))) return true;
+ found++;
+ EnumChildWindows(h, (c, q) => {
+ if (ClassOf(c) != "Button") return true;
+ string t = TextOf(c).Replace("&", "").Trim();
+ if (t.Equals("Yes", StringComparison.OrdinalIgnoreCase) ||
+ t.Equals("OK", StringComparison.OrdinalIgnoreCase)) {
+ SendMessage(c, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
+ }
+ return true;
+ }, IntPtr.Zero);
+ return true;
+ }, IntPtr.Zero);
+ return found;
+ }
+}
+"@
+ $dismisserLoaded = $true
+} catch {
+ Write-Host "Warning: could not load dialog dismisser ($($_.Exception.Message)); uninstaller may block on prompts."
+}
+
Write-Host "Uninstall command: $uninstPath"
Write-Host "Uninstall args: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG=`"$logFile`""
@@ -83,7 +148,22 @@
-ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG=`"$logFile`"" `
-PassThru
- $exited = $process.WaitForExit($waitSeconds * 1000)
+ # Poll for exit while dismissing any uninstaller dialog that pops up, instead
+ # of a single blocking WaitForExit -- otherwise the custom "keep user data?"
+ # prompt stalls the uninstaller until it is force-killed.
+ $deadline = (Get-Date).AddSeconds($waitSeconds)
+ $exited = $false
+ while ((Get-Date) -lt $deadline) {
+ if ($process.HasExited) { $exited = $true; break }
+ if ($dismisserLoaded) {
+ try {
+ $n = [InnoDlg]::DismissOnce()
+ if ($n -gt 0) { Write-Host "Dismissed $n uninstaller dialog(s)" }
+ } catch { }
+ }
+ Start-Sleep -Milliseconds 750
+ }
+
if ($exited) {
Write-Host "Uninstaller process exited. ExitCode: $($process.ExitCode)"
} else {ee/maintained-apps/outputs/microsoft-remote-help/windows.json=== Install Script (no changes) ===
=== Uninstall // 5b9b3cee -> ab7e89f8 ===
--- /tmp/old.wx4JdL 2026-06-04 13:51:00.627254572 +0000
+++ /tmp/new.vNJVc1 2026-06-04 13:51:00.628254583 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "RemoteHelp" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/nessus-agent/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/plantronics-hub/windows.json=== Install Script (no changes) ===
=== Uninstall // 0a94703a -> 832a8c7d ===
--- /tmp/old.RgMZly 2026-06-04 13:51:00.859257132 +0000
+++ /tmp/new.Ij9L4h 2026-06-04 13:51:00.859257132 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PLTHub" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/power-automate/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===ee/maintained-apps/outputs/powertoys/windows.json=== Install Script (no changes) ===
=== Uninstall // 7a43d033 -> a97a3cf8 ===
--- /tmp/old.PaikT0 2026-06-04 13:51:01.090259682 +0000
+++ /tmp/new.eoIcmk 2026-06-04 13:51:01.090259682 +0000
@@ -10,19 +10,34 @@
$ExpectedExitCodes = @(0, 1641, 3010, 1223)
-$entry = $null
+# A WiX Burn bundle and its child MSI packages can all share the same DisplayName.
+# The bundle is the entry that exposes a QuietUninstallString / an .exe bootstrapper
+# UninstallString; child MSIs only expose "MsiExec.exe /I{GUID}". Prefer the bundle
+# so the whole product is removed (uninstalling a single child MSI would not).
+$candidates = @()
foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+ $candidates += Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
$_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike
}
- if ($items) { $entry = $items | Select-Object -First 1; break }
}
-if (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {
+if (-not $candidates -or $candidates.Count -eq 0) {
Write-Host "Uninstall entry not found"
Exit 0
}
+$entry = $candidates | Where-Object { $_.QuietUninstallString } | Select-Object -First 1
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString -and $_.UninstallString -notmatch '(?i)msiexec' } | Select-Object -First 1
+}
+if (-not $entry) {
+ $entry = $candidates | Where-Object { $_.UninstallString } | Select-Object -First 1
+}
+if (-not $entry) {
+ Write-Host "Uninstall entry found but has no uninstall string"
+ Exit 0
+}
+
Stop-Process -Name "PowerToys" -Force -ErrorAction SilentlyContinue
$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }
@@ -39,9 +54,19 @@
Throw "Could not parse uninstall string: $uninstallCommand"
}
-if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
-if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
-if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+if ($exePath -match '(?i)msiexec') {
+ # Force an uninstall (/x), never a repair (/i), and run with MSI quiet flags.
+ $existingArgs = $existingArgs -replace '(?i)/i(\{)', '/x$1'
+ $existingArgs = ($existingArgs -replace '(?i)/uninstall', '') -replace '(?i)/quiet', ''
+ if ($existingArgs -notmatch '(?i)/x') { $existingArgs = ("/x $existingArgs").Trim() }
+ if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+} else {
+ # WiX Burn bootstrapper.
+ if ($existingArgs -notmatch '(?i)/uninstall') { $existingArgs = ("$existingArgs /uninstall").Trim() }
+ if ($existingArgs -notmatch '(?i)/quiet') { $existingArgs = ("$existingArgs /quiet").Trim() }
+ if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() }
+}
Write-Host "Uninstall command: $exePath"
Write-Host "Uninstall args: $existingArgs"ee/maintained-apps/outputs/rstudio/windows.json=== Install Script (no changes) ===
=== Uninstall Script (no changes) === |
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Pull request overview
Adds several new Windows Fleet-maintained apps (Winget-backed) by introducing input manifests, installer/uninstaller PowerShell helpers, and generated Windows output metadata (versions, queries, URLs, checksums, and script refs). Also wires up new software icons so these apps display with custom icons on the Software page.
Changes:
- Added Winget input manifests for multiple Windows maintained apps and corresponding install/uninstall scripts.
- Added generated
windows.jsonoutput metadata for each new app and updatedee/maintained-apps/outputs/apps.json. - Added new frontend software icons and extended the software-name-to-icon mapping.
Reviewed changes
Copilot reviewed 49 out of 58 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/pages/SoftwarePage/components/icons/Rstudio.tsx | Adds RStudio icon component. |
| frontend/pages/SoftwarePage/components/icons/Powertoys.tsx | Adds PowerToys icon component. |
| frontend/pages/SoftwarePage/components/icons/PowerAutomate.tsx | Adds Power Automate icon component. |
| frontend/pages/SoftwarePage/components/icons/PlantronicsHub.tsx | Adds Plantronics Hub icon component. |
| frontend/pages/SoftwarePage/components/icons/MicrosoftRemoteHelp.tsx | Adds Microsoft Remote Help icon component. |
| frontend/pages/SoftwarePage/components/icons/LenovoDockManager.tsx | Adds Lenovo Dock Manager icon component. |
| frontend/pages/SoftwarePage/components/icons/DellDisplayManager.tsx | Adds Dell Display Manager icon component. |
| frontend/pages/SoftwarePage/components/icons/index.ts | Registers new icon components and maps software names to icons. |
| ee/maintained-apps/outputs/rstudio/windows.json | Adds generated Windows metadata + embedded scripts for RStudio. |
| ee/maintained-apps/outputs/powertoys/windows.json | Adds generated Windows metadata + embedded scripts for PowerToys. |
| ee/maintained-apps/outputs/power-automate/windows.json | Adds generated Windows metadata + embedded scripts for Power Automate. |
| ee/maintained-apps/outputs/plantronics-hub/windows.json | Adds generated Windows metadata + embedded scripts for Plantronics Hub. |
| ee/maintained-apps/outputs/nessus-agent/windows.json | Adds generated Windows metadata + MSI upgrade-code uninstall handling for Nessus Agent. |
| ee/maintained-apps/outputs/microsoft-remote-help/windows.json | Adds generated Windows metadata + embedded scripts for Remote Help. |
| ee/maintained-apps/outputs/lenovo-dock-manager/windows.json | Adds generated Windows metadata + complex Inno installer handling for Lenovo Dock Manager. |
| ee/maintained-apps/outputs/dell-display-manager/windows.json | Adds generated Windows metadata + embedded scripts for Dell Display Manager. |
| ee/maintained-apps/outputs/dell-command-update/windows.json | Adds generated Windows metadata + embedded scripts for Dell Command Update. |
| ee/maintained-apps/outputs/amazon-dcv-server/windows.json | Adds generated Windows metadata + MSI upgrade-code uninstall handling for Amazon DCV Server. |
| ee/maintained-apps/outputs/amazon-dcv-client/windows.json | Adds generated Windows metadata + MSI upgrade-code uninstall handling for Amazon DCV Client. |
| ee/maintained-apps/outputs/apps.json | Registers new maintained apps in the apps catalog. |
| ee/maintained-apps/inputs/winget/scripts/rstudio_install.ps1 | Adds RStudio installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/rstudio_uninstall.ps1 | Adds RStudio uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/powertoys_install.ps1 | Adds PowerToys installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/powertoys_uninstall.ps1 | Adds PowerToys uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/power_automate_install.ps1 | Adds Power Automate installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/power_automate_uninstall.ps1 | Adds Power Automate uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/plantronics_hub_install.ps1 | Adds Plantronics Hub installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/plantronics_hub_uninstall.ps1 | Adds Plantronics Hub uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/microsoft_remote_help_install.ps1 | Adds Remote Help installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/microsoft_remote_help_uninstall.ps1 | Adds Remote Help uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/lenovo_dock_manager_install.ps1 | Adds Lenovo Dock Manager installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/lenovo_dock_manager_uninstall.ps1 | Adds Lenovo Dock Manager uninstaller helper script (dialog dismissal + service/task teardown). |
| ee/maintained-apps/inputs/winget/scripts/dell_display_manager_install.ps1 | Adds Dell Display Manager installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/dell_display_manager_uninstall.ps1 | Adds Dell Display Manager uninstaller helper script. |
| ee/maintained-apps/inputs/winget/scripts/dell_command_update_install.ps1 | Adds Dell Command Update installer helper script. |
| ee/maintained-apps/inputs/winget/scripts/dell_command_update_uninstall.ps1 | Adds Dell Command Update uninstaller helper script. |
| ee/maintained-apps/inputs/winget/rstudio.json | Adds Winget input manifest for RStudio. |
| ee/maintained-apps/inputs/winget/powertoys.json | Adds Winget input manifest for PowerToys. |
| ee/maintained-apps/inputs/winget/power-automate.json | Adds Winget input manifest for Power Automate. |
| ee/maintained-apps/inputs/winget/plantronics-hub.json | Adds Winget input manifest for Plantronics Hub. |
| ee/maintained-apps/inputs/winget/nessus-agent.json | Adds Winget input manifest for Nessus Agent. |
| ee/maintained-apps/inputs/winget/microsoft-remote-help.json | Adds Winget input manifest for Microsoft Remote Help. |
| ee/maintained-apps/inputs/winget/lenovo-dock-manager.json | Adds Winget input manifest for Lenovo Dock Manager. |
| ee/maintained-apps/inputs/winget/dell-display-manager.json | Adds Winget input manifest for Dell Display Manager. |
| ee/maintained-apps/inputs/winget/dell-command-update.json | Adds Winget input manifest for Dell Command Update. |
| ee/maintained-apps/inputs/winget/amazon-dcv-server.json | Adds Winget input manifest for Amazon DCV Server. |
| ee/maintained-apps/inputs/winget/amazon-dcv-client.json | Adds Winget input manifest for Amazon DCV Client. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| marvel: Marvel, | ||
| max: Max, | ||
| "microsoft remote help": MicrosoftRemoteHelp, | ||
| "microsoft.companyportal": IntuneCompanyPortal, |
| crashplan: CrashPlan, | ||
| cryptomator: Cryptomator, | ||
| "dell command update": DellCommandUpdate, | ||
| "dell display manager": DellDisplayManager, |
| if ($exePath -match '(?i)msiexec') { | ||
| if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = ("$existingArgs /qn").Trim() } | ||
| if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = ("$existingArgs /norestart").Trim() } | ||
| } else { | ||
| if ($existingArgs -notmatch '(?i)(^|\s)/s(\s|$)') { $existingArgs = ("$existingArgs /s").Trim() } | ||
| } |
| "refs": { | ||
| "7a313b8a": "$displayNameLike = \"Dell Command*Update*\"\n$publisherLike = \"Dell Inc.*\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$ExpectedExitCodes = @(0, 1641, 3010, 1223)\n\n$entry = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -like $displayNameLike -and $_.Publisher -like $publisherLike\n }\n if ($items) { $entry = $items | Select-Object -First 1; break }\n}\n\nif (-not $entry -or (-not $entry.UninstallString -and -not $entry.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found\"\n Exit 0\n}\n\nStop-Process -Name \"DellCommandUpdate\" -Force -ErrorAction SilentlyContinue\nStop-Process -Name \"dcu-cli\" -Force -ErrorAction SilentlyContinue\n\n$uninstallCommand = if ($entry.QuietUninstallString) { $entry.QuietUninstallString } else { $entry.UninstallString }\n\n$exePath = \"\"\n$existingArgs = \"\"\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exePath = $matches[1]; $existingArgs = $matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exePath = $matches[1]; $existingArgs = $matches[2].Trim()\n} elseif ($uninstallCommand -match '^\\s*(\\S+)\\s*(.*)$') {\n $exePath = $matches[1]; $existingArgs = $matches[2].Trim()\n} else {\n Throw \"Could not parse uninstall string: $uninstallCommand\"\n}\n\nif ($exePath -match '(?i)msiexec') {\n if ($existingArgs -notmatch '(?i)/qn') { $existingArgs = (\"$existingArgs /qn\").Trim() }\n if ($existingArgs -notmatch '(?i)/norestart') { $existingArgs = (\"$existingArgs /norestart\").Trim() }\n} else {\n if ($existingArgs -notmatch '(?i)(^|\\s)/s(\\s|$)') { $existingArgs = (\"$existingArgs /s\").Trim() }\n}\n\nWrite-Host \"Uninstall command: $exePath\"\nWrite-Host \"Uninstall args: $existingArgs\"\n\ntry {\n $processOptions = @{\n FilePath = $exePath\n ArgumentList = $existingArgs\n NoNewWindow = $true\n PassThru = $true\n Wait = $true\n }\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n", | ||
| "9f0e4526": "$exeFilePath = \"${env:INSTALLER_PATH}\"\n$ExpectedExitCodes = @(0, 2, 6, 1641, 3010, 1223)\n\ntry {\n\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/s\"\n PassThru = $true\n Wait = $true\n}\n\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\nWrite-Host \"Install exit code: $exitCode\"\nif ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (9)
📒 Files selected for processing (49)
WalkthroughThis PR adds 10 new Windows applications to Fleet's managed application system across multiple architectural layers. Each application receives an input manifest (with installer metadata and script paths), paired installation and uninstallation PowerShell scripts (handling silent execution, process management, registry operations, and exit code validation), output definitions (with SQL detection queries and embedded script logic), registry entries (in the main apps catalog), and frontend UI icon components. Applications span utilities (Dell Command Update, PowerToys, Nessus Agent), communication (Microsoft Remote Help, Plantronics Hub), remote access (Amazon DCV client/server, Lenovo Dock Manager), and data science (RStudio, Power Automate). The scripts implement platform-specific behaviors: MSI-based uninstallers receive Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add Winget input manifests for multiple maintained apps (Amazon DCV client/server, Dell Command Update, Dell Display Manager, Lenovo Dock Manager, Microsoft Remote Help, Nessus Agent, Plantronics Hub, Power Automate, PowerToys, and RStudio). Include corresponding install/uninstall PowerShell helper scripts under ee/maintained-apps/inputs/winget/scripts and add Windows output metadata (versions, installer URLs, checksums, installer/uninstaller refs) under ee/maintained-apps/outputs/*/windows.json. Also update ee/maintained-apps/outputs/apps.json to reflect the new entries. These additions enable automated install/uninstall and fleet management for the new Winget-backed apps.
Summary by CodeRabbit