| 266 | self.cdp._event_registry.handle_event = tap |
| 267 | |
| 268 | async def handle(self, req): |
| 269 | # Token guard for Windows TCP loopback: any local process can otherwise |
| 270 | # connect and issue CDP commands. expected_token() is None on POSIX so |
| 271 | # this check is a no-op there (AF_UNIX + chmod 600 is the boundary). |
| 272 | expected = ipc.expected_token() |
| 273 | if expected is not None and req.get("token") != expected: |
| 274 | return {"error": "unauthorized"} |
| 275 | meta = req.get("meta") |
| 276 | # Liveness probe — lets clients confirm the listener is actually this |
| 277 | # daemon and not an unrelated process that reused our port post-crash. |
| 278 | # `pid` lets restart_daemon() verify the live daemon's identity before |
| 279 | # signaling — protects against SIGTERM-by-stale-pid-file after PID reuse. |
| 280 | if meta == "ping": return {"pong": True, "pid": os.getpid()} |
| 281 | if meta == "drain_events": |
| 282 | out = list(self.events); self.events.clear() |
| 283 | return {"events": out} |
| 284 | if meta == "session": return {"session_id": self.session} |
| 285 | if meta == "current_tab": |
| 286 | # Resolve the attached page's target info server-side. Helpers can't |
| 287 | # send Target.getTargetInfo themselves: daemon strips session_id for |
| 288 | # any Target.* method (browser-level call), and without a targetId |
| 289 | # Chrome silently returns the *browser* target. |
| 290 | if not self.target_id: |
| 291 | return {"error": "not_attached"} |
| 292 | try: |
| 293 | info = (await self.cdp.send_raw("Target.getTargetInfo", {"targetId": self.target_id}))["targetInfo"] |
| 294 | except Exception: |
| 295 | return {"error": "cdp_disconnected"} |
| 296 | return {"targetId": info.get("targetId"), "url": info.get("url", ""), "title": info.get("title", "")} |
| 297 | if meta == "connection_status": |
| 298 | if not self.target_id: |
| 299 | return {"error": "not_attached"} |
| 300 | try: |
| 301 | info = (await self.cdp.send_raw("Target.getTargetInfo", {"targetId": self.target_id}))["targetInfo"] |
| 302 | except Exception: |
| 303 | return {"error": "cdp_disconnected"} |
| 304 | page = None |
| 305 | if is_real_page(info): |
| 306 | page = { |
| 307 | "targetId": info.get("targetId"), |
| 308 | "title": info.get("title") or "(untitled)", |
| 309 | "url": info.get("url") or "", |
| 310 | } |
| 311 | return {"target_id": self.target_id, "session_id": self.session, "page": page} |
| 312 | if meta == "set_session": |
| 313 | old_session = self.session |
| 314 | self.session = req.get("session_id") |
| 315 | self.target_id = req.get("target_id") or self.target_id |
| 316 | # Run the old-session Network.disable (defense in depth — keeps |
| 317 | # background-tab traffic out of the global event buffer; the |
| 318 | # consumer-side filter in wait_for_network_idle is the actual |
| 319 | # correctness gate) in parallel with the four enables on the new |
| 320 | # session. Different sessions, independent CDP requests. Keeps |
| 321 | # the synchronous reply under the helper's 5s IPC read timeout |
| 322 | # even on a remote daemon — sequentially these would have stacked |
| 323 | # to ~22s worst case. |
| 324 | tasks = [] |
| 325 | if old_session and old_session != self.session: |