Created
February 21, 2026 14:23
-
-
Save alexfazio/22c703c947d0698cc8339b950fcf3a28 to your computer and use it in GitHub Desktop.
fix-setapp.sh — Automated recovery for SetappAgent v3.49.x crash on macOS Sequoia (kills apps, stabilizes agent, relaunches)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # fix-setapp.sh — Kill Setapp apps, wait for stable agent, relaunch apps. | |
| # | |
| # Usage: ./fix-setapp.sh | |
| # | |
| # Based on diagnosis in setapp-intermittent-launch-failure.md | |
| # SetappAgent v3.49.10 crashes within ~60s of startup (SIGSEGV). | |
| # This script automates the recovery: kill apps → stabilize agent → relaunch. | |
| set -euo pipefail | |
| readonly SETAPP_DIR="/Applications/Setapp" | |
| readonly AGENT_LABEL="com.setapp.DesktopClient.SetappAgent" | |
| readonly STABILITY_THRESHOLD=90 # seconds agent must survive | |
| readonly POLL_INTERVAL=5 # seconds between stability checks | |
| readonly TIMEOUT=300 # 5 minutes max wait | |
| readonly GRACE_PERIOD=5 # seconds before force-killing apps | |
| # --- Helpers ---------------------------------------------------------------- | |
| log() { | |
| local ts | |
| ts=$(date +%H:%M:%S) || true | |
| printf '[%s] %s\n' "${ts}" "$*" | |
| } | |
| err() { log "ERROR: $*" >&2; } | |
| get_agent_pid() { | |
| pgrep -f 'SetappAgent' 2>/dev/null | head -1 || true | |
| } | |
| get_pid_uptime_seconds() { | |
| local pid="$1" | |
| # ps etime format: [[dd-]hh:]mm:ss | |
| local etime | |
| etime=$(ps -o etime= -p "${pid}" 2>/dev/null | xargs) || return 1 | |
| [[ -z "${etime}" ]] && return 1 | |
| local days=0 hours=0 mins=0 secs=0 | |
| if [[ "${etime}" == *-* ]]; then | |
| days="${etime%%-*}" | |
| etime="${etime#*-}" | |
| fi | |
| # Split remaining by colons | |
| IFS=':' read -ra parts <<<"${etime}" | |
| case ${#parts[@]} in | |
| 3) | |
| hours="${parts[0]}" | |
| mins="${parts[1]}" | |
| secs="${parts[2]}" | |
| ;; | |
| 2) | |
| mins="${parts[0]}" | |
| secs="${parts[1]}" | |
| ;; | |
| 1) secs="${parts[0]}" ;; | |
| *) return 1 ;; | |
| esac | |
| # Strip leading zeros to avoid octal interpretation | |
| days=$((10#${days})) | |
| hours=$((10#${hours})) | |
| mins=$((10#${mins})) | |
| secs=$((10#${secs})) | |
| echo $((days * 86400 + hours * 3600 + mins * 60 + secs)) | |
| } | |
| # --- Step 1: Discover Setapp apps to manage --------------------------------- | |
| discover_running_setapp_apps() { | |
| if [[ ! -d "${SETAPP_DIR}" ]]; then | |
| err "Setapp directory not found: ${SETAPP_DIR}" | |
| exit 1 | |
| fi | |
| local -a apps=() | |
| for app_path in "${SETAPP_DIR}"/*.app; do | |
| [[ -d "${app_path}" ]] || continue | |
| if pgrep -f "${app_path}" >/dev/null 2>&1; then | |
| apps+=("${app_path}") | |
| fi | |
| done | |
| [[ ${#apps[@]} -gt 0 ]] && printf '%s\n' "${apps[@]}" | |
| } | |
| discover_login_setapp_apps() { | |
| # Query macOS Login Items via System Events, cross-reference with Setapp dir | |
| local login_items | |
| login_items=$(osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null) || return 0 | |
| [[ -z "${login_items}" ]] && return 0 | |
| local -a apps=() | |
| IFS=',' read -ra items <<<"${login_items}" | |
| for item in "${items[@]}"; do | |
| item=$(echo "${item}" | xargs) # trim whitespace | |
| for app_path in "${SETAPP_DIR}"/*.app; do | |
| [[ -d "${app_path}" ]] || continue | |
| local app_name | |
| app_name=$(basename "${app_path}" .app) | |
| if [[ "${app_name}" == "${item}" ]]; then | |
| apps+=("${app_path}") | |
| break | |
| fi | |
| done | |
| done | |
| [[ ${#apps[@]} -gt 0 ]] && printf '%s\n' "${apps[@]}" | |
| } | |
| # Merge two newline-delimited lists, deduplicate, preserve order | |
| union_lists() { | |
| local list1="$1" list2="$2" | |
| printf '%s\n%s\n' "${list1}" "${list2}" | awk 'NF && !seen[$0]++' | |
| } | |
| # --- Step 2: Kill Setapp apps ----------------------------------------------- | |
| kill_setapp_apps() { | |
| local -a app_paths=() | |
| while IFS= read -r line; do | |
| [[ -n "$line" ]] && app_paths+=("$line") | |
| done | |
| if [[ ${#app_paths[@]} -eq 0 ]]; then | |
| log "No running Setapp apps found to kill." | |
| return | |
| fi | |
| log "Killing ${#app_paths[@]} Setapp app(s)..." | |
| # Graceful quit via osascript | |
| for app_path in "${app_paths[@]}"; do | |
| local app_name | |
| app_name=$(basename "$app_path" .app) | |
| log " Quitting: $app_name" | |
| osascript -e "tell application \"$app_name\" to quit" 2>/dev/null || true | |
| done | |
| log "Waiting ${GRACE_PERIOD}s for graceful shutdown..." | |
| sleep "$GRACE_PERIOD" | |
| # Force-kill survivors | |
| local killed_any=false | |
| for app_path in "${app_paths[@]}"; do | |
| local app_name | |
| app_name=$(basename "$app_path" .app) | |
| local pids | |
| pids=$(pgrep -f "$app_path" 2>/dev/null || true) | |
| if [[ -n "$pids" ]]; then | |
| log " Force-killing: $app_name (PIDs: $(echo "$pids" | tr '\n' ' '))" | |
| echo "$pids" | xargs kill -9 2>/dev/null || true | |
| killed_any=true | |
| fi | |
| done | |
| if [[ "$killed_any" == true ]]; then | |
| log "Force-killed lingering processes." | |
| else | |
| log "All apps quit gracefully." | |
| fi | |
| } | |
| # --- Step 3: Stabilize SetappAgent ------------------------------------------ | |
| wait_for_stable_agent() { | |
| local agent_pid | |
| agent_pid=$(get_agent_pid) | |
| # Check if already stable | |
| if [[ -n "$agent_pid" ]]; then | |
| local uptime | |
| uptime=$(get_pid_uptime_seconds "$agent_pid" 2>/dev/null) || uptime=0 | |
| if [[ "$uptime" -ge "$STABILITY_THRESHOLD" ]]; then | |
| log "SetappAgent (PID $agent_pid) already stable — uptime ${uptime}s >= ${STABILITY_THRESHOLD}s." | |
| return 0 | |
| fi | |
| log "SetappAgent (PID $agent_pid) running but only ${uptime}s old. Monitoring..." | |
| else | |
| log "SetappAgent not running. Kickstarting..." | |
| launchctl kickstart -k "gui/$(id -u)/$AGENT_LABEL" 2>/dev/null || true | |
| sleep 2 | |
| fi | |
| # Poll until stable or timeout | |
| local elapsed=0 | |
| local last_pid="" | |
| local stable_since="" | |
| while [[ "$elapsed" -lt "$TIMEOUT" ]]; do | |
| agent_pid=$(get_agent_pid) | |
| if [[ -z "$agent_pid" ]]; then | |
| log " Agent not running (${elapsed}s elapsed). Kickstarting..." | |
| launchctl kickstart -k "gui/$(id -u)/$AGENT_LABEL" 2>/dev/null || true | |
| last_pid="" | |
| stable_since="" | |
| sleep "$POLL_INTERVAL" | |
| elapsed=$((elapsed + POLL_INTERVAL)) | |
| continue | |
| fi | |
| # PID changed — agent restarted (crashed) | |
| if [[ "$agent_pid" != "$last_pid" ]]; then | |
| if [[ -n "$last_pid" ]]; then | |
| log " Agent PID changed ($last_pid -> $agent_pid) — crash detected (${elapsed}s elapsed)." | |
| fi | |
| last_pid="$agent_pid" | |
| stable_since="$elapsed" | |
| fi | |
| local alive_for=$((elapsed - stable_since)) | |
| local remaining=$((STABILITY_THRESHOLD - alive_for)) | |
| if [[ "$alive_for" -ge "$STABILITY_THRESHOLD" ]]; then | |
| log "SetappAgent (PID $agent_pid) stable for ${alive_for}s. Proceeding." | |
| return 0 | |
| fi | |
| log " Waiting for agent stability... PID $agent_pid alive ${alive_for}s/${STABILITY_THRESHOLD}s (timeout in $((TIMEOUT - elapsed))s)" | |
| sleep "$POLL_INTERVAL" | |
| elapsed=$((elapsed + POLL_INTERVAL)) | |
| done | |
| err "SetappAgent failed to stabilize after ${TIMEOUT}s." | |
| err "The agent may have a persistent crash bug." | |
| err "See: setapp-intermittent-launch-failure.md → Fix Path" | |
| return 1 | |
| } | |
| # --- Step 4: Relaunch apps -------------------------------------------------- | |
| relaunch_apps() { | |
| local -a app_paths=() | |
| while IFS= read -r line; do | |
| [[ -n "$line" ]] && app_paths+=("$line") | |
| done | |
| if [[ ${#app_paths[@]} -eq 0 ]]; then | |
| log "No apps to relaunch." | |
| return | |
| fi | |
| log "Relaunching ${#app_paths[@]} app(s)..." | |
| for app_path in "${app_paths[@]}"; do | |
| local app_name | |
| app_name=$(basename "$app_path" .app) | |
| log " Opening: $app_name" | |
| open "$app_path" | |
| sleep 1 # stagger launches slightly | |
| done | |
| log "All apps relaunched." | |
| } | |
| # --- Main -------------------------------------------------------------------- | |
| main() { | |
| log "=== fix-setapp: Setapp recovery script ===" | |
| # Step 1a: Discover currently running Setapp apps | |
| log "Discovering running Setapp apps..." | |
| local running_apps | |
| running_apps=$(discover_running_setapp_apps) || true | |
| if [[ -n "${running_apps}" ]]; then | |
| local count | |
| count=$(echo "${running_apps}" | wc -l | xargs) | |
| log "Found ${count} running Setapp app(s):" | |
| while IFS= read -r app; do | |
| log " - $(basename "${app}" .app)" | |
| done <<<"${running_apps}" | |
| else | |
| log "No running Setapp apps found." | |
| fi | |
| # Step 1b: Discover Setapp apps registered as Login Items | |
| log "Discovering Setapp login items..." | |
| local login_apps | |
| login_apps=$(discover_login_setapp_apps) || true | |
| if [[ -n "${login_apps}" ]]; then | |
| local lcount | |
| lcount=$(echo "${login_apps}" | wc -l | xargs) | |
| log "Found ${lcount} Setapp login item(s):" | |
| while IFS= read -r app; do | |
| log " - $(basename "${app}" .app)" | |
| done <<<"${login_apps}" | |
| else | |
| log "No Setapp login items found." | |
| fi | |
| # Step 1c: Compute union (running + login items, deduplicated) | |
| local all_apps | |
| all_apps=$(union_lists "${running_apps:-}" "${login_apps:-}") | |
| if [[ -n "${all_apps}" ]]; then | |
| local total | |
| total=$(echo "${all_apps}" | wc -l | xargs) | |
| log "Total apps to manage: ${total}" | |
| else | |
| log "No Setapp apps to manage." | |
| fi | |
| # Step 2: Kill running apps (only ones actually running) | |
| if [[ -n "${running_apps}" ]]; then | |
| kill_setapp_apps <<<"${running_apps}" | |
| fi | |
| # Step 3: Wait for stable agent | |
| if ! wait_for_stable_agent; then | |
| err "Aborting — agent is not stable. Apps will not be relaunched." | |
| exit 1 | |
| fi | |
| # Step 4: Relaunch all apps (running + login items) | |
| if [[ -n "${all_apps}" ]]; then | |
| relaunch_apps <<<"${all_apps}" | |
| fi | |
| log "=== fix-setapp: Done ===" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment