#!/usr/bin/env python3
"""Real desktop parent-death proof. Run under headless-desktop, from either checkout.
Usage: python3 desktop-proof.py before|after OUTPUT_DIRECTORY [--rebuild]
No database seed or provider turn is used. Sends signals only to recorded descendants.
"""
import os, sys, time, json, signal, subprocess, pathlib
side, outarg = sys.argv[1:3]
out = pathlib.Path(outarg); out.mkdir(parents=True, exist_ok=False)
root = pathlib.Path.cwd(); tracked = {}; events = []
status = out/'status.txt'
sha = subprocess.check_output(['git','rev-parse','HEAD'],text=True).strip()
start = time.monotonic()
def table():
    result = {}
    for entry in pathlib.Path('/proc').iterdir():
        if not entry.name.isdigit(): continue
        try:
            stat=(entry/'stat').read_text().rsplit(')',1)[1].split()
            result[int(entry.name)] = (int(stat[1]),stat[0],(entry/'cmdline').read_bytes().replace(b'\0',b' ').decode(errors='replace'))
        except (OSError,ValueError): pass
    return result
def remember():
    procs=table(); ids={app.pid, *tracked}
    for _ in range(20):
        new={pid for pid,(ppid,_,_) in procs.items() if ppid in ids}
        if new <= ids: break
        ids |= new
    for pid in ids:
        if pid in procs: tracked[pid]=procs[pid]
    return procs, ids
def alive(pid):
    p=table().get(pid); return bool(p and p[1]!='Z')
def log(message):
    events.append({'seconds':round(time.monotonic()-start,1),'message':message})
    (out/'events.json').write_text(json.dumps(events,indent=2))
    print(message,flush=True)
def caption(message,sup=None,parent=None):
    procs,ids=remember()
    tools=subprocess.run(['xdotool','search','--name','Developer Tools'],capture_output=True,text=True)
    for wid in tools.stdout.split():
        subprocess.run(['xdotool','windowunmap',wid],capture_output=True)
    children=[pid for pid,(ppid,state,cmd) in procs.items() if ppid==sup and state!='Z'] if sup else []
    text=f'{side.upper()}  {sha[:10]}  |  Linux / Xvfb 1920x1080 / fresh isolated home\n{message}\n'
    if sup:
        pp=procs.get(sup,(None,'gone',''))[0]
        text+=f'Recorded parent {parent}: {"alive" if alive(parent) else "gone"}    Supervisor {sup}: {"alive" if alive(sup) else "gone"} (PPID {pp})\nDirect app child PIDs: {children or "none"}'
    status.write_text(text)
def wait_supervisor(exclude=()):
    for _ in range(900):
        procs,ids=remember()
        for pid in ids:
            if pid not in exclude and procs.get(pid,('','',''))[2].strip()=='node scripts/dev-electron.mjs' and procs[pid][1]!='Z':
                return pid
        if app.poll() is not None: raise RuntimeError('dev runner exited before supervisor appeared')
        time.sleep(1)
    raise RuntimeError('supervisor startup timed out')
def wait_window():
    for _ in range(180):
        r=subprocess.run(['xdotool','search','--onlyvisible','--name','T3 Code'],capture_output=True,text=True)
        if r.returncode==0:
            for wid in r.stdout.split():
                subprocess.run(['xdotool','windowmove',wid,'0','0'],capture_output=True)
                subprocess.run(['xdotool','windowsize',wid,'1920','1080'],capture_output=True)
            return
        remember(); time.sleep(1)
    raise RuntimeError('desktop window did not appear')
def pause(seconds,message,sup,parent):
    log(message)
    for _ in range(seconds*2):
        caption(message,sup,parent); time.sleep(.5)
logfile=open(out/'private-runtime.log','w')
env={**os.environ,'T3CODE_DEV_SEED':'0','LIBGL_ALWAYS_SOFTWARE':'1'}
app=subprocess.Popen(['vp','run','dev:desktop','--home-dir',str(out/'private-home')],env=env,stdout=logfile,stderr=subprocess.STDOUT)
rec=None
try:
    sup=wait_supervisor(); wait_window(); time.sleep(12)
    if '--rebuild' in sys.argv:
        log('Rebuild: touching desktop source and waiting for a replacement supervisor')
        (root/'apps/desktop/src/main.ts').touch()
        sup=wait_supervisor((sup,)); wait_window(); time.sleep(12)
    procs,_=remember(); parent=procs[sup][0]
    log('Initial direct app PIDs: '+str([pid for pid,(ppid,state,cmd) in procs.items() if ppid==sup and state!='Z']))
    log(f'Recorded supervisor PID {sup}; direct parent PID {parent}; parent command kind '+('pack-bin.js' if 'pack-bin' in procs[parent][2] else 'shell' if 'sh ' in procs[parent][2] else 'other'))
    caption('Window ready; direct parent recorded',sup,parent)
    rec=subprocess.Popen(['ffmpeg','-y','-loglevel','error','-filter_threads','1','-f','x11grab','-framerate','10','-video_size','1920x1080','-i',os.environ['DISPLAY'],'-vf',f'scale=1280:720,pad=1280:900:0:0:color=0x15202b,drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:textfile={status}:reload=1:fontcolor=white:fontsize=20:x=20:y=740','-c:v','libx264','-threads','2','-preset','ultrafast','-crf','23','-pix_fmt','yuv420p',str(out/'desktop.mp4')],stdout=subprocess.DEVNULL,stderr=open(out/'ffmpeg.log','w'))
    pause(4,'Window ready; next: SIGKILL to recorded direct parent only',sup,parent)
    assert parent in tracked, 'refusing to kill an unrecorded parent'
    os.kill(parent,signal.SIGKILL)
    pause(8,'Sent SIGKILL to direct parent; waiting beyond shutdown grace periods',sup,parent)
    (root/'apps/desktop/dist-electron/main.cjs').touch()
    pause(15 if '--short-observation' in sys.argv else 30,'Touched built main.cjs after parent death; watching for Electron restart',sup,parent)
    result={'side':side,'sha':sha,'rebuilt':'--rebuild' in sys.argv,'supervisorPid':sup,'parentPid':parent,'supervisorAlive':alive(sup),'appChildren':[pid for pid,(ppid,state,cmd) in table().items() if ppid==sup and state!='Z']}
    log(json.dumps(result)); (out/'result.json').write_text(json.dumps(result,indent=2))
    pause(4 if '--short-observation' in sys.argv else 8,'Observation complete: '+('orphan supervisor remains' if result['supervisorAlive'] else 'supervisor exited; no relaunch'),sup,parent)
finally:
    if rec:
        rec.send_signal(signal.SIGINT); rec.wait(timeout=20)
    remember()
    for pid in reversed(list(tracked)):
        try:
            if alive(pid): os.kill(pid,signal.SIGTERM)
        except ProcessLookupError: pass
    time.sleep(4)
    for pid in reversed(list(tracked)):
        try:
            if alive(pid): os.kill(pid,signal.SIGKILL)
        except ProcessLookupError: pass
    logfile.close()
