Skip to content

Add multiple Windows FMAs - #46794

Merged
allenhouchins merged 9 commits into
mainfrom
allenhouchins-windows-fmas
Jun 4, 2026
Merged

Add multiple Windows FMAs#46794
allenhouchins merged 9 commits into
mainfrom
allenhouchins-windows-fmas

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Jun 4, 2026

Copy link
Copy Markdown
Member

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

  • New Features
    • Added support for 11 new Windows applications: Amazon DCV Client, Amazon DCV Server, Dell Command Update, Dell Display Manager, Lenovo Dock Manager, Microsoft Remote Help, Nessus Agent, Plantronics Hub, Power Automate, PowerToys, and RStudio.

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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.91%. Comparing base (35a0107) to head (0feef03).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
...oftwarePage/components/icons/DellCommandUpdate.tsx 50.00% 1 Missing ⚠️
...ftwarePage/components/icons/DellDisplayManager.tsx 50.00% 1 Missing ⚠️
...oftwarePage/components/icons/LenovoDockManager.tsx 50.00% 1 Missing ⚠️
...twarePage/components/icons/MicrosoftRemoteHelp.tsx 50.00% 1 Missing ⚠️
...ages/SoftwarePage/components/icons/NessusAgent.tsx 50.00% 1 Missing ⚠️
...s/SoftwarePage/components/icons/PlantronicsHub.tsx 50.00% 1 Missing ⚠️
...es/SoftwarePage/components/icons/PowerAutomate.tsx 50.00% 1 Missing ⚠️
.../pages/SoftwarePage/components/icons/Powertoys.tsx 50.00% 1 Missing ⚠️
...nd/pages/SoftwarePage/components/icons/Rstudio.tsx 50.00% 1 Missing ⚠️
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            
Flag Coverage Δ
frontend 56.75% <50.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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 1

ee/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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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 0

ee/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).
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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 SilentlyContinue

ee/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.
fleet-release
fleet-release previously approved these changes Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/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) ===

@allenhouchins
allenhouchins marked this pull request as ready for review June 4, 2026 14:39
@allenhouchins
allenhouchins requested a review from a team as a code owner June 4, 2026 14:39
Copilot AI review requested due to automatic review settings June 4, 2026 14:39
@fleet-release
fleet-release requested a review from eashaw June 4, 2026 14:39

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@allenhouchins
allenhouchins merged commit 591a86f into main Jun 4, 2026
28 of 29 checks passed
@allenhouchins
allenhouchins deleted the allenhouchins-windows-fmas branch June 4, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json output metadata for each new app and updated ee/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.

Comment on lines 384 to 387
marvel: Marvel,
max: Max,
"microsoft remote help": MicrosoftRemoteHelp,
"microsoft.companyportal": IntuneCompanyPortal,
Comment on lines 367 to +370
crashplan: CrashPlan,
cryptomator: Cryptomator,
"dell command update": DellCommandUpdate,
"dell display manager": DellDisplayManager,
Comment on lines +43 to +48
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() }
}
Comment on lines +18 to +20
"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"
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4829f666-7c9d-43b2-8eb5-79568bd5dfdf

📥 Commits

Reviewing files that changed from the base of the PR and between 35a0107 and 0feef03.

⛔ Files ignored due to path filters (9)
  • website/assets/images/app-icon-dell-command-update-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-dell-display-manager-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-lenovo-dock-manager-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-microsoft-remote-help-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-nessus-agent-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-plantronics-hub-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-power-automate-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-powertoys-60x60@2x.png is excluded by !**/*.png
  • website/assets/images/app-icon-rstudio-60x60@2x.png is excluded by !**/*.png
📒 Files selected for processing (49)
  • ee/maintained-apps/inputs/winget/amazon-dcv-client.json
  • ee/maintained-apps/inputs/winget/amazon-dcv-server.json
  • ee/maintained-apps/inputs/winget/dell-command-update.json
  • ee/maintained-apps/inputs/winget/dell-display-manager.json
  • ee/maintained-apps/inputs/winget/lenovo-dock-manager.json
  • ee/maintained-apps/inputs/winget/microsoft-remote-help.json
  • ee/maintained-apps/inputs/winget/nessus-agent.json
  • ee/maintained-apps/inputs/winget/plantronics-hub.json
  • ee/maintained-apps/inputs/winget/power-automate.json
  • ee/maintained-apps/inputs/winget/powertoys.json
  • ee/maintained-apps/inputs/winget/rstudio.json
  • ee/maintained-apps/inputs/winget/scripts/dell_command_update_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/dell_command_update_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/dell_display_manager_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/dell_display_manager_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/lenovo_dock_manager_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/lenovo_dock_manager_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/microsoft_remote_help_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/microsoft_remote_help_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/plantronics_hub_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/plantronics_hub_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/power_automate_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/power_automate_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/powertoys_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/powertoys_uninstall.ps1
  • ee/maintained-apps/inputs/winget/scripts/rstudio_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/rstudio_uninstall.ps1
  • ee/maintained-apps/outputs/amazon-dcv-client/windows.json
  • ee/maintained-apps/outputs/amazon-dcv-server/windows.json
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/dell-command-update/windows.json
  • ee/maintained-apps/outputs/dell-display-manager/windows.json
  • ee/maintained-apps/outputs/lenovo-dock-manager/windows.json
  • ee/maintained-apps/outputs/microsoft-remote-help/windows.json
  • ee/maintained-apps/outputs/nessus-agent/windows.json
  • ee/maintained-apps/outputs/plantronics-hub/windows.json
  • ee/maintained-apps/outputs/power-automate/windows.json
  • ee/maintained-apps/outputs/powertoys/windows.json
  • ee/maintained-apps/outputs/rstudio/windows.json
  • frontend/pages/SoftwarePage/components/icons/DellCommandUpdate.tsx
  • frontend/pages/SoftwarePage/components/icons/DellDisplayManager.tsx
  • frontend/pages/SoftwarePage/components/icons/LenovoDockManager.tsx
  • frontend/pages/SoftwarePage/components/icons/MicrosoftRemoteHelp.tsx
  • frontend/pages/SoftwarePage/components/icons/NessusAgent.tsx
  • frontend/pages/SoftwarePage/components/icons/PlantronicsHub.tsx
  • frontend/pages/SoftwarePage/components/icons/PowerAutomate.tsx
  • frontend/pages/SoftwarePage/components/icons/Powertoys.tsx
  • frontend/pages/SoftwarePage/components/icons/Rstudio.tsx
  • frontend/pages/SoftwarePage/components/icons/index.ts

Walkthrough

This 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 /qn /norestart flags, WiX Burn bootstrappers receive /uninstall /quiet /norestart, and Inno Setup uninstallers use registry polling and embedded C# dialogs to suppress custom prompts.

Possibly related PRs

  • fleetdm/fleet#45883: Extends the frontend icon registry by adding a new icon component and updating SOFTWARE_NAME_TO_ICON_MAP.
  • fleetdm/fleet#46409: Adds Prisma icon component and updates SOFTWARE_NAME_TO_ICON_MAP in the same icons module.
  • fleetdm/fleet#46304: Updates SOFTWARE_NAME_TO_ICON_MAP by adding an icon for Amazon WorkSpaces.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch allenhouchins-windows-fmas

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants