browserwright 0.6.7__tar.gz → 0.6.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {browserwright-0.6.7 → browserwright-0.6.9}/PKG-INFO +1 -1
- {browserwright-0.6.7 → browserwright-0.6.9}/pyproject.toml +1 -1
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/relay.py +164 -3
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/_smart_goto.py +22 -2
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/PKG-INFO +1 -1
- {browserwright-0.6.7 → browserwright-0.6.9}/README.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/setup.cfg +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/__main__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/_executor/client.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/_executor/process.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/_executor/protocol.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/api.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/cdp.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/cli.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/_ipc.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/active_tab.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/auth.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/base.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/cloud.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/env.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/extension.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/backends/rdp.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/cli.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/config.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/doctor.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/launch_chrome.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/observability.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/platforms.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/resolver.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/daemon.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/executor_registry.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/extension_upstream.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/facade.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/facade_extension.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/listener.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/proxy.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/state.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/upstream.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/discovery.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/errors.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/health.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/install.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/_yaml.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/global_mem.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/repl_mem.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/session_decisions.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/memory/site_mem.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/mode_b_client.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/multitask.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/inspect.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/interact.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/page.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/primitives/site.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/release_install.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/inline.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/playwright_handle.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/session.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/session_create.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/session_registry.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/session_runtime.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/skill_doc.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/skill_runtime.md +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/subscriptions.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/task_runner.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/version.py +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/SOURCES.txt +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/requires.txt +0 -0
- {browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: cdp-use==1.4.5
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browserwright"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.9"
|
|
8
8
|
description = "Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends)."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
@@ -78,6 +78,9 @@ DEFAULT_RELAY_PORT = 19989
|
|
|
78
78
|
# with the playwriter / OpenCLI experience.
|
|
79
79
|
ATTACH_RETRY_LIMIT = 3
|
|
80
80
|
ATTACH_RETRY_BACKOFF = (0.1, 0.3, 0.8) # seconds; len must equal ATTACH_RETRY_LIMIT
|
|
81
|
+
APP_PING_INTERVAL = 5.0
|
|
82
|
+
STALE_FRAME_AFTER = 30.0
|
|
83
|
+
RECONNECT_WAIT_TIMEOUT = 35.0
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
@dataclass
|
|
@@ -110,6 +113,8 @@ class _ExtensionConn:
|
|
|
110
113
|
hello_received: asyncio.Event = field(default_factory=asyncio.Event)
|
|
111
114
|
pending: dict[int, asyncio.Future] = field(default_factory=dict)
|
|
112
115
|
tabs: dict[int, GhostTarget] = field(default_factory=dict)
|
|
116
|
+
last_frame_ts: float = field(default_factory=time.monotonic)
|
|
117
|
+
app_ping_task: asyncio.Task | None = None
|
|
113
118
|
|
|
114
119
|
|
|
115
120
|
class RelayServer:
|
|
@@ -273,6 +278,9 @@ class RelayServer:
|
|
|
273
278
|
heuristic-recent-activate table.
|
|
274
279
|
"""
|
|
275
280
|
ext = self._pick_active_extension()
|
|
281
|
+
if ext is None:
|
|
282
|
+
return None
|
|
283
|
+
ext = await self._ensure_extension_fresh(ext)
|
|
276
284
|
if ext is None:
|
|
277
285
|
return None
|
|
278
286
|
return await self._request(ext, {"type": "queryActiveTab"},
|
|
@@ -280,7 +288,7 @@ class RelayServer:
|
|
|
280
288
|
|
|
281
289
|
async def query_group_tabs(self, group_name: str | None = None, *,
|
|
282
290
|
group_id: int | None = None,
|
|
283
|
-
timeout: float =
|
|
291
|
+
timeout: float = 15.0) -> dict | None:
|
|
284
292
|
"""Live membership query: ask the extension for the tabs of the
|
|
285
293
|
session's tab group. ``group_id`` is the durable primary key (the
|
|
286
294
|
numeric Chrome groupId); ``group_name`` is accepted for older callers
|
|
@@ -291,6 +299,9 @@ class RelayServer:
|
|
|
291
299
|
Returns None when no extension is connected (mirrors
|
|
292
300
|
query_active_tab's caller-falls-back contract)."""
|
|
293
301
|
ext = self._pick_active_extension()
|
|
302
|
+
if ext is None:
|
|
303
|
+
return None
|
|
304
|
+
ext = await self._ensure_extension_fresh(ext)
|
|
294
305
|
if ext is None:
|
|
295
306
|
return None
|
|
296
307
|
body: dict = {"type": "queryGroup"}
|
|
@@ -322,6 +333,7 @@ class RelayServer:
|
|
|
322
333
|
ext = self._pick_active_extension()
|
|
323
334
|
if ext is None:
|
|
324
335
|
raise RuntimeError("no extension connected")
|
|
336
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
325
337
|
last_err: Exception | None = None
|
|
326
338
|
body: dict = {"type": "attachActive"}
|
|
327
339
|
if group_name:
|
|
@@ -367,6 +379,7 @@ class RelayServer:
|
|
|
367
379
|
ext = self._pick_active_extension()
|
|
368
380
|
if ext is None:
|
|
369
381
|
raise RuntimeError("no extension connected")
|
|
382
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
370
383
|
# Idempotency: extension may already hold chrome.debugger.attach on
|
|
371
384
|
# this tab (popup click, prior daemon lifecycle — the SW survives
|
|
372
385
|
# daemon restarts and re-announces attached tabs on reconnect, so
|
|
@@ -403,6 +416,9 @@ class RelayServer:
|
|
|
403
416
|
async def detach_tab(self, tab_id: int, *,
|
|
404
417
|
timeout: float = 5.0) -> None:
|
|
405
418
|
ext = self._extension_for_tab(tab_id)
|
|
419
|
+
if ext is None:
|
|
420
|
+
return
|
|
421
|
+
ext = await self._ensure_extension_fresh(ext)
|
|
406
422
|
if ext is None:
|
|
407
423
|
return
|
|
408
424
|
try:
|
|
@@ -440,6 +456,7 @@ class RelayServer:
|
|
|
440
456
|
ext = self._pick_active_extension()
|
|
441
457
|
if ext is None:
|
|
442
458
|
raise RuntimeError("no extension connected")
|
|
459
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
443
460
|
body: dict = {"type": "createTab", "url": url}
|
|
444
461
|
if group_name:
|
|
445
462
|
body["groupName"] = group_name
|
|
@@ -489,6 +506,7 @@ class RelayServer:
|
|
|
489
506
|
ext = self._extension_for_tab(tab_id)
|
|
490
507
|
if ext is None:
|
|
491
508
|
raise RuntimeError(f"no extension knows tab {tab_id}")
|
|
509
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
492
510
|
try:
|
|
493
511
|
await self._request(
|
|
494
512
|
ext, {"type": "closeTab", "tabId": tab_id}, timeout=timeout)
|
|
@@ -504,6 +522,7 @@ class RelayServer:
|
|
|
504
522
|
ext = self._extension_for_tab(tab_id)
|
|
505
523
|
if ext is None:
|
|
506
524
|
raise RuntimeError(f"no extension owns tab {tab_id}")
|
|
525
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
507
526
|
return await self._request(ext, {
|
|
508
527
|
"type": "command",
|
|
509
528
|
"tabId": tab_id,
|
|
@@ -522,6 +541,7 @@ class RelayServer:
|
|
|
522
541
|
ext = self._pick_active_extension()
|
|
523
542
|
if ext is None:
|
|
524
543
|
raise RuntimeError("no extension connected")
|
|
544
|
+
ext = await self._ensure_extension_fresh_or_raise(ext)
|
|
525
545
|
return await self._request(
|
|
526
546
|
ext, {"type": f"userscript.{verb}", **payload}, timeout=timeout)
|
|
527
547
|
|
|
@@ -546,6 +566,106 @@ class RelayServer:
|
|
|
546
566
|
self._next_cmd_id += 1
|
|
547
567
|
return v
|
|
548
568
|
|
|
569
|
+
def _extension_is_stale(self, ext: _ExtensionConn) -> bool:
|
|
570
|
+
if not ext.hello_received.is_set():
|
|
571
|
+
return False
|
|
572
|
+
return (time.monotonic() - ext.last_frame_ts) > STALE_FRAME_AFTER
|
|
573
|
+
|
|
574
|
+
async def _ensure_extension_fresh(
|
|
575
|
+
self, ext: _ExtensionConn,
|
|
576
|
+
) -> _ExtensionConn | None:
|
|
577
|
+
"""Return a live extension connection, force-closing ghost sockets.
|
|
578
|
+
|
|
579
|
+
MV3 can suspend the SW while Chrome's network process keeps the TCP
|
|
580
|
+
websocket ESTABLISHED. Protocol pings still succeed there, but no app
|
|
581
|
+
frames arrive. The daemon treats missing app frames as authoritative
|
|
582
|
+
and tears down the ghost before sending a user command.
|
|
583
|
+
"""
|
|
584
|
+
if not self._extension_is_stale(ext):
|
|
585
|
+
return ext
|
|
586
|
+
await self._force_close_extension(ext, reason="stale app-level heartbeat")
|
|
587
|
+
return await self._wait_for_replacement(ext, timeout=RECONNECT_WAIT_TIMEOUT)
|
|
588
|
+
|
|
589
|
+
async def _ensure_extension_fresh_or_raise(
|
|
590
|
+
self, ext: _ExtensionConn,
|
|
591
|
+
) -> _ExtensionConn:
|
|
592
|
+
fresh = await self._ensure_extension_fresh(ext)
|
|
593
|
+
if fresh is None:
|
|
594
|
+
raise RuntimeError(
|
|
595
|
+
"extension relay connection appears stale and did not reconnect "
|
|
596
|
+
f"within {RECONNECT_WAIT_TIMEOUT:.0f}s")
|
|
597
|
+
return fresh
|
|
598
|
+
|
|
599
|
+
async def _force_close_extension(self, ext: _ExtensionConn, *, reason: str) -> None:
|
|
600
|
+
logger.warning(
|
|
601
|
+
"force-closing stale extension relay connection: install_id=%s reason=%s",
|
|
602
|
+
ext.install_id or "(pending)",
|
|
603
|
+
reason,
|
|
604
|
+
)
|
|
605
|
+
for fut in list(ext.pending.values()):
|
|
606
|
+
if not fut.done():
|
|
607
|
+
fut.set_exception(ConnectionError(f"extension relay closed: {reason}"))
|
|
608
|
+
if fut.cancelled():
|
|
609
|
+
continue
|
|
610
|
+
with contextlib.suppress(BaseException):
|
|
611
|
+
fut.exception()
|
|
612
|
+
with contextlib.suppress(Exception):
|
|
613
|
+
await asyncio.wait_for(
|
|
614
|
+
ext.conn.close(code=1011, reason=reason),
|
|
615
|
+
timeout=1.0,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
async def _wait_for_replacement(
|
|
619
|
+
self, old_ext: _ExtensionConn, *, timeout: float,
|
|
620
|
+
allow_any_install: bool = False,
|
|
621
|
+
) -> _ExtensionConn | None:
|
|
622
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
623
|
+
install_id = old_ext.install_id
|
|
624
|
+
while time.monotonic() < deadline:
|
|
625
|
+
candidates = [
|
|
626
|
+
ext for ext in self._extensions.values()
|
|
627
|
+
if ext is not old_ext and ext.hello_received.is_set()
|
|
628
|
+
]
|
|
629
|
+
if install_id:
|
|
630
|
+
for ext in candidates:
|
|
631
|
+
if ext.install_id == install_id:
|
|
632
|
+
return ext
|
|
633
|
+
if allow_any_install and candidates:
|
|
634
|
+
return candidates[0]
|
|
635
|
+
await asyncio.sleep(0.1)
|
|
636
|
+
if install_id:
|
|
637
|
+
return None
|
|
638
|
+
if candidates := [
|
|
639
|
+
ext for ext in self._extensions.values()
|
|
640
|
+
if ext is not old_ext and ext.hello_received.is_set()
|
|
641
|
+
]:
|
|
642
|
+
return candidates[0]
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
async def _retry_request_on_replacement(
|
|
646
|
+
self,
|
|
647
|
+
ext: _ExtensionConn,
|
|
648
|
+
body: dict,
|
|
649
|
+
*,
|
|
650
|
+
timeout: float,
|
|
651
|
+
loop: asyncio.AbstractEventLoop,
|
|
652
|
+
) -> dict | None:
|
|
653
|
+
await self._force_close_extension(ext, reason=f"{body.get('type')} request failed")
|
|
654
|
+
replacement = await self._wait_for_replacement(
|
|
655
|
+
ext, timeout=RECONNECT_WAIT_TIMEOUT)
|
|
656
|
+
if replacement is None:
|
|
657
|
+
raise ConnectionError(
|
|
658
|
+
"extension relay did not reconnect after request failure")
|
|
659
|
+
retry_id = self._alloc_id()
|
|
660
|
+
retry_body = {**{k: v for k, v in body.items() if k != "id"}, "id": retry_id}
|
|
661
|
+
retry_fut: asyncio.Future = loop.create_future()
|
|
662
|
+
replacement.pending[retry_id] = retry_fut
|
|
663
|
+
try:
|
|
664
|
+
await replacement.conn.send(json.dumps(retry_body))
|
|
665
|
+
return await asyncio.wait_for(retry_fut, timeout=timeout)
|
|
666
|
+
finally:
|
|
667
|
+
replacement.pending.pop(retry_id, None)
|
|
668
|
+
|
|
549
669
|
async def _request(self, ext: _ExtensionConn, body: dict, *,
|
|
550
670
|
timeout: float) -> dict | None:
|
|
551
671
|
cmd_id = self._alloc_id()
|
|
@@ -556,7 +676,18 @@ class RelayServer:
|
|
|
556
676
|
try:
|
|
557
677
|
await ext.conn.send(json.dumps(body))
|
|
558
678
|
return await asyncio.wait_for(fut, timeout=timeout)
|
|
679
|
+
except asyncio.TimeoutError:
|
|
680
|
+
if not self._extension_is_stale(ext):
|
|
681
|
+
raise
|
|
682
|
+
return await self._retry_request_on_replacement(
|
|
683
|
+
ext, body, timeout=timeout, loop=loop)
|
|
684
|
+
except (ConnectionError, websockets.exceptions.ConnectionClosed):
|
|
685
|
+
return await self._retry_request_on_replacement(
|
|
686
|
+
ext, body, timeout=timeout, loop=loop)
|
|
559
687
|
finally:
|
|
688
|
+
if not fut.cancelled():
|
|
689
|
+
with contextlib.suppress(BaseException):
|
|
690
|
+
fut.exception()
|
|
560
691
|
ext.pending.pop(cmd_id, None)
|
|
561
692
|
|
|
562
693
|
# ---- ws handlers -----------------------------------------------------
|
|
@@ -633,6 +764,7 @@ class RelayServer:
|
|
|
633
764
|
self._extensions[temp_key] = ext
|
|
634
765
|
try:
|
|
635
766
|
async for raw in conn:
|
|
767
|
+
ext.last_frame_ts = time.monotonic()
|
|
636
768
|
if not isinstance(raw, (str, bytes)):
|
|
637
769
|
continue
|
|
638
770
|
text = raw if isinstance(raw, str) else raw.decode("utf-8", errors="replace")
|
|
@@ -650,8 +782,12 @@ class RelayServer:
|
|
|
650
782
|
logger.warning("extension handler crashed: %r", e)
|
|
651
783
|
finally:
|
|
652
784
|
key = ext.install_id or temp_key
|
|
653
|
-
self._extensions.
|
|
654
|
-
|
|
785
|
+
if self._extensions.get(key) is ext:
|
|
786
|
+
self._extensions.pop(key, None)
|
|
787
|
+
if self._extensions.get(temp_key) is ext:
|
|
788
|
+
self._extensions.pop(temp_key, None)
|
|
789
|
+
if ext.app_ping_task is not None:
|
|
790
|
+
ext.app_ping_task.cancel()
|
|
655
791
|
for fut in list(ext.pending.values()):
|
|
656
792
|
if not fut.done():
|
|
657
793
|
fut.set_exception(ConnectionError("extension disconnected"))
|
|
@@ -673,6 +809,8 @@ class RelayServer:
|
|
|
673
809
|
self._extensions.pop(temp_key, None)
|
|
674
810
|
self._extensions[ext.install_id or temp_key] = ext
|
|
675
811
|
ext.hello_received.set()
|
|
812
|
+
if ext.app_ping_task is None or ext.app_ping_task.done():
|
|
813
|
+
ext.app_ping_task = asyncio.create_task(self._app_ping_loop(ext))
|
|
676
814
|
self._first_ready.set()
|
|
677
815
|
if (
|
|
678
816
|
ext.extension_protocol_version
|
|
@@ -709,6 +847,9 @@ class RelayServer:
|
|
|
709
847
|
pass
|
|
710
848
|
return
|
|
711
849
|
|
|
850
|
+
if kind == "pong":
|
|
851
|
+
return
|
|
852
|
+
|
|
712
853
|
if kind == "attached":
|
|
713
854
|
tab_id = int(msg.get("tabId", -1))
|
|
714
855
|
if tab_id < 0:
|
|
@@ -762,6 +903,26 @@ class RelayServer:
|
|
|
762
903
|
|
|
763
904
|
logger.debug("extension sent unknown type %r: %s", kind, str(msg)[:100])
|
|
764
905
|
|
|
906
|
+
async def _app_ping_loop(self, ext: _ExtensionConn) -> None:
|
|
907
|
+
try:
|
|
908
|
+
while True:
|
|
909
|
+
await asyncio.sleep(APP_PING_INTERVAL)
|
|
910
|
+
if not ext.hello_received.is_set():
|
|
911
|
+
continue
|
|
912
|
+
if self._extension_is_stale(ext):
|
|
913
|
+
await self._force_close_extension(
|
|
914
|
+
ext, reason="missing app-level frames")
|
|
915
|
+
return
|
|
916
|
+
try:
|
|
917
|
+
await ext.conn.send(json.dumps({
|
|
918
|
+
"type": "ping",
|
|
919
|
+
"ts": int(time.time() * 1000),
|
|
920
|
+
}))
|
|
921
|
+
except Exception:
|
|
922
|
+
return
|
|
923
|
+
except asyncio.CancelledError:
|
|
924
|
+
raise
|
|
925
|
+
|
|
765
926
|
async def _fanout_listeners(self, msg: dict) -> None:
|
|
766
927
|
"""Call every additional fan-out observer with the raw extension
|
|
767
928
|
message (PR2). Isolated from the primary `_on_event` so one observer
|
|
@@ -62,8 +62,11 @@ def patch_page_goto(page: Any) -> Any:
|
|
|
62
62
|
response = orig_goto(url, timeout=timeout_ms,
|
|
63
63
|
wait_until="commit", referer=referer)
|
|
64
64
|
except Exception as exc: # noqa: BLE001 - translate Playwright failures.
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if _looks_loaded(self):
|
|
66
|
+
response = None
|
|
67
|
+
else:
|
|
68
|
+
network.detach()
|
|
69
|
+
raise _page_load_failed(url, "commit", exc) from exc
|
|
67
70
|
|
|
68
71
|
_wait_for_domcontentloaded(self, _remaining_timeout_ms(deadline))
|
|
69
72
|
try:
|
|
@@ -115,6 +118,23 @@ def _wait_for_domcontentloaded(page: Any, remaining_timeout_ms: int) -> None:
|
|
|
115
118
|
pass
|
|
116
119
|
|
|
117
120
|
|
|
121
|
+
def _looks_loaded(page: Any) -> bool:
|
|
122
|
+
"""Detect successful navigations masked by commit watcher races.
|
|
123
|
+
|
|
124
|
+
Some redirects/client transitions can leave Playwright's commit wait in an
|
|
125
|
+
error state even after the document is usable. Treat any probe failure as
|
|
126
|
+
not loaded so true failures still follow the existing PageLoadFailed path.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
url = page.url
|
|
130
|
+
if not url or url == "about:blank":
|
|
131
|
+
return False
|
|
132
|
+
ready_state = page.evaluate("() => document.readyState")
|
|
133
|
+
return ready_state != "loading"
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
118
138
|
def _smart_wait_settled(page: Any, deadline: float | None, network: "_NetworkMonitor") -> None:
|
|
119
139
|
try:
|
|
120
140
|
page.evaluate(_INSTALL_MONITOR_JS)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: Browserwright — let AI/code agents drive a real or isolated browser and author userscripts. Single package: the agent-facing REPL/site-skills/memory layer plus the bundled browser-resolving daemon (CDP proxy + extension/cloud backends).
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: cdp-use==1.4.5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/executor_registry.py
RENAMED
|
File without changes
|
{browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/extension_upstream.py
RENAMED
|
File without changes
|
|
File without changes
|
{browserwright-0.6.7 → browserwright-0.6.9}/src/browserwright/daemon/server/facade_extension.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|