browserwright 0.17.4__tar.gz → 0.18.0__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.17.4/src/browserwright.egg-info → browserwright-0.18.0}/PKG-INFO +1 -1
- {browserwright-0.17.4 → browserwright-0.18.0}/pyproject.toml +1 -1
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_executor/client.py +5 -7
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_executor/process.py +94 -13
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/cdp.py +2 -3
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/cli.py +106 -3
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/_ipc.py +330 -4
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/cli.py +81 -11
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/doctor.py +5 -1
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/launchagent.py +160 -9
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/probe.py +24 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/daemon.py +19 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/exec_relay.py +66 -3
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/executor_registry.py +143 -16
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/facade.py +22 -4
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/facade_extension.py +2 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/listener.py +260 -31
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/relay.py +35 -7
- browserwright-0.18.0/src/browserwright/daemon/server/session_state.py +218 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/status.py +51 -2
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/verbs.py +204 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon_url.py +82 -39
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/errors.py +21 -17
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/health.py +102 -20
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/mode_b_client.py +4 -2
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/session_create.py +90 -7
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/skill_runtime.md +18 -28
- {browserwright-0.17.4 → browserwright-0.18.0/src/browserwright.egg-info}/PKG-INFO +1 -1
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright.egg-info/SOURCES.txt +1 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/LICENSE +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/README.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/setup.cfg +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/__main__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_executor/protocol.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/_text.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/_net.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/_rpc.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/_stale.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/backends/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/backends/base.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/backends/cdp.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/backends/extension.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/config.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/launch_chrome.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/observability.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/platforms.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/relay_status.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/resolver.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/restart_guard.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/extension_upstream.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/proxy.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/state.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/server/upstream.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/supervise.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/discovery.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/install.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/_lock.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/_yaml.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/global_mem.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/memory/site_mem.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/primitives/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/primitives/site.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/_md_convert.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/_md_normalize.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/_smart_goto.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/inline.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/markdown.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/playwright_handle.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/vendor/README.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/repl/vendor/readability.js +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/session.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/session_registry.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/session_runtime.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/skill_doc.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/tab_surface.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/task_runner.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright/version.py +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/src/browserwright.egg-info/requires.txt +0 -0
- {browserwright-0.17.4 → browserwright-0.18.0}/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.
|
|
3
|
+
Version: 0.18.0
|
|
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
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -15,7 +15,7 @@ name = "browserwright"
|
|
|
15
15
|
# stamping regex is `^(version\s*=\s*)["'][^"']+["']\s*$` and CI aborts unless
|
|
16
16
|
# it matches exactly once. `tests/skill/test_release_versioning.py` enforces all
|
|
17
17
|
# of that before a tag is ever pushed. See RELEASING.md.
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.18.0"
|
|
19
19
|
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)."
|
|
20
20
|
requires-python = ">=3.11"
|
|
21
21
|
license = "AGPL-3.0-only"
|
|
@@ -56,11 +56,9 @@ class ExecutorUnavailable(BrowserwrightError):
|
|
|
56
56
|
connected — actionable: the daemon must be running (it spawns the
|
|
57
57
|
executor)."""
|
|
58
58
|
|
|
59
|
-
default_fix = ("
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"then retry in a new command; or run `browserwright "
|
|
63
|
-
"session reset <id>` before retrying.")
|
|
59
|
+
default_fix = ("run `browserwright recover --session <id>`; it diagnoses "
|
|
60
|
+
"the daemon, tab, and executor in order and reports the "
|
|
61
|
+
"one layer that still needs human attention")
|
|
64
62
|
|
|
65
63
|
|
|
66
64
|
@dataclass(frozen=True)
|
|
@@ -105,8 +103,8 @@ def _ensure_executor_lease(sess) -> ExecutorLease:
|
|
|
105
103
|
)
|
|
106
104
|
if not isinstance(executor_id, str) or not executor_id:
|
|
107
105
|
raise ExecutorUnavailable(
|
|
108
|
-
"ensureExecutor returned no executor instance identity;
|
|
109
|
-
"
|
|
106
|
+
"ensureExecutor returned no executor instance identity; run "
|
|
107
|
+
"`browserwright recover --session <id>` before retrying"
|
|
110
108
|
)
|
|
111
109
|
return ExecutorLease(session_id=sid, executor_id=executor_id)
|
|
112
110
|
|
|
@@ -269,6 +269,11 @@ class _Worker:
|
|
|
269
269
|
self._active_globals: dict[str, Any] | None = None
|
|
270
270
|
# The currently-armed facade-death handler (Fork 4).
|
|
271
271
|
self._facade_death_handler: Any = None
|
|
272
|
+
# PID of the daemon whose facade produced the live Browser. The sync
|
|
273
|
+
# Playwright driver does not dispatch ``disconnected`` while this
|
|
274
|
+
# worker is idle, so this out-of-band identity check detects a daemon
|
|
275
|
+
# replacement before we touch the stale context on the next call.
|
|
276
|
+
self._facade_daemon_pid: int | None = None
|
|
272
277
|
# Persistent per-session state, injected by reference each call (Fork 5).
|
|
273
278
|
self._state: dict[str, Any] = {}
|
|
274
279
|
# One live-page holder shared by every injected view (snapshot /
|
|
@@ -425,7 +430,41 @@ class _Worker:
|
|
|
425
430
|
constraint). Bind the session FIRST so ``current_session()`` /
|
|
426
431
|
``resolve_current_target()`` resolve the right ledger record."""
|
|
427
432
|
if self._connected:
|
|
428
|
-
|
|
433
|
+
try:
|
|
434
|
+
from ..daemon import _ipc
|
|
435
|
+
|
|
436
|
+
current_daemon_pid = _ipc.ping_status_sync().pid
|
|
437
|
+
except Exception: # noqa: BLE001 - the connect path reports details
|
|
438
|
+
current_daemon_pid = None
|
|
439
|
+
if (self._facade_daemon_pid is not None
|
|
440
|
+
and current_daemon_pid != self._facade_daemon_pid):
|
|
441
|
+
self._on_facade_dead(self._browser)
|
|
442
|
+
# A resident executor is usually parked in ``queue.get`` between
|
|
443
|
+
# calls. Playwright's sync driver only dispatches the browser
|
|
444
|
+
# ``disconnected`` event while a sync API call pumps its fiber, so
|
|
445
|
+
# a daemon swap can leave this flag stale until the next command.
|
|
446
|
+
# Pump once before trusting it; the armed callback then clears the
|
|
447
|
+
# old objects and this same call reconnects below. Without this,
|
|
448
|
+
# the first post-swap command tries to bind on the dead context,
|
|
449
|
+
# times out, and the client reaps the adopted executor (losing
|
|
450
|
+
# persistent Python state).
|
|
451
|
+
try:
|
|
452
|
+
from ..repl.playwright_handle import drain_page_events
|
|
453
|
+
|
|
454
|
+
if self._context is not None:
|
|
455
|
+
drain_page_events(self._context)
|
|
456
|
+
except Exception: # noqa: BLE001 - liveness pump is best-effort
|
|
457
|
+
pass
|
|
458
|
+
if self._connected:
|
|
459
|
+
is_connected = getattr(self._browser, "is_connected", None)
|
|
460
|
+
try:
|
|
461
|
+
alive = bool(is_connected()) if callable(is_connected) else True
|
|
462
|
+
except Exception: # noqa: BLE001 - a failed probe is disconnected
|
|
463
|
+
alive = False
|
|
464
|
+
if not alive:
|
|
465
|
+
self._on_facade_dead()
|
|
466
|
+
if self._connected:
|
|
467
|
+
return
|
|
429
468
|
from ..session import Session, set_session
|
|
430
469
|
from ..session_ctx import resolve_session
|
|
431
470
|
|
|
@@ -494,6 +533,12 @@ class _Worker:
|
|
|
494
533
|
# the previous call's list and the notice would vanish.
|
|
495
534
|
warn=lambda m: self._call_warnings.append(m),
|
|
496
535
|
)
|
|
536
|
+
try:
|
|
537
|
+
from ..daemon import _ipc
|
|
538
|
+
|
|
539
|
+
self._facade_daemon_pid = _ipc.ping_status_sync().pid
|
|
540
|
+
except Exception: # noqa: BLE001 - browser connection itself is live
|
|
541
|
+
self._facade_daemon_pid = None
|
|
497
542
|
self._arm_facade_death()
|
|
498
543
|
# Issue #21: the live page must follow the session's current target
|
|
499
544
|
# (``session_runtime.bind_target`` / ``open_session_tab`` /
|
|
@@ -692,8 +737,14 @@ class _Worker:
|
|
|
692
737
|
|
|
693
738
|
The handler reference is retained for testability and lifecycle
|
|
694
739
|
introspection."""
|
|
740
|
+
# Capture the exact connection this callback belongs to. A
|
|
741
|
+
# disconnected event from the old daemon can be delivered late while
|
|
742
|
+
# the worker is already connecting a replacement Browser; that stale
|
|
743
|
+
# callback must not clear the newly assigned objects.
|
|
744
|
+
armed_browser = self._browser
|
|
745
|
+
|
|
695
746
|
def handler(*_):
|
|
696
|
-
return self._on_facade_dead()
|
|
747
|
+
return self._on_facade_dead(armed_browser)
|
|
697
748
|
|
|
698
749
|
try:
|
|
699
750
|
self._browser.on("disconnected", handler)
|
|
@@ -701,14 +752,46 @@ class _Worker:
|
|
|
701
752
|
except Exception: # noqa: BLE001 - never let arming break cold-start
|
|
702
753
|
self._facade_death_handler = None
|
|
703
754
|
|
|
704
|
-
def _on_facade_dead(self) -> None:
|
|
755
|
+
def _on_facade_dead(self, browser: Any | None = None) -> None:
|
|
756
|
+
"""ADR-0013 rule 1: survive the daemon, reconnect lazily.
|
|
757
|
+
|
|
758
|
+
This used to ``os._exit(0)`` ("Fork 4"), which made every daemon
|
|
759
|
+
restart destroy every session's ``state``. Now the executor only
|
|
760
|
+
forgets its Playwright objects; the next call finds ``_connected``
|
|
761
|
+
False and re-runs :meth:`_connect_and_bind` on the same live driver —
|
|
762
|
+
the exact path a failed cold-start already retries. The replacement
|
|
763
|
+
daemon adopts this process from its discovery record, so the agent's
|
|
764
|
+
variables survive the swap. If no daemon comes back, the reconnect
|
|
765
|
+
fails with an actionable error and the flag stays False for the next
|
|
766
|
+
attempt; the daemon's idle reaper eventually retires us.
|
|
767
|
+
|
|
768
|
+
Fires on Playwright's driver thread; only flags and references are
|
|
769
|
+
touched here, never the (thread-affine) Playwright objects."""
|
|
770
|
+
if browser is not None and self._browser is not browser:
|
|
771
|
+
return
|
|
705
772
|
sys.stderr.write(
|
|
706
773
|
f"executor {self._session_id}: facade transport dropped "
|
|
707
|
-
"(daemon restart?);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
774
|
+
"(daemon restart?); will reconnect on the next call\n")
|
|
775
|
+
self._connected = False
|
|
776
|
+
self._facade_death_handler = None
|
|
777
|
+
self._facade_daemon_pid = None
|
|
778
|
+
self._browser = None
|
|
779
|
+
self._context = None
|
|
780
|
+
self._page = None
|
|
781
|
+
self._live_page_holder.page = None
|
|
782
|
+
# The session's control-plane socket to the old daemon is dead too;
|
|
783
|
+
# drop it so the rebind dials the replacement instead of a corpse.
|
|
784
|
+
try:
|
|
785
|
+
from ..session import current_session
|
|
786
|
+
sess = current_session()
|
|
787
|
+
close = getattr(sess, "close", None)
|
|
788
|
+
if callable(close):
|
|
789
|
+
close()
|
|
790
|
+
invalidate = getattr(getattr(sess, "daemon", None), "invalidate", None)
|
|
791
|
+
if callable(invalidate):
|
|
792
|
+
invalidate()
|
|
793
|
+
except Exception: # noqa: BLE001 - best-effort; the rebind re-dials
|
|
794
|
+
pass
|
|
712
795
|
|
|
713
796
|
def _execute(self, req: ExecuteRequest) -> ExecuteResponse:
|
|
714
797
|
"""Run one code blob in the persistent namespace, capturing stdout +
|
|
@@ -871,11 +954,9 @@ class _Worker:
|
|
|
871
954
|
"the session's tab binding is gone (extension "
|
|
872
955
|
"reloaded/updated, daemon restarted, or the tab was closed) "
|
|
873
956
|
f"and re-binding a fresh tab failed ({failure}). The "
|
|
874
|
-
"executor recycled itself
|
|
875
|
-
"
|
|
876
|
-
|
|
877
|
-
f"`browserwright session attach-active -s {sid}` to adopt the "
|
|
878
|
-
"tab you are looking at, then retry."
|
|
957
|
+
"executor recycled itself. Run "
|
|
958
|
+
f"`browserwright recover --session {sid}`; it re-attaches or "
|
|
959
|
+
"opens the session tab and returns healthy before you retry."
|
|
879
960
|
)
|
|
880
961
|
return self._finish(
|
|
881
962
|
buf, error=error, exit_code=exit_code,
|
|
@@ -40,9 +40,8 @@ def _rpc_error_fix(method: str, err: object) -> str:
|
|
|
40
40
|
# breaks the deadlock; the generic -32601 hint does not apply.
|
|
41
41
|
return (
|
|
42
42
|
"an executor orphaned by a daemon crash may still hold this "
|
|
43
|
-
"target: run `browserwright session
|
|
44
|
-
"
|
|
45
|
-
"then retry"
|
|
43
|
+
"target: run `browserwright recover --session <session-id>`; it "
|
|
44
|
+
"adopts or replaces that executor without changing other sessions"
|
|
46
45
|
)
|
|
47
46
|
return ""
|
|
48
47
|
|
|
@@ -30,7 +30,8 @@ Usage:
|
|
|
30
30
|
browserwright -s <session-id> [--env NAME ...] -f script.py
|
|
31
31
|
browserwright -s <session-id> [--env NAME ...] --code-stdin < script.py
|
|
32
32
|
|
|
33
|
-
browserwright session new --backend=<extension|cdp
|
|
33
|
+
browserwright session new --backend=<extension|cdp> --name=SESSION_LABEL [--reuse] [--create | --attach=PORT]
|
|
34
|
+
browserwright recover --session=<id> (the one recovery verb: exit 0 healthy, 4 needs-human)
|
|
34
35
|
browserwright session reset <id>
|
|
35
36
|
browserwright session end --session=ID
|
|
36
37
|
browserwright session attach-active [--session=ID | -s ID] [--json]
|
|
@@ -726,6 +727,101 @@ def _cmd_memory(args: list[str]) -> int:
|
|
|
726
727
|
return 1
|
|
727
728
|
|
|
728
729
|
|
|
730
|
+
def _cmd_recover(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
731
|
+
"""`browserwright recover --session <id>` — ADR-0013 rule 2, the ONE
|
|
732
|
+
thing an agent does when a call failed.
|
|
733
|
+
|
|
734
|
+
Client side first: a dead daemon cannot answer an RPC, so the daemon
|
|
735
|
+
self-check (three criteria, two agreeing probes) runs here. "Gone" or
|
|
736
|
+
"stale version" is repaired through the default endpoint's on-demand
|
|
737
|
+
start (the sanctioned restart path); "something else answers on the
|
|
738
|
+
port" and "the probes disagree" are `needs-human` because recovery never
|
|
739
|
+
signals or replaces an unidentified process. Then the daemon runs
|
|
740
|
+
the per-session ladder and reports the state reached. Exit 0 = healthy,
|
|
741
|
+
4 = needs-human, 2 = usage."""
|
|
742
|
+
kw = _parse_kv_args(args)
|
|
743
|
+
sid = kw.get("session") or session_id
|
|
744
|
+
if not sid:
|
|
745
|
+
print("usage: browserwright recover --session=<id>", file=sys.stderr)
|
|
746
|
+
return 2
|
|
747
|
+
sid = str(sid)
|
|
748
|
+
from . import session_create
|
|
749
|
+
from .daemon.launchagent import daemon_self_check
|
|
750
|
+
from .daemon_url import daemon_endpoint
|
|
751
|
+
|
|
752
|
+
steps: list[str] = []
|
|
753
|
+
ep = daemon_endpoint()
|
|
754
|
+
verdict = daemon_self_check(None)
|
|
755
|
+
if not verdict["healthy"]:
|
|
756
|
+
crit = verdict["criterion"]
|
|
757
|
+
if crit in ("gone", "version") and not ep.explicit:
|
|
758
|
+
steps.append(f"daemon: {verdict['detail']}; starting the installed one")
|
|
759
|
+
from .daemon import _ipc
|
|
760
|
+
_ipc.log_lifecycle(
|
|
761
|
+
"automatic-recovery", criterion=crit,
|
|
762
|
+
probes=",".join(verdict.get("probes") or []),
|
|
763
|
+
reason=verdict["detail"], session=sid)
|
|
764
|
+
try:
|
|
765
|
+
session_create._ensure_daemon_running()
|
|
766
|
+
except Exception as e: # noqa: BLE001
|
|
767
|
+
return _recover_report(sid, "needs-human", steps,
|
|
768
|
+
f"daemon could not be started: {e}")
|
|
769
|
+
from .mode_b_client import ModeBClient
|
|
770
|
+
if not ModeBClient().wait_until_alive(timeout=20.0):
|
|
771
|
+
return _recover_report(sid, "needs-human", steps,
|
|
772
|
+
"no daemon answered within 20s after the "
|
|
773
|
+
"start; `browserwright-daemon logs` has "
|
|
774
|
+
"its last words")
|
|
775
|
+
steps.append("daemon: up")
|
|
776
|
+
else:
|
|
777
|
+
return _recover_report(sid, "needs-human", steps, verdict["detail"])
|
|
778
|
+
else:
|
|
779
|
+
steps.append(f"daemon: {verdict['detail']}")
|
|
780
|
+
|
|
781
|
+
import json as _json
|
|
782
|
+
import subprocess
|
|
783
|
+
try:
|
|
784
|
+
proc = subprocess.run(
|
|
785
|
+
["browserwright-daemon", "recover", "--session", sid],
|
|
786
|
+
capture_output=True, text=True, timeout=120.0,
|
|
787
|
+
env=session_create._daemon_child_env())
|
|
788
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
789
|
+
return _recover_report(sid, "needs-human", steps,
|
|
790
|
+
f"could not ask the daemon to recover: {e}")
|
|
791
|
+
try:
|
|
792
|
+
result = _json.loads(proc.stdout.strip().splitlines()[-1])
|
|
793
|
+
if not isinstance(result, dict):
|
|
794
|
+
raise ValueError("recover result is not an object")
|
|
795
|
+
except (ValueError, IndexError):
|
|
796
|
+
detail = ((proc.stderr or proc.stdout).strip()[:400]
|
|
797
|
+
if proc.returncode != 0 else
|
|
798
|
+
f"unreadable recover result: {proc.stdout[:200]!r}")
|
|
799
|
+
return _recover_report(
|
|
800
|
+
sid, "needs-human", steps,
|
|
801
|
+
detail or f"daemon recover exited {proc.returncode}")
|
|
802
|
+
if proc.returncode not in (0, 4):
|
|
803
|
+
return _recover_report(sid, "needs-human", steps,
|
|
804
|
+
(proc.stderr or f"daemon recover exited "
|
|
805
|
+
f"{proc.returncode}").strip()[:400])
|
|
806
|
+
for st in result.get("steps") or []:
|
|
807
|
+
steps.append(f"{st.get('rung')}: {st.get('detail')}")
|
|
808
|
+
return _recover_report(sid, result.get("state") or "needs-human", steps,
|
|
809
|
+
result.get("reason") or "")
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _recover_report(sid: str, state: str, steps: list[str], reason: str) -> int:
|
|
813
|
+
for line in steps:
|
|
814
|
+
print(f" - {line}", file=sys.stderr)
|
|
815
|
+
if state == "healthy":
|
|
816
|
+
print(f"session {sid}: healthy — retry your call")
|
|
817
|
+
return 0
|
|
818
|
+
print(f"session {sid}: {state}" + (f" — {reason}" if reason else ""))
|
|
819
|
+
if state == "needs-human":
|
|
820
|
+
print(" a retry will not change this; report it with the lines above "
|
|
821
|
+
"(`browserwright doctor` has the full picture)", file=sys.stderr)
|
|
822
|
+
return 4
|
|
823
|
+
|
|
824
|
+
|
|
729
825
|
def _cmd_session(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
730
826
|
"""``browserwright session {new|attach-active|reset|end|list|prune} ...`` (P2)."""
|
|
731
827
|
from . import session_create
|
|
@@ -756,7 +852,7 @@ def _cmd_session(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
|
756
852
|
file=sys.stderr)
|
|
757
853
|
return 1
|
|
758
854
|
print("usage: browserwright session new --backend=<extension|cdp> "
|
|
759
|
-
"--name=SESSION_LABEL [--create | --attach=<port|url>]",
|
|
855
|
+
"--name=SESSION_LABEL [--reuse] [--create | --attach=<port|url>]",
|
|
760
856
|
file=sys.stderr)
|
|
761
857
|
print("--name is a short task-specific session label. Extension sessions "
|
|
762
858
|
"use it as the Chrome tab group title; cdp sessions use it only to "
|
|
@@ -769,11 +865,16 @@ def _cmd_session(args: list[str], *, session_id: Optional[str] = None) -> int:
|
|
|
769
865
|
sid = session_create.new(
|
|
770
866
|
backend=backend, create=bool(kw.get("create")),
|
|
771
867
|
attach=kw.get("attach"), name=kw.get("name"),
|
|
868
|
+
reuse=bool(kw.get("reuse")),
|
|
772
869
|
)
|
|
773
870
|
except ValueError as e:
|
|
774
871
|
print(str(e), file=sys.stderr)
|
|
775
872
|
return 1
|
|
776
|
-
|
|
873
|
+
if session_create.last_new_reused == sid:
|
|
874
|
+
print(f"OK: reusing session {sid} (--reuse matched an existing "
|
|
875
|
+
f"{backend} session named {kw.get('name')!r})", file=sys.stderr)
|
|
876
|
+
else:
|
|
877
|
+
print(f"OK: session {sid} created", file=sys.stderr)
|
|
777
878
|
print(sid) # token-frugal: bare id
|
|
778
879
|
return 0
|
|
779
880
|
|
|
@@ -1110,6 +1211,8 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|
|
1110
1211
|
sys.exit(_cmd_session(rest, session_id=global_session))
|
|
1111
1212
|
if cmd == "whoami":
|
|
1112
1213
|
sys.exit(_cmd_whoami(rest, session_id=global_session))
|
|
1214
|
+
if cmd == "recover":
|
|
1215
|
+
sys.exit(_cmd_recover(rest, session_id=global_session))
|
|
1113
1216
|
if cmd == "userscript":
|
|
1114
1217
|
sys.exit(_cmd_userscript(rest, session_id=global_session))
|
|
1115
1218
|
|