#!/usr/bin/env bash # Serialises the two things that must not overlap on this Mac: BUILDS (cargo, swiftc, zigbuild, large cross-compiles) and # MEASUREMENTS (hash-rate samples, millisecond latencies, power: anything a build on the same machine could disturb). # tools/lock/with-lock.sh build one of THREE build slots (each build is nice -n 19 with 4 jobs, so # three at once use 12 of the 16 cores); waits when all are busy # tools/lock/with-lock.sh measure exclusive: waits for every build slot and blocks new builds # tools/lock/with-lock.sh run a functional run (test network, attack harness, simulator whose # outputs are counts, locks, forks, seconds): one of THREE run slots, # builds continue; a measurement waits for all of them # tools/lock/with-lock.sh status who holds what, and for how long # Rule (4 October 2026): code and documents run in parallel; builds share three slots; measurements are exclusive. # Evening of 4 October: one build slot and a 3-hour network run under `measure` left twelve agents queued; hence the # three slots, the `run` mode and `status`. Waits up to 2 hours. This file is replaced atomically (never edited in # place) because waiting instances are still reading it. set -euo pipefail kind="${1:-}"; shift || true dir=/tmp/igneum-locks; mkdir -p "$dir" if [ "$kind" = status ]; then for f in measure build build-0 build-1 build-2 run-0 run-1 run-2; do [ -e "$dir/$f" ] || continue holder=$(python3 - "$dir/$f" <<'PY' import fcntl, sys, os fd=os.open(sys.argv[1], os.O_RDWR) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB); fcntl.flock(fd, fcntl.LOCK_UN); print("free") except BlockingIOError: print("HELD") PY ) printf '%-8s %s' "$f" "$holder" if [ "$holder" = HELD ]; then # the holder wrote its pid and start time into the lock file printf ' %s' "$(tr '\n' ' ' < "$dir/$f" | cut -c1-140)" fi echo done echo "waiting:"; ps -eo pid,etime,command | grep -E "[w]ith-lock.sh (build|measure|run) " | grep -v grep | awk '{printf " %s %s %s %s\n", $1, $2, $4, $5}' | head -20 exit 0 fi case "$kind" in build) mode=build;; measure) mode=measure;; run) mode=run;; *) echo "usage: with-lock.sh build|measure|run | status" >&2; exit 2;; esac # Python holds the locks for the lifetime of this process: it writes the holder line, then execs the command with the # file descriptors inherited (flock locks follow the open file, so the command keeps them until it exits). exec python3 - "$mode" "$dir" "$@" <<'PY' import fcntl, sys, time, os mode, d = sys.argv[1], sys.argv[2]; cmd = sys.argv[3:] t0 = time.time() def opened(name): fd = os.open(os.path.join(d, name), os.O_RDWR | os.O_CREAT, 0o644) os.set_inheritable(fd, True); return fd def try_lock(fd): try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB); return True except BlockingIOError: return False def wait_lock(fd, name): while not try_lock(fd): if time.time() - t0 > 7200: print(f"with-lock: gave up waiting for {name} after 2 h", file=sys.stderr); sys.exit(1) time.sleep(5) held = [] if mode == 'build': # the number of build slots open to NEW builds comes from ~/.config/igneum/build-slots (1 to 3, default 3): 1 when the # owner needs the Mac responsive (5 October 2026), builds already running keep their slot try: nslots = max(1, min(3, int(open(os.path.expanduser('~/.config/igneum/build-slots')).read().strip()))) except Exception: nslots = 3 slots = [opened(f"build-{k}") for k in range(nslots)] got = None while got is None: for k, fd in enumerate(slots): if try_lock(fd): got = (k, fd); break if got is None: if time.time() - t0 > 7200: print("with-lock: gave up waiting for a build slot after 2 h", file=sys.stderr); sys.exit(1) time.sleep(5) for k, fd in enumerate(slots): if got[0] != k: os.close(fd) held = [(f"build-{got[0]}", got[1])] elif mode == 'measure': for name in ['measure', 'build', 'build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2']: fd = opened(name); wait_lock(fd, name); held.append((name, fd)) else: # run: one of THREE run slots (functional runs on their own port ranges can overlap), none while a measurement # holds every slot; a measurement waits for every run slot in turn slots = [opened(f"run-{k}") for k in range(3)] got = None while got is None: for k, fd in enumerate(slots): if try_lock(fd): got = (k, fd); break if got is None: if time.time() - t0 > 7200: print("with-lock: gave up waiting for a run slot after 2 h", file=sys.stderr); sys.exit(1) time.sleep(5) for k, fd in enumerate(slots): if got[0] != k: os.close(fd) held = [(f"run-{got[0]}", got[1])] for name, fd in held: os.ftruncate(fd, 0); os.lseek(fd, 0, 0) os.write(fd, f"pid {os.getpid()} since {time.strftime('%H:%M:%S', time.gmtime())}Z waited {int(time.time()-t0)} s: {' '.join(cmd)[:120]}\n".encode()) print(f"with-lock: holding {' '.join(n for n, _ in held)} for: {' '.join(cmd)}", file=sys.stderr) os.execvp(cmd[0], cmd) PY