fix macos installation
This commit is contained in:
@@ -11,6 +11,8 @@
|
|||||||
<string>@PROJECT_NAME@</string>
|
<string>@PROJECT_NAME@</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@</string>
|
<string>@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@-@PROJECT_RELEASE_ITER@_@PROJECT_GIT_REV@</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>@PROJECT_MAJOR_VERSION@.@PROJECT_MINOR_VERSION@.@PROJECT_REVISION_VERSION@.@PROJECT_RELEASE_NUM@</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
442
repertory/Install repertory.command
Normal file
442
repertory/Install repertory.command
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# No `-e` (we tolerate benign non-zero returns); keep -Euo
|
||||||
|
set -Euo pipefail
|
||||||
|
|
||||||
|
# ---- logging to a writable place (DMG is read-only) ----
|
||||||
|
LOG_DIR="/tmp"
|
||||||
|
LOG_FILE="${LOG_DIR}/Install-$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
||||||
|
echo "Logging to: $LOG_FILE"
|
||||||
|
|
||||||
|
TARGET_DIR="/Applications"
|
||||||
|
APP_NAME="repertory.app" # final install path: /Applications/repertory.app
|
||||||
|
|
||||||
|
# Embedded at pack time (from CFBundleIdentifier prefix)
|
||||||
|
LABEL_PREFIX="__LABEL_PREFIX__"
|
||||||
|
|
||||||
|
# make staged visible to trap cleanup
|
||||||
|
staged=""
|
||||||
|
|
||||||
|
log() { printf "[%(%H:%M:%S)T] %s\n" -1 "$*"; }
|
||||||
|
warn() { log "WARN: $*"; }
|
||||||
|
die() {
|
||||||
|
log "ERROR: $*"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Find the source app anywhere under the DMG root (supports hidden .payload/)
|
||||||
|
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)"
|
||||||
|
fi
|
||||||
|
[[ -z "${src_app:-}" ]] && die "No .app found on this disk image."
|
||||||
|
|
||||||
|
app_basename="$(basename "$src_app")" # repertory.app
|
||||||
|
dest_app="${TARGET_DIR}/${APP_NAME}"
|
||||||
|
|
||||||
|
bundle_id_of() { /usr/bin/defaults read "$1/Contents/Info" CFBundleIdentifier 2>/dev/null || true; }
|
||||||
|
bundle_exec_of() { /usr/bin/defaults read "$1/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}"; }
|
||||||
|
bundle_version_of() {
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$1/Contents/Info.plist" 2>/dev/null ||
|
||||||
|
/usr/bin/defaults read "$1/Contents/Info" CFBundleShortVersionString 2>/dev/null || echo "(unknown)"
|
||||||
|
}
|
||||||
|
bundle_build_of() {
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$1/Contents/Info.plist" 2>/dev/null ||
|
||||||
|
/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() {
|
||||||
|
if mkdir -p "${TARGET_DIR}/.repertory_install_test.$$" 2>/dev/null; then
|
||||||
|
rmdir "${TARGET_DIR}/.repertory_install_test.$$" 2>/dev/null || true
|
||||||
|
USE_SUDO=0
|
||||||
|
SUDO=""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
log "Elevating privileges to write to ${TARGET_DIR}…"
|
||||||
|
sudo -v || die "Administrator privileges required to install into ${TARGET_DIR}."
|
||||||
|
USE_SUDO=1
|
||||||
|
SUDO="sudo"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
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" == *.* ]]
|
||||||
|
}
|
||||||
|
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
|
||||||
|
local search_roots=("/Applications" "$HOME/Applications" "/Volumes")
|
||||||
|
if [[ -n "${here:-}" && "$here" == /Volumes/* ]]; then search_roots+=("$here"); fi
|
||||||
|
local candidates=""
|
||||||
|
for root in "${search_roots[@]}"; do
|
||||||
|
[[ -d "$root" ]] || continue
|
||||||
|
candidates+=$'\n'"$(/usr/bin/mdfind -onlyin "$root" "kMDItemCFBundleIdentifier == '${bundle_id}'" 2>/dev/null || true)"
|
||||||
|
done
|
||||||
|
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
|
||||||
|
[[ -z "$p" || ! -d "$p" || "$p" == "$keep_path" ]] && continue
|
||||||
|
log "Unregistering older LS entry for ${bundle_id}: $p"
|
||||||
|
"$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() {
|
||||||
|
local mnt="${1:-}"
|
||||||
|
[[ -n "$mnt" ]] || return 0
|
||||||
|
/usr/sbin/diskutil unmount "$mnt" >/dev/null 2>&1 || /sbin/umount "$mnt" >/dev/null 2>&1 || true
|
||||||
|
if is_mounted "$mnt"; then
|
||||||
|
/usr/sbin/diskutil unmount force "$mnt" >/dev/null 2>&1 || /sbin/umount -f "$mnt" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
for _ in {1..20}; do
|
||||||
|
is_mounted "$mnt" || return 0
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
unmount_existing_repertory_volumes_first() {
|
||||||
|
local failed=0 any=0
|
||||||
|
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
|
||||||
|
}
|
||||||
|
done < <(_list_repertory_fuse_mounts)
|
||||||
|
[[ $any -eq 1 ]] && {
|
||||||
|
sync || true
|
||||||
|
sleep 0.3
|
||||||
|
}
|
||||||
|
return $failed
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- 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; }
|
||||||
|
unload_launchd_helpers_user() {
|
||||||
|
local uid
|
||||||
|
uid="$(id -u)"
|
||||||
|
local user_agents="$HOME/Library/LaunchAgents"
|
||||||
|
[[ -d "$user_agents" ]] || return 0
|
||||||
|
while IFS= read -r plist; do
|
||||||
|
[[ -z "$plist" ]] && continue
|
||||||
|
local base
|
||||||
|
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})"
|
||||||
|
/bin/launchctl bootout "gui/${uid}" "$plist" 2>/dev/null ||
|
||||||
|
/bin/launchctl bootout "gui/${uid}" "$label" 2>/dev/null ||
|
||||||
|
/bin/launchctl remove "$label" 2>/dev/null || true
|
||||||
|
done < <(/usr/bin/find "$user_agents" -maxdepth 1 -type f -name "${LABEL_PREFIX}"'*.plist' -print 2>/dev/null)
|
||||||
|
/bin/launchctl list 2>/dev/null | /usr/bin/awk -v pre="$LABEL_PREFIX" 'NF>=3 && index($3, pre)==1 {print $3}' |
|
||||||
|
while read -r lbl; do
|
||||||
|
[[ -z "$lbl" ]] && continue
|
||||||
|
log "Booting out leftover user label: $lbl"
|
||||||
|
/bin/launchctl bootout "gui/${uid}" "$lbl" 2>/dev/null || /bin/launchctl remove "$lbl" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
remove_launchd_plists_user() {
|
||||||
|
local user_agents="$HOME/Library/LaunchAgents"
|
||||||
|
[[ -d "$user_agents" ]] || return 0
|
||||||
|
while IFS= read -r plist; do
|
||||||
|
[[ -z "$plist" ]] && continue
|
||||||
|
local base
|
||||||
|
base="$(basename "$plist")"
|
||||||
|
[[ "$base" == "${LABEL_PREFIX}"* ]] || continue
|
||||||
|
log "Removing LaunchAgent plist: $plist"
|
||||||
|
/bin/rm -f "$plist" 2>/dev/null || warn "Failed to remove $plist"
|
||||||
|
done < <(/usr/bin/find "$user_agents" -maxdepth 1 -type f -name "${LABEL_PREFIX}"'*.plist' -print 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- quarantine helper -----
|
||||||
|
remove_quarantine() {
|
||||||
|
local path="${1:-}"
|
||||||
|
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||||
|
sudo /usr/bin/xattr -dr com.apple.quarantine "$path" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
/usr/bin/xattr -dr com.apple.quarantine "$path" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- optional diagnostics -----
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ----- process helpers -----
|
||||||
|
_current_user() { id -un; } # username (ps prints 'user' as a name)
|
||||||
|
|
||||||
|
# Faster/more robust "alive" check than parsing `ps`.
|
||||||
|
ps_alive() {
|
||||||
|
local p="${1:-}"
|
||||||
|
[[ -n "$p" ]] && /bin/kill -0 "$p" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Newest PID for current user/exec, started >= since_epoch, where command line is:
|
||||||
|
# - exactly the binary (no args), OR
|
||||||
|
# - contains -ui or --ui (possibly with other args)
|
||||||
|
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}'
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_repertory_processes() {
|
||||||
|
local exec_name="$1"
|
||||||
|
/usr/bin/pkill -TERM -f "$dest_app" >/dev/null 2>&1 || true
|
||||||
|
/usr/bin/pkill -TERM -x "$exec_name" >/dev/null 2>&1 || true
|
||||||
|
for _ in {1..20}; do
|
||||||
|
/usr/bin/pgrep -af "$dest_app" >/dev/null 2>&1 || /usr/bin/pgrep -x "$exec_name" >/dev/null 2>&1 || break
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
/usr/bin/pkill -KILL -f "$dest_app" >/dev/null 2>&1 || true
|
||||||
|
/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; }
|
||||||
|
|
||||||
|
# --- main ---
|
||||||
|
main() {
|
||||||
|
ensure_target_writable
|
||||||
|
|
||||||
|
local exec_name
|
||||||
|
exec_name="$(bundle_exec_of "$src_app")"
|
||||||
|
|
||||||
|
# 1) Unmount FUSE mounts first
|
||||||
|
unmount_existing_repertory_volumes_first || warn "One or more FUSE mounts resisted unmount; continuing."
|
||||||
|
|
||||||
|
# 2) Stop/remove user LaunchAgents
|
||||||
|
unload_launchd_helpers_user
|
||||||
|
remove_launchd_plists_user
|
||||||
|
|
||||||
|
# 3) Kill any remaining repertory processes
|
||||||
|
kill_repertory_processes "$exec_name"
|
||||||
|
|
||||||
|
# ---- Stage → atomic swap (keep backup until success confirmed) ----
|
||||||
|
staged="${dest_app}.new-$$"
|
||||||
|
log "Staging new app → $staged"
|
||||||
|
$SUDO /usr/bin/ditto "$src_app" "$staged" || die "ditto to stage failed"
|
||||||
|
remove_quarantine "$staged"
|
||||||
|
|
||||||
|
# Sanity
|
||||||
|
if [[ ! -f "$staged/Contents/Info.plist" ]]; then
|
||||||
|
$SUDO /bin/rm -rf "$staged"
|
||||||
|
die "staged app missing Info.plist"
|
||||||
|
fi
|
||||||
|
local exe_name_staged
|
||||||
|
exe_name_staged="$(/usr/bin/defaults read "$staged/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}")"
|
||||||
|
if [[ ! -x "$staged/Contents/MacOS/$exe_name_staged" ]]; then
|
||||||
|
$SUDO /bin/rm -rf "$staged"
|
||||||
|
die "staged app missing main executable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate (keep backup until success)
|
||||||
|
local backup=""
|
||||||
|
if [[ -d "$dest_app" ]]; then
|
||||||
|
backup="${dest_app}.$(date +%Y%m%d%H%M%S).bak"
|
||||||
|
log "Moving existing app to backup: $backup"
|
||||||
|
$SUDO /bin/mv "$dest_app" "$backup" || {
|
||||||
|
$SUDO /bin/rm -rf "$staged"
|
||||||
|
die "failed to move existing app out of the way"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
log "Activating new app → $dest_app"
|
||||||
|
if ! $SUDO /bin/mv "$staged" "$dest_app"; then
|
||||||
|
warn "Activation failed; attempting rollback…"
|
||||||
|
[[ -n "$backup" && -d "$backup" ]] && $SUDO /bin/mv "$backup" "$dest_app" || true
|
||||||
|
$SUDO /bin/rm -rf "$staged" || true
|
||||||
|
die "install activation failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Clearing quarantine on installed app…"
|
||||||
|
remove_quarantine "$dest_app"
|
||||||
|
log "Clearing hidden flags on installed app…"
|
||||||
|
unhide_path "$dest_app"
|
||||||
|
|
||||||
|
# LS refresh/prune
|
||||||
|
local LSREG="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||||
|
[[ -x "$LSREG" ]] && "$LSREG" -f "$dest_app" >/dev/null 2>&1 || true
|
||||||
|
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 ONLY by full path (no -b fallback) ----
|
||||||
|
log "Launching the new app…"
|
||||||
|
local launch_epoch
|
||||||
|
launch_epoch="$(now_epoch)"
|
||||||
|
local exec_name_now
|
||||||
|
exec_name_now="$(/usr/bin/defaults read "$dest_app/Contents/Info" CFBundleExecutable 2>/dev/null || echo "${app_basename%.app}")"
|
||||||
|
|
||||||
|
# Tunables
|
||||||
|
local PID_DETECT_TIMEOUT_SEC="${PID_DETECT_TIMEOUT_SEC:-2}"
|
||||||
|
local POLL_INTERVAL="${POLL_INTERVAL:-0.10}" # seconds; fractional OK
|
||||||
|
local STABILITY_WINDOW_SEC="${STABILITY_WINDOW_SEC:-8}"
|
||||||
|
local GAP_TOLERANCE_SEC="${GAP_TOLERANCE_SEC:-1}" # allow brief handoff gaps
|
||||||
|
|
||||||
|
/usr/bin/open -n "$dest_app" || warn "open -n by path failed; not falling back to -b to avoid launching a stale copy."
|
||||||
|
|
||||||
|
# Detect initial UI pid (don’t fail if we miss the very first parent)
|
||||||
|
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
|
||||||
|
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
|
||||||
|
log "Done."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log "Tracking UI pid $cur_pid."
|
||||||
|
|
||||||
|
# === Stability shepherd: succeed if *any* qualifying UI pid is present
|
||||||
|
# for the entire window, with small tolerance for handoffs.
|
||||||
|
local start_ts
|
||||||
|
start_ts="$(now_epoch)"
|
||||||
|
local last_seen_ts="$start_ts"
|
||||||
|
local last_logged_pid="$cur_pid"
|
||||||
|
|
||||||
|
# Ensure the first observed PID is alive; if not, we still allow a quick adopt.
|
||||||
|
ps_alive "$cur_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
|
||||||
|
# update last-seen
|
||||||
|
last_seen_ts="$(now_epoch)"
|
||||||
|
if [[ "$newest" != "$cur_pid" ]]; then
|
||||||
|
# only log when PID actually changes; never print "(was )"
|
||||||
|
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
|
||||||
|
|
||||||
|
# success if window elapsed and we’ve seen a live UI within tolerance
|
||||||
|
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)."
|
||||||
|
if [[ -n "$backup" && -d "$backup" ]]; then
|
||||||
|
log "Removing backup: $backup"
|
||||||
|
$SUDO /bin/rm -rf "$backup" || warn "Could not remove backup (safe to delete manually): $backup"
|
||||||
|
fi
|
||||||
|
log "Done."
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
log "Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_staged() {
|
||||||
|
if [[ -n "${staged:-}" && -d "${staged}" ]]; then
|
||||||
|
log "Cleaning up staged folder: ${staged}"
|
||||||
|
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||||
|
sudo /bin/rm -rf "${staged}" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
/bin/rm -rf "${staged}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap 'rc=$?; cleanup_staged; if (( rc != 0 )); then echo "Installer failed with code $rc. See $LOG_FILE"; fi' EXIT
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -101,6 +101,22 @@ namespace {
|
|||||||
|
|
||||||
return std::string{value};
|
return std::string{value};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto get_last_net_error() -> int {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return WSAGetLastError();
|
||||||
|
#else // !defined(_WIN32)
|
||||||
|
return errno;
|
||||||
|
#endif // defined(_WIN32)
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto is_addr_in_use(int err) -> bool {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return err == WSAEADDRINUSE;
|
||||||
|
#else // !defined(_WIN32)
|
||||||
|
return err == EADDRINUSE;
|
||||||
|
#endif // defined(_WIN32)
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace repertory::ui {
|
namespace repertory::ui {
|
||||||
@@ -140,32 +156,24 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uint16_t port{};
|
|
||||||
if (not utils::get_next_available_port(config_->get_api_port(), port)) {
|
|
||||||
fmt::println("failed to detect if port is available|{}",
|
|
||||||
config_->get_api_port());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port != config_->get_api_port()) {
|
|
||||||
fmt::println("failed to listen on port|{}|next available|{}",
|
|
||||||
config_->get_api_port(), port);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
server_->set_socket_options([](auto &&sock) {
|
server_->set_socket_options([](auto &&sock) {
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
int enable{1};
|
BOOL enable = TRUE;
|
||||||
setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
|
::setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
|
||||||
reinterpret_cast<const char *>(&enable), sizeof(enable));
|
reinterpret_cast<const char *>(&enable), sizeof(enable));
|
||||||
#else // !defined(_WIN32)
|
#else // !defined(_WIN32)!
|
||||||
linger opt{
|
int one = 1;
|
||||||
.l_onoff = 1,
|
::setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
|
||||||
.l_linger = 0,
|
reinterpret_cast<const char *>(&one), sizeof(one));
|
||||||
};
|
#ifdef SO_REUSEPORT
|
||||||
setsockopt(sock, SOL_SOCKET, SO_LINGER,
|
::setsockopt(sock, SOL_SOCKET, SO_REUSEPORT,
|
||||||
reinterpret_cast<const char *>(&opt), sizeof(opt));
|
reinterpret_cast<const char *>(&one), sizeof(one));
|
||||||
#endif // defined(_WIN32)
|
#endif // SO_REUSEPORT
|
||||||
|
#ifdef SO_NOSIGPIPE
|
||||||
|
::setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE,
|
||||||
|
reinterpret_cast<const char *>(&one), sizeof(one));
|
||||||
|
#endif // SO_NOSIGPIPE
|
||||||
|
#endif
|
||||||
});
|
});
|
||||||
|
|
||||||
server_->set_pre_routing_handler(
|
server_->set_pre_routing_handler(
|
||||||
@@ -296,61 +304,100 @@ handlers::handlers(mgmt_app_config *config, httplib::Server *server)
|
|||||||
|
|
||||||
event_system::instance().start();
|
event_system::instance().start();
|
||||||
|
|
||||||
std::thread auto_mount([this]() {
|
{
|
||||||
for (const auto &[prov, names] : config_->get_auto_start_list()) {
|
auto deadline = std::chrono::steady_clock::now() + 30s;
|
||||||
for (const auto &name : names) {
|
std::string host{"127.0.0.1"};
|
||||||
try {
|
auto desired_port = config_->get_api_port();
|
||||||
auto location = config_->get_mount_location(prov, name);
|
for (;;) {
|
||||||
if (location.empty()) {
|
if (server_->bind_to_port(host, desired_port)) {
|
||||||
utils::error::raise_error(function_name,
|
break; // bound successfully; proceed to route registration
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto err = get_last_net_error();
|
||||||
|
if (!is_addr_in_use(err)) {
|
||||||
|
utils::error::raise_error(function_name,
|
||||||
|
utils::error::create_error_message({
|
||||||
|
"failed to bind",
|
||||||
|
"host",
|
||||||
|
host,
|
||||||
|
"port",
|
||||||
|
std::to_string(desired_port),
|
||||||
|
"errno",
|
||||||
|
std::to_string(err),
|
||||||
|
}));
|
||||||
|
return; // abort constructor on non-EADDRINUSE errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::chrono::steady_clock::now() >= deadline) {
|
||||||
|
utils::error::raise_error(function_name,
|
||||||
|
utils::error::create_error_message({
|
||||||
|
"bind timeout (port in use)",
|
||||||
|
"host",
|
||||||
|
host,
|
||||||
|
"port",
|
||||||
|
std::to_string(desired_port),
|
||||||
|
}));
|
||||||
|
return; // abort constructor on timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(250ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread([this]() {
|
||||||
|
for (const auto &[prov, names] : config_->get_auto_start_list()) {
|
||||||
|
for (const auto &name : names) {
|
||||||
|
try {
|
||||||
|
auto location = config_->get_mount_location(prov, name);
|
||||||
|
if (location.empty()) {
|
||||||
|
utils::error::raise_error(function_name,
|
||||||
|
utils::error::create_error_message({
|
||||||
|
"failed to auto-mount",
|
||||||
|
"provider",
|
||||||
|
provider_type_to_string(prov),
|
||||||
|
"name",
|
||||||
|
name,
|
||||||
|
"location is empty",
|
||||||
|
}));
|
||||||
|
} else if (not mount(prov, name, location)) {
|
||||||
|
utils::error::raise_error(function_name,
|
||||||
|
utils::error::create_error_message({
|
||||||
|
"failed to auto-mount",
|
||||||
|
"provider",
|
||||||
|
provider_type_to_string(prov),
|
||||||
|
"name",
|
||||||
|
name,
|
||||||
|
"mount failed",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
utils::error::raise_error(function_name, e,
|
||||||
utils::error::create_error_message({
|
utils::error::create_error_message({
|
||||||
"failed to auto-mount",
|
"failed to auto-mount",
|
||||||
"provider",
|
"provider",
|
||||||
provider_type_to_string(prov),
|
provider_type_to_string(prov),
|
||||||
"name",
|
"name",
|
||||||
name,
|
name,
|
||||||
"location is empty",
|
|
||||||
}));
|
}));
|
||||||
} else if (not mount(prov, name, location)) {
|
} catch (...) {
|
||||||
utils::error::raise_error(function_name,
|
utils::error::raise_error(function_name, "unknown error",
|
||||||
utils::error::create_error_message({
|
utils::error::create_error_message({
|
||||||
"failed to auto-mount",
|
"failed to auto-mount",
|
||||||
"provider",
|
"provider",
|
||||||
provider_type_to_string(prov),
|
provider_type_to_string(prov),
|
||||||
"name",
|
"name",
|
||||||
name,
|
name,
|
||||||
"mount failed",
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (const std::exception &e) {
|
|
||||||
utils::error::raise_error(function_name, e,
|
|
||||||
utils::error::create_error_message({
|
|
||||||
"failed to auto-mount",
|
|
||||||
"provider",
|
|
||||||
provider_type_to_string(prov),
|
|
||||||
"name",
|
|
||||||
name,
|
|
||||||
}));
|
|
||||||
} catch (...) {
|
|
||||||
utils::error::raise_error(function_name, "unknown error",
|
|
||||||
utils::error::create_error_message({
|
|
||||||
"failed to auto-mount",
|
|
||||||
"provider",
|
|
||||||
provider_type_to_string(prov),
|
|
||||||
"name",
|
|
||||||
name,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}).join();
|
||||||
});
|
|
||||||
|
|
||||||
nonce_thread_ =
|
nonce_thread_ =
|
||||||
std::make_unique<std::thread>([this]() { removed_expired_nonces(); });
|
std::make_unique<std::thread>([this]() { removed_expired_nonces(); });
|
||||||
|
|
||||||
auto_mount.join();
|
server_->listen_after_bind();
|
||||||
|
}
|
||||||
|
|
||||||
server_->listen("127.0.0.1", config_->get_api_port());
|
|
||||||
quit_handler(SIGTERM);
|
quit_handler(SIGTERM);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,9 +876,11 @@ auto handlers::launch_process(provider_type prov, std::string_view name,
|
|||||||
::CloseHandle(proc_info.hProcess);
|
::CloseHandle(proc_info.hProcess);
|
||||||
::CloseHandle(proc_info.hThread);
|
::CloseHandle(proc_info.hThread);
|
||||||
}
|
}
|
||||||
#else // !defined(_WIN32)
|
#else // !defined(_WIN32)
|
||||||
args.insert(args.begin(), repertory_binary_);
|
args.insert(args.begin(), repertory_binary_);
|
||||||
|
#if !defined(__APPLE__)
|
||||||
args.insert(std::next(args.begin()), "-f");
|
args.insert(std::next(args.begin()), "-f");
|
||||||
|
#endif // defined(__APPLE_)
|
||||||
|
|
||||||
std::vector<const char *> exec_args;
|
std::vector<const char *> exec_args;
|
||||||
exec_args.reserve(args.size() + 1U);
|
exec_args.reserve(args.size() + 1U);
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ fi
|
|||||||
if [ -f "${PROJECT_FILE_PART}_setup.exe.sig" ]; then
|
if [ -f "${PROJECT_FILE_PART}_setup.exe.sig" ]; then
|
||||||
rm -f "${PROJECT_FILE_PART}_setup.exe.sig" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sig" 1
|
rm -f "${PROJECT_FILE_PART}_setup.exe.sig" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sig" 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -f "${PROJECT_FILE_PART}.dmg" ]; then
|
||||||
|
rm -f "${PROJECT_FILE_PART}.dmg" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe" 1
|
||||||
|
fi
|
||||||
|
if [ -f "${PROJECT_FILE_PART}.dmg.sha256" ]; then
|
||||||
|
rm -f "${PROJECT_FILE_PART}.dmg.sha256" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sha256" 1
|
||||||
|
fi
|
||||||
|
if [ -f "${PROJECT_FILE_PART}.dmg.sig" ]; then
|
||||||
|
rm -f "${PROJECT_FILE_PART}.dmg.sig" || error_exit "failed to delete file: ${PROJECT_FILE_PART}_setup.exe.sig" 1
|
||||||
|
fi
|
||||||
popd
|
popd
|
||||||
|
|
||||||
rsync -av --progress ${PROJECT_DIST_DIR}/ ${TEMP_DIR}/${PROJECT_NAME}/ || error_exit "failed to rsync" 1
|
rsync -av --progress ${PROJECT_DIST_DIR}/ ${TEMP_DIR}/${PROJECT_NAME}/ || error_exit "failed to rsync" 1
|
||||||
@@ -62,7 +72,7 @@ done
|
|||||||
pushd "${TEMP_DIR}/${PROJECT_NAME}/"
|
pushd "${TEMP_DIR}/${PROJECT_NAME}/"
|
||||||
IFS=$'\n'
|
IFS=$'\n'
|
||||||
set -f
|
set -f
|
||||||
FILE_LIST=$(find . -type f)
|
FILE_LIST=$(find . -type f -not -path "*/.app/*")
|
||||||
for FILE in ${FILE_LIST}; do
|
for FILE in ${FILE_LIST}; do
|
||||||
create_file_validations "${FILE}"
|
create_file_validations "${FILE}"
|
||||||
done
|
done
|
||||||
@@ -76,23 +86,63 @@ create_file_validations "${PROJECT_OUT_FILE}"
|
|||||||
popd
|
popd
|
||||||
|
|
||||||
if [ -d "${TEMP_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.app" ]; then
|
if [ -d "${TEMP_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.app" ]; then
|
||||||
APP_SRC="${TEMP_DIR}/${PROJECT_NAME}/${PROJECT_NAME}.app"
|
|
||||||
|
|
||||||
DMG_ROOT="${TEMP_DIR}/dmgroot"
|
DMG_ROOT="${TEMP_DIR}/dmgroot"
|
||||||
mkdir -p "${DMG_ROOT}" || error_exit "failed to create dmgroot" 1
|
mkdir -p "${DMG_ROOT}" || error_exit "failed to create dmgroot" 1
|
||||||
|
|
||||||
rsync -a "${APP_SRC}" "${DMG_ROOT}/" || error_exit "failed to stage app bundle" 1
|
INSTALLER="${DMG_ROOT}/Install ${PROJECT_NAME}.command"
|
||||||
|
INSTALLER_SRC="${PROJECT_SOURCE_DIR}/${PROJECT_NAME}/Install ${PROJECT_NAME}.command"
|
||||||
|
|
||||||
ln -s /Applications "${DMG_ROOT}/Applications" || true
|
if [ -f "${INSTALLER_SRC}" ]; then
|
||||||
|
HIDDEN_DIR="${DMG_ROOT}/.payload"
|
||||||
|
mkdir -p "${HIDDEN_DIR}" || error_exit "failed to create payload dir" 1
|
||||||
|
APP_DEST_DIR="${HIDDEN_DIR}"
|
||||||
|
else
|
||||||
|
APP_DEST_DIR="${DMG_ROOT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync -a "${APP_SRC}" "${APP_DEST_DIR}/" || error_exit "failed to stage app bundle" 1
|
||||||
|
|
||||||
|
if [ -f "${INSTALLER_SRC}" ]; then
|
||||||
|
chflags hidden "${HIDDEN_DIR}" "${HIDDEN_DIR}/${PROJECT_NAME}.app" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
ln -s /Applications "${DMG_ROOT}/Applications" 2>/dev/null || true
|
||||||
|
|
||||||
|
BUNDLE_ID="$(/usr/bin/defaults read "${APP_SRC}/Contents/Info" CFBundleIdentifier 2>/dev/null || true)"
|
||||||
|
if [ -z "${BUNDLE_ID}" ] && [ -f "${APP_SRC}/Contents/Info.plist" ]; then
|
||||||
|
BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP_SRC}/Contents/Info.plist" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
if [ -z "${BUNDLE_ID}" ]; then
|
||||||
|
BUNDLE_ID="${PROJECT_MACOS_BUNDLE_ID:-com.example.${PROJECT_NAME}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${INSTALLER_SRC}" ]; then
|
||||||
|
cp -f "${INSTALLER_SRC}" "${INSTALLER}" || error_exit "failed to copy install command" 1
|
||||||
|
chmod +x "${INSTALLER}" || error_exit "failed to chmod install command" 1
|
||||||
|
|
||||||
|
SAFE_PREFIX="$(printf '%s' "${BUNDLE_ID}" | sed -e 's/[\/&]/\\&/g')"
|
||||||
|
/usr/bin/sed -i '' -e "s|^LABEL_PREFIX=.*$|LABEL_PREFIX=\"${SAFE_PREFIX}\"|g" "${INSTALLER}"
|
||||||
|
|
||||||
|
LABEL_ASSIGNED="$(/usr/bin/awk -F= '/^LABEL_PREFIX=/{sub(/^[^=]*=/,""); gsub(/^"|"$/,""); print; exit}' "${INSTALLER}")"
|
||||||
|
if ! /usr/bin/awk -v v="${LABEL_ASSIGNED}" '
|
||||||
|
BEGIN {
|
||||||
|
if (length(v) == 0) exit 1;
|
||||||
|
if (v == "__LABEL_PREFIX__") exit 1;
|
||||||
|
if (v !~ /^[A-Za-z0-9._-]+$/) exit 1;
|
||||||
|
if (v !~ /\./) exit 1;
|
||||||
|
exit 0;
|
||||||
|
}'; then
|
||||||
|
error_exit "DMG build abort: invalid LABEL_PREFIX written to installer (value: \"${LABEL_ASSIGNED}\"). Check BUNDLE_ID and sed substitution." 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
DMG_OUT="${PROJECT_FILE_PART}.dmg"
|
DMG_OUT="${PROJECT_FILE_PART}.dmg"
|
||||||
|
|
||||||
hdiutil create \
|
hdiutil create \
|
||||||
-volname "${PROJECT_NAME}" \
|
-volname "${PROJECT_NAME}" \
|
||||||
|
-fs HFS+ \
|
||||||
-srcfolder "${DMG_ROOT}" \
|
-srcfolder "${DMG_ROOT}" \
|
||||||
-ov -format UDZO \
|
-ov -format UDZO \
|
||||||
"${PROJECT_DIST_DIR}/${DMG_OUT}" ||
|
"${PROJECT_DIST_DIR}/${DMG_OUT}" || error_exit "hdiutil failed" 1
|
||||||
error_exit "hdiutil failed" 1
|
|
||||||
|
|
||||||
pushd "${PROJECT_DIST_DIR}"
|
pushd "${PROJECT_DIST_DIR}"
|
||||||
create_file_validations "${DMG_OUT}"
|
create_file_validations "${DMG_OUT}"
|
||||||
|
|||||||
Reference in New Issue
Block a user