Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/browser_harness/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def _load_env_file(p):
LOG = str(ipc.log_path(NAME))
PID = str(ipc.pid_path(NAME))
BUF = 500
CDP_CALL_TIMEOUT = 4.5
PROFILES = [
Path.home() / "Library/Application Support/Google/Chrome",
Path.home() / "Library/Application Support/Google/Chrome Canary",
Expand Down Expand Up @@ -229,6 +230,17 @@ async def enable_one(d):
log(f"enable {d} on {session_id}: {e}")
await asyncio.gather(*(enable_one(d) for d in ("Page", "DOM", "Runtime", "Network")))

async def send_cdp(self, method, params=None, session_id=None):
if self.cdp is None:
raise RuntimeError("CDP client not started")
try:
return await asyncio.wait_for(
self.cdp.send_raw(method, params, session_id=session_id),
timeout=CDP_CALL_TIMEOUT,
)
except asyncio.TimeoutError:
raise TimeoutError(f"{method} timed out after {CDP_CALL_TIMEOUT:g}s") from None

async def start(self):
self.stop = asyncio.Event()
url = get_ws_url()
Expand Down Expand Up @@ -346,13 +358,13 @@ async def disable_old():
# For everything else, explicit session in req wins; else default.
sid = None if method.startswith("Target.") else (req.get("session_id") or self.session)
try:
return {"result": await self.cdp.send_raw(method, params, session_id=sid)}
return {"result": await self.send_cdp(method, params, session_id=sid)}
except Exception as e:
msg = str(e)
if "Session with given id not found" in msg and sid == self.session and sid:
log(f"stale session {sid}, re-attaching")
if await self.attach_first_page():
return {"result": await self.cdp.send_raw(method, params, session_id=self.session)}
return {"result": await self.send_cdp(method, params, session_id=self.session)}
return {"error": msg}


Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,36 @@ def test_current_tab_meta_returns_not_attached_when_no_target_id():
assert result == {"error": "not_attached"}
# No CDP call should have been issued.
assert d.cdp.calls == []


def test_regular_cdp_calls_timeout_before_client_socket_deadline(monkeypatch):
"""If Chrome/CDP stops answering, daemon.handle() must return a useful
error before the helpers.py IPC client hits its 5s socket timeout.

Otherwise the helper sees only a client-side timeout at _ipc.request(),
and the daemon can later log ``conn: Connection lost`` when it finally
tries to write to a socket the helper already closed.
"""
class _HangingCDP:
def __init__(self):
self.calls = []

async def send_raw(self, method, params=None, session_id=None):
self.calls.append((method, params, session_id))
await asyncio.Event().wait()

d = daemon.Daemon()
d.cdp = _HangingCDP()
d.session = "session-current"
monkeypatch.setattr(daemon, "CDP_CALL_TIMEOUT", 0.01, raising=False)

async def run():
return await asyncio.wait_for(
d.handle({"method": "Page.navigate", "params": {"url": "https://example.com"}}),
timeout=0.5,
)

result = asyncio.run(run())

assert result == {"error": "Page.navigate timed out after 0.01s"}
assert d.cdp.calls == [("Page.navigate", {"url": "https://example.com"}, "session-current")]