refactored script
This commit is contained in:
@@ -10,6 +10,7 @@ echo "Logging to: $LOG_FILE"
|
||||
TARGET_DIR="/Applications"
|
||||
APP_NAME="repertory.app"
|
||||
|
||||
# Embedded at pack time (from CFBundleIdentifier prefix)
|
||||
LABEL_PREFIX="__LABEL_PREFIX__"
|
||||
UI_LABEL="${LABEL_PREFIX}.ui"
|
||||
|
||||
@@ -27,11 +28,12 @@ die() {
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
src_app="$(/usr/bin/find "$here" -type d -name "$APP_NAME" -print -quit 2>/dev/null || true)"
|
||||
if [[ -z "${src_app:-}" ]]; then
|
||||
src_app="$(/usr/bin/find "$here" -type d -name "*.app" -print -quit 2>/dev/null || true)"
|
||||
# Locate source app on the DMG (supports hidden payload dirs)
|
||||
src_app="${here}/${APP_NAME}"
|
||||
if [[ ! -d "$src_app" ]]; then
|
||||
src_app="$(/usr/bin/find "$here" -type d -name "$APP_NAME" -print -quit 2>/dev/null || true)"
|
||||
fi
|
||||
[[ -z "${src_app:-}" ]] && die "No .app found on this disk image."
|
||||
[[ -d "$src_app" ]] || die "Could not find ${APP_NAME} on this disk image."
|
||||
|
||||
app_basename="$(basename "$src_app")"
|
||||
dest_app="${TARGET_DIR}/${APP_NAME}"
|
||||
@@ -47,6 +49,7 @@ bundle_build_of() {
|
||||
/usr/bin/defaults read "$1/Contents/Info" CFBundleVersion 2>/dev/null || echo "(unknown)"
|
||||
}
|
||||
|
||||
# Require /Applications; prompt for sudo if needed; abort if cannot elevate
|
||||
USE_SUDO=0
|
||||
SUDO=""
|
||||
ensure_target_writable() {
|
||||
@@ -66,6 +69,7 @@ ensure_target_writable() {
|
||||
die "Cannot write to ${TARGET_DIR} and sudo is unavailable."
|
||||
}
|
||||
|
||||
# ----- STRICT LABEL PREFIX GATE (fail if invalid) -----
|
||||
_is_valid_label_prefix() {
|
||||
local p="${1:-}"
|
||||
[[ -n "$p" ]] && [[ "$p" != "__LABEL_PREFIX__" ]] && [[ "$p" =~ ^[A-Za-z0-9._-]+$ ]] && [[ "$p" == *.* ]]
|
||||
@@ -74,6 +78,7 @@ if ! _is_valid_label_prefix "${LABEL_PREFIX:-}"; then
|
||||
die "Invalid LABEL_PREFIX in installer (value: \"${LABEL_PREFIX:-}\"). Rebuild the DMG so the installer contains a valid reverse-DNS prefix."
|
||||
fi
|
||||
|
||||
# ----- LaunchServices helpers -----
|
||||
ls_prune_bundle_id() {
|
||||
local bundle_id="$1" keep_path="$2"
|
||||
[[ -z "$bundle_id" ]] && return 0
|
||||
@@ -84,6 +89,7 @@ ls_prune_bundle_id() {
|
||||
[[ -d "$root" ]] || continue
|
||||
candidates+=$'\n'"$(/usr/bin/mdfind -onlyin "$root" "kMDItemCFBundleIdentifier == '${bundle_id}'" 2>/dev/null || true)"
|
||||
done
|
||||
# Include backups adjacent to keep_path
|
||||
candidates+=$'\n'$(/bin/ls -1d "${keep_path%/*.app}"/*.bak 2>/dev/null || true)
|
||||
local LSREG="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||
printf "%s\n" "$candidates" | /usr/bin/awk 'NF' | /usr/bin/sort -u | while IFS= read -r p; do
|
||||
@@ -92,11 +98,8 @@ ls_prune_bundle_id() {
|
||||
"$LSREG" -u "$p" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
ls_register_exact() {
|
||||
local app_path="$1"
|
||||
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$app_path" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ----- FUSE unmount (no process killing here) -----
|
||||
is_mounted() { /sbin/mount | /usr/bin/awk '{print $3}' | /usr/bin/grep -Fx "${1:-}" >/dev/null 2>&1; }
|
||||
_list_repertory_fuse_mounts() { /sbin/mount | /usr/bin/grep -Ei 'macfuse|osxfuse' | /usr/bin/awk '{print $3}' | /usr/bin/grep -i "repertory" || true; }
|
||||
_unmount_one() {
|
||||
@@ -112,31 +115,33 @@ _unmount_one() {
|
||||
done
|
||||
return 1
|
||||
}
|
||||
unmount_existing_repertory_volumes_first() {
|
||||
local failed=0 any=0
|
||||
unmount_existing_repertory_volumes() {
|
||||
# Hard-fail on the first unmount problem.
|
||||
while IFS= read -r mnt; do
|
||||
[[ -z "$mnt" ]] && continue
|
||||
any=1
|
||||
log "Unmounting FUSE mount: $mnt"
|
||||
_unmount_one "$mnt" || {
|
||||
warn "Could not unmount $mnt"
|
||||
failed=1
|
||||
}
|
||||
if ! _unmount_one "$mnt"; then
|
||||
warn "Failed to unmount $mnt"
|
||||
return 1
|
||||
fi
|
||||
done < <(_list_repertory_fuse_mounts)
|
||||
[[ $any -eq 1 ]] && {
|
||||
sync || true
|
||||
sleep 0.3
|
||||
}
|
||||
return $failed
|
||||
sync || true
|
||||
sleep 0.3
|
||||
return 0
|
||||
}
|
||||
|
||||
# ----- user LaunchAgents (by LABEL_PREFIX only) -----
|
||||
get_plist_label() {
|
||||
/usr/bin/defaults read "$1" Label 2>/dev/null || /usr/libexec/PlistBuddy -c "Print :Label" "$1" 2>/dev/null || basename "$1" .plist
|
||||
}
|
||||
|
||||
snapshot_launchagents_user() {
|
||||
local user_agents="$HOME/Library/LaunchAgents"
|
||||
snapfile="$(/usr/bin/mktemp "/tmp/repertory_launchagents.XXXXXX")"
|
||||
snapfile="$(/usr/bin/mktemp "/tmp/repertory_launchagents.XXXXXX")" || snapfile=""
|
||||
if [[ -z "$snapfile" ]]; then
|
||||
warn "Could not create temporary snapshot file; skipping LaunchAgent restart snapshot."
|
||||
return 0
|
||||
fi
|
||||
if [[ -d "$user_agents" ]]; then
|
||||
/usr/bin/find "$user_agents" -maxdepth 1 -type f -name "${LABEL_PREFIX}"'*.plist' -print 2>/dev/null |
|
||||
while IFS= read -r plist; do
|
||||
@@ -150,16 +155,16 @@ snapshot_launchagents_user() {
|
||||
}
|
||||
|
||||
unload_launchd_helpers_user() {
|
||||
local uid
|
||||
local uid user_agents
|
||||
uid="$(id -u)"
|
||||
local user_agents="$HOME/Library/LaunchAgents"
|
||||
user_agents="$HOME/Library/LaunchAgents"
|
||||
[[ -d "$user_agents" ]] || return 0
|
||||
|
||||
while IFS= read -r plist; do
|
||||
[[ -z "$plist" ]] && continue
|
||||
local base
|
||||
local base label
|
||||
base="$(basename "$plist")"
|
||||
[[ "$base" == "${LABEL_PREFIX}"* ]] || continue
|
||||
local label
|
||||
label="$(get_plist_label "$plist")"
|
||||
[[ -n "$label" && "$label" == "${LABEL_PREFIX}"* ]] || continue
|
||||
log "Booting out user label ${label} (${plist})"
|
||||
@@ -178,10 +183,8 @@ unload_launchd_helpers_user() {
|
||||
|
||||
restart_launchagents_from_snapshot() {
|
||||
[[ -n "${snapfile:-}" && -f "${snapfile}" ]] || return 0
|
||||
local uid
|
||||
local uid count=0 ui_seen=0
|
||||
uid="$(id -u)"
|
||||
local count=0
|
||||
local ui_seen=0
|
||||
while IFS=$'\t' read -r plist label; do
|
||||
[[ -z "$plist" || -z "$label" ]] && continue
|
||||
[[ -f "$plist" ]] || continue
|
||||
@@ -189,7 +192,7 @@ restart_launchagents_from_snapshot() {
|
||||
/bin/launchctl bootstrap "gui/${uid}" "$plist" 2>/dev/null || true
|
||||
/bin/launchctl kickstart -k "gui/${uid}/${label}" 2>/dev/null || true
|
||||
((count++)) || true
|
||||
if [[ "$label" == "$UI_LABEL" ]]; then ui_seen=1; fi
|
||||
[[ "$label" == "$UI_LABEL" ]] && ui_seen=1 || true
|
||||
done <"$snapfile"
|
||||
log "Re-started ${count} LaunchAgent(s) with prefix ${LABEL_PREFIX}." || true
|
||||
|
||||
@@ -202,6 +205,7 @@ restart_launchagents_from_snapshot() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ----- quarantine helper -----
|
||||
remove_quarantine() {
|
||||
local path="${1:-}"
|
||||
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||
@@ -211,52 +215,7 @@ remove_quarantine() {
|
||||
fi
|
||||
}
|
||||
|
||||
dump_quick_diagnostics() {
|
||||
local dest_app="$1" exec_name="$2"
|
||||
warn "codesign verify:"
|
||||
/usr/bin/codesign --verify --deep --strict --verbose=2 "$dest_app" || true
|
||||
warn "Gatekeeper assess:"
|
||||
/usr/sbin/spctl --assess --type execute --verbose=2 "$dest_app" || true
|
||||
warn "quarantine xattr:"
|
||||
/usr/bin/xattr -p com.apple.quarantine "$dest_app" 2>/dev/null || echo "(none)"
|
||||
local crash="$(/bin/ls -t "$HOME/Library/Logs/DiagnosticReports/${exec_name}"*.crash 2>/dev/null | /usr/bin/head -n1 || true)"
|
||||
if [[ -n "$crash" ]]; then
|
||||
warn "Recent crash report: $crash (tail)"
|
||||
/usr/bin/tail -n 60 "$crash" || true
|
||||
fi
|
||||
warn "Unified log (2m) for ${exec_name}:"
|
||||
/usr/bin/log show --style syslog --last 2m --predicate "process == \"${exec_name}\"" 2>/dev/null | /usr/bin/tail -n 120 || true
|
||||
}
|
||||
|
||||
_current_user() { id -un; }
|
||||
ps_alive() {
|
||||
local p="${1:-}"
|
||||
[[ -n "$p" ]] && /bin/kill -0 "$p" 2>/dev/null
|
||||
}
|
||||
|
||||
find_latest_repertory_pid() {
|
||||
local since_epoch="$1" exec_name="$2" user="$(_current_user)"
|
||||
/bin/ps ax -o pid=,lstart=,user=,command= |
|
||||
/usr/bin/awk -v since="$since_epoch" -v en="$exec_name" -v u="$user" '
|
||||
{
|
||||
pid=$1; lstart=$2" "$3" "$4" "$5" "$6; usr=$7;
|
||||
cmd=""; for (i=8;i<=NF;i++) { if (i>8) cmd=cmd" "; cmd=cmd $i }
|
||||
if (usr != u) next;
|
||||
n=split(cmd, tok, /[[:space:]]+/); if (n < 1) next;
|
||||
argv0 = tok[1]; m=split(argv0, parts, "/"); base = parts[m];
|
||||
if (base != en) next;
|
||||
has_ui = (cmd ~ /(^|[[:space:]])-{1,2}ui([[:space:]]|$)/) ? 1 : 0;
|
||||
no_args = (n == 1) ? 1 : 0;
|
||||
if (!has_ui && !no_args) next;
|
||||
printf "%s\t%s\n", pid, lstart
|
||||
}' |
|
||||
while IFS=$'\t' read -r pid lstart; do
|
||||
lstart_epoch="$(/bin/date -j -f "%a %b %d %T %Y" "$lstart" +%s 2>/dev/null || echo 0)"
|
||||
[[ "$lstart_epoch" -ge "$since_epoch" ]] || continue
|
||||
printf "%s\t%s\n" "$pid" "$lstart_epoch"
|
||||
done | /usr/bin/sort -k2,2n | /usr/bin/tail -n1 | /usr/bin/awk '{print $1}'
|
||||
}
|
||||
|
||||
# ----- process helpers -----
|
||||
kill_repertory_processes() {
|
||||
local exec_name="$1"
|
||||
/usr/bin/pkill -TERM -f "$dest_app" >/dev/null 2>&1 || true
|
||||
@@ -269,14 +228,14 @@ kill_repertory_processes() {
|
||||
/usr/bin/pkill -KILL -x "$exec_name" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ----- visibility helper -----
|
||||
unhide_path() {
|
||||
local path="$1"
|
||||
/usr/bin/chflags -R nohidden "$path" 2>/dev/null || true
|
||||
/usr/bin/xattr -d -r com.apple.FinderInfo "$path" 2>/dev/null || true
|
||||
}
|
||||
|
||||
now_epoch() { /bin/date +%s; }
|
||||
|
||||
# ----- stage / validate / activate / post-activate -----
|
||||
stage_new_app() {
|
||||
staged="${dest_app}.new-$$"
|
||||
log "Staging new app → $staged"
|
||||
@@ -315,7 +274,7 @@ activate_staged_app() {
|
||||
fi
|
||||
}
|
||||
|
||||
post_activate_housekeeping() {
|
||||
post_activate_cleanup() {
|
||||
log "Clearing quarantine on installed app…"
|
||||
remove_quarantine "$dest_app"
|
||||
log "Clearing hidden flags on installed app…"
|
||||
@@ -326,135 +285,20 @@ post_activate_housekeeping() {
|
||||
local BID
|
||||
BID="$(bundle_id_of "$dest_app")"
|
||||
ls_prune_bundle_id "$BID" "$dest_app"
|
||||
ls_register_exact "$dest_app"
|
||||
|
||||
log "Installed ${app_basename}: version=$(bundle_version_of "$dest_app") build=$(bundle_build_of "$dest_app")"
|
||||
}
|
||||
|
||||
launch_ui() {
|
||||
log "Launching the new app…"
|
||||
/usr/bin/open -n "$dest_app" || warn "open -n by path failed; not falling back to -b to avoid launching a stale copy."
|
||||
}
|
||||
|
||||
wait_for_ui_stability() {
|
||||
local exec_name_now="$1"
|
||||
|
||||
local PID_DETECT_TIMEOUT_SEC="${PID_DETECT_TIMEOUT_SEC:-2}"
|
||||
local POLL_INTERVAL="${POLL_INTERVAL:-0.10}"
|
||||
local STABILITY_WINDOW_SEC="${STABILITY_WINDOW_SEC:-8}"
|
||||
local GAP_TOLERANCE_SEC="${GAP_TOLERANCE_SEC:-1}"
|
||||
|
||||
local launch_epoch
|
||||
launch_epoch="$(now_epoch)"
|
||||
|
||||
local cur_pid=""
|
||||
for ((i = 0; i < PID_DETECT_TIMEOUT_SEC * 10; i++)); do
|
||||
cur_pid="$(find_latest_repertory_pid "$launch_epoch" "$exec_name_now" || true)"
|
||||
[[ -n "$cur_pid" ]] && break
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
if [[ -z "$cur_pid" ]]; then
|
||||
warn "No UI process observed for ${exec_name_now} after launch."
|
||||
dump_quick_diagnostics "$dest_app" "$exec_name_now" || true
|
||||
return 1
|
||||
fi
|
||||
log "Tracking UI pid $cur_pid."
|
||||
ps_alive "$cur_pid" || cur_pid=""
|
||||
|
||||
local start_ts
|
||||
start_ts="$(now_epoch)"
|
||||
local last_seen_ts="$start_ts"
|
||||
local last_logged_pid="$cur_pid"
|
||||
|
||||
while :; do
|
||||
local newest
|
||||
newest="$(find_latest_repertory_pid "$launch_epoch" "$exec_name_now" || true)"
|
||||
|
||||
if [[ -n "$newest" ]] && ps_alive "$newest"; then
|
||||
last_seen_ts="$(now_epoch)"
|
||||
if [[ "$newest" != "$cur_pid" ]]; then
|
||||
if [[ -n "$cur_pid" && "$newest" != "$last_logged_pid" ]]; then
|
||||
log "Adopting successor UI pid $newest (was $cur_pid)."
|
||||
elif [[ -z "$cur_pid" ]]; then
|
||||
log "Adopting UI pid $newest."
|
||||
fi
|
||||
cur_pid="$newest"
|
||||
last_logged_pid="$newest"
|
||||
fi
|
||||
fi
|
||||
|
||||
local now
|
||||
now="$(now_epoch)"
|
||||
local elapsed=$((now - start_ts))
|
||||
local gap=$((now - last_seen_ts))
|
||||
if ((elapsed >= STABILITY_WINDOW_SEC)); then
|
||||
if ((gap <= GAP_TOLERANCE_SEC)); then
|
||||
log "UI verified alive across stability window (~${STABILITY_WINDOW_SEC}s)."
|
||||
return 0
|
||||
else
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
ensure_target_writable
|
||||
|
||||
local exec_name
|
||||
exec_name="$(bundle_exec_of "$src_app")"
|
||||
|
||||
snapshot_launchagents_user
|
||||
|
||||
unmount_existing_repertory_volumes_first || warn "One or more FUSE mounts resisted unmount; continuing."
|
||||
|
||||
unload_launchd_helpers_user
|
||||
|
||||
kill_repertory_processes "$exec_name"
|
||||
|
||||
stage_new_app
|
||||
|
||||
validate_staged_app
|
||||
|
||||
activate_staged_app
|
||||
|
||||
post_activate_housekeeping
|
||||
|
||||
restart_launchagents_from_snapshot
|
||||
|
||||
if ((skip_ui_launch)); then
|
||||
[[ -n "$backup" && -d "$backup" ]] && {
|
||||
log "Removing backup: $backup"
|
||||
$SUDO /bin/rm -rf "$backup" || warn "Could not remove backup (safe to delete manually): $backup"
|
||||
}
|
||||
log "Done."
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Launching the new app…"
|
||||
launch_ui
|
||||
local exec_name_now
|
||||
exec_name_now="$(/usr/bin/defaults read "$dest_app/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}")"
|
||||
|
||||
if wait_for_ui_stability "$exec_name_now"; then
|
||||
[[ -n "$backup" && -d "$backup" ]] && {
|
||||
log "Removing backup: $backup"
|
||||
$SUDO /bin/rm -rf "$backup" || warn "Could not remove backup (safe to delete manually): $backup"
|
||||
}
|
||||
log "Done."
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "No surviving UI process for ${exec_name_now} by end of verification."
|
||||
dump_quick_diagnostics "$dest_app" "$exec_name_now" || true
|
||||
if [[ -n "$backup" && -d "$backup" ]]; then
|
||||
warn "Rolling back to previous app…"
|
||||
$SUDO /bin/rm -rf "$dest_app" || true
|
||||
$SUDO /bin/mv "$backup" "$dest_app" || true
|
||||
/usr/bin/open -n "$dest_app" || true
|
||||
fi
|
||||
remove_backup() {
|
||||
[[ -n "$backup" && -d "$backup" ]] && {
|
||||
log "Removing backup: $backup"
|
||||
$SUDO /bin/rm -rf "$backup" || warn "Could not remove backup (safe to delete manually): $backup"
|
||||
}
|
||||
log "Done."
|
||||
}
|
||||
|
||||
@@ -471,6 +315,42 @@ cleanup_staged() {
|
||||
/bin/rm -f "${snapfile}" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap 'rc=$?; cleanup_staged; if (( rc != 0 )); then echo "Installer failed with code $rc. See $LOG_FILE"; fi' EXIT
|
||||
|
||||
main() {
|
||||
ensure_target_writable
|
||||
|
||||
local exec_name
|
||||
exec_name="$(bundle_exec_of "$src_app")"
|
||||
|
||||
# 1) Snapshot agents we’ll restart later
|
||||
snapshot_launchagents_user
|
||||
|
||||
# 2) Hard-fail if any FUSE unmount fails
|
||||
unmount_existing_repertory_volumes || die "One or more FUSE mounts resisted unmount."
|
||||
|
||||
# 3) Stop user LaunchAgents (do NOT delete plists)
|
||||
unload_launchd_helpers_user
|
||||
|
||||
# 4) Kill any remaining repertory processes
|
||||
kill_repertory_processes "$exec_name"
|
||||
|
||||
# 5) Stage → validate → activate → post-activate
|
||||
stage_new_app
|
||||
validate_staged_app
|
||||
activate_staged_app
|
||||
post_activate_cleanup
|
||||
|
||||
# 6) Re-start previously-running LaunchAgents (so automount helpers come up)
|
||||
restart_launchagents_from_snapshot
|
||||
|
||||
# 7) If UI LaunchAgent came back, skip manual launch
|
||||
if ((!skip_ui_launch)); then
|
||||
launch_ui
|
||||
fi
|
||||
|
||||
# 8) Remove backup now that everything is good
|
||||
remove_backup
|
||||
}
|
||||
|
||||
trap 'rc=$?; cleanup_staged; if (( rc != 0 )); then echo "Installer failed with code $rc. See $LOG_FILE"; fi' EXIT
|
||||
main "$@"
|
||||
|
Reference in New Issue
Block a user