90 lines
4.1 KiB
Python
90 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""tools/harness-cdp.py CHROME URL [TIMEOUT_S] [CHROME_FLAG ...]
|
||
|
|
|
||
|
|
Runs a harness page in headless Chrome in real time and prints its #diag once it says END (or the timeout passes).
|
||
|
|
--dump-dom with --virtual-time-budget (the loop harness) is deterministic but IndexedDB answers in real time and
|
||
|
|
virtual time does not wait for it, so the resume harness is driven over the DevTools protocol instead: start Chrome
|
||
|
|
with a debugging port, attach to the page, poll Runtime.evaluate. Stdlib only (a 40-line websocket client)."""
|
||
|
|
import base64, json, os, re, socket, struct, subprocess, sys, tempfile, time, urllib.request
|
||
|
|
|
||
|
|
|
||
|
|
class WS:
|
||
|
|
def __init__(self, url):
|
||
|
|
m = re.match(r"ws://([^:/]+):(\d+)(/.*)", url)
|
||
|
|
self.s = socket.create_connection((m.group(1), int(m.group(2))), timeout=10)
|
||
|
|
key = base64.b64encode(os.urandom(16)).decode()
|
||
|
|
self.s.sendall((f"GET {m.group(3)} HTTP/1.1\r\nHost: {m.group(1)}:{m.group(2)}\r\nUpgrade: websocket\r\n"
|
||
|
|
f"Connection: Upgrade\r\nSec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
|
||
|
|
buf = b""
|
||
|
|
while b"\r\n\r\n" not in buf: buf += self.s.recv(4096)
|
||
|
|
assert b" 101 " in buf.split(b"\r\n")[0], buf[:200]
|
||
|
|
self.buf = buf.split(b"\r\n\r\n", 1)[1]
|
||
|
|
self.n = 0
|
||
|
|
|
||
|
|
def _read(self, n):
|
||
|
|
while len(self.buf) < n: self.buf += self.s.recv(65536)
|
||
|
|
out, self.buf = self.buf[:n], self.buf[n:]
|
||
|
|
return out
|
||
|
|
|
||
|
|
def send(self, obj):
|
||
|
|
data = json.dumps(obj).encode()
|
||
|
|
head = bytes([0x81])
|
||
|
|
if len(data) < 126: head += bytes([0x80 | len(data)])
|
||
|
|
elif len(data) < 65536: head += bytes([0x80 | 126]) + struct.pack(">H", len(data))
|
||
|
|
else: head += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
|
||
|
|
mask = os.urandom(4)
|
||
|
|
self.s.sendall(head + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
|
||
|
|
|
||
|
|
def recv(self):
|
||
|
|
while True:
|
||
|
|
b0, b1 = self._read(2)
|
||
|
|
n = b1 & 0x7F
|
||
|
|
if n == 126: n = struct.unpack(">H", self._read(2))[0]
|
||
|
|
elif n == 127: n = struct.unpack(">Q", self._read(8))[0]
|
||
|
|
if b1 & 0x80: self._read(4)
|
||
|
|
data = self._read(n)
|
||
|
|
op = b0 & 0x0F
|
||
|
|
if op == 1: return json.loads(data)
|
||
|
|
if op == 8: raise EOFError("closed")
|
||
|
|
|
||
|
|
def call(self, method, **params):
|
||
|
|
self.n += 1
|
||
|
|
self.send({"id": self.n, "method": method, "params": params})
|
||
|
|
while True:
|
||
|
|
m = self.recv()
|
||
|
|
if m.get("id") == self.n: return m.get("result", {})
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
chrome, url = sys.argv[1], sys.argv[2]
|
||
|
|
timeout = float(sys.argv[3]) if len(sys.argv) > 3 else 30
|
||
|
|
flags = sys.argv[4:]
|
||
|
|
prof = tempfile.mkdtemp()
|
||
|
|
p = subprocess.Popen([chrome, "--headless=new", "--disable-gpu", "--remote-debugging-port=0", f"--user-data-dir={prof}", *flags, "about:blank"],
|
||
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
||
|
|
port = None
|
||
|
|
for line in p.stderr:
|
||
|
|
m = re.search(r"DevTools listening on ws://127\.0\.0\.1:(\d+)/", line)
|
||
|
|
if m: port = m.group(1); break
|
||
|
|
if not port: print("no diag: Chrome gave no DevTools port"); p.kill(); return 1
|
||
|
|
try:
|
||
|
|
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
|
||
|
|
page = next(t for t in targets if t["type"] == "page")
|
||
|
|
ws = WS(page["webSocketDebuggerUrl"])
|
||
|
|
ws.call("Page.enable"); ws.call("Runtime.enable")
|
||
|
|
ws.call("Page.navigate", url=url)
|
||
|
|
t0 = time.time(); text = ""
|
||
|
|
while time.time() - t0 < timeout:
|
||
|
|
time.sleep(0.25)
|
||
|
|
r = ws.call("Runtime.evaluate", expression="(document.getElementById('diag')||{}).textContent||''", returnByValue=True)
|
||
|
|
text = r.get("result", {}).get("value", "") or ""
|
||
|
|
if "\nEND" in text or text.startswith("END") or text.startswith("TOPERR"): break
|
||
|
|
print(text if text else "no diag: the harness did not run")
|
||
|
|
finally:
|
||
|
|
p.kill()
|
||
|
|
subprocess.run(["rm", "-rf", prof])
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|