myxce/src/tools/test-build-lock.sh
Rongsong Shen 980a0054d1
fix(tools): one xce-build.sh run per build dir at a time (bug-1604)
Two xce-build.sh runs against the same build dir raced: one run's
setup_build rm -rf hit files the other was writing ('Directory not
empty'), or deleted a binary the other had just built and tested.

xce-build.sh now takes an exclusive per-build-dir lock right after cd-ing
into src/ and holds it until it exits; a second run against the same dir
waits, runs against different dirs are unaffected. The lock
(src/tools/build-lock.sh) is an mkdir lock holding the owner PID, outside
the build dir, keyed by a hash of its absolute path; it needs only
coreutils (the nix dev shell has no flock) and takes over a lock whose
owner died. src/tools/test-build-lock.sh tests it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:43:48 +00:00

116 lines
3.8 KiB
Bash

#!/bin/bash
# test-build-lock.sh — bug-1604: concurrent xce-build.sh runs against the same
# build dir must not race on its rm -rf / meson setup.
#
# Exercises src/tools/build-lock.sh (sourced by xce-build.sh) directly, so no
# real build is needed:
# 1. a second acquire of the SAME build dir waits until the first releases
# 2. a lock left by a dead process is taken over, not waited on forever
# 3. DIFFERENT build dirs never block each other
# 4. xce-build.sh takes the lock before setup_build's rm -rf
#
# Usage:
# bash src/tools/test-build-lock.sh
set -u
TOOLS=$(cd "$(dirname "$0")" && pwd)
PASS=0
FAIL=0
green() { printf '\033[32m%s\033[0m\n' "$*"; }
red() { printf '\033[31m%s\033[0m\n' "$*"; }
pass() { green " PASS: $*"; PASS=$((PASS+1)); }
fail() { red " FAIL: $*"; FAIL=$((FAIL+1)); }
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
export XCE_BUILD_LOCK_ROOT="$WORK/locks"
export XCE_BUILD_LOCK_POLL_SECS=0.1
mkdir -p "$XCE_BUILD_LOCK_ROOT"
if [ ! -f "$TOOLS/build-lock.sh" ]; then
fail "src/tools/build-lock.sh does not exist"
echo "Results: ${PASS} passed, ${FAIL} failed"
exit 1
fi
# A holder: takes the lock on $1, records when it has it, holds it for $2 s,
# records the release, exits.
holder() {
(
. "$TOOLS/build-lock.sh"
build_lock_acquire "$1" >/dev/null 2>&1
echo "$3 got $(date +%s.%N)" >> "$WORK/events"
sleep "$2"
echo "$3 released $(date +%s.%N)" >> "$WORK/events"
build_lock_release
)
}
event_time() { awk -v w="$1 $2" '$1" "$2 == w {print $3}' "$WORK/events"; }
echo "── 1. same build dir: the second acquire waits for the first ──"
: > "$WORK/events"
holder "$WORK/b1" 1 A &
sleep 0.3
holder "$WORK/b1" 0 B &
wait
a_rel=$(event_time A released)
b_got=$(event_time B got)
if [ -n "$a_rel" ] && [ -n "$b_got" ] && \
awk -v a="$a_rel" -v b="$b_got" 'BEGIN { exit !(b >= a) }'; then
pass "B got the lock only after A released it"
else
fail "B got the lock while A still held it (A released=$a_rel, B got=$b_got)"
fi
echo "── 2. a lock left by a dead process is taken over ──"
: > "$WORK/events"
(
. "$TOOLS/build-lock.sh"
build_lock_acquire "$WORK/b2" >/dev/null 2>&1
# exit WITHOUT releasing, like a build killed with SIGKILL
)
start=$(date +%s)
holder "$WORK/b2" 0 C &
cpid=$!
( sleep 10; kill "$cpid" 2>/dev/null ) &
watchdog=$!
wait "$cpid" 2>/dev/null
kill "$watchdog" 2>/dev/null
if [ -n "$(event_time C got)" ] && [ $(( $(date +%s) - start )) -lt 10 ]; then
pass "a stale lock from a dead owner was taken over"
else
fail "waited on a lock whose owner is dead"
fi
echo "── 3. different build dirs do not block each other ──"
: > "$WORK/events"
holder "$WORK/b3" 2 D &
sleep 0.3
start=$(date +%s.%N)
holder "$WORK/b4" 0 E &
wait
e_got=$(event_time E got)
if [ -n "$e_got" ] && awk -v s="$start" -v e="$e_got" 'BEGIN { exit !(e - s < 1.5) }'; then
pass "a lock on another build dir was granted immediately"
else
fail "a lock on another build dir waited for an unrelated holder"
fi
echo "── 4. xce-build.sh takes the lock before setup_build's rm -rf ──"
lock_line=$(grep -n '^build_lock_acquire ' "$TOOLS/xce-build.sh" | head -1 | cut -d: -f1)
setup_line=$(grep -n '^setup_build() {' "$TOOLS/xce-build.sh" | head -1 | cut -d: -f1)
first_call=$(grep -n '^ *setup_build$' "$TOOLS/xce-build.sh" | head -1 | cut -d: -f1)
if grep -q 'build-lock.sh' "$TOOLS/xce-build.sh" && [ -n "$lock_line" ] && \
[ -n "$first_call" ] && [ "$lock_line" -lt "$first_call" ]; then
pass "xce-build.sh acquires the build-dir lock (line $lock_line) before the first setup_build call (line $first_call)"
else
fail "xce-build.sh does not acquire the build-dir lock before setup_build (lock=$lock_line setup_build()=$setup_line first call=$first_call)"
fi
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ]