swayfx-mosaic/tooling/probes/step20c-static-e2-completion/run.py
fy59 4ef16a4019 test(mosaic): qualify static nested standard return and integration readiness
E2 is FEASIBLE for the measured matrix. The initial private integration candidate is BUILDABLE; its accepted comment delta is NOT REBUILT. STEP20C remains NEXT and production multi-output remains NOT YET SUPPORTED.
2026-10-13 22:28:14 +02:00

376 lines
25 KiB
Python

#!/usr/bin/env python3
"""One immutable-static headless process, one genuinely grabbed E2 XDG chain.
WHY: one chain preserves both popups through standard-return measurements.
CONTRACT: every mutation/injection/capture consumes a persistent reservation.
INVARIANT: only owned groups/sockets; actual codes survive terminal IPC failure.
"""
import argparse
import fcntl
import json
import os
from pathlib import Path
import re
import shutil
import signal
import subprocess
import tempfile
import threading
import time
from PIL import Image
from support import sha,save,reserve,environment,verify
from model import *
from authorization import authorize
from transition import validate_applied_standard_transition, transition_window_start, source_bindings
from lifecycle import opening_pair,explicit_child_close,a_survival,normal_owner_close,final_restoration
from observations import applied_model,output_model,viewport_model,verify_owner_occlusion
from ipc_predicates import nodes,workspace,validate_outputs,validate_reference,measure_owner,scene_from_log,validate_camera
from predicates import Defect,COLORS,paint,point,equal,reference,protocol,geometry,mapped,pair,dismissal,root_clip,popup_pixels,terminal,terminal_exchange
p=argparse.ArgumentParser();p.add_argument('lab',type=Path);p.add_argument('--future-authorization',type=Path)
args=p.parse_args();lab=args.lab.resolve()
# Fail CLOSED before owner/lock/ledger/runtime creation or any subprocess.
auth=authorize(lab,args.future_authorization)
OWNER=Path(auth['owner_registry']);BINARY=Path(auth['binary']);BINARY_SHA=auth['binary_sha256'];WLROOTS_SHA=auth['wlroots_sha256']
require(not __import__('sys').flags.optimize,'assertions required')
assert OWNER.read_text().strip()==str(lab) and (lab/'campaign-owner').read_text().strip()==str(OWNER)
lock=(lab/'run.lock').open('a+');fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
freeze=verify(lab/'results/fixture-freeze-sha256.txt');gate=json.loads((lab/'results/offline-gate.json').read_text())
assert gate['marker']=='E2_COMPLETION_OFFLINE=READY_FOR_REVIEW' and gate['fixture_sha256']==freeze['sha256']
assert json.loads((lab/'results/binary-identity.json').read_text())['status']=='PASS'
assert sha(BINARY)==sha(lab/'swayfx-mosaic')==BINARY_SHA
assert not (lab/'run').exists(),'run path already used'
reserve(lab,'run')
run=lab/'run';run.mkdir();runtime=Path(tempfile.mkdtemp(prefix='msng-',dir='/tmp'));runtime.chmod(0o700)
assert len(os.fsencode(str(runtime/'sway-ipc.4294967295.2147483647.sock')))<108
(run/'runtime-path').write_text(str(runtime)+'\n')
config='output HEADLESS-1 resolution 800x600 scale 1 transform normal position -800 0\noutput HEADLESS-2 resolution 800x600 scale 1 transform normal position 0 0\nworkspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\nfocus_follows_mouse no\nseat seat0 hide_cursor 100\n'
(run/'config').write_text(config)
env=environment();env.update(XDG_RUNTIME_DIR=str(runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman')
processes=[];files=[];comp=None;pointer=None;failure=None;stage='startup';measures=False;rescue=False
started=time.monotonic();finished=threading.Event();expired=threading.Event();chains={};clips={};root_boundary={};captures=[];clicks=[];owners={};camera_rows=[];fully_mapped_ns={}
def event(kind,**data):
with (run/'events.jsonl').open('a') as out:out.write(json.dumps(dict(ns=time.monotonic_ns(),kind=kind,**data))+'\n')
def spawn(name,argv,debug=False):
assert not expired.is_set()
out=(run/(name+'.log')).open('w');files.append(out)
proc=subprocess.Popen(list(map(str,argv)),stdin=subprocess.PIPE,stdout=out,stderr=subprocess.STDOUT,
text=True,start_new_session=True,env={**env,**({'WAYLAND_DEBUG':'1'} if debug else {})})
processes.append((name,proc));event('spawn',name=name,pid=proc.pid,argv=list(map(str,argv)));return proc
def stop_owned(sig=signal.SIGTERM):
for name,proc in list(processes):
try:os.killpg(proc.pid,sig)
except ProcessLookupError:pass
def watchdog():
while not finished.wait(.1):
if time.monotonic()-started>143 or any(p.stat().st_size>32*1024*1024 for p in run.glob('*.log')):
expired.set();stop_owned();return
thread=threading.Thread(target=watchdog,daemon=True);thread.start()
def log(name):return (run/(name+'.log')).read_text(errors='replace')
def wait(fn,seconds=6):
end=min(time.monotonic()+seconds,started+143)
while time.monotonic()<end:
if expired.is_set():raise RuntimeError('deadline/log bound exceeded')
if comp is not None and comp.poll() is not None:raise RuntimeError('unexpected compositor exit '+str(comp.returncode))
value=fn()
if value:return value
time.sleep(.025)
raise RuntimeError('fixture wait timeout at '+stage)
def ipc(*args):
assert sock.is_relative_to(runtime) and sock.is_socket()
event('ipc-request-start',args=args)
r=subprocess.run(['swaymsg','-s',str(sock),*args],env=env,capture_output=True,text=True,timeout=4)
event('ipc',args=args,exit=r.returncode,stdout=r.stdout,stderr=r.stderr)
assert r.returncode==0,('IPC failure',args,r.returncode)
return json.loads(r.stdout)
def cmd(text):
assert all(x['success'] for x in ipc(text)),text
def snapshot(tag):
tree=ipc('-t','get_tree');outputs=ipc('-t','get_outputs');ws=ipc('-t','get_workspaces')
data=dict(ns=time.monotonic_ns(),tree=tree,outputs=outputs,workspaces=ws,focused=[n['id'] for n in nodes(tree) if n.get('focused')])
save(lab,tag+'-snapshot',data);return data
def find(app,pid):
candidates=[n for n in nodes(ipc('-t','get_tree')) if n.get('app_id')==app and n.get('pid')==pid]
assert len(candidates)<=1
return candidates[0] if candidates else None
def capture(tag,output):
reserve(lab,'capture');time.sleep(.20)
path=run/(tag+'.png');assert not path.exists()
before=time.monotonic_ns()
r=subprocess.run(['grim','-s','1','-o',output,str(path)],env=env,capture_output=True,text=True,timeout=4)
event('capture',file=path.name,output=output,start_ns=before,exit=r.returncode,stderr=r.stderr)
assert r.returncode==0
im=Image.open(path).convert('RGB');assert im.size==(800,600)
captures.append(dict(file=path.name,output=output,sha256=sha(path),start_ns=before,end_ns=time.monotonic_ns(),exit=r.returncode))
save(lab,'captures',captures);return im
def inject(action,xy,grab=False):
assert action in ['left','right','move'] and len(xy)==2
x,y=xy;assert type(x) is int and type(y) is int and -800<=x<800 and 0<=y<600
kinds=['move'] if action=='move' else ['click','move']+(['grab'] if grab else [])
reserve(lab,*kinds)
marker=f'INJECT_DONE {action} {x+800},{y}';before=log('pointer').count(marker)
pointer.stdin.write(f'{action} {x+800} {y}\n');pointer.stdin.flush()
wait(lambda:log('pointer').count(marker)>before);time.sleep(.18)
event('injection',action=action,global_point=xy,sent=[x+800,y],extent=[1600,600],grab=grab)
def buttons(name):
return [s for s in log(name).splitlines() if s.startswith(('POINTER_BUTTON ','BUTTON '))]
def click(tag,name,xy,grab=False,action='right'):
before={n:len(buttons(n)) for n in [name,'reference','filler']}
cuts={n:len(log(n)) for n in before}
t0=time.monotonic_ns();inject(action,xy,grab)
row=dict(tag=tag,global_point=xy,action=action,start_ns=t0,end_ns=time.monotonic_ns(),events={n:buttons(n)[before[n]:] for n in before},pointer_protocol={n:log(n)[cuts[n]:] for n in before})
clicks.append(row);save(lab,'clicks',clicks);return row
def delivery(row,name,target):
assert not row['events']['reference'] and not row['events']['filler'],'other client received visible popup button'
result=pair(row['events'][name],target)
for _,serial,button,state in result:
require(re.search(rf'wl_pointer#\d+\.button\({serial}, \d+, {button}, {state}\)',row['pointer_protocol'][name]) is not None,'button-wire-corroboration')
return result
def correlate_pointer(name,xy,rect,target,evidence):
# WHY: an injection request alone does not identify applied popup origin.
rows=re.findall(rf'POINTER_(?:ENTER|MOTION) target={target} .*?x=([\d.-]+) y=([\d.-]+)',evidence)
assert rows,'missing pointer local coordinates'
local=list(map(float,rows[-1]));expected=[xy[0]-rect[0],xy[1]-rect[1]]
prov=Provenance(1,1,'pointer-sample','actual pointer log')
pointer_local(xy,Rect(Space.GLOBAL,*rect,prov),local)
wl=int(re.search(rf'{target} CREATE wl=(\d+)',log(name))[1])
wire=re.findall(rf'wl_pointer#\d+\.(?:enter\(\d+, wl_surface#{wl},|motion\(\d+,) ([\d.-]+), ([\d.-]+)\)',evidence)
require(any(list(map(float,v))==local for v in wire),'fresh-pointer-wire-coordinates')
return dict(global_point=xy,local=local,rectangle=rect)
def close_client(proc,name,value):
proc.stdin.write(value+'\n');proc.stdin.flush()
code=proc.wait(timeout=3);assert code==0,(name,code)
assert 'TOPLEVEL DESTROY' in log(name)
wait(lambda:find('org.mosaic.NestedGrabProbe',proc.pid) is None)
# CONTRACT: applied owner, paint and XDG surface size are distinct measurements.
def owner_state(name,proc,tag):
snap=snapshot(tag);ws=workspace(snap['tree'],'1');owner=next(n for n in nodes(ws) if n.get('pid')==proc.pid)
assert owner['app_id']=='org.mosaic.NestedGrabProbe' and owner['shell']=='xdg_shell' and owner['visible']
assert ws['output']=='HEADLESS-1' and owner['focused'] and snap['focused']==[owner['id']]
measured=measure_owner(log('compositor'),owner);scene=scene_from_log(log('compositor'),'HEADLESS-1')
assert scene['workspace']==ws['id']
measured['workspace']=ws['id']
return snap,ws,owner,measured,scene
def open_chain(name):
global stage
reserve(lab,'chain');cmd('workspace 1');proc=spawn(name,[lab/'nested-client','static_cross'],debug=True)
wait(lambda:find('org.mosaic.NestedGrabProbe',proc.pid) and 'TOPLEVEL MAP ' in log(name));time.sleep(.6)
snap,ws,owner,before,sc=owner_state(name,proc,name+'-before-camera')
assert ws['layout']=='mosaic'
current=ws['mosaic']['rows'][0]['scroll_x'];x,w=before['horizontal_content'];desired=-w//2;delta=int(x-desired)
if delta:
reserve(lab,'camera');cmd(f'[con_id={owner["id"]}] layout mosaic scroll {delta}');time.sleep(.6)
snap,ws,owner,after,scene=owner_state(name,proc,name+'-applied')
validate_camera(current,ws['mosaic']['rows'][0]['scroll_x'],delta,scene)
assert scene['clip_valid'] and scene['global_clip']==[-800,0,800,600]
assert not scene_from_log(log('compositor'),'HEADLESS-2')['clip_valid']
refstate=validate_reference(snap,refid,ref.pid)
camera_rows.append(dict(chain=name,before=current,delta=delta,after=scene['scroll'],before_owner=before,after_owner=after,scene=scene,reference=refstate))
save(lab,'camera-calibration',camera_rows)
if name=='E2':
stage='reference';inject('move',[-790,590]);time.sleep(.5)
first=capture('H2-reference-1','HEADLESS-2');time.sleep(.6)
second=capture('H2-reference-2','HEADLESS-2');cmp=reference(first,second)
save(lab,'reference-stability',dict(status='PASS',comparison=cmp,reference=refstate,interval='>=600ms',focus=snap['focused']))
global baseline
baseline=first
root_boundary[name]=time.monotonic_ns()
stage=name+'-open-A';topim=capture(name+'-top','HEADLESS-1')
topbox=paint(topim,'TOPLEVEL')['bbox'];assert topbox
# Tiled scene+window offset is checked against actual painted blue origin.
applied=applied_model(after,owner,log(name),name+'-mosaic')
content_rect=content(applied);origin=[content_rect.x,content_rect.y]
out=output_model(snap,'HEADLESS-1',content_rect.provenance)
verify_paint(content_rect,out,topbox,paint(topim,'TOPLEVEL')['count'],viewport_model(scene,content_rect.provenance))
assert topbox[0]-800==origin[0]
assert origin[1]==after['scene'][1]+owner['deco_rect']['height']+owner['window_rect']['y']
owners[name]=dict(id=owner['id'],pid=proc.pid,workspace=ws['id'],logical_rect=owner['rect'],applied=after,drawn_size=[420,320],paint=paint(topim,'TOPLEVEL'),content_origin=origin)
save(lab,'applied-owner-geometry',owners)
top_point=point(topim,'TOPLEVEL',-800)
require(0 <= top_point[0]-origin[0] < applied.native.rectangle.width and 0 <= top_point[1]-origin[1] < applied.native.rectangle.height, 'opening-point-native-input-bounds')
cp=click(name+'-open-A',name,top_point,True,'left')
wait(lambda:'POPUP_A MAP ' in log(name));time.sleep(.25)
correlate_pointer(name,top_point,content_rect.values(),'TOPLEVEL',cp['pointer_protocol'][name])
aim=capture(name+'-A','HEADLESS-1');ap=point(aim,'POPUP_A',-800)
stage=name+'-open-B';cpB=click(name+'-open-B',name,ap,True,'left')
wait(lambda:'POPUP_B MAP ' in log(name));time.sleep(.3)
prot=protocol(log(name))
require(not cp['events']['reference'] and not cp['events']['filler'] and not cpB['events']['reference'] and not cpB['events']['filler'],'opening-other-recipient')
opening_pair(cp['events'][name],'TOPLEVEL',prot['grabs'][0]['serial'])
opening_pair(cpB['events'][name],'POPUP_A',prot['grabs'][1]['serial'])
g=geometry(log(name),origin,content_rect.provenance)
# Actual configure/ACK/map drives geometry; requests are predictions only.
g['positioner_prediction']={'flags':0,'A':[100,80,350,170],'B':[100,60,300,130],'evidence':'source-backed prediction only'}
require(all(value=='EXERCISED' for value in g['crossing'].values()),'popup crossing NOT EXERCISED')
correlate_pointer(name,ap,g['A'],'POPUP_A',cpB['pointer_protocol'][name])
fully_mapped_ns[name]=time.monotonic_ns();event('fully-mapped-chain',name=name,ids=prot['ids'])
left=capture(name+'-mosaic-H1','HEADLESS-1');right=capture(name+'-mosaic-H2','HEADLESS-2')
rgb=popup_pixels(left,right,g,True,baseline)
clip=root_clip(log('compositor'),origin,True,owner['id'],root_boundary[name],time.monotonic_ns())
chains[name]=dict(protocol=prot,geometry=g,pixels=rgb,owner=owners[name])
clips[name]=dict(tile=scene,root=clip,child=clip['child'])
save(lab,'grab-serials',{n:v['protocol']['grabs'] for n,v in chains.items()})
save(lab,'xdg-protocol-order',{n:v['protocol'] for n,v in chains.items()})
save(lab,'popup-root-geometry',{n:v['geometry']['A'] for n,v in chains.items()})
save(lab,'popup-nested-geometry',{n:v['geometry']['B'] for n,v in chains.items()})
save(lab,'applied-clips',clips);save(lab,'mosaic-pixels',{n:v['pixels'] for n,v in chains.items()})
return proc,owner,g,left
try:
comp=spawn('compositor',[lab/'swayfx-mosaic','-d','-c',run/'config'])
sock=wait(lambda:next(iter(runtime.glob('sway-ipc.*.sock')),None))
display=wait(lambda:next((x for x in runtime.glob('wayland-*') if x.is_socket()),None))
env.update(WAYLAND_DISPLAY=display.name,SWAYSOCK=str(sock))
assert Path(f'/proc/{comp.pid}/exe').resolve()==lab/'swayfx-mosaic'
maps=Path(f'/proc/{comp.pid}/maps').read_text();(run/'proc-maps.txt').write_text(maps)
paths=sorted({s.split()[-1] for s in maps.splitlines() if len(s.split())>=6 and s.split()[-1].startswith('/')})
wl=Path('/usr/lib/libwlroots-0.20.so').resolve()
assert [Path(x).resolve() for x in paths if 'libwlroots' in x]==[wl] and sha(wl)==WLROOTS_SHA
assert not any('libscenefx' in x for x in paths)
save(lab,'dependency-mappings',dict(status='PASS',paths=paths,wlroots=str(wl),wlroots_sha256=sha(wl),SceneFX_shared=False))
outputs=ipc('-t','get_outputs');validate_outputs(outputs);save(lab,'output-geometry',outputs)
pointer=spawn('pointer',[lab/'pointer-daemon']);wait(lambda:'POINTER_READY' in log('pointer'))
cmd('workspace 2');ref=spawn('reference',[lab/'static-reference'],debug=True)
rn=wait(lambda:find('org.mosaic.StaticReference',ref.pid));refid=rn['id'];cmd(f'[con_id={refid}] border none')
cmd('workspace 1');filler=spawn('filler',[lab/'static-reference'],debug=True)
wait(lambda:find('org.mosaic.StaticReference',filler.pid));cmd('layout mosaic')
E2,owner2,g2,im2=open_chain('E2')
stage='E2-standard';mapped(log('E2'));standard_boundary=time.monotonic_ns();reserve(lab,'transition');cmd(f'[con_id={owner2["id"]}] layout splith');time.sleep(.6)
snap,ws,std_owner,std_measure,std_scene=owner_state('E2',E2,'standard')
assert ws['id']==owners['E2']['workspace'] and ws['layout']=='splith'
if std_scene['clip_valid'] or std_scene['layout_enum']!=1:raise Defect('standard tiled clip retained')
mapped(log('E2'))
stdleft=capture('standard-H1','HEADLESS-1');stdright=capture('standard-H2','HEADLESS-2')
blue=paint(stdleft,'TOPLEVEL')['bbox'];assert blue
stdapplied=applied_model(std_measure,std_owner,log('E2'),'E2-standard')
stdcontent=content(stdapplied);stdorigin=[stdcontent.x,stdcontent.y]
fresh_standard(Rect(Space.GLOBAL,*owners['E2']['applied']['scene'],Provenance(owner2['id'],ws['id'],'E2-mosaic',owners['E2']['applied']['raw'])),stdapplied.scene)
# Blue is occluded by A/B; full pixel mask checked against actual geometry below.
assert blue[0]-800==stdorigin[0] and blue[1]==stdorigin[1]
stdg=geometry(log('E2'),stdorigin,stdcontent.provenance)
transition_end=time.monotonic_ns()
events=[json.loads(l) for l in (run/'events.jsonl').read_text().splitlines()]
request=[e for e in events if e['kind']=='ipc-request-start' and e.get('args')==[f'[con_id={owner2["id"]}] layout splith']]
replies=[e for e in events if e['kind']=='ipc' and e.get('args')==[f'[con_id={owner2["id"]}] layout splith']]
require(len(request)==len(replies)==1,'one-transition-request-reply')
applied_transition_start=transition_window_start(log('compositor'),'HEADLESS-1',clips['E2']['root']['tree'],
owner2['id'],ws['id'],fully_mapped_ns['E2'],request[0]['ns'])
transition=validate_applied_standard_transition(request[0]['ns'],replies[0],log('compositor'),
owner2['id'],clips['E2']['root']['tree'],ws['id'],'HEADLESS-1',ws['layout'],captures[-2:],
log('E2'),snap,chains['E2']['protocol']['ids'],[applied_transition_start,transition_end],source_bindings())
stdrgb=popup_pixels(stdleft,stdright,stdg,False,baseline)
save(lab,'standard-return',dict(status='PASS',transition=transition,geometry=stdg,
mapped_before_and_after=True,no_prior_popup_done=True))
verify_owner_occlusion(stdleft,stdright,stdcontent,stdg,False)
save(lab,'standard-popup-pixels',stdrgb)
stage='E2-standard-B';xy=point(stdright,'POPUP_B',0);row=click(stage,'E2',xy)
Bpair=delivery(row,'E2','POPUP_B');Bcoords=correlate_pointer('E2',xy,stdg['B'],'POPUP_B',row['pointer_protocol']['E2']);mapped(log('E2'))
# Re-examine every trace through the B input, including any late clip reactivation.
transition=validate_applied_standard_transition(request[0]['ns'],replies[0],log('compositor'),
owner2['id'],clips['E2']['root']['tree'],ws['id'],'HEADLESS-1',ws['layout'],captures[-2:],
log('E2'),snap,chains['E2']['protocol']['ids'],[applied_transition_start,time.monotonic_ns()],source_bindings())
save(lab,'standard-transition-through-B-input',transition)
before_close=log('E2');request_close_ns=time.monotonic_ns()
event('client-close-request',name='E2',command='close_b')
E2.stdin.write('close_b\n');E2.stdin.flush();wait(lambda:'COMMAND close_b DONE' in log('E2'))
close_proof=explicit_child_close(before_close,log('E2'),chains['E2']['protocol']['ids'],request_close_ns,time.monotonic_ns())
save(lab,'explicit-child-close',close_proof)
stage='E2-standard-A';Aleft=capture('standard-A-H1','HEADLESS-1');Aimage=capture('standard-A-H2','HEADLESS-2');assert paint(Aimage,'POPUP_B')['count']==0
from pixels import color_geometry
for im,ox in ((Aleft,-800),(Aimage,0)):
require(paint(im,'POPUP_B')['count']==0,'no-child-residual-both-outputs')
color_geometry(im,'POPUP_A',Rect(Space.GLOBAL,*stdg['A'],stdcontent.provenance),
Output(str(ox),Rect(Space.GLOBAL,ox,0,800,600,stdcontent.provenance),1.,'normal'),None)
xy=point(Aimage,'POPUP_A',0);before_A=log('E2');rowA=click(stage,'E2',xy);Apair=delivery(rowA,'E2','POPUP_A')
Aproof=a_survival(Aimage,before_A,log('E2'),chains['E2']['protocol']['ids'],rowA['events']['E2'],xy,stdg['A'])
Acoords=correlate_pointer('E2',xy,stdg['A'],'POPUP_A',rowA['pointer_protocol']['E2'])
save(lab,'standard-popup-input',dict(status='PASS',B=Bpair,A=Apair,B_coordinates=Bcoords,A_coordinates=Acoords,explicit_B_close=close_proof,A_survived=Aproof))
stage='E2-cleanup';before_owner_close=log('E2')
event('client-close-request',name='E2',command='close_owner')
close_client(E2,'E2','close_owner')
ownerclose=normal_owner_close(before_owner_close,log('E2'),chains['E2']['protocol']['ids'],E2.returncode)
save(lab,'normal-owner-close',ownerclose);time.sleep(.35)
final=capture('final-H2','HEADLESS-2');save(lab,'final-restoration',final_restoration(baseline,final))
assert not any(paint(final,n)['count'] for n in ['POPUP_A','POPUP_B'])
finalleft=capture('final-H1','HEADLESS-1')
require(not any(paint(finalleft,n)['count'] for n in ('POPUP_A','POPUP_B')),'final-H1-popup-residual')
snap=snapshot('final');validate_reference(snap,refid,ref.pid)
assert filler.poll() is None and ref.poll() is None
measures=True;event('measurements-completed',status='PASS');stage='completed'
except Exception as exc:
failure=dict(type=type(exc).__name__,message=str(exc),stage=stage,verdict='BLOCKED' if isinstance(exc,Defect) else 'INCONCLUSIVE')
event('failure',**failure)
finally:
# WHY: shutdown transport outcome is not folded into check=True scientific gates.
exit_data=None
if comp is not None and comp.poll() is None and 'sock' in globals() and sock.is_socket():
# WHY: record transport evidence before wait can raise or time out.
def record_terminal(stdout,stderr):
(run/'terminal-exit.stdout').write_text(stdout)
(run/'terminal-exit.stderr').write_text(stderr)
exit_data=terminal_exchange(subprocess.run,comp.wait,
['swaymsg','-s',str(sock),'exit'],env,measures,
lambda:log('compositor'),
lambda:any(re.search(r'wl_display[^\n]*\.error\(|protocol error',log(n),re.I)
for n,_ in processes if n not in ['compositor','pointer']),
expired.is_set,record_terminal)
if exit_data is None:exit_data=dict(scientific_cleanup_pass=False,compositor_exit=comp.poll() if comp else None,measurements_completed=measures)
codes=[]
for name,proc in reversed(processes):
if proc.poll() is None:
try:proc.wait(timeout=.7)
except subprocess.TimeoutExpired:
if name=='pointer':proc.stdin.close() # normal EOF destroys virtual pointer
else:
rescue=True
try:os.killpg(proc.pid,signal.SIGTERM)
except ProcessLookupError:pass
try:proc.wait(timeout=.7)
except subprocess.TimeoutExpired:
rescue=True
try:os.killpg(proc.pid,signal.SIGKILL)
except ProcessLookupError:pass
proc.wait(timeout=.7)
codes.append(dict(name=name,pid=proc.pid,exit=proc.returncode))
if proc.stdin and not proc.stdin.closed:proc.stdin.close()
finished.set();thread.join(timeout=1)
for out in files:out.close()
for name,proc in processes:
try:os.killpg(proc.pid,0)
except ProcessLookupError:pass
else:rescue=True;os.killpg(proc.pid,signal.SIGKILL)
assert all(not Path('/proc',str(row['pid'])).exists() for row in codes),'owned process residual'
private_sockets=[str(p) for p in runtime.iterdir() if p.is_socket()]
shutil.rmtree(runtime)
exit_data.update(rescue_cleanup=rescue,owned_process_codes=codes)
exit_data['scientific_cleanup_pass']=bool(exit_data['scientific_cleanup_pass'] and not rescue and all(row['exit']==0 for row in codes))
exit_data['all_owned_reaped']=all(not Path('/proc',str(row['pid'])).exists() for row in codes)
if rescue:exit_data['scientific_cleanup_pass']=False
if measures and (not exit_data['scientific_cleanup_pass'] or any(r['exit']!=0 for r in codes)):
failure=dict(type='TerminalCleanupFailure',message='terminal/process evidence failed',stage='completed',verdict='INCONCLUSIVE')
save(lab,'terminal-exit',exit_data)
save(lab,'cleanup',dict(status='PASS' if not rescue else 'RESCUE',runtime=str(runtime),runtime_removed=not runtime.exists(),owned_processes=codes,sockets_before_owned_directory_removal=private_sockets,private_socket_residual=False))
save(lab,'runtime',dict(status='PASS' if failure is None else failure['verdict'],failure=failure,stage=stage,measurements_completed=measures,duration_seconds=time.monotonic()-started,watchdog=expired.is_set(),rescue_cleanup=rescue,runner_exit=0 if failure is None else 1))
verify(lab/'results/fixture-freeze-sha256.txt')
print('RUNTIME', 'PASS' if failure is None else failure)
raise SystemExit(0 if failure is None else 1)