Repository navigation
Check osascript output instead of exit status - #42951
Conversation
Capture osascript output into a variable and compare it to "true" when checking if an app is running. Updated quit_application and quit_and_track_application to use app_running=$(osascript ...) and [[ "$app_running" != "true" ]] rather than relying on the command's exit status. This makes the running check more reliable across osascript behaviors and avoids depending on its exit code.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #42951 +/- ##
==========================================
- Coverage 66.89% 66.88% -0.01%
==========================================
Files 2571 2571
Lines 206307 206307
Branches 9242 9243 +1
==========================================
- Hits 137999 137997 -2
+ Misses 55757 55755 -2
- Partials 12551 12555 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Pull request overview
This PR improves the reliability of the “is the app running?” check in the generated Homebrew uninstall shell functions by validating osascript’s boolean output (true/false) instead of relying on its exit status.
Changes:
- Capture
osascriptoutput intoapp_runninginquit_applicationand return early unless it equals"true". - Apply the same output-based check in
quit_and_track_application, ensuring the tracking env var is set to0when the app isn’t running.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@allenhouchins Guessing you'll take responsibility for merging this and the resulting script regen? Or do we want to regen scripts pre-merge and run at least a subset of them through QA? |
|
@iansltx I will get this merged and monitor script regen and FMA validation. I manually tested a few apps before and after this script change and it works as expected now. |
…pers (#48639) The custom install script embedded pre-#42951/#43842 copies of quit_and_track_application and relaunch_application. The stale 'if ! osascript' check treats any non-erroring osascript call as "app is running" (osascript exits 0 whether it prints true or false), so the app was marked for relaunch on every install and launched after every patch, even from a fully-quit state. Replace both functions with the current scripts.go constants (output-based running check, launchctl-asuser relaunch, updated console-user guards) and regenerate the darwin manifest.
…pers (#49030) **Related issue:** Resolves #48639 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] QA'd all new/changed functionality manually ## Details The GitHub Desktop FMA uses a custom install script (`ee/maintained-apps/inputs/homebrew/scripts/github-desktop-install.sh`) that embeds its own copies of `quit_and_track_application` and `relaunch_application`. Those copies were frozen before two fixes landed in the generated helpers in `ee/maintained-apps/ingesters/homebrew/scripts.go`: - #42951 — check osascript **output** instead of exit status. `osascript -e '... is running'` exits 0 whether it prints `true` or `false`, so the stale `if ! osascript ...` guard never fired. The app was marked `APP_WAS_RUNNING=1` on **every** install with a GUI user logged in and relaunched after every patch — even from a fully-quit state. This is the root cause of #48639. - #43842 — relaunch via `launchctl asuser ... open -b` as the console user instead of `osascript ... to activate` (which is unreliable from a root context), plus the updated empty/root/loginwindow console-user guards. This PR replaces both embedded functions with the current scripts.go constants (verified byte-for-byte identical) and regenerates `ee/maintained-apps/outputs/github/darwin.json` via `go run ./cmd/maintained-apps -slug github`. The manifest diff is script-ref-only (`98ab6ed8` → `c91ea2b5`); version and uninstall script are unchanged. The other five custom scripts (Docker Desktop, OpenVPN Connect, Webex, Max, Pd) already carry the updated helpers — GitHub Desktop was the only one missed. ## Manual QA Tested the updated `quit_and_track_application` / `relaunch_application` functions on macOS against GitHub Desktop itself (`com.github.GitHubClient`): - **Fully quit (the bug scenario):** verified `is running` returns `false` and zero `GitHub Desktop.app` processes. Fixed functions set `APP_WAS_RUNNING=0` and the app stays closed. Running the old shipped check (`if ! osascript ...`) against the same state misclassifies the app as running (osascript exits 0 with output `false`) and would have relaunched it. - **Running:** quit succeeds, `APP_WAS_RUNNING=1`, app relaunches successfully afterward. - `bash -n` passes on the updated script. Note: hosts where the FMA was already added keep the baked `98ab6ed8` script until their instance refreshes the manifest. The by-design behavior "app running with dock icon but no visible window → relaunched with a window" is unchanged; window-aware relaunching would be a separate enhancement. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved GitHub Desktop installation behavior on macOS so the app is more reliably closed and reopened after install. * Better handles login/session edge cases, helping ensure the app relaunches in the correct user’s desktop session. * Reduces failed or missed relaunches when the installer is run with elevated permissions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
**Related issue:** Resolves #48638, resolves #48225 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] 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. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] QA'd all new/changed functionality manually ## Details Follow-up to #49030 (GitHub Desktop, #48639), which fixed one instance of this bug. An audit for the raw pattern found **15 more custom FMA scripts** carrying the same broken check that #42951 fixed in the generated helpers: gating on the **exit status** of `osascript -e 'application id "..." is running'`. osascript exits 0 whether it prints `true` or `false`, so a fully-quit app is misclassified as running whenever the bundle id resolves. Impact by script: - **Relaunch after every patch (user-visible — the filed bugs):** `zoom_install.sh` (#48638) and `google_chrome_install.sh` (#48225) set `*_WAS_RUNNING=true` unconditionally and reopen the app after `installer`, even when the user had nothing open. - **Broken check, no relaunch step (needless quit attempts, misleading logs):** install scripts for 1Password, Adobe CC, ExpressVPN, Grammarly, LogiTune, Microsoft Edge, P4V, Slack; uninstall scripts for Adobe CC, CleanMyMac, GPG Suite, Microsoft Word, P4V. Note `tell application id X to quit` against a not-running app can briefly launch it to deliver the quit event, so these aren't purely cosmetic either. The fix is the same one-line pattern everywhere, style-matched to each script (`local`/POSIX `[ ]`/top-level variants preserved): capture osascript output and compare it to `"true"`. No other behavior changed — this PR deliberately does not touch relaunch methods or console-user guards. Regenerated the 13 affected darwin manifests with `go run ./cmd/maintained-apps -slug <slug>`. All diffs are script-ref-only except `google-chrome/darwin.json`, which also picked up the legitimate upstream 150.0.7871.115 version bump during regeneration (the daily ingest cron would publish it tonight regardless). An unrelated `google-chrome/windows.json` winget bump was excluded. ## Manual QA Reproduced the bug live on macOS with the shipped Zoom script logic (ref `05e6a85c`) against a **fully-quit** Zoom (verified `is running` = `false`, zero processes): the exit-status check set `ZOOM_WAS_RUNNING=true` and the relaunch step launched Zoom — exactly the customer report, no background helpers needed. The corrected output-compare check on the same state correctly reported not running. Equivalent verification for the shared-helper variant was done against GitHub Desktop in #49030 (both the fully-quit and running→quit→relaunch paths). Verified for all 16 scripts: `bash -n` passes, no `if [!] osascript -e "application id ...` pattern remains anywhere under `inputs/homebrew/scripts/`, and every regenerated manifest ref carries the output-compare check. Remaining by-design behavior (unchanged): an app running with a dock icon but no visible window is genuinely running and will still be quit and relaunched; window-aware relaunching would be a separate enhancement. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved app detection before install/uninstall steps, reducing unnecessary quit attempts and making setup flows more reliable. * Updated several app install/uninstall workflows to better handle running apps, cleanup, and restart behavior. * Refined a few app package definitions to point to newer supported versions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
**Related issue:** Part of #54170 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] 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. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] Added/updated automated tests - [ ] QA'd all new/changed functionality manually `TestInstallScriptStopsWhenAppWontQuit` runs generated install scripts (app copy and pkg) in bash with `osascript`, `sudo` and friends stubbed. It covers an app that isn't running, one that quits, one that accepts the quit event but keeps running, and one whose quit is canceled. The last two must exit 1 without copying or installing anything. The test fails against `main` and passes with this change. I also ran each edited custom install script end to end with stubs and a refused quit. All of them exit 1 before replacing the app, except Harmony SASE (see below). Docker Desktop's helper was checked under its `set -euo pipefail` for both outcomes. Not run on a real Mac. ## AI **AI:** Claude Code (claude-opus-5-5) ## Summary When a macOS app won't quit (for example, the user cancels a prompt to save changes), the install script moves it aside anyway, copies the new version in, and exits 0. fleetd then deletes `$TMPDIR`, which takes the moved-aside bundle the app is still running from. Fleet reports the update as installed while the user is still on the old version (#54170). Two problems in `quit_and_track_application` caused this: - It confirmed the quit with `pgrep -f "$bundle_id"`, which never matches because a macOS app's command line is its executable path. So it reported success as soon as the quit event was accepted. - When the app was still running at the timeout, it only logged "did not quit", and the caller carried on. Changes: - The helper now polls `osascript … is running` after each quit attempt and returns 1 at the timeout. This matches the KiCad FMA (#54613). An `is running` error doesn't count as quit. - Generated install scripts call `quit_and_track_application '<id>' || exit 1` before replacing the app, for both app-copy and pkg installs. - Custom install scripts that copy the helper get the same fix. Callers stop the install, and Pd detaches its disk image first. Docker Desktop already stopped on failure (it runs under `set -e`) and only needed the `pgrep` fix. **Harmony SASE** keeps going: its pkg force-quits the app on its own, so stopping would block an update that works today. - The `new-fma` skill now says how custom install scripts should quit the app and that helper copies must be kept in sync. Behavior change: a forced patch whose app won't quit now fails, and it's retried on the next policy run instead of being reported as installed. The yaml-files and REST API docs describe when the install is *attempted* ("whether or not the app is open"), which is still accurate, so they're unchanged. Outputs aren't regenerated here: like #43842 and #42951, the FMA bot picks up the new script refs. The 18 frozen macOS apps keep their current scripts until they're unfrozen. Follow-ups (not in this PR): - 12 custom install scripts have their own `quit_application` helper with the same `pgrep` check and no stop on failure: 1Password, Adobe Creative Cloud, Box Tools, ExpressVPN, Google Chrome, Grammarly, Gyazo, Logi Tune, Microsoft Edge, P4V, Slack, Zoom. Each has its own flow, so they need a per-script look. - The uninstall helper `quit_application` has the same `pgrep` check. It's left unchanged here to avoid churning every uninstall script ref. - #53570 changes the `var_name` line of the same helper, in a separate hunk. The Wispr Flow custom script in #54708 copies the old helper and needs this fix after whichever merges second. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * On macOS, updates now stop if the app being replaced doesn’t quit within the timeout. The running app’s files are preserved, and the update is reported as failed rather than installed. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…53570) **Related issue:** NA — found while reviewing #53566 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`. - [x] 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. ## What & why `quit_and_track_application` and `relaunch_application` derive the name of the "was it running" flag from the bundle ID and interpolate it into an `eval`: ```sh local var_name="APP_WAS_RUNNING_$(echo "$bundle_id" | tr '.-' '__')" eval "export $var_name=1" ``` `tr '.-' '__'` maps only dots and dashes. Any other character survives into the variable name, and a space breaks the `eval` in two: - **Write:** `eval "export APP_WAS_RUNNING_org_mozilla_pale moon=1"` — `export` takes two arguments, so it sets `APP_WAS_RUNNING_org_mozilla_pale` and a stray `moon=1`. The intended variable is never set. - **Read:** `eval "was_running=$APP_WAS_RUNNING_org_mozilla_pale moon"` — assigns empty, then tries to run `moon` as a command, printing `moon: command not found` into the install output. - **Result:** `was_running` is empty, so `relaunch_application` returns early. **Pale Moon hits this today.** `pale-moon.json` has `"unique_identifier": "org.mozilla.pale moon"`, and the shipped script calls `quit_and_track_application 'org.mozilla.pale moon'`. The app is quit during an update and never reopened. It is the only one of the 967 shipping macOS FMAs affected — every other identifier is drawn from `[A-Za-z0-9._-]`. This replaces the derivation with bash parameter expansion that maps everything outside `[A-Za-z0-9_]`: ```sh local var_name="APP_WAS_RUNNING_${bundle_id//[^[:alnum:]_]/_}" ``` Also applied to the thirteen custom install scripts under `inputs/homebrew/scripts/` that carry their own copy of these functions, so the two don't drift. ### Reviewer notes - **Inert for every app but pale-moon.** For identifiers drawn from `[A-Za-z0-9._-]` the new mapping produces a byte-identical variable name to `tr '.-' '__'`. I compared old vs. new across all 967 homebrew `unique_identifier` values: 966 identical, 1 changed (pale-moon, the broken one). Runtime behavior for every other app is unchanged. - **Why parameter expansion instead of `tr -c '[:alnum:]_' '_'`.** Same result, but no subshell, and it sidesteps two hazards: `echo` appends a newline that `tr -c` would map to a trailing underscore, and BSD vs. GNU `tr` differences in CI. Verified on `/bin/bash` 3.2, the macOS runtime — generated scripts have used a bash shebang since #44836. - **No output regeneration here.** This changes a shared function body, so all 956 generated scripts containing it will shift by two lines when the nightly regenerates. That matches how prior changes to these same functions landed (#43842, #42951, #44836 were all generator-only). - **Not a security fix.** `unique_identifier` comes from Fleet's own input files, not the brew API, so this isn't the untrusted-input class #51324 addressed. Hardening the `eval` is still worthwhile because the name reaches a root-privileged `eval`, but the bug being fixed is the relaunch failure. ## Testing - [x] Added/updated automated tests `TestQuitRelaunchVarNameSurvivesEval` runs the real write/read round trip through bash for four shipping identifier shapes including `org.mozilla.pale moon`, and asserts the flag survives. `TestQuitRelaunchVarNameNeutralizesCommandSubstitution` asserts the derived name is a bare shell identifier and that a `$(touch ...)` payload doesn't execute. `TestQuitAndRelaunchDeriveVarNameIdentically` pins the two derivations to the same line so they can't drift, and `TestCustomInstallScriptsDoNotUseOldVarNameDerivation` fails if any custom install script still uses `tr '.-' '__'`. Confirmed these fail against the old derivation: under `set -u`, pale-moon errors with `APP_WAS_RUNNING_org_mozilla_pale: unbound variable`; without it, the read back is silently empty. - [x] QA'd all new/changed functionality manually `go test ./ee/maintained-apps/...` passes. `bash -n` clean on all thirteen patched custom scripts. `go vet` and `gofmt` clean. (`make lint-go-incremental` couldn't run locally — it needs a network clone of golangci-lint — so leaving that to CI.) ## AI **AI:** Claude Code (claude-opus-5) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed macOS app updates so Fleet-maintained apps with spaces or other special characters in their bundle identifiers can relaunch successfully. * Improved tracking of whether applications were running before installation for identifiers containing special characters. * **Tests** * Added coverage for application tracking with special-character identifiers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…one is logged in (#56457) **Related issue:** Combines #56016, #56314, #56333, #56336, #56337, #56453 and #56454 into one PR. Those PRs are closed in favor of this one. Follow-up to #55157 (#54170) and #54210. #56333 came up in review of #56085 (Logitech G HUB). ## Summary All seven change how macOS FMA scripts are generated or tested. Fleet runs these scripts as root at any time, including at the login window. ### Quit apps by bundle ID when the cask lists names (#56016) A few casks list app names in `uninstall quit:` instead of bundle IDs. `tell application id "DeskTime"` fails with -1728, so `quit_application` treated the app as not running and the script removed the bundle while the app was open. A dotless `quit:` value now becomes the app's `unique_identifier`, and repeated IDs in a stanza are quit once. A dotless value with `*` (such as `*WaveLink*`) is still a pattern. Affected: DeskTime, DisplayLink, NotesOllama and Fathom. ### Stop the uninstall when the app won't quit (#56314) The uninstall `quit_application` helper had the bug that #55157 fixed in the install helper. It confirmed the quit with `pgrep -f "$bundle_id"`, which never matches, and only logged "did not quit" before `sudo rm -rf` removed the bundle. - `quit_application` now polls `osascript … is running` after a 1s sleep, and returns 1 at the 10s timeout. `quit_matching_applications` returns 1 if any match didn't quit. - Generated scripts add `|| exit 1` only when the quit ID names the app being uninstalled: its `unique_identifier`, case-insensitively, or a wildcard that matches it. Helpers, shared updaters such as Microsoft AutoUpdate, and other vendors' apps keep Homebrew's warn-and-continue. A helper that launchd relaunches would otherwise block the uninstall for good. - With #56016, a name in `quit:` becomes the app's own ID first, so DeskTime, DisplayLink, NotesOllama and Fathom stop too. - The six custom uninstall scripts with stale copies of the helper now carry the current one: Box Tools, CleanMyMac, ForkLift, GPG Suite, Microsoft Word and P4V. **Behavior change:** if the app won't quit, the uninstall fails with "Application '<id>' did not quit." It no longer removes the app while the app is running. ### Quit the app when the cask doesn't (#56337) Generated uninstall scripts only quit the IDs in the cask's `uninstall quit:`. Most casks list none, and Excel, Outlook, PowerPoint and Teams quit only Microsoft AutoUpdate. So the app could be removed while it was still running. - The generator now writes `quit_application '<unique_identifier>' || exit 1` once, before anything removes the bundle. With an uninstall stanza, it goes at the stanza's quit step: after `launchctl` and the cask's own quits, and before `signal`, `early_script`, `script`, `pkgutil` and `delete`. Without one, it goes right before the app's `rm -rf`. A zap quit doesn't count, because zap runs after the app is removed. - It's left out when an uninstall stanza already quits or signals the app. 17 casks signal apps that don't quit cleanly, such as DBeaver, Parallels, Webex and Zoom for IT Admins. A quit that stops the script mustn't run ahead of the signal. Perimeter 81 splits its stanza across three artifacts, so every stanza is checked first. - A cask quit that lists the app's name counts as quitting the app, through #56016's mapping. So DeskTime, DisplayLink, Fathom and NotesOllama aren't quit twice. - Vendor `script` and `early_script` directives aren't exempt. A directive doesn't say whether it stops the app, and the install script already quits the same ID with `|| exit 1` on every update. - Some apps' `unique_identifier` differs from the cask's quit ID, such as KeePassXC, CleanShot X, Tower, JASP and Sequel Ace. They now also quit the `unique_identifier`, which is the ID Fleet sees installed. If LaunchServices doesn't know that ID, the helper returns 0. The new-fma skill's guidance for custom uninstall scripts moved to #56459. ### Trash cleanup with no GUI user (#56333) At the login window, `LOGGED_IN_USER` is `loginwindow`, so `trash()` tried to move every path into `/Users/loginwindow/.Trash` and failed. When the user was empty, the unquoted variable disappeared and the arguments shifted. Either way, system paths like `/Users/Shared/LGHUB` were left behind. - Generated calls quote the user: `trash "$LOGGED_IN_USER" '<path>'`. - With no real console user (empty, `root` or `loginwindow`), `~` paths are skipped with `Not logged into a non-root GUI; skipping <path>.` Other paths are deleted with `rm -rf`, and globs still expand through `compgen -G`. These are only the cask's app-specific `zap` entries. None of the absolute paths in the outputs is a shared parent folder. - Cleanup stays best-effort. A failed removal is logged, and `trash()` always returns 0. Before, the exit status depended on which path happened to be last. - Nine custom uninstall scripts carry the current `trash()` verbatim, with quoted call sites: Camtasia, CleanMyMac, Cycling '74 Max, ForkLift, GPG Suite, Homebrew, KiCad, Microsoft Word and Teleport. ### Script directives with no GUI user (#56336) Cask `script` and `early_script` directives were generated as `(cd /Users/$LOGGED_IN_USER && …)`. At the login window the `cd` failed, so vendor uninstallers never ran (Anka, Cloudflare WARP, Chrome Remote Desktop Host, Microsoft Defender, Santa, Mullvad VPN and 11 more). - Executable directives run as root, so they now start in the user's home when there is one and in `/` otherwise: `(cd "/Users/$LOGGED_IN_USER" 2>/dev/null || cd /; sudo '<exe>' …)`. `must_succeed: false` still appends `|| true`. - String directives run as the user, so they're skipped with the same "Not logged into a non-root GUI" message when there's no real user. Box Drive is the only output with one. - Box Drive's `pre_uninstall_scripts` run their per-user steps only when someone is logged in. With an empty user, the old sudoers line wasn't valid syntax. GPG Suite's custom uninstaller call uses the same `cd … || cd /` form. ### Quit 1Password with SIGTERM in the uninstall (#56453) With "Keep 1Password in the menu bar" on (the default), 1Password's Electron `before-quit` handler cancels a quit event and drops to the menu bar. Its `SIGTERM` handler quits it fully, the same as Quit in its menu bar menu. Without a custom script, the bot would regenerate 1Password's uninstall with `quit_application 'com.1password.1password' || exit 1`, and every uninstall with 1Password running would fail with "did not quit." - `1password_uninstall.sh` is this branch's generated uninstall with one change: `quit_application` is replaced by `quit_1password`. It sends `pkill -TERM -x 1Password`, polls `is running` for up to 10s, and returns 1 if 1Password is still running. The call is `quit_1password || exit 1`, right before `rm -rf`. - `quit_1password` is the install script's `quit_and_track_1password` (#56452) without the relaunch tracking. - The script was built from this branch's generator, so it already has the #56333 `trash()` and quoted call sites. ### Check every `quit_*` call in custom install scripts (#56454) `TestCustomInstallScriptsStopWhenAppWontQuit` only matched `quit_application` and `quit_and_track_application`. It now matches any `quit_*` call, so a script with its own quit function, like `quit_and_track_1password` (#56452), can't drop `|| exit 1` unnoticed. `qgis-ltr-install.sh` is allowlisted: its `quit_app_at_path` sends SIGTERM, then SIGKILL after 10 seconds. ## Merge order - #56459 (new-fma skill guidance, split out of #56337) should merge after this PR. - #55865 (`rmdir` with `~`) also changes `scripts.go` and many of the same outputs. Whichever merges second needs a rebuild of the overlapping outputs. ## Outputs 975 of 980 macOS outputs change. In each one, only the uninstall script and its ref change. Version, URL, sha256, install script and queries are byte-identical to `main`. - **Generated outputs:** they have the #56333 and #56336 changes, applied to the uninstall script that's on `main` today. The 116 outputs the bot bumped since #56333 was cut were rebuilt from `main`'s file the same way. LM Studio and Logitech G HUB were added after #56333 was cut, so they're new in this PR. The edit reproduces all 955 generated outputs in #56333 and #56336 byte for byte. - **#56314's quit change isn't applied to generated outputs.** That follows #56314 and the precedent of #55157, #43842 and #42951: the FMA bot picks up the new uninstall refs for the apps that quit something. For 35 of those apps, the bot also adds #56337's quit, for example Excel, Outlook, PowerPoint and Teams. - **#56337's quit is applied to 589 outputs.** None of them quits anything yet, and each already matched this branch's generator. So each gains only `quit_application '<unique_identifier>' || exit 1` and the helper. Eight are frozen apps, which the bot never rewrites: BrickLink Studio, Cisco Jabber, Comet, Mobirise, Twobird, Usage, Visual Paradigm and Whispering. Angry IP Scanner, InVesalius and KeyboardCleanTool weren't changed by the trash or directive edits, so they're new in this PR. - **Frozen outputs (14):** hand-edited with the trash and directive changes, since ingest never rewrites them. Logitech G HUB is newly frozen since #56333. As in #56314, frozen apps keep the old quit helper until they're unfrozen. Avast Secure Browser and Intune Company Portal also don't get #56337's quit until then. - **Custom-script outputs:** each uninstall script equals its input file. Microsoft Word was rebuilt on `main`'s newer version. 1Password's uninstall now comes from its new custom script (#56453). `go run ./cmd/maintained-apps --slug=1password/darwin` matches the committed output, and its install script is unchanged from `main`. - **DeskTime, DisplayLink, Fathom, NotesOllama:** #56016's regenerated quit line, plus the trash change. Generator cross-check: I ran the combined ingester for 20 apps, including the four from #56016, LM Studio, Logitech G HUB, Box Drive, Cloudflare WARP, Docker Desktop, the custom-script apps and a bot-bumped app. Every uninstall script is identical to this PR's, except for #56314's quit helpers and `|| exit 1` on the app's own quit, which the bot adds. #56337 cross-check: I generated all 964 non-custom macOS uninstall scripts from today's cask API with this branch's generator, before and after #56337. 624 change, each by one `quit_application '<unique_identifier>' || exit 1` line, plus the helper in the 587 that didn't have it. The next statement is always a removal: `sudo rm -rf` (553), `remove_pkg_files` (60) or the cask's uninstall script (8). DeskTime, DisplayLink, Fathom and NotesOllama don't change. For Asana, Google Chrome, Microsoft Defender and Obsidian, `go run ./cmd/maintained-apps` produces the same uninstall script as the committed output. # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - None. The combined PRs' changes files were removed. - [x] 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. - Cask-supplied paths, executables, args and string scripts still go through `shellSingleQuote`. The only new interpolation is the quoted `"$LOGGED_IN_USER"`. - [x] Timeouts are implemented and retries are limited to avoid infinite loops ## Testing - [x] Added/updated automated tests - `TestUninstallScriptStopsWhenAppWontQuit`, `TestQuitTargetsApp`, `TestCustomUninstallScriptsUseCurrentQuitHelper` (#56314) - `TestUninstallScriptQuitsAppByBundleIDWhenCaskListsNames` (#56016) - `TestUninstallScriptTrashWithoutGUIUser`, `TestCustomUninstallScriptsUseCurrentTrash` (#56333) - `TestUninstallScriptScriptDirectiveWithoutGUIUser` (#56336) - `TestCustomInstallScriptsStopWhenAppWontQuit` checks every `quit_*` call (#56454) - `TestUninstallScriptQuitsAppCaskDoesNotName`, `TestUninstallScriptQuitsAppOnce` (#56337), and a quit-by-name case in `TestUninstallScriptQuitsAppByBundleIDWhenCaskListsNames` that fails if the app is quit twice - 1Password's uninstall (#56453): I ran it with `/bin/bash` 3.2 and `osascript`, `pkill`, `sudo`, `launchctl`, `stat`, `scutil`, `mv` and `rm` stubbed. The `osascript` stub ignores quit events, as 1Password does in menu-bar mode. Not running: no `pkill`, the app is removed, exit 0. Quits on SIGTERM: one `pkill -TERM -x 1Password`, the app is removed, exit 0. Ignores SIGTERM: "did not quit", exit 1, nothing is removed. shellcheck reports the same warnings as the generated script. The SIGTERM behavior was checked on a real Mac for #56452. - `go test ./ee/maintained-apps/... ./cmd/maintained-apps/...` passes with Homebrew bash 5.3 and with `/bin/bash` 3.2 first on `PATH`. golangci-lint (full and incremental) reports 0 issues on those packages. Two incremental findings in #56314's tests now use `errors.AsType` and `require.Contains`, like the other tests. A gosec G602 false positive in #56337's order check is restructured. - [ ] QA'd all new/changed functionality manually ## AI **AI:** Claude Code (claude-opus-5-5) --------- Co-authored-by: Claude <noreply@anthropic.com>
Capture osascript output into a variable and compare it to "true" when checking if an app is running. Updated quit_application and quit_and_track_application to use app_running=$(osascript ...) and [[ "$app_running" != "true" ]] rather than relying on the command's exit status. This makes the running check more reliable across osascript behaviors and avoids depending on its exit code.