<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #50056 ## Summary macOS FMA install scripts never checked the exit code of the install command (`installer -pkg` / `cp -R`) — the script's last statement is always `relaunch_application`, which exits 0 — so a failed install exited 0 and Fleet reported it installed. **Generated scripts.** The generator now propagates failure: both `installer -pkg` variants end with `|| exit $?`, and the `cp -R` path exits non-zero on a failed copy, removes the partial copy (so a failed fresh install isn't inventoried as the new version), and restores the app it moved aside. Regenerated `outputs/` for non-frozen generated apps are produced by the `ingest-maintained-apps` job, so they aren't committed here. **Custom scripts.** 9 of the 18 custom input scripts had the same bug and are fixed with the same pattern: Google Chrome, Zoom, Microsoft Edge, GitHub Desktop, Webex, Cycling '74 Max, Pd, Grammarly Desktop, and P4V. The DMG-based ones also now fail before removing/moving the existing app when the mount or staging copy fails, so a bad download can't leave a host with nothing. Their `outputs/*/darwin.json` are updated in the same commit (script content + recomputed 8-char sha256 ref, versions untouched), following the precedent of #49033. Docker Desktop (`set -euo pipefail`), 1Password/Slack/LogiTune (installer is the last statement), and the rest already propagated errors. **Frozen apps.** The ingest job never rewrites frozen outputs, so the 10 frozen apps with generated scripts (adobe-acrobat-pro, comet, evernote, firealpaca, keeper-password-manager, nvidia-geforce-now, pritunl, vnc-viewer, wins, worksheet-crafter) had the fix applied directly to their published `darwin.json` scripts — the exact text the current generator would emit, with pinned versions/URLs/hashes untouched. The 11th frozen app (logi-options+) uses a custom script that was already correct and in sync. # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/` (`changes/50056-fma-install-scripts-ignore-errors`). - [x] Untrusted data interpolated into shell scripts is validated against shell metacharacters. (No new untrusted interpolation: the guard reuses the same curated cask-derived name the adjacent lines already interpolate.) ## Testing - [x] Added/updated automated tests (three generator tests: pkg, pkg-with-choices, cp-R restore — the last now pins the exact emitted block). - [x] All 19 updated output manifests validated: embedded scripts pass `bash -n`, refs match `sha256(script)[:8]`, refs map stays key-sorted like Go's encoder. - [x] QA'd all new/changed functionality manually. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * macOS Fleet-maintained app installations now fail fast when installers, DMG extraction/mounting, or app copy steps error. * If an upgrade fails, the system removes any partial app and restores the previously installed version when available. * Improved robustness during app staging/copying, including safer handling of paths with spaces or special characters. * **Tests** * Added unit coverage to verify installer failure propagation and rollback behavior. * **Documentation** * Clarified that the install-script error handling applies to both generated and custom scripts, including already-published frozen apps. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Allen Houchins <allenhouchins@mac.com>
139 lines
4.6 KiB
Bash
139 lines
4.6 KiB
Bash
#!/bin/bash
|
|
|
|
# variables
|
|
APPDIR="/Applications/"
|
|
TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")")
|
|
# functions
|
|
|
|
quit_and_track_application() {
|
|
local bundle_id="$1"
|
|
local var_name="APP_WAS_RUNNING_$(echo "$bundle_id" | tr '.-' '__')"
|
|
local timeout_duration=10
|
|
|
|
# check if the application is running
|
|
local app_running
|
|
app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
|
|
if [[ "$app_running" != "true" ]]; then
|
|
eval "export $var_name=0"
|
|
return
|
|
fi
|
|
|
|
local console_user
|
|
console_user=$(stat -f "%Su" /dev/console)
|
|
if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
|
|
echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
|
|
eval "export $var_name=0"
|
|
return
|
|
fi
|
|
|
|
# App was running, mark it for relaunch
|
|
eval "export $var_name=1"
|
|
echo "Application '$bundle_id' was running; will relaunch after installation."
|
|
|
|
echo "Quitting application '$bundle_id'..."
|
|
|
|
# try to quit the application within the timeout period
|
|
local quit_success=false
|
|
SECONDS=0
|
|
while (( SECONDS < timeout_duration )); do
|
|
if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then
|
|
if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then
|
|
echo "Application '$bundle_id' quit successfully."
|
|
quit_success=true
|
|
break
|
|
fi
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [[ "$quit_success" = false ]]; then
|
|
echo "Application '$bundle_id' did not quit."
|
|
fi
|
|
}
|
|
|
|
|
|
relaunch_application() {
|
|
local bundle_id="$1"
|
|
local var_name="APP_WAS_RUNNING_$(echo "$bundle_id" | tr '.-' '__')"
|
|
local was_running
|
|
|
|
# Check if the app was running before installation
|
|
eval "was_running=\$$var_name"
|
|
if [[ "$was_running" != "1" ]]; then
|
|
return
|
|
fi
|
|
|
|
local console_user
|
|
console_user=$(stat -f "%Su" /dev/console)
|
|
if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
|
|
echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
|
|
return
|
|
fi
|
|
|
|
echo "Relaunching application '$bundle_id'..."
|
|
|
|
# Launch the app in the logged-in user's GUI session. Apps launched by root
|
|
# won't register with the user's Dock/GUI, so run 'open' as the console user.
|
|
# Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
|
|
# and GUI session — 'sudo -u' alone doesn't do this, which can cause
|
|
# LSOpenURLsWithRole() failures even when 'open' exits 0.
|
|
local open_status=0
|
|
if [[ $EUID -eq 0 ]]; then
|
|
local console_uid
|
|
console_uid=$(id -u "$console_user")
|
|
/bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
|
|
else
|
|
open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
|
|
fi
|
|
|
|
if [[ $open_status -eq 0 ]]; then
|
|
echo "Application '$bundle_id' relaunched successfully."
|
|
else
|
|
echo "Failed to relaunch application '$bundle_id'."
|
|
fi
|
|
}
|
|
|
|
|
|
remove_stale_upgrade_bundles() {
|
|
# Webex's own auto-updater stages downloaded updates under
|
|
# "Cisco Spark/Webexteams_upgrades_*" (one dir per architecture) and leaves
|
|
# older, fully formed Webex.app bundles behind after applying them. osquery's
|
|
# apps table indexes those staged bundles by their (old) bundle_short_version
|
|
# and bundle_identifier ('Cisco-Systems.Spark'), so version-based patch
|
|
# policies keep reporting the host as out of date even after the app in
|
|
# /Applications has been updated. Removing them is safe: they are cached,
|
|
# already-applied updates that Webex re-downloads as needed, and Webex only
|
|
# ever stages versions at or newer than what is installed.
|
|
local home
|
|
for home in /Users/*; do
|
|
[ -d "$home" ] || continue
|
|
rm -rf "$home/Library/Application Support/Cisco Spark/Webexteams_upgrades_"*
|
|
done
|
|
}
|
|
|
|
|
|
# extract contents
|
|
MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)
|
|
yes | hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" || exit 1
|
|
if ! sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"; then
|
|
hdiutil detach "$MOUNT_POINT" || true
|
|
exit 1
|
|
fi
|
|
hdiutil detach "$MOUNT_POINT" || true
|
|
# copy to the applications folder
|
|
quit_and_track_application 'Cisco-Systems.Spark'
|
|
if [ -d "$APPDIR/Webex.app" ]; then
|
|
sudo mv "$APPDIR/Webex.app" "$TMPDIR/Webex.app.bkp" || exit $?
|
|
fi
|
|
if ! sudo cp -R "$TMPDIR/Webex.app" "$APPDIR"; then
|
|
# remove the partial copy so a failed install isn't inventoried as the new
|
|
# version, then restore the previous version if there was one
|
|
sudo rm -rf "$APPDIR/Webex.app"
|
|
if [ -d "$TMPDIR/Webex.app.bkp" ]; then
|
|
sudo mv "$TMPDIR/Webex.app.bkp" "$APPDIR/Webex.app"
|
|
fi
|
|
exit 1
|
|
fi
|
|
remove_stale_upgrade_bundles
|
|
relaunch_application 'Cisco-Systems.Spark'
|