browserwright 0.6.2__py3-none-any.whl
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/__init__.py +33 -0
- browserwright/__main__.py +6 -0
- browserwright/_executor/__init__.py +47 -0
- browserwright/_executor/__main__.py +9 -0
- browserwright/_executor/client.py +127 -0
- browserwright/_executor/process.py +652 -0
- browserwright/_executor/protocol.py +152 -0
- browserwright/api.py +66 -0
- browserwright/cdp.py +285 -0
- browserwright/cli.py +741 -0
- browserwright/daemon/__init__.py +8 -0
- browserwright/daemon/_ipc.py +444 -0
- browserwright/daemon/active_tab.py +183 -0
- browserwright/daemon/auth.py +395 -0
- browserwright/daemon/backends/__init__.py +59 -0
- browserwright/daemon/backends/base.py +120 -0
- browserwright/daemon/backends/cloud.py +222 -0
- browserwright/daemon/backends/env.py +119 -0
- browserwright/daemon/backends/extension.py +185 -0
- browserwright/daemon/backends/rdp.py +214 -0
- browserwright/daemon/cli.py +1437 -0
- browserwright/daemon/config.py +380 -0
- browserwright/daemon/doctor.py +179 -0
- browserwright/daemon/errors.py +34 -0
- browserwright/daemon/launch_chrome.py +353 -0
- browserwright/daemon/observability.py +181 -0
- browserwright/daemon/platforms.py +234 -0
- browserwright/daemon/resolver.py +72 -0
- browserwright/daemon/server/__init__.py +6 -0
- browserwright/daemon/server/daemon.py +229 -0
- browserwright/daemon/server/executor_registry.py +434 -0
- browserwright/daemon/server/extension_upstream.py +677 -0
- browserwright/daemon/server/facade.py +375 -0
- browserwright/daemon/server/facade_extension.py +969 -0
- browserwright/daemon/server/listener.py +1058 -0
- browserwright/daemon/server/proxy.py +1991 -0
- browserwright/daemon/server/relay.py +783 -0
- browserwright/daemon/server/state.py +432 -0
- browserwright/daemon/server/upstream.py +266 -0
- browserwright/daemon/userscripts.py +150 -0
- browserwright/discovery.py +213 -0
- browserwright/errors.py +177 -0
- browserwright/health.py +169 -0
- browserwright/install.py +628 -0
- browserwright/memory/__init__.py +15 -0
- browserwright/memory/_md.py +120 -0
- browserwright/memory/_yaml.py +217 -0
- browserwright/memory/global_mem.py +201 -0
- browserwright/memory/repl_mem.py +28 -0
- browserwright/memory/session_decisions.py +53 -0
- browserwright/memory/site_mem.py +381 -0
- browserwright/mode_b_client.py +590 -0
- browserwright/multitask.py +131 -0
- browserwright/output_schema.py +99 -0
- browserwright/primitives/__init__.py +67 -0
- browserwright/primitives/discovery_api.py +79 -0
- browserwright/primitives/http.py +42 -0
- browserwright/primitives/inspect.py +876 -0
- browserwright/primitives/interact.py +518 -0
- browserwright/primitives/page.py +556 -0
- browserwright/primitives/site.py +143 -0
- browserwright/release_install.py +466 -0
- browserwright/repl/__init__.py +6 -0
- browserwright/repl/_namespace.py +106 -0
- browserwright/repl/_smart_goto.py +236 -0
- browserwright/repl/inline.py +180 -0
- browserwright/repl/playwright_handle.py +449 -0
- browserwright/repl/snapshot.py +150 -0
- browserwright/session.py +229 -0
- browserwright/session_create.py +252 -0
- browserwright/session_ctx.py +24 -0
- browserwright/session_registry.py +133 -0
- browserwright/session_runtime.py +133 -0
- browserwright/site_skills_starter/github.com/SKILL.md +14 -0
- browserwright/site_skills_starter/github.com/memory.md +29 -0
- browserwright/site_skills_starter/github.com/tasks/list_issues.py +55 -0
- browserwright/site_skills_starter/google.com/SKILL.md +16 -0
- browserwright/site_skills_starter/google.com/memory.md +27 -0
- browserwright/site_skills_starter/google.com/tasks/search.py +53 -0
- browserwright/site_skills_starter/producthunt.com/SKILL.md +7 -0
- browserwright/site_skills_starter/producthunt.com/memory.md +26 -0
- browserwright/site_skills_starter/producthunt.com/tasks/today.py +64 -0
- browserwright/site_skills_starter/wikipedia.org/SKILL.md +7 -0
- browserwright/site_skills_starter/wikipedia.org/memory.md +22 -0
- browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +55 -0
- browserwright/site_skills_starter/ycombinator.com/SKILL.md +8 -0
- browserwright/site_skills_starter/ycombinator.com/memory.md +25 -0
- browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +63 -0
- browserwright/skill_doc.py +140 -0
- browserwright/skill_runtime.md +194 -0
- browserwright/subscriptions.py +213 -0
- browserwright/task_runner.py +125 -0
- browserwright/version.py +117 -0
- browserwright-0.6.2.dist-info/METADATA +12 -0
- browserwright-0.6.2.dist-info/RECORD +98 -0
- browserwright-0.6.2.dist-info/WHEEL +5 -0
- browserwright-0.6.2.dist-info/entry_points.txt +3 -0
- browserwright-0.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""rdp backend — Chrome launched with --remote-debugging-port=NNNN.
|
|
2
|
+
|
|
3
|
+
Spec §8.2. The interesting part is the Chrome 136/147+ default-profile lockdown:
|
|
4
|
+
those builds disable `/json/version` HTTP discovery when the user-data-dir is
|
|
5
|
+
the *real* user profile (privacy hardening), returning a 404. The websocket
|
|
6
|
+
path still works, and Chrome still writes it into `DevToolsActivePort`. So the
|
|
7
|
+
fallback is: HTTP 404 → walk PROFILES, match the port number on line 1, read
|
|
8
|
+
the ws path from line 2.
|
|
9
|
+
|
|
10
|
+
This mirrors browser-harness `daemon.py:83-101` `_ws_from_devtools_active_port`
|
|
11
|
+
— which has already eaten the IPv6-host-bracket and stale-port edges.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from urllib.parse import urlparse
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from ..config import Config
|
|
21
|
+
from ..errors import Unavailable
|
|
22
|
+
from ..platforms import profile_paths
|
|
23
|
+
from .base import Backend, DoctorResult, ResolveResult
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RdpBackend(Backend):
|
|
27
|
+
name = "rdp"
|
|
28
|
+
kind = "UPSTREAM_WS"
|
|
29
|
+
recommended_mode: str = "A"
|
|
30
|
+
ux_cost = "none"
|
|
31
|
+
|
|
32
|
+
def __init__(self, cfg: Config):
|
|
33
|
+
self._cfg = cfg
|
|
34
|
+
self.port = cfg.backends.rdp.port
|
|
35
|
+
|
|
36
|
+
def caps(self) -> dict:
|
|
37
|
+
# --create launches/owns an isolated Chrome and can use real browser
|
|
38
|
+
# contexts for per-session isolation (P4).
|
|
39
|
+
return {"owns_browser": True, "supports_browser_context": True}
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------ probe
|
|
42
|
+
|
|
43
|
+
async def probe(self) -> DoctorResult:
|
|
44
|
+
"""Cheap reachability check — does HTTP discovery succeed, OR (404-case)
|
|
45
|
+
does some profile's DevToolsActivePort point at this port?
|
|
46
|
+
|
|
47
|
+
Per spec §5.2, probe never opens a ws and never produces ws_url.
|
|
48
|
+
"""
|
|
49
|
+
port = self.port
|
|
50
|
+
url = f"http://127.0.0.1:{port}/json/version"
|
|
51
|
+
try:
|
|
52
|
+
# trust_env=False keeps the user's HTTP(S)_PROXY / ALL_PROXY from
|
|
53
|
+
# being applied to localhost probes — proxying to your own loopback
|
|
54
|
+
# is never what anyone wants here and triggers httpx[socks] import
|
|
55
|
+
# errors when ALL_PROXY=socks5://...
|
|
56
|
+
async with httpx.AsyncClient(timeout=1.0, trust_env=False) as client:
|
|
57
|
+
resp = await client.get(url)
|
|
58
|
+
except (httpx.HTTPError, OSError) as e:
|
|
59
|
+
return DoctorResult(
|
|
60
|
+
name=self.name,
|
|
61
|
+
available=False,
|
|
62
|
+
ws_url=None,
|
|
63
|
+
detail=f"no service on 127.0.0.1:{port} ({type(e).__name__})",
|
|
64
|
+
ux_cost=self.ux_cost,
|
|
65
|
+
)
|
|
66
|
+
if resp.status_code == 200:
|
|
67
|
+
# 200 == happy path: discovery works. We don't actually parse
|
|
68
|
+
# webSocketDebuggerUrl here — `resolve` does that for real.
|
|
69
|
+
return DoctorResult(
|
|
70
|
+
name=self.name,
|
|
71
|
+
available=True,
|
|
72
|
+
ws_url=None,
|
|
73
|
+
detail=f"HTTP 200 from {url}",
|
|
74
|
+
ux_cost=self.ux_cost,
|
|
75
|
+
)
|
|
76
|
+
if resp.status_code == 404:
|
|
77
|
+
# Chrome 136/147+ default-profile lockdown. Probe by matching the
|
|
78
|
+
# port number in the existing DevToolsActivePort files. No ws open.
|
|
79
|
+
matched = _find_matching_profile(port)
|
|
80
|
+
if matched is not None:
|
|
81
|
+
return DoctorResult(
|
|
82
|
+
name=self.name,
|
|
83
|
+
available=True,
|
|
84
|
+
ws_url=None,
|
|
85
|
+
detail=(
|
|
86
|
+
f"HTTP 404 from {url} (Chrome 136/147+ default-profile "
|
|
87
|
+
f"lockdown); fallback matched {matched}/DevToolsActivePort"
|
|
88
|
+
),
|
|
89
|
+
ux_cost=self.ux_cost,
|
|
90
|
+
)
|
|
91
|
+
return DoctorResult(
|
|
92
|
+
name=self.name,
|
|
93
|
+
available=False,
|
|
94
|
+
ws_url=None,
|
|
95
|
+
detail=(
|
|
96
|
+
f"HTTP 404 from {url}; no DevToolsActivePort file on port {port} "
|
|
97
|
+
"in known profiles. Chrome likely needs --user-data-dir=<isolated>."
|
|
98
|
+
),
|
|
99
|
+
ux_cost=self.ux_cost,
|
|
100
|
+
)
|
|
101
|
+
return DoctorResult(
|
|
102
|
+
name=self.name,
|
|
103
|
+
available=False,
|
|
104
|
+
ws_url=None,
|
|
105
|
+
detail=f"HTTP {resp.status_code} from {url}",
|
|
106
|
+
ux_cost=self.ux_cost,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------- resolve
|
|
110
|
+
|
|
111
|
+
async def resolve(self, timeout: float) -> ResolveResult:
|
|
112
|
+
port = self.port
|
|
113
|
+
url = f"http://127.0.0.1:{port}/json/version"
|
|
114
|
+
try:
|
|
115
|
+
async with httpx.AsyncClient(timeout=timeout, trust_env=False) as client:
|
|
116
|
+
resp = await client.get(url)
|
|
117
|
+
except (httpx.HTTPError, OSError) as e:
|
|
118
|
+
raise Unavailable(
|
|
119
|
+
f"rdp: cannot reach {url}: {e}",
|
|
120
|
+
attempts={self.name: f"GET {url} -> {type(e).__name__}: {e}"},
|
|
121
|
+
) from e
|
|
122
|
+
if resp.status_code == 200:
|
|
123
|
+
try:
|
|
124
|
+
body = resp.json()
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
raise Unavailable(
|
|
127
|
+
f"rdp: {url} returned non-JSON: {e}",
|
|
128
|
+
attempts={self.name: f"GET {url} -> non-JSON"},
|
|
129
|
+
) from e
|
|
130
|
+
ws = body.get("webSocketDebuggerUrl") if isinstance(body, dict) else None
|
|
131
|
+
if not isinstance(ws, str) or not ws:
|
|
132
|
+
raise Unavailable(
|
|
133
|
+
f"rdp: {url} JSON has no webSocketDebuggerUrl",
|
|
134
|
+
attempts={self.name: "no webSocketDebuggerUrl field"},
|
|
135
|
+
)
|
|
136
|
+
return ResolveResult(
|
|
137
|
+
ws_url=ws,
|
|
138
|
+
backend=self.name,
|
|
139
|
+
extras={"isolated_profile": None, "profile_path": None},
|
|
140
|
+
)
|
|
141
|
+
if resp.status_code == 404:
|
|
142
|
+
ws = _ws_from_devtools_active_port(url)
|
|
143
|
+
if ws is None:
|
|
144
|
+
raise Unavailable(
|
|
145
|
+
f"rdp: HTTP 404 on {url} (Chrome 136/147+ default-profile lockdown) "
|
|
146
|
+
f"and no matching DevToolsActivePort file in known profiles",
|
|
147
|
+
attempts={self.name: f"GET {url} -> 404, fallback no match"},
|
|
148
|
+
)
|
|
149
|
+
matched_profile = _find_matching_profile(self.port)
|
|
150
|
+
return ResolveResult(
|
|
151
|
+
ws_url=ws,
|
|
152
|
+
backend=self.name,
|
|
153
|
+
extras={
|
|
154
|
+
"isolated_profile": False, # default-profile lockdown ⇒ user is on default
|
|
155
|
+
"profile_path": str(matched_profile) if matched_profile else None,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
raise Unavailable(
|
|
159
|
+
f"rdp: {url} returned HTTP {resp.status_code}",
|
|
160
|
+
attempts={self.name: f"GET {url} -> HTTP {resp.status_code}"},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---- helpers (module-private, also imported by tests) ----------------------
|
|
165
|
+
|
|
166
|
+
def _find_matching_profile(want_port: int) -> Path | None:
|
|
167
|
+
"""Walk PROFILES, return the first whose DevToolsActivePort line 1 == want_port
|
|
168
|
+
AND whose line 2 is a non-empty ws path.
|
|
169
|
+
|
|
170
|
+
Multiple matches → mtime-newest.
|
|
171
|
+
"""
|
|
172
|
+
matches: list[tuple[float, Path]] = []
|
|
173
|
+
for base in profile_paths():
|
|
174
|
+
f = base / "DevToolsActivePort"
|
|
175
|
+
try:
|
|
176
|
+
lines = f.read_text().splitlines()
|
|
177
|
+
mtime = f.stat().st_mtime
|
|
178
|
+
except (FileNotFoundError, NotADirectoryError, PermissionError, OSError):
|
|
179
|
+
continue
|
|
180
|
+
if not lines:
|
|
181
|
+
continue
|
|
182
|
+
port = lines[0].strip()
|
|
183
|
+
ws_path = lines[1].strip() if len(lines) > 1 else ""
|
|
184
|
+
if port == str(want_port) and ws_path:
|
|
185
|
+
matches.append((mtime, base))
|
|
186
|
+
if not matches:
|
|
187
|
+
return None
|
|
188
|
+
return max(matches, key=lambda t: t[0])[1]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _ws_from_devtools_active_port(http_url: str) -> str | None:
|
|
192
|
+
"""Build a ws:// URL from a DevToolsActivePort file when /json/version 404s.
|
|
193
|
+
|
|
194
|
+
Ported from browser-harness daemon.py:83-101 — preserves the IPv6 bracket
|
|
195
|
+
handling (urlparse strips brackets; we restore them) and the line-1/line-2
|
|
196
|
+
contract.
|
|
197
|
+
"""
|
|
198
|
+
p = urlparse(http_url)
|
|
199
|
+
want_port = str(p.port) if p.port else ""
|
|
200
|
+
if not want_port:
|
|
201
|
+
return None
|
|
202
|
+
host = p.hostname or "127.0.0.1"
|
|
203
|
+
if ":" in host: # IPv6 literal
|
|
204
|
+
host = f"[{host}]"
|
|
205
|
+
for base in profile_paths():
|
|
206
|
+
try:
|
|
207
|
+
active = (base / "DevToolsActivePort").read_text().splitlines()
|
|
208
|
+
except (FileNotFoundError, NotADirectoryError, PermissionError, OSError):
|
|
209
|
+
continue
|
|
210
|
+
port = active[0].strip() if active else ""
|
|
211
|
+
ws_path = active[1].strip() if len(active) > 1 else ""
|
|
212
|
+
if port == want_port and ws_path:
|
|
213
|
+
return f"ws://{host}:{port}{ws_path}"
|
|
214
|
+
return None
|