# helpers.sh: admission control for parallel xcodebuild runs. Source it from bash or zsh:  . ./helpers.sh
#
#   with_worktree_lock DIR CMD...  one build or package mutation at a time per worktree (waits for it)
#   build_slot CMD...              at most XCB_SLOTS (default 2) commands at once on this Mac (waits for one)
#   with_simulator CMD...          runs CMD with SIM_UDID set to a free device from $SIM_POOL_DIR/pool.txt
#
# Take locks in this order to avoid deadlocks: worktree, then Simulator, then build slot.
# Each helper locks an open file descriptor with lockf(1) (fds 7, 8 and 9), runs CMD with that descriptor
# closed and returns CMD's exit status unchanged. Lock contention is never reported as CMD's status, and the
# helpers behave the same with or without `set -e`. A helper returns 2 for its own setup errors.
# Waiting callers poll every 2 seconds; they are not served in arrival order. This is an illustration of
# admission control, not a production scheduler.

XCB_LOCK_DIR="${XCB_LOCK_DIR:-${TMPDIR:-/tmp}/xcb-locks}"

with_worktree_lock() {
  local dir="$1" key rc st
  shift
  [ -d "$dir" ] || { echo "with_worktree_lock: no such directory: $dir" >&2; return 2; }
  mkdir -p "$XCB_LOCK_DIR" || return 2
  key=$(cd "$dir" && pwd -P | shasum | cut -c1-16)
  exec 8>>"$XCB_LOCK_DIR/worktree-$key.lock" || return 2
  if lockf -s 8; then :; else st=$?; exec 8>&-; echo "with_worktree_lock: lockf failed ($st)" >&2; return 2; fi
  if "$@" 8>&-; then rc=0; else rc=$?; fi
  exec 8>&-
  return "$rc"
}

build_slot() {
  local slots="${XCB_SLOTS:-2}" i rc st
  case "$slots" in ''|*[!0-9]*) slots=0 ;; esac
  [ "$slots" -gt 0 ] || { echo "build_slot: XCB_SLOTS must be a positive integer" >&2; return 2; }
  mkdir -p "$XCB_LOCK_DIR" || return 2
  while :; do
    i=1
    while [ "$i" -le "$slots" ]; do
      exec 9>>"$XCB_LOCK_DIR/slot-$i.lock" || return 2
      if lockf -s -t 0 9; then
        if "$@" 9>&-; then rc=0; else rc=$?; fi
        exec 9>&-
        return "$rc"
      else
        st=$?
        exec 9>&-
        [ "$st" -eq 75 ] || { echo "build_slot: lockf failed ($st)" >&2; return 2; }   # 75: slot busy
      fi
      i=$((i + 1))
    done
    sleep 2
  done
}

with_simulator() {
  local dir="${SIM_POOL_DIR:-}" udid free rc st
  [ -n "$dir" ] || { echo "with_simulator: export SIM_POOL_DIR first" >&2; return 2; }
  [ -s "$dir/pool.txt" ] || { echo "with_simulator: $dir/pool.txt is missing or empty" >&2; return 2; }
  while :; do
    free=0
    for udid in $(cat "$dir/pool.txt"); do
      if [ -f "$dir/quarantine.txt" ] && grep -qx "$udid" "$dir/quarantine.txt"; then continue; fi
      free=$((free + 1))
      exec 7>>"$dir/$udid.lock" || return 2
      if lockf -s -t 0 7; then
        if (export SIM_UDID="$udid"; "$@") 7>&-; then rc=0; else rc=$?; fi
        exec 7>&-
        return "$rc"
      else
        st=$?
        exec 7>&-
        [ "$st" -eq 75 ] || { echo "with_simulator: lockf failed ($st)" >&2; return 2; }   # 75: device leased
      fi
    done
    [ "$free" -gt 0 ] || { echo "with_simulator: every device in the pool is quarantined" >&2; return 2; }
    sleep 2
  done
}
