diff --git a/roadscore/prototype/native_receiver.sh b/roadscore/prototype/native_receiver.sh index 6da87a3cfb..0ea31cded2 100755 --- a/roadscore/prototype/native_receiver.sh +++ b/roadscore/prototype/native_receiver.sh @@ -26,7 +26,8 @@ if [ "${ROADSCORE_COMPOSER:-ace}" = ace ]; then preparation_wait=1500; fi if ! pgrep -f "^/data/sa3-feasibility/venv/bin/python -u ${worker_script}$" >/dev/null; then worker_reused=0 rm -f generated/worker_ready - setsid env -u OPENPILOT_PREFIX /usr/local/venv/bin/python -u prototype/power_worker.py > results/native_worker.log 2>&1 < /dev/null & + # Only this receiver owns fd 9; the warm resident must not retain its session lock. + setsid env -u OPENPILOT_PREFIX /usr/local/venv/bin/python -u prototype/power_worker.py > results/native_worker.log 2>&1 < /dev/null 9>&- & power_pid=$! if [ "${ROADSCORE_RESIDENT:-0}" = 1 ]; then /usr/local/venv/bin/python -c 'import json,sys,time; from pathlib import Path; p=int(sys.argv[1]); Path("generated/resident_owner.json").write_text(json.dumps({"power_worker_pid":p,"process_start_ticks":Path(f"/proc/{p}/stat").read_text().split()[21],"created_wall":time.time(),"stop":"SIGTERM power_worker_pid; it restores CPU and stops its child"}))' "$power_pid" @@ -55,7 +56,7 @@ mkdir -p results/current /usr/local/venv/bin/python prototype/runtime_manifest.py audio_args=() if [ "${ROADSCORE_AUDIBLE:-0}" = 1 ]; then audio_args+=(--audible); fi -/usr/local/venv/bin/python -u prototype/app.py "${audio_args[@]}" > results/current/app.log 2>&1 < /dev/null & +/usr/local/venv/bin/python -u prototype/app.py "${audio_args[@]}" > results/current/app.log 2>&1 < /dev/null 9>&- & audio_pid=$! for attempt in $(seq 1 30); do [ -f results/current/ready ] && break @@ -65,7 +66,7 @@ done [ -f results/current/ready ] || { cat results/current/app.log; exit 1; } # Supervise score failures during delivery rather than leaving silent replay running. exec 8<&0 -/usr/local/venv/bin/python -u prototype/replay_bridge.py receive --route "$1" <&8 8<&- & +/usr/local/venv/bin/python -u prototype/replay_bridge.py receive --route "$1" <&8 8<&- 9>&- & bridge_pid=$! exec 8<&- while kill -0 "$bridge_pid" 2>/dev/null; do diff --git a/roadscore/prototype/test_native_receiver_lock.py b/roadscore/prototype/test_native_receiver_lock.py new file mode 100644 index 0000000000..f5a24d4dca --- /dev/null +++ b/roadscore/prototype/test_native_receiver_lock.py @@ -0,0 +1,48 @@ +"""Local regression for the session descriptor inherited by detached workers.""" +import fcntl +import os +from pathlib import Path +import shlex +import signal +import subprocess +import sys +import tempfile +import unittest + + +class NativeReceiverLockTests(unittest.TestCase): + def test_every_background_child_closes_session_descriptor(self): + source = Path(__file__).with_name('native_receiver.sh').read_text() + launches = [line for line in source.splitlines() if line.rstrip().endswith(' &')] + self.assertEqual(len(launches), 3) + for line in launches: + self.assertIn('9>&-', line, line) + self.assertIn('resident_keep=1', source) + self.assertNotIn('rm -f generated/native_session.lock', source) + + def test_live_child_cannot_retain_exited_receivers_lock(self): + source = Path(__file__).with_name('native_receiver.sh').read_text() + launch = next(line for line in source.splitlines() if 'setsid env ' in line) + # Exercise the exact fd-close redirection on the resident launch locally. + redirect = next(token for token in launch.split() if token == '9>&-') + with tempfile.TemporaryDirectory() as directory: + lock = Path(directory) / 'session.lock' + child_pid = Path(directory) / 'child.pid' + acquire = 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' + script = f'''exec 9>{shlex.quote(str(lock))} +{shlex.quote(sys.executable)} -c {shlex.quote(acquire)} +sleep 30 {redirect} /dev/null 2>&1 & +echo $! >{shlex.quote(str(child_pid))} +''' + subprocess.run(['bash', '-c', script], check=True, timeout=5) + pid = int(child_pid.read_text()) + try: + os.kill(pid, 0) # resident surrogate stays warm/alive + with lock.open('w') as stream: + fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.kill(pid, signal.SIGTERM) + + +if __name__ == '__main__': + unittest.main()