Skip to content

Treat MSI reboot-required exit codes as success and quote the msiexec log path - #50407

Merged
allenhouchins merged 3 commits into
mainfrom
msi-install-reboot-required-exit-codes
Aug 3, 2026
Merged

Treat MSI reboot-required exit codes as success and quote the msiexec log path#50407
allenhouchins merged 3 commits into
mainfrom
msi-install-reboot-required-exit-codes

Conversation

@allenhouchins

@allenhouchins allenhouchins commented Aug 3, 2026

Copy link
Copy Markdown
Member

Related issue: NA

What & why

Two independent defects in the default MSI scripts. Neither has any server-side handling, so the script text is the only place either can be fixed.

1. Reboot-required exit codes reported as install failures

The default MSI install script (pkg/file/scripts/install_msi.ps1) ended with Exit $installProcess.ExitCode, passing msiexec's raw exit code straight through. An install that succeeded but requested a reboot therefore reported as a failed install:

  • 3010ERROR_SUCCESS_REBOOT_REQUIRED
  • 1641ERROR_SUCCESS_REBOOT_INITIATED

Both default MSI uninstall scripts (uninstall_msi.ps1 and uninstall_msi_with_upgrade_code.ps1) already carve these out via $successCodes = @(0, 3010, 1641) — install was the only MSI script missing it. This change adds the same check, using the identical idiom and comment wording as the uninstall scripts.

2. Unquoted log file path in the /lv argument

The default MSI install and remove scripts passed the log path unquoted:

-ArgumentList "/quiet /norestart /lv ${logFile} /i `"${env:INSTALLER_PATH}`""

Start-Process appends a single-string -ArgumentList to the command line verbatim — it adds no quoting of its own. ${env:INSTALLER_PATH} was already protected by escaped quotes; ${logFile} was not. So when TEMP contains a space, msiexec tokenizes the path on whitespace: /lv receives only the chunk up to the first space (C:\Users\John), and the remainder (Smith\AppData\...\fleet-install-software.log) is left as a stray token, which msiexec rejects as an invalid command line (1639). The install fails outright rather than merely writing its log somewhere unexpected.

The fix quotes it the way ${env:INSTALLER_PATH} already was:

-ArgumentList "/quiet /norestart /lv `"${logFile}`" /i `"${env:INSTALLER_PATH}`""

On severity: this is latent under normal fleetd operation. Install scripts inherit os.Environ() from orbit (orbit/pkg/installer/installer.go), which runs as a LocalSystem service, so TEMP is C:\Windows\TEMP — no spaces. It bites when the system TEMP is redirected to a path containing a space, or when an admin copies the script (Fleet renders it in the UI) and runs it in a user context whose profile name contains a space. Not reproduced on a Windows host; the analysis is from msiexec's whitespace tokenizing, not from an observed failure.

The newer hand-written FMA scripts (mozilla-vpn_install.ps1, egnyte_install.ps1, vnc-server_install.ps1, vnc-viewer_install.ps1, agent-ransack_install.ps1) already used the quoted form. This brings the older ones in line with them.

Scope

GetInstallScript("msi") feeds two paths, both fixed by change 1:

  1. The default install script for user-uploaded MSI packages (ee/server/service/software_installers.go).
  2. The generated install script for MSI-based Fleet-maintained apps (ee/maintained-apps/ingesters/winget/ingester.go).

Change 2 additionally covers remove_msi.ps1 (the uninstall script used for packages added before the uninstall feature shipped) and the nine hand-written winget install scripts that still carried the unquoted form: azure-functions-core-tools, bluej, crisisgo, delinea-connection-manager, geogebra-classic, google-ads-editor, gotomeeting, imageglass, sourcetree.

Notes:

  • FMA outputs are not regenerated here. install_script_ref is content-addressed, and existing outputs/*/windows.json files carry both the ref and the script text, so they stay internally consistent. The ingest workflow runs every 4 hours and will roll the refs for MSI-based apps forward on its own. Regenerating them in this PR would produce a huge diff and trigger Windows FMA validation for every MSI app.
  • Several per-app FMA install scripts exist only to add the exit-code carve-out and become redundant once this lands (for example scribe_install.ps1 from Add Scribe as a Windows Fleet-maintained app #50341). They are harmless duplicates of the new default and can be removed in follow-up. Per-app scripts that do other work too (e.g. delinea-connection-manager_install.ps1 forcing ALLUSERS=1) still need to keep their own copy — those got the quoting fix instead.
  • Neither change applies to uninstall_msi.ps1 or uninstall_msi_with_upgrade_code.ps1: they already handle the reboot codes, and they build -ArgumentList as an array with no /lv argument at all.
  • install_exe.ps1 deliberately left alone — EXE installers have no standard exit-code convention, which is why they use per-app scripts.
  • The per-app example scripts embedded in articles/ (CrowdStrike, Cloudflare WARP, SentinelOne) are separate copy-paste content and are not touched.

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

    No changes file is currently in this PR — the earlier one was removed. Both fixes change user-visible install/uninstall outcomes, so one may be warranted before merge.

  • 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.

    Change 2 is precisely this: a path interpolated into a command line is now quoted so whitespace can't split it into extra arguments.

Testing

  • Added/updated automated tests

pkg/file's golden test (TestGetInstallAndRemoveScript) covers the script contents; each script and its golden were changed in lockstep, so they remain byte-identical. go test ./pkg/file/ -run Script and go test ./ee/server/service/ -run TestGetInstallScript pass. Goldens can be regenerated with go test ./pkg/file/... -update.

  • QA'd all new/changed functionality manually

Not QA'd on a Windows host. Change 1 needs an MSI that returns 3010 under Fleet's SYSTEM context to confirm the install now reports success. Change 2 needs an MSI install run with TEMP pointed at a path containing a space.

Summary by CodeRabbit

  • Bug Fixes
    • Improved MSI installation and removal reliability when log-file paths contain spaces.
    • MSI installations requiring a restart are now recognized as successful.
    • Standard MSI success and restart-required results are handled consistently while other errors remain available for troubleshooting.
    • Updated supported application installers to use the more reliable logging behavior.

…ript

The default MSI install script exited with msiexec's raw exit code, so an
install that succeeded but requested a reboot (3010 = ERROR_SUCCESS_REBOOT_REQUIRED,
1641 = ERROR_SUCCESS_REBOOT_INITIATED) was reported as a failed install. Both
default MSI uninstall scripts already treat these codes as success, so install
was the outlier.

Add the same $successCodes carve-out used by the uninstall scripts. This covers
both user-uploaded MSI packages and the install script generated for MSI-based
Fleet-maintained apps, removing the need for per-app scripts that exist only to
add this check.
Copilot AI review requested due to automatic review settings August 3, 2026 01:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

This PR updates Fleet’s default Windows MSI install PowerShell script to treat “successful but reboot required” MSI exit codes (3010, 1641) as success, matching the behavior already present in the default MSI uninstall scripts. This prevents installs that completed correctly (but requested/initiated a reboot) from being reported as failures.

Changes:

  • Add $successCodes = @(0, 3010, 1641) to the default MSI install script and exit 0 when msiexec returns a reboot-required success code.
  • Update the corresponding golden test fixture to reflect the new script contents.
  • Add a changes entry (content excluded from diff by policy).

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

File Description
pkg/file/scripts/install_msi.ps1 Treat MSI reboot-required “success” exit codes (3010/1641) as success by exiting 0 instead of propagating the raw msiexec exit code.
pkg/file/testdata/scripts/install_msi.ps1.golden Update golden script output to match the new install script behavior and comments.
changes/msi-install-reboot-required-exit-codes Release note entry (not reviewable here due to content exclusion policy).
Files excluded by content exclusion policy (1)
  • changes/msi-install-reboot-required-exit-codes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

MSI installation scripts now treat exit codes 0, 3010, and 1641 as successful and return process exit code 0. Other exit codes remain unchanged. MSI installation and removal commands now quote temporary log-file paths passed with /lv. Golden test scripts reflect these changes.

Possibly related PRs

  • fleetdm/fleet#50341: Modifies PowerShell MSI logging paths and reboot-related exit-code handling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes both primary changes: MSI reboot-required exit codes now succeed and msiexec log paths are quoted.
Description check ✅ Passed The description is detailed and covers the rationale, scope, testing, limitations, and unchecked changes-file and manual-QA items.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch msi-install-reboot-required-exit-codes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.20%. Comparing base (da1947c) to head (042a191).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #50407      +/-   ##
==========================================
- Coverage   68.20%   68.20%   -0.01%     
==========================================
  Files        3941     3941              
  Lines      251274   251274              
  Branches    13442    13442              
==========================================
- Hits       171389   171384       -5     
- Misses      64533    64538       +5     
  Partials    15352    15352              
Flag Coverage Δ
backend 69.49% <ø> (-0.01%) ⬇️

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

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

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

Copilot AI review requested due to automatic review settings August 3, 2026 04:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

The /lv argument passed the log path unquoted. Start-Process appends a
single-string -ArgumentList to the command line verbatim, so when TEMP
contains a space msiexec tokenizes the path: /lv gets the chunk up to the
first space and the remainder is left as a stray token, which msiexec
rejects as an invalid command line. The install/uninstall fails outright
rather than just misplacing the log.

Quote it the same way ${env:INSTALLER_PATH} already is in the same string.
Covers the embedded install/remove MSI scripts (and their goldens) plus the
nine hand-written winget install scripts that predate the quoted form used
by the newer ones. The inline copies in outputs/*/windows.json pick this up
on the next regeneration.
Copilot AI review requested due to automatic review settings August 3, 2026 05:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ee/maintained-apps/inputs/winget/scripts/bluej_install.ps1 (1)

10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the required PowerShell file encoding.

PSScriptAnalyzer reports PSUseBOMForUnicodeEncodedFile for this file. If Windows PowerShell encoding compatibility is required, save bluej_install.ps1 as UTF-8 with BOM, or configure the repository to permit the current encoding.

🤖 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/bluej_install.ps1` at line 10, Save
bluej_install.ps1 using UTF-8 with a BOM to satisfy the
PSUseBOMForUnicodeEncodedFile requirement and preserve Windows PowerShell
compatibility; do not alter the installer argument logic.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@ee/maintained-apps/inputs/winget/scripts/bluej_install.ps1`:
- Line 10: Save bluej_install.ps1 using UTF-8 with a BOM to satisfy the
PSUseBOMForUnicodeEncodedFile requirement and preserve Windows PowerShell
compatibility; do not alter the installer argument logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2359610f-c38f-4287-bb95-66ade21fdff6

📥 Commits

Reviewing files that changed from the base of the PR and between 49e7f54 and 042a191.

📒 Files selected for processing (13)
  • ee/maintained-apps/inputs/winget/scripts/azure-functions-core-tools_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/bluej_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/crisisgo_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/delinea-connection-manager_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/geogebra-classic_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/google-ads-editor_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/gotomeeting_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/imageglass_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/sourcetree_install.ps1
  • pkg/file/scripts/install_msi.ps1
  • pkg/file/scripts/remove_msi.ps1
  • pkg/file/testdata/scripts/install_msi.ps1.golden
  • pkg/file/testdata/scripts/remove_msi.ps1.golden
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/file/testdata/scripts/install_msi.ps1.golden
  • pkg/file/scripts/install_msi.ps1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

ee/maintained-apps/inputs/winget/scripts/sourcetree_install.ps1:7

  • This per-app MSI install script still exits with msiexec’s raw exit code. If the installer succeeds but returns 3010 (reboot required) or 1641 (reboot initiated), Fleet will treat it as a failed install. Consider using the same $successCodes carve-out as the default MSI install script so reboot-required installs report success.
$installProcess = Start-Process msiexec.exe `
  -ArgumentList "/quiet /norestart /lv `"${logFile}`" /i `"${env:INSTALLER_PATH}`" ACCEPTEULA=1" `
  -PassThru -Verb RunAs -Wait

ee/maintained-apps/inputs/winget/scripts/gotomeeting_install.ps1:12

  • This per-app MSI install script still exits with msiexec’s raw exit code. If the installer succeeds but returns 3010 (reboot required) or 1641 (reboot initiated), Fleet will treat it as a failed install. Consider using the same $successCodes carve-out as the default MSI install script so reboot-required installs report success.
$installProcess = Start-Process msiexec.exe `
  -ArgumentList "/quiet /norestart /lv `"${logFile}`" /i `"${env:INSTALLER_PATH}`" G2MINSTALLFORALLUSERS=1" `
  -PassThru -Verb RunAs -Wait

ee/maintained-apps/inputs/winget/scripts/bluej_install.ps1:11

  • This per-app MSI install script still exits with msiexec’s raw exit code. If the installer succeeds but returns 3010 (reboot required) or 1641 (reboot initiated), Fleet will treat it as a failed install. Consider using the same $successCodes carve-out as the default MSI install script so reboot-required installs report success.
$installProcess = Start-Process msiexec.exe `
  -ArgumentList "/quiet /norestart /lv `"${logFile}`" /i `"${env:INSTALLER_PATH}`" ALLUSERS=2" `
  -PassThru -Verb RunAs -Wait

pkg/file/scripts/remove_msi.ps1:5

  • This MSI remove script still exits with msiexec’s raw exit code. Like the other MSI uninstall scripts, it should treat 3010 (reboot required) and 1641 (reboot initiated) as successful outcomes; otherwise a successful uninstall that requests reboot will be reported as a failure.
$removeProcess = Start-Process msiexec.exe `
  -ArgumentList "/quiet /norestart /lv `"${logFile}`" /x `"${env:INSTALLER_PATH}`"" `
  -PassThru -Verb RunAs -Wait

@allenhouchins allenhouchins changed the title Treat MSI reboot-required exit codes as success in default install script Treat MSI reboot-required exit codes as success and quote the msiexec log path Aug 3, 2026

@tux234 tux234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Took a look and it looks good to me. Good find!

@allenhouchins
allenhouchins merged commit 5104432 into main Aug 3, 2026
41 checks passed
@allenhouchins
allenhouchins deleted the msi-install-reboot-required-exit-codes branch August 3, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants