#!/usr/bin/env python3
"""Real Electron display-loss reproduction. Usage: driver.py CHECKOUT OUTPUT SIDE DISPLAY.
Run under headless-desktop. OUTPUT must be new; uses fresh HOME/XDG/T3 state.
Only terminates processes spawned here or observed descendants of this Electron.
"""
import os, sys, time, json, subprocess, pathlib, hashlib, signal
root, out, side, display = sys.argv[1:]
root=pathlib.Path(root).resolve(); out=pathlib.Path(out).resolve(); out.mkdir(parents=True,exist_ok=False)
events=[]; start=time.monotonic()
def event(name, **data):
    row=dict(seconds=round(time.monotonic()-start,3),event=name,**data); events.append(row)
    print(json.dumps(row),flush=True)
def state(pid):
    try: return pathlib.Path(f'/proc/{pid}/stat').read_text().rsplit(')',1)[1].split()[0]
    except OSError: return 'gone'
def children(pid):
    found=set()
    try:
        for task in pathlib.Path(f'/proc/{pid}/task').iterdir():
            try: found.update(map(int,(task/'children').read_text().split()))
            except OSError: pass
    except OSError: pass
    return found
def descendants(pid):
    found=children(pid)
    for child in list(found): found.update(descendants(child))
    return found
home=out/'home'; home.mkdir()
env=os.environ.copy(); env.update(HOME=str(home),XDG_CONFIG_HOME=str(home/'config'),XDG_DATA_HOME=str(home/'data'),T3CODE_HOME=str(home/'t3'),T3CODE_DEV_SEED='0',ELECTRON_ENABLE_LOGGING='1',DISPLAY=display)
for key in ['VITE_HTTP_URL','VITE_WS_URL','T3CODE_DEV_SERVER_URL']: env.pop(key,None)
xlog=open(out/'xvfb.log','w'); x=subprocess.Popen(['Xvfb',display,'-screen','0','1280x800x24','-nolisten','tcp'],stdout=xlog,stderr=xlog)
app=None; rec=None; known=set()
try:
    for _ in range(100):
        if subprocess.run(['xdpyinfo','-display',display],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0: break
        time.sleep(.1)
    log=open(out/'electron.log','w')
    app=subprocess.Popen([str(root/'apps/desktop/node_modules/electron/dist/electron'),'dist-electron/main.cjs','--no-sandbox','--disable-gpu'],cwd=root/'apps/desktop',env=env,stdout=log,stderr=log)
    event('electron launched',pid=app.pid,viewport='1280x800',seed='fresh empty state')
    deadline=time.monotonic()+180
    while time.monotonic()<deadline:
        logs='\n'.join(p.read_text(errors='replace') for p in home.rglob('*.log')) + (out/'electron.log').read_text(errors='replace')
        if 'backend ready' in logs: break
        if app.poll() is not None: raise RuntimeError('Electron exited before backend ready')
        time.sleep(.2)
    else: raise RuntimeError('backend never ready')
    event('backend ready')
    time.sleep(15)
    # Static capture is accepted only after two identical frames 500ms apart.
    last=None
    for i in range(30):
        subprocess.run(['import','-display',display,'-window','root',str(out/'ready.png')],check=True)
        current=hashlib.sha256((out/'ready.png').read_bytes()).hexdigest()
        if last==current: event('stable screenshot',identicalFrames=2,separationMs=500); break
        last=current; time.sleep(.5)
    else: raise RuntimeError('screen never stabilized')
    rec_log=open(out/'ffmpeg.log','w')
    rec=subprocess.Popen(['ffmpeg','-y','-f','x11grab','-video_size','1280x800','-framerate','10','-i',display,'-c:v','libx264','-preset','ultrafast','-pix_fmt','yuv420p',str(out/'display-loss.mp4')],stdout=rec_log,stderr=rec_log)
    time.sleep(5)
    known=descendants(app.pid); direct=children(app.pid)
    event('before display loss',mainRunning=app.poll() is None,directChildren=sorted(direct),descendants=sorted(known))
    x.terminate(); x.wait(timeout=10); event('private Xvfb stopped')
    code=app.wait(timeout=30)
    event('electron exited',exitCode=code,childStates={str(pid):state(pid) for pid in sorted(known)})
    time.sleep(3)
    event('three seconds after exit',childStates={str(pid):state(pid) for pid in sorted(known)})
    try: rec.wait(timeout=10)
    except subprocess.TimeoutExpired: rec.send_signal(signal.SIGINT); rec.wait(timeout=10)
    traces=[]
    for p in home.rglob('desktop.trace.ndjson'):
        for line in p.read_text().splitlines():
            try:
                row=json.loads(line)
                if 'desktop.lifecycle' in line or 'requestShutdownAndWait' in line: traces.append(row)
            except ValueError: pass
    (out/'lifecycle-raw.json').write_text(json.dumps(traces,indent=2))
    event('lifecycle records',count=len(traces))
finally:
    (out/'events.json').write_text(json.dumps(events,indent=2))
    if app is not None:
        if app.poll() is None:
            known.update(descendants(app.pid)); app.terminate()
            try: app.wait(timeout=15)
            except subprocess.TimeoutExpired: app.kill(); app.wait()
        for pid in known:
            if state(pid) not in ['gone','Z','X']:
                try: os.kill(pid,signal.SIGTERM)
                except ProcessLookupError: pass
    if rec is not None and rec.poll() is None: rec.send_signal(signal.SIGINT); rec.wait(timeout=10)
    if x.poll() is None: x.terminate(); x.wait(timeout=10)
