#!/usr/bin/env python3
"""
Drives Chrome via --remote-debugging-pipe to install the PWA at
http://127.0.0.1:8080/, which exercises ManifestToWebAppInstallInfoJob
and proves the cross-origin share_target.action is persisted.

The pipe transport (fd 3 -> chrome, fd 4 <- chrome) gives the client
AllowUnsafeOperations() = true, which is required for PWA.install.

Chrome stderr (containing the [POC-*] log lines) is printed to our stderr.
"""
import json
import os
import select
import subprocess
import sys
import time

CHROME = os.environ.get("CHROME", "/chromium/chromium/src/out/asan/chrome")
PROFILE = "/tmp/chrome-poc-profile"
TARGET_URL = "http://127.0.0.1:8080/"
MANIFEST_ID = "http://127.0.0.1:8080/"


def main():
    if os.path.exists(PROFILE):
        subprocess.run(["rm", "-rf", PROFILE], check=False)

    # Pipes: parent writes to write_fd, child reads from fd 3.
    #        child writes to fd 4, parent reads from read_fd.
    child_read, write_fd = os.pipe()      # child fd 3 <- parent write_fd
    read_fd, child_write = os.pipe()      # parent read_fd <- child fd 4

    cmd = [
        CHROME,
        "--no-sandbox",
        "--no-first-run",
        "--no-default-browser-check",
        "--disable-gpu",
        "--headless=new",
        "--user-data-dir=" + PROFILE,
        "--remote-debugging-pipe",
        "--enable-logging=stderr",
        "--v=0",
        "about:blank",
    ]

    # Manual fork/exec — subprocess.Popen's fd handling is too clever.
    pid = os.fork()
    if pid == 0:
        # Child.
        try:
            devnull = os.open(os.devnull, os.O_RDWR)
            os.dup2(devnull, 0)
            os.dup2(devnull, 1)
            # Leave fd 2 (stderr) inherited for log output.
            os.dup2(child_read, 3)
            os.dup2(child_write, 4)
            os.set_inheritable(3, True)
            os.set_inheritable(4, True)
            # Close everything else.
            for fd in (devnull, child_read, child_write, write_fd, read_fd):
                if fd > 4:
                    try:
                        os.close(fd)
                    except OSError:
                        pass
            os.execvp(cmd[0], cmd)
        except Exception as e:
            os.write(2, f"exec failed: {e}\n".encode())
        os._exit(127)

    # Parent.
    os.close(child_read)
    os.close(child_write)

    class _Proc:
        def __init__(self, pid):
            self.pid = pid
        def terminate(self):
            try: os.kill(self.pid, 15)
            except OSError: pass
        def kill(self):
            try: os.kill(self.pid, 9)
            except OSError: pass
        def wait(self, timeout=None):
            deadline = time.time() + (timeout or 1e9)
            while time.time() < deadline:
                p, _ = os.waitpid(self.pid, os.WNOHANG)
                if p == self.pid:
                    return
                time.sleep(0.1)
            raise subprocess.TimeoutExpired(cmd, timeout)
    proc = _Proc(pid)
    msg_id = 0
    buf = b""

    def send(method, params=None):
        nonlocal msg_id
        msg_id += 1
        payload = json.dumps({"id": msg_id, "method": method, "params": params or {}})
        os.write(write_fd, payload.encode() + b"\x00")  # NUL-separated frames
        return msg_id

    def recv_until(want_id, timeout=20.0):
        nonlocal buf
        deadline = time.time() + timeout
        while time.time() < deadline:
            ready, _, _ = select.select([read_fd], [], [], 0.5)
            if ready:
                chunk = os.read(read_fd, 65536)
                if not chunk:
                    return None  # EOF
                buf += chunk
                while b"\x00" in buf:
                    frame, buf = buf.split(b"\x00", 1)
                    if not frame:
                        continue
                    try:
                        msg = json.loads(frame)
                    except json.JSONDecodeError:
                        continue
                    if msg.get("id") == want_id:
                        return msg
        return None

    try:
        time.sleep(2)  # let chrome start up

        # Open the PWA page so the installer can resolve the manifest.
        i = send("Target.createTarget", {"url": TARGET_URL})
        r = recv_until(i)
        print(f"[cdp] Target.createTarget -> {r}", file=sys.stderr)
        time.sleep(4)  # let manifest fetch + installability checks finish

        # Install. This drives ManifestToWebAppInstallInfoJob.
        i = send("PWA.install", {
            "manifestId": MANIFEST_ID,
            "installUrlOrBundleUrl": TARGET_URL,
        })
        r = recv_until(i)
        print(f"[cdp] PWA.install -> {r}", file=sys.stderr)

        time.sleep(2)  # let install finish + log flush

    finally:
        os.close(write_fd)
        proc.terminate()
        try:
            proc.wait(timeout=5)
        except subprocess.TimeoutExpired:
            proc.kill()


if __name__ == "__main__":
    main()
