browserwright 0.7.0__tar.gz → 0.7.2__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.7.0 → browserwright-0.7.2}/PKG-INFO +1 -1
- {browserwright-0.7.0 → browserwright-0.7.2}/pyproject.toml +1 -1
- browserwright-0.7.2/src/browserwright/daemon/_stale.py +174 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/cli.py +30 -2
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/listener.py +92 -1
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/skill_runtime.md +8 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright.egg-info/PKG-INFO +1 -1
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright.egg-info/SOURCES.txt +1 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/README.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/setup.cfg +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/__main__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/_executor/client.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/_executor/process.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/_executor/protocol.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/api.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/cdp.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/cli.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/_ipc.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/active_tab.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/auth.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/base.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/cloud.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/env.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/extension.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/backends/rdp.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/config.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/doctor.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/launch_chrome.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/observability.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/platforms.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/resolver.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/daemon.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/executor_registry.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/extension_upstream.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/facade.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/facade_extension.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/proxy.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/relay.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/state.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/upstream.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/discovery.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/errors.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/health.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/install.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/_yaml.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/global_mem.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/repl_mem.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/session_decisions.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/memory/site_mem.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/mode_b_client.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/multitask.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/inspect.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/interact.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/page.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/primitives/site.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/release_install.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/_smart_goto.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/inline.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/playwright_handle.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/session.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/session_create.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/session_registry.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/session_runtime.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/skill_doc.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/subscriptions.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/task_runner.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/version.py +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright.egg-info/requires.txt +0 -0
- {browserwright-0.7.0 → browserwright-0.7.2}/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.7.
|
|
3
|
+
Version: 0.7.2
|
|
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.7.
|
|
7
|
+
version = "0.7.2"
|
|
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 = [
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Detect + reclaim a stale / half-alive browserwright daemon (issue #15 P2).
|
|
2
|
+
|
|
3
|
+
A daemon killed mid-life — or one whose serve loop failed *after* binding its
|
|
4
|
+
relay/facade TCP ports — can linger holding those ports while its control
|
|
5
|
+
socket is dead. Then:
|
|
6
|
+
|
|
7
|
+
- `browserwright-daemon status` lies (`not_running`) though a process holds
|
|
8
|
+
the ports (issue #15, 2.1), and
|
|
9
|
+
- a fresh `serve` crash-loops on `EADDRINUSE` because nothing reclaims the
|
|
10
|
+
ports from the zombie (issue #15, 2.2).
|
|
11
|
+
|
|
12
|
+
These helpers give `status` a truthful third state and let `serve` take over
|
|
13
|
+
the ports from a *confirmed* browserwright daemon. The safety rule for any
|
|
14
|
+
kill: only ever signal a pid that lsof confirms is holding the port AND whose
|
|
15
|
+
command line confirms it is a browserwright daemon. When lsof is unavailable we
|
|
16
|
+
never auto-kill — we surface the pid-file pid for the user to handle.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import signal
|
|
22
|
+
import socket
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def daemon_tcp_ports(cfg) -> list[int]:
|
|
29
|
+
"""The relay + facade TCP ports a daemon binds, derived from ``cfg``.
|
|
30
|
+
|
|
31
|
+
Best-effort + deduped — used by both `status` (to detect a port-holding
|
|
32
|
+
zombie) and `serve` (to reclaim those ports before binding). Kept here so
|
|
33
|
+
the two paths can never disagree on which ports matter."""
|
|
34
|
+
ports: list[int] = []
|
|
35
|
+
try:
|
|
36
|
+
_, relay_port = cfg.backends.extension.resolved_host_port()
|
|
37
|
+
if relay_port:
|
|
38
|
+
ports.append(int(relay_port))
|
|
39
|
+
except Exception: # noqa: BLE001 - best-effort
|
|
40
|
+
pass
|
|
41
|
+
try:
|
|
42
|
+
fp = cfg.resolved_facade_port()
|
|
43
|
+
if fp:
|
|
44
|
+
ports.append(int(fp))
|
|
45
|
+
except Exception: # noqa: BLE001
|
|
46
|
+
pass
|
|
47
|
+
return list(dict.fromkeys(ports))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def port_is_listening(host: str, port: int) -> bool:
|
|
51
|
+
"""True if something already holds ``host:port`` (a fresh bind fails).
|
|
52
|
+
|
|
53
|
+
Uses a plain bind attempt (no ``SO_REUSEADDR``) so a live listener reliably
|
|
54
|
+
trips ``EADDRINUSE``. Cheap, cross-platform, no external process."""
|
|
55
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
56
|
+
try:
|
|
57
|
+
s.bind((host, port))
|
|
58
|
+
return False # we could bind → nobody holds it
|
|
59
|
+
except OSError:
|
|
60
|
+
return True
|
|
61
|
+
finally:
|
|
62
|
+
s.close()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def port_holder_pids(port: int) -> list[int]:
|
|
66
|
+
"""PIDs listening on TCP ``port`` (best-effort via ``lsof``).
|
|
67
|
+
|
|
68
|
+
Returns ``[]`` when lsof is missing / errors / finds nothing — callers must
|
|
69
|
+
treat an empty list as "unknown", never as "nobody"."""
|
|
70
|
+
if sys.platform == "win32":
|
|
71
|
+
return []
|
|
72
|
+
try:
|
|
73
|
+
out = subprocess.run(
|
|
74
|
+
["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"],
|
|
75
|
+
capture_output=True, text=True, timeout=3,
|
|
76
|
+
)
|
|
77
|
+
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
|
78
|
+
return []
|
|
79
|
+
pids: list[int] = []
|
|
80
|
+
for tok in out.stdout.split():
|
|
81
|
+
try:
|
|
82
|
+
pid = int(tok)
|
|
83
|
+
except ValueError:
|
|
84
|
+
continue
|
|
85
|
+
if pid > 0 and pid not in pids:
|
|
86
|
+
pids.append(pid)
|
|
87
|
+
return pids
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pid_is_browserwright_daemon(pid: int) -> bool:
|
|
91
|
+
"""True iff ``pid``'s command line looks like a browserwright daemon.
|
|
92
|
+
|
|
93
|
+
The safety gate before any SIGTERM — we must never signal a stranger that
|
|
94
|
+
happens to hold the port. Matches both the installed console-script
|
|
95
|
+
(``browserwright-daemon serve``) and the dev module invocation
|
|
96
|
+
(``python -m browserwright.daemon.cli serve``)."""
|
|
97
|
+
if pid <= 0 or sys.platform == "win32":
|
|
98
|
+
return False
|
|
99
|
+
try:
|
|
100
|
+
out = subprocess.run(
|
|
101
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
102
|
+
capture_output=True, text=True, timeout=3,
|
|
103
|
+
)
|
|
104
|
+
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
|
105
|
+
return False
|
|
106
|
+
cmd = out.stdout.strip()
|
|
107
|
+
if not cmd:
|
|
108
|
+
return False
|
|
109
|
+
return "browserwright-daemon" in cmd or "browserwright.daemon" in cmd
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def pid_alive(pid: int) -> bool:
|
|
113
|
+
"""True if ``pid`` exists (signal 0 probe). POSIX only."""
|
|
114
|
+
if pid <= 0 or sys.platform == "win32":
|
|
115
|
+
return False
|
|
116
|
+
try:
|
|
117
|
+
os.kill(pid, 0)
|
|
118
|
+
except ProcessLookupError:
|
|
119
|
+
return False
|
|
120
|
+
except PermissionError:
|
|
121
|
+
return True # exists, owned by someone else
|
|
122
|
+
except OSError:
|
|
123
|
+
return False
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def confirmed_stale_holder(ports: list[int]) -> int | None:
|
|
128
|
+
"""Return the pid of a *confirmed browserwright* daemon holding any of
|
|
129
|
+
``ports`` (excluding ourselves), or None.
|
|
130
|
+
|
|
131
|
+
Confirmation = lsof says it holds the port AND its command line is a
|
|
132
|
+
browserwright daemon. Callers must have already established that the control
|
|
133
|
+
socket is dead (ping negative) before treating this as "stale". lsof
|
|
134
|
+
unavailable → None (we won't guess a kill target)."""
|
|
135
|
+
me = os.getpid()
|
|
136
|
+
for port in ports:
|
|
137
|
+
for pid in port_holder_pids(port):
|
|
138
|
+
if pid != me and pid_is_browserwright_daemon(pid):
|
|
139
|
+
return pid
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def reclaim_ports(pid: int, ports: list[int], *, timeout: float = 5.0) -> bool:
|
|
144
|
+
"""SIGTERM ``pid`` and wait until every port in ``ports`` frees; escalate to
|
|
145
|
+
SIGKILL as a last resort. Returns True once the ports are free.
|
|
146
|
+
|
|
147
|
+
The caller MUST have confirmed ``pid`` via :func:`confirmed_stale_holder`
|
|
148
|
+
(or an equivalent lsof+cmdline check) — this function does not re-verify."""
|
|
149
|
+
def _all_free() -> bool:
|
|
150
|
+
return all(not port_is_listening("127.0.0.1", p) for p in ports)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
os.kill(pid, signal.SIGTERM)
|
|
154
|
+
except ProcessLookupError:
|
|
155
|
+
return _all_free()
|
|
156
|
+
except OSError:
|
|
157
|
+
return False
|
|
158
|
+
if _wait(_all_free, timeout):
|
|
159
|
+
return True
|
|
160
|
+
# Escalate — a wedged daemon may never handle SIGTERM.
|
|
161
|
+
try:
|
|
162
|
+
os.kill(pid, signal.SIGKILL)
|
|
163
|
+
except OSError:
|
|
164
|
+
pass
|
|
165
|
+
return _wait(_all_free, 2.0)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _wait(pred, timeout: float) -> bool:
|
|
169
|
+
deadline = time.monotonic() + timeout
|
|
170
|
+
while time.monotonic() < deadline:
|
|
171
|
+
if pred():
|
|
172
|
+
return True
|
|
173
|
+
time.sleep(0.15)
|
|
174
|
+
return pred()
|
|
@@ -615,9 +615,12 @@ async def _run_stats(args, cfg: Config) -> int:
|
|
|
615
615
|
|
|
616
616
|
def _cmd_status(args, cfg: Config) -> int:
|
|
617
617
|
"""Report endpoint + liveness. JSON shape used by Skill for status pings."""
|
|
618
|
-
from . import _ipc
|
|
618
|
+
from . import _ipc, _stale
|
|
619
619
|
pid, version = _ipc.ping_status_sync(timeout=1.0)
|
|
620
620
|
probe_state = "ok" if pid is not None else "not_running"
|
|
621
|
+
# issue #15 (2.1): pid of the process holding the relay/facade ports when
|
|
622
|
+
# the control socket is dead but its file lingers — a half-alive daemon.
|
|
623
|
+
port_holder_pid = None
|
|
621
624
|
if pid is None and _ipc.sock_path().exists():
|
|
622
625
|
deadline = time.monotonic() + 0.6
|
|
623
626
|
while time.monotonic() < deadline:
|
|
@@ -628,6 +631,20 @@ def _cmd_status(args, cfg: Config) -> int:
|
|
|
628
631
|
break
|
|
629
632
|
else:
|
|
630
633
|
probe_state = "transient_probe_failed"
|
|
634
|
+
# Still unresponsive, but its socket file is present — a half-alive
|
|
635
|
+
# daemon may be holding the relay/facade ports. Report the truth
|
|
636
|
+
# instead of a bare "not_running" that loops the user through
|
|
637
|
+
# restarts that crash on EADDRINUSE.
|
|
638
|
+
ports = _stale.daemon_tcp_ports(cfg)
|
|
639
|
+
if any(_stale.port_is_listening("127.0.0.1", p) for p in ports):
|
|
640
|
+
probe_state = "port_held_by_unresponsive_process"
|
|
641
|
+
port_holder_pid = _stale.confirmed_stale_holder(ports)
|
|
642
|
+
if port_holder_pid is None:
|
|
643
|
+
# lsof unavailable / non-daemon holder — surface the
|
|
644
|
+
# pid-file pid as a best-effort hint (never a kill target).
|
|
645
|
+
fp = _ipc.read_pid()
|
|
646
|
+
if fp and _stale.pid_alive(fp):
|
|
647
|
+
port_holder_pid = fp
|
|
631
648
|
ep = _ipc.endpoint_describe()
|
|
632
649
|
facade_ws, facade_port = _ipc.read_facade_file()
|
|
633
650
|
status = {
|
|
@@ -635,6 +652,10 @@ def _cmd_status(args, cfg: Config) -> int:
|
|
|
635
652
|
"alive": pid is not None,
|
|
636
653
|
"probe_state": probe_state,
|
|
637
654
|
"pid": pid,
|
|
655
|
+
# issue #15 (2.1): pid of the process holding the relay/facade ports
|
|
656
|
+
# when the daemon is unresponsive. Actionable: `browserwright-daemon
|
|
657
|
+
# restart` reclaims it, or kill it manually.
|
|
658
|
+
"port_holder_pid": port_holder_pid,
|
|
638
659
|
"version": version,
|
|
639
660
|
"endpoint": ep,
|
|
640
661
|
# Playwright facade discovery (Phase C). None when the facade is
|
|
@@ -649,7 +670,14 @@ def _cmd_status(args, cfg: Config) -> int:
|
|
|
649
670
|
print(json.dumps(status, sort_keys=True))
|
|
650
671
|
else:
|
|
651
672
|
if pid is None:
|
|
652
|
-
|
|
673
|
+
if probe_state == "port_held_by_unresponsive_process":
|
|
674
|
+
held = f" (pid {port_holder_pid})" if port_holder_pid else ""
|
|
675
|
+
print("daemon unresponsive but holding its ports"
|
|
676
|
+
f"{held} — a half-alive daemon.")
|
|
677
|
+
print(" fix: `browserwright-daemon restart` reclaims the "
|
|
678
|
+
"ports; or kill the pid above manually.")
|
|
679
|
+
else:
|
|
680
|
+
print("daemon not running")
|
|
653
681
|
else:
|
|
654
682
|
print(f"daemon alive (pid {pid})")
|
|
655
683
|
if ep["transport"] == "unix":
|
|
@@ -75,6 +75,61 @@ def make_context(*, backend: str, cfg: Config,
|
|
|
75
75
|
# ---- top-level entry -------------------------------------------------------
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _reclaim_stale_daemon_ports(cfg: Config) -> None:
|
|
79
|
+
"""Reclaim the relay/facade ports from a *confirmed* stale browserwright
|
|
80
|
+
daemon (issue #15, 2.2). No-op when the ports are free or held by an
|
|
81
|
+
unconfirmed process (we never SIGTERM a stranger)."""
|
|
82
|
+
from .. import _stale
|
|
83
|
+
ports = _stale.daemon_tcp_ports(cfg)
|
|
84
|
+
held = [p for p in ports if _stale.port_is_listening("127.0.0.1", p)]
|
|
85
|
+
if not held:
|
|
86
|
+
return
|
|
87
|
+
pid = _stale.confirmed_stale_holder(held)
|
|
88
|
+
if pid is None:
|
|
89
|
+
logger.warning(
|
|
90
|
+
"ports %s are in use but no confirmed browserwright daemon holds "
|
|
91
|
+
"them; leaving them alone (bind will surface a clear error)", held)
|
|
92
|
+
return
|
|
93
|
+
logger.warning(
|
|
94
|
+
"reclaiming ports %s from stale browserwright daemon pid %d (issue #15)",
|
|
95
|
+
held, pid)
|
|
96
|
+
if _stale.reclaim_ports(pid, held):
|
|
97
|
+
logger.info("reclaimed ports %s from pid %d", held, pid)
|
|
98
|
+
else:
|
|
99
|
+
logger.warning("could not free ports %s from pid %d; bind may fail",
|
|
100
|
+
held, pid)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _control_socket_watchdog(sock_ident: tuple, stop: asyncio.Event,
|
|
104
|
+
*, interval: float = 2.0) -> None:
|
|
105
|
+
"""Self-exit when our control socket disappears or is replaced by a
|
|
106
|
+
different inode — i.e. another daemon took over, or the socket was removed
|
|
107
|
+
(issue #15, 2.4). Setting ``stop`` runs the normal graceful shutdown, which
|
|
108
|
+
releases the relay/facade ports instead of lingering as a port-holding
|
|
109
|
+
zombie. A single stat error is ignored (transient FS hiccup); only a
|
|
110
|
+
definitive gone/replaced verdict self-exits."""
|
|
111
|
+
while not stop.is_set():
|
|
112
|
+
try:
|
|
113
|
+
await asyncio.wait_for(stop.wait(), timeout=interval)
|
|
114
|
+
return # stop was set elsewhere (signal / another watchdog)
|
|
115
|
+
except asyncio.TimeoutError:
|
|
116
|
+
pass
|
|
117
|
+
try:
|
|
118
|
+
st = os.stat(_ipc.sock_path())
|
|
119
|
+
except FileNotFoundError:
|
|
120
|
+
logger.warning("control socket removed; self-exiting to release "
|
|
121
|
+
"ports (issue #15)")
|
|
122
|
+
stop.set()
|
|
123
|
+
return
|
|
124
|
+
except OSError:
|
|
125
|
+
continue # transient — don't self-exit on one error
|
|
126
|
+
if (st.st_dev, st.st_ino) != sock_ident:
|
|
127
|
+
logger.warning("control socket replaced (another daemon took over); "
|
|
128
|
+
"self-exiting to release ports (issue #15)")
|
|
129
|
+
stop.set()
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
|
|
78
133
|
async def run_serve(cfg: Config) -> int:
|
|
79
134
|
"""Run a Mode B daemon until SIGTERM / Ctrl-C / shutdown. Returns exit code.
|
|
80
135
|
|
|
@@ -100,6 +155,12 @@ async def run_serve(cfg: Config) -> int:
|
|
|
100
155
|
)
|
|
101
156
|
return 1
|
|
102
157
|
_ipc.cleanup_endpoint()
|
|
158
|
+
# issue #15 (2.2): the control-socket ping above was negative, but a
|
|
159
|
+
# half-alive prior daemon can still hold the relay/facade TCP ports and
|
|
160
|
+
# crash-loop us on EADDRINUSE. Reclaim them before binding — but only from a
|
|
161
|
+
# process lsof confirms holds the port AND whose cmdline is a browserwright
|
|
162
|
+
# daemon (never a stranger).
|
|
163
|
+
_reclaim_stale_daemon_ports(cfg)
|
|
103
164
|
|
|
104
165
|
# Phase 3 (C2 ephemeral): rdp Chrome processes are daemon children and die
|
|
105
166
|
# with us — but a hard crash / SIGKILL can leave orphan Chrome processes
|
|
@@ -159,6 +220,19 @@ async def run_serve(cfg: Config) -> int:
|
|
|
159
220
|
handler = _ClientHandler(daemon, cfg)
|
|
160
221
|
server = await _open_server(handler)
|
|
161
222
|
|
|
223
|
+
# issue #15 (2.4): watch our own control socket. If another `serve` (or a
|
|
224
|
+
# manual `rm`) removes/replaces it, self-exit so we release the relay/facade
|
|
225
|
+
# ports instead of lingering as a port-holding zombie. POSIX only — Windows
|
|
226
|
+
# uses a port file, not a unix socket inode.
|
|
227
|
+
sock_watch_task: asyncio.Task | None = None
|
|
228
|
+
if not _ipc.IS_WINDOWS:
|
|
229
|
+
try:
|
|
230
|
+
_st = os.stat(_ipc.sock_path())
|
|
231
|
+
sock_watch_task = asyncio.create_task(
|
|
232
|
+
_control_socket_watchdog((_st.st_dev, _st.st_ino), stop))
|
|
233
|
+
except OSError:
|
|
234
|
+
pass
|
|
235
|
+
|
|
162
236
|
# v0.4: for the extension shared context, start the relay ws server eagerly
|
|
163
237
|
# so `browserwright-daemon doctor` can probe `__status__` even before any
|
|
164
238
|
# Skill client connects. The relay belongs to the shared context's holder
|
|
@@ -173,8 +247,21 @@ async def run_serve(cfg: Config) -> int:
|
|
|
173
247
|
port = await shared_context.holder.relay.start()
|
|
174
248
|
logger.info("extension relay started on port %d", port)
|
|
175
249
|
except OSError as e:
|
|
250
|
+
# issue #15 (2.2): if we still can't bind after the reclaim pass, the
|
|
251
|
+
# port is held by something we couldn't confirm as our daemon (lsof
|
|
252
|
+
# missing, or a genuine stranger). Point the user at the exact port +
|
|
253
|
+
# how to find the holder rather than a bare errno.
|
|
254
|
+
hint = ""
|
|
255
|
+
with contextlib.suppress(Exception):
|
|
256
|
+
_, rport = cfg.backends.extension.resolved_host_port()
|
|
257
|
+
hint = (
|
|
258
|
+
f" — port {rport} is held by another process. Run "
|
|
259
|
+
f"`lsof -nP -iTCP:{rport} -sTCP:LISTEN` to find it, then "
|
|
260
|
+
f"`browserwright-daemon restart` (reclaims a stale "
|
|
261
|
+
f"browserwright daemon) or kill that pid."
|
|
262
|
+
)
|
|
176
263
|
print(
|
|
177
|
-
f"browserwright-daemon failed to bind extension relay: {e}",
|
|
264
|
+
f"browserwright-daemon failed to bind extension relay: {e}{hint}",
|
|
178
265
|
file=sys.stderr,
|
|
179
266
|
)
|
|
180
267
|
server.close()
|
|
@@ -235,6 +322,10 @@ async def run_serve(cfg: Config) -> int:
|
|
|
235
322
|
logger.info("browserwright-daemon shutdown requested")
|
|
236
323
|
await _graceful_shutdown(daemon)
|
|
237
324
|
finally:
|
|
325
|
+
if sock_watch_task is not None:
|
|
326
|
+
sock_watch_task.cancel()
|
|
327
|
+
with contextlib.suppress(Exception):
|
|
328
|
+
await sock_watch_task
|
|
238
329
|
if idle_task is not None:
|
|
239
330
|
idle_task.cancel()
|
|
240
331
|
with contextlib.suppress(Exception):
|
|
@@ -205,6 +205,14 @@ browserwright userscript toggle <id> --enabled=false
|
|
|
205
205
|
browserwright userscript remove <id>
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
## Reporting browserwright bugs
|
|
209
|
+
|
|
210
|
+
If browserwright itself misbehaves — a primitive, CLI verb, the daemon, the extension backend, or the Playwright facade crashes, hangs, times out, or breaks a contract this guide documents — file a bug against the upstream repo (`broven/browserwright`) on the user's behalf, so the user does not have to. This is *only* for browserwright defects; a site that changed, blocks automation, or shows a captcha is a site note (`remember(host, ...)`), and hostile page content is a trust-boundary matter, not a bug.
|
|
211
|
+
|
|
212
|
+
A report is only useful if a maintainer can reproduce it. Before filing: reproduce it twice, shrink it to the smallest runnable script against a page the maintainer can reach (prefer `https://example.com` or a `data:text/html,...` fixture, never an auth-gated/private page), capture `browserwright version check` + `browserwright-daemon status --json`, record expected vs. actual, and redact secrets and private content. A GitHub issue is public and filed under the user's identity: draft it, show the user, and submit with `gh issue create --repo broven/browserwright` only after they approve.
|
|
213
|
+
|
|
214
|
+
Read the installed skill's `reporting-issues.md` for the exact scope gate, reproducibility checklist, issue template, and the `gh` filing + fallback commands before you file.
|
|
215
|
+
|
|
208
216
|
## Memory
|
|
209
217
|
|
|
210
218
|
Read the installed skill's `memory.md` for backend preferences and scenario decisions. When the user expresses a stable browser preference, record it there or with the memory helpers so future tasks do not re-ask.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browserwright
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -36,6 +36,7 @@ src/browserwright/_executor/process.py
|
|
|
36
36
|
src/browserwright/_executor/protocol.py
|
|
37
37
|
src/browserwright/daemon/__init__.py
|
|
38
38
|
src/browserwright/daemon/_ipc.py
|
|
39
|
+
src/browserwright/daemon/_stale.py
|
|
39
40
|
src/browserwright/daemon/active_tab.py
|
|
40
41
|
src/browserwright/daemon/auth.py
|
|
41
42
|
src/browserwright/daemon/cli.py
|
|
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.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/executor_registry.py
RENAMED
|
File without changes
|
{browserwright-0.7.0 → browserwright-0.7.2}/src/browserwright/daemon/server/extension_upstream.py
RENAMED
|
File without changes
|
|
File without changes
|
{browserwright-0.7.0 → browserwright-0.7.2}/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
|