diff --git a/slab/deskflow-handoff/README.md b/slab/deskflow-handoff/README.md index a70ec6559e..f54c259df2 100644 --- a/slab/deskflow-handoff/README.md +++ b/slab/deskflow-handoff/README.md @@ -121,6 +121,13 @@ controllers in server mode. Login/resume and the 45-second watchdog also compare controller generations: the newer server reasserts the same generation to missing clients, while an older server demotes itself. +Claim, reconciliation, and role changes use kernel locks through `lockf`. +Process exit or reboot releases them automatically; the persistent `.flock` +files do not themselves mean a lock is held. Detached work closes the lock's +file descriptor. The former empty-directory locks could survive a reboot +indefinitely. The installer also disables the obsolete studio watcher, whose +server-only check repeatedly restarted healthy clients. + `install.sh` installs one machine. `UNIPOINTER_PREBUILT` can supply an existing compatible binary when the destination's Swift compiler and SDK do not match. `deploy.fish` installs the five-Mac Neo/Blueberry/Chicken/Panda/Frisbee topology diff --git a/slab/deskflow-handoff/deskflow-claim-control b/slab/deskflow-handoff/deskflow-claim-control index 35c6cd7dbc..1495e2809e 100755 --- a/slab/deskflow-handoff/deskflow-claim-control +++ b/slab/deskflow-handoff/deskflow-claim-control @@ -6,6 +6,7 @@ SET_ROLE="$HOME/.local/bin/deskflow-set-role" RETARGET="$HOME/.local/bin/deskflow-retarget-client" RECONCILE="$HOME/.local/bin/deskflow-reconcile-topology" LOCK="$HOME/.config/slab/deskflow-claim.lock" +source "$(dirname "$0")/deskflow-lock" if [[ ! -f "$CONFIG" ]]; then echo "deskflow claim: no handoff config" >&2 @@ -40,7 +41,7 @@ if [[ "$CONTROLLER" != "true" ]]; then fi ROLE=${VALUES[1]:-} -if ! mkdir "$LOCK" 2>/dev/null; then +if ! deskflow_lock_acquire "$LOCK"; then echo "deskflow claim: already switching" >&2 exit 75 fi @@ -49,7 +50,7 @@ PEER_OUT="" cleanup() { [[ -n "$LOCAL_OUT" ]] && rm -f "$LOCAL_OUT" [[ -n "$PEER_OUT" ]] && rm -f "$PEER_OUT" - rmdir "$LOCK" 2>/dev/null || true + deskflow_lock_release } trap cleanup EXIT @@ -90,20 +91,20 @@ CORE_LOG="$HOME/Library/Logs/deskflow-core.log" # data. A line offset made BSD tail rescan the full, long-lived core log on # every 25 ms poll and could stretch a three-second handoff into minutes. SERVER_LOG_START=$(stat -f %z "$CORE_LOG" 2>/dev/null || echo 0) -"$SET_ROLE" server "" "$EPOCH" > "$LOCAL_OUT" 2>&1 & +"$SET_ROLE" server "" "$EPOCH" > "$LOCAL_OUT" 2>&1 9>&- & LOCAL_PID=$! if [[ "$ROLE" == "server" ]]; then peer_transition "${PEERS[0]}" \ - "~/.local/bin/deskflow-set-role client '$ADDRESS' '$EPOCH' '$SCREEN_NAME'" > "$PEER_OUT" 2>&1 & + "~/.local/bin/deskflow-set-role client '$ADDRESS' '$EPOCH' '$SCREEN_NAME'" > "$PEER_OUT" 2>&1 9>&- & else peer_transition "${PEERS[0]}" \ - "~/.local/bin/deskflow-yield-control '$ADDRESS' '$EPOCH' '$SCREEN_NAME'" > "$PEER_OUT" 2>&1 & + "~/.local/bin/deskflow-yield-control '$ADDRESS' '$EPOCH' '$SCREEN_NAME'" > "$PEER_OUT" 2>&1 9>&- & fi PEER_PID=$! LOG="$HOME/Library/Logs/deskflow-handoff.log" for peer in "${PEERS[@]:1}"; do - nohup "$RETARGET" "$peer" "$ADDRESS" "$EPOCH" "$SCREEN_NAME" >> "$LOG" 2>&1 & + nohup "$RETARGET" "$peer" "$ADDRESS" "$EPOCH" "$SCREEN_NAME" >> "$LOG" 2>&1 9>&- & done LOCAL_STATUS=0 diff --git a/slab/deskflow-handoff/deskflow-lock b/slab/deskflow-handoff/deskflow-lock new file mode 100755 index 0000000000..5df04983a4 --- /dev/null +++ b/slab/deskflow-handoff/deskflow-lock @@ -0,0 +1,15 @@ +#!/bin/bash +# Source from the role scripts. The kernel releases this lock when the process +# exits, including SIGKILL/reboot. Keep the file so contenders share one inode. +# Detached work must close fd 9 so it cannot outlive the owning transaction. +deskflow_lock_acquire() { + exec 9> "$1.flock" + if ! /usr/bin/lockf -s -t "${2:-0}" 9; then + exec 9>&- + return 1 + fi +} + +deskflow_lock_release() { + exec 9>&- +} diff --git a/slab/deskflow-handoff/deskflow-reconcile-topology b/slab/deskflow-handoff/deskflow-reconcile-topology index 0bfbd48e17..329553d746 100755 --- a/slab/deskflow-handoff/deskflow-reconcile-topology +++ b/slab/deskflow-handoff/deskflow-reconcile-topology @@ -9,6 +9,7 @@ SET_ROLE="$HOME/.local/bin/deskflow-set-role" RETARGET="$HOME/.local/bin/deskflow-retarget-client" LOCK="$HOME/.config/slab/deskflow-reconcile.lock" LOG="$HOME/Library/Logs/deskflow-handoff.log" +source "$(dirname "$0")/deskflow-lock" if [[ ! -f "$HANDOFF" || ! -f "$STATE" ]]; then exit 0 @@ -48,10 +49,10 @@ PEERS=("${VALUES[@]:5}") [[ "$CONTROLLER" == "true" ]] || exit 0 [[ ${#PEERS[@]} -ge 1 && -n "$ADDRESS" ]] || exit 0 -if ! mkdir "$LOCK" 2>/dev/null; then +if ! deskflow_lock_acquire "$LOCK"; then exit 0 fi -trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT +trap deskflow_lock_release EXIT SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=4 -o ConnectionAttempts=1 -o ControlMaster=no -o ControlPath=none) @@ -129,7 +130,7 @@ fi for peer in "${PEERS[@]:1}"; do targets+=("$peer"); done for peer in "${targets[@]}"; do - "$RETARGET" "$peer" "$ADDRESS" "$EPOCH" >> "$LOG" 2>&1 & + "$RETARGET" "$peer" "$ADDRESS" "$EPOCH" >> "$LOG" 2>&1 9>&- & pids+=("$!") done diff --git a/slab/deskflow-handoff/deskflow-set-role b/slab/deskflow-handoff/deskflow-set-role index fb381ff606..2244e4e7da 100755 --- a/slab/deskflow-handoff/deskflow-set-role +++ b/slab/deskflow-handoff/deskflow-set-role @@ -16,6 +16,7 @@ EPOCH_FILE="$HOME/.config/slab/deskflow-role-epoch" ROLE_LOCK="$HOME/.config/slab/deskflow-set-role.lock" LABEL="computer.aesthetic.deskflow" UID_=$(id -u) +source "$(dirname "$0")/deskflow-lock" case "$ROLE" in server) ;; @@ -36,22 +37,11 @@ mkdir -p "$HOME/.config/slab" "$HOME/Library/Deskflow" # Claims from the two physical controllers can overlap. Serialize the epoch # check, state write, and core restart as one transaction; otherwise an older # claim that passed the check first can finish last and resurrect split-brain. -LOCKED=false -for _ in {1..500}; do - if /usr/bin/shlock -f "$ROLE_LOCK" -p "$$"; then - LOCKED=true - break - fi - sleep 0.01 -done -if [[ "$LOCKED" != "true" ]]; then +if ! deskflow_lock_acquire "$ROLE_LOCK" 5; then echo "deskflow-set-role: role transaction lock timed out" >&2 exit 75 fi -cleanup_role_lock() { - rm -f "$ROLE_LOCK" -} -trap cleanup_role_lock EXIT +trap deskflow_lock_release EXIT if ! [[ "$EPOCH" =~ ^[0-9]+$ ]]; then echo "deskflow-set-role: invalid claim generation" >&2 @@ -158,7 +148,7 @@ fi if [[ "$ROLE" == "client" ]]; then # Headless display clients can report an invalid screen shape while their # monitor is asleep. A brief user-activity pulse makes the first edge usable. - /usr/bin/caffeinate -u -t 2 >/dev/null 2>&1 & + /usr/bin/caffeinate -u -t 2 >/dev/null 2>&1 9>&- & fi /bin/launchctl kill SIGKILL "gui/${UID_}/${LABEL}" 2>/dev/null || true sleep 0.02 diff --git a/slab/deskflow-handoff/install.sh b/slab/deskflow-handoff/install.sh index 7f34674021..3e1ab4b243 100755 --- a/slab/deskflow-handoff/install.sh +++ b/slab/deskflow-handoff/install.sh @@ -56,7 +56,7 @@ write_fingerprints() { done } -for file in deskflow-role-runner deskflow-set-role deskflow-role-state deskflow-retarget-client deskflow-reconcile-topology deskflow-claim-control deskflow-role-watchdog deskflow-seat-ready deskflow-active-screen deskflow-yield-control deskflow-start deskflow-resolve-ipv4; do +for file in deskflow-lock deskflow-role-runner deskflow-set-role deskflow-role-state deskflow-retarget-client deskflow-reconcile-topology deskflow-claim-control deskflow-role-watchdog deskflow-seat-ready deskflow-active-screen deskflow-yield-control deskflow-start deskflow-resolve-ipv4; do cp "$HERE/$file" "$HOME/.local/bin/$file" chmod 755 "$HOME/.local/bin/$file" done @@ -148,10 +148,13 @@ STANDBY="$HOME/Library/LaunchAgents/computer.aesthetic.deskflow-standby-server.p launchctl bootout "gui/${UID_}/computer.aesthetic.deskflow-standby-server" 2>/dev/null || true rm -f "$HOME/.config/slab/deskflow-standby-enabled" "$STANDBY" -for old in computer.aesthetic.deskflow-watchdog computer.aesthetic.deskflow-server-watchdog; do +# The pre-handoff studio watcher treats a healthy client as a dead server and +# restarts it every two minutes. Only the role-aware watchdog should own this. +for old in computer.aesthetic.deskflow-watchdog computer.aesthetic.deskflow-server-watchdog computer.aesthetic.deskflow-studio-watch; do launchctl bootout "gui/${UID_}/${old}" 2>/dev/null || true done launchctl disable "gui/${UID_}/computer.aesthetic.deskflow-server-watchdog" 2>/dev/null || true +launchctl disable "gui/${UID_}/computer.aesthetic.deskflow-studio-watch" 2>/dev/null || true WATCHDOG="$HOME/Library/LaunchAgents/computer.aesthetic.deskflow-watchdog.plist" cat > "$WATCHDOG" <