modcdp 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modcdp-0.0.2/.gitignore +5 -0
- modcdp-0.0.2/PKG-INFO +12 -0
- modcdp-0.0.2/README.md +3 -0
- modcdp-0.0.2/demo.py +379 -0
- modcdp-0.0.2/modcdp/ModCDPClient.py +912 -0
- modcdp-0.0.2/modcdp/__init__.py +3 -0
- modcdp-0.0.2/modcdp/translate.py +262 -0
- modcdp-0.0.2/modcdp/types.py +170 -0
- modcdp-0.0.2/pyproject.toml +23 -0
- modcdp-0.0.2/uv.lock +90 -0
modcdp-0.0.2/.gitignore
ADDED
modcdp-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modcdp
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Python client for ModCDP.
|
|
5
|
+
Project-URL: Repository, https://github.com/pirate/ModCDP
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: websocket-client>=1.8.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# ModCDP Python Client
|
|
11
|
+
|
|
12
|
+
Python client for ModCDP.
|
modcdp-0.0.2/README.md
ADDED
modcdp-0.0.2/demo.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Python demo for ModCDPClient. Mirrors client/js/demo.js.
|
|
2
|
+
|
|
3
|
+
Modes (mirror the JS / Go demos):
|
|
4
|
+
--live Use the running Google Chrome enabled via chrome://inspect.
|
|
5
|
+
--direct *.* -> direct_cdp on the client.
|
|
6
|
+
--loopback *.* -> service_worker on the client; *.* -> loopback_cdp on
|
|
7
|
+
the server. Default.
|
|
8
|
+
--debugger *.* -> service_worker on the client; *.* -> chrome_debugger
|
|
9
|
+
on the server.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
from modcdp import ModCDPClient
|
|
23
|
+
from modcdp.types import JsonValue, ProtocolPayload
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
26
|
+
EXTENSION_PATH = ROOT / "dist" / "extension"
|
|
27
|
+
LIVE_DEVTOOLS_ACTIVE_PORTS = [
|
|
28
|
+
Path.home() / "Library" / "Application Support" / "Google" / "Chrome" / "DevToolsActivePort",
|
|
29
|
+
Path.home() / "Library" / "Application Support" / "Google" / "Chrome Beta" / "DevToolsActivePort",
|
|
30
|
+
] if sys.platform == "darwin" else [
|
|
31
|
+
Path.home() / ".config" / "google-chrome" / "DevToolsActivePort",
|
|
32
|
+
Path.home() / ".config" / "chromium" / "DevToolsActivePort",
|
|
33
|
+
]
|
|
34
|
+
CHROME = os.environ.get("CHROME_PATH") or (
|
|
35
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
|
|
36
|
+
if sys.platform == "darwin"
|
|
37
|
+
else "/opt/pw-browsers/chromium-1194/chrome-linux/chrome"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def expect_object(value: JsonValue, label: str) -> ProtocolPayload:
|
|
42
|
+
if not isinstance(value, dict):
|
|
43
|
+
raise RuntimeError(f"{label} returned non-object value: {value!r}")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def server_routes_for(mode: str) -> ProtocolPayload:
|
|
48
|
+
route = "loopback_cdp" if mode == "loopback" else "chrome_debugger" if mode == "debugger" else "auto"
|
|
49
|
+
routes: ProtocolPayload = {
|
|
50
|
+
"Mod.*": "service_worker",
|
|
51
|
+
"Custom.*": "service_worker",
|
|
52
|
+
"*.*": route,
|
|
53
|
+
}
|
|
54
|
+
return routes
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def client_options_for(mode, cdp_url, launch_options=None):
|
|
58
|
+
direct_normal_event_routes = {
|
|
59
|
+
"Target.setDiscoverTargets": "direct_cdp",
|
|
60
|
+
"Target.createTarget": "direct_cdp",
|
|
61
|
+
"Target.activateTarget": "direct_cdp",
|
|
62
|
+
}
|
|
63
|
+
if mode == "direct":
|
|
64
|
+
return {
|
|
65
|
+
"cdp_url": cdp_url,
|
|
66
|
+
"extension_path": str(EXTENSION_PATH),
|
|
67
|
+
"launch_options": launch_options or {},
|
|
68
|
+
"routes": {"Mod.*": "service_worker", "Custom.*": "service_worker", "*.*": "direct_cdp", **direct_normal_event_routes},
|
|
69
|
+
}
|
|
70
|
+
server = {
|
|
71
|
+
"routes": server_routes_for(mode),
|
|
72
|
+
}
|
|
73
|
+
if cdp_url and mode == "loopback":
|
|
74
|
+
server["loopback_cdp_url"] = cdp_url
|
|
75
|
+
return {
|
|
76
|
+
"cdp_url": cdp_url,
|
|
77
|
+
"extension_path": str(EXTENSION_PATH),
|
|
78
|
+
"launch_options": launch_options or {},
|
|
79
|
+
"routes": {"Mod.*": "service_worker", "Custom.*": "service_worker", "*.*": "service_worker", **direct_normal_event_routes},
|
|
80
|
+
"server": server,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def wait_for_live_cdp_url():
|
|
85
|
+
started_at = time.time()
|
|
86
|
+
opener = ["open", "chrome://inspect/#remote-debugging"] if sys.platform == "darwin" else ["xdg-open", "chrome://inspect/#remote-debugging"]
|
|
87
|
+
subprocess.Popen(opener, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
88
|
+
print("opened chrome://inspect/#remote-debugging")
|
|
89
|
+
print("waiting for Chrome to expose DevToolsActivePort; click Allow when Chrome asks.")
|
|
90
|
+
while True:
|
|
91
|
+
for path in LIVE_DEVTOOLS_ACTIVE_PORTS:
|
|
92
|
+
try:
|
|
93
|
+
if path.stat().st_mtime < started_at - 1:
|
|
94
|
+
continue
|
|
95
|
+
lines = [line.strip() for line in path.read_text().splitlines() if line.strip()]
|
|
96
|
+
if len(lines) >= 2:
|
|
97
|
+
return f"ws://127.0.0.1:{lines[0]}{lines[1]}"
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
time.sleep(0.25)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
flags = {a[2:] for a in sys.argv[1:] if a.startswith("--")}
|
|
105
|
+
live = "live" in flags
|
|
106
|
+
mode = "debugger" if "debugger" in flags else "direct" if "direct" in flags else "loopback" if "loopback" in flags else "direct" if live else "loopback"
|
|
107
|
+
print(f"== mode: {'live/' if live else ''}{mode} ==")
|
|
108
|
+
|
|
109
|
+
cdp = None
|
|
110
|
+
try:
|
|
111
|
+
if live:
|
|
112
|
+
cdp_url = wait_for_live_cdp_url()
|
|
113
|
+
launch_options = {}
|
|
114
|
+
else:
|
|
115
|
+
cdp_url = None
|
|
116
|
+
launch_options = {
|
|
117
|
+
"executable_path": CHROME,
|
|
118
|
+
"headless": sys.platform.startswith("linux"),
|
|
119
|
+
"sandbox": not sys.platform.startswith("linux"),
|
|
120
|
+
"extra_args": [f"--load-extension={EXTENSION_PATH}"],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cdp = ModCDPClient(**client_options_for(mode, cdp_url, launch_options))
|
|
124
|
+
foreground_events = []
|
|
125
|
+
target_created_events = []
|
|
126
|
+
events_lock = threading.Lock()
|
|
127
|
+
|
|
128
|
+
def on_target_created(payload, *_):
|
|
129
|
+
print(f"Target.targetCreated -> {payload.get('targetInfo', {}).get('targetId')}")
|
|
130
|
+
with events_lock:
|
|
131
|
+
target_created_events.append(payload)
|
|
132
|
+
|
|
133
|
+
def on_foreground_changed(payload, *_):
|
|
134
|
+
print(f"Custom.foregroundTargetChanged -> {payload}")
|
|
135
|
+
with events_lock:
|
|
136
|
+
foreground_events.append(payload)
|
|
137
|
+
|
|
138
|
+
cdp.on("Target.targetCreated", on_target_created)
|
|
139
|
+
|
|
140
|
+
cdp.connect()
|
|
141
|
+
print(f"upstream cdp: {cdp.cdp_url}")
|
|
142
|
+
print(f"connected; ext {cdp.extension_id} session {cdp.ext_session_id}")
|
|
143
|
+
print(f"ping latency -> {cdp.latency}")
|
|
144
|
+
|
|
145
|
+
configure_params: ProtocolPayload = {"routes": server_routes_for(mode)}
|
|
146
|
+
if mode == "loopback":
|
|
147
|
+
if cdp.cdp_url is None:
|
|
148
|
+
raise RuntimeError("loopback mode requires a resolved cdp_url after connect")
|
|
149
|
+
configure_params["loopback_cdp_url"] = cdp.cdp_url
|
|
150
|
+
configure_result = expect_object(cdp.send("Mod.configure", configure_params), "Mod.configure")
|
|
151
|
+
if expect_object(configure_result.get("routes"), "Mod.configure.routes").get("*.*") != server_routes_for(mode)["*.*"]:
|
|
152
|
+
raise RuntimeError(f"unexpected Mod.configure result {configure_result}")
|
|
153
|
+
print(f"Mod.configure -> {configure_result.get('routes')}")
|
|
154
|
+
|
|
155
|
+
pong_events = []
|
|
156
|
+
pong_lock = threading.Lock()
|
|
157
|
+
ping_sent_at = int(time.time() * 1000)
|
|
158
|
+
|
|
159
|
+
def on_pong(payload, *_):
|
|
160
|
+
with pong_lock:
|
|
161
|
+
pong_events.append(payload)
|
|
162
|
+
|
|
163
|
+
cdp.on("Mod.pong", on_pong)
|
|
164
|
+
ping_result = expect_object(cdp.send("Mod.ping", {"sentAt": ping_sent_at}), "Mod.ping")
|
|
165
|
+
deadline = time.monotonic() + 3.0
|
|
166
|
+
while True:
|
|
167
|
+
with pong_lock:
|
|
168
|
+
pong = next((event for event in pong_events if event.get("sentAt") == ping_sent_at), None)
|
|
169
|
+
if pong or time.monotonic() >= deadline:
|
|
170
|
+
break
|
|
171
|
+
time.sleep(0.02)
|
|
172
|
+
if ping_result.get("ok") is not True or not pong or pong.get("from") != "extension-service-worker":
|
|
173
|
+
raise RuntimeError(f"unexpected Mod.ping/Mod.pong result ping={ping_result} pong={pong}")
|
|
174
|
+
print(f"Mod.ping/pong -> {ping_result} {pong}")
|
|
175
|
+
|
|
176
|
+
if mode == "debugger":
|
|
177
|
+
try:
|
|
178
|
+
version = expect_object(cdp.send("Browser.getVersion"), "Browser.getVersion")
|
|
179
|
+
if not isinstance(version.get("protocolVersion"), str) or not isinstance(version.get("product"), str):
|
|
180
|
+
raise RuntimeError(f"unexpected Browser.getVersion result {version}")
|
|
181
|
+
print(f"Browser.getVersion -> {version}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Browser.getVersion -> (debugger route rejected: {str(e).splitlines()[0]} )")
|
|
184
|
+
runtime_eval = expect_object(cdp.send("Runtime.evaluate", {"expression": "(() => 42)()", "returnByValue": True}), "Runtime.evaluate")
|
|
185
|
+
result = expect_object(runtime_eval.get("result"), "Runtime.evaluate.result")
|
|
186
|
+
if result.get("value") != 42:
|
|
187
|
+
raise RuntimeError(f"unexpected Runtime.evaluate result {runtime_eval}")
|
|
188
|
+
print(f"Runtime.evaluate -> {runtime_eval}")
|
|
189
|
+
else:
|
|
190
|
+
version = expect_object(cdp.send("Browser.getVersion"), "Browser.getVersion")
|
|
191
|
+
if not isinstance(version.get("protocolVersion"), str) or not isinstance(version.get("product"), str):
|
|
192
|
+
raise RuntimeError(f"unexpected Browser.getVersion result {version}")
|
|
193
|
+
print(f"Browser.getVersion -> {version}")
|
|
194
|
+
|
|
195
|
+
modcdp_eval = expect_object(cdp.send("Mod.evaluate", {"expression": "({ extensionId: chrome.runtime.id })"}), "Mod.evaluate")
|
|
196
|
+
if modcdp_eval.get("extensionId") != cdp.extension_id:
|
|
197
|
+
raise RuntimeError(f"unexpected Mod.evaluate result {modcdp_eval}")
|
|
198
|
+
print(f"Mod.evaluate -> {modcdp_eval}")
|
|
199
|
+
|
|
200
|
+
echo_registration = expect_object(cdp.send("Mod.addCustomCommand", {
|
|
201
|
+
"name": "Custom.echo",
|
|
202
|
+
"expression": "async (params, method) => ({ echoed: params.value, method })",
|
|
203
|
+
}), "Mod.addCustomCommand Custom.echo")
|
|
204
|
+
if echo_registration.get("registered") is not True or echo_registration.get("name") != "Custom.echo":
|
|
205
|
+
raise RuntimeError(f"unexpected Custom.echo registration {echo_registration}")
|
|
206
|
+
echo_result = expect_object(cdp.send("Custom.echo", {"value": "custom-command-ok"}), "Custom.echo")
|
|
207
|
+
if echo_result.get("echoed") != "custom-command-ok" or echo_result.get("method") != "Custom.echo":
|
|
208
|
+
raise RuntimeError(f"unexpected Custom.echo result {echo_result}")
|
|
209
|
+
print(f"Custom.echo -> {echo_result}")
|
|
210
|
+
|
|
211
|
+
tab_command_registration = expect_object(cdp.send("Mod.addCustomCommand", {
|
|
212
|
+
"name": "Custom.TabIdFromTargetId",
|
|
213
|
+
"expression": '''async ({ targetId }) => {
|
|
214
|
+
const targets = await chrome.debugger.getTargets();
|
|
215
|
+
const target = targets.find(target => target.id === targetId);
|
|
216
|
+
return { tabId: target?.tabId ?? null };
|
|
217
|
+
}''',
|
|
218
|
+
}), "Mod.addCustomCommand Custom.TabIdFromTargetId")
|
|
219
|
+
if tab_command_registration.get("registered") is not True:
|
|
220
|
+
raise RuntimeError(f"unexpected TabIdFromTargetId registration {tab_command_registration}")
|
|
221
|
+
target_command_registration = expect_object(cdp.send("Mod.addCustomCommand", {
|
|
222
|
+
"name": "Custom.targetIdFromTabId",
|
|
223
|
+
"expression": '''async ({ tabId }) => {
|
|
224
|
+
const targets = await chrome.debugger.getTargets();
|
|
225
|
+
const target = targets.find(target => target.type === "page" && target.tabId === tabId);
|
|
226
|
+
return { targetId: target?.id ?? null };
|
|
227
|
+
}''',
|
|
228
|
+
}), "Mod.addCustomCommand Custom.targetIdFromTabId")
|
|
229
|
+
if target_command_registration.get("registered") is not True:
|
|
230
|
+
raise RuntimeError(f"unexpected targetIdFromTabId registration {target_command_registration}")
|
|
231
|
+
middleware_registered = False
|
|
232
|
+
for phase in ("response", "event"):
|
|
233
|
+
middleware_registration = expect_object(cdp.send("Mod.addMiddleware", {
|
|
234
|
+
"name": "*",
|
|
235
|
+
"phase": phase,
|
|
236
|
+
"expression": '''async (payload, next) => {
|
|
237
|
+
const seen = new WeakSet();
|
|
238
|
+
const visit = async value => {
|
|
239
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
240
|
+
seen.add(value);
|
|
241
|
+
if (!Array.isArray(value) && typeof value.targetId === "string" && value.tabId == null) {
|
|
242
|
+
const { tabId } = await cdp.send("Custom.TabIdFromTargetId", { targetId: value.targetId });
|
|
243
|
+
if (tabId != null) value.tabId = tabId;
|
|
244
|
+
}
|
|
245
|
+
for (const child of Array.isArray(value) ? value : Object.values(value)) await visit(child);
|
|
246
|
+
};
|
|
247
|
+
await visit(payload);
|
|
248
|
+
return next(payload);
|
|
249
|
+
}''',
|
|
250
|
+
}), f"Mod.addMiddleware {phase}")
|
|
251
|
+
if middleware_registration.get("registered") is not True or middleware_registration.get("phase") != phase:
|
|
252
|
+
raise RuntimeError(f"unexpected {phase} middleware registration {middleware_registration}")
|
|
253
|
+
middleware_registered = True
|
|
254
|
+
if not middleware_registered:
|
|
255
|
+
raise RuntimeError("middleware registration loop did not run")
|
|
256
|
+
|
|
257
|
+
demo_events = []
|
|
258
|
+
demo_lock = threading.Lock()
|
|
259
|
+
|
|
260
|
+
def on_demo_event(payload, *_):
|
|
261
|
+
with demo_lock:
|
|
262
|
+
demo_events.append(payload)
|
|
263
|
+
|
|
264
|
+
demo_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.demoEvent"}), "Mod.addCustomEvent Custom.demoEvent")
|
|
265
|
+
if demo_event_registration.get("registered") is not True or demo_event_registration.get("name") != "Custom.demoEvent":
|
|
266
|
+
raise RuntimeError(f"unexpected Custom.demoEvent registration {demo_event_registration}")
|
|
267
|
+
cdp.on("Custom.demoEvent", on_demo_event)
|
|
268
|
+
emit_result = expect_object(cdp.send("Mod.evaluate", {"expression": '''async () => await ModCDP.emit("Custom.demoEvent", { value: "custom-event-ok" })'''}), "Custom.demoEvent emit")
|
|
269
|
+
if emit_result.get("emitted") is not True:
|
|
270
|
+
raise RuntimeError(f"unexpected Custom.demoEvent emit result {emit_result}")
|
|
271
|
+
deadline = time.monotonic() + 3.0
|
|
272
|
+
while True:
|
|
273
|
+
with demo_lock:
|
|
274
|
+
demo_event = next((event for event in demo_events if event.get("value") == "custom-event-ok"), None)
|
|
275
|
+
if demo_event or time.monotonic() >= deadline:
|
|
276
|
+
break
|
|
277
|
+
time.sleep(0.02)
|
|
278
|
+
if not demo_event:
|
|
279
|
+
raise RuntimeError("expected Custom.demoEvent")
|
|
280
|
+
print(f"Custom.demoEvent -> {demo_event}")
|
|
281
|
+
|
|
282
|
+
foreground_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.foregroundTargetChanged"}), "Mod.addCustomEvent Custom.foregroundTargetChanged")
|
|
283
|
+
if foreground_event_registration.get("registered") is not True:
|
|
284
|
+
raise RuntimeError(f"unexpected foreground event registration {foreground_event_registration}")
|
|
285
|
+
cdp.on("Custom.foregroundTargetChanged", on_foreground_changed)
|
|
286
|
+
cdp.send("Mod.evaluate", {"expression": '''chrome.tabs.onActivated.addListener(async ({ tabId }) => {
|
|
287
|
+
const targets = await chrome.debugger.getTargets();
|
|
288
|
+
const target = targets.find(target => target.type === "page" && target.tabId === tabId);
|
|
289
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
290
|
+
await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null });
|
|
291
|
+
})'''})
|
|
292
|
+
|
|
293
|
+
cdp.send("Target.setDiscoverTargets", {"discover": True})
|
|
294
|
+
created_target = expect_object(cdp.send("Target.createTarget", {"url": "https://example.com", "background": True}), "Target.createTarget")
|
|
295
|
+
created_target_id = created_target.get("targetId")
|
|
296
|
+
if not created_target_id:
|
|
297
|
+
raise RuntimeError(f"Target.createTarget returned no targetId: {created_target}")
|
|
298
|
+
deadline = time.monotonic() + 3.0
|
|
299
|
+
while True:
|
|
300
|
+
with events_lock:
|
|
301
|
+
matched_target_event = next((event for event in target_created_events if event.get("targetInfo", {}).get("targetId") == created_target_id), None)
|
|
302
|
+
if matched_target_event or time.monotonic() >= deadline:
|
|
303
|
+
break
|
|
304
|
+
time.sleep(0.02)
|
|
305
|
+
if not matched_target_event:
|
|
306
|
+
raise RuntimeError(f"expected Target.targetCreated for {created_target_id}")
|
|
307
|
+
print(f"normal event matched -> {created_target_id}")
|
|
308
|
+
|
|
309
|
+
tab_from_target = expect_object(cdp.send("Custom.TabIdFromTargetId", {"targetId": created_target_id}), "Custom.TabIdFromTargetId")
|
|
310
|
+
if not isinstance(tab_from_target.get("tabId"), int | float):
|
|
311
|
+
raise RuntimeError(f"unexpected Custom.TabIdFromTargetId result {tab_from_target}")
|
|
312
|
+
print(f"Custom.TabIdFromTargetId -> {tab_from_target}")
|
|
313
|
+
|
|
314
|
+
cdp.send("Target.activateTarget", {"targetId": created_target_id})
|
|
315
|
+
deadline = time.monotonic() + 3.0
|
|
316
|
+
while True:
|
|
317
|
+
with events_lock:
|
|
318
|
+
foreground = next((event for event in foreground_events if event.get("targetId") == created_target_id), None)
|
|
319
|
+
if foreground or time.monotonic() >= deadline:
|
|
320
|
+
break
|
|
321
|
+
time.sleep(0.02)
|
|
322
|
+
if not foreground:
|
|
323
|
+
raise RuntimeError(f"expected Custom.foregroundTargetChanged for {created_target_id}")
|
|
324
|
+
if tab_from_target.get("tabId") != foreground.get("tabId"):
|
|
325
|
+
raise RuntimeError(f"unexpected Custom.foregroundTargetChanged result {foreground}")
|
|
326
|
+
|
|
327
|
+
target_from_tab = expect_object(cdp.send("Custom.targetIdFromTabId", {"tabId": foreground["tabId"]}), "Custom.targetIdFromTabId")
|
|
328
|
+
if target_from_tab.get("targetId") != created_target_id or target_from_tab.get("tabId") != foreground.get("tabId"):
|
|
329
|
+
raise RuntimeError(f"unexpected Custom.targetIdFromTabId/middleware result {target_from_tab}")
|
|
330
|
+
print(f"Custom.targetIdFromTabId -> {target_from_tab}")
|
|
331
|
+
|
|
332
|
+
print(f"\nSUCCESS ({mode}): normal command, normal event, custom commands, custom event, and middleware all passed")
|
|
333
|
+
|
|
334
|
+
# TTY-only: drop into a REPL where you can send live commands and
|
|
335
|
+
# watch events as they print. Skip when run non-interactively so the
|
|
336
|
+
# demo stays CI-friendly.
|
|
337
|
+
if sys.stdin.isatty():
|
|
338
|
+
cdp.on("Mod.pong", lambda e: print(f"\n[event] Mod.pong {e}"))
|
|
339
|
+
run_repl(cdp, mode)
|
|
340
|
+
|
|
341
|
+
return 0
|
|
342
|
+
finally:
|
|
343
|
+
if cdp is not None:
|
|
344
|
+
try: cdp.close()
|
|
345
|
+
except Exception: pass
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def run_repl(cdp, mode):
|
|
349
|
+
import re
|
|
350
|
+
print(f"\nBrowser remains running. Mode: {mode}.")
|
|
351
|
+
print("Enter commands as Domain.method({...JSON params...}). Examples:")
|
|
352
|
+
print(' Browser.getVersion({})')
|
|
353
|
+
print(' Mod.evaluate({"expression": "chrome.tabs.query({active: true})"})')
|
|
354
|
+
print(' Custom.TabIdFromTargetId({"targetId": "..."})')
|
|
355
|
+
print("Type exit or quit to disconnect (browser keeps running).")
|
|
356
|
+
cmd_re = re.compile(r"^([A-Za-z_]\w*\.[A-Za-z_]\w*)(?:\((.*)\))?$")
|
|
357
|
+
while True:
|
|
358
|
+
try:
|
|
359
|
+
line = input("ModCDP> ").strip()
|
|
360
|
+
except (EOFError, KeyboardInterrupt):
|
|
361
|
+
print()
|
|
362
|
+
break
|
|
363
|
+
if not line: continue
|
|
364
|
+
if line in ("exit", "quit"): break
|
|
365
|
+
try:
|
|
366
|
+
m = cmd_re.match(line)
|
|
367
|
+
if not m:
|
|
368
|
+
raise ValueError("format: Domain.method({...JSON...})")
|
|
369
|
+
method = m.group(1)
|
|
370
|
+
raw = (m.group(2) or "").strip()
|
|
371
|
+
params = json.loads(raw) if raw else {}
|
|
372
|
+
result = cdp.send(method, params)
|
|
373
|
+
print(json.dumps(result, indent=2))
|
|
374
|
+
except Exception as e:
|
|
375
|
+
print(f"error: {e}")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == "__main__":
|
|
379
|
+
sys.exit(main())
|