#!/usr/bin/env python3
"""Record a real isolated T3 server shutdown; run once for each revision.
Prepare a fresh --base-dir, start bin.ts serve, add a project, and complete
one Codex turn with the CLI. Pass that base directory and an output prefix.
Only the server-runtime.json PID and its direct Codex child are signalled.
"""
import argparse, json, os, pathlib, signal, time
p=argparse.ArgumentParser()
p.add_argument('home'); p.add_argument('output'); p.add_argument('--side',required=True); p.add_argument('--sha',required=True)
a=p.parse_args(); start=time.monotonic(); events=[]
def emit(message):
    t=round(time.monotonic()-start,3); events.append([t,message]); print(f'{t:7.3f}s  {message}',flush=True)
def proc(pid):
    try:
        stat=pathlib.Path(f'/proc/{pid}/stat').read_text().split(') ',1)[1].split()
        return {'state':stat[0],'ppid':int(stat[1]),'cmd':pathlib.Path(f'/proc/{pid}/cmdline').read_bytes().replace(b'\0',b' ').decode()}
    except FileNotFoundError: return None
def alive(pid):
    s=proc(pid); return s is not None and s['state']!='Z'
r=json.loads((pathlib.Path(a.home)/'userdata/server-runtime.json').read_text()); server=r['pid']
children=[]
for entry in pathlib.Path('/proc').iterdir():
    if entry.name.isdigit():
        s=proc(int(entry.name))
        if s and s['ppid']==server and 'codex app-server' in s['cmd']: children.append(int(entry.name))
if len(children)!=1: raise RuntimeError('Expected exactly one direct Codex app-server child')
child=children[0]
emit(f'{a.side.upper()} | commit {a.sha[:10]} | Linux x86_64 | server CLI')
emit('Fresh isolated T3 home; one real Codex turn completed: OK')
emit('Verified app-server is a direct child of this isolated T3 server')
os.kill(child,signal.SIGSTOP)
for _ in range(100):
    if proc(child)['state']=='T':break
    time.sleep(.01)
else:raise RuntimeError('Child did not stop')
emit('Sent SIGSTOP to app-server; process state T (stopped)')
emit('Sending SIGTERM to T3 server: request graceful shutdown')
os.kill(server,signal.SIGTERM); sent=time.monotonic()
for checkpoint in [0.5,1,2,2.5,3,4,6,8]:
    time.sleep(max(0,sent+checkpoint-time.monotonic()))
    c=proc(child); s=alive(server)
    pending=''
    if c and c['state']!='Z':
        status=pathlib.Path(f'/proc/{child}/status').read_text()
        masks=[int(line.split()[1],16) for line in status.splitlines() if line.startswith(('SigPnd:','ShdPnd:'))]
        if any(mask & (1 << (signal.SIGTERM-1)) for mask in masks): pending='; SIGTERM pending'
    emit(f'+{checkpoint:.1f}s: T3 server {"alive" if s else "exited"}; app-server {c["state"] if c and c["state"]!="Z" else "exited"}{pending}')
    if not s and not alive(child):break
hung=alive(server) and alive(child)
emit('OBSERVED: cleanup still blocked after 8 seconds' if hung else 'OBSERVED: app-server terminated and graceful server shutdown finished')
if alive(child):
    emit('Evidence window ended; rescue SIGKILL to the captured child PID')
    os.kill(child,signal.SIGKILL)
for _ in range(100):
    if not alive(server):break
    time.sleep(.1)
if alive(server):
    emit('Rescue SIGKILL to the captured server PID');os.kill(server,signal.SIGKILL)
else:emit('Confirmed isolated server exited; no process left running')
pathlib.Path(a.output+'.json').write_text(json.dumps({'side':a.side,'sha':a.sha,'hung':hung,'events':events},indent=2))
pathlib.Path(a.output+'.txt').write_text('\n'.join(f'{t:7.3f}s  {m}' for t,m in events)+'\n')
with open(a.output+'.cast','w') as f:
    f.write(json.dumps({'version':2,'width':110,'height':28,'timestamp':int(time.time()),'env':{'TERM':'xterm-256color'}})+'\n')
    for t,m in events:f.write(json.dumps([t,'o',f'{t:7.3f}s  {m}\r\n'])+'\n')
