mithwire 0.50.3__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.
- mithwire/__init__.py +32 -0
- mithwire/cdp/README.md +4 -0
- mithwire/cdp/__init__.py +6 -0
- mithwire/cdp/accessibility.py +668 -0
- mithwire/cdp/animation.py +494 -0
- mithwire/cdp/audits.py +1995 -0
- mithwire/cdp/autofill.py +292 -0
- mithwire/cdp/background_service.py +215 -0
- mithwire/cdp/bluetooth_emulation.py +626 -0
- mithwire/cdp/browser.py +821 -0
- mithwire/cdp/cache_storage.py +311 -0
- mithwire/cdp/cast.py +172 -0
- mithwire/cdp/console.py +107 -0
- mithwire/cdp/crash_report_context.py +55 -0
- mithwire/cdp/css.py +2750 -0
- mithwire/cdp/database.py +179 -0
- mithwire/cdp/debugger.py +1405 -0
- mithwire/cdp/device_access.py +141 -0
- mithwire/cdp/device_orientation.py +45 -0
- mithwire/cdp/dom.py +2257 -0
- mithwire/cdp/dom_debugger.py +321 -0
- mithwire/cdp/dom_snapshot.py +876 -0
- mithwire/cdp/dom_storage.py +222 -0
- mithwire/cdp/emulation.py +1779 -0
- mithwire/cdp/event_breakpoints.py +56 -0
- mithwire/cdp/extensions.py +238 -0
- mithwire/cdp/fed_cm.py +283 -0
- mithwire/cdp/fetch.py +507 -0
- mithwire/cdp/file_system.py +115 -0
- mithwire/cdp/headless_experimental.py +115 -0
- mithwire/cdp/heap_profiler.py +401 -0
- mithwire/cdp/indexed_db.py +528 -0
- mithwire/cdp/input_.py +701 -0
- mithwire/cdp/inspector.py +95 -0
- mithwire/cdp/io.py +101 -0
- mithwire/cdp/layer_tree.py +464 -0
- mithwire/cdp/log.py +190 -0
- mithwire/cdp/media.py +313 -0
- mithwire/cdp/memory.py +305 -0
- mithwire/cdp/network.py +5342 -0
- mithwire/cdp/overlay.py +1468 -0
- mithwire/cdp/page.py +3972 -0
- mithwire/cdp/performance.py +124 -0
- mithwire/cdp/performance_timeline.py +200 -0
- mithwire/cdp/preload.py +575 -0
- mithwire/cdp/profiler.py +420 -0
- mithwire/cdp/pwa.py +278 -0
- mithwire/cdp/py.typed +0 -0
- mithwire/cdp/runtime.py +1589 -0
- mithwire/cdp/schema.py +50 -0
- mithwire/cdp/security.py +518 -0
- mithwire/cdp/service_worker.py +401 -0
- mithwire/cdp/smart_card_emulation.py +891 -0
- mithwire/cdp/storage.py +1573 -0
- mithwire/cdp/system_info.py +327 -0
- mithwire/cdp/target.py +829 -0
- mithwire/cdp/tethering.py +65 -0
- mithwire/cdp/tracing.py +377 -0
- mithwire/cdp/util.py +18 -0
- mithwire/cdp/web_audio.py +606 -0
- mithwire/cdp/web_authn.py +598 -0
- mithwire/cdp/web_mcp.py +293 -0
- mithwire/core/_contradict.py +142 -0
- mithwire/core/browser.py +923 -0
- mithwire/core/cf_templates/cf_dark_checkbox.png +0 -0
- mithwire/core/cf_templates/cf_light_checkbox.png +0 -0
- mithwire/core/config.py +323 -0
- mithwire/core/connection.py +564 -0
- mithwire/core/element.py +1205 -0
- mithwire/core/tab.py +2202 -0
- mithwire/core/util.py +5063 -0
- mithwire-0.50.3.dist-info/METADATA +1049 -0
- mithwire-0.50.3.dist-info/RECORD +76 -0
- mithwire-0.50.3.dist-info/WHEEL +5 -0
- mithwire-0.50.3.dist-info/licenses/LICENSE.txt +619 -0
- mithwire-0.50.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# Copyright 2024 by UltrafunkAmsterdam (https://github.com/UltrafunkAmsterdam)
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
# This file is part of the mithwire package.
|
|
4
|
+
# and is released under the "GNU AFFERO GENERAL PUBLIC LICENSE".
|
|
5
|
+
# Please see the LICENSE.txt file that should have been included as part of this package.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import collections
|
|
11
|
+
import contextlib
|
|
12
|
+
import inspect
|
|
13
|
+
import itertools
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
import types
|
|
18
|
+
from asyncio import iscoroutine, iscoroutinefunction
|
|
19
|
+
from typing import Any, Awaitable, Callable, Generator, List, TypeVar, Union
|
|
20
|
+
|
|
21
|
+
import websockets.asyncio.client
|
|
22
|
+
from websockets import InvalidStatus
|
|
23
|
+
|
|
24
|
+
from .. import cdp
|
|
25
|
+
from . import browser as _browser
|
|
26
|
+
from . import util
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
|
|
30
|
+
GLOBAL_DELAY = 0.005
|
|
31
|
+
MAX_SIZE: int = 2**28
|
|
32
|
+
PING_TIMEOUT: int = 900 # 15 minutes
|
|
33
|
+
|
|
34
|
+
TargetType = Union[cdp.target.TargetInfo, cdp.target.TargetID, str]
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProtocolException(Exception):
|
|
40
|
+
def __init__(self, *args, **kwargs): # real signature unknown
|
|
41
|
+
|
|
42
|
+
self.message = None
|
|
43
|
+
self.code = None
|
|
44
|
+
self.args = args
|
|
45
|
+
self.extra = kwargs
|
|
46
|
+
|
|
47
|
+
if isinstance(args[0], dict):
|
|
48
|
+
|
|
49
|
+
self.message = args[0].get("message", None) # noqa
|
|
50
|
+
self.code = args[0].get("code", None)
|
|
51
|
+
|
|
52
|
+
elif hasattr(args[0], "to_json"):
|
|
53
|
+
|
|
54
|
+
def serialize(obj, _d=0):
|
|
55
|
+
res = "\n"
|
|
56
|
+
for k, v in obj.items():
|
|
57
|
+
space = "\t" * _d
|
|
58
|
+
if isinstance(v, dict):
|
|
59
|
+
res += f"{space}{k}: {serialize(v, _d + 1)}\n"
|
|
60
|
+
else:
|
|
61
|
+
res += f"{space}{k}: {v}\n"
|
|
62
|
+
|
|
63
|
+
return res
|
|
64
|
+
|
|
65
|
+
self.message = serialize(args[0].to_json())
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
self.message = "| ".join(str(x) for x in args)
|
|
69
|
+
|
|
70
|
+
self.message += "\n".join(f"{k}:{v}" for k, v in self.extra.items())
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
return (
|
|
74
|
+
f"{self.message} [code: {self.code}]"
|
|
75
|
+
if self.code
|
|
76
|
+
else f"{self.message}" + f"\n{self.extra}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Transaction:
|
|
81
|
+
def __init__(self, request: dict):
|
|
82
|
+
self.id: int | None = request["id"]
|
|
83
|
+
self.request: dict = request
|
|
84
|
+
self.events: list = []
|
|
85
|
+
self.result: Any = None
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
return (
|
|
89
|
+
f"{self.__class__.__name__}"
|
|
90
|
+
f"[\n"
|
|
91
|
+
f"request: {self._truncate(self.request)}\n"
|
|
92
|
+
f"events:{len(self.events)}\n"
|
|
93
|
+
f"result: {self._truncate(self.result)}\n"
|
|
94
|
+
f"]"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __repr__(self):
|
|
98
|
+
return self.__str__()
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _truncate(item: Any, max_length: int = 256, ellipsis: bool = True) -> str:
|
|
102
|
+
string = str(item)
|
|
103
|
+
if len(string) > max_length:
|
|
104
|
+
return f"{string[:max_length]} {'...' if ellipsis else ''}"
|
|
105
|
+
return string
|
|
106
|
+
|
|
107
|
+
# class MapperEntry(asyncio.Future):
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
#
|
|
111
|
+
# def __init__(self, cdp_obj: Generator, id: int, session_id: str = None):
|
|
112
|
+
#
|
|
113
|
+
# super().__init__()
|
|
114
|
+
# self._cdp_obj = cdp_obj
|
|
115
|
+
#
|
|
116
|
+
# method, *params = next(self._cdp_obj).values()
|
|
117
|
+
# if params:
|
|
118
|
+
# params = params.pop()
|
|
119
|
+
#
|
|
120
|
+
# request = {"method": method, "params": params, "id": id}
|
|
121
|
+
# if session_id:
|
|
122
|
+
# request["sessionId"] = session_id
|
|
123
|
+
#
|
|
124
|
+
# self.request = request
|
|
125
|
+
# self.response = None
|
|
126
|
+
# self.events = []
|
|
127
|
+
#
|
|
128
|
+
# def set_session_id(self, session_id: str):
|
|
129
|
+
# self.request['sessionId'] = session_id
|
|
130
|
+
#
|
|
131
|
+
# def __call__(self, response: dict):
|
|
132
|
+
# self.response = response
|
|
133
|
+
# if "error" in response:
|
|
134
|
+
# # set exception and bail out
|
|
135
|
+
# return self.set_exception(ProtocolException(response["error"], request=self.request))
|
|
136
|
+
# try:
|
|
137
|
+
# # try to parse the result according to the py cdp docs.
|
|
138
|
+
# self._cdp_obj.send(response["result"])
|
|
139
|
+
# except StopIteration as e:
|
|
140
|
+
# # exception value holds the parsed response
|
|
141
|
+
# self.set_result(e.value)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Connection:
|
|
145
|
+
socket: websockets.asyncio.client.ClientConnection | None
|
|
146
|
+
websocket_url: str = None
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def attached(self):
|
|
150
|
+
return bool(self.session_id and self.target)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def transactions(self):
|
|
154
|
+
"""
|
|
155
|
+
simple array that holds recent transactions.
|
|
156
|
+
each transaction consist of a response, request, result, events fields
|
|
157
|
+
the latest transaction is the last element in the list.
|
|
158
|
+
"""
|
|
159
|
+
return self._transactions
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def last_transaction(self):
|
|
163
|
+
"""
|
|
164
|
+
simple alias to get the last transaction to inspect.
|
|
165
|
+
"""
|
|
166
|
+
return self.transactions[-1]
|
|
167
|
+
|
|
168
|
+
def add_handler(self, event_type: type, callback: Callable) -> None:
|
|
169
|
+
"""Register an event callback for a parsed CDP event type."""
|
|
170
|
+
if callback not in self.handlers[event_type]:
|
|
171
|
+
self.handlers[event_type].append(callback)
|
|
172
|
+
|
|
173
|
+
def remove_handler(self, event_type: type, callback: Callable) -> bool:
|
|
174
|
+
"""Remove an event callback; returns True when a handler was removed."""
|
|
175
|
+
callbacks = self.handlers.get(event_type)
|
|
176
|
+
if not callbacks:
|
|
177
|
+
return False
|
|
178
|
+
try:
|
|
179
|
+
callbacks.remove(callback)
|
|
180
|
+
except ValueError:
|
|
181
|
+
return False
|
|
182
|
+
if not callbacks:
|
|
183
|
+
self.handlers.pop(event_type, None)
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
target: TargetType = None,
|
|
189
|
+
parent: Connection = None,
|
|
190
|
+
auto_attach: bool = False,
|
|
191
|
+
**kwargs,
|
|
192
|
+
) -> None:
|
|
193
|
+
|
|
194
|
+
self.session_id = None
|
|
195
|
+
self.websocket_url = parent.websocket_url if parent else None
|
|
196
|
+
self.handlers = collections.defaultdict(list)
|
|
197
|
+
self.lock = asyncio.Lock()
|
|
198
|
+
self.socket = None
|
|
199
|
+
self.target = target
|
|
200
|
+
self.parent = parent
|
|
201
|
+
self.__count__ = itertools.count(0)
|
|
202
|
+
self._auto_attach = auto_attach
|
|
203
|
+
self._transactions: List[Transaction] = []
|
|
204
|
+
self._targets: List[Connection] = []
|
|
205
|
+
self._mapper: dict[int, asyncio.Future] = {}
|
|
206
|
+
self._tx_by_id: dict[int, Transaction] = {}
|
|
207
|
+
self._listener_task: asyncio.Task | None = None
|
|
208
|
+
|
|
209
|
+
#
|
|
210
|
+
# @classmethod
|
|
211
|
+
# async def from_parent(
|
|
212
|
+
# cls,
|
|
213
|
+
# target: cdp.target.TargetID | cdp.target.TargetInfo,
|
|
214
|
+
# parent: Connection,
|
|
215
|
+
# auto_attach: bool = False,
|
|
216
|
+
# ) -> Connection:
|
|
217
|
+
# target_id = None
|
|
218
|
+
# if isinstance(target, cdp.target.TargetInfo):
|
|
219
|
+
# target_id = target.target_id
|
|
220
|
+
# elif isinstance(target, (cdp.target.TargetID, str)):
|
|
221
|
+
# target_id = target
|
|
222
|
+
# instance = cls(target=target_id, parent=parent, auto_attach=auto_attach)
|
|
223
|
+
# await instance.attach()
|
|
224
|
+
# return instance
|
|
225
|
+
|
|
226
|
+
async def aopen(self):
|
|
227
|
+
""" """
|
|
228
|
+
if not self.websocket_url:
|
|
229
|
+
raise RuntimeError("having no parent and no websocket url")
|
|
230
|
+
|
|
231
|
+
if not self.socket or bool(self.socket.close_code):
|
|
232
|
+
self.socket = await websockets.connect(
|
|
233
|
+
self.websocket_url,
|
|
234
|
+
ping_timeout=PING_TIMEOUT,
|
|
235
|
+
max_size=MAX_SIZE,
|
|
236
|
+
)
|
|
237
|
+
self._listener_task = asyncio.create_task(self._listener())
|
|
238
|
+
|
|
239
|
+
async def aclose(self):
|
|
240
|
+
""" """
|
|
241
|
+
self._fail_pending_futures(ConnectionError("Connection closing"))
|
|
242
|
+
if self._listener_task is not None:
|
|
243
|
+
self._listener_task.cancel()
|
|
244
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
245
|
+
await self._listener_task
|
|
246
|
+
self._listener_task = None
|
|
247
|
+
if self.socket:
|
|
248
|
+
await self.socket.close()
|
|
249
|
+
await self.socket.wait_closed()
|
|
250
|
+
self.socket = None
|
|
251
|
+
|
|
252
|
+
def _fail_pending_futures(self, exc: BaseException) -> None:
|
|
253
|
+
for future in self._mapper.values():
|
|
254
|
+
if not future.done():
|
|
255
|
+
if isinstance(exc, asyncio.CancelledError):
|
|
256
|
+
future.cancel()
|
|
257
|
+
else:
|
|
258
|
+
future.set_exception(exc)
|
|
259
|
+
self._mapper.clear()
|
|
260
|
+
|
|
261
|
+
async def attach(self, target: TargetType = None):
|
|
262
|
+
""" " """
|
|
263
|
+
await self.aopen()
|
|
264
|
+
target = target or self.target
|
|
265
|
+
|
|
266
|
+
if not target:
|
|
267
|
+
# is browser target
|
|
268
|
+
|
|
269
|
+
self.handlers[cdp.target.AttachedToTarget] = [
|
|
270
|
+
lambda event: setattr(self, "target", event.target_info)
|
|
271
|
+
]
|
|
272
|
+
self.session_id = await self.send(
|
|
273
|
+
cdp.target.attach_to_browser_target(), _attach=True
|
|
274
|
+
)
|
|
275
|
+
self.handlers[cdp.target.AttachedToTarget] = []
|
|
276
|
+
self.handlers.clear()
|
|
277
|
+
if self._auto_attach:
|
|
278
|
+
self.handlers[cdp.target.AttachedToTarget].append(self._attach_handler)
|
|
279
|
+
self.handlers[cdp.target.DetachedFromTarget].append(
|
|
280
|
+
self._attach_handler
|
|
281
|
+
)
|
|
282
|
+
await self.send(
|
|
283
|
+
cdp.target.auto_attach_related(self.target.target_id, False)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
else:
|
|
287
|
+
target_id = None
|
|
288
|
+
if isinstance(target, cdp.target.TargetInfo):
|
|
289
|
+
target_id = target.target_id
|
|
290
|
+
elif isinstance(target, (cdp.target.TargetID, str)):
|
|
291
|
+
target_id = target
|
|
292
|
+
self.session_id = await self.send(
|
|
293
|
+
cdp.target.attach_to_target(target_id=target_id, flatten=True),
|
|
294
|
+
_attach=True,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
self.target = await self.send(cdp.target.get_target_info(target_id))
|
|
298
|
+
if self._auto_attach:
|
|
299
|
+
self.handlers.clear()
|
|
300
|
+
self.handlers[cdp.target.AttachedToTarget].append(self._attach_handler)
|
|
301
|
+
self.handlers[cdp.target.DetachedFromTarget].append(
|
|
302
|
+
self._attach_handler
|
|
303
|
+
)
|
|
304
|
+
self.handlers[cdp.target.TargetInfoChanged].append(self._attach_handler)
|
|
305
|
+
self.handlers[cdp.target.TargetCrashed].append(self._attach_handler)
|
|
306
|
+
await self.send(
|
|
307
|
+
cdp.target.set_auto_attach(
|
|
308
|
+
True, wait_for_debugger_on_start=False, flatten=True
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
# self.target = await self.send(cdp.target.get_target_info(target_id))
|
|
312
|
+
|
|
313
|
+
async def detach(self):
|
|
314
|
+
|
|
315
|
+
for child in self._targets:
|
|
316
|
+
await child.detach()
|
|
317
|
+
await self.send(
|
|
318
|
+
cdp.target.detach_from_target(
|
|
319
|
+
session_id=self.session_id, target_id=self.target.target_id
|
|
320
|
+
),
|
|
321
|
+
_attach=True,
|
|
322
|
+
)
|
|
323
|
+
self.session_id = None
|
|
324
|
+
|
|
325
|
+
self.handlers.clear()
|
|
326
|
+
self._targets.clear()
|
|
327
|
+
|
|
328
|
+
async def _attach_handler(
|
|
329
|
+
self,
|
|
330
|
+
event: (
|
|
331
|
+
cdp.target.AttachedToTarget
|
|
332
|
+
| cdp.target.DetachedFromTarget
|
|
333
|
+
| cdp.target.TargetInfoChanged
|
|
334
|
+
| cdp.target.TargetDestroyed
|
|
335
|
+
),
|
|
336
|
+
):
|
|
337
|
+
if isinstance(event, cdp.target.AttachedToTarget):
|
|
338
|
+
if event.target_info.target_id == self.target.target_id:
|
|
339
|
+
self.session_id = event.session_id
|
|
340
|
+
self.target = event.target_info
|
|
341
|
+
else:
|
|
342
|
+
if event.target_info.type_ == "browser":
|
|
343
|
+
return
|
|
344
|
+
elif event.target_info.type_ == "page":
|
|
345
|
+
from mithwire.core.tab import Tab
|
|
346
|
+
|
|
347
|
+
new_child = Tab(target=event.target_info, parent=self)
|
|
348
|
+
self._targets.append(new_child)
|
|
349
|
+
elif event.target_info.type_ == "iframe":
|
|
350
|
+
from mithwire.core.tab import IFrame
|
|
351
|
+
|
|
352
|
+
new_child = IFrame(target=event.target_info, parent=self)
|
|
353
|
+
self._targets.append(new_child)
|
|
354
|
+
else:
|
|
355
|
+
new_child = Connection(target=event.target_info, parent=self)
|
|
356
|
+
self._targets.append(new_child)
|
|
357
|
+
|
|
358
|
+
elif isinstance(event, cdp.target.DetachedFromTarget):
|
|
359
|
+
if event.target_id == self.target.target_id:
|
|
360
|
+
self.session_id = None
|
|
361
|
+
removed = [
|
|
362
|
+
child for child in self._targets if child.session_id == event.session_id
|
|
363
|
+
]
|
|
364
|
+
for child in removed:
|
|
365
|
+
self._targets.remove(child)
|
|
366
|
+
|
|
367
|
+
elif isinstance(event, cdp.target.TargetInfoChanged):
|
|
368
|
+
if event.target_info.target_id == self.target.target_id:
|
|
369
|
+
self.target = event.target_info
|
|
370
|
+
else:
|
|
371
|
+
for child in self._targets:
|
|
372
|
+
if (
|
|
373
|
+
child.target
|
|
374
|
+
and child.target.target_id == event.target_info.target_id
|
|
375
|
+
):
|
|
376
|
+
child.target = event.target_info
|
|
377
|
+
|
|
378
|
+
elif isinstance(event, cdp.target.TargetDestroyed):
|
|
379
|
+
if event.target_id == self.target.target_id:
|
|
380
|
+
self.target = None
|
|
381
|
+
else:
|
|
382
|
+
for child in self._targets.copy():
|
|
383
|
+
if child.target.target_id == event.target_id:
|
|
384
|
+
self._targets.remove(child)
|
|
385
|
+
|
|
386
|
+
async def send(
|
|
387
|
+
self,
|
|
388
|
+
cdp_obj: Generator[dict[str, Any], dict[str, Any], Any],
|
|
389
|
+
_attach: bool = False,
|
|
390
|
+
**kwargs,
|
|
391
|
+
) -> Any:
|
|
392
|
+
|
|
393
|
+
# if self.parent and self.session_id:
|
|
394
|
+
# return await self.parent.send(cdp_obj, **kwargs)
|
|
395
|
+
|
|
396
|
+
if not _attach:
|
|
397
|
+
if not self.attached or not self.socket:
|
|
398
|
+
await self.attach()
|
|
399
|
+
|
|
400
|
+
method, *params = next(cdp_obj).values()
|
|
401
|
+
if params:
|
|
402
|
+
params = params.pop()
|
|
403
|
+
_id = next(self.__count__)
|
|
404
|
+
message = {"method": method, "params": params, "id": _id}
|
|
405
|
+
if not _attach:
|
|
406
|
+
message["sessionId"] = self.session_id
|
|
407
|
+
message.update(kwargs)
|
|
408
|
+
|
|
409
|
+
tx = Transaction(message)
|
|
410
|
+
self.transactions.append(tx)
|
|
411
|
+
self._tx_by_id[_id] = tx
|
|
412
|
+
|
|
413
|
+
future = asyncio.get_running_loop().create_future()
|
|
414
|
+
self._mapper[_id] = future
|
|
415
|
+
|
|
416
|
+
while len(self.transactions) > 25:
|
|
417
|
+
# clears the oldest request/response pair from transactions
|
|
418
|
+
removed_tx = self.transactions.pop(0)
|
|
419
|
+
if removed_tx.id is not None:
|
|
420
|
+
self._tx_by_id.pop(removed_tx.id, None)
|
|
421
|
+
|
|
422
|
+
ws = self.socket
|
|
423
|
+
if ws is None:
|
|
424
|
+
raise RuntimeError("WebSocket is not connected")
|
|
425
|
+
|
|
426
|
+
async with self.lock:
|
|
427
|
+
try:
|
|
428
|
+
await ws.send(json.dumps(message))
|
|
429
|
+
except Exception:
|
|
430
|
+
self._mapper.pop(_id, None)
|
|
431
|
+
if not future.done():
|
|
432
|
+
future.cancel()
|
|
433
|
+
raise
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
response_message = await future
|
|
437
|
+
finally:
|
|
438
|
+
self._mapper.pop(_id, None)
|
|
439
|
+
|
|
440
|
+
if "error" in response_message:
|
|
441
|
+
exception = ProtocolException(response_message["error"])
|
|
442
|
+
tx.result = exception
|
|
443
|
+
raise exception
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
cdp_obj.send(response_message["result"])
|
|
447
|
+
except StopIteration as e:
|
|
448
|
+
tx.result = e.value
|
|
449
|
+
return e.value
|
|
450
|
+
except (Exception,) as e:
|
|
451
|
+
tx.result = e
|
|
452
|
+
raise
|
|
453
|
+
|
|
454
|
+
async def _listener(self):
|
|
455
|
+
ws = self.socket
|
|
456
|
+
if ws is None:
|
|
457
|
+
raise RuntimeError("Listener started without an active socket connection.")
|
|
458
|
+
|
|
459
|
+
while True:
|
|
460
|
+
try:
|
|
461
|
+
raw = await ws.recv()
|
|
462
|
+
if not raw:
|
|
463
|
+
continue
|
|
464
|
+
message = json.loads(raw)
|
|
465
|
+
|
|
466
|
+
if "id" in message:
|
|
467
|
+
future = self._mapper.pop(message["id"], None)
|
|
468
|
+
if future and not future.done():
|
|
469
|
+
future.set_result(message)
|
|
470
|
+
elif future and future.done():
|
|
471
|
+
logger.warning(
|
|
472
|
+
"received response for already-completed future id=%s",
|
|
473
|
+
message.get("id"),
|
|
474
|
+
)
|
|
475
|
+
elif "method" in message:
|
|
476
|
+
# Unsolicited events are out-of-band unless an explicit tx id is provided.
|
|
477
|
+
await self.process_event(message, None)
|
|
478
|
+
|
|
479
|
+
except websockets.exceptions.ConnectionClosed:
|
|
480
|
+
self._fail_pending_futures(ConnectionError("Connection closed"))
|
|
481
|
+
break
|
|
482
|
+
except asyncio.CancelledError as e:
|
|
483
|
+
self._fail_pending_futures(e)
|
|
484
|
+
break
|
|
485
|
+
except Exception as e:
|
|
486
|
+
logger.error(f"background listener error: {e}", exc_info=True)
|
|
487
|
+
|
|
488
|
+
async def process_event(self, message: dict, tx_id: int | None = None) -> None:
|
|
489
|
+
""" """
|
|
490
|
+
event = None
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
event = cdp.util.parse_json_event(message)
|
|
494
|
+
tx = self._tx_by_id.get(tx_id) if tx_id is not None else None
|
|
495
|
+
if tx_id is not None and tx is None:
|
|
496
|
+
logger.debug(
|
|
497
|
+
"received event for unknown transaction id=%s method=%s",
|
|
498
|
+
tx_id,
|
|
499
|
+
message.get("method"),
|
|
500
|
+
)
|
|
501
|
+
if tx is not None:
|
|
502
|
+
tx.events.append(event)
|
|
503
|
+
except KeyError as e:
|
|
504
|
+
logger.exception(e)
|
|
505
|
+
return
|
|
506
|
+
if type(event) in self.handlers:
|
|
507
|
+
callbacks = self.handlers[type(event)]
|
|
508
|
+
else:
|
|
509
|
+
return
|
|
510
|
+
if not callbacks:
|
|
511
|
+
return
|
|
512
|
+
for callback in callbacks:
|
|
513
|
+
try:
|
|
514
|
+
if iscoroutinefunction(callback) or iscoroutine(callback):
|
|
515
|
+
try:
|
|
516
|
+
# handler is defined with the second param being the tab instance
|
|
517
|
+
asyncio.create_task(callback(event, self))
|
|
518
|
+
except TypeError as e:
|
|
519
|
+
# handler is defined without the tab parameter
|
|
520
|
+
asyncio.create_task(callback(event))
|
|
521
|
+
# if this causes an exception it is handled by the outer handler
|
|
522
|
+
else:
|
|
523
|
+
try:
|
|
524
|
+
# handler is defined with the second param being the tab instance
|
|
525
|
+
callback(event, self)
|
|
526
|
+
except TypeError:
|
|
527
|
+
# handler is defined without the tab parameter
|
|
528
|
+
callback(event)
|
|
529
|
+
# if this causes an exception it is handled by the outer handler
|
|
530
|
+
|
|
531
|
+
except Exception as e:
|
|
532
|
+
logger.warning(
|
|
533
|
+
"exception in callback %s for event %s => %s",
|
|
534
|
+
callback,
|
|
535
|
+
event.__class__.__name__,
|
|
536
|
+
e,
|
|
537
|
+
exc_info=True,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def __str__(self):
|
|
541
|
+
name = self.__class__.__name__
|
|
542
|
+
if self.target and self.target.type_:
|
|
543
|
+
name = self.target.type_.title()
|
|
544
|
+
name = util.to_camel(name)
|
|
545
|
+
return (
|
|
546
|
+
f"<{name}\n"
|
|
547
|
+
f"\turl: {self.target.url if type(self.target) is cdp.target.TargetInfo else ''}\n"
|
|
548
|
+
f"\tattached: {self.attached}>\n"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
#
|
|
552
|
+
# def __hash__(self):
|
|
553
|
+
# return hash(frozenset([self.target.target_id, self.session_id]))
|
|
554
|
+
|
|
555
|
+
# def __eq__(self, other: Union[Connection]):
|
|
556
|
+
# try:
|
|
557
|
+
# assert self.session_id and other.session_id and other.session_id == self.session_id
|
|
558
|
+
# assert self.target and other.target and self.target.target_id == other.target.target_id
|
|
559
|
+
# return True
|
|
560
|
+
# except (Exception,):
|
|
561
|
+
# return NotImplemented
|
|
562
|
+
|
|
563
|
+
def __repr__(self):
|
|
564
|
+
return self.__str__()
|