import os, sys, subprocess, json, time, pathlib, signal, urllib.request
root=pathlib.Path(sys.argv[1]); out=pathlib.Path(sys.argv[2]); out.mkdir(parents=True)
wrapper=out/'bin'; wrapper.mkdir(); (wrapper/'tailscale').write_text('#!/bin/sh\nexec sudo -n /usr/bin/tailscale "$@"\n'); (wrapper/'tailscale').chmod(0o755)
env=dict(os.environ, T3CODE_DEV_SEED='0', PATH=str(wrapper)+':'+os.environ['PATH'])
node=subprocess.check_output(['which','node'],text=True).strip()
status=json.loads(subprocess.check_output(['tailscale','status','--json']))
dns=status['Self']['DNSName'].rstrip('.'); ip=status['Self']['TailscaleIPs'][0]
start=time.monotonic(); lines=[]; processes=[]; frames=[]
def emit(s):
    line=f'{time.monotonic()-start:6.1f}s  {s}'; print(line,flush=True); lines.append(line)
    (out/'transcript.txt').write_text('\n'.join(lines)+'\n'); frames.append({'t':time.monotonic()-start,'lines':list(lines)})
def cli(*args): return subprocess.check_output(['sudo','-n','tailscale',*args],text=True,stderr=subprocess.STDOUT)
def http(port):
    try: return urllib.request.urlopen(f'http://127.0.0.1:{port}/.well-known/t3/environment',timeout=2).status
    except Exception: return 'unreachable'
def snapshot(label):
    data=json.loads(cli('serve','status','--json')); routes=[]
    for key,value in data.get('Web',{}).items():
        if key.endswith(':24463'): routes.extend(h.get('Proxy','non-proxy') for h in value.get('Handlers',{}).values())
    code=subprocess.run(['curl','--noproxy','*','-ksS','--max-time','5','--resolve',f'{dns}:24463:{ip}','-o','/dev/null','-w','%{http_code}',f'https://{dns}:24463/.well-known/t3/environment'],capture_output=True,text=True).stdout
    emit(f'{label}: Serve :24463 / -> {", ".join(routes) or "none"}')
    emit(f'  A loopback={http(24461)}; B loopback={http(24462)}; tailnet HTTPS={code}')
def spawn(name,port):
    log=open(out/f'{name}-private.log','w'); p=subprocess.Popen([node,'src/bin.ts','serve','--host','127.0.0.1','--port',str(port),'--base-dir',str(out/name),'--tailscale-serve','--tailscale-serve-port','24463'],cwd=root/'apps/server',env=env,stdout=log,stderr=subprocess.STDOUT); processes.append(p)
    for _ in range(120):
        if p.poll() is not None: raise RuntimeError(f'{name} exited; inspect private log')
        if http(port)==200: break
        time.sleep(.5)
    time.sleep(3)
    for line in (out/f'{name}-private.log').read_text().splitlines():
        for phrase in ['Tailscale Serve configured','Tailscale Serve left off; its port already serves another server or app']:
            if phrase in line: emit(f'{name} log: {phrase}')
    return p
try:
    emit('Linux; real T3 servers; fresh independent homes; real Tailscale CLI via sudo')
    emit('Start A on 24461, HTTPS Serve port 24463'); a=spawn('A',24461); snapshot('A started')
    emit('Start B on 24462 with the same Serve port'); b=spawn('B',24462); snapshot('B started')
    emit('SIGKILL B (unclean exit)'); b.kill(); b.wait(); snapshot('Immediately after killing B')
    for i in range(8): time.sleep(5); snapshot(f'After B exit +{(i+1)*5}s')
    emit('Set Serve explicitly to dead 24462 to exercise stale-route recovery on both commits')
    cli('serve','--bg','--https','24463','http://127.0.0.1:24462'); snapshot('Stale route injected')
    for i in range(8): time.sleep(5); snapshot(f'After stale route +{(i+1)*5}s')
    log=(out/'A-private.log').read_text()
    if 'Tailscale Serve repaired' in log: emit('A log: Tailscale Serve repaired')
    emit('End of observation; stop A cleanly and remove only the dedicated Serve route')
finally:
    for p in processes:
        if p.poll() is None:
            p.terminate()
            try: p.wait(15)
            except subprocess.TimeoutExpired: p.kill(); p.wait()
    (out/'frames.json').write_text(json.dumps(frames))
    if json.loads(cli('serve','status','--json')).get('Web'):
        cli('serve','--https','24463','off')
