"""Private probe support; no desktop environment or global process ownership. WHY: failures must consume the same persistent experiment budget as successes. CONTRACT: a stable flock file serializes durable atomic ledger updates. INVARIANT: no reset/refund; only argv execution and owned process groups. """ import fcntl import hashlib import json import os from pathlib import Path import signal import subprocess import tempfile import time REPO = Path(__file__).resolve().parents[3] LIMITS = {'scene_build': 1, 'mosaic_build': 1, 'stage': 1, 'notices': 1, 'elf': 1, 'run': 1, 'control': 1, 'capture': 8, 'camera': 1} def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest() def save(lab, name, value): (Path(lab)/'results'/(name+'.json')).write_text(json.dumps(value, indent=2)+'\n') def reserve(lab, kind): lab = Path(lab) if kind not in LIMITS: raise ValueError(kind) with (lab/'budget.lock').open('a+') as lock: fcntl.flock(lock, fcntl.LOCK_EX) path = lab/'results/budget.json' state = json.loads(path.read_text()) if path.exists() else { 'limits': LIMITS, 'used': dict.fromkeys(LIMITS, 0), 'events': []} if state['limits'] != LIMITS or state['used'][kind] >= LIMITS[kind]: raise RuntimeError('budget exhausted or changed: '+kind) state['used'][kind] += 1 state['events'].append({'sequence': len(state['events']), 'kind': kind}) fd, temporary = tempfile.mkstemp(prefix='.budget-', dir=path.parent) with os.fdopen(fd, 'w') as out: json.dump(state, out, indent=2); out.write('\n'); out.flush(); os.fsync(out.fileno()) os.replace(temporary, path) fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(fd) finally: os.close(fd) return state def environment(): env = {k:v for k,v in os.environ.items() if k not in ( 'SWAYSOCK','I3SOCK','WAYLAND_DISPLAY','DISPLAY','XDG_RUNTIME_DIR', 'LD_LIBRARY_PATH','LD_PRELOAD','PKG_CONFIG_PATH')} env.update(LC_ALL='C', PYTHONDONTWRITEBYTECODE='1', MESON_ROOT_CMD='/bin/false') return env def command(lab, label, argv, cwd=None, extra=None, timeout=600, allow_failure=False): lab=Path(lab); path=lab/'raw'/(label+'.log') assert not path.exists(), 'diagnostic already recorded: '+label with path.open('wb') as out: proc=subprocess.Popen(list(map(str,argv)),cwd=cwd,env={**environment(),**(extra or {})}, stdout=out,stderr=subprocess.STDOUT,start_new_session=True) try: deadline=time.monotonic()+timeout while proc.poll() is None: if time.monotonic()>=deadline or path.stat().st_size>64*1024*1024: raise subprocess.TimeoutExpired(argv,timeout) time.sleep(.1) except subprocess.TimeoutExpired: os.killpg(proc.pid,signal.SIGTERM) try: proc.wait(timeout=3) except subprocess.TimeoutExpired: os.killpg(proc.pid,signal.SIGKILL);proc.wait() try: os.killpg(proc.pid,signal.SIGKILL) except ProcessLookupError: pass raise with (lab/'raw/commands.jsonl').open('a') as out: out.write(json.dumps({'label':label,'argv':list(map(str,argv)), 'cwd':str(cwd), 'env':extra or {},'exit':proc.returncode})+'\n') assert path.stat().st_size < 64*1024*1024, 'output bound exceeded' if not allow_failure and proc.returncode: raise RuntimeError(f'{label} exit={proc.returncode}; inspect {path}') return path.read_text(errors='replace')