osn-selenium 0.0.0__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.
- osn_selenium/__init__.py +1 -0
- osn_selenium/browsers_handler/__init__.py +70 -0
- osn_selenium/browsers_handler/_windows.py +130 -0
- osn_selenium/browsers_handler/types.py +20 -0
- osn_selenium/captcha_workers/__init__.py +26 -0
- osn_selenium/dev_tools/__init__.py +1 -0
- osn_selenium/dev_tools/_types.py +22 -0
- osn_selenium/dev_tools/domains/__init__.py +63 -0
- osn_selenium/dev_tools/domains/abstract.py +378 -0
- osn_selenium/dev_tools/domains/fetch.py +1295 -0
- osn_selenium/dev_tools/domains_default/__init__.py +1 -0
- osn_selenium/dev_tools/domains_default/fetch.py +155 -0
- osn_selenium/dev_tools/errors.py +89 -0
- osn_selenium/dev_tools/logger.py +558 -0
- osn_selenium/dev_tools/manager.py +1551 -0
- osn_selenium/dev_tools/utils.py +509 -0
- osn_selenium/errors.py +16 -0
- osn_selenium/types.py +118 -0
- osn_selenium/webdrivers/BaseDriver/__init__.py +1 -0
- osn_selenium/webdrivers/BaseDriver/_utils.py +37 -0
- osn_selenium/webdrivers/BaseDriver/flags.py +644 -0
- osn_selenium/webdrivers/BaseDriver/protocols.py +2135 -0
- osn_selenium/webdrivers/BaseDriver/trio_wrapper.py +71 -0
- osn_selenium/webdrivers/BaseDriver/webdriver.py +2626 -0
- osn_selenium/webdrivers/Blink/__init__.py +1 -0
- osn_selenium/webdrivers/Blink/flags.py +1349 -0
- osn_selenium/webdrivers/Blink/protocols.py +330 -0
- osn_selenium/webdrivers/Blink/webdriver.py +637 -0
- osn_selenium/webdrivers/Chrome/__init__.py +1 -0
- osn_selenium/webdrivers/Chrome/flags.py +192 -0
- osn_selenium/webdrivers/Chrome/protocols.py +228 -0
- osn_selenium/webdrivers/Chrome/webdriver.py +394 -0
- osn_selenium/webdrivers/Edge/__init__.py +1 -0
- osn_selenium/webdrivers/Edge/flags.py +192 -0
- osn_selenium/webdrivers/Edge/protocols.py +228 -0
- osn_selenium/webdrivers/Edge/webdriver.py +394 -0
- osn_selenium/webdrivers/Yandex/__init__.py +1 -0
- osn_selenium/webdrivers/Yandex/flags.py +192 -0
- osn_selenium/webdrivers/Yandex/protocols.py +211 -0
- osn_selenium/webdrivers/Yandex/webdriver.py +350 -0
- osn_selenium/webdrivers/__init__.py +1 -0
- osn_selenium/webdrivers/_functions.py +504 -0
- osn_selenium/webdrivers/js_scripts/check_element_in_viewport.js +18 -0
- osn_selenium/webdrivers/js_scripts/get_document_scroll_size.js +4 -0
- osn_selenium/webdrivers/js_scripts/get_element_css.js +6 -0
- osn_selenium/webdrivers/js_scripts/get_element_rect_in_viewport.js +9 -0
- osn_selenium/webdrivers/js_scripts/get_random_element_point_in_viewport.js +59 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_position.js +4 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_rect.js +6 -0
- osn_selenium/webdrivers/js_scripts/get_viewport_size.js +4 -0
- osn_selenium/webdrivers/js_scripts/open_new_tab.js +1 -0
- osn_selenium/webdrivers/js_scripts/stop_window_loading.js +1 -0
- osn_selenium/webdrivers/types.py +390 -0
- osn_selenium-0.0.0.dist-info/METADATA +710 -0
- osn_selenium-0.0.0.dist-info/RECORD +57 -0
- osn_selenium-0.0.0.dist-info/WHEEL +5 -0
- osn_selenium-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
import trio
|
|
2
|
+
import inspect
|
|
3
|
+
import warnings
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from contextlib import (
|
|
9
|
+
AbstractAsyncContextManager
|
|
10
|
+
)
|
|
11
|
+
from selenium.webdriver.remote.bidi_connection import BidiConnection
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Awaitable,
|
|
15
|
+
Callable,
|
|
16
|
+
Optional,
|
|
17
|
+
TYPE_CHECKING,
|
|
18
|
+
Union
|
|
19
|
+
)
|
|
20
|
+
from osn_selenium.dev_tools.domains.abstract import (
|
|
21
|
+
AbstractDomain,
|
|
22
|
+
AbstractEvent
|
|
23
|
+
)
|
|
24
|
+
from selenium.webdriver.common.bidi.cdp import (
|
|
25
|
+
BrowserError,
|
|
26
|
+
CdpSession,
|
|
27
|
+
open_cdp
|
|
28
|
+
)
|
|
29
|
+
from osn_selenium.dev_tools._types import (
|
|
30
|
+
LogLevelsType,
|
|
31
|
+
devtools_background_func_type
|
|
32
|
+
)
|
|
33
|
+
from osn_selenium.dev_tools.domains import (
|
|
34
|
+
Domains,
|
|
35
|
+
DomainsSettings,
|
|
36
|
+
domains_classes_type,
|
|
37
|
+
domains_type
|
|
38
|
+
)
|
|
39
|
+
from osn_selenium.dev_tools.errors import (
|
|
40
|
+
BidiConnectionNotEstablishedError,
|
|
41
|
+
CantEnterDevToolsContextError,
|
|
42
|
+
cdp_end_exceptions
|
|
43
|
+
)
|
|
44
|
+
from osn_selenium.dev_tools.logger import (
|
|
45
|
+
LogEntry,
|
|
46
|
+
LogLevelStats,
|
|
47
|
+
LoggerChannelStats,
|
|
48
|
+
LoggerSettings,
|
|
49
|
+
MainLogEntry,
|
|
50
|
+
MainLogger,
|
|
51
|
+
TargetLogger,
|
|
52
|
+
TargetTypeStats,
|
|
53
|
+
build_main_logger,
|
|
54
|
+
build_target_logger
|
|
55
|
+
)
|
|
56
|
+
from osn_selenium.dev_tools.utils import (
|
|
57
|
+
TargetData,
|
|
58
|
+
TargetFilter,
|
|
59
|
+
_background_task_decorator,
|
|
60
|
+
_prepare_log_dir,
|
|
61
|
+
_validate_type_filter,
|
|
62
|
+
execute_cdp_command,
|
|
63
|
+
extract_exception_trace,
|
|
64
|
+
log_exception,
|
|
65
|
+
log_on_error,
|
|
66
|
+
wait_one,
|
|
67
|
+
warn_if_active
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
from osn_selenium.webdrivers.BaseDriver.webdriver import BrowserWebDriver
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class DevToolsSettings:
|
|
77
|
+
"""
|
|
78
|
+
Settings for configuring the DevTools manager.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
new_targets_filter (Optional[Sequence[TargetFilter]]): A sequence of `TargetFilter` objects
|
|
82
|
+
to control which new browser targets (e.g., tabs, iframes) DevTools should discover and attach to.
|
|
83
|
+
Defaults to None, meaning all targets are considered.
|
|
84
|
+
new_targets_buffer_size (int): The buffer size for the Trio memory channel
|
|
85
|
+
used to receive new target events. A larger buffer can prevent `trio.WouldBlock`
|
|
86
|
+
errors under high event load. Defaults to 100.
|
|
87
|
+
target_background_task (Optional[devtools_background_func_type]): An optional asynchronous function
|
|
88
|
+
that will be run as a background task for each attached DevTools target. This can be used
|
|
89
|
+
for custom per-target logic. Defaults to None.
|
|
90
|
+
logger_settings (Optional[LoggerSettings]): Configuration settings for the internal logging system.
|
|
91
|
+
If None, default logging settings will be used (no file logging by default).
|
|
92
|
+
Defaults to None.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
new_targets_filter: Optional[Sequence[TargetFilter]] = None
|
|
96
|
+
new_targets_buffer_size: int = 100
|
|
97
|
+
target_background_task: devtools_background_func_type = None
|
|
98
|
+
logger_settings: Optional[LoggerSettings] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DevToolsTarget:
|
|
102
|
+
"""
|
|
103
|
+
Manages the DevTools Protocol session and event handling for a specific browser target.
|
|
104
|
+
|
|
105
|
+
Each `DevToolsTarget` instance represents a single CDP target (e.g., a browser tab,
|
|
106
|
+
an iframe, or a service worker) and handles its dedicated CDP session, event listeners,
|
|
107
|
+
and associated logging.
|
|
108
|
+
|
|
109
|
+
Attributes:
|
|
110
|
+
target_data (TargetData): Data describing the browser target.
|
|
111
|
+
_logger_settings (LoggerSettings): Logging configuration for this target.
|
|
112
|
+
devtools_package (Any): The DevTools protocol package (e.g., `selenium.webdriver.common.bidi.cdp.devtools`).
|
|
113
|
+
websocket_url (Optional[str]): The WebSocket URL for establishing the CDP connection.
|
|
114
|
+
_new_targets_filter (Optional[list[dict[str, Any]]]): Filter settings for discovering new targets.
|
|
115
|
+
_new_targets_buffer_size (int): Buffer size for new target events.
|
|
116
|
+
_domains (Domains): Configuration for DevTools domains and their event handlers.
|
|
117
|
+
_nursery_object (trio.Nursery): The Trio nursery for spawning concurrent tasks.
|
|
118
|
+
exit_event (trio.Event): An event signaling that the main DevTools context is exiting.
|
|
119
|
+
_target_type_log_accepted (bool): Indicates if this target's type is accepted by the logger filter.
|
|
120
|
+
_target_background_task (Optional[devtools_background_func_type]): An optional background task to run for this target.
|
|
121
|
+
_add_target_func (Callable[[Any], Awaitable[bool]]): Callback function to add new targets to the manager.
|
|
122
|
+
_remove_target_func (Callable[["DevToolsTarget"], Awaitable[bool]]): Callback function to remove targets from the manager.
|
|
123
|
+
_add_log_func (Callable[[LogEntry], Awaitable[None]]): Callback function to add log entries to the main logger.
|
|
124
|
+
started_event (trio.Event): An event set when the target's `run` method has started.
|
|
125
|
+
about_to_stop_event (trio.Event): An event set when the target is signaled to stop.
|
|
126
|
+
background_task_ended (Optional[trio.Event]): An event set when the target's background task completes.
|
|
127
|
+
stopped_event (trio.Event): An event set when the target's `run` method has fully stopped.
|
|
128
|
+
_log_stats (LoggerChannelStats): Statistics specific to this target's logging.
|
|
129
|
+
_logger_send_channel (Optional[trio.MemorySendChannel[LogEntry]]): Send channel for this target's logger.
|
|
130
|
+
_logger (Optional[TargetLogger]): The logger instance for this specific target.
|
|
131
|
+
_cdp_session (Optional[CdpSession]): The active CDP session for this target.
|
|
132
|
+
_new_target_receive_channel (Optional[tuple[trio.MemoryReceiveChannel[Any], trio.Event]]): Channel and event for new target events.
|
|
133
|
+
_events_receive_channels (dict[str, tuple[trio.MemoryReceiveChannel[Any], trio.Event]]): Channels and events for domain-specific events.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
target_data: TargetData,
|
|
139
|
+
logger_settings: LoggerSettings,
|
|
140
|
+
devtools_package: Any,
|
|
141
|
+
websocket_url: Optional[str],
|
|
142
|
+
new_targets_filter: list[dict[str, Any]],
|
|
143
|
+
new_targets_buffer_size: int,
|
|
144
|
+
domains: Domains,
|
|
145
|
+
nursery: trio.Nursery,
|
|
146
|
+
exit_event: trio.Event,
|
|
147
|
+
target_background_task: Optional[devtools_background_func_type],
|
|
148
|
+
add_target_func: Callable[[Any], Awaitable[bool]],
|
|
149
|
+
remove_target_func: Callable[["DevToolsTarget"], Awaitable[bool]],
|
|
150
|
+
add_log_func: Callable[[LogEntry], Awaitable[None]],
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Initializes a DevToolsTarget instance.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
target_data (TargetData): Initial data for this target.
|
|
157
|
+
logger_settings (LoggerSettings): Logging configuration.
|
|
158
|
+
devtools_package (Any): The DevTools protocol package.
|
|
159
|
+
websocket_url (Optional[str]): WebSocket URL for CDP connection.
|
|
160
|
+
new_targets_filter (Optional[list[dict[str, Any]]]): Filters for new targets.
|
|
161
|
+
new_targets_buffer_size (int): Buffer size for new target events.
|
|
162
|
+
domains (Domains): Configured DevTools domains.
|
|
163
|
+
nursery (trio.Nursery): The Trio nursery for tasks.
|
|
164
|
+
exit_event (trio.Event): Event to signal global exit.
|
|
165
|
+
target_background_task (Optional[devtools_background_func_type]): Optional background task.
|
|
166
|
+
add_target_func (Callable[[Any], Awaitable[Optional[bool]]]): Function to add new targets.
|
|
167
|
+
remove_target_func (Callable[["DevToolsTarget"], Awaitable[Optional[bool]]]): Function to remove targets.
|
|
168
|
+
add_log_func (Callable[[LogEntry], Awaitable[None]]): Function to add logs to main logger.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
self.target_data = target_data
|
|
172
|
+
self._logger_settings = logger_settings
|
|
173
|
+
self.devtools_package = devtools_package
|
|
174
|
+
self.websocket_url = websocket_url
|
|
175
|
+
self._new_targets_filter = new_targets_filter
|
|
176
|
+
self._new_targets_buffer_size = new_targets_buffer_size
|
|
177
|
+
self._domains = domains
|
|
178
|
+
self._nursery_object = nursery
|
|
179
|
+
self.exit_event = exit_event
|
|
180
|
+
|
|
181
|
+
self._target_type_log_accepted = _validate_type_filter(
|
|
182
|
+
self.type_,
|
|
183
|
+
self._logger_settings.target_type_filter_mode,
|
|
184
|
+
self._logger_settings.target_type_filter
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
self._target_background_task = target_background_task
|
|
188
|
+
self._add_target_func = add_target_func
|
|
189
|
+
self._remove_target_func = remove_target_func
|
|
190
|
+
self._add_log_func = add_log_func
|
|
191
|
+
self.started_event = trio.Event()
|
|
192
|
+
self.about_to_stop_event = trio.Event()
|
|
193
|
+
self.background_task_ended: Optional[trio.Event] = None
|
|
194
|
+
self.stopped_event = trio.Event()
|
|
195
|
+
|
|
196
|
+
self._log_stats = LoggerChannelStats(
|
|
197
|
+
target_id=target_data.target_id,
|
|
198
|
+
title=target_data.title,
|
|
199
|
+
url=target_data.url,
|
|
200
|
+
num_logs=0,
|
|
201
|
+
last_log_time=datetime.now(),
|
|
202
|
+
log_level_stats={}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self._logger_send_channel: Optional[trio.MemorySendChannel] = None
|
|
206
|
+
self._logger: Optional[TargetLogger] = None
|
|
207
|
+
self._cdp_session: Optional[CdpSession] = None
|
|
208
|
+
self._new_target_receive_channel: Optional[tuple[trio.MemoryReceiveChannel, trio.Event]] = None
|
|
209
|
+
self._events_receive_channels: dict[str, tuple[trio.MemoryReceiveChannel, trio.Event]] = {}
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def type_(self) -> Optional[str]:
|
|
213
|
+
"""
|
|
214
|
+
Gets the type of the target (e.g., "page", "iframe", "service_worker").
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Optional[str]: The type of the target, or None if not set.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
return self.target_data.type_
|
|
221
|
+
|
|
222
|
+
@type_.setter
|
|
223
|
+
def type_(self, value: Optional[str]) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Sets the type of the target and updates the logging acceptance flag.
|
|
226
|
+
|
|
227
|
+
When the type is updated, this setter also re-evaluates whether
|
|
228
|
+
this target's type should be accepted by the logging system's filters.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
value (Optional[str]): The new type string for the target, or None to clear it.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
self._target_type_log_accepted = _validate_type_filter(
|
|
235
|
+
value,
|
|
236
|
+
self._logger_settings.target_type_filter_mode,
|
|
237
|
+
self._logger_settings.target_type_filter
|
|
238
|
+
)
|
|
239
|
+
self.target_data.type_ = value
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def attached(self) -> Optional[bool]:
|
|
243
|
+
"""
|
|
244
|
+
Gets whether the DevTools session is currently attached to this target.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Optional[bool]: True if attached, False if not, or None if status is unknown.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
return self.target_data.attached
|
|
251
|
+
|
|
252
|
+
@attached.setter
|
|
253
|
+
def attached(self, value: Optional[bool]) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Sets whether the DevTools session is currently attached to this target.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
value (Optional[bool]): The new attached status (True, False, or None).
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
self.target_data.attached = value
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def browser_context_id(self) -> Optional[str]:
|
|
265
|
+
"""
|
|
266
|
+
Gets the ID of the browser context this target belongs to.
|
|
267
|
+
|
|
268
|
+
Browser contexts are isolated environments, often used for incognito mode
|
|
269
|
+
or separate user profiles.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Optional[str]: The ID of the browser context, or None if not associated
|
|
273
|
+
with a specific context.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
return self.target_data.browser_context_id
|
|
277
|
+
|
|
278
|
+
@browser_context_id.setter
|
|
279
|
+
def browser_context_id(self, value: Optional[str]) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Sets the ID of the browser context this target belongs to.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
value (Optional[str]): The new browser context ID string, or None to clear it.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
self.target_data.browser_context_id = value
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def can_access_opener(self) -> Optional[bool]:
|
|
291
|
+
"""
|
|
292
|
+
Gets whether the target can access its opener.
|
|
293
|
+
|
|
294
|
+
This property indicates if the target has permission to interact with
|
|
295
|
+
the target that opened it.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Optional[bool]: True if it can access the opener, False if not,
|
|
299
|
+
or None if the status is unknown.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
return self.target_data.can_access_opener
|
|
303
|
+
|
|
304
|
+
@can_access_opener.setter
|
|
305
|
+
def can_access_opener(self, value: Optional[bool]) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Sets whether the target can access its opener.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
value (Optional[bool]): The new status for opener access (True, False, or None).
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
self.target_data.can_access_opener = value
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def cdp_session(self) -> CdpSession:
|
|
317
|
+
"""
|
|
318
|
+
Gets the active Chrome DevTools Protocol (CDP) session for this target.
|
|
319
|
+
|
|
320
|
+
This session object is the primary interface for sending CDP commands
|
|
321
|
+
and receiving events specific to this target.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
CdpSession: The CDP session object associated with this target.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
return self._cdp_session
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def log_stats(self) -> LoggerChannelStats:
|
|
331
|
+
"""
|
|
332
|
+
Gets the logging statistics for this specific target channel.
|
|
333
|
+
|
|
334
|
+
This provides aggregated data such as total log count, last log time,
|
|
335
|
+
and per-level log counts for this target.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
LoggerChannelStats: An object containing the logging statistics for this target.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
return self._log_stats
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def opener_frame_id(self) -> Optional[str]:
|
|
345
|
+
"""
|
|
346
|
+
Gets the frame ID of the target that opened this one.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Optional[str]: The frame ID of the opener, or None if not applicable or known.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
return self.target_data.opener_frame_id
|
|
353
|
+
|
|
354
|
+
@opener_frame_id.setter
|
|
355
|
+
def opener_frame_id(self, value: Optional[str]) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Sets the frame ID of the target that opened this one.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
value (Optional[str]): The new opener frame ID string, or None to clear it.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
self.target_data.opener_frame_id = value
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def opener_id(self) -> Optional[str]:
|
|
367
|
+
"""
|
|
368
|
+
Gets the ID of the target that opened this one.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Optional[str]: The ID of the opener target, or None if not applicable or known.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
return self.target_data.opener_id
|
|
375
|
+
|
|
376
|
+
@opener_id.setter
|
|
377
|
+
def opener_id(self, value: Optional[str]) -> None:
|
|
378
|
+
"""
|
|
379
|
+
Sets the ID of the target that opened this one.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
value (Optional[str]): The new opener target ID string, or None to clear it.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
self.target_data.opener_id = value
|
|
386
|
+
|
|
387
|
+
async def stop(self):
|
|
388
|
+
"""
|
|
389
|
+
Signals the target to begin its shutdown process.
|
|
390
|
+
|
|
391
|
+
This sets the `about_to_stop_event`, which is used to gracefully
|
|
392
|
+
terminate ongoing tasks within the target's `run` method.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
self.about_to_stop_event.set()
|
|
396
|
+
|
|
397
|
+
async def _close_instances(self):
|
|
398
|
+
"""
|
|
399
|
+
Closes all associated instances and channels for this target.
|
|
400
|
+
|
|
401
|
+
This includes the new target receive channel, the logger send channel,
|
|
402
|
+
the target logger itself, and all event receive channels. It also waits
|
|
403
|
+
for the background task to end if one was started.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
if self._new_target_receive_channel is not None:
|
|
407
|
+
await self._new_target_receive_channel[0].aclose()
|
|
408
|
+
await self._new_target_receive_channel[1].wait()
|
|
409
|
+
|
|
410
|
+
self._new_target_receive_channel = None
|
|
411
|
+
|
|
412
|
+
if self._logger_send_channel is not None:
|
|
413
|
+
await self._logger_send_channel.aclose()
|
|
414
|
+
self._logger_send_channel = None
|
|
415
|
+
|
|
416
|
+
if self._logger is not None:
|
|
417
|
+
await self._logger.close()
|
|
418
|
+
self._logger = None
|
|
419
|
+
|
|
420
|
+
for channel in self._events_receive_channels.values():
|
|
421
|
+
await channel[0].aclose()
|
|
422
|
+
await channel[1].wait()
|
|
423
|
+
|
|
424
|
+
self._events_receive_channels = {}
|
|
425
|
+
|
|
426
|
+
if self.background_task_ended is not None:
|
|
427
|
+
await self.background_task_ended.wait()
|
|
428
|
+
self.background_task_ended = None
|
|
429
|
+
|
|
430
|
+
async def log_error(self, error: BaseException, extra_data: Optional[dict[str, Any]] = None):
|
|
431
|
+
"""
|
|
432
|
+
Logs an error message, including its traceback, to the relevant target's log file
|
|
433
|
+
and also logs it globally via the standard logging module.
|
|
434
|
+
|
|
435
|
+
This method formats the exception's traceback using `extract_exception_trace`
|
|
436
|
+
and sends it as an "ERROR" level log entry. It also calls `log_exception`
|
|
437
|
+
to ensure the error is processed by the default Python logging system.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
error (BaseException): The exception object to be logged.
|
|
441
|
+
extra_data (Optional[dict[str, Any]]): Optional additional data to include
|
|
442
|
+
with the error log entry.
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
await self.log(
|
|
446
|
+
level="ERROR",
|
|
447
|
+
message=extract_exception_trace(error),
|
|
448
|
+
source_function=" <- ".join(stack.function for stack in inspect.stack()[1:]),
|
|
449
|
+
extra_data=extra_data
|
|
450
|
+
)
|
|
451
|
+
log_exception(error)
|
|
452
|
+
|
|
453
|
+
async def get_devtools_object(self, path: str) -> Any:
|
|
454
|
+
"""
|
|
455
|
+
Navigates and retrieves a specific object within the DevTools API structure.
|
|
456
|
+
|
|
457
|
+
Using a dot-separated path, this method traverses the nested DevTools API objects to retrieve a target object.
|
|
458
|
+
For example, a path like "fetch.enable" would access `self.devtools_module.fetch.enable`.
|
|
459
|
+
Results are cached for faster access.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
path (str): A dot-separated string representing the path to the desired DevTools API object.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Any: The DevTools API object located at the specified path.
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
cdp_end_exceptions: If a CDP-related connection error occurs.
|
|
469
|
+
BaseException: If the object cannot be found or another error occurs during retrieval.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
package = self.devtools_package
|
|
474
|
+
|
|
475
|
+
for part in path.split("."):
|
|
476
|
+
package = getattr(package, part)
|
|
477
|
+
|
|
478
|
+
return package
|
|
479
|
+
except cdp_end_exceptions as error:
|
|
480
|
+
raise error
|
|
481
|
+
except BaseException as error:
|
|
482
|
+
await self.log_error(error=error)
|
|
483
|
+
raise error
|
|
484
|
+
|
|
485
|
+
async def _run_event_handler(
|
|
486
|
+
self,
|
|
487
|
+
domain_handler_ready_event: trio.Event,
|
|
488
|
+
event_config: AbstractEvent
|
|
489
|
+
):
|
|
490
|
+
"""
|
|
491
|
+
Runs a single DevTools event handler for a specific target.
|
|
492
|
+
|
|
493
|
+
This method sets up a listener for the specified CDP event and continuously
|
|
494
|
+
receives and dispatches events to the configured `handle_function`.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
domain_handler_ready_event (trio.Event): An event that will be set once the handler is started.
|
|
498
|
+
event_config (AbstractEvent): The configuration for the specific CDP event handler.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
cdp_end_exceptions: If a CDP-related connection error occurs during listener setup or event processing.
|
|
502
|
+
BaseException: If another unexpected error occurs during listener setup or event processing.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
await self.log_step(
|
|
506
|
+
message=f"Event handler '{event_config['class_to_use_path']}' starting."
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
receiver_channel: trio.MemoryReceiveChannel = self.cdp_session.listen(
|
|
511
|
+
await self.get_devtools_object(event_config["class_to_use_path"]),
|
|
512
|
+
buffer_size=event_config["listen_buffer_size"]
|
|
513
|
+
)
|
|
514
|
+
channel_stopped_event = trio.Event()
|
|
515
|
+
|
|
516
|
+
self._events_receive_channels[event_config["class_to_use_path"]] = (receiver_channel, channel_stopped_event)
|
|
517
|
+
|
|
518
|
+
domain_handler_ready_event.set()
|
|
519
|
+
handler = event_config["handle_function"]
|
|
520
|
+
except cdp_end_exceptions as error:
|
|
521
|
+
raise error
|
|
522
|
+
except BaseException as error:
|
|
523
|
+
await self.log_error(error=error)
|
|
524
|
+
raise error
|
|
525
|
+
|
|
526
|
+
await self.log_step(
|
|
527
|
+
message=f"Event handler '{event_config['class_to_use_path']}' started."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
keep_alive = True
|
|
531
|
+
while keep_alive:
|
|
532
|
+
try:
|
|
533
|
+
event = await receiver_channel.receive()
|
|
534
|
+
self._nursery_object.start_soon(handler, self, event_config, event)
|
|
535
|
+
except* cdp_end_exceptions:
|
|
536
|
+
keep_alive = False
|
|
537
|
+
except* BaseException as error:
|
|
538
|
+
await self.log_error(error=error)
|
|
539
|
+
keep_alive = False
|
|
540
|
+
|
|
541
|
+
channel_stopped_event.set()
|
|
542
|
+
|
|
543
|
+
async def _run_events_handlers(self, events_ready_event: trio.Event, domain_config: AbstractDomain):
|
|
544
|
+
"""
|
|
545
|
+
Runs all configured event handlers for a specific DevTools domain within a target.
|
|
546
|
+
|
|
547
|
+
This method iterates through the event configurations for a given domain and
|
|
548
|
+
starts a separate task for each event handler.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
events_ready_event (trio.Event): An event that will be set once all domain handlers are started.
|
|
552
|
+
domain_config (AbstractDomain): The configuration for the DevTools domain.
|
|
553
|
+
|
|
554
|
+
Raises:
|
|
555
|
+
cdp_end_exceptions: If a CDP-related connection error occurs during handler setup.
|
|
556
|
+
BaseException: If another unexpected error occurs during the setup of any event handler.
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
await self.log_step(
|
|
560
|
+
message=f"Domain '{domain_config['name']}' events handlers setup started."
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
events_handlers_ready_events: list[trio.Event] = []
|
|
565
|
+
|
|
566
|
+
for event_name, event_config in domain_config.get("handlers", {}).items():
|
|
567
|
+
if event_config is not None:
|
|
568
|
+
event_handler_ready_event = trio.Event()
|
|
569
|
+
events_handlers_ready_events.append(event_handler_ready_event)
|
|
570
|
+
|
|
571
|
+
self._nursery_object.start_soon(self._run_event_handler, event_handler_ready_event, event_config)
|
|
572
|
+
|
|
573
|
+
for event_handler_ready_event in events_handlers_ready_events:
|
|
574
|
+
await event_handler_ready_event.wait()
|
|
575
|
+
|
|
576
|
+
events_ready_event.set()
|
|
577
|
+
|
|
578
|
+
await self.log_step(
|
|
579
|
+
message=f"Domain '{domain_config['name']}' events handlers setup complete."
|
|
580
|
+
)
|
|
581
|
+
except* cdp_end_exceptions as error:
|
|
582
|
+
raise error
|
|
583
|
+
except* BaseException as error:
|
|
584
|
+
await self.log_error(error=error)
|
|
585
|
+
raise error
|
|
586
|
+
|
|
587
|
+
async def _run_new_targets_listener(self, new_targets_listener_ready_event: trio.Event):
|
|
588
|
+
"""
|
|
589
|
+
Runs a listener for new browser targets (e.g., new tabs, iframes).
|
|
590
|
+
|
|
591
|
+
This method continuously listens for `TargetCreated`, `AttachedToTarget`, and
|
|
592
|
+
`TargetInfoChanged` events, and spawns a new task to handle each new target.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
new_targets_listener_ready_event (trio.Event): An event that will be set once the listener is started.
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
cdp_end_exceptions: If a CDP-related connection error occurs during listener setup or event processing.
|
|
599
|
+
BaseException: If another unexpected error occurs during listener setup or event processing.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
await self.log_step(message="New Targets listener starting.")
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
self._new_target_receive_channel: tuple[trio.MemoryReceiveChannel, trio.Event] = (
|
|
606
|
+
self.cdp_session.listen(
|
|
607
|
+
await self.get_devtools_object("target.TargetCreated"),
|
|
608
|
+
await self.get_devtools_object("target.AttachedToTarget"),
|
|
609
|
+
await self.get_devtools_object("target.TargetInfoChanged"),
|
|
610
|
+
buffer_size=self._new_targets_buffer_size
|
|
611
|
+
),
|
|
612
|
+
trio.Event()
|
|
613
|
+
)
|
|
614
|
+
new_targets_listener_ready_event.set()
|
|
615
|
+
except cdp_end_exceptions as error:
|
|
616
|
+
raise error
|
|
617
|
+
except BaseException as error:
|
|
618
|
+
await self.log_error(error=error)
|
|
619
|
+
raise error
|
|
620
|
+
|
|
621
|
+
await self.log_step(message="New Targets listener started.")
|
|
622
|
+
|
|
623
|
+
keep_alive = True
|
|
624
|
+
while keep_alive:
|
|
625
|
+
try:
|
|
626
|
+
event = await self._new_target_receive_channel[0].receive()
|
|
627
|
+
self._nursery_object.start_soon(self._add_target_func, event)
|
|
628
|
+
except* cdp_end_exceptions:
|
|
629
|
+
keep_alive = False
|
|
630
|
+
except* BaseException as error:
|
|
631
|
+
await self.log_error(error=error)
|
|
632
|
+
keep_alive = False
|
|
633
|
+
|
|
634
|
+
self._new_target_receive_channel[1].set()
|
|
635
|
+
|
|
636
|
+
async def _setup_new_targets_attaching(self):
|
|
637
|
+
"""
|
|
638
|
+
Configures the DevTools protocol to discover and auto-attach to new targets.
|
|
639
|
+
|
|
640
|
+
This method uses `target.setDiscoverTargets` and `target.setAutoAttach`
|
|
641
|
+
to ensure that new browser contexts (like new tabs or iframes) are
|
|
642
|
+
automatically detected and attached to, allowing DevTools to manage them.
|
|
643
|
+
|
|
644
|
+
Raises:
|
|
645
|
+
cdp_end_exceptions: If a CDP-related connection error occurs during setup.
|
|
646
|
+
BaseException: If another unexpected error occurs while setting up target discovery or auto-attachment.
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
target_filter = (await self.get_devtools_object("target.TargetFilter"))(self._new_targets_filter) if self._new_targets_filter is not None else None
|
|
651
|
+
|
|
652
|
+
await execute_cdp_command(
|
|
653
|
+
self,
|
|
654
|
+
"log",
|
|
655
|
+
await self.get_devtools_object("target.set_discover_targets"),
|
|
656
|
+
discover=True,
|
|
657
|
+
filter_=target_filter,
|
|
658
|
+
)
|
|
659
|
+
await execute_cdp_command(
|
|
660
|
+
self,
|
|
661
|
+
"log",
|
|
662
|
+
await self.get_devtools_object("target.set_auto_attach"),
|
|
663
|
+
auto_attach=True,
|
|
664
|
+
wait_for_debugger_on_start=True,
|
|
665
|
+
flatten=True,
|
|
666
|
+
filter_=target_filter,
|
|
667
|
+
)
|
|
668
|
+
except cdp_end_exceptions as error:
|
|
669
|
+
raise error
|
|
670
|
+
except BaseException as error:
|
|
671
|
+
await self.log_error(error=error)
|
|
672
|
+
raise error
|
|
673
|
+
|
|
674
|
+
async def _setup_target(self):
|
|
675
|
+
"""
|
|
676
|
+
Sets up a new browser target for DevTools interaction.
|
|
677
|
+
|
|
678
|
+
This involves enabling target discovery and auto-attachment, and
|
|
679
|
+
starting event handlers for configured DevTools domains within the target's session.
|
|
680
|
+
|
|
681
|
+
Raises:
|
|
682
|
+
cdp_end_exceptions: If a CDP-related connection error occurs during setup.
|
|
683
|
+
BaseException: If any other unexpected error occurs during the target setup process.
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
await self.log_step(message="Target setup started.")
|
|
688
|
+
|
|
689
|
+
await self._setup_new_targets_attaching()
|
|
690
|
+
|
|
691
|
+
target_ready_events: list[trio.Event] = []
|
|
692
|
+
|
|
693
|
+
new_targets_listener_ready_event = trio.Event()
|
|
694
|
+
target_ready_events.append(new_targets_listener_ready_event)
|
|
695
|
+
|
|
696
|
+
self._nursery_object.start_soon(self._run_new_targets_listener, new_targets_listener_ready_event)
|
|
697
|
+
|
|
698
|
+
for domain_name, domain_config in self._domains.items():
|
|
699
|
+
if domain_config.get("enable_func_path", None) is not None:
|
|
700
|
+
enable_func_kwargs = domain_config.get("enable_func_kwargs", {})
|
|
701
|
+
await execute_cdp_command(
|
|
702
|
+
self,
|
|
703
|
+
"raise",
|
|
704
|
+
await self.get_devtools_object(domain_config["enable_func_path"]),
|
|
705
|
+
**enable_func_kwargs
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
domain_handlers_ready_event = trio.Event()
|
|
709
|
+
target_ready_events.append(domain_handlers_ready_event)
|
|
710
|
+
self._nursery_object.start_soon(self._run_events_handlers, domain_handlers_ready_event, domain_config)
|
|
711
|
+
|
|
712
|
+
for domain_handlers_ready_event in target_ready_events:
|
|
713
|
+
await domain_handlers_ready_event.wait()
|
|
714
|
+
|
|
715
|
+
await execute_cdp_command(
|
|
716
|
+
self,
|
|
717
|
+
"log",
|
|
718
|
+
await self.get_devtools_object("runtime.run_if_waiting_for_debugger")
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
await self.log_step(message="Target setup complete.")
|
|
722
|
+
except* cdp_end_exceptions as error:
|
|
723
|
+
raise error
|
|
724
|
+
except* BaseException as error:
|
|
725
|
+
await self.log_error(error=error)
|
|
726
|
+
raise error
|
|
727
|
+
|
|
728
|
+
@property
|
|
729
|
+
def target_id(self) -> Optional[str]:
|
|
730
|
+
"""
|
|
731
|
+
Gets the unique identifier for the target.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
Optional[str]: The unique ID of the target, or None if not set.
|
|
735
|
+
"""
|
|
736
|
+
|
|
737
|
+
return self.target_data.target_id
|
|
738
|
+
|
|
739
|
+
@target_id.setter
|
|
740
|
+
def target_id(self, value: Optional[str]) -> None:
|
|
741
|
+
"""
|
|
742
|
+
Sets the unique identifier for the target and updates associated log statistics.
|
|
743
|
+
|
|
744
|
+
When the target ID is updated, this setter ensures that the `target_data`
|
|
745
|
+
object reflects the new ID and that the `_log_stats` object
|
|
746
|
+
(which tracks per-channel statistics) is also updated.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
value (Optional[str]): The new unique ID string to set, or None to clear it.
|
|
750
|
+
"""
|
|
751
|
+
|
|
752
|
+
self._log_stats.target_id = value
|
|
753
|
+
self.target_data.target_id = value
|
|
754
|
+
|
|
755
|
+
async def run(self):
|
|
756
|
+
"""
|
|
757
|
+
Runs the DevTools session for this target, handling its lifecycle.
|
|
758
|
+
|
|
759
|
+
This method establishes the CDP session, sets up event listeners,
|
|
760
|
+
runs the optional background task, and waits for a stop signal.
|
|
761
|
+
It handles various exceptions during its lifecycle, logging them
|
|
762
|
+
and ensuring graceful shutdown.
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
self._logger_send_channel, self._logger = build_target_logger(self.target_data, self._nursery_object, self._logger_settings)
|
|
767
|
+
|
|
768
|
+
if self._target_type_log_accepted:
|
|
769
|
+
await self._logger.run()
|
|
770
|
+
|
|
771
|
+
await self.log_step(message=f"Target '{self.target_id}' added.")
|
|
772
|
+
|
|
773
|
+
async with open_cdp(self.websocket_url) as new_connection:
|
|
774
|
+
async with new_connection.open_session(self.target_id) as new_session:
|
|
775
|
+
self._cdp_session = new_session
|
|
776
|
+
|
|
777
|
+
await self._setup_target()
|
|
778
|
+
|
|
779
|
+
if self._target_background_task is not None:
|
|
780
|
+
self._nursery_object.start_soon(_background_task_decorator(self._target_background_task), self)
|
|
781
|
+
|
|
782
|
+
await wait_one(self.exit_event, self.about_to_stop_event)
|
|
783
|
+
except* (BrowserError, RuntimeError):
|
|
784
|
+
self.about_to_stop_event.set()
|
|
785
|
+
except* cdp_end_exceptions:
|
|
786
|
+
self.about_to_stop_event.set()
|
|
787
|
+
except* BaseException as error:
|
|
788
|
+
self.about_to_stop_event.set()
|
|
789
|
+
await self.log_error(error=error)
|
|
790
|
+
finally:
|
|
791
|
+
await self._close_instances()
|
|
792
|
+
await self._remove_target_func(self)
|
|
793
|
+
self.stopped_event.set()
|
|
794
|
+
|
|
795
|
+
@property
|
|
796
|
+
def subtype(self) -> Optional[str]:
|
|
797
|
+
"""
|
|
798
|
+
Gets the subtype of the target, if applicable.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Optional[str]: The subtype of the target, or None if not set.
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
return self.target_data.subtype
|
|
805
|
+
|
|
806
|
+
@subtype.setter
|
|
807
|
+
def subtype(self, value: Optional[str]) -> None:
|
|
808
|
+
"""
|
|
809
|
+
Sets the subtype of the target.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
value (Optional[str]): The new subtype string to set, or None to clear it.
|
|
813
|
+
"""
|
|
814
|
+
|
|
815
|
+
self.target_data.subtype = value
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def target_type_log_accepted(self) -> bool:
|
|
819
|
+
"""
|
|
820
|
+
Checks if this target's type is accepted by the logger's filter.
|
|
821
|
+
|
|
822
|
+
This property reflects whether log entries originating from this target's
|
|
823
|
+
type are configured to be processed by the logging system.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
bool: True if the target's type is accepted for logging, False otherwise.
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
return self._target_type_log_accepted
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def title(self) -> Optional[str]:
|
|
833
|
+
"""
|
|
834
|
+
Gets the title of the target (e.g., the page title).
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
Optional[str]: The current title of the target, or None if not available.
|
|
838
|
+
"""
|
|
839
|
+
|
|
840
|
+
return self.target_data.title
|
|
841
|
+
|
|
842
|
+
@title.setter
|
|
843
|
+
def title(self, value: Optional[str]) -> None:
|
|
844
|
+
"""
|
|
845
|
+
Sets the title of the target and updates associated log statistics.
|
|
846
|
+
|
|
847
|
+
When the title is updated, this setter ensures that the `target_data`
|
|
848
|
+
object reflects the new title and that the `_log_stats` object
|
|
849
|
+
(which tracks per-channel statistics) is also updated.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
value (Optional[str]): The new title string to set, or None to clear it.
|
|
853
|
+
"""
|
|
854
|
+
|
|
855
|
+
self._log_stats.title = value
|
|
856
|
+
self.target_data.title = value
|
|
857
|
+
|
|
858
|
+
@property
|
|
859
|
+
def url(self) -> Optional[str]:
|
|
860
|
+
"""
|
|
861
|
+
Gets the URL of the target.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Optional[str]: The current URL of the target, or None if not available.
|
|
865
|
+
"""
|
|
866
|
+
|
|
867
|
+
return self.target_data.url
|
|
868
|
+
|
|
869
|
+
@url.setter
|
|
870
|
+
def url(self, value: Optional[str]) -> None:
|
|
871
|
+
"""
|
|
872
|
+
Sets the URL of the target and updates associated log statistics.
|
|
873
|
+
|
|
874
|
+
When the URL is updated, this setter ensures that the `target_data`
|
|
875
|
+
object reflects the new URL and that the `_log_stats` object
|
|
876
|
+
(which tracks per-channel statistics) is also updated.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
value (Optional[str]): The new URL string to set, or None to clear it.
|
|
880
|
+
"""
|
|
881
|
+
|
|
882
|
+
self._log_stats.url = value
|
|
883
|
+
self.target_data.url = value
|
|
884
|
+
|
|
885
|
+
async def log(
|
|
886
|
+
self,
|
|
887
|
+
level: LogLevelsType,
|
|
888
|
+
message: str,
|
|
889
|
+
source_function: Optional[str] = None,
|
|
890
|
+
extra_data: Optional[dict[str, Any]] = None
|
|
891
|
+
):
|
|
892
|
+
"""
|
|
893
|
+
Logs a message to the internal logger manager, automatically determining the source function.
|
|
894
|
+
|
|
895
|
+
This method acts as a convenient wrapper around the underlying `_logger.log` method.
|
|
896
|
+
If `source_function` is not explicitly provided, it automatically determines the
|
|
897
|
+
calling function's name from the call stack to enrich the log entry.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
level (LogLevelsType): The severity level of the log (e.g., "INFO", "ERROR").
|
|
901
|
+
message (str): The main log message.
|
|
902
|
+
source_function (Optional[str]): The name of the function that generated the log.
|
|
903
|
+
If None, the function will attempt to determine it from the call stack.
|
|
904
|
+
extra_data (Optional[dict[str, Any]]): Optional additional data to associate
|
|
905
|
+
with the log entry.
|
|
906
|
+
"""
|
|
907
|
+
|
|
908
|
+
log_entry = LogEntry(
|
|
909
|
+
target_data=self.target_data,
|
|
910
|
+
message=message,
|
|
911
|
+
level=level,
|
|
912
|
+
timestamp=datetime.now(),
|
|
913
|
+
source_function=" <- ".join(stack.function for stack in inspect.stack()[1:])
|
|
914
|
+
if source_function is None
|
|
915
|
+
else source_function,
|
|
916
|
+
extra_data=extra_data
|
|
917
|
+
)
|
|
918
|
+
await self._add_log_func(log_entry)
|
|
919
|
+
|
|
920
|
+
if self._target_type_log_accepted and self._logger is not None and self._logger_send_channel is not None:
|
|
921
|
+
await self._log_stats.add_log(log_entry)
|
|
922
|
+
await self._logger.run()
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
self._logger_send_channel.send_nowait(log_entry)
|
|
926
|
+
except trio.WouldBlock:
|
|
927
|
+
warnings.warn(
|
|
928
|
+
f"WARNING: Log channel for session {self.target_id} is full. Log dropped:\n{log_entry.to_string()}"
|
|
929
|
+
)
|
|
930
|
+
except trio.BrokenResourceError:
|
|
931
|
+
warnings.warn(
|
|
932
|
+
f"WARNING: Log channel for session {self.target_id} is broken. Log dropped:\n{log_entry.to_string()}"
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
async def log_step(self, message: str):
|
|
936
|
+
"""
|
|
937
|
+
Logs an informational step message using the internal logger manager.
|
|
938
|
+
|
|
939
|
+
This is a convenience method for logging "INFO" level messages,
|
|
940
|
+
automatically determining the source function from the call stack.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
message (str): The step message to log.
|
|
944
|
+
"""
|
|
945
|
+
|
|
946
|
+
await self.log(
|
|
947
|
+
level="INFO",
|
|
948
|
+
message=message,
|
|
949
|
+
source_function=" <- ".join(stack.function for stack in inspect.stack()[1:])
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
class DevTools:
|
|
954
|
+
"""
|
|
955
|
+
Base class for handling DevTools functionalities in Selenium WebDriver.
|
|
956
|
+
|
|
957
|
+
Provides an interface to interact with Chrome DevTools Protocol (CDP)
|
|
958
|
+
for advanced browser control and monitoring. This class supports event handling
|
|
959
|
+
and allows for dynamic modifications of browser behavior, such as network request interception,
|
|
960
|
+
by using an asynchronous context manager.
|
|
961
|
+
|
|
962
|
+
Attributes:
|
|
963
|
+
_webdriver ("BrowserWebDriver"): The parent WebDriver instance associated with this DevTools instance.
|
|
964
|
+
_new_targets_filter (Optional[list[dict[str, Any]]]): Processed filters for new targets.
|
|
965
|
+
_new_targets_buffer_size (int): Buffer size for new target events.
|
|
966
|
+
_target_background_task (Optional[devtools_background_func_type]): Optional background task for targets.
|
|
967
|
+
_logger_settings (LoggerSettings): Logging configuration for the entire DevTools manager.
|
|
968
|
+
_bidi_connection (Optional[AbstractAsyncContextManager[BidiConnection, Any]]): Asynchronous context manager for the BiDi connection.
|
|
969
|
+
_bidi_connection_object (Optional[BidiConnection]): The BiDi connection object when active.
|
|
970
|
+
_nursery (Optional[AbstractAsyncContextManager[trio.Nursery, object]]): Asynchronous context manager for the Trio nursery.
|
|
971
|
+
_nursery_object (Optional[trio.Nursery]): The Trio nursery object when active, managing concurrent tasks.
|
|
972
|
+
_domains_settings (Domains): Settings for configuring DevTools domain handlers.
|
|
973
|
+
_handling_targets (dict[str, DevToolsTarget]): Dictionary of target IDs currently being handled by event listeners.
|
|
974
|
+
targets_lock (trio.Lock): A lock used for synchronizing access to shared resources, like the list of handled targets.
|
|
975
|
+
exit_event (Optional[trio.Event]): Trio Event to signal exiting of DevTools event handling.
|
|
976
|
+
_is_active (bool): Flag indicating if the DevTools event handler is currently active.
|
|
977
|
+
_is_closing (bool): Flag indicating if the DevTools manager is in the process of closing.
|
|
978
|
+
_num_logs (int): Total count of all log entries across all targets.
|
|
979
|
+
_targets_types_stats (dict[str, TargetTypeStats]): Statistics for each target type.
|
|
980
|
+
_log_level_stats (dict[str, LogLevelStats]): Overall statistics for each log level.
|
|
981
|
+
_main_logger (Optional[MainLogger]): The main logger instance.
|
|
982
|
+
_main_logger_send_channel (Optional[trio.MemorySendChannel[MainLogEntry]]): Send channel for the main logger.
|
|
983
|
+
|
|
984
|
+
EXAMPLES
|
|
985
|
+
________
|
|
986
|
+
>>> from osn_selenium.webdrivers.Chrome.webdriver import ChromeWebDriver
|
|
987
|
+
... from osn_selenium.dev_tools.domains import DomainsSettings
|
|
988
|
+
...
|
|
989
|
+
... async def main():
|
|
990
|
+
... driver = ChromeWebDriver("path/to/chromedriver")
|
|
991
|
+
... driver.dev_tools.set_domains_handlers(DomainsSettings(...))
|
|
992
|
+
...
|
|
993
|
+
... driver_wrapper = driver.to_wrapper()
|
|
994
|
+
...
|
|
995
|
+
... # Configure domain handlers here.
|
|
996
|
+
... async with driver.dev_tools:
|
|
997
|
+
... # DevTools event handling is active within this block.
|
|
998
|
+
... await driver_wrapper.search_url("https://example.com")
|
|
999
|
+
... # DevTools event handling is deactivated after exiting the block.
|
|
1000
|
+
"""
|
|
1001
|
+
|
|
1002
|
+
def __init__(
|
|
1003
|
+
self,
|
|
1004
|
+
parent_webdriver: "BrowserWebDriver",
|
|
1005
|
+
devtools_settings: Optional[DevToolsSettings] = None
|
|
1006
|
+
):
|
|
1007
|
+
"""
|
|
1008
|
+
Initializes the DevTools manager.
|
|
1009
|
+
|
|
1010
|
+
Args:
|
|
1011
|
+
parent_webdriver ("BrowserWebDriver"): The WebDriver instance to which this DevTools manager is attached.
|
|
1012
|
+
devtools_settings (Optional[DevToolsSettings]): Configuration settings for DevTools.
|
|
1013
|
+
If None, default settings will be used.
|
|
1014
|
+
"""
|
|
1015
|
+
|
|
1016
|
+
if devtools_settings is None:
|
|
1017
|
+
devtools_settings = DevToolsSettings()
|
|
1018
|
+
|
|
1019
|
+
self._webdriver = parent_webdriver
|
|
1020
|
+
|
|
1021
|
+
self._new_targets_filter = [filter_.to_dict() for filter_ in devtools_settings.new_targets_filter] if devtools_settings.new_targets_filter is not None else None
|
|
1022
|
+
|
|
1023
|
+
self._new_targets_buffer_size = devtools_settings.new_targets_buffer_size
|
|
1024
|
+
self._target_background_task = devtools_settings.target_background_task
|
|
1025
|
+
self._logger_settings = devtools_settings.logger_settings
|
|
1026
|
+
self._bidi_connection: Optional[AbstractAsyncContextManager[BidiConnection, Any]] = None
|
|
1027
|
+
self._bidi_connection_object: Optional[BidiConnection] = None
|
|
1028
|
+
self._nursery: Optional[AbstractAsyncContextManager[trio.Nursery, Optional[bool]]] = None
|
|
1029
|
+
self._nursery_object: Optional[trio.Nursery] = None
|
|
1030
|
+
self._domains_settings: Domains = {}
|
|
1031
|
+
self._handling_targets: dict[str, DevToolsTarget] = {}
|
|
1032
|
+
self.targets_lock = trio.Lock()
|
|
1033
|
+
self._websocket_url: Optional[str] = None
|
|
1034
|
+
self.exit_event: Optional[trio.Event] = None
|
|
1035
|
+
self._is_active = False
|
|
1036
|
+
self._is_closing = False
|
|
1037
|
+
self._num_logs = 0
|
|
1038
|
+
self._targets_types_stats: dict[str, TargetTypeStats] = {}
|
|
1039
|
+
self._log_level_stats: dict[str, LogLevelStats] = {}
|
|
1040
|
+
self._main_logger: Optional[MainLogger] = None
|
|
1041
|
+
self._main_logger_send_channel: Optional[trio.MemorySendChannel[MainLogEntry]] = None
|
|
1042
|
+
|
|
1043
|
+
_prepare_log_dir(devtools_settings.logger_settings)
|
|
1044
|
+
|
|
1045
|
+
async def _main_log(self):
|
|
1046
|
+
"""
|
|
1047
|
+
Sends updated overall logging statistics to the main logger.
|
|
1048
|
+
|
|
1049
|
+
This method constructs a `MainLogEntry` with current statistics and
|
|
1050
|
+
sends it to the `_main_logger_send_channel`. If the channel buffer is full,
|
|
1051
|
+
the log is dropped silently.
|
|
1052
|
+
"""
|
|
1053
|
+
|
|
1054
|
+
try:
|
|
1055
|
+
if self._main_logger_send_channel is not None and self._main_logger is not None:
|
|
1056
|
+
log_entry = MainLogEntry(
|
|
1057
|
+
num_channels=len(self._handling_targets),
|
|
1058
|
+
targets_types_stats=self._targets_types_stats,
|
|
1059
|
+
num_logs=self._num_logs,
|
|
1060
|
+
log_level_stats=self._log_level_stats,
|
|
1061
|
+
channels_stats=list(
|
|
1062
|
+
map(
|
|
1063
|
+
lambda target: target.log_stats,
|
|
1064
|
+
filter(
|
|
1065
|
+
lambda target: target.target_type_log_accepted,
|
|
1066
|
+
self._handling_targets.values()
|
|
1067
|
+
)
|
|
1068
|
+
)
|
|
1069
|
+
),
|
|
1070
|
+
)
|
|
1071
|
+
self._main_logger_send_channel.send_nowait(log_entry)
|
|
1072
|
+
except (trio.WouldBlock, trio.BrokenResourceError):
|
|
1073
|
+
pass
|
|
1074
|
+
except cdp_end_exceptions as error:
|
|
1075
|
+
raise error
|
|
1076
|
+
except BaseException as error:
|
|
1077
|
+
log_exception(error)
|
|
1078
|
+
raise error
|
|
1079
|
+
|
|
1080
|
+
async def _add_log(self, log_entry: LogEntry):
|
|
1081
|
+
"""
|
|
1082
|
+
Updates internal logging statistics based on a new log entry.
|
|
1083
|
+
|
|
1084
|
+
This method increments total log counts and updates per-channel and per-level statistics.
|
|
1085
|
+
It also triggers an update to the main logger.
|
|
1086
|
+
|
|
1087
|
+
Args:
|
|
1088
|
+
log_entry (LogEntry): The log entry to use for updating statistics.
|
|
1089
|
+
|
|
1090
|
+
Raises:
|
|
1091
|
+
BaseException: Catches and logs any unexpected errors during the log aggregation process.
|
|
1092
|
+
"""
|
|
1093
|
+
|
|
1094
|
+
try:
|
|
1095
|
+
self._num_logs += 1
|
|
1096
|
+
|
|
1097
|
+
if log_entry.level not in self._log_level_stats:
|
|
1098
|
+
self._log_level_stats[log_entry.level] = LogLevelStats(num_logs=1, last_log_time=log_entry.timestamp)
|
|
1099
|
+
else:
|
|
1100
|
+
self._log_level_stats[log_entry.level].num_logs += 1
|
|
1101
|
+
self._log_level_stats[log_entry.level].last_log_time = log_entry.timestamp
|
|
1102
|
+
|
|
1103
|
+
await self._main_log()
|
|
1104
|
+
except cdp_end_exceptions:
|
|
1105
|
+
pass
|
|
1106
|
+
except BaseException as error:
|
|
1107
|
+
log_exception(error)
|
|
1108
|
+
raise error
|
|
1109
|
+
|
|
1110
|
+
async def _remove_target(self, target: DevToolsTarget) -> Optional[bool]:
|
|
1111
|
+
"""
|
|
1112
|
+
Removes a target ID from the list of currently handled targets.
|
|
1113
|
+
|
|
1114
|
+
This method also triggers the removal of the target's specific logger channel
|
|
1115
|
+
and updates overall logging statistics.
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
target (DevToolsTarget): The target instance to remove.
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
Optional[bool]: True if the target ID was successfully removed, False if it was not found.
|
|
1122
|
+
Returns None if an exception occurs.
|
|
1123
|
+
"""
|
|
1124
|
+
|
|
1125
|
+
try:
|
|
1126
|
+
async with self.targets_lock:
|
|
1127
|
+
if target.target_id in self._handling_targets:
|
|
1128
|
+
self._targets_types_stats[target.type_].num_targets -= 1
|
|
1129
|
+
|
|
1130
|
+
target = self._handling_targets.pop(target.target_id)
|
|
1131
|
+
await target.log_step(message=f"Target '{target.target_id}' removed.")
|
|
1132
|
+
await target.stop()
|
|
1133
|
+
|
|
1134
|
+
await self._main_log()
|
|
1135
|
+
|
|
1136
|
+
return True
|
|
1137
|
+
else:
|
|
1138
|
+
return False
|
|
1139
|
+
except cdp_end_exceptions:
|
|
1140
|
+
pass
|
|
1141
|
+
except BaseException as error:
|
|
1142
|
+
log_exception(error)
|
|
1143
|
+
|
|
1144
|
+
@property
|
|
1145
|
+
def _devtools_package(self) -> Any:
|
|
1146
|
+
"""
|
|
1147
|
+
Retrieves the DevTools protocol package from the active BiDi connection.
|
|
1148
|
+
|
|
1149
|
+
Returns:
|
|
1150
|
+
Any: The DevTools protocol package object, providing access to CDP domains and commands.
|
|
1151
|
+
|
|
1152
|
+
Raises:
|
|
1153
|
+
BidiConnectionNotEstablishedError: If the BiDi connection is not active.
|
|
1154
|
+
"""
|
|
1155
|
+
|
|
1156
|
+
try:
|
|
1157
|
+
if self._bidi_connection_object is not None:
|
|
1158
|
+
return self._bidi_connection_object.devtools
|
|
1159
|
+
else:
|
|
1160
|
+
raise BidiConnectionNotEstablishedError()
|
|
1161
|
+
except cdp_end_exceptions as error:
|
|
1162
|
+
raise error
|
|
1163
|
+
except BaseException as error:
|
|
1164
|
+
log_exception(error)
|
|
1165
|
+
raise error
|
|
1166
|
+
|
|
1167
|
+
async def _add_target(self, target_event: Any) -> Optional[bool]:
|
|
1168
|
+
"""
|
|
1169
|
+
Adds a new browser target to the manager based on a target event.
|
|
1170
|
+
|
|
1171
|
+
This method processes events like `TargetCreated` or `AttachedToTarget`
|
|
1172
|
+
to initialize and manage new `DevToolsTarget` instances. It ensures
|
|
1173
|
+
that targets are not added if the manager is closing or if they already exist.
|
|
1174
|
+
|
|
1175
|
+
Args:
|
|
1176
|
+
target_event (Any): The event object containing target information.
|
|
1177
|
+
Expected to have a `target_info` attribute or be the target info itself.
|
|
1178
|
+
|
|
1179
|
+
Returns:
|
|
1180
|
+
Optional[bool]: True if a new target was successfully added and started,
|
|
1181
|
+
False if the target already existed or was filtered,
|
|
1182
|
+
or None if an error occurred.
|
|
1183
|
+
|
|
1184
|
+
Raises:
|
|
1185
|
+
BaseException: Catches and logs any unexpected errors during target addition.
|
|
1186
|
+
"""
|
|
1187
|
+
|
|
1188
|
+
try:
|
|
1189
|
+
if hasattr(target_event, "target_info"):
|
|
1190
|
+
target_info = target_event.target_info
|
|
1191
|
+
else:
|
|
1192
|
+
target_info = target_event
|
|
1193
|
+
|
|
1194
|
+
async with self.targets_lock:
|
|
1195
|
+
target_id = target_info.target_id
|
|
1196
|
+
|
|
1197
|
+
if self._is_closing:
|
|
1198
|
+
return False
|
|
1199
|
+
|
|
1200
|
+
if target_id not in self._handling_targets:
|
|
1201
|
+
self._handling_targets[target_id] = DevToolsTarget(
|
|
1202
|
+
target_data=TargetData(
|
|
1203
|
+
target_id=target_id,
|
|
1204
|
+
type_=target_info.type_,
|
|
1205
|
+
title=target_info.title,
|
|
1206
|
+
url=target_info.url,
|
|
1207
|
+
attached=target_info.attached,
|
|
1208
|
+
can_access_opener=target_info.can_access_opener,
|
|
1209
|
+
opener_id=target_info.opener_id,
|
|
1210
|
+
opener_frame_id=target_info.opener_frame_id,
|
|
1211
|
+
browser_context_id=target_info.browser_context_id,
|
|
1212
|
+
subtype=target_info.subtype,
|
|
1213
|
+
),
|
|
1214
|
+
logger_settings=self._logger_settings,
|
|
1215
|
+
devtools_package=self._devtools_package,
|
|
1216
|
+
websocket_url=self._websocket_url,
|
|
1217
|
+
new_targets_filter=self._new_targets_filter,
|
|
1218
|
+
new_targets_buffer_size=self._new_targets_buffer_size,
|
|
1219
|
+
domains=self._domains_settings,
|
|
1220
|
+
nursery=self._nursery_object,
|
|
1221
|
+
exit_event=self.exit_event,
|
|
1222
|
+
target_background_task=self._target_background_task,
|
|
1223
|
+
add_target_func=self._add_target,
|
|
1224
|
+
remove_target_func=self._remove_target,
|
|
1225
|
+
add_log_func=self._add_log,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
if target_info.type_ not in self._targets_types_stats:
|
|
1229
|
+
self._targets_types_stats[target_info.type_] = TargetTypeStats(num_targets=1)
|
|
1230
|
+
else:
|
|
1231
|
+
self._targets_types_stats[target_info.type_].num_targets += 1
|
|
1232
|
+
|
|
1233
|
+
await self._main_log()
|
|
1234
|
+
|
|
1235
|
+
self._nursery_object.start_soon(self._handling_targets[target_id].run,)
|
|
1236
|
+
|
|
1237
|
+
return True
|
|
1238
|
+
else:
|
|
1239
|
+
self._handling_targets[target_id].type_ = target_info.type_
|
|
1240
|
+
self._handling_targets[target_id].title = target_info.title
|
|
1241
|
+
self._handling_targets[target_id].url = target_info.url
|
|
1242
|
+
self._handling_targets[target_id].attached = target_info.attached
|
|
1243
|
+
self._handling_targets[target_id].can_access_opener = target_info.can_access_opener
|
|
1244
|
+
self._handling_targets[target_id].opener_id = target_info.opener_id
|
|
1245
|
+
self._handling_targets[target_id].opener_frame_id = target_info.opener_frame_id
|
|
1246
|
+
self._handling_targets[target_id].browser_context_id = target_info.browser_context_id
|
|
1247
|
+
self._handling_targets[target_id].subtype = target_info.subtype
|
|
1248
|
+
|
|
1249
|
+
return False
|
|
1250
|
+
except* cdp_end_exceptions:
|
|
1251
|
+
pass
|
|
1252
|
+
except* BaseException as error:
|
|
1253
|
+
log_exception(error)
|
|
1254
|
+
raise error
|
|
1255
|
+
|
|
1256
|
+
async def _get_devtools_object(self, path: str) -> Any:
|
|
1257
|
+
"""
|
|
1258
|
+
Navigates and retrieves a specific object within the DevTools API structure.
|
|
1259
|
+
|
|
1260
|
+
Using a dot-separated path, this method traverses the nested DevTools API objects to retrieve a target object.
|
|
1261
|
+
For example, a path like "fetch.enable" would access `self.devtools_module.fetch.enable`.
|
|
1262
|
+
Results are cached for faster access.
|
|
1263
|
+
|
|
1264
|
+
Args:
|
|
1265
|
+
path (str): A dot-separated string representing the path to the desired DevTools API object.
|
|
1266
|
+
|
|
1267
|
+
Returns:
|
|
1268
|
+
Any: The DevTools API object located at the specified path.
|
|
1269
|
+
|
|
1270
|
+
Raises:
|
|
1271
|
+
cdp_end_exceptions: If a CDP-related connection error occurs.
|
|
1272
|
+
BaseException: If the object cannot be found or another error occurs during retrieval.
|
|
1273
|
+
"""
|
|
1274
|
+
|
|
1275
|
+
try:
|
|
1276
|
+
package = self._devtools_package
|
|
1277
|
+
|
|
1278
|
+
for part in path.split("."):
|
|
1279
|
+
package = getattr(package, part)
|
|
1280
|
+
|
|
1281
|
+
return package
|
|
1282
|
+
except cdp_end_exceptions as error:
|
|
1283
|
+
raise error
|
|
1284
|
+
except BaseException as error:
|
|
1285
|
+
log_exception(error)
|
|
1286
|
+
raise error
|
|
1287
|
+
|
|
1288
|
+
async def _get_all_targets(self) -> list[Any]:
|
|
1289
|
+
"""
|
|
1290
|
+
Retrieves a list of all currently active browser targets.
|
|
1291
|
+
|
|
1292
|
+
Returns:
|
|
1293
|
+
list[Any]: A list of target objects, each containing information like target ID, type, and URL.
|
|
1294
|
+
|
|
1295
|
+
Raises:
|
|
1296
|
+
BidiConnectionNotEstablishedError: If the BiDi connection is not active.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
try:
|
|
1300
|
+
if self._bidi_connection_object is not None:
|
|
1301
|
+
targets_filter = (await self._get_devtools_object("target.TargetFilter"))(
|
|
1302
|
+
[
|
|
1303
|
+
{"exclude": False, "type": "page"},
|
|
1304
|
+
{"exclude": False, "type": "tab"},
|
|
1305
|
+
{"exclude": True}
|
|
1306
|
+
]
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
return await self._bidi_connection_object.session.execute(self._devtools_package.target.get_targets(targets_filter))
|
|
1310
|
+
else:
|
|
1311
|
+
raise BidiConnectionNotEstablishedError()
|
|
1312
|
+
except cdp_end_exceptions as error:
|
|
1313
|
+
raise error
|
|
1314
|
+
except BaseException as error:
|
|
1315
|
+
log_exception(error)
|
|
1316
|
+
raise error
|
|
1317
|
+
|
|
1318
|
+
def _get_websocket_url(self) -> Optional[str]:
|
|
1319
|
+
"""
|
|
1320
|
+
Retrieves the WebSocket URL for DevTools from the WebDriver.
|
|
1321
|
+
|
|
1322
|
+
This method attempts to get the WebSocket URL from the WebDriver capabilities or by directly querying the CDP details.
|
|
1323
|
+
The WebSocket URL is necessary to establish a connection to the browser's DevTools.
|
|
1324
|
+
|
|
1325
|
+
Returns:
|
|
1326
|
+
Optional[str]: The WebSocket URL for DevTools, or None if it cannot be retrieved.
|
|
1327
|
+
|
|
1328
|
+
Raises:
|
|
1329
|
+
cdp_end_exceptions: If a CDP-related connection error occurs.
|
|
1330
|
+
BaseException: If another unexpected error occurs during URL retrieval.
|
|
1331
|
+
"""
|
|
1332
|
+
|
|
1333
|
+
try:
|
|
1334
|
+
driver = self._webdriver.driver
|
|
1335
|
+
|
|
1336
|
+
if driver is None:
|
|
1337
|
+
self._websocket_url = None
|
|
1338
|
+
|
|
1339
|
+
if driver.caps.get("se:cdp"):
|
|
1340
|
+
self._websocket_url = driver.caps.get("se:cdp")
|
|
1341
|
+
|
|
1342
|
+
self._websocket_url = driver._get_cdp_details()[1]
|
|
1343
|
+
except cdp_end_exceptions as error:
|
|
1344
|
+
raise error
|
|
1345
|
+
except BaseException as error:
|
|
1346
|
+
log_exception(error)
|
|
1347
|
+
raise error
|
|
1348
|
+
|
|
1349
|
+
async def __aenter__(self):
|
|
1350
|
+
"""
|
|
1351
|
+
Enters the asynchronous context for DevTools event handling.
|
|
1352
|
+
|
|
1353
|
+
This method establishes the BiDi connection, initializes the Trio nursery,
|
|
1354
|
+
sets up the main target, and starts listening for DevTools events.
|
|
1355
|
+
|
|
1356
|
+
Raises:
|
|
1357
|
+
CantEnterDevToolsContextError: If the WebDriver is not initialized.
|
|
1358
|
+
BaseException: If any other unexpected error occurs during context entry.
|
|
1359
|
+
"""
|
|
1360
|
+
|
|
1361
|
+
if self._webdriver.driver is None:
|
|
1362
|
+
raise CantEnterDevToolsContextError("Driver is not initialized")
|
|
1363
|
+
|
|
1364
|
+
self._bidi_connection: AbstractAsyncContextManager[BidiConnection, Any] = self._webdriver.driver.bidi_connection()
|
|
1365
|
+
self._bidi_connection_object = await self._bidi_connection.__aenter__()
|
|
1366
|
+
|
|
1367
|
+
self._nursery = trio.open_nursery()
|
|
1368
|
+
self._nursery_object = await self._nursery.__aenter__()
|
|
1369
|
+
|
|
1370
|
+
self._get_websocket_url()
|
|
1371
|
+
|
|
1372
|
+
self._main_logger_send_channel, self._main_logger = build_main_logger(self._nursery_object, self._logger_settings)
|
|
1373
|
+
await self._main_logger.run()
|
|
1374
|
+
|
|
1375
|
+
self.exit_event = trio.Event()
|
|
1376
|
+
|
|
1377
|
+
main_target = (await self._get_all_targets())[0]
|
|
1378
|
+
await self._add_target(main_target)
|
|
1379
|
+
|
|
1380
|
+
self._is_active = True
|
|
1381
|
+
|
|
1382
|
+
async def __aexit__(
|
|
1383
|
+
self,
|
|
1384
|
+
exc_type: Optional[type],
|
|
1385
|
+
exc_val: Optional[BaseException],
|
|
1386
|
+
exc_tb: Optional[TracebackType]
|
|
1387
|
+
):
|
|
1388
|
+
"""
|
|
1389
|
+
Asynchronously exits the DevTools event handling context.
|
|
1390
|
+
|
|
1391
|
+
This method is called when exiting an `async with` block with a DevTools instance.
|
|
1392
|
+
It ensures that all event listeners are cancelled, the Trio nursery is closed,
|
|
1393
|
+
and the BiDi connection is properly shut down. Cleanup attempts are made even if
|
|
1394
|
+
an exception occurred within the `async with` block.
|
|
1395
|
+
|
|
1396
|
+
Args:
|
|
1397
|
+
exc_type (Optional[type[BaseException]]): The exception type, if any, that caused the context to be exited.
|
|
1398
|
+
exc_val (Optional[BaseException]): The exception value, if any.
|
|
1399
|
+
exc_tb (Optional[TracebackType]): The exception traceback, if any.
|
|
1400
|
+
"""
|
|
1401
|
+
|
|
1402
|
+
@log_on_error
|
|
1403
|
+
async def _stop_main_logger():
|
|
1404
|
+
"""Stops the main logger and closes its channels."""
|
|
1405
|
+
|
|
1406
|
+
if self._main_logger_send_channel is not None:
|
|
1407
|
+
await self._main_logger_send_channel.aclose()
|
|
1408
|
+
self._main_logger_send_channel = None
|
|
1409
|
+
|
|
1410
|
+
if self._main_logger is not None:
|
|
1411
|
+
await self._main_logger.close()
|
|
1412
|
+
self._main_logger = None
|
|
1413
|
+
|
|
1414
|
+
@log_on_error
|
|
1415
|
+
async def _stop_all_targets():
|
|
1416
|
+
"""Signals all active targets to stop and waits for their completion."""
|
|
1417
|
+
|
|
1418
|
+
for target in self._handling_targets.copy().values():
|
|
1419
|
+
await target.stop()
|
|
1420
|
+
await target.stopped_event.wait()
|
|
1421
|
+
|
|
1422
|
+
self._handling_targets = {}
|
|
1423
|
+
|
|
1424
|
+
@log_on_error
|
|
1425
|
+
async def _close_nursery():
|
|
1426
|
+
"""Asynchronously exits the Trio nursery context manager."""
|
|
1427
|
+
|
|
1428
|
+
if self._nursery_object is not None:
|
|
1429
|
+
self._nursery_object.cancel_scope.cancel()
|
|
1430
|
+
self._nursery_object = None
|
|
1431
|
+
|
|
1432
|
+
if self._nursery is not None:
|
|
1433
|
+
await self._nursery.__aexit__(exc_type, exc_val, exc_tb)
|
|
1434
|
+
self._nursery = None
|
|
1435
|
+
|
|
1436
|
+
@log_on_error
|
|
1437
|
+
async def _close_bidi_connection():
|
|
1438
|
+
"""Asynchronously exits the BiDi connection context manager."""
|
|
1439
|
+
|
|
1440
|
+
if self._bidi_connection is not None:
|
|
1441
|
+
await self._bidi_connection.__aexit__(exc_type, exc_val, exc_tb)
|
|
1442
|
+
self._bidi_connection = None
|
|
1443
|
+
self._bidi_connection_object = None
|
|
1444
|
+
|
|
1445
|
+
if self._is_active:
|
|
1446
|
+
self._is_closing = True
|
|
1447
|
+
self.exit_event.set()
|
|
1448
|
+
|
|
1449
|
+
await _stop_main_logger()
|
|
1450
|
+
await _stop_all_targets()
|
|
1451
|
+
await _close_nursery()
|
|
1452
|
+
await _close_bidi_connection()
|
|
1453
|
+
|
|
1454
|
+
self.exit_event = None
|
|
1455
|
+
self._websocket_url = None
|
|
1456
|
+
self._num_logs = 0
|
|
1457
|
+
self._targets_types_stats = {}
|
|
1458
|
+
self._log_level_stats = {}
|
|
1459
|
+
self._is_active = False
|
|
1460
|
+
self._is_closing = False
|
|
1461
|
+
|
|
1462
|
+
@property
|
|
1463
|
+
def is_active(self) -> bool:
|
|
1464
|
+
"""
|
|
1465
|
+
Checks if DevTools is currently active.
|
|
1466
|
+
|
|
1467
|
+
Returns:
|
|
1468
|
+
bool: True if DevTools event handler context manager is active, False otherwise.
|
|
1469
|
+
"""
|
|
1470
|
+
|
|
1471
|
+
return self._is_active
|
|
1472
|
+
|
|
1473
|
+
@warn_if_active
|
|
1474
|
+
def _remove_handler_settings(self, domain: domains_type):
|
|
1475
|
+
"""
|
|
1476
|
+
Removes the settings for a specific domain.
|
|
1477
|
+
|
|
1478
|
+
This is an internal method intended to be used only when the DevTools context is not active.
|
|
1479
|
+
It uses the `@warn_if_active` decorator to log a warning if called incorrectly.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
domain (domains_type): The name of the domain to remove settings for.
|
|
1483
|
+
"""
|
|
1484
|
+
|
|
1485
|
+
self._domains_settings.pop(domain, None)
|
|
1486
|
+
|
|
1487
|
+
def remove_domains_handlers(self, domains: Union[domains_type, Sequence[domains_type]]):
|
|
1488
|
+
"""
|
|
1489
|
+
Removes handler settings for one or more DevTools domains.
|
|
1490
|
+
|
|
1491
|
+
This method can be called with a single domain name or a sequence of domain names.
|
|
1492
|
+
It should only be called when the DevTools context is not active.
|
|
1493
|
+
|
|
1494
|
+
Args:
|
|
1495
|
+
domains (Union[domains_type, Sequence[domains_type]]): A single domain name as a string,
|
|
1496
|
+
or a sequence of domain names to be removed.
|
|
1497
|
+
|
|
1498
|
+
Raises:
|
|
1499
|
+
TypeError: If the `domains` argument is not a string or a sequence of strings.
|
|
1500
|
+
"""
|
|
1501
|
+
|
|
1502
|
+
if isinstance(domains, Sequence) and all(isinstance(domain, str) for domain in domains):
|
|
1503
|
+
for domain in domains:
|
|
1504
|
+
self._remove_handler_settings(domain)
|
|
1505
|
+
elif isinstance(domains, str):
|
|
1506
|
+
self._remove_handler_settings(domains)
|
|
1507
|
+
else:
|
|
1508
|
+
raise TypeError(f"domains must be a str or a sequence of str, got {type(domains)}.")
|
|
1509
|
+
|
|
1510
|
+
@warn_if_active
|
|
1511
|
+
def _set_handler_settings(self, domain: domains_type, settings: domains_classes_type):
|
|
1512
|
+
"""
|
|
1513
|
+
Sets the handler settings for a specific domain.
|
|
1514
|
+
|
|
1515
|
+
This is an internal method intended to be used only when the DevTools context is not active.
|
|
1516
|
+
It uses the `@warn_if_active` decorator to log a warning if called incorrectly.
|
|
1517
|
+
|
|
1518
|
+
Args:
|
|
1519
|
+
domain (domains_type): The name of the domain to configure.
|
|
1520
|
+
settings (domains_classes_type): The configuration settings for the domain.
|
|
1521
|
+
"""
|
|
1522
|
+
|
|
1523
|
+
self._domains_settings[domain] = settings
|
|
1524
|
+
|
|
1525
|
+
def set_domains_handlers(self, settings: DomainsSettings):
|
|
1526
|
+
"""
|
|
1527
|
+
Sets handler settings for multiple domains from a DomainsSettings object.
|
|
1528
|
+
|
|
1529
|
+
This method iterates through the provided settings and applies them to the corresponding domains.
|
|
1530
|
+
It should only be called when the DevTools context is not active.
|
|
1531
|
+
|
|
1532
|
+
Args:
|
|
1533
|
+
settings (DomainsSettings): An object containing the configuration for one or more domains.
|
|
1534
|
+
"""
|
|
1535
|
+
|
|
1536
|
+
for domain_name, domain_settings in settings.to_dict().items():
|
|
1537
|
+
self._set_handler_settings(domain_name, domain_settings)
|
|
1538
|
+
|
|
1539
|
+
@property
|
|
1540
|
+
def websocket_url(self) -> Optional[str]:
|
|
1541
|
+
"""
|
|
1542
|
+
Gets the WebSocket URL for the DevTools session.
|
|
1543
|
+
|
|
1544
|
+
This URL is used to establish a direct Chrome DevTools Protocol (CDP) connection
|
|
1545
|
+
to the browser, enabling low-level control and event listening.
|
|
1546
|
+
|
|
1547
|
+
Returns:
|
|
1548
|
+
Optional[str]: The WebSocket URL, or None if it has not been retrieved yet.
|
|
1549
|
+
"""
|
|
1550
|
+
|
|
1551
|
+
return self._websocket_url
|