Improve schedule and mackup reliability
- bin/ssr-schedule: use `launchctl bootstrap`/`bootout` (modern API) with legacy `load -w` fallback. The previous `load -w`-only flow silently no-op'd on macOS 10.15+ in some configurations, leaving `ssr status` reporting `inactive` after a successful `ssr schedule on`. - bin/ssr-status: query the service via `launchctl print`/`list <label>` for consistent active/inactive detection. - lib/mackup.sh: pre-materialize iCloud placeholders via `brctl download` and retry `mackup -f backup` up to three times to dodge the recurring `OSError [Errno 66] Directory not empty` race between mackup's rmtree and iCloud's filesystem provider. Soft-fails so the rest of `ssr sync` still runs even if mackup never settles; logs actionable hints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+45
-6
@@ -17,6 +17,20 @@ INTERVAL_HOUR="${SSR_SCHEDULE_HOUR:-9}"
|
|||||||
INTERVAL_MIN="${SSR_SCHEDULE_MINUTE:-0}"
|
INTERVAL_MIN="${SSR_SCHEDULE_MINUTE:-0}"
|
||||||
SSR_BIN="$SSR_ROOT/bin/ssr"
|
SSR_BIN="$SSR_ROOT/bin/ssr"
|
||||||
|
|
||||||
|
# Modern launchd target. `launchctl load -w` has been deprecated since 10.11
|
||||||
|
# and is a silent no-op on recent macOS — use bootstrap/bootout against the
|
||||||
|
# GUI domain of the current user.
|
||||||
|
GUI_DOMAIN="gui/$(id -u)"
|
||||||
|
SERVICE_TARGET="${GUI_DOMAIN}/${LABEL}"
|
||||||
|
|
||||||
|
# ssr::schedule::is_loaded — exit 0 iff launchd knows the job.
|
||||||
|
# `launchctl print` is the modern check; `launchctl list <label>` is the
|
||||||
|
# legacy fallback that still works on every macOS we care about.
|
||||||
|
is_loaded() {
|
||||||
|
launchctl print "$SERVICE_TARGET" >/dev/null 2>&1 \
|
||||||
|
|| launchctl list "$LABEL" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
render_plist() {
|
render_plist() {
|
||||||
ssr::ensure_dir "$LOG_DIR"
|
ssr::ensure_dir "$LOG_DIR"
|
||||||
ssr::ensure_dir "$(dirname "$PLIST_DST")"
|
ssr::ensure_dir "$(dirname "$PLIST_DST")"
|
||||||
@@ -31,18 +45,43 @@ render_plist() {
|
|||||||
chmod 600 "$PLIST_DST"
|
chmod 600 "$PLIST_DST"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Boot the agent into the user's GUI domain, captures stderr for diagnostics.
|
||||||
|
# Falls back to the legacy `load -w` only if bootstrap fails entirely.
|
||||||
|
load_agent() {
|
||||||
|
local err
|
||||||
|
# Always bootout first so re-runs pick up plist changes.
|
||||||
|
launchctl bootout "$SERVICE_TARGET" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
err="$(launchctl bootstrap "$GUI_DOMAIN" "$PLIST_DST" 2>&1)" || {
|
||||||
|
ssr::warn "launchctl bootstrap failed: ${err:-no stderr}"
|
||||||
|
ssr::warn "Falling back to legacy 'launchctl load -w'"
|
||||||
|
launchctl load -w "$PLIST_DST" 2>&1 || true
|
||||||
|
}
|
||||||
|
# `enable` makes the job survive logout/reboot (parity with old `load -w`).
|
||||||
|
launchctl enable "$SERVICE_TARGET" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
unload_agent() {
|
||||||
|
launchctl bootout "$SERVICE_TARGET" >/dev/null 2>&1 \
|
||||||
|
|| launchctl unload "$PLIST_DST" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
cmd_on() {
|
cmd_on() {
|
||||||
[[ -f "$PLIST_SRC" ]] || ssr::die "launchd template missing: $PLIST_SRC"
|
[[ -f "$PLIST_SRC" ]] || ssr::die "launchd template missing: $PLIST_SRC"
|
||||||
render_plist
|
render_plist
|
||||||
# Reload to pick up changes if it was already loaded.
|
load_agent
|
||||||
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
if is_loaded; then
|
||||||
launchctl load -w "$PLIST_DST"
|
ssr::ok "Scheduled sync enabled (${INTERVAL_HOUR}:$(printf '%02d' "$INTERVAL_MIN") daily) → $PLIST_DST"
|
||||||
ssr::ok "Scheduled sync enabled (${INTERVAL_HOUR}:$(printf '%02d' "$INTERVAL_MIN") daily) → $PLIST_DST"
|
else
|
||||||
|
ssr::die "launchd did not register the job. Diagnose with:
|
||||||
|
launchctl bootstrap $GUI_DOMAIN $PLIST_DST
|
||||||
|
launchctl print $SERVICE_TARGET"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_off() {
|
cmd_off() {
|
||||||
if [[ -f "$PLIST_DST" ]]; then
|
if [[ -f "$PLIST_DST" ]]; then
|
||||||
launchctl unload "$PLIST_DST" 2>/dev/null || true
|
unload_agent
|
||||||
rm -f "$PLIST_DST"
|
rm -f "$PLIST_DST"
|
||||||
ssr::ok "Scheduled sync disabled"
|
ssr::ok "Scheduled sync disabled"
|
||||||
else
|
else
|
||||||
@@ -51,7 +90,7 @@ cmd_off() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
if [[ -f "$PLIST_DST" ]] && launchctl list | grep -q "$LABEL"; then
|
if is_loaded; then
|
||||||
ssr::ok "Scheduled sync: active (label=$LABEL)"
|
ssr::ok "Scheduled sync: active (label=$LABEL)"
|
||||||
else
|
else
|
||||||
ssr::log "Scheduled sync: inactive"
|
ssr::log "Scheduled sync: inactive"
|
||||||
|
|||||||
+3
-1
@@ -31,7 +31,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||||
if launchctl list 2>/dev/null | grep -q "$label"; then
|
service_target="gui/$(id -u)/${label}"
|
||||||
|
if launchctl print "$service_target" >/dev/null 2>&1 \
|
||||||
|
|| launchctl list "$label" >/dev/null 2>&1; then
|
||||||
echo " schedule: active ($label)"
|
echo " schedule: active ($label)"
|
||||||
else
|
else
|
||||||
echo " schedule: inactive"
|
echo " schedule: inactive"
|
||||||
|
|||||||
+55
-1
@@ -32,9 +32,63 @@ ssr::mackup::configure() {
|
|||||||
ssr::ok "Wrote $cfg"
|
ssr::ok "Wrote $cfg"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Force iCloud Drive to materialize placeholder (.icloud) files under the
|
||||||
|
# Mackup storage directory before mackup walks them. The most common ssr sync
|
||||||
|
# crash is `OSError [Errno 66] Directory not empty` raised by shutil.rmtree
|
||||||
|
# inside mackup when iCloud lazily re-creates dataless placeholders between
|
||||||
|
# listdir() and rmdir(). Pre-downloading sidesteps the race.
|
||||||
|
ssr::mackup::_materialize_icloud() {
|
||||||
|
local mackup_root="$1"
|
||||||
|
[[ -d "$mackup_root" ]] || return 0
|
||||||
|
case "$mackup_root" in
|
||||||
|
*Mobile\ Documents*) ;; # only worth doing for iCloud
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
# `brctl download` is the documented way to request file materialization.
|
||||||
|
# Available on macOS 10.12+; silently skip if missing on older systems.
|
||||||
|
if command -v brctl >/dev/null 2>&1; then
|
||||||
|
brctl download "$mackup_root" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
# Belt-and-braces: prune any stub files that survived the download
|
||||||
|
# request (sometimes left over from interrupted prior runs).
|
||||||
|
find "$mackup_root" -name '*.icloud' -type f -delete 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try `mackup -f backup` with a small retry budget. iCloud-induced races are
|
||||||
|
# transient: a 2-second sleep is usually enough for the provider to settle.
|
||||||
|
# We deliberately do NOT abort `ssr sync` on persistent failure — the user
|
||||||
|
# still benefits from brew dump, cleanup, and the unmanaged-apps scan that
|
||||||
|
# run afterwards. The failure is surfaced loudly via ssr::warn and syslog.
|
||||||
ssr::mackup::backup() {
|
ssr::mackup::backup() {
|
||||||
ssr::log "mackup backup"
|
ssr::log "mackup backup"
|
||||||
mackup -f backup
|
|
||||||
|
# Derive mackup root from ~/.mackup.cfg (path + directory).
|
||||||
|
local cfg="$HOME/.mackup.cfg" mackup_root="" path dir
|
||||||
|
if [[ -f "$cfg" ]]; then
|
||||||
|
path="$(awk -F= '/^path/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
||||||
|
dir="$( awk -F= '/^directory/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}' "$cfg")"
|
||||||
|
[[ -n "$path" && -n "$dir" ]] && mackup_root="$path/$dir"
|
||||||
|
fi
|
||||||
|
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
||||||
|
|
||||||
|
local attempt rc=0
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if mackup -f backup; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rc=$?
|
||||||
|
ssr::warn "mackup backup failed (attempt $attempt/3, rc=$rc)"
|
||||||
|
if [[ $attempt -lt 3 ]]; then
|
||||||
|
[[ -n "$mackup_root" ]] && ssr::mackup::_materialize_icloud "$mackup_root"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ssr::warn "mackup backup did not complete after 3 attempts (rc=$rc)."
|
||||||
|
ssr::warn "Sync will continue without it; rerun 'mackup -f backup' manually,"
|
||||||
|
ssr::warn "or switch ~/.mackup.cfg to a non-iCloud storage path if this persists."
|
||||||
|
ssr::syslog "mackup backup failed after retries (rc=$rc)"
|
||||||
|
return 0 # soft-fail so the rest of ssr sync still runs
|
||||||
}
|
}
|
||||||
|
|
||||||
ssr::mackup::restore() {
|
ssr::mackup::restore() {
|
||||||
|
|||||||
Reference in New Issue
Block a user