swayfx-mosaic/tooling/probes/step20c-dynamic-output/run.py

66 lines
4.4 KiB
Python

"""EXPERIMENTAL adapter; inherited bounded process/socket cleanup stays intact."""
import argparse
from pathlib import Path
import subprocess
import sys
from campaign import Campaign
from scenario import SCENARIO
p = argparse.ArgumentParser()
p.add_argument('lab', type=Path)
p.add_argument('mode', choices=('control', 'private_standard', 'private_mosaic'))
a = p.parse_args()
lab = a.lab.resolve()
repo = Path(__file__).resolve().parents[3]
# WHY: one lock covers all modes, dynamic reservations and complete child cleanup.
# CONTRACT: repeated/failed runs cannot silently replenish campaign budgets.
# INVARIANT: no hide/return camera pair is generated by this adapter.
campaign = Campaign(lab)
campaign.begin(a.mode)
s = (repo / 'tooling/probes/step20c-applied-state-input/run.py').read_text()
s = s.replace("f'{args.mode}-{args.kind}-asi{args.asi_mode}-{time.time_ns()}'",
"f'{args.mode}-dynamic-{time.time_ns()}'")
s = s.replace('output HEADLESS-1 resolution 800x600 position -800 0',
'output HEADLESS-1 resolution 1000x750 scale 1.25 position -800 0')
s = s.replace("Path('/usr/bin/sway')", "lab / 'standard-build/sway/sway'")
s = s.replace('import argparse', 'import argparse\nfrom PIL import Image\nimport sys\nsys.path.insert(0, '+repr(str(Path(__file__).parent.resolve()))+')\nfrom campaign import reserve')
s = s.replace("run.mkdir(parents=True, mode=0o700)", "run.mkdir(parents=True, mode=0o700)\n(run / 'fixture-source.py').write_text(Path(__file__).read_text())")
start = s.index(" expected = {'HEADLESS-1':")
end = s.index(" for output, side in", start)
s = s[:start]+''' expected = {'HEADLESS-1': (-960, 0, 960, 600) if globals().get('dynamic_stage') == 'D1' else (-800, 0, 800, 600), 'HEADLESS-2': (0, 0, 800, 600)}
actual = {o['name']: tuple(o['rect'][k] for k in ('x','y','width','height')) for o in outputs if o['active']}
if actual != expected or any(o['transform'] != 'normal' or o['scale'] != (1.25 if o['name']=='HEADLESS-1' else 1) for o in outputs if o['active']):
raise RuntimeError(f'output geometry/scale/transform mismatch: {actual}')
record('state', tag=tag, workspaces=ipc('-t','get_workspaces'), workspace=workspace(), owner=owner(), outputs=outputs)
'''+s[end:]
s = s.replace("['grim', '-o', output, str(path)]", "['grim', '-s', str(next(o['scale'] for o in outputs if o['name']==output)), '-o', output, str(path)]")
s = s.replace(" record('capture', tag=tag", """ out = next(o for o in outputs if o['name']==output)
dimensions = list(Image.open(path).size)
if dimensions != [out['current_mode']['width'],out['current_mode']['height']]:
raise RuntimeError('physical PNG/mode mismatch')
record('capture_geometry',tag=tag,output=output,dimensions=dimensions,rect=out['rect'],scale=out['scale'],transform=out['transform'],mode=out['current_mode'])
record('capture', tag=tag""")
start=s.index('def inject(action, x, y):');end=s.index('\ndef client_command',start)
s=s[:start]+'''def inject(action, x, y):
outputs = [o for o in ipc('-t','get_outputs') if o['active']]
min_x = min(o['rect']['x'] for o in outputs)
min_y = min(o['rect']['y'] for o in outputs)
max_x = max(o['rect']['x']+o['rect']['width'] for o in outputs)
max_y = max(o['rect']['y']+o['rect']['height'] for o in outputs)
if not any(o['rect']['x']<=x<o['rect']['x']+o['rect']['width'] and o['rect']['y']<=y<o['rect']['y']+o['rect']['height'] for o in outputs):
raise ValueError('global point is outside applied outputs')
sent_x,sent_y = int(x-min_x),int(y-min_y)
width,height = max_x-min_x,max_y-min_y
marker=f'INJECT_DONE {action} {sent_x},{sent_y}'
before=(run/'pointer.log').read_text().count(marker)
record('pointer_request',action=action,global_point=[x,y],sent=[sent_x,sent_y],extent=[width,height],minimum=[min_x,min_y])
pointer.stdin.write(f'{action} {sent_x} {sent_y} {width} {height}\\n');pointer.stdin.flush()
wait(lambda: (run/'pointer.log').read_text().count(marker)>before)
'''+s[end:]
start=s.index(" if args.kind == 'cycles':\n");end=s.index('except Exception as exc:',start)
scenario=SCENARIO
s=s[:start]+scenario+'\n'+s[end:]
generated=lab/f'dynamic-{a.mode}.py'
generated.write_text(s)
raise SystemExit(subprocess.call([sys.executable,str(generated),str(lab),a.mode,'cycles','--max-seconds','240','--max-captures','100','--max-bytes','60000000'], pass_fds=(campaign.lock.fileno(),)))