Skip to content

Instantly share code, notes, and snippets.

@alexfazio
Created February 21, 2026 14:23
Show Gist options
  • Select an option

  • Save alexfazio/22c703c947d0698cc8339b950fcf3a28 to your computer and use it in GitHub Desktop.

Select an option

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)
#!/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