#!/usr/bin/env python3
"""Runtime observer, not an automated timing assertion. Same driver for both SHAs.
Traces forks so the post-return snapshot accounts for descendants after reparenting.
The three-second observation interval is evidence collection, not test synchronization.
"""
import os, sys, subprocess, json, time, datetime, pathlib, re, selectors, signal, ctypes
ROOT=pathlib.Path('/tmp/lead-27224-r1/capture')
SCRIPTS=ROOT/'scripts'
CHROME=ROOT/'download/chrome-headless-shell-linux64/chrome-headless-shell'
SIDES={'Before':'/home/ubuntu/wt/pr-27224-before','After':'/home/ubuntu/wt/pr-27224'}
ctypes.CDLL(None).prctl(36,1,0,0,0) # Adopt our own orphan descendants for deterministic cleanup/reaping.
def now(): return datetime.datetime.now(datetime.timezone.utc).isoformat()
def read_jsonl(path):
 if not path.exists():return []
 return [json.loads(s) for s in path.read_text().splitlines() if s.strip()]
def trace_tree(path,roots):
 text=path.read_text() if path.exists() else ''
 edges=[]; exited=set()
 for line in text.splitlines():
  m=re.match(r'^(\d+)\s+',line)
  if not m:continue
  parent=int(m[1])
  if ('+++ exited with' in line or '+++ killed by' in line):exited.add(parent)
  if any(x in line for x in ['clone(', 'clone3(', 'fork(', 'vfork(', 'clone resumed>', 'clone3 resumed>', 'fork resumed>', 'vfork resumed>']):
   c=re.search(r'= (\d+)(?:\s+<[^>]*>)?$',line)
   if c and int(c[1])>0:edges.append((parent,int(c[1])))
 ids=set(roots)
 while True:
  nxt=ids|{c for p,c in edges if p in ids}
  if nxt==ids:break
  ids=nxt
 return ids,exited
def ps_snapshot(ids):
 out=subprocess.check_output(['ps','-eo','pid,ppid,pgid,sid,stat,etime,cmd'],text=True)
 lines=out.splitlines(); chosen=[lines[0]]
 for line in lines[1:]:
  if int(line.split()[0]) in ids:chosen.append(line)
 return '\n'.join(chosen)+'\n'
def run(side,row):
 stamp=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')
 directory=ROOT/f'{stamp}-{row}-{side}';directory.mkdir()
 checkout=SIDES[side]
 exe=SCRIPTS/('real.py' if row in ['P','H'] else 'detached.cjs' if row=='U' else 'fake.cjs')
 env=os.environ.copy();env.update(CAPTURE_RUN_DIR=str(directory),CAPTURE_ROW=row,CAPTURE_CHROME=str(CHROME),T3CODE_DEV_SEED='0',T3CODE_SERVER_BROWSER_SANDBOX='0',TMPDIR=str(directory))
 cmd=['strace','-f','-ttt','-T','-yy','-s','220','-e','trace=kill,exit_group,clone,clone3,fork,vfork,execve,write','-o',str(directory/'strace.log'),'node','--experimental-import-meta-resolve',str(SCRIPTS/'harness.mjs'),checkout,row,str(directory),str(exe)]
 metadata={'side':side,'row':row,'sha':subprocess.check_output(['git','rev-parse','HEAD'],cwd=checkout,text=True).strip(),'start':now(),'command':cmd,'cwd':checkout+'/apps/server','driver':'HtmlRender.preview / HtmlRender.publish service harness; no dev server or database','sandbox':'explicit T3CODE_SERVER_BROWSER_SANDBOX=0'}
 (directory/'metadata.json').write_text(json.dumps(metadata,indent=2)+'\n')
 print(json.dumps({'event':'capture_start','time':now(),'side':side,'row':row,'directory':str(directory)}),flush=True)
 with open(directory/'stdout.log','w') as stdout,open(directory/'stderr.log','w') as stderr:
  p=subprocess.Popen(cmd,cwd=checkout+'/apps/server',env=env,stdout=subprocess.PIPE,stderr=stderr,text=True,bufsize=1)
  metadata['strace_pid']=p.pid
  reaps=[]
  def reap_adopted():
   try: children=pathlib.Path(f'/proc/{os.getpid()}/task/{os.getpid()}/children').read_text().split()
   except FileNotFoundError:return
   for child in children:
    pid=int(child)
    if pid==p.pid:continue
    try:
     got,status=os.waitpid(pid,os.WNOHANG)
     if got:reaps.append({'time':now(),'pid':got,'wait_status':status})
    except ChildProcessError:pass
  selector=selectors.DefaultSelector();selector.register(p.stdout,selectors.EVENT_READ)
  deadline=time.monotonic()+50;done=False
  while time.monotonic()<deadline:
   reap_adopted()
   events=selector.select(timeout=min(.01,max(0,deadline-time.monotonic())))
   if events:
    line=p.stdout.readline()
    if not line:
     if p.poll() is not None:break
    else:
     stdout.write(line);stdout.flush()
     try:item=json.loads(line)
     except ValueError:continue
     if item.get('event')=='driver_done':done=True;break
   elif p.poll() is not None:break
  selector.close()
  metadata['driver_done_seen']=done;metadata['return_observed']=now()
  # Deliberate after-return survivor observation requested by the matrix.
  observation_end=time.monotonic()+3
  while time.monotonic()<observation_end:
   reap_adopted(); time.sleep(.01)
  browser=read_jsonl(directory/'browser.jsonl');roots={x['pid'] for x in browser if x['event']=='browser_start'}
  ids,exited=trace_tree(directory/'strace.log',roots)
  snapshot=ps_snapshot(ids)
  (directory/'ps-after-3s.txt').write_text('Observed '+now()+' (three seconds after driver_done / failed-driver observation)\n'+snapshot)
  survivors=[]
  for line in snapshot.splitlines()[1:]:
   fields=line.split(None,7)
   if fields and int(fields[0]) in ids and not fields[4].startswith('Z'):survivors.append(int(fields[0]))
  metadata.update(browser_roots=sorted(roots),tracked_browser_pids=sorted(ids),live_survivors_before_controller_cleanup=survivors,snapshot_time=now())
  cleanup=[]
  for pid in sorted(ids-exited,reverse=True):
   try:os.kill(pid,signal.SIGKILL);cleanup.append({'time':now(),'pid':pid,'signal':'SIGKILL','reason':'exact tracked browser PID after survivor snapshot'})
   except ProcessLookupError:pass
  if not done and p.poll() is None:
   hs=read_jsonl(directory/'harness.jsonl')
   if hs:
    try:os.kill(hs[0]['pid'],signal.SIGKILL);cleanup.append({'time':now(),'pid':hs[0]['pid'],'signal':'SIGKILL','reason':'driver deadline'})
    except ProcessLookupError:pass
  try:rest,_=p.communicate(timeout=10);stdout.write(rest or '')
  except subprocess.TimeoutExpired:
   os.kill(p.pid,signal.SIGTERM);rest,_=p.communicate(timeout=5);stdout.write(rest or '')
  metadata['strace_exit_code']=p.returncode
  metadata['cleanup']=cleanup
  metadata['adopted_reaps']=reaps
  while True:
   try:
    adopted,status=os.waitpid(-1,os.WNOHANG)
    if adopted==0:break
   except ChildProcessError:break
  (directory/'ps-after-cleanup.txt').write_text('Observed '+now()+'\n'+ps_snapshot(ids))
 metadata['end']=now()
 (directory/'metadata.json').write_text(json.dumps(metadata,indent=2)+'\n')
 with open(ROOT/'runs.jsonl','a') as f:f.write(json.dumps({'directory':str(directory),**metadata})+'\n')
 print(json.dumps({'event':'capture_end','time':now(),'side':side,'row':row,'done':done,'survivors':survivors,'strace_exit':metadata['strace_exit_code']}),flush=True)
 return directory
if __name__=='__main__':
 rows=sys.argv[1:] or ['P','H','U','F','T','K','D','L','E-error','E-interrupt','N']
 for row in rows:
  for side in SIDES:run(side,row)
