79 lines
5.4 KiB
Python
79 lines
5.4 KiB
Python
"""Derived EXPERIMENTAL Session, preserving sealed historical engine bytes."""
|
|
import importlib.util,sys,os,re,time,shutil,signal,subprocess
|
|
from pathlib import Path
|
|
import ledger
|
|
from plans import HIST,BASE
|
|
# WHY CONTRACT INVARIANT: historical imports have their own plans/gate names;
|
|
# load them in a temporary namespace and restore this campaign's modules.
|
|
def historical(name):
|
|
saved={k:sys.modules.get(k) for k in ('plans','gate','engine','validators')};old=sys.path[:]
|
|
try:
|
|
for k in saved:sys.modules.pop(k,None)
|
|
sys.path[:0]=[str(HIST),str(BASE)]
|
|
spec=importlib.util.spec_from_file_location('historical_'+name,HIST/(name+'.py'));m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
|
|
finally:
|
|
sys.path[:]=old
|
|
for k,v in saved.items():
|
|
sys.modules.pop(k,None)
|
|
if v is not None:sys.modules[k]=v
|
|
engine=historical('engine')
|
|
cases=historical('cases')
|
|
SessionBase=engine.Session
|
|
class Session(SessionBase):
|
|
def debit(self,*kinds):ledger.action_debit(self.lab,self.spec['id'],*kinds)
|
|
def spawn(self,name,argv,debug=False):
|
|
argv=list(map(str,argv))
|
|
if name=='compositor':argv=[a.replace('txn-timeout=2500','txn-timeout=5000') for a in argv]
|
|
p=super().spawn(name,argv,debug)
|
|
self.persist_identity('spawn');return p
|
|
def persist_identity(self,phase):
|
|
ledger.durable(self.run/'owned-runtime.json',dict(phase=phase,uid=os.getuid(),runtime=str(self.runtime),runtime_mode=oct(self.runtime.stat().st_mode&0o777) if self.runtime.exists() else None,socket=str(getattr(self,'sock','')),processes=[dict(name=n,pid=p.pid,exit=p.poll()) for n,p in self.proc.items()],ns=time.monotonic_ns()))
|
|
def start(self):
|
|
if self.spec['variant']=='C3':self.env['SAVED_DIAGNOSTIC']='1'
|
|
super().start();self.persist_identity('socket-bound')
|
|
def inject(self,action,point,grab=False):
|
|
# No settling sleep: mutation during delivery is the measured phenomenon.
|
|
engine.need(action in ('move','left','right'),'action');outs=self.ipc('-t','get_outputs');rects=[[o['rect'][k] for k in ('x','y','width','height')] for o in outs if o['active']]
|
|
engine.need(any(engine.contains(r,point) for r in rects),'inside output')
|
|
x=min(r[0] for r in rects);y=min(r[1] for r in rects);w=max(r[0]+r[2] for r in rects)-x;h=max(r[1]+r[3] for r in rects)-y
|
|
self.debit('move',*(['click'] if action!='move' else []),*(['grab'] if grab else []));sent=[point[0]-x,point[1]-y];marker=f'INJECT_DONE {action} {sent[0]},{sent[1]}';before=self.log('pointer').count(marker);start=time.monotonic_ns()
|
|
self.pointer.stdin.write(f'{action} {sent[0]} {sent[1]} {w} {h}\n');self.pointer.stdin.flush();self.wait(lambda:self.log('pointer').count(marker)>before)
|
|
self.record('injection',action=action,global_logical=point,sent=sent,logical_extent=[w,h],grab=grab,start_ns=start,end_ns=time.monotonic_ns())
|
|
def close_client(self,name,command='close_owner'):
|
|
self.client_cmd(name,command);code=self.proc[name].wait(timeout=3);engine.need(code==0,'client exit0');self.wait(lambda:self.find(name) is None)
|
|
exits=re.findall(r'EXIT[^\n]*error=(\d+)',self.log(name));engine.need(all(e=='0' for e in exits),'Wayland error0 required even with process exit0')
|
|
if not exits:self.record('client-error-observability',name=name,status='NOT OBSERVABLE; scientific cleanup NOT PROVEN')
|
|
def cleanup(self,measurement_status,failure=None):
|
|
self.persist_identity('before-cleanup')
|
|
# Record identities/exit status durably immediately before historical
|
|
# removal, including watchdog rescue; no live desktop socket is used.
|
|
original=engine.shutil.rmtree
|
|
def remove(path,*args,**kwargs):
|
|
if Path(path)==self.runtime:self.persist_identity('before-runtime-removal')
|
|
return original(path,*args,**kwargs)
|
|
engine.shutil.rmtree=remove
|
|
try:status=super().cleanup(measurement_status,failure)
|
|
finally:engine.shutil.rmtree=original
|
|
outcome=ledger.read(self.run/'outcome.json');errors={n:re.findall(r'EXIT[^\n]*error=(\d+)',self.log(n)) for n in self.proc if n not in ('compositor','pointer')}
|
|
explicit_errors=errors.get('owner',[])
|
|
clean=outcome['cleanup_pass'] and bool(explicit_errors) and not any(e!='0' for es in errors.values() for e in es)
|
|
outcome.update(scientific_cleanup='PASS' if clean else 'INCONCLUSIVE',wayland_exit_errors=errors,measurements_pass=measurement_status,runtime_exit=outcome['processes'])
|
|
if not clean:outcome['status']='INCONCLUSIVE'
|
|
ledger.durable(self.run/'outcome.json',outcome);return outcome['status']
|
|
|
|
def main():
|
|
import argparse
|
|
from plans import DEFAULT_LAB,RUNS
|
|
p=argparse.ArgumentParser();p.add_argument('run_id',choices=RUNS);p.add_argument('--lab',type=Path,default=DEFAULT_LAB);args=p.parse_args()
|
|
auth=ledger.begin(args.lab,args.run_id);s=None;status='INCONCLUSIVE';failure=None
|
|
try:
|
|
s=Session(RUNS[args.run_id],auth,args.lab);s.start()
|
|
if s.spec['campaign']=='saved':from saved import run
|
|
elif s.spec['campaign']=='input':from input import run
|
|
else:raise ValueError('conditional diagnostics require separate reviewed runner')
|
|
status=run(s)
|
|
except Exception as e:failure=f'{type(e).__name__}: {e}'
|
|
finally:
|
|
if s is not None:status=s.cleanup(status,failure)
|
|
print(status,failure or '');return 0 if status=='PASS' else 1
|
|
if __name__=='__main__':raise SystemExit(main())
|