gecko-web-runtime-client 1.1.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.
- gecko_web_runtime_client-1.1.0/PKG-INFO +293 -0
- gecko_web_runtime_client-1.1.0/README.md +282 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/__init__.py +51 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/__main__.py +23 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/api/__init__.py +7 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/api/context.py +110 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/api/page.py +350 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/api/pool.py +146 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/api/runtime.py +186 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/data_plane.py +111 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/__init__.py +14 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/gc_cc.py +68 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/logging.py +39 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/memory.py +194 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/metrics.py +551 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/diagnostics/resources.py +60 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/http.py +184 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/installer.py +198 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/models/__init__.py +5 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/models/navigation.py +41 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/pipe.py +52 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/session.py +995 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/workflows/__init__.py +6 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/workflows/turnstile.py +680 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime/workflows/wechat.py +706 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime_client.egg-info/PKG-INFO +293 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime_client.egg-info/SOURCES.txt +30 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime_client.egg-info/dependency_links.txt +1 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime_client.egg-info/requires.txt +3 -0
- gecko_web_runtime_client-1.1.0/gecko_web_runtime_client.egg-info/top_level.txt +1 -0
- gecko_web_runtime_client-1.1.0/pyproject.toml +35 -0
- gecko_web_runtime_client-1.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gecko-web-runtime-client
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Thin Python client for the Firefox-derived WPR Gecko Worker protocol
|
|
5
|
+
Author: WPR project
|
|
6
|
+
License: MPL-2.0-compatible client; see the runtime bundle notices
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: diagnostics
|
|
10
|
+
Requires-Dist: loguru>=0.7.2; extra == "diagnostics"
|
|
11
|
+
|
|
12
|
+
# Python client
|
|
13
|
+
|
|
14
|
+
The client provides a standard-library core plus an optional Loguru-backed
|
|
15
|
+
diagnostics layer. From this directory:
|
|
16
|
+
|
|
17
|
+
```powershell
|
|
18
|
+
$env:PYTHONPATH = "$PWD"
|
|
19
|
+
python ..\examples\normal_navigation.py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The package resolves `runtime/gecko-web-worker.exe` relative to its own
|
|
23
|
+
bundle. Override paths with `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`,
|
|
24
|
+
`WPR_WORKER_EXE`, `WPR_PROFILE_ROOT`, or `WPR_LOG_ROOT` when embedding it in a
|
|
25
|
+
larger application.
|
|
26
|
+
|
|
27
|
+
## Firefox Necko network client
|
|
28
|
+
|
|
29
|
+
The version-neutral `wpr-http-v1` API is exposed by
|
|
30
|
+
`gecko_web_runtime.AsyncWprHttpClient`:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from gecko_web_runtime import AsyncWprHttpClient
|
|
34
|
+
|
|
35
|
+
async with AsyncWprHttpClient() as client:
|
|
36
|
+
response = await client.get("https://example.com/")
|
|
37
|
+
html = await response.atext()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The selected Firefox bundle launches `runtime/net-v155/gecko-net-worker.exe`
|
|
41
|
+
(the versioned adapter may be replaced by a later bundle). Its response body
|
|
42
|
+
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
|
|
43
|
+
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
|
|
44
|
+
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.
|
|
45
|
+
|
|
46
|
+
To install the pinned runtime automatically from the GitHub Release:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
python -m gecko_web_runtime install-runtime --version 155
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The command verifies the pinned SHA256 and stores the selected runtime under
|
|
53
|
+
`%LOCALAPPDATA%\WPR\runtime\155`.
|
|
54
|
+
|
|
55
|
+
For development, use `uv` from this directory so the Python environment and
|
|
56
|
+
lock file are reproducible:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
cd G:\wpr-runtime-155-win64\python
|
|
60
|
+
uv sync --extra diagnostics
|
|
61
|
+
uv run python -m unittest discover -s tests\unit -v
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The Gecko binary is intentionally not a Python dependency. Install or select
|
|
65
|
+
the matching runtime separately after the environment is ready:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uv run python -m gecko_web_runtime install-runtime --version 155
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Live network workflow tests are opt-in:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
$env:WPR_RUN_ONLINE_TESTS = "1"
|
|
75
|
+
uv run python -m unittest discover -s tests\integration -v
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To measure cold/warm navigation and the current isolation boundary:
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
uv run python tests\integration\context_reuse_benchmark.py `
|
|
82
|
+
--url https://example.com/ `
|
|
83
|
+
--out context-reuse.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The benchmark records first/second navigation timing, frame remote types,
|
|
87
|
+
WindowGlobal replacement, Worker PIDs and whether an isolated context sees a
|
|
88
|
+
Cookie from the persistent context.
|
|
89
|
+
|
|
90
|
+
An optional per-Worker `fpfile` path can be passed to the session. The Worker
|
|
91
|
+
reads the file before creating the remote page; its Firefox profile remains a
|
|
92
|
+
separate Cookie/Storage directory:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
async with AsyncGeckoWorkerSession(
|
|
96
|
+
fpfile=r"G:\browser-runtime\fpfile.txt"
|
|
97
|
+
) as session:
|
|
98
|
+
await session.page_create()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For application code, prefer the high-level runtime facade and workflow
|
|
102
|
+
adapter. Frame handles, pointer coordinates, WindowGlobal rebinding and
|
|
103
|
+
challenge waits remain internal:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from gecko_web_runtime import AsyncGeckoRuntime
|
|
107
|
+
|
|
108
|
+
async with AsyncGeckoRuntime(fpfile=r"G:\browser-runtime\fpfile.txt") as runtime:
|
|
109
|
+
page = await runtime.new_page()
|
|
110
|
+
result = await page.navigate(
|
|
111
|
+
"https://nopecha.com/demo/cloudflare",
|
|
112
|
+
workflow="turnstile",
|
|
113
|
+
)
|
|
114
|
+
print(result.ok, result.url, len(result.html))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For WeChat public-account articles, use the site adapter instead of the
|
|
118
|
+
Cloudflare-specific Turnstile workflow:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
async with AsyncGeckoRuntime() as runtime:
|
|
122
|
+
page = await runtime.new_page()
|
|
123
|
+
result = await page.navigate(article_url, workflow="wechat_article")
|
|
124
|
+
if result.ok:
|
|
125
|
+
article_html = result.html
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Use `WorkflowMetrics` when an application needs a stable result/metrics
|
|
129
|
+
boundary. The default output contains the product chain stages (`T0`...`T9`)
|
|
130
|
+
plus a compact report with success, final URL, HTML size,
|
|
131
|
+
total/navigation/final-ready time, and parent/whole-process-tree memory peaks.
|
|
132
|
+
Detailed attempts, phase timings, and per-stage resource snapshots are
|
|
133
|
+
emitted only with `debug=True` or `emit="debug"`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from gecko_web_runtime import WorkflowMetrics
|
|
137
|
+
|
|
138
|
+
metrics = WorkflowMetrics(name="wechat", emit="summary", print_summary=True)
|
|
139
|
+
summary = metrics.emit(result)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Library code can set `emit="none"` and consume the returned dictionary without
|
|
143
|
+
printing. `WPR_METRICS_EMIT`, `WPR_METRICS_LOG_LEVEL`, `WPR_METRICS_DEBUG`,
|
|
144
|
+
`WPR_METRICS_PRINT`, and `WPR_METRICS_MEMORY` provide process-level defaults.
|
|
145
|
+
|
|
146
|
+
The adapter recognizes the WeChat/Tencent gate, rebinds the current top
|
|
147
|
+
WindowGlobal, and dispatches a real pointer sequence to the visible
|
|
148
|
+
verification target. It reports slider/image challenges as a bounded result
|
|
149
|
+
for a future dedicated solver rather than treating the challenge page as an
|
|
150
|
+
article. It is one-shot by default and closes the Page after the workflow
|
|
151
|
+
boundary; pass `WechatArticleWorkflow(close_after=False)` when intentionally
|
|
152
|
+
reusing the Page.
|
|
153
|
+
|
|
154
|
+
For multiple logical sessions, keep the Worker runtime alive but make the
|
|
155
|
+
storage boundary explicit:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
async with AsyncGeckoRuntime(fpfile="fpfile-a.txt") as runtime:
|
|
159
|
+
async with runtime.new_context(isolation="persistent") as context_a:
|
|
160
|
+
page_a = await context_a.new_page()
|
|
161
|
+
result_a = await page_a.navigate(url_a)
|
|
162
|
+
|
|
163
|
+
async with runtime.new_context(isolation="isolated") as context_b:
|
|
164
|
+
page_b = await context_b.new_page()
|
|
165
|
+
result_b = await page_b.navigate(url_b)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`persistent` reuses the runtime's current Worker while each Context receives
|
|
169
|
+
its native Firefox OriginAttributes boundary (`user_context_id` and optional
|
|
170
|
+
`private_browsing_id`). `isolated` remains the explicit separate Worker and
|
|
171
|
+
profile mode for callers that require process-level isolation.
|
|
172
|
+
|
|
173
|
+
Turnstile results expose phase timings in `result.timing_ms`, including
|
|
174
|
+
per-attempt iframe/action waits, POST, redirect, final Document readiness, and
|
|
175
|
+
HTML serialization. Worker PID, Working Set, Private Bytes, and CPU samples
|
|
176
|
+
are available in `result.resource_usage`. Sampling can be disabled for a
|
|
177
|
+
low-overhead benchmark with `TurnstileWorkflow(resource_sampling=False)` or
|
|
178
|
+
`WPR_RESOURCE_SAMPLING=0`. The integration test writes the complete result
|
|
179
|
+
to the path in `WPR_RESULT_OUT` when set.
|
|
180
|
+
|
|
181
|
+
Challenge retries use the `adaptive` policy by default. After a top-level
|
|
182
|
+
state transition it waits for the network settle window and stops immediately
|
|
183
|
+
on POST/redirect; it only dispatches another action when no submit arrives.
|
|
184
|
+
`retry_policy="legacy"` preserves the previous retry loop for A/B experiments,
|
|
185
|
+
while `retry_policy="generation"` is a strict diagnostic policy that requires a
|
|
186
|
+
new `BrowsingContext`/`WindowGlobal` before another action. The third-party
|
|
187
|
+
client exposes the same switch as `--retry-policy`. Debug results include
|
|
188
|
+
`state_change_to_submit_ms` when both observations were captured on the same
|
|
189
|
+
client monotonic clock.
|
|
190
|
+
|
|
191
|
+
The final boundary is configurable. `final_wait_until="complete"` waits for
|
|
192
|
+
the full DOM/load boundary and is the compatibility default. Set
|
|
193
|
+
`WPR_TURNSTILE_WAIT_UNTIL=document` (or pass `final_wait_until="document"`)
|
|
194
|
+
when the caller only needs the final Document HTML; the result then reports
|
|
195
|
+
`final_document`, `final_load`, and the selected `final_ready` boundary
|
|
196
|
+
separately. Document mode does not wait for the page's scripts, stylesheets,
|
|
197
|
+
images, or analytics resources to finish.
|
|
198
|
+
|
|
199
|
+
Large results use a separate data plane. `page.eval_shared_blob()` uses a
|
|
200
|
+
one-shot mapping, while `page.eval_shared_ring()`/`page.html_shared_ring()` use
|
|
201
|
+
a Worker-lifetime fixed-slot ring. Both return a small descriptor over the
|
|
202
|
+
control pipe and a read-only Windows named mapping for the bytes. Read it as
|
|
203
|
+
a `memoryview` or decode it with `read_text()`, then release the lease
|
|
204
|
+
explicitly:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
blob = await page.html_shared_ring()
|
|
208
|
+
try:
|
|
209
|
+
html = blob.read_text()
|
|
210
|
+
finally:
|
|
211
|
+
await blob.release()
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This keeps multi-megabyte HTML out of JSON serialization and the named-pipe
|
|
215
|
+
response. The ring mapping is created once per Worker and reuses slots using
|
|
216
|
+
generation-bearing leases; `data.release` returns a slot to the ring. Set
|
|
217
|
+
`WPR_HTML_TRANSPORT=json`, `blob`, or `ring` to A/B the adapter boundary. The
|
|
218
|
+
WeChat and Turnstile adapters default to `ring` and fall back to ordinary
|
|
219
|
+
`page.eval` when an older Worker has no data-plane method.
|
|
220
|
+
|
|
221
|
+
The long-lived comparison probe keeps one Worker per site/transport, uses a
|
|
222
|
+
fresh OriginAttributes context for every sample, and records success rate,
|
|
223
|
+
P95, CPU, Worker/process-tree memory, and Gecko reporter stages:
|
|
224
|
+
|
|
225
|
+
```powershell
|
|
226
|
+
uv run python tests\integration\data_plane_ab_benchmark.py `
|
|
227
|
+
--runs 10 --modes json ring
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The low-level `AsyncGeckoWorkerSession` API remains available for custom
|
|
231
|
+
workflows and backward compatibility. Turnstile is implemented in
|
|
232
|
+
`gecko_web_runtime.workflows.turnstile`; future site adapters belong in the
|
|
233
|
+
same package and are not part of the Gecko core.
|
|
234
|
+
|
|
235
|
+
The client project keeps its tests alongside the package source:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
python/tests/unit/ # import/API contract tests
|
|
239
|
+
python/tests/integration/ # opt-in live Worker/workflow tests
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The larger `G:\browser-runtime\tests\probes` tree is a separate black-box
|
|
243
|
+
runtime audit harness. It exercises the complete bundled Gecko runtime and
|
|
244
|
+
stores diagnostic artifacts, so it is intentionally not included in the
|
|
245
|
+
Python wheel.
|
|
246
|
+
|
|
247
|
+
To enable structured application diagnostics:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install gecko-web-runtime-client[diagnostics]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from gecko_web_runtime.diagnostics import configure_logging
|
|
255
|
+
|
|
256
|
+
logger = configure_logging(log_file="runtime.log")
|
|
257
|
+
logger.info("application.start")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
For a real page WebRTC permission boundary, use the same session and answer
|
|
261
|
+
the native pending request. The Worker forwards Firefox's
|
|
262
|
+
`PeerConnection:response:allow` or `PeerConnection:response:deny` topic; it
|
|
263
|
+
does not manufacture SDP, ICE candidates, or media devices:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
pending = await session.page_permission_requests()
|
|
267
|
+
for request in pending:
|
|
268
|
+
await session.page_permission_respond(request["callID"], "allow")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Leaving the list unanswered preserves Firefox's normal prompt-pending
|
|
272
|
+
behavior. A response is scoped to the current WindowGlobal, so a navigation
|
|
273
|
+
that replaces it requires polling the new page's request list.
|
|
274
|
+
|
|
275
|
+
For a private GitHub Release, set `GITHUB_TOKEN`, `GH_TOKEN`, or
|
|
276
|
+
`WPR_GITHUB_TOKEN`, or configure Git Credential Manager for the GitHub account.
|
|
277
|
+
# Firefox Necko network client
|
|
278
|
+
|
|
279
|
+
The version-neutral `wpr-http-v1` API is exposed by
|
|
280
|
+
`gecko_web_runtime.AsyncWprHttpClient`:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from gecko_web_runtime import AsyncWprHttpClient
|
|
284
|
+
|
|
285
|
+
async with AsyncWprHttpClient() as client:
|
|
286
|
+
response = await client.get("https://example.com/")
|
|
287
|
+
html = await response.atext()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The selected Firefox bundle launches `gecko-net-worker.exe`. Its response body
|
|
291
|
+
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
|
|
292
|
+
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
|
|
293
|
+
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Python client
|
|
2
|
+
|
|
3
|
+
The client provides a standard-library core plus an optional Loguru-backed
|
|
4
|
+
diagnostics layer. From this directory:
|
|
5
|
+
|
|
6
|
+
```powershell
|
|
7
|
+
$env:PYTHONPATH = "$PWD"
|
|
8
|
+
python ..\examples\normal_navigation.py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package resolves `runtime/gecko-web-worker.exe` relative to its own
|
|
12
|
+
bundle. Override paths with `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`,
|
|
13
|
+
`WPR_WORKER_EXE`, `WPR_PROFILE_ROOT`, or `WPR_LOG_ROOT` when embedding it in a
|
|
14
|
+
larger application.
|
|
15
|
+
|
|
16
|
+
## Firefox Necko network client
|
|
17
|
+
|
|
18
|
+
The version-neutral `wpr-http-v1` API is exposed by
|
|
19
|
+
`gecko_web_runtime.AsyncWprHttpClient`:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from gecko_web_runtime import AsyncWprHttpClient
|
|
23
|
+
|
|
24
|
+
async with AsyncWprHttpClient() as client:
|
|
25
|
+
response = await client.get("https://example.com/")
|
|
26
|
+
html = await response.atext()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The selected Firefox bundle launches `runtime/net-v155/gecko-net-worker.exe`
|
|
30
|
+
(the versioned adapter may be replaced by a later bundle). Its response body
|
|
31
|
+
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
|
|
32
|
+
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
|
|
33
|
+
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.
|
|
34
|
+
|
|
35
|
+
To install the pinned runtime automatically from the GitHub Release:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python -m gecko_web_runtime install-runtime --version 155
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The command verifies the pinned SHA256 and stores the selected runtime under
|
|
42
|
+
`%LOCALAPPDATA%\WPR\runtime\155`.
|
|
43
|
+
|
|
44
|
+
For development, use `uv` from this directory so the Python environment and
|
|
45
|
+
lock file are reproducible:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
cd G:\wpr-runtime-155-win64\python
|
|
49
|
+
uv sync --extra diagnostics
|
|
50
|
+
uv run python -m unittest discover -s tests\unit -v
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The Gecko binary is intentionally not a Python dependency. Install or select
|
|
54
|
+
the matching runtime separately after the environment is ready:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
uv run python -m gecko_web_runtime install-runtime --version 155
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Live network workflow tests are opt-in:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
$env:WPR_RUN_ONLINE_TESTS = "1"
|
|
64
|
+
uv run python -m unittest discover -s tests\integration -v
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To measure cold/warm navigation and the current isolation boundary:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
uv run python tests\integration\context_reuse_benchmark.py `
|
|
71
|
+
--url https://example.com/ `
|
|
72
|
+
--out context-reuse.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The benchmark records first/second navigation timing, frame remote types,
|
|
76
|
+
WindowGlobal replacement, Worker PIDs and whether an isolated context sees a
|
|
77
|
+
Cookie from the persistent context.
|
|
78
|
+
|
|
79
|
+
An optional per-Worker `fpfile` path can be passed to the session. The Worker
|
|
80
|
+
reads the file before creating the remote page; its Firefox profile remains a
|
|
81
|
+
separate Cookie/Storage directory:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async with AsyncGeckoWorkerSession(
|
|
85
|
+
fpfile=r"G:\browser-runtime\fpfile.txt"
|
|
86
|
+
) as session:
|
|
87
|
+
await session.page_create()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For application code, prefer the high-level runtime facade and workflow
|
|
91
|
+
adapter. Frame handles, pointer coordinates, WindowGlobal rebinding and
|
|
92
|
+
challenge waits remain internal:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from gecko_web_runtime import AsyncGeckoRuntime
|
|
96
|
+
|
|
97
|
+
async with AsyncGeckoRuntime(fpfile=r"G:\browser-runtime\fpfile.txt") as runtime:
|
|
98
|
+
page = await runtime.new_page()
|
|
99
|
+
result = await page.navigate(
|
|
100
|
+
"https://nopecha.com/demo/cloudflare",
|
|
101
|
+
workflow="turnstile",
|
|
102
|
+
)
|
|
103
|
+
print(result.ok, result.url, len(result.html))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For WeChat public-account articles, use the site adapter instead of the
|
|
107
|
+
Cloudflare-specific Turnstile workflow:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
async with AsyncGeckoRuntime() as runtime:
|
|
111
|
+
page = await runtime.new_page()
|
|
112
|
+
result = await page.navigate(article_url, workflow="wechat_article")
|
|
113
|
+
if result.ok:
|
|
114
|
+
article_html = result.html
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `WorkflowMetrics` when an application needs a stable result/metrics
|
|
118
|
+
boundary. The default output contains the product chain stages (`T0`...`T9`)
|
|
119
|
+
plus a compact report with success, final URL, HTML size,
|
|
120
|
+
total/navigation/final-ready time, and parent/whole-process-tree memory peaks.
|
|
121
|
+
Detailed attempts, phase timings, and per-stage resource snapshots are
|
|
122
|
+
emitted only with `debug=True` or `emit="debug"`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from gecko_web_runtime import WorkflowMetrics
|
|
126
|
+
|
|
127
|
+
metrics = WorkflowMetrics(name="wechat", emit="summary", print_summary=True)
|
|
128
|
+
summary = metrics.emit(result)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Library code can set `emit="none"` and consume the returned dictionary without
|
|
132
|
+
printing. `WPR_METRICS_EMIT`, `WPR_METRICS_LOG_LEVEL`, `WPR_METRICS_DEBUG`,
|
|
133
|
+
`WPR_METRICS_PRINT`, and `WPR_METRICS_MEMORY` provide process-level defaults.
|
|
134
|
+
|
|
135
|
+
The adapter recognizes the WeChat/Tencent gate, rebinds the current top
|
|
136
|
+
WindowGlobal, and dispatches a real pointer sequence to the visible
|
|
137
|
+
verification target. It reports slider/image challenges as a bounded result
|
|
138
|
+
for a future dedicated solver rather than treating the challenge page as an
|
|
139
|
+
article. It is one-shot by default and closes the Page after the workflow
|
|
140
|
+
boundary; pass `WechatArticleWorkflow(close_after=False)` when intentionally
|
|
141
|
+
reusing the Page.
|
|
142
|
+
|
|
143
|
+
For multiple logical sessions, keep the Worker runtime alive but make the
|
|
144
|
+
storage boundary explicit:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
async with AsyncGeckoRuntime(fpfile="fpfile-a.txt") as runtime:
|
|
148
|
+
async with runtime.new_context(isolation="persistent") as context_a:
|
|
149
|
+
page_a = await context_a.new_page()
|
|
150
|
+
result_a = await page_a.navigate(url_a)
|
|
151
|
+
|
|
152
|
+
async with runtime.new_context(isolation="isolated") as context_b:
|
|
153
|
+
page_b = await context_b.new_page()
|
|
154
|
+
result_b = await page_b.navigate(url_b)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`persistent` reuses the runtime's current Worker while each Context receives
|
|
158
|
+
its native Firefox OriginAttributes boundary (`user_context_id` and optional
|
|
159
|
+
`private_browsing_id`). `isolated` remains the explicit separate Worker and
|
|
160
|
+
profile mode for callers that require process-level isolation.
|
|
161
|
+
|
|
162
|
+
Turnstile results expose phase timings in `result.timing_ms`, including
|
|
163
|
+
per-attempt iframe/action waits, POST, redirect, final Document readiness, and
|
|
164
|
+
HTML serialization. Worker PID, Working Set, Private Bytes, and CPU samples
|
|
165
|
+
are available in `result.resource_usage`. Sampling can be disabled for a
|
|
166
|
+
low-overhead benchmark with `TurnstileWorkflow(resource_sampling=False)` or
|
|
167
|
+
`WPR_RESOURCE_SAMPLING=0`. The integration test writes the complete result
|
|
168
|
+
to the path in `WPR_RESULT_OUT` when set.
|
|
169
|
+
|
|
170
|
+
Challenge retries use the `adaptive` policy by default. After a top-level
|
|
171
|
+
state transition it waits for the network settle window and stops immediately
|
|
172
|
+
on POST/redirect; it only dispatches another action when no submit arrives.
|
|
173
|
+
`retry_policy="legacy"` preserves the previous retry loop for A/B experiments,
|
|
174
|
+
while `retry_policy="generation"` is a strict diagnostic policy that requires a
|
|
175
|
+
new `BrowsingContext`/`WindowGlobal` before another action. The third-party
|
|
176
|
+
client exposes the same switch as `--retry-policy`. Debug results include
|
|
177
|
+
`state_change_to_submit_ms` when both observations were captured on the same
|
|
178
|
+
client monotonic clock.
|
|
179
|
+
|
|
180
|
+
The final boundary is configurable. `final_wait_until="complete"` waits for
|
|
181
|
+
the full DOM/load boundary and is the compatibility default. Set
|
|
182
|
+
`WPR_TURNSTILE_WAIT_UNTIL=document` (or pass `final_wait_until="document"`)
|
|
183
|
+
when the caller only needs the final Document HTML; the result then reports
|
|
184
|
+
`final_document`, `final_load`, and the selected `final_ready` boundary
|
|
185
|
+
separately. Document mode does not wait for the page's scripts, stylesheets,
|
|
186
|
+
images, or analytics resources to finish.
|
|
187
|
+
|
|
188
|
+
Large results use a separate data plane. `page.eval_shared_blob()` uses a
|
|
189
|
+
one-shot mapping, while `page.eval_shared_ring()`/`page.html_shared_ring()` use
|
|
190
|
+
a Worker-lifetime fixed-slot ring. Both return a small descriptor over the
|
|
191
|
+
control pipe and a read-only Windows named mapping for the bytes. Read it as
|
|
192
|
+
a `memoryview` or decode it with `read_text()`, then release the lease
|
|
193
|
+
explicitly:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
blob = await page.html_shared_ring()
|
|
197
|
+
try:
|
|
198
|
+
html = blob.read_text()
|
|
199
|
+
finally:
|
|
200
|
+
await blob.release()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This keeps multi-megabyte HTML out of JSON serialization and the named-pipe
|
|
204
|
+
response. The ring mapping is created once per Worker and reuses slots using
|
|
205
|
+
generation-bearing leases; `data.release` returns a slot to the ring. Set
|
|
206
|
+
`WPR_HTML_TRANSPORT=json`, `blob`, or `ring` to A/B the adapter boundary. The
|
|
207
|
+
WeChat and Turnstile adapters default to `ring` and fall back to ordinary
|
|
208
|
+
`page.eval` when an older Worker has no data-plane method.
|
|
209
|
+
|
|
210
|
+
The long-lived comparison probe keeps one Worker per site/transport, uses a
|
|
211
|
+
fresh OriginAttributes context for every sample, and records success rate,
|
|
212
|
+
P95, CPU, Worker/process-tree memory, and Gecko reporter stages:
|
|
213
|
+
|
|
214
|
+
```powershell
|
|
215
|
+
uv run python tests\integration\data_plane_ab_benchmark.py `
|
|
216
|
+
--runs 10 --modes json ring
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The low-level `AsyncGeckoWorkerSession` API remains available for custom
|
|
220
|
+
workflows and backward compatibility. Turnstile is implemented in
|
|
221
|
+
`gecko_web_runtime.workflows.turnstile`; future site adapters belong in the
|
|
222
|
+
same package and are not part of the Gecko core.
|
|
223
|
+
|
|
224
|
+
The client project keeps its tests alongside the package source:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
python/tests/unit/ # import/API contract tests
|
|
228
|
+
python/tests/integration/ # opt-in live Worker/workflow tests
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The larger `G:\browser-runtime\tests\probes` tree is a separate black-box
|
|
232
|
+
runtime audit harness. It exercises the complete bundled Gecko runtime and
|
|
233
|
+
stores diagnostic artifacts, so it is intentionally not included in the
|
|
234
|
+
Python wheel.
|
|
235
|
+
|
|
236
|
+
To enable structured application diagnostics:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
pip install gecko-web-runtime-client[diagnostics]
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from gecko_web_runtime.diagnostics import configure_logging
|
|
244
|
+
|
|
245
|
+
logger = configure_logging(log_file="runtime.log")
|
|
246
|
+
logger.info("application.start")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
For a real page WebRTC permission boundary, use the same session and answer
|
|
250
|
+
the native pending request. The Worker forwards Firefox's
|
|
251
|
+
`PeerConnection:response:allow` or `PeerConnection:response:deny` topic; it
|
|
252
|
+
does not manufacture SDP, ICE candidates, or media devices:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
pending = await session.page_permission_requests()
|
|
256
|
+
for request in pending:
|
|
257
|
+
await session.page_permission_respond(request["callID"], "allow")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Leaving the list unanswered preserves Firefox's normal prompt-pending
|
|
261
|
+
behavior. A response is scoped to the current WindowGlobal, so a navigation
|
|
262
|
+
that replaces it requires polling the new page's request list.
|
|
263
|
+
|
|
264
|
+
For a private GitHub Release, set `GITHUB_TOKEN`, `GH_TOKEN`, or
|
|
265
|
+
`WPR_GITHUB_TOKEN`, or configure Git Credential Manager for the GitHub account.
|
|
266
|
+
# Firefox Necko network client
|
|
267
|
+
|
|
268
|
+
The version-neutral `wpr-http-v1` API is exposed by
|
|
269
|
+
`gecko_web_runtime.AsyncWprHttpClient`:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from gecko_web_runtime import AsyncWprHttpClient
|
|
273
|
+
|
|
274
|
+
async with AsyncWprHttpClient() as client:
|
|
275
|
+
response = await client.get("https://example.com/")
|
|
276
|
+
html = await response.atext()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The selected Firefox bundle launches `gecko-net-worker.exe`. Its response body
|
|
280
|
+
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
|
|
281
|
+
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
|
|
282
|
+
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Portable Python client for the Firefox-derived WPR runtime bundle."""
|
|
2
|
+
|
|
3
|
+
from .session import (
|
|
4
|
+
AsyncGeckoWorkerSession,
|
|
5
|
+
GeckoWorkerSession,
|
|
6
|
+
RuntimeConfig,
|
|
7
|
+
WprProtocolError,
|
|
8
|
+
)
|
|
9
|
+
from .installer import install_runtime
|
|
10
|
+
from .models.navigation import TurnstileResult, WechatArticleResult
|
|
11
|
+
from .workflows.turnstile import TurnstileWorkflow
|
|
12
|
+
from .workflows.wechat import WechatArticleWorkflow
|
|
13
|
+
from .api.runtime import AsyncGeckoRuntime, WorkerRecyclePolicy
|
|
14
|
+
from .api.context import BrowserContext
|
|
15
|
+
from .api.page import Page
|
|
16
|
+
from .api.pool import AsyncGeckoWorkerPool
|
|
17
|
+
from .data_plane import SharedBlob, SharedBlobDescriptor
|
|
18
|
+
from .diagnostics.metrics import MetricsOptions, WorkflowMetrics
|
|
19
|
+
from .http import (
|
|
20
|
+
AsyncWprHttpClient,
|
|
21
|
+
HttpResponse,
|
|
22
|
+
NetworkTiming,
|
|
23
|
+
WprHttpClient,
|
|
24
|
+
WprHttpError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AsyncGeckoWorkerSession",
|
|
29
|
+
"GeckoWorkerSession",
|
|
30
|
+
"RuntimeConfig",
|
|
31
|
+
"WprProtocolError",
|
|
32
|
+
"install_runtime",
|
|
33
|
+
"TurnstileResult",
|
|
34
|
+
"WechatArticleResult",
|
|
35
|
+
"TurnstileWorkflow",
|
|
36
|
+
"WechatArticleWorkflow",
|
|
37
|
+
"AsyncGeckoRuntime",
|
|
38
|
+
"WorkerRecyclePolicy",
|
|
39
|
+
"BrowserContext",
|
|
40
|
+
"Page",
|
|
41
|
+
"AsyncGeckoWorkerPool",
|
|
42
|
+
"SharedBlob",
|
|
43
|
+
"SharedBlobDescriptor",
|
|
44
|
+
"MetricsOptions",
|
|
45
|
+
"WorkflowMetrics",
|
|
46
|
+
"AsyncWprHttpClient",
|
|
47
|
+
"WprHttpClient",
|
|
48
|
+
"HttpResponse",
|
|
49
|
+
"NetworkTiming",
|
|
50
|
+
"WprHttpError",
|
|
51
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from .installer import install_runtime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="python -m gecko_web_runtime")
|
|
10
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
11
|
+
install = commands.add_parser("install-runtime", help="download a pinned Gecko runtime")
|
|
12
|
+
install.add_argument("--version", required=True, help="runtime major version, e.g. 155")
|
|
13
|
+
install.add_argument("--runtime-home", help="optional installation directory")
|
|
14
|
+
install.add_argument("--force", action="store_true", help="replace an existing installation")
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
if args.command == "install-runtime":
|
|
17
|
+
location = install_runtime(args.version, args.runtime_home, force=args.force)
|
|
18
|
+
print(location)
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
raise SystemExit(main())
|