Add Windows FMA for Citrix Workspace App (LTSR) - #50903
Conversation
Citrix publishes LTSR as its own winget package (Citrix.Workspace.LTSR), separate from the Current Release track. Kept as its own catalog entry/slug rather than grouping with the existing macOS Homebrew Citrix Workspace entry, since that one tracks Current Release, not LTSR.
There was a problem hiding this comment.
Pull request overview
Adds a new Windows Fleet-maintained app entry for Citrix Workspace App (LTSR) sourced from the winget package Citrix.Workspace.LTSR, including install/uninstall automation and UI icon mapping.
Changes:
- Added a new winget input definition for
citrix-workspace-ltsr/windowswith custom PowerShell install/uninstall scripts. - Added the generated Windows output manifest for Citrix Workspace LTSR and registered the app in the maintained-apps catalog.
- Mapped the new software icon key (
citrix workspace ltsr) to reuse the existing Citrix Workspace icon.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/pages/SoftwarePage/components/icons/index.ts | Adds an icon-map key for “citrix workspace ltsr”. |
| ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json | New generated Windows maintained-app output (queries + installer URL + script refs). |
| ee/maintained-apps/outputs/apps.json | Registers the new app entry in the maintained-apps catalog list. |
| ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1 | Adds custom uninstall logic that locates the uninstall entry and re-runs the uninstaller silently. |
| ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_install.ps1 | Adds custom silent install logic for the Citrix bootstrapper exe. |
| ee/maintained-apps/inputs/winget/citrix-workspace-ltsr.json | Adds the new winget input definition for Citrix Workspace LTSR. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #50903 +/- ##
==========================================
+ Coverage 68.53% 68.65% +0.12%
==========================================
Files 3980 3994 +14
Lines 256356 257543 +1187
Branches 13661 13805 +144
==========================================
+ Hits 175691 176814 +1123
+ Misses 65025 65006 -19
- Partials 15640 15723 +83
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Citrix Workspace LTSR as a Windows Fleet-maintained app. The change includes Winget metadata, generated application metadata, PowerShell installation and uninstallation scripts, catalog registration, validation handling, and a Software page icon alias. The app targets version Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1`:
- Around line 35-38: Update the missing-entry branch in the uninstall script to
treat an absent matching uninstall entry as successful: retain the informational
Write-Host message, but replace the failure exit status in the -not $selected
branch with a success status.
In `@ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json`:
- Around line 6-8: Update
ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json lines 6-8 to use
an LTSR-specific version or registry discriminator for detection and removal
queries. Update
ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1
lines 11-33 to select the same LTSR-specific registry entry, ensuring non-LTSR
Citrix Workspace installations are neither detected nor uninstalled.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: db332183-02a5-4b21-a4bd-84739f344737
📒 Files selected for processing (6)
ee/maintained-apps/inputs/winget/citrix-workspace-ltsr.jsonee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_install.ps1ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1ee/maintained-apps/outputs/apps.jsonee/maintained-apps/outputs/citrix-workspace-ltsr/windows.jsonfrontend/pages/SoftwarePage/components/icons/index.ts
…e LTSR Start-Process -Wait blocked for the full 10-minute validator timeout because the Citrix bootstrapper leaves a resident tray/self-service process running as a child, so -Wait never returns even though the install itself completes. Switch to polling Programs and Features for the app's own registry entry instead of trusting the installer process's exit code. Also scope both the install poll and the uninstall registry lookup to publisher "Citrix Systems, Inc." (matching the generated exists query) so a DisplayName-prefix match alone can't pick the wrong entry, and treat a missing uninstall entry as already-uninstalled (exit 0) instead of failing.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install // 36e68d54 -> 9542c22e ===
--- /tmp/old.lKEiqN 2026-08-10 20:57:13.831644500 +0000
+++ /tmp/new.jcKzlo 2026-08-10 20:57:13.831644500 +0000
@@ -5,22 +5,55 @@
# Custom): /silent runs without dialogs/prompts, /noreboot suppresses the
# reboot prompt, and /AutoUpdateCheck=disabled keeps Citrix's own updater
# from taking over version management.
+#
+# The Citrix bootstrapper leaves resident processes running after install
+# (e.g. its self-service/notification tray app), so Start-Process -Wait
+# never returns -- it waits on the whole process tree, not just the
+# installer itself. Start without -Wait and poll Programs and Features for
+# the app's own registry entry instead of trusting the installer process's
+# exit code.
+
+$softwareNameLike = "Citrix Workspace *"
+$paths = @(
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+)
+$timeoutSeconds = 480
+$pollIntervalSeconds = 10
+
+function Test-CitrixWorkspaceInstalled {
+ [array]$uninstallKeys = Get-ChildItem `
+ -Path $paths `
+ -ErrorAction SilentlyContinue |
+ ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+
+ foreach ($key in $uninstallKeys) {
+ if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
+ -and $key.Publisher -eq "Citrix Systems, Inc.") {
+ return $true
+ }
+ }
+ return $false
+}
try {
-$installProcess = Start-Process -FilePath "${env:INSTALLER_PATH}" `
+Start-Process -FilePath "${env:INSTALLER_PATH}" `
-ArgumentList "/silent /noreboot /AutoUpdateCheck=disabled" `
- -PassThru -Wait
-
-$exitCode = $installProcess.ExitCode
-Write-Host "Install exit code: $exitCode"
+ -PassThru | Out-Null
-# Treat msiexec-style reboot-required codes as success too.
-if ($exitCode -eq 0 -or $exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
+$elapsed = 0
+while ($elapsed -lt $timeoutSeconds) {
+ if (Test-CitrixWorkspaceInstalled) {
+ Write-Host "Citrix Workspace registered in Programs and Features after ${elapsed}s"
+ Exit 0
+ }
+ Start-Sleep -Seconds $pollIntervalSeconds
+ $elapsed += $pollIntervalSeconds
}
-Exit $exitCode
+Write-Host "Timed out after ${timeoutSeconds}s waiting for Citrix Workspace to register"
+Exit 1
} catch {
Write-Host "Error: $_"
=== Uninstall // 9d1d208e -> f5031b25 ===
--- /tmp/old.QIsgqs 2026-08-10 20:57:13.855644349 +0000
+++ /tmp/new.3JYgk7 2026-08-10 20:57:13.855644349 +0000
@@ -26,15 +26,17 @@
$selected = $null
foreach ($key in $uninstallKeys) {
- if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike) {
+ if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
+ -and $key.Publisher -eq "Citrix Systems, Inc.") {
$selected = $key
break
}
}
if (-not $selected) {
+ # Already uninstalled (or never installed) -- nothing to do.
Write-Host "Uninstall entry not found for $softwareNameLike"
- Exit 1
+ Exit 0
}
$uninstallCommand = if ($selected.QuietUninstallString) { |
CI validation showed the Citrix bootstrapper's Programs and Features entry is its bundled "ReceiverInside" component (DisplayName "Citrix Workspace Inside"), which carries its own internal build version (e.g. 25.7.2000.9) distinct from the outer installer package version (25.7.2000.2020) pulled from winget. There's no documented mapping between the two, so fall back to an existence-only check once a genuine, publisher-scoped Citrix Workspace entry is found -- same pattern already used for Google Chrome and Microsoft Office's Click-to-Run versioning.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install // 36e68d54 -> 9542c22e ===
--- /tmp/old.AFjV3z 2026-08-10 21:16:47.410486949 +0000
+++ /tmp/new.6eC7GL 2026-08-10 21:16:47.411486943 +0000
@@ -5,22 +5,55 @@
# Custom): /silent runs without dialogs/prompts, /noreboot suppresses the
# reboot prompt, and /AutoUpdateCheck=disabled keeps Citrix's own updater
# from taking over version management.
+#
+# The Citrix bootstrapper leaves resident processes running after install
+# (e.g. its self-service/notification tray app), so Start-Process -Wait
+# never returns -- it waits on the whole process tree, not just the
+# installer itself. Start without -Wait and poll Programs and Features for
+# the app's own registry entry instead of trusting the installer process's
+# exit code.
+
+$softwareNameLike = "Citrix Workspace *"
+$paths = @(
+ 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+)
+$timeoutSeconds = 480
+$pollIntervalSeconds = 10
+
+function Test-CitrixWorkspaceInstalled {
+ [array]$uninstallKeys = Get-ChildItem `
+ -Path $paths `
+ -ErrorAction SilentlyContinue |
+ ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+
+ foreach ($key in $uninstallKeys) {
+ if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
+ -and $key.Publisher -eq "Citrix Systems, Inc.") {
+ return $true
+ }
+ }
+ return $false
+}
try {
-$installProcess = Start-Process -FilePath "${env:INSTALLER_PATH}" `
+Start-Process -FilePath "${env:INSTALLER_PATH}" `
-ArgumentList "/silent /noreboot /AutoUpdateCheck=disabled" `
- -PassThru -Wait
-
-$exitCode = $installProcess.ExitCode
-Write-Host "Install exit code: $exitCode"
+ -PassThru | Out-Null
-# Treat msiexec-style reboot-required codes as success too.
-if ($exitCode -eq 0 -or $exitCode -eq 3010 -or $exitCode -eq 1641) {
- Exit 0
+$elapsed = 0
+while ($elapsed -lt $timeoutSeconds) {
+ if (Test-CitrixWorkspaceInstalled) {
+ Write-Host "Citrix Workspace registered in Programs and Features after ${elapsed}s"
+ Exit 0
+ }
+ Start-Sleep -Seconds $pollIntervalSeconds
+ $elapsed += $pollIntervalSeconds
}
-Exit $exitCode
+Write-Host "Timed out after ${timeoutSeconds}s waiting for Citrix Workspace to register"
+Exit 1
} catch {
Write-Host "Error: $_"
=== Uninstall // 9d1d208e -> f5031b25 ===
--- /tmp/old.4JruGp 2026-08-10 21:16:47.427486846 +0000
+++ /tmp/new.EPVLFM 2026-08-10 21:16:47.427486846 +0000
@@ -26,15 +26,17 @@
$selected = $null
foreach ($key in $uninstallKeys) {
- if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike) {
+ if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
+ -and $key.Publisher -eq "Citrix Systems, Inc.") {
$selected = $key
break
}
}
if (-not $selected) {
+ # Already uninstalled (or never installed) -- nothing to do.
Write-Host "Uninstall entry not found for $softwareNameLike"
- Exit 1
+ Exit 0
}
$uninstallCommand = if ($selected.QuietUninstallString) { |
…eyExpress
CI showed the real UninstallString for "Citrix Workspace Inside" starts with
MsiExec.exe (the maintenance/repair form), not the TrolleyExpress.exe or
CWAInstaller.exe bootstrapper uninstaller community docs describe for this
DisplayName. Our previous parser discarded everything after "MsiExec.exe"
and then ran it with Citrix's own /uninstall /cleanup /silent switches,
which msiexec doesn't understand -- it sat for the full 10-minute timeout.
Detect the msiexec case, resolve the ProductCode from the registry key name
(or the UninstallString's GUID as a fallback), and run a clean
"msiexec /x {ProductCode} /qn /norestart", matching the existing
foxit-pdf-editor/4k-video-downloader-plus precedent in this repo. Keep the
exe-based fallback for the non-MSI shape in case a future build reverts to
the bootstrapper-owned uninstaller.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // f5031b25 -> fa9409d1 ===
--- /tmp/old.7qicLt 2026-08-10 21:46:54.410060906 +0000
+++ /tmp/new.ClPD2X 2026-08-10 21:46:54.410060906 +0000
@@ -1,12 +1,16 @@
# Uninstall for Citrix Workspace App LTSR.
#
-# The installer self-registers under Programs and Features as "Citrix
-# Workspace <version>" (e.g. "Citrix Workspace 2507"), with an UninstallString
-# that points at the vendor's own uninstaller (TrolleyExpress.exe on older
-# builds, CWAInstaller.exe on 2311.1+). We look up that entry, extract just
-# the executable path, and re-run it with the vendor-documented silent
-# uninstall switches rather than trusting whatever args are already in the
-# registry string.
+# The Programs and Features entry that's actually visible is Citrix's
+# bundled "ReceiverInside" component (DisplayName "Citrix Workspace Inside"),
+# whose UninstallString is a standard MSI reference (MsiExec.exe
+# /I{ProductCode}, the maintenance/repair form) -- not the TrolleyExpress.exe
+# or CWAInstaller.exe bootstrapper uninstaller some older community docs
+# describe for this DisplayName pattern. Resolve the ProductCode (registry
+# key name, or the first GUID in the UninstallString) and run a clean
+# "msiexec /x {ProductCode} /qn /norestart" -- never reuse the /I switch
+# from the registry string. If the selected entry isn't MSI-based, fall back
+# to re-running its own uninstaller exe with Citrix's documented silent
+# switches.
$softwareNameLike = "Citrix Workspace *"
@@ -39,6 +43,8 @@
Exit 0
}
+Write-Host "Selected entry DisplayName: $($selected.DisplayName)"
+
$uninstallCommand = if ($selected.QuietUninstallString) {
$selected.QuietUninstallString
} else {
@@ -50,25 +56,42 @@
Exit 1
}
-$exePath = ""
-if ($uninstallCommand -match '^\s*"([^"]+)"') {
- # Quoted path
- $exePath = $matches[1]
-} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
- # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
- $exePath = $matches[1]
-} else {
- Throw "Could not parse uninstaller path from: $uninstallCommand"
-}
+if ($uninstallCommand -match '(?i)msiexec') {
+ # MSI-based entry: resolve the ProductCode and run a clean uninstall,
+ # ignoring whatever switches are already in the registry string.
+ $productCode = $selected.PSChildName
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
+ $productCode = $matches[1]
+ }
+ }
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ Throw "Could not determine ProductCode from: $uninstallCommand"
+ }
-# Vendor-documented silent uninstall switches.
-$uninstallArgs = "/uninstall /cleanup /silent"
+ Write-Host "Uninstalling product code: $productCode"
+ $process = Start-Process -FilePath "msiexec.exe" `
+ -ArgumentList "/x $productCode /qn /norestart" `
+ -PassThru -Wait
+} else {
+ # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run the
+ # vendor's own uninstaller with its documented silent switches.
+ $exePath = ""
+ if ($uninstallCommand -match '^\s*"([^"]+)"') {
+ # Quoted path
+ $exePath = $matches[1]
+ } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
+ # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
+ $exePath = $matches[1]
+ } else {
+ Throw "Could not parse uninstaller path from: $uninstallCommand"
+ }
-Write-Host "Selected entry DisplayName: $($selected.DisplayName)"
-Write-Host "Uninstall command: $exePath"
-Write-Host "Uninstall args: $uninstallArgs"
+ Write-Host "Uninstall command: $exePath"
+ $process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
+ -PassThru -Wait
+}
-$process = Start-Process -FilePath $exePath -ArgumentList $uninstallArgs -PassThru -Wait
$exitCode = $process.ExitCode
Write-Host "Uninstall exit code: $exitCode" |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1`:
- Around line 73-75: Update both uninstaller invocations in
ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1 at
lines 73-75 and 90-92: remove -Wait while retaining -PassThru, call
WaitForExit() with a defined timeout, terminate any process that exceeds it, and
fail immediately before reading ExitCode.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e28b92cf-882b-4b3e-94c3-21b0e8e738c8
📒 Files selected for processing (4)
cmd/maintained-apps/validate/windows.goee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_install.ps1ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
🚧 Files skipped from review as they are similar to previous changes (1)
- ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
Start-Process -Wait blocked indefinitely if the uninstaller hangs, which is exactly what happened to the install script earlier in this PR. Switch both the msiexec and exe-fallback branches to -PassThru + WaitForExit(timeout), stopping and failing the process if it doesn't exit within 3 minutes instead of reading a stale ExitCode.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // fa9409d1 -> 046e77f7 ===
--- /tmp/old.GBc5Jz 2026-08-11 01:36:31.995984239 +0000
+++ /tmp/new.8COYPr 2026-08-11 01:36:31.995984239 +0000
@@ -20,6 +20,7 @@
)
$exitCode = 0
+$timeoutSeconds = 180
try {
@@ -72,7 +73,7 @@
Write-Host "Uninstalling product code: $productCode"
$process = Start-Process -FilePath "msiexec.exe" `
-ArgumentList "/x $productCode /qn /norestart" `
- -PassThru -Wait
+ -PassThru
} else {
# Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run the
# vendor's own uninstaller with its documented silent switches.
@@ -89,7 +90,14 @@
Write-Host "Uninstall command: $exePath"
$process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
- -PassThru -Wait
+ -PassThru
+}
+
+$exited = $process.WaitForExit($timeoutSeconds * 1000)
+if (-not $exited) {
+ Write-Host "Uninstaller did not exit within ${timeoutSeconds}s, stopping it."
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ Exit 1
}
$exitCode = $process.ExitCode |
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // fa9409d1 -> 046e77f7 ===
--- /tmp/old.v76j3s 2026-08-11 01:55:47.044496521 +0000
+++ /tmp/new.4ayqxe 2026-08-11 01:55:47.044496521 +0000
@@ -20,6 +20,7 @@
)
$exitCode = 0
+$timeoutSeconds = 180
try {
@@ -72,7 +73,7 @@
Write-Host "Uninstalling product code: $productCode"
$process = Start-Process -FilePath "msiexec.exe" `
-ArgumentList "/x $productCode /qn /norestart" `
- -PassThru -Wait
+ -PassThru
} else {
# Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run the
# vendor's own uninstaller with its documented silent switches.
@@ -89,7 +90,14 @@
Write-Host "Uninstall command: $exePath"
$process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
- -PassThru -Wait
+ -PassThru
+}
+
+$exited = $process.WaitForExit($timeoutSeconds * 1000)
+if (-not $exited) {
+ Write-Host "Uninstaller did not exit within ${timeoutSeconds}s, stopping it."
+ Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
+ Exit 1
}
$exitCode = $process.ExitCode |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1 (1)
27-30: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not treat suppressed registry-read failures as a successful uninstall.
When both registry reads fail,
-ErrorAction SilentlyContinueproduces no entries.$selectedremains$null, and lines 41-44 exit with0, even if Citrix remains installed. Preserve registry-read failures before treating a missing entry as success.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1` around lines 27 - 30, Update the registry lookup in the uninstall script around $uninstallKeys to preserve and detect Get-ChildItem or Get-ItemProperty failures instead of suppressing them as empty results. Ensure the uninstall flow involving $selected returns a failure when registry reads fail, while still treating a successfully completed lookup with no matching entry as an already-uninstalled success.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1`:
- Around line 27-30: Update the registry lookup in the uninstall script around
$uninstallKeys to preserve and detect Get-ChildItem or Get-ItemProperty failures
instead of suppressing them as empty results. Ensure the uninstall flow
involving $selected returns a failure when registry reads fail, while still
treating a successfully completed lookup with no matching entry as an
already-uninstalled success.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 18214349-a7df-40e0-8376-75c6db35eed5
📒 Files selected for processing (2)
ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
🚧 Files skipped from review as they are similar to previous changes (1)
- ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
|
Addressing CodeRabbit's outside-diff finding on 🤖 Addressed by Claude Code |
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // ca292dea -> f13cfbb2 ===
--- /tmp/old.wqpgqZ 2026-08-11 03:19:07.273075178 +0000
+++ /tmp/new.XnuJ13 2026-08-11 03:19:07.274075185 +0000
@@ -1,21 +1,26 @@
# Uninstall for Citrix Workspace App LTSR.
#
-# The Programs and Features entry that's actually visible is Citrix's
-# bundled "ReceiverInside" component (DisplayName "Citrix Workspace Inside"),
-# whose UninstallString is a standard MSI reference (MsiExec.exe
-# /I{ProductCode}, the maintenance/repair form) -- not the TrolleyExpress.exe
-# or CWAInstaller.exe bootstrapper uninstaller some older community docs
-# describe for this DisplayName pattern. Resolve the ProductCode (registry
-# key name, or the first GUID in the UninstallString) and run a clean
-# "msiexec /x {ProductCode} /qn /norestart" -- never reuse the /I switch
-# from the registry string. If the selected entry isn't MSI-based, fall back
-# to re-running its own uninstaller exe with Citrix's documented silent
+# The bootstrap registers multiple separate Programs and Features entries
+# (confirmed by CI: "Citrix Workspace Inside" plus a distinct "Citrix
+# Workspace(USB)" entry, and possibly others depending on selected
+# components) -- removing just the first one found still leaves the app
+# detectable. Enumerate every entry matching the "Citrix Workspace " prefix
+# and publisher, and uninstall each one.
+#
+# Most of these entries' UninstallStrings are standard MSI references
+# (MsiExec.exe /I{ProductCode}, the maintenance/repair form) -- not the
+# TrolleyExpress.exe or CWAInstaller.exe bootstrapper uninstaller some older
+# community docs describe for this DisplayName pattern. Resolve the
+# ProductCode (registry key name, or the first GUID in the UninstallString)
+# and run a clean "msiexec /x {ProductCode} /qn /norestart" -- never reuse
+# the /I switch from the registry string. If an entry isn't MSI-based, fall
+# back to re-running its own uninstaller exe with Citrix's documented silent
# switches.
#
-# The Citrix bootstrapper installs several components; other internal MSI
-# transactions can still hold the Windows Installer mutex right after our own
-# install script returns, so msiexec /x fails transiently with 1618
-# (ERROR_INSTALL_ALREADY_RUNNING). Retry a few times with a short delay.
+# The Citrix bootstrapper installs several components as separate MSI
+# transactions, so uninstalling one can transiently fail with 1618
+# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
+# Retry a few times with a short delay.
$softwareNameLike = "Citrix Workspace *"
@@ -24,7 +29,6 @@
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
-$exitCode = 0
$timeoutSeconds = 180
$maxMsiAttempts = 5
$msiRetryDelaySeconds = 15
@@ -40,105 +44,121 @@
return $Process.ExitCode
}
-try {
+function Uninstall-CitrixEntry {
+ param($Entry)
-[array]$uninstallKeys = Get-ChildItem `
- -Path $paths `
- -ErrorAction SilentlyContinue |
- ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+ Write-Host "Uninstalling component: $($Entry.DisplayName)"
-$selected = $null
-foreach ($key in $uninstallKeys) {
- if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
- -and $key.Publisher -eq "Citrix Systems, Inc.") {
- $selected = $key
- break
+ $uninstallCommand = if ($Entry.QuietUninstallString) {
+ $Entry.QuietUninstallString
+ } else {
+ $Entry.UninstallString
}
-}
-if (-not $selected) {
- # Already uninstalled (or never installed) -- nothing to do.
- Write-Host "Uninstall entry not found for $softwareNameLike"
- Exit 0
-}
+ if (-not $uninstallCommand) {
+ Write-Host "Entry has no UninstallString: $($Entry.DisplayName)"
+ return 1
+ }
+
+ if ($uninstallCommand -match '(?i)msiexec') {
+ # MSI-based entry: resolve the ProductCode and run a clean uninstall,
+ # ignoring whatever switches are already in the registry string.
+ $productCode = $Entry.PSChildName
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
+ $productCode = $matches[1]
+ }
+ }
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ Write-Host "Could not determine ProductCode from: $uninstallCommand"
+ return 1
+ }
-Write-Host "Selected entry DisplayName: $($selected.DisplayName)"
+ Write-Host "Uninstalling product code: $productCode"
-$uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
-} else {
- $selected.UninstallString
-}
-
-if (-not $uninstallCommand) {
- Write-Host "Selected entry has no UninstallString: $($selected.DisplayName)"
- Exit 1
-}
-
-if ($uninstallCommand -match '(?i)msiexec') {
- # MSI-based entry: resolve the ProductCode and run a clean uninstall,
- # ignoring whatever switches are already in the registry string.
- $productCode = $selected.PSChildName
- if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
- if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
- $productCode = $matches[1]
- }
- }
- if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
- Throw "Could not determine ProductCode from: $uninstallCommand"
- }
+ $exitCode = 1
+ for ($attempt = 1; $attempt -le $maxMsiAttempts; $attempt++) {
+ $process = Start-Process -FilePath "msiexec.exe" `
+ -ArgumentList "/x $productCode /qn /norestart" `
+ -PassThru
+
+ $exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
+ if ($null -eq $exitCode) {
+ return 1
+ }
+ Write-Host "Uninstall exit code: $exitCode (attempt $attempt of $maxMsiAttempts)"
+
+ if ($exitCode -ne 1618) {
+ break
+ }
- Write-Host "Uninstalling product code: $productCode"
+ Write-Host "Windows Installer busy (1618) -- another of Citrix's own component installs likely still holds the mutex. Retrying in ${msiRetryDelaySeconds}s."
+ Start-Sleep -Seconds $msiRetryDelaySeconds
+ }
+ } else {
+ # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run
+ # the vendor's own uninstaller with its documented silent switches.
+ $exePath = ""
+ if ($uninstallCommand -match '^\s*"([^"]+)"') {
+ # Quoted path
+ $exePath = $matches[1]
+ } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
+ # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
+ $exePath = $matches[1]
+ } else {
+ Write-Host "Could not parse uninstaller path from: $uninstallCommand"
+ return 1
+ }
- for ($attempt = 1; $attempt -le $maxMsiAttempts; $attempt++) {
- $process = Start-Process -FilePath "msiexec.exe" `
- -ArgumentList "/x $productCode /qn /norestart" `
+ Write-Host "Uninstall command: $exePath"
+ $process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
-PassThru
$exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
if ($null -eq $exitCode) {
- Exit 1
+ return 1
}
- Write-Host "Uninstall exit code: $exitCode (attempt $attempt of $maxMsiAttempts)"
-
- if ($exitCode -ne 1618) {
- break
- }
-
- Write-Host "Windows Installer busy (1618) -- another of Citrix's own component installs likely still holds the mutex. Retrying in ${msiRetryDelaySeconds}s."
- Start-Sleep -Seconds $msiRetryDelaySeconds
+ Write-Host "Uninstall exit code: $exitCode"
}
-} else {
- # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run the
- # vendor's own uninstaller with its documented silent switches.
- $exePath = ""
- if ($uninstallCommand -match '^\s*"([^"]+)"') {
- # Quoted path
- $exePath = $matches[1]
- } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
- # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
- $exePath = $matches[1]
- } else {
- Throw "Could not parse uninstaller path from: $uninstallCommand"
+
+ # Treat msiexec-style reboot-required codes as success too.
+ if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+ return 0
}
+ return $exitCode
+}
- Write-Host "Uninstall command: $exePath"
- $process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
- -PassThru
+try {
- $exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
- if ($null -eq $exitCode) {
- Exit 1
- }
- Write-Host "Uninstall exit code: $exitCode"
+[array]$uninstallKeys = Get-ChildItem `
+ -Path $paths `
+ -ErrorAction SilentlyContinue -ErrorVariable readErrors |
+ ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+
+if (@($readErrors).Count -ge $paths.Count) {
+ Throw "Failed to read the registry uninstall keys: $($readErrors -join '; ')"
+}
+
+[array]$entries = $uninstallKeys | Where-Object {
+ $_.DisplayName -and $_.DisplayName -like $softwareNameLike `
+ -and $_.Publisher -eq "Citrix Systems, Inc."
}
-# Treat msiexec-style reboot-required codes as success too.
-if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+if ($entries.Count -eq 0) {
+ # Already uninstalled (or never installed) -- nothing to do.
+ Write-Host "No entries found matching $softwareNameLike"
Exit 0
}
-Exit $exitCode
+$overallExitCode = 0
+foreach ($entry in $entries) {
+ $result = Uninstall-CitrixEntry -Entry $entry
+ if ($result -ne 0) {
+ $overallExitCode = $result
+ }
+}
+
+Exit $overallExitCode
} catch {
Write-Host "Error: $_" |
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // ca292dea -> f13cfbb2 ===
--- /tmp/old.JiHfk8 2026-08-11 03:26:02.457296091 +0000
+++ /tmp/new.nNWNkO 2026-08-11 03:26:02.457296091 +0000
@@ -1,21 +1,26 @@
# Uninstall for Citrix Workspace App LTSR.
#
-# The Programs and Features entry that's actually visible is Citrix's
-# bundled "ReceiverInside" component (DisplayName "Citrix Workspace Inside"),
-# whose UninstallString is a standard MSI reference (MsiExec.exe
-# /I{ProductCode}, the maintenance/repair form) -- not the TrolleyExpress.exe
-# or CWAInstaller.exe bootstrapper uninstaller some older community docs
-# describe for this DisplayName pattern. Resolve the ProductCode (registry
-# key name, or the first GUID in the UninstallString) and run a clean
-# "msiexec /x {ProductCode} /qn /norestart" -- never reuse the /I switch
-# from the registry string. If the selected entry isn't MSI-based, fall back
-# to re-running its own uninstaller exe with Citrix's documented silent
+# The bootstrap registers multiple separate Programs and Features entries
+# (confirmed by CI: "Citrix Workspace Inside" plus a distinct "Citrix
+# Workspace(USB)" entry, and possibly others depending on selected
+# components) -- removing just the first one found still leaves the app
+# detectable. Enumerate every entry matching the "Citrix Workspace " prefix
+# and publisher, and uninstall each one.
+#
+# Most of these entries' UninstallStrings are standard MSI references
+# (MsiExec.exe /I{ProductCode}, the maintenance/repair form) -- not the
+# TrolleyExpress.exe or CWAInstaller.exe bootstrapper uninstaller some older
+# community docs describe for this DisplayName pattern. Resolve the
+# ProductCode (registry key name, or the first GUID in the UninstallString)
+# and run a clean "msiexec /x {ProductCode} /qn /norestart" -- never reuse
+# the /I switch from the registry string. If an entry isn't MSI-based, fall
+# back to re-running its own uninstaller exe with Citrix's documented silent
# switches.
#
-# The Citrix bootstrapper installs several components; other internal MSI
-# transactions can still hold the Windows Installer mutex right after our own
-# install script returns, so msiexec /x fails transiently with 1618
-# (ERROR_INSTALL_ALREADY_RUNNING). Retry a few times with a short delay.
+# The Citrix bootstrapper installs several components as separate MSI
+# transactions, so uninstalling one can transiently fail with 1618
+# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
+# Retry a few times with a short delay.
$softwareNameLike = "Citrix Workspace *"
@@ -24,7 +29,6 @@
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
-$exitCode = 0
$timeoutSeconds = 180
$maxMsiAttempts = 5
$msiRetryDelaySeconds = 15
@@ -40,105 +44,121 @@
return $Process.ExitCode
}
-try {
+function Uninstall-CitrixEntry {
+ param($Entry)
-[array]$uninstallKeys = Get-ChildItem `
- -Path $paths `
- -ErrorAction SilentlyContinue |
- ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+ Write-Host "Uninstalling component: $($Entry.DisplayName)"
-$selected = $null
-foreach ($key in $uninstallKeys) {
- if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
- -and $key.Publisher -eq "Citrix Systems, Inc.") {
- $selected = $key
- break
+ $uninstallCommand = if ($Entry.QuietUninstallString) {
+ $Entry.QuietUninstallString
+ } else {
+ $Entry.UninstallString
}
-}
-if (-not $selected) {
- # Already uninstalled (or never installed) -- nothing to do.
- Write-Host "Uninstall entry not found for $softwareNameLike"
- Exit 0
-}
+ if (-not $uninstallCommand) {
+ Write-Host "Entry has no UninstallString: $($Entry.DisplayName)"
+ return 1
+ }
+
+ if ($uninstallCommand -match '(?i)msiexec') {
+ # MSI-based entry: resolve the ProductCode and run a clean uninstall,
+ # ignoring whatever switches are already in the registry string.
+ $productCode = $Entry.PSChildName
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
+ $productCode = $matches[1]
+ }
+ }
+ if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
+ Write-Host "Could not determine ProductCode from: $uninstallCommand"
+ return 1
+ }
-Write-Host "Selected entry DisplayName: $($selected.DisplayName)"
+ Write-Host "Uninstalling product code: $productCode"
-$uninstallCommand = if ($selected.QuietUninstallString) {
- $selected.QuietUninstallString
-} else {
- $selected.UninstallString
-}
-
-if (-not $uninstallCommand) {
- Write-Host "Selected entry has no UninstallString: $($selected.DisplayName)"
- Exit 1
-}
-
-if ($uninstallCommand -match '(?i)msiexec') {
- # MSI-based entry: resolve the ProductCode and run a clean uninstall,
- # ignoring whatever switches are already in the registry string.
- $productCode = $selected.PSChildName
- if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
- if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
- $productCode = $matches[1]
- }
- }
- if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
- Throw "Could not determine ProductCode from: $uninstallCommand"
- }
+ $exitCode = 1
+ for ($attempt = 1; $attempt -le $maxMsiAttempts; $attempt++) {
+ $process = Start-Process -FilePath "msiexec.exe" `
+ -ArgumentList "/x $productCode /qn /norestart" `
+ -PassThru
+
+ $exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
+ if ($null -eq $exitCode) {
+ return 1
+ }
+ Write-Host "Uninstall exit code: $exitCode (attempt $attempt of $maxMsiAttempts)"
+
+ if ($exitCode -ne 1618) {
+ break
+ }
- Write-Host "Uninstalling product code: $productCode"
+ Write-Host "Windows Installer busy (1618) -- another of Citrix's own component installs likely still holds the mutex. Retrying in ${msiRetryDelaySeconds}s."
+ Start-Sleep -Seconds $msiRetryDelaySeconds
+ }
+ } else {
+ # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run
+ # the vendor's own uninstaller with its documented silent switches.
+ $exePath = ""
+ if ($uninstallCommand -match '^\s*"([^"]+)"') {
+ # Quoted path
+ $exePath = $matches[1]
+ } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
+ # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
+ $exePath = $matches[1]
+ } else {
+ Write-Host "Could not parse uninstaller path from: $uninstallCommand"
+ return 1
+ }
- for ($attempt = 1; $attempt -le $maxMsiAttempts; $attempt++) {
- $process = Start-Process -FilePath "msiexec.exe" `
- -ArgumentList "/x $productCode /qn /norestart" `
+ Write-Host "Uninstall command: $exePath"
+ $process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
-PassThru
$exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
if ($null -eq $exitCode) {
- Exit 1
+ return 1
}
- Write-Host "Uninstall exit code: $exitCode (attempt $attempt of $maxMsiAttempts)"
-
- if ($exitCode -ne 1618) {
- break
- }
-
- Write-Host "Windows Installer busy (1618) -- another of Citrix's own component installs likely still holds the mutex. Retrying in ${msiRetryDelaySeconds}s."
- Start-Sleep -Seconds $msiRetryDelaySeconds
+ Write-Host "Uninstall exit code: $exitCode"
}
-} else {
- # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run the
- # vendor's own uninstaller with its documented silent switches.
- $exePath = ""
- if ($uninstallCommand -match '^\s*"([^"]+)"') {
- # Quoted path
- $exePath = $matches[1]
- } elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
- # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
- $exePath = $matches[1]
- } else {
- Throw "Could not parse uninstaller path from: $uninstallCommand"
+
+ # Treat msiexec-style reboot-required codes as success too.
+ if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+ return 0
}
+ return $exitCode
+}
- Write-Host "Uninstall command: $exePath"
- $process = Start-Process -FilePath $exePath -ArgumentList "/uninstall /cleanup /silent" `
- -PassThru
+try {
- $exitCode = Wait-ProcessBounded -Process $process -TimeoutSeconds $timeoutSeconds
- if ($null -eq $exitCode) {
- Exit 1
- }
- Write-Host "Uninstall exit code: $exitCode"
+[array]$uninstallKeys = Get-ChildItem `
+ -Path $paths `
+ -ErrorAction SilentlyContinue -ErrorVariable readErrors |
+ ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
+
+if (@($readErrors).Count -ge $paths.Count) {
+ Throw "Failed to read the registry uninstall keys: $($readErrors -join '; ')"
+}
+
+[array]$entries = $uninstallKeys | Where-Object {
+ $_.DisplayName -and $_.DisplayName -like $softwareNameLike `
+ -and $_.Publisher -eq "Citrix Systems, Inc."
}
-# Treat msiexec-style reboot-required codes as success too.
-if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+if ($entries.Count -eq 0) {
+ # Already uninstalled (or never installed) -- nothing to do.
+ Write-Host "No entries found matching $softwareNameLike"
Exit 0
}
-Exit $exitCode
+$overallExitCode = 0
+foreach ($entry in $entries) {
+ $result = Uninstall-CitrixEntry -Entry $entry
+ if ($result -ne 0) {
+ $overallExitCode = $result
+ }
+}
+
+Exit $overallExitCode
} catch {
Write-Host "Error: $_" |
CI showed the previous fix (enumerate all matching entries) still left
"Citrix Workspace(USB)" installed. Root cause: the -like pattern was
"Citrix Workspace *" with a literal space before the wildcard, but that
entry's DisplayName has no space after "Workspace" ("Citrix
Workspace(USB)"), so it silently never matched the filter and was never
even attempted. Drop the space so the wildcard covers both shapes.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // f13cfbb2 -> 2a500494 ===
--- /tmp/old.dj5RhF 2026-08-11 03:33:33.387038954 +0000
+++ /tmp/new.Oy7wgw 2026-08-11 03:33:33.387038954 +0000
@@ -4,8 +4,9 @@
# (confirmed by CI: "Citrix Workspace Inside" plus a distinct "Citrix
# Workspace(USB)" entry, and possibly others depending on selected
# components) -- removing just the first one found still leaves the app
-# detectable. Enumerate every entry matching the "Citrix Workspace " prefix
-# and publisher, and uninstall each one.
+# detectable. Enumerate every entry matching the "Citrix Workspace" prefix
+# and publisher, and uninstall each one. No space before the wildcard: some
+# entries (e.g. "Citrix Workspace(USB)") have no space after "Workspace".
#
# Most of these entries' UninstallStrings are standard MSI references
# (MsiExec.exe /I{ProductCode}, the maintenance/repair form) -- not the
@@ -22,7 +23,7 @@
# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
# Retry a few times with a short delay.
-$softwareNameLike = "Citrix Workspace *"
+$softwareNameLike = "Citrix Workspace*"
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', |
…the first
CI kept showing "Citrix Workspace(USB)" still installed after uninstall, even
after fixing the uninstall script to enumerate every matching entry. The
install script was declaring success as soon as the FIRST component
("Citrix Workspace Inside") registered, but the bootstrap installs several
components as separate, sequential MSI transactions -- "Citrix
Workspace(USB)" was still mid-install (or not yet started) at that point.
The subsequent uninstall step only ever saw the entries that existed yet.
Require at least one entry registered AND no msiexec.exe process running,
stable across two consecutive 10s polls, before declaring the install done.
Also fixes the same missing-wildcard-space bug from the uninstall script fix
(this script's copy of the pattern had it too).
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install // 9542c22e -> 0f4f1460 ===
--- /tmp/old.AfXrxi 2026-08-11 03:44:06.055601237 +0000
+++ /tmp/new.EqlDQr 2026-08-11 03:44:06.055601237 +0000
@@ -9,17 +9,25 @@
# The Citrix bootstrapper leaves resident processes running after install
# (e.g. its self-service/notification tray app), so Start-Process -Wait
# never returns -- it waits on the whole process tree, not just the
-# installer itself. Start without -Wait and poll Programs and Features for
-# the app's own registry entry instead of trusting the installer process's
-# exit code.
+# installer itself. Start without -Wait and poll Programs and Features
+# instead of trusting the installer process's exit code.
+#
+# The bootstrap installs several components as separate, sequential MSI
+# transactions (confirmed by CI: "Citrix Workspace Inside" registers before
+# "Citrix Workspace(USB)" does). Declaring success as soon as the FIRST
+# entry appears races the still-running later components -- our uninstall
+# script would then only find and remove whichever ones had registered so
+# far. Require at least one entry AND no msiexec.exe process running,
+# stable across two consecutive polls, before declaring the install done.
-$softwareNameLike = "Citrix Workspace *"
+$softwareNameLike = "Citrix Workspace*"
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
)
$timeoutSeconds = 480
$pollIntervalSeconds = 10
+$requiredStableChecks = 2
function Test-CitrixWorkspaceInstalled {
[array]$uninstallKeys = Get-ChildItem `
@@ -43,16 +51,26 @@
-PassThru | Out-Null
$elapsed = 0
+$stableChecks = 0
while ($elapsed -lt $timeoutSeconds) {
- if (Test-CitrixWorkspaceInstalled) {
- Write-Host "Citrix Workspace registered in Programs and Features after ${elapsed}s"
- Exit 0
+ $registered = Test-CitrixWorkspaceInstalled
+ $msiexecIdle = -not (Get-Process -Name "msiexec" -ErrorAction SilentlyContinue)
+
+ if ($registered -and $msiexecIdle) {
+ $stableChecks++
+ if ($stableChecks -ge $requiredStableChecks) {
+ Write-Host "Citrix Workspace registered and no MSI transaction in flight after ${elapsed}s"
+ Exit 0
+ }
+ } else {
+ $stableChecks = 0
}
+
Start-Sleep -Seconds $pollIntervalSeconds
$elapsed += $pollIntervalSeconds
}
-Write-Host "Timed out after ${timeoutSeconds}s waiting for Citrix Workspace to register"
+Write-Host "Timed out after ${timeoutSeconds}s waiting for Citrix Workspace to finish installing"
Exit 1
} catch {
=== Uninstall Script (no changes) === |
CI showed the real shape now that the install script waits for all components: 5 entries total, including a master "Citrix Workspace <version>" entry whose UninstallString runs bootstrapperhelper.exe rather than msiexec. Removing that master entry cascades and removes the individual MSI sub-components (USB, SSON, Inside, DV) too. Since we snapshot every entry before uninstalling any of them, the later msiexec /x calls for those already-cascaded components correctly report 1605 (ERROR_UNKNOWN_PRODUCT, "not currently installed") -- that's the expected outcome, not a failure.
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // 2a500494 -> 955cb307 ===
--- /tmp/old.vMqfJC 2026-08-11 03:57:29.447361810 +0000
+++ /tmp/new.KzMWy9 2026-08-11 03:57:29.447361810 +0000
@@ -22,6 +22,14 @@
# transactions, so uninstalling one can transiently fail with 1618
# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
# Retry a few times with a short delay.
+#
+# One entry (DisplayName "Citrix Workspace <version>", UninstallString
+# bootstrapperhelper.exe rather than msiexec) is the master entry: removing
+# it cascades and removes the other MSI sub-components (USB, SSON, Inside,
+# DV, ...) too. Since every entry was already snapshotted before any
+# uninstall ran, a later msiexec /x for an already-cascaded component fails
+# with 1605 (ERROR_UNKNOWN_PRODUCT, "not currently installed") -- that's
+# success, not a failure.
$softwareNameLike = "Citrix Workspace*"
@@ -122,8 +130,9 @@
Write-Host "Uninstall exit code: $exitCode"
}
- # Treat msiexec-style reboot-required codes as success too.
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+ # Treat msiexec-style reboot-required codes as success, and 1605 (product
+ # already removed, e.g. by another entry's cascade) as success too.
+ if ($exitCode -eq 3010 -or $exitCode -eq 1641 -or $exitCode -eq 1605) {
return 0
}
return $exitCode |
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install Script (no changes) ===
=== Uninstall // 2a500494 -> 955cb307 ===
--- /tmp/old.z4c9IR 2026-08-11 14:58:13.603832173 +0000
+++ /tmp/new.9q62jh 2026-08-11 14:58:13.604832182 +0000
@@ -22,6 +22,14 @@
# transactions, so uninstalling one can transiently fail with 1618
# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
# Retry a few times with a short delay.
+#
+# One entry (DisplayName "Citrix Workspace <version>", UninstallString
+# bootstrapperhelper.exe rather than msiexec) is the master entry: removing
+# it cascades and removes the other MSI sub-components (USB, SSON, Inside,
+# DV, ...) too. Since every entry was already snapshotted before any
+# uninstall ran, a later msiexec /x for an already-cascaded component fails
+# with 1605 (ERROR_UNKNOWN_PRODUCT, "not currently installed") -- that's
+# success, not a failure.
$softwareNameLike = "Citrix Workspace*"
@@ -122,8 +130,9 @@
Write-Host "Uninstall exit code: $exitCode"
}
- # Treat msiexec-style reboot-required codes as success too.
- if ($exitCode -eq 3010 -or $exitCode -eq 1641) {
+ # Treat msiexec-style reboot-required codes as success, and 1605 (product
+ # already removed, e.g. by another entry's cascade) as success too.
+ if ($exitCode -eq 3010 -or $exitCode -eq 1641 -or $exitCode -eq 1605) {
return 0
}
return $exitCode |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_install.ps1`:
- Line 23: Update the installation-detection logic using $softwareNameLike to
match the exact display name "Citrix Workspace Inside" instead of a wildcard,
while preserving the existing publisher check and related checks around lines
38-41.
In
`@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1`:
- Line 34: Update the uninstall selection logic using $softwareNameLike so it
distinguishes Citrix Workspace LTSR from Current Release entries. Use a
track-specific registry value or validate the installed package identity before
proceeding, ensuring only the LTSR package is selected for uninstallation.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 468ea9ba-b69e-4802-90f3-4ef89ecb5e44
📒 Files selected for processing (3)
ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_install.ps1ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
🚧 Files skipped from review as they are similar to previous changes (1)
- ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json
| # with 1605 (ERROR_UNKNOWN_PRODUCT, "not currently installed") -- that's | ||
| # success, not a failure. | ||
|
|
||
| $softwareNameLike = "Citrix Workspace*" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate all Citrix maintained-app definitions and lifecycle scripts.
fd -i 'citrix' ee/maintained-apps | sort
# Compare registry matching and uninstall behavior across Citrix app variants.
rg -n -i -C 5 \
'Citrix Workspace|softwareNameLike|DisplayName|Publisher|UninstallString' \
ee/maintained-appsRepository: fleetdm/fleet
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Citrix files ---'
fd -i 'citrix' ee/maintained-apps | sort
printf '%s\n' '--- Uninstall script ---'
sed -n '1,230p' ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1
printf '%s\n' '--- Citrix definitions and references ---'
rg -n -i -C 8 --glob '*.{yaml,yml,json,md,ps1}' \
'Citrix|Workspace.*LTSR|Current Release|LTSR' \
ee/maintained-apps | head -n 1200Repository: fleetdm/fleet
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Tracked Citrix paths ---'
git ls-files | rg -i 'citrix|workspace' | head -n 300
printf '%s\n' '--- Input definitions containing Citrix or Workspace ---'
rg -l -i --glob 'ee/maintained-apps/inputs/**' 'Citrix|Workspace' | sort | head -n 300
printf '%s\n' '--- Relevant input definitions ---'
while IFS= read -r file; do
case "$file" in
*.json|*.yaml|*.yml) printf '\n### %s\n' "$file"; cat -n "$file" ;;
esac
done < <(rg -l -i --glob 'ee/maintained-apps/inputs/**' 'Citrix|Workspace' | sort | head -n 100)
printf '%s\n' '--- Windows output definitions for Citrix workspace variants ---'
for file in ee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json ee/maintained-apps/outputs/citrix-workspace/windows.json; do
if [ -f "$file" ]; then
printf '\n### %s\n' "$file"
python3 - "$file" <<'PY'
import json, sys
with open(sys.argv[1], encoding='utf-8') as f:
data = json.load(f)
for version in data.get("versions", []):
print(json.dumps({k: version.get(k) for k in ("version", "queries", "installer_url")}, indent=2))
PY
fi
doneRepository: fleetdm/fleet
Length of output: 2823
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Citrix references outside generated outputs ---'
rg -n -i -C 5 --glob '!ee/maintained-apps/outputs/**' 'Citrix Workspace|citrix-workspace|LTSR|Current Release' . | head -n 1200
printf '%s\n' '--- Citrix input definitions ---'
for file in ee/maintained-apps/inputs/homebrew/citrix-workspace.json ee/maintained-apps/inputs/winget/citrix-workspace-ltsr.json; do
printf '\n### %s\n' "$file"
cat -n "$file"
done
printf '%s\n' '--- All generated Citrix output files ---'
find ee/maintained-apps/outputs -maxdepth 2 -type f -iname '*citrix*' -print | sortRepository: fleetdm/fleet
Length of output: 50370
🌐 Web query:
Citrix Workspace app LTSR Current Release Windows Programs and Features registry DisplayName Publisher side-by-side installation
💡 Result:
Citrix Workspace app does not support side-by-side installation of multiple versions (e.g., LTSR and Current Release) on a single Windows machine [1][2]. Installing a new version typically upgrades or replaces the existing installation [3]. In Windows Programs and Features, the DisplayName and Publisher entries are generally consistent for both LTSR and Current Release (CR) builds, typically appearing as Citrix Workspace app [3]. Because the product uses identical registry paths and naming conventions, it is difficult to distinguish between LTSR and CR installations based solely on the standard Programs and Features registry keys (e.g., HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\CitrixOnlinePluginPackWeb) [3][4][5]. To identify if an installation is an LTSR release, administrators often rely on checking specific registry flags used for auto-update configuration rather than the display name itself [4][5]. Specifically, if an environment is configured for LTSR, the following registry value is commonly set to true [5]: HKLM\SOFTWARE\Citrix\ICA Client\AutoUpdate\Commandline Policy\LTSROnly = true or HKLM\SOFTWARE\Policies\Citrix\ICA Client\AutoUpdate\LTSROnly = true Because Citrix uses overlapping versioning schemes—where LTSR and CR tracks may have similar or even higher build numbers—the underlying DisplayVersion cannot be reliably used to distinguish between the two tracks without context [3]. Furthermore, installing a user-level instance alongside a system-level (administrator) instance is not recommended and can cause conflicts [2]. If you need to manage specific versions, it is recommended to control them through deployment policies (such as AutoUpdateStream) rather than attempting to maintain concurrent installations [6][2].
Citations:
- 1: https://docs.citrix.com/en-us/citrix-workspace-app-for-windows-store/install?lang-switch=true
- 2: https://docs.citrix.com/en-us/citrix-workspace-app-for-windows/1912-ltsr/install
- 3: https://help.automox.com/hc/en-us/articles/31573182580756-Why-is-my-Citrix-version-different-from-what-is-installed
- 4: https://forum.patchmypc.com/index.php/topic,4995.0.html
- 5: https://forum.patchmypc.com/index.php/topic,3971.0.html
- 6: https://docs.citrix.com/en-us/citrix-workspace-app-for-windows/2402-ltsr/install
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import fnmatch
pattern = "Citrix Workspace*"
publisher = "Citrix Systems, Inc."
entries = [
("Citrix Workspace 2402", publisher),
("Citrix Workspace Inside", publisher),
("Citrix Workspace(USB)", publisher),
("Citrix Workspace app", publisher),
("Citrix Workspace app", "Other publisher"),
]
matched = [
(name, pub)
for name, pub in entries
if fnmatch.fnmatchcase(name.lower(), pattern.lower())
and pub == publisher
]
print("pattern:", pattern)
print("publisher:", publisher)
print("matched:", matched)
print("current-release-shaped entry matched:",
("Citrix Workspace app", publisher) in matched)
PYRepository: fleetdm/fleet
Length of output: 468
Restrict the selector to LTSR.
If Current Release is installed, $softwareNameLike = "Citrix Workspace*" matches its entries because Citrix uses the same Programs and Features name and publisher for both tracks. Use a track-specific registry value or validate the installed package identity before uninstalling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ee/maintained-apps/inputs/winget/scripts/citrix_workspace_ltsr_uninstall.ps1`
at line 34, Update the uninstall selection logic using $softwareNameLike so it
distinguishes Citrix Workspace LTSR from Current Release entries. Use a
track-specific registry value or validate the installed package identity before
proceeding, ensuring only the LTSR package is selected for uninstallation.
There was a problem hiding this comment.
Didn't implement a track-specific selector -- and notably, your own web research in this same comment reaches the same conclusion I did on the earlier round of this PR: "the DisplayVersion cannot be reliably used to distinguish between the two tracks" and "it is difficult to distinguish between LTSR and CR installations based solely on the standard Programs and Features registry keys." The LTSROnly policy registry value you found (HKLM\SOFTWARE\Citrix\ICA Client\AutoUpdate\...\LTSROnly) is an admin-configured auto-update policy, not installed-package identity -- it's usually unset, and if I gated on it the uninstall script would silently no-op on real LTSR installs that didn't have that GPO configured, which is worse than the current behavior. There's also still no Windows Current Release FMA in this catalog to actually collide with today. Leaving this thread open (not resolving) since it's a real tradeoff a human reviewer may want to weigh in on rather than something I should decide unilaterally.
🤖 Addressed by Claude Code
A wildcard match could let a leftover entry from an unrelated prior install falsely satisfy the install script's completion check before this install has actually registered anything. "Citrix Workspace Inside" is confirmed (via CI) to always be one of the components this package installs, so match it exactly. The uninstall script still needs the broad wildcard since it has to enumerate and remove every component, not just detect one.
…ndows-citrix-workspace-ltsr
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install // 0f4f1460 -> b4bd52f2 ===
--- /tmp/old.C3131o 2026-08-11 16:23:29.175951711 +0000
+++ /tmp/new.viF6Se 2026-08-11 16:23:29.175951711 +0000
@@ -17,10 +17,13 @@
# "Citrix Workspace(USB)" does). Declaring success as soon as the FIRST
# entry appears races the still-running later components -- our uninstall
# script would then only find and remove whichever ones had registered so
-# far. Require at least one entry AND no msiexec.exe process running,
-# stable across two consecutive polls, before declaring the install done.
+# far. Require the core "Citrix Workspace Inside" entry AND no msiexec.exe
+# process running, stable across two consecutive polls, before declaring the
+# install done. Match that DisplayName exactly (not a "Citrix Workspace*"
+# wildcard) so a leftover entry from an unrelated prior install can't cause
+# a false-positive success before this install has actually registered.
-$softwareNameLike = "Citrix Workspace*"
+$softwareName = "Citrix Workspace Inside"
$paths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
@@ -36,7 +39,7 @@
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }
foreach ($key in $uninstallKeys) {
- if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike `
+ if ($key.DisplayName -eq $softwareName `
-and $key.Publisher -eq "Citrix Systems, Inc.") {
return $true
}
=== Uninstall Script (no changes) === |
|
@kitzy we need to drastically cut down on the comments in scripts. These are customer facing. This has been added to the new-fma skill but waiting for it to be merged. |
These scripts ship in the generated output manifests and run on customer machines. Cut CI-run history, vendor-bug archaeology, and narrative about prior revisions down to the operational essentials -- from 24/41 comment lines to 6/12, in line with the repo median (5) and p90 (14).
|
Trimmed both scripts down (24/41 comment lines → 6/12, in line with the repo median/p90) — pushed in 2a11827. Kept only what's needed to understand a non-obvious switch or exit code at the callsite; the rest of the investigation history is in this PR's description and review thread. 🤖 Addressed by Claude Code |
Script Diff Resultsee/maintained-apps/outputs/citrix-workspace-ltsr/windows.json=== Install // b4bd52f2 -> ed1646b6 ===
--- /tmp/old.LCeYYH 2026-08-12 20:32:52.327246353 +0000
+++ /tmp/new.WCzSx4 2026-08-12 20:32:52.327246353 +0000
@@ -1,27 +1,10 @@
# Learn more about .exe install scripts:
# http://fleetdm.com/learn-more-about/exe-install-scripts
-#
-# Vendor-documented silent switches (winget InstallerSwitches: Silent +
-# Custom): /silent runs without dialogs/prompts, /noreboot suppresses the
-# reboot prompt, and /AutoUpdateCheck=disabled keeps Citrix's own updater
-# from taking over version management.
-#
-# The Citrix bootstrapper leaves resident processes running after install
-# (e.g. its self-service/notification tray app), so Start-Process -Wait
-# never returns -- it waits on the whole process tree, not just the
-# installer itself. Start without -Wait and poll Programs and Features
-# instead of trusting the installer process's exit code.
-#
-# The bootstrap installs several components as separate, sequential MSI
-# transactions (confirmed by CI: "Citrix Workspace Inside" registers before
-# "Citrix Workspace(USB)" does). Declaring success as soon as the FIRST
-# entry appears races the still-running later components -- our uninstall
-# script would then only find and remove whichever ones had registered so
-# far. Require the core "Citrix Workspace Inside" entry AND no msiexec.exe
-# process running, stable across two consecutive polls, before declaring the
-# install done. Match that DisplayName exactly (not a "Citrix Workspace*"
-# wildcard) so a leftover entry from an unrelated prior install can't cause
-# a false-positive success before this install has actually registered.
+
+# The installer leaves resident processes running and its bootstrap installs
+# several components as separate MSI transactions, so Start-Process -Wait
+# never returns reliably. Poll for the core entry instead, and wait for
+# msiexec to go idle so we don't race the later components.
$softwareName = "Citrix Workspace Inside"
$paths = @(
=== Uninstall // 955cb307 -> c05a6493 ===
--- /tmp/old.NFdo0D 2026-08-12 20:32:52.357246926 +0000
+++ /tmp/new.FFqKZz 2026-08-12 20:32:52.357246926 +0000
@@ -1,35 +1,7 @@
-# Uninstall for Citrix Workspace App LTSR.
-#
-# The bootstrap registers multiple separate Programs and Features entries
-# (confirmed by CI: "Citrix Workspace Inside" plus a distinct "Citrix
-# Workspace(USB)" entry, and possibly others depending on selected
-# components) -- removing just the first one found still leaves the app
-# detectable. Enumerate every entry matching the "Citrix Workspace" prefix
-# and publisher, and uninstall each one. No space before the wildcard: some
-# entries (e.g. "Citrix Workspace(USB)") have no space after "Workspace".
-#
-# Most of these entries' UninstallStrings are standard MSI references
-# (MsiExec.exe /I{ProductCode}, the maintenance/repair form) -- not the
-# TrolleyExpress.exe or CWAInstaller.exe bootstrapper uninstaller some older
-# community docs describe for this DisplayName pattern. Resolve the
-# ProductCode (registry key name, or the first GUID in the UninstallString)
-# and run a clean "msiexec /x {ProductCode} /qn /norestart" -- never reuse
-# the /I switch from the registry string. If an entry isn't MSI-based, fall
-# back to re-running its own uninstaller exe with Citrix's documented silent
-# switches.
-#
-# The Citrix bootstrapper installs several components as separate MSI
-# transactions, so uninstalling one can transiently fail with 1618
-# (ERROR_INSTALL_ALREADY_RUNNING) while another is still mid-transaction.
-# Retry a few times with a short delay.
-#
-# One entry (DisplayName "Citrix Workspace <version>", UninstallString
-# bootstrapperhelper.exe rather than msiexec) is the master entry: removing
-# it cascades and removes the other MSI sub-components (USB, SSON, Inside,
-# DV, ...) too. Since every entry was already snapshotted before any
-# uninstall ran, a later msiexec /x for an already-cascaded component fails
-# with 1605 (ERROR_UNKNOWN_PRODUCT, "not currently installed") -- that's
-# success, not a failure.
+# The bootstrap registers several separate Programs and Features entries, so
+# enumerate everything matching the "Citrix Workspace" prefix and publisher
+# and uninstall each one. No space before the wildcard: some entries (e.g.
+# "Citrix Workspace(USB)") have none after "Workspace".
$softwareNameLike = "Citrix Workspace*"
@@ -70,8 +42,8 @@
}
if ($uninstallCommand -match '(?i)msiexec') {
- # MSI-based entry: resolve the ProductCode and run a clean uninstall,
- # ignoring whatever switches are already in the registry string.
+ # Resolve the ProductCode and run our own clean uninstall -- never
+ # reuse the /I (repair) switch already in the registry string.
$productCode = $Entry.PSChildName
if ($productCode -notmatch '^\{[0-9A-Fa-f-]+\}$') {
if ($uninstallCommand -match '(\{[0-9A-Fa-f-]+\})') {
@@ -97,22 +69,20 @@
}
Write-Host "Uninstall exit code: $exitCode (attempt $attempt of $maxMsiAttempts)"
+ # 1618: another component's transaction still holds the Windows
+ # Installer mutex -- retry rather than fail.
if ($exitCode -ne 1618) {
break
}
-
- Write-Host "Windows Installer busy (1618) -- another of Citrix's own component installs likely still holds the mutex. Retrying in ${msiRetryDelaySeconds}s."
Start-Sleep -Seconds $msiRetryDelaySeconds
}
} else {
- # Non-MSI entry (e.g. TrolleyExpress.exe / CWAInstaller.exe): re-run
- # the vendor's own uninstaller with its documented silent switches.
+ # Non-MSI entry: re-run its own uninstaller with Citrix's documented
+ # silent switches.
$exePath = ""
if ($uninstallCommand -match '^\s*"([^"]+)"') {
- # Quoted path
$exePath = $matches[1]
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)') {
- # Unquoted path that may contain spaces (e.g. "C:\Program Files (x86)\...")
$exePath = $matches[1]
} else {
Write-Host "Could not parse uninstaller path from: $uninstallCommand"
@@ -130,8 +100,8 @@
Write-Host "Uninstall exit code: $exitCode"
}
- # Treat msiexec-style reboot-required codes as success, and 1605 (product
- # already removed, e.g. by another entry's cascade) as success too.
+ # 3010/1641: success pending reboot. 1605: already removed, e.g. by
+ # another entry's cascade uninstall. All count as success.
if ($exitCode -eq 3010 -or $exitCode -eq 1641 -or $exitCode -eq 1605) {
return 0
}
@@ -155,7 +125,6 @@
}
if ($entries.Count -eq 0) {
- # Already uninstalled (or never installed) -- nothing to do.
Write-Host "No entries found matching $softwareNameLike"
Exit 0
} |
Related issue: Resolves #50900
Summary
Adds a Windows Fleet-maintained app for Citrix Workspace App — LTSR (Long Term Service Release), sourced from winget's
Citrix.Workspace.LTSRpackage (separate from the Current ReleaseCitrix.Workspacepackage).Kept as its own catalog entry/slug (
citrix-workspace-ltsr/windows) rather than grouping with the existing macOS Homebrew "Citrix Workspace" entry, since that one tracks Current Release, not LTSR — same pattern as the existing Firefox / Firefox ESR split.Verification notes
.exeleaves a resident tray/self-service process running after install, soStart-Process -Waitnever returns (it waits on the whole process tree). The install script polls Programs and Features for the app's own registry entry instead of trusting the installer process's exit code.Citrix Workspace Inside, publisherCitrix Systems, Inc.— not theCitrix Workspace <marketing-version>naming community docs describe for older builds. ItsDisplayVersion(e.g.25.7.2000.9) is that component's own internal build number, independent of the outer installer package version (25.7.2000.2020) pulled from winget, with no documented mapping between the two. Added an existence-only version-check fallback incmd/maintained-apps/validate/windows.go, scoped to this app's catalog name and Citrix's publisher, mirroring the existing Google Chrome / Microsoft Office Click-to-Run exemptions.Citrix Workspaceand publisherCitrix Systems, Inc.(matching the generated exists query), so an unrelated app can't be picked up. Also confirmed by CI, contradicting older community docs: the "Citrix Workspace Inside" entry'sUninstallStringis a standard MSI reference (MsiExec.exe /I{ProductCode}), not theTrolleyExpress.exe/CWAInstaller.exebootstrapper uninstaller. The uninstall script resolves the ProductCode (registry key name, or the GUID in the UninstallString) and runs a cleanmsiexec /x {ProductCode} /qn /norestart, matching the existingfoxit-pdf-editor/4k-video-downloader-plusprecedent in this repo — with a fallback to re-running the entry's own exe if a future build isn't MSI-based. A missing uninstall entry is treated as already-uninstalled (exit 0), not a failure.Citrix Workspaceisn't scoped to LTSR specifically — Current Release registers under the same naming pattern. Not a problem today (no Windows Current Release FMA exists yet), but worth knowing if one is added later. Not adding a version-based discriminator for this, since LTSR vs. Current Release version numbers don't follow a documented, stable format difference (see PR discussion).citrix workspace ltsricon-map key rather than generating a new one.Checklist for submitter
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.Testing
cmd/maintained-apps/ ingester test suites, which pass with this app added.Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes