browserwright 0.7.4__tar.gz → 0.8.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.7.4 → browserwright-0.8.0}/PKG-INFO +1 -1
- {browserwright-0.7.4 → browserwright-0.8.0}/pyproject.toml +1 -1
- browserwright-0.8.0/src/browserwright/_executor/client.py +301 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/process.py +241 -157
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/protocol.py +139 -3
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/cli.py +163 -43
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/_ipc.py +46 -6
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/cli.py +19 -6
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/executor_registry.py +126 -15
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/verbs.py +30 -5
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/errors.py +31 -3
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/inline.py +13 -7
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/playwright_handle.py +167 -40
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session.py +20 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_create.py +16 -1
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/skill_runtime.md +50 -8
- browserwright-0.8.0/src/browserwright/task_runner.py +348 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/PKG-INFO +1 -1
- browserwright-0.7.4/src/browserwright/_executor/client.py +0 -135
- browserwright-0.7.4/src/browserwright/task_runner.py +0 -134
- {browserwright-0.7.4 → browserwright-0.8.0}/README.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/setup.cfg +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/__main__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/_executor/__main__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/api.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/cdp.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/_stale.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/base.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/cdp.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/backends/extension.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/config.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/doctor.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/errors.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/launch_chrome.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/observability.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/platforms.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/resolver.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/daemon.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/extension_upstream.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/facade.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/facade_extension.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/listener.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/proxy.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/relay.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/state.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/server/upstream.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/daemon/userscripts.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/discovery.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/health.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/install.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_lock.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_md.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/_yaml.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/global_mem.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/memory/site_mem.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/mode_b_client.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/output_schema.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/discovery_api.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/http.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/primitives/site.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/__init__.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/_namespace.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/_smart_goto.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/repl/snapshot.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_ctx.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_registry.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/session_runtime.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/skill_doc.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright/version.py +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/SOURCES.txt +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/dependency_links.txt +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/entry_points.txt +0 -0
- {browserwright-0.7.4 → browserwright-0.8.0}/src/browserwright.egg-info/requires.txt +0 -0
- {browserwright-0.7.4 → browserwright-0.8.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.8.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
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: websockets==15.0.1
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "browserwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
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,301 @@
|
|
|
1
|
+
"""Thin-client side of the executor data plane.
|
|
2
|
+
|
|
3
|
+
Used by ``repl/inline.py`` when inline code touches ``page`` / ``context`` /
|
|
4
|
+
``snapshot`` / ``state`` / ``reset``: the whole code body is shipped to the
|
|
5
|
+
session's resident executor and the response is replayed locally.
|
|
6
|
+
|
|
7
|
+
Control plane (spawn + discover) goes through the daemon's
|
|
8
|
+
``BrowserwrightDaemon.ensureExecutor`` verb over the EXISTING mode_b socket
|
|
9
|
+
(tiny payload). The daemon spawns the executor if absent, waits for it to bind +
|
|
10
|
+
write its ``_ipc`` discovery file, and returns the socket path. The data plane
|
|
11
|
+
(this module) then connects DIRECTLY to that socket — keeping arbitrary code +
|
|
12
|
+
large output off the daemon's event loop (Fork 2).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import signal
|
|
17
|
+
import socket
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from .. import session_registry as reg
|
|
25
|
+
from ..errors import BrowserwrightError
|
|
26
|
+
from .protocol import (
|
|
27
|
+
DEFAULT_TIMEOUT_MS,
|
|
28
|
+
ExecuteRequest,
|
|
29
|
+
ExecuteResponse,
|
|
30
|
+
TaskEnvelope,
|
|
31
|
+
recv_message,
|
|
32
|
+
send_message,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Allow the executor's deadline response a small framing/delivery margin. The
|
|
36
|
+
# browser cold-start is part of the request deadline, not hidden extra time.
|
|
37
|
+
_RESPONSE_DELIVERY_SLACK_S = 2.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExecutorUnavailable(BrowserwrightError):
|
|
41
|
+
"""The session's executor could not be ensured/reached.
|
|
42
|
+
|
|
43
|
+
Surfaced when ``ensureExecutor`` fails or the executor socket can't be
|
|
44
|
+
connected — actionable: the daemon must be running (it spawns the
|
|
45
|
+
executor)."""
|
|
46
|
+
|
|
47
|
+
default_fix = ("ensure the daemon is running (`browserwright-daemon status "
|
|
48
|
+
"--json` should show `alive`); if the executor is stale, "
|
|
49
|
+
"call `reset()` as a standalone/final inline statement, "
|
|
50
|
+
"then retry in a new command; or run `browserwright "
|
|
51
|
+
"session reset <id>` before retrying.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ExecutorLease:
|
|
56
|
+
sock_path: str
|
|
57
|
+
executor_id: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_executor_lease(sess) -> ExecutorLease:
|
|
61
|
+
"""Ask the daemon to ensure the session's executor and return its socket
|
|
62
|
+
path. Uses the session's mode_b CDP client (``sess.cdp``) to send the
|
|
63
|
+
control-plane verb."""
|
|
64
|
+
sid = _session_id(sess)
|
|
65
|
+
try:
|
|
66
|
+
# The browserwright session is already bound on the websocket query
|
|
67
|
+
# (`?session=<id>`). Do not pass it as CDP's top-level `sessionId`;
|
|
68
|
+
# that field means "attached target session" inside the proxy mux.
|
|
69
|
+
res = sess.cdp.send("BrowserwrightDaemon.ensureExecutor", bsSession=sid)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise ExecutorUnavailable(
|
|
72
|
+
f"ensureExecutor failed for session {sid!r}: {e}"
|
|
73
|
+
) from e
|
|
74
|
+
sock_path = res.get("exec_sock") if isinstance(res, dict) else None
|
|
75
|
+
executor_id = res.get("executor_id") if isinstance(res, dict) else None
|
|
76
|
+
if not isinstance(sock_path, str) or not sock_path:
|
|
77
|
+
raise ExecutorUnavailable(
|
|
78
|
+
f"ensureExecutor returned no socket for session {sid!r}: {res!r}"
|
|
79
|
+
)
|
|
80
|
+
if not isinstance(executor_id, str) or not executor_id:
|
|
81
|
+
raise ExecutorUnavailable(
|
|
82
|
+
"ensureExecutor returned no executor instance identity; restart "
|
|
83
|
+
"the daemon so timeout cleanup cannot target a newer process"
|
|
84
|
+
)
|
|
85
|
+
return ExecutorLease(sock_path=sock_path, executor_id=executor_id)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ensure_executor(sess) -> str:
|
|
89
|
+
"""Compatibility wrapper returning only the executor socket path."""
|
|
90
|
+
return _ensure_executor_lease(sess).sock_path
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_on_executor(
|
|
94
|
+
sess,
|
|
95
|
+
code: str,
|
|
96
|
+
*,
|
|
97
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS,
|
|
98
|
+
env: dict[str, str] | None = None,
|
|
99
|
+
) -> ExecuteResponse:
|
|
100
|
+
"""Ship ``code`` to the session's executor and return its response.
|
|
101
|
+
|
|
102
|
+
Ensures the executor (control plane), connects its socket (data plane),
|
|
103
|
+
sends one :class:`ExecuteRequest`, reads one :class:`ExecuteResponse`.
|
|
104
|
+
|
|
105
|
+
The executor enforces ``timeout_ms`` across cold-start and user code. A
|
|
106
|
+
terminal deadline/reset response is not returned to the caller until the
|
|
107
|
+
daemon confirms that exact executor instance has exited."""
|
|
108
|
+
return _run_request_on_executor(
|
|
109
|
+
sess,
|
|
110
|
+
ExecuteRequest(
|
|
111
|
+
code=code,
|
|
112
|
+
timeout_ms=timeout_ms,
|
|
113
|
+
env=dict(env or {}),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_task_on_executor(
|
|
119
|
+
sess,
|
|
120
|
+
site: str,
|
|
121
|
+
name: str,
|
|
122
|
+
*,
|
|
123
|
+
args: dict | None = None,
|
|
124
|
+
isolated: bool = False,
|
|
125
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS,
|
|
126
|
+
env: dict[str, str] | None = None,
|
|
127
|
+
) -> ExecuteResponse:
|
|
128
|
+
"""Run one validated site-skill task on the resident executor surface."""
|
|
129
|
+
return _run_request_on_executor(
|
|
130
|
+
sess,
|
|
131
|
+
ExecuteRequest(
|
|
132
|
+
code="",
|
|
133
|
+
timeout_ms=timeout_ms,
|
|
134
|
+
env=dict(env or {}),
|
|
135
|
+
task=TaskEnvelope(
|
|
136
|
+
site=site,
|
|
137
|
+
name=name,
|
|
138
|
+
args=dict(args or {}),
|
|
139
|
+
isolated=isolated,
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _run_request_on_executor(
|
|
146
|
+
sess,
|
|
147
|
+
request: ExecuteRequest,
|
|
148
|
+
) -> ExecuteResponse:
|
|
149
|
+
"""Send one request through the shared lease/reap data-plane lifecycle."""
|
|
150
|
+
# Validate/copy before touching the daemon or opening a socket. The
|
|
151
|
+
# executor validates again at the trust boundary.
|
|
152
|
+
request = ExecuteRequest.from_dict(request.to_dict())
|
|
153
|
+
sid = _session_id(sess)
|
|
154
|
+
# Session idle is "time since the last user/agent instruction", not
|
|
155
|
+
# executor process liveness. Touch before contacting the executor so a
|
|
156
|
+
# wedged or long-running executor cannot prevent the durable idle clock
|
|
157
|
+
# from reflecting that a new instruction arrived.
|
|
158
|
+
reg.touch(sid)
|
|
159
|
+
lease = _ensure_executor_lease(sess)
|
|
160
|
+
# The executor owns the exact outer deadline, including first-call
|
|
161
|
+
# cold-start. Give only a small framing/delivery margin so its terminal
|
|
162
|
+
# response wins the race against the local socket timeout.
|
|
163
|
+
recv_timeout = (
|
|
164
|
+
max(request.timeout_ms, 1) / 1000.0 + _RESPONSE_DELIVERY_SLACK_S
|
|
165
|
+
)
|
|
166
|
+
conn = _connect(lease.sock_path, timeout=recv_timeout)
|
|
167
|
+
sent = False
|
|
168
|
+
interrupted: BaseException | None = None
|
|
169
|
+
transport_error: ConnectionError | OSError | ValueError | None = None
|
|
170
|
+
msg: dict | None = None
|
|
171
|
+
try:
|
|
172
|
+
with _sigterm_as_system_exit():
|
|
173
|
+
request.executor_id = lease.executor_id
|
|
174
|
+
send_message(conn, request.to_dict())
|
|
175
|
+
sent = True
|
|
176
|
+
msg = recv_message(conn)
|
|
177
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
|
178
|
+
interrupted = e
|
|
179
|
+
except (ConnectionError, OSError, ValueError) as e:
|
|
180
|
+
transport_error = e
|
|
181
|
+
finally:
|
|
182
|
+
try:
|
|
183
|
+
conn.close()
|
|
184
|
+
except OSError:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Close the data plane before asking the daemon to wait for process death.
|
|
188
|
+
if interrupted is not None:
|
|
189
|
+
if sent:
|
|
190
|
+
_best_effort_recycle(sess, lease.executor_id)
|
|
191
|
+
raise interrupted
|
|
192
|
+
if transport_error is not None:
|
|
193
|
+
recycle_error: Exception | None = None
|
|
194
|
+
if sent:
|
|
195
|
+
try:
|
|
196
|
+
_confirm_recycled(sess, lease.executor_id)
|
|
197
|
+
except Exception as cleanup_error: # noqa: BLE001
|
|
198
|
+
recycle_error = cleanup_error
|
|
199
|
+
suffix = (
|
|
200
|
+
f"; executor reap could not be confirmed: {recycle_error}"
|
|
201
|
+
if recycle_error is not None
|
|
202
|
+
else ""
|
|
203
|
+
)
|
|
204
|
+
raise ExecutorUnavailable(
|
|
205
|
+
f"executor data-plane error on {lease.sock_path!r}: "
|
|
206
|
+
f"{transport_error}{suffix}"
|
|
207
|
+
) from transport_error
|
|
208
|
+
if msg is None:
|
|
209
|
+
raise ExecutorUnavailable(
|
|
210
|
+
f"executor returned no response on {lease.sock_path!r}"
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
response = ExecuteResponse.from_dict(msg)
|
|
214
|
+
except (TypeError, ValueError) as e:
|
|
215
|
+
try:
|
|
216
|
+
_confirm_recycled(sess, lease.executor_id)
|
|
217
|
+
except Exception as cleanup_error: # noqa: BLE001
|
|
218
|
+
raise ExecutorUnavailable(
|
|
219
|
+
"executor returned a malformed response and its reap could "
|
|
220
|
+
f"not be confirmed: {cleanup_error}"
|
|
221
|
+
) from e
|
|
222
|
+
raise ExecutorUnavailable(
|
|
223
|
+
f"executor returned a malformed response: {e}"
|
|
224
|
+
) from e
|
|
225
|
+
if response.terminal_reason is not None:
|
|
226
|
+
_confirm_recycled(sess, lease.executor_id)
|
|
227
|
+
return response
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _confirm_recycled(sess, executor_id: str) -> dict:
|
|
231
|
+
"""Ask the daemon to reap one exact executor and wait for process death."""
|
|
232
|
+
sid = _session_id(sess)
|
|
233
|
+
result = sess.cdp.send(
|
|
234
|
+
"BrowserwrightDaemon.killExecutor",
|
|
235
|
+
bsSession=sid,
|
|
236
|
+
executorId=executor_id,
|
|
237
|
+
wait=True,
|
|
238
|
+
)
|
|
239
|
+
if not isinstance(result, dict) or result.get("reaped") is not True:
|
|
240
|
+
raise ExecutorUnavailable(
|
|
241
|
+
f"daemon did not confirm executor {executor_id!r} was reaped: {result!r}"
|
|
242
|
+
)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _best_effort_recycle(sess, executor_id: str) -> None:
|
|
247
|
+
try:
|
|
248
|
+
_confirm_recycled(sess, executor_id)
|
|
249
|
+
except BaseException: # noqa: BLE001 - cannot mask the original interrupt
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@contextmanager
|
|
254
|
+
def _sigterm_as_system_exit() -> Iterator[None]:
|
|
255
|
+
"""Make SIGTERM unwind the blocking recv so exact-instance reap is tried."""
|
|
256
|
+
if (
|
|
257
|
+
not hasattr(signal, "SIGTERM")
|
|
258
|
+
or threading.current_thread() is not threading.main_thread()
|
|
259
|
+
):
|
|
260
|
+
yield
|
|
261
|
+
return
|
|
262
|
+
previous = signal.getsignal(signal.SIGTERM)
|
|
263
|
+
|
|
264
|
+
def _handle(signum, _frame):
|
|
265
|
+
raise SystemExit(128 + int(signum))
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
signal.signal(signal.SIGTERM, _handle)
|
|
269
|
+
yield
|
|
270
|
+
finally:
|
|
271
|
+
signal.signal(signal.SIGTERM, previous)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _connect(sock_path: str, *, timeout: float = 30.0,
|
|
275
|
+
retry_until: float = 5.0) -> socket.socket:
|
|
276
|
+
"""Connect the executor's unix socket, briefly retrying a not-yet-bound
|
|
277
|
+
socket (the daemon returns the path the moment it spawns; the bind may race
|
|
278
|
+
by a few ms)."""
|
|
279
|
+
deadline = time.monotonic() + retry_until
|
|
280
|
+
last: OSError | None = None
|
|
281
|
+
while True:
|
|
282
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
283
|
+
s.settimeout(timeout)
|
|
284
|
+
try:
|
|
285
|
+
s.connect(sock_path)
|
|
286
|
+
return s
|
|
287
|
+
except OSError as e:
|
|
288
|
+
last = e
|
|
289
|
+
s.close()
|
|
290
|
+
if time.monotonic() >= deadline:
|
|
291
|
+
raise ExecutorUnavailable(
|
|
292
|
+
f"could not connect executor socket {sock_path!r}: {e}"
|
|
293
|
+
) from last
|
|
294
|
+
time.sleep(0.05)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _session_id(sess) -> str:
|
|
298
|
+
rec = getattr(sess, "session_record", None)
|
|
299
|
+
if isinstance(rec, dict) and rec.get("id"):
|
|
300
|
+
return str(rec["id"])
|
|
301
|
+
raise ExecutorUnavailable("no session id bound; cannot reach an executor")
|