#!/usr/bin/env python3 """One budgeted follow-up of the unchanged verified static compositor. WHY: resolved relocations do not demonstrate real compositor execution. CONTRACT: verify /proc executable/library identity before mapping any client. INVARIANT: owned sockets/groups only, one camera correction, durable captures. """ import argparse 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,ImageChops from support import environment,reserve,save,sha from pixels import analyze, compare, interior, button_pair p=argparse.ArgumentParser();p.add_argument('lab',type=Path);a=p.parse_args();lab=a.lab.resolve() assert json.loads((lab/'results/binary-identity.json').read_text())['status']=='PASS' assert sha(lab/'swayfx-mosaic')=='f82557c1a9978a5e9665b9b4291b74880253725359e3df1c5b8d554921f7603b' assert (Path.home()/'swayfx-mosaic-static-followup-v1.owner').read_text().strip()==str(lab) reserve(lab,'run');run=lab/'run';run.mkdir();runtime=Path(tempfile.mkdtemp(prefix='msr-',dir='/tmp')) runtime.chmod(0o700);(run/'runtime-path').write_text(str(runtime)+'\n') (run/'config').write_text('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') env=environment();env.update(XDG_RUNTIME_DIR=str(runtime),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman') binary=lab/'swayfx-mosaic';processes=[];files=[];failure=None;compositor=None;events=[] started=time.monotonic();finished=threading.Event();expired=threading.Event() def event(kind,**data): row={'ns':time.monotonic_ns(),'kind':kind,**data};events.append(row) with (run/'events.jsonl').open('a') as out:out.write(json.dumps(row)+'\n') def stop_owned(): for proc in list(processes): try:os.killpg(proc.pid,signal.SIGTERM) except ProcessLookupError:pass def watchdog(): while not finished.wait(.1): if time.monotonic()-started>150 or any(x.stat().st_size>64*1024*1024 for x in run.glob('*.log')): expired.set();stop_owned();return thread=threading.Thread(target=watchdog,daemon=True);thread.start() def spawn(name,argv,stdin=False,extra=None): assert not expired.is_set() out=(run/(name+'.log')).open('w');files.append(out) proc=subprocess.Popen(list(map(str,argv)),env={**env,**(extra or {})},stdin=subprocess.PIPE if stdin else subprocess.DEVNULL,stdout=out,stderr=subprocess.STDOUT,text=True,start_new_session=True) processes.append(proc);event('spawn',name=name,pid=proc.pid,argv=list(map(str,argv)));return proc def wait(predicate,seconds=8): end=time.monotonic()+seconds while time.monotonic()before) time.sleep(.15);event('pointer',action=action,global_point=[x,y],target_tail=log('target').splitlines()[-6:] if (run/'target.log').exists() else []) def capture(tag,output): reserve(lab,'capture');path=run/(tag+'.png') subprocess.run(['grim','-s','1','-o',output,str(path)],env=env,check=True,timeout=5,stdout=subprocess.DEVNULL) im=Image.open(path).convert('RGB');assert im.size==(800,600) event('capture',file=path.name,output=output,sha256=sha(path));return im def measure(owner): rows=[x for x in log('compositor').splitlines() if 'PI_OWNER output=HEADLESS-1 ' in x and f' id={owner["id"]} ' in x] assert rows,'applied owner trace unavailable' line=rows[-1];m=re.search(r'scene_valid=1 scene=(-?\d+),(-?\d+) applied_size=(\d+),(\d+)',line);assert m,line x,y,w,h=map(int,m.groups());win=owner['window_rect'] return {'id':owner['id'],'scene':[x,y,w,h],'window_rect':win,'horizontal_content':[x+win['x'],win['width']],'raw':line,'IPC':owner} # WHY: global focus becomes H2 after the forbidden-region click. Workspace and # owner IDs, rather than global command context, bind the standard-return test. focus_states=[] def snapshot(tag): tree=ipc('-t','get_tree');outputs=ipc('-t','get_outputs');workspaces=ipc('-t','get_workspaces') row={'tag':tag,'ns':time.monotonic_ns(),'tree':tree,'outputs':outputs,'workspaces':workspaces, 'focused':[n['id'] for n in nodes(tree) if n.get('focused')]} focus_states.append(row);save(lab,'focus-states',focus_states) save(lab,tag+'-snapshot',row) return tree def workspace(tree,name): return next(n for n in nodes(tree) if n['type']=='workspace' and n['name']==name) def scene(output): rows=[s for s in log('compositor').splitlines() if 'PI_SCENE output='+output+' ' in s] assert rows,'scene trace absent';line=rows[-1] m=re.search(r'workspace=(\d+) layout=(\d+) scroll=(-?[\d.]+) tile_scene=(-?\d+),(-?\d+) clip_valid=(\d+) clip=(-?\d+),(-?\d+),(\d+),(\d+)',line);assert m,line v=list(map(float,m.groups())) return {'workspace':int(v[0]),'layout_enum':int(v[1]),'scroll':v[2], 'tile_local':v[3:5],'clip_valid':bool(v[5]),'clip_local':v[6:10], 'global_clip':[(-800 if output=='HEADLESS-1' else 0)+v[3]+v[6],v[4]+v[7],v[8],v[9]],'raw':line} def camera(tree):return workspace(tree,'1')['mosaic']['rows'][0]['scroll_x'] def buttons(name):return [x for x in log(name).splitlines() if x.startswith('BUTTON ')] def measured_click(kind,point): before={n:len(buttons(n)) for n in ['target','reference','filler']} inject('right',*point,kind=kind) row={'global_point':point,'events':{n:buttons(n)[before[n]:] for n in before}, 'tails':{n:log(n).splitlines()[-15:] for n in before},'ns':time.monotonic_ns()} save(lab,kind,row);return row def whole(tag,baseline,im): value=compare(baseline,im);save(lab,tag,value);assert value['equal'],'whole H2 differs: '+tag return value try: compositor=spawn('compositor',[binary,'-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(SWAYSOCK=str(sock),WAYLAND_DISPLAY=display.name) maps=Path(f'/proc/{compositor.pid}/maps').read_text();(run/'proc-maps.txt').write_text(maps) exe=Path(f'/proc/{compositor.pid}/exe').resolve();assert exe==binary and sha(exe)==sha(binary) paths=sorted({line.split()[-1] for line in maps.splitlines() if len(line.split())>=6 and line.split()[-1].startswith('/')}) wl=Path('/usr/lib/libwlroots-0.20.so').resolve() assert not any('libscenefx' in x for x in paths) assert [Path(x).resolve() for x in paths if 'libwlroots' in x]==[wl] assert sha(wl)=='7edca9a7d65855addda68e29ba5db94d57c803eb5fa7d0f86db3136b90ca9837' assert not any('/swayfx-mosaic-' in x and Path(x)!=binary for x in paths) save(lab,'dependency-mappings',{'status':'PASS','exe':str(exe),'paths':paths,'wlroots_sha256':sha(wl),'SceneFX_shared':False}) outputs=ipc('-t','get_outputs');actual={o['name']:(o['rect']['x'],o['rect']['y'],o['rect']['width'],o['rect']['height'],o['scale'],o['transform']) for o in outputs if o['active']} assert actual=={'HEADLESS-1':(-800,0,800,600,1.0,'normal'),'HEADLESS-2':(0,0,800,600,1.0,'normal')},actual save(lab,'output-geometry',outputs) pointer=spawn('pointer',[lab/'pointer-daemon','1600','600'],stdin=True);wait(lambda:'POINTER_READY' in log('pointer')) cmd('workspace 2');ref=spawn('reference',[lab/'static-reference'],stdin=True,extra={'WAYLAND_DEBUG':'1'}) reference=wait(lambda:find('org.mosaic.StaticReference'));refid=reference['id'] cmd(f'[con_id={refid}] border none') cmd('workspace 1');filler=spawn('filler',[lab/'static-reference'],stdin=True) wait(lambda:len([n for n in nodes(ipc('-t','get_tree')) if n.get('app_id')=='org.mosaic.StaticReference'])==2) target=spawn('target',[lab/'saved-client'],stdin=True,extra={'WAYLAND_DEBUG':'1'}) wait(lambda:find('org.mosaic.SavedBufferProbe')) cmd('layout mosaic');time.sleep(.6) tree=snapshot('before-camera');owner=find('org.mosaic.SavedBufferProbe');targetid=owner['id'];wsid=workspace(tree,'1')['id'] before=measure(owner);sc_before=scene('HEADLESS-1');scroll_before=camera(tree) assert sc_before['scroll']==scroll_before x,w=before['horizontal_content'];desired=-w//2;delta=x-desired reserve(lab,'camera');cmd(f'layout mosaic scroll {delta}') time.sleep(.6);owner=find('org.mosaic.SavedBufferProbe');after=measure(owner) tree=snapshot('preflight');scroll_after=camera(tree);sc_after=scene('HEADLESS-1') save(lab,'camera-calibration',{'before':scroll_before,'delta':delta,'desired_left':desired,'after':scroll_after,'before_scene':sc_before,'after_scene':sc_after}) assert scroll_after==scroll_before+delta and sc_after['scroll']==scroll_after x,w=after['horizontal_content'];assert x<00 and forbidden['count']==0 whole('v1-H2-comparison',baseline,right) bx,by,bw,bh=visible['bbox'];assert bx-800==x and bw==800 last_commit=[s for s in log('target').splitlines() if s.startswith('COMMIT ')][-1] dimensions=re.search(r'size=(\d+)x(\d+)',last_commit);assert dimensions assert int(dimensions[1])==w and bh-by==int(dimensions[2]) # Paint proves vertical position; only horizontal continuation is inferred # from the applied scene, IPC offset and actual configured client width. px,py=interior(left);permitted=(px-800,py) forbidden_point=(min(100,int(x+w)-30),py);assert forbidden_point[0]>=20 assert x