Windows FMA - Power BI - #46284
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #46284 +/- ##
==========================================
- Coverage 66.89% 66.89% -0.01%
==========================================
Files 2783 2784 +1
Lines 221728 221730 +2
Branches 11258 11258
==========================================
+ Hits 148330 148331 +1
- Misses 59997 59998 +1
Partials 13401 13401
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…leet into adding-powerbi-win-fma
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.SnxPJd 2026-05-28 00:30:56.804590883 +0000
+++ /tmp/new.s7kfKT 2026-05-28 00:30:56.804590883 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.EUtWd0 2026-05-28 00:44:22.660631314 +0000
+++ /tmp/new.yOhC2e 2026-05-28 00:44:22.660631314 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.JfBHu2 2026-05-28 00:45:04.046379499 +0000
+++ /tmp/new.QkwGqK 2026-05-28 00:45:04.047379516 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR adds Power BI Desktop (x64) as a Fleet-managed Windows application. It introduces an installer script that runs the installer silently with EULA acceptance, a three-phase uninstaller handling WiX Burn bundles and leftover MSIs plus registry cleanup, a distribution manifest with installer URL and SHA-256, Winget input metadata, app catalog registration, and a frontend React SVG icon component with a "power bi" name-to-icon mapping. Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.xStjSL 2026-05-28 02:11:34.467447886 +0000
+++ /tmp/new.nBe1Sv 2026-05-28 02:11:34.467447886 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.Goa12X 2026-05-28 02:12:11.385000979 +0000
+++ /tmp/new.oQ7Ksz 2026-05-28 02:12:11.385000979 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.JqgIms 2026-05-28 02:13:40.720414149 +0000
+++ /tmp/new.qvlGMw 2026-05-28 02:13:40.720414149 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
|
Actionable comments posted: 0 |
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Script Diff Resultsee/maintained-apps/outputs/power-bi/windows.json=== Install Script (no changes) ===
=== Uninstall // ea2d4556 -> fa3b8ea6 ===
--- /tmp/old.0FTnb5 2026-05-28 18:05:33.805751466 +0000
+++ /tmp/new.QRNRjc 2026-05-28 18:05:33.805751466 +0000
@@ -1,109 +1,124 @@
-# Define acceptable/expected exit codes (0 = success, 3010/1641 = success, reboot required)
-$ExpectedExitCodes = @(0, 3010, 1641)
+# Power BI Desktop's EXE installer is a WiX "Burn" bundle. It registers TWO
+# uninstall entries:
+# * "Microsoft PowerBI Desktop (x64)" -> the bundle bootstrapper
+# (...\Package Cache\{afa18d15-...}\PBIDesktopSetup_x64.exe)
+# * "Microsoft Power BI Desktop (x64)" -> the MSI the bundle installed
+# (MsiExec.exe /X{c7d2053f-...})
+#
+# Removing the MSI directly orphans the bundle: its uninstall then no-ops
+# (returns 0) and leaves the "Microsoft PowerBI Desktop (x64)" registration
+# behind, which is what Fleet's osquery-based validator keeps detecting.
+# Correct approach: uninstall via the BUNDLE first; it removes the MSI and its
+# own registration. Burn relaunches a cached copy of itself + spawns msiexec
+# asynchronously, so wait for those to finish.
-# Power BI Desktop registers in Programs and Features. Match on DisplayName since the
-# MSI product code (GUID) changes between versions.
-$softwareNameLike = "Microsoft Power BI Desktop*"
-$publisher = "Microsoft Corporation"
-
-# Silent flag used only if the uninstaller turns out to be a plain EXE (not MsiExec)
-# and does not provide its own QuietUninstallString.
-$exeSilentArgs = "-q -norestart"
-
-$paths = @(
- 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
- 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+$ExpectedExitCodes = @(0, 1605, 1641, 3010)
+$exitCode = 0
+
+$installDirs = @(
+ (Join-Path $env:ProgramFiles 'Microsoft Power BI Desktop'),
+ (Join-Path ${env:ProgramFiles(x86)} 'Microsoft Power BI Desktop')
)
-# Initialize exit code
-$exitCode = 0
+function Wait-ForProcessExit {
+ param([string[]]$Names, [int]$TimeoutSeconds = 240)
+ $elapsed = 0
+ while ($elapsed -lt $TimeoutSeconds) {
+ $running = $Names | Where-Object { Get-Process -Name $_ -ErrorAction SilentlyContinue }
+ if (-not $running) { break }
+ Start-Sleep -Seconds 3
+ $elapsed += 3
+ }
+}
-try {
- # Locate the uninstall entry
- $selected = $null
- foreach ($p in $paths) {
- $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
- $_.DisplayName -and ($_.DisplayName -like $softwareNameLike) -and
- ($publisher -eq "" -or $_.Publisher -eq $publisher)
+function Get-PowerBIEntries {
+ param([string[]]$Roots)
+ $list = @()
+ foreach ($root in $Roots) {
+ foreach ($sub in (Get-ChildItem -Path $root -ErrorAction SilentlyContinue)) {
+ $key = Get-ItemProperty $sub.PSPath -ErrorAction SilentlyContinue
+ if (-not $key.DisplayName) { continue }
+ if (-not (($key.DisplayName -replace '\s', '').ToLower().Contains("powerbidesktop"))) { continue }
+ $list += [PSCustomObject]@{
+ DisplayName = $key.DisplayName
+ KeyPath = $sub.PSPath
+ KeyName = $sub.PSChildName
+ Command = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
+ }
}
- if ($items) { $selected = $items | Select-Object -First 1; break }
+ }
+ return $list
+}
+
+function Get-ExePath {
+ param([string]$Command)
+ if ($Command -match '"([^"]+\.exe)"') { return $Matches[1] }
+ if ($Command -match '(?i)([A-Z]:\\[^"]+?\.exe)') { return $Matches[1] }
+ return $null
+}
+
+try {
+ $roots = [System.Collections.Generic.List[string]]::new()
+ $roots.Add('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
+ $roots.Add('HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
+ foreach ($hive in (Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction SilentlyContinue)) {
+ if ($hive.Name -match '_Classes$') { continue }
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")
+ $roots.Add("Registry::$($hive.Name)\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall")
}
- if (-not $selected -or -not $selected.UninstallString) {
- Write-Host "Uninstall entry not found for $softwareNameLike"
+ $entries = Get-PowerBIEntries -Roots $roots
+ if ($entries.Count -eq 0) {
+ Write-Host "No Power BI Desktop entries found (already removed)."
Exit 0
}
- # Best-effort: stop running Power BI processes so the uninstaller doesn't fail on locked files
foreach ($proc in @("PBIDesktop", "msmdsrv", "Microsoft.Mashup.Container")) {
Stop-Process -Name $proc -Force -ErrorAction SilentlyContinue
}
- # Prefer QuietUninstallString (already includes silent switches) when present.
- $uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
- } else {
- $selected.UninstallString
- }
-
- $uninstallArgs = ""
-
- if ($uninstallCommand -match "MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") {
- # MSI-backed uninstall (the common case for the Power BI EXE installer)
- $productCode = $Matches[1]
- $uninstallArgs = "/X $productCode /qn /norestart"
- $uninstallCommand = "MsiExec.exe"
- } else {
- # Plain EXE uninstaller. Split the quoted command from its args.
- $splitArgs = $uninstallCommand.Split('"')
- if ($splitArgs.Length -gt 1) {
- if ($splitArgs.Length -eq 3) {
- $uninstallArgs = $splitArgs[2].Trim()
- } elseif ($splitArgs.Length -gt 3) {
- Throw "Uninstall command contains multiple quoted strings. Please update the uninstall script.`nUninstall command: $uninstallCommand"
- }
- $uninstallCommand = $splitArgs[1]
- }
- # If the registry didn't supply silent switches, add ours.
- if (-not $selected.QuietUninstallString) {
- $uninstallArgs = "$uninstallArgs $exeSilentArgs".Trim()
+ # Phase 1: uninstall via the Burn bundle bootstrapper(s) FIRST.
+ foreach ($e in ($entries | Where-Object { $_.Command -match "(?i)PBIDesktopSetup.*\.exe" })) {
+ $exe = Get-ExePath $e.Command
+ if (-not $exe) { Write-Host "Could not parse bundle exe from: $($e.Command)"; continue }
+ if (-not (Test-Path -LiteralPath $exe)) { Write-Host "Bundle exe missing: $exe"; continue }
+
+ Write-Host "Uninstalling bundle: '$($e.DisplayName)'"
+ $p = Start-Process -FilePath $exe -ArgumentList "/uninstall /quiet /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ Wait-ForProcessExit -Names @("PBIDesktopSetup_x64", "PBIDesktopSetup", "msiexec") -TimeoutSeconds 240
+ }
+
+ # Phase 2: remove any MSI entries the bundle didn't clean up.
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ $msiCode = $null
+ if ($e.Command -match "(?i)MsiExec\.exe\s+/[IX]\s*(\{[A-F0-9-]+\})") { $msiCode = $Matches[1] }
+ elseif ($e.KeyName -match "(?i)^\{[A-F0-9-]+\}$") { $msiCode = $e.KeyName }
+ if (-not $msiCode) { continue }
+
+ Write-Host "Removing leftover MSI: '$($e.DisplayName)' ($msiCode)"
+ $p = Start-Process -FilePath "MsiExec.exe" -ArgumentList "/X $msiCode /qn /norestart" -PassThru -Wait
+ Write-Host " Exit code: $($p.ExitCode)"
+ Wait-ForProcessExit -Names @("msiexec") -TimeoutSeconds 180
+ if (($ExpectedExitCodes -notcontains $p.ExitCode) -and ($exitCode -eq 0)) { $exitCode = $p.ExitCode }
+ }
+
+ # Phase 3: safety net for stale registration when product files are gone.
+ $productGone = -not ($installDirs | Where-Object { $_ -and (Test-Path -LiteralPath $_) })
+ foreach ($e in (Get-PowerBIEntries -Roots $roots)) {
+ if ($productGone) {
+ Write-Host "Removing orphaned registration: '$($e.DisplayName)' ($($e.KeyPath))"
+ Remove-Item -Path $e.KeyPath -Recurse -Force -ErrorAction SilentlyContinue
+ } else {
+ Write-Host "WARNING: entry still present and product files remain: '$($e.DisplayName)'"
+ if ($exitCode -eq 0) { $exitCode = 1 }
}
}
- Write-Host "Uninstall command: $uninstallCommand"
- Write-Host "Uninstall args: $uninstallArgs"
-
- $processOptions = @{
- FilePath = $uninstallCommand
- NoNewWindow = $true
- PassThru = $true
- Wait = $true
- }
- if ($uninstallArgs -ne '') {
- $processOptions.ArgumentList = "$uninstallArgs"
- }
-
- # Start uninstall process
- $process = Start-Process @processOptions
- $exitCode = $process.ExitCode
- Write-Host "Uninstall exit code: $exitCode"
-
- # msiexec can return before the uninstall is fully complete; wait it out
- $timeout = 120
- $elapsed = 0
- while ((Get-Process -Name "msiexec" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
- Start-Sleep -Seconds 2
- $elapsed += 2
- }
-
} catch {
Write-Host "Error: $_"
$exitCode = 1
}
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
- Exit 0
-} else {
- Exit $exitCode
-}
+if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } else { Exit $exitCode } |
|
Actionable comments posted: 0 |
Related issue: Resolves #
Checklist for submitter
If some of the following don't apply, delete the relevant line.
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.Timeouts are implemented and retries are limited to avoid infinite loops
If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes
Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually
For unreleased bug fixes in a release candidate, one of:
Database migrations
COLLATE utf8mb4_unicode_ci).New Fleet configuration settings
If you didn't check the box above, follow this checklist for GitOps-enabled settings:
fleetctl generate-gitopsfleetd/orbit/Fleet Desktop
runtime.GOOSis used as needed to isolate changesSummary by CodeRabbit