#!/usr/bin/env python3
"""Real built T3 server reproduction. Requires node, git, strace; no live state.
Run: python3 driver.py CHECKOUT OUTPUT_ROOT before|after PORT
Each invocation creates a new timestamped run with fresh synthetic repositories.
Private server startup output stays local; only recording.cast, summary.json,
process-evidence.txt and the rendered video are intended for publication.
"""
import sys, pathlib, subprocess, os, time, json, threading, urllib.request, signal, re, statistics
checkout=pathlib.Path(sys.argv[1]).resolve(); root=pathlib.Path(sys.argv[2]).resolve(); side=sys.argv[3]; port=int(sys.argv[4])
out=root/(side+'-'+time.strftime('%Y%m%d-%H%M%S')); out.mkdir(parents=True)
start=time.monotonic(); events=[]
cast=(out/'recording.cast').open('w'); cast.write(json.dumps({'version':2,'width':112,'height':30,'timestamp':int(time.time()),'title':'T3 server Git launch evidence - '+side})+'\n')
def emit(s):
    print(s,flush=True); events.append([round(time.monotonic()-start,3),s]); cast.write(json.dumps([round(time.monotonic()-start,3),'o',s+'\r\n'])+'\n'); cast.flush()
sha=subprocess.check_output(['git','rev-parse','HEAD'],cwd=checkout,text=True).strip()
emit('T3 Code | '+side.upper()+' | '+sha)
emit('Environment: Linux x86_64; built Node server; loopback HTTP; fresh database')
emit('No browser viewport/theme applies. Synthetic Git repositories only.')
repos=[]
for i in range(8):
    repo=out/('synthetic-repo-%02d'%i); repo.mkdir(); (repo/'README.md').write_text('Synthetic evidence project\n')
    subprocess.run(['git','init','-q',str(repo)],check=True)
    subprocess.run(['git','-C',str(repo),'add','.'],check=True)
    subprocess.run(['git','-C',str(repo),'-c','user.name=Evidence','-c','user.email=evidence@example.invalid','commit','-qm','Synthetic seed'],check=True)
    repos.append(repo)
home=out/'state'; env={**os.environ,'T3CODE_DEV_SEED':'0','T3CODE_TELEMETRY_DISABLED':'1'}
log=(out/'server-private.log').open('w')
cmd=['strace','-f','-ttt','-T','-s','160','-e','trace=process','-o',str(out/'trace-private.log'),'node',str(checkout/'apps/server/dist/bin.mjs'),'serve','--base-dir',str(home),'--host','127.0.0.1','--port',str(port),'--no-browser']
p=subprocess.Popen(cmd,cwd=checkout,env=env,stdout=log,stderr=log,start_new_session=True)
serverpid=None
try:
    url='http://127.0.0.1:'+str(port)+'/'
    for _ in range(300):
        if p.poll() is not None: raise RuntimeError('server exited; inspect private startup log')
        try:
            with urllib.request.urlopen(url,timeout=1) as r:
                status=r.status; body=r.read()
            break
        except Exception: time.sleep(.1)
    else: raise RuntimeError('server not ready')
    # strace is the captured parent; its direct child is the server.
    serverpid=int(pathlib.Path('/proc/%d/task/%d/children'%(p.pid,p.pid)).read_text().split()[0])
    emit('Server ready: GET / -> HTTP %d, %d bytes'%(status,len(body)))
    emit('Trace role: server PID %d (recorded at spawn)'%serverpid)
    lat=[]; statuses=[]; stop=threading.Event()
    def probe():
        while not stop.is_set():
            t=time.perf_counter()
            try:
                with urllib.request.urlopen(url,timeout=10) as r: statuses.append(r.status); r.read()
                lat.append((time.perf_counter()-t)*1000)
            except Exception: statuses.append('error')
            stop.wait(.02)
    thread=threading.Thread(target=probe); thread.start()
    for i,repo in enumerate(repos):
        t=time.monotonic()
        result=subprocess.run(['node',str(checkout/'apps/server/dist/bin.mjs'),'project','add',str(repo),'--title','Synthetic %02d'%i,'--base-dir',str(home)],cwd=checkout,env=env,capture_output=True,text=True,timeout=60)
        (out/('project-%02d-private.log'%i)).write_text(result.stdout+result.stderr)
        if result.returncode: raise RuntimeError('project add failed; inspect private output')
        emit('Step %d/8: add synthetic Git project through live server CLI -> OK (%.2fs)'%(i+1,time.monotonic()-t))
    time.sleep(2); stop.set(); thread.join()
    rss=int(re.search(r'VmRSS:\s+(\d+)',pathlib.Path('/proc/%d/status'%serverpid).read_text())[1])
    trace=(out/'trace-private.log').read_text()
    parents={}; execs={}; forks=[]
    for line in trace.splitlines():
        m=re.match(r'(\d+)\s+(\d+\.\d+)\s+(.*)',line)
        if not m: continue
        pid,ts,s=int(m[1]),float(m[2]),m[3]
        if ('clone(' in s or 'clone3(' in s or 'fork(' in s or 'vfork(' in s or 'resumed>' in s) and re.search(r'= (\d+)',s):
            child=int(re.search(r'= (\d+)',s)[1]); parents[child]=pid; forks.append((pid,child,s))
        x=re.search(r'execve\("([^"]+)"',s)
        if x and '= 0' in s: execs[pid]=x[1]
    gitpids=[pid for pid,path in execs.items() if pathlib.Path(path).name=='git']
    emit('Observed Git execs in server process tree: %d'%len(gitpids))
    lines=['Environment: Linux x86_64; built server; fresh database; synthetic seed.','Commit: '+sha,'Server PID: '+str(serverpid)]
    direct=0; helper=0
    for pid in gitpids:
        parent=parents.get(pid); role='server' if parent==serverpid else ('launcher' if parent in execs and pathlib.Path(execs[parent]).name=='node' else 'other')
        if role=='server': direct+=1
        if role=='launcher': helper+=1
        line='Git PID %d <- %s PID %s'%(pid,role,parent); lines.append(line)
    for line in lines[3:15]: emit(line)
    # Full process launch syscalls, with all private filesystem paths redacted.
    lines.append('\nSanitized process syscalls:')
    for line in trace.splitlines():
        if 'clone' in line or 'fork' in line or ('execve(' in line and ('/git"' in line or 'process-launcher' in line)):
            line=line.replace(str(root),'<evidence-root>').replace(str(checkout),'<checkout>')
            line=re.sub(r'"/[^\"]+"',lambda m: '"<executable>/'+pathlib.Path(m[0].strip('"')).name+'"',line)
            lines.append(line)
    (out/'process-evidence.txt').write_text('\n'.join(lines)+'\n')
    emit('RESULT: direct server Git forks = %d; helper Git forks = %d'%(direct,helper))
    sortedlat=sorted(lat); p95=sortedlat[int((len(sortedlat)-1)*.95)]
    emit('GET /: n=%d median=%.2fms p95=%.2fms max=%.2fms'%(len(lat),statistics.median(lat),p95,max(lat)))
    emit('HTTP statuses: '+str(dict((str(k),statuses.count(k)) for k in set(statuses))))
    emit('Server RSS: %.1f MiB. This run does not simulate high-memory agent load.'%(rss/1024))
    emit('Structural verdict only: verify server versus helper ancestry above.')
    summary={'side':side,'commit':sha,'gitExecs':len(gitpids),'directServerGitForks':direct,'helperGitForks':helper,'serverRssMiB':rss/1024,'httpSamples':len(lat),'medianMs':statistics.median(lat),'p95Ms':p95,'maxMs':max(lat),'statuses':dict((str(k),statuses.count(k)) for k in set(statuses)),'events':events}
    (out/'summary.json').write_text(json.dumps(summary,indent=2))
finally:
    if 'stop' in locals(): stop.set(); thread.join()
    # Only terminate the exact spawned process group, never a pattern.
    os.killpg(p.pid,signal.SIGTERM)
    try: p.wait(timeout=15)
    except subprocess.TimeoutExpired: os.killpg(p.pid,signal.SIGKILL); p.wait()
    cast.close(); log.close()
print('PRIVATE_RUN_DIRECTORY='+str(out))
