@mefistofelix/cdp.js 0.6.0 → 0.7.0
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.
- package/README.md +165 -17
- package/cdp.js +63 -16
- package/cdp_ext/cdp_ext.js +136 -37
- package/cdp_ext/manifest.json +1 -1
- package/cdp_ext/sidebar.html +6 -4
- package/cdp_ext/sidebar.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above
|
|
|
43
43
|
without browser remote-debugging arguments.
|
|
44
44
|
- Generic `chrome.x.y` extension API calls on that connection, including scripting,
|
|
45
45
|
tabs/windows, cookies, storage, downloads and browser settings.
|
|
46
|
+
- Reverse target management and XPath helpers default to browser APIs, with
|
|
47
|
+
optional debugger execution; explicit native CDP commands still execute.
|
|
46
48
|
- Shared initialization for concurrent calls; reconnection and target recreation
|
|
47
49
|
on subsequent use, without replaying failed commands.
|
|
48
50
|
- Public object mappings and directly replaceable custom method handlers.
|
|
@@ -119,7 +121,7 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
|
|
|
119
121
|
| --- | --- |
|
|
120
122
|
| `new cdp(options = {})` | Create a manager; does not launch a browser yet. |
|
|
121
123
|
| `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
|
|
122
|
-
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath
|
|
124
|
+
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath. Direct/debugger mode returns native `Runtime.evaluate` data; reverse API mode returns `{ result: { type, value } }`. |
|
|
123
125
|
| `client.close()` | Close owned processes, disconnect attached/reverse browsers, stop the reverse server and clear browser/socket mappings. Profiles and external browsers remain. |
|
|
124
126
|
| `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
|
|
125
127
|
| `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
|
|
@@ -529,6 +531,19 @@ the local URL and connects to the waiting manager. The user enters no extension
|
|
|
529
531
|
ID or WebSocket address. Load the package's `cdp_ext/` once through
|
|
530
532
|
`chrome://extensions` (developer mode, Load unpacked); reload it after upgrading.
|
|
531
533
|
|
|
534
|
+
After accepting a trigger, the extension records `cdp_ext.trigger` in its debug
|
|
535
|
+
sidebar and closes that trigger tab automatically, including when the server is
|
|
536
|
+
currently offline. If it is Chrome's only tab, it becomes the extension's
|
|
537
|
+
**CDP.js connection** page instead, keeping Chrome available for subsequent calls.
|
|
538
|
+
This reuses the debug view and displays the logical browser and live connected/
|
|
539
|
+
waiting status; it never claims a connection before one exists. No extra
|
|
540
|
+
tab is created. Other loopback pages are preserved: only the exact `/cdp_ext`
|
|
541
|
+
route with non-empty `browser`, `websocket_url` and `token` parameters and a
|
|
542
|
+
same-host/port `ws:` endpoint is accepted. The tab URL is checked again before
|
|
543
|
+
cleanup, so a tab already navigated elsewhere is left alone. Duplicate tab updates
|
|
544
|
+
reuse the trigger's diagnostic entry. A conflicting live browser binding is
|
|
545
|
+
reported and its trigger page is cleaned up without replacing the connection.
|
|
546
|
+
|
|
532
547
|
```js
|
|
533
548
|
const client = new cdp({
|
|
534
549
|
cdp_ext: true,
|
|
@@ -648,8 +663,9 @@ native send/close and live ready state. The same `jsrpc` handles both runtimes;
|
|
|
648
663
|
registration, request IDs, pending waits, events and session routing are shared.
|
|
649
664
|
Native Bun WebSocket limits still apply.
|
|
650
665
|
|
|
651
|
-
Reverse calls
|
|
652
|
-
|
|
666
|
+
Reverse calls share target selection, custom handlers, mappings, routed events,
|
|
667
|
+
request correlation, timeout and error behavior. Implicit CDP setup is opt-in
|
|
668
|
+
through `target.execution_mode: 'debugger'`. `reverse: true` alone
|
|
653
669
|
attaches externally: the manager neither launches nor updates that browser.
|
|
654
670
|
`cdp_ext_install: true` owns the launched process and applies normal launch preferences.
|
|
655
671
|
With installation off, Chrome is external and remains open on manager close.
|
|
@@ -688,15 +704,27 @@ network diagnostic if the server disappears between the HTTP check and upgrade.
|
|
|
688
704
|
### Debug sidebar
|
|
689
705
|
|
|
690
706
|
Click the extension's toolbar icon to open its native Chrome side panel.
|
|
707
|
+
If the icon is hidden, open Chrome's Extensions menu (the puzzle piece), find
|
|
708
|
+
**CDP reverse connection**, and pin it or click its entry. The panel opens on a
|
|
709
|
+
user action; loading the extension or connecting a browser does not open it.
|
|
710
|
+
After updating the package, an already running Chrome still has its previously
|
|
711
|
+
loaded extension: reload it in `chrome://extensions` after updating its directory,
|
|
712
|
+
or restart that custom-profile browser through a fresh manager. For a manually
|
|
713
|
+
installed extension, update its installed files too. Reopening an old side panel
|
|
714
|
+
alone does not upgrade the worker.
|
|
715
|
+
|
|
691
716
|
It shows the current connection status, logical browser name, WebSocket endpoint,
|
|
692
|
-
native extension ID
|
|
717
|
+
native extension ID, number of routing sessions and number of debugger-attached
|
|
718
|
+
root tabs. A routing session does not imply debugger attachment. It works before
|
|
693
719
|
the first trigger too, displaying **Waiting for trigger** without starting a
|
|
694
720
|
server connection.
|
|
695
721
|
|
|
696
|
-
The most recent 50
|
|
697
|
-
method, optional session ID, running/done/error status, elapsed milliseconds and
|
|
722
|
+
The most recent 50 commands and trigger entries appear newest first, with request ID,
|
|
723
|
+
method, optional session ID/execution mode, running/done/error status, elapsed milliseconds and
|
|
698
724
|
error text. This is command metadata only: parameters, results and the trigger
|
|
699
|
-
token are not included.
|
|
725
|
+
token are not included. Trigger entries show the tab ID, browser label, endpoint
|
|
726
|
+
and cleanup action; the full trigger URL is never recorded. Empty heartbeat
|
|
727
|
+
messages and CDP events are not logged.
|
|
700
728
|
**Clear** empties this history without cancelling commands or changing the
|
|
701
729
|
connection. Reopening the panel retrieves the current snapshot; a worker restart
|
|
702
730
|
or extension reload clears the history. No debug history is written to storage.
|
|
@@ -736,8 +764,10 @@ Worker and out-of-process iframe commands still use real flattened CDP sessions.
|
|
|
736
764
|
Their native session IDs pass through unchanged, without prefixes. The extension
|
|
737
765
|
uses one `sessions` object for both roots and children: a tab ID maps to `{ tabId }`,
|
|
738
766
|
and a native child session ID maps to `{ tabId, sessionId }`. Every target command
|
|
739
|
-
looks up that object
|
|
740
|
-
|
|
767
|
+
looks up that object. API-mode root selection only registers the routing reference;
|
|
768
|
+
an explicit session CDP command attaches its root lazily, sharing concurrent
|
|
769
|
+
attachment, then calls `chrome.debugger.sendCommand`. There is no fallback that
|
|
770
|
+
interprets unknown session IDs as tab IDs.
|
|
741
771
|
Explicit detach takes `params.sessionId` for either a tab or child
|
|
742
772
|
session. Logical target labels remain inside the manager. Reconnecting invalidates
|
|
743
773
|
attachment/setup state even though a surviving root tab's routing ID stays the same.
|
|
@@ -752,12 +782,13 @@ tab and focuses its window. `Browser.getVersion` returns product/user-agent data
|
|
|
752
782
|
its protocol version is `1.3`, with empty `revision` and `jsVersion` fields.
|
|
753
783
|
Other browser-level commands return explicit unsupported-command errors.
|
|
754
784
|
|
|
755
|
-
|
|
785
|
+
Explicit native target/session CDP commands go through
|
|
756
786
|
[chrome.debugger](https://developer.chrome.com/docs/extensions/reference/api/debugger),
|
|
757
787
|
which supports a restricted set of CDP domains. Runtime, Page, Network, DOM,
|
|
758
788
|
Input and Emulation are available; ServiceWorker and BackgroundService are not.
|
|
759
|
-
Their
|
|
760
|
-
those setup flags if desired.
|
|
789
|
+
Their setup attempts in debugger target mode therefore appear in
|
|
790
|
+
`target.setup_errors`; disable those setup flags if desired. API targets skip
|
|
791
|
+
that setup. Chrome restrictions, policies and debugger detach
|
|
761
792
|
events remain visible. Opening DevTools on an attached tab can detach this debugger.
|
|
762
793
|
Firefox/Camoufox support remains planned.
|
|
763
794
|
|
|
@@ -1335,7 +1366,8 @@ await client.call({
|
|
|
1335
1366
|
| `incognito` | `false` | Create/select a private target; direct mode uses the shared `'incognito'` context by default, reverse uses a native incognito window with the user's extension grant. |
|
|
1336
1367
|
| `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
|
|
1337
1368
|
| `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
|
|
1338
|
-
| `
|
|
1369
|
+
| `execution_mode` | `'api'` in reverse | Reverse-only choice for implicit target setup and built-in XPath helpers: `'api'` uses extension APIs; `'debugger'` attaches and runs CDP setup. Direct CDP ignores it. |
|
|
1370
|
+
| `initialize` | `true` | Run the convenience CDP setup sequence in direct/debugger mode; false skips it. API mode skips that sequence regardless. |
|
|
1339
1371
|
| `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
|
|
1340
1372
|
| `page` | `true` | Enable Page domain reporting for navigation/page operations and events. |
|
|
1341
1373
|
| `network` | `true` | Enable Network reporting so native network events are available. |
|
|
@@ -1344,6 +1376,102 @@ await client.call({
|
|
|
1344
1376
|
| `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
|
|
1345
1377
|
| `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
|
|
1346
1378
|
|
|
1379
|
+
### Reverse execution mode
|
|
1380
|
+
|
|
1381
|
+
In extension mode (`cdp_ext: true` or passive `reverse: true`), a target defaults
|
|
1382
|
+
to `execution_mode: 'api'`. Creating/selecting it, grouping it and running
|
|
1383
|
+
`_.find`/`_.click` do not attach the debugger. Root tabs are created through
|
|
1384
|
+
`chrome.tabs`/`chrome.windows`; choosing debugger mode then attaches the selected
|
|
1385
|
+
tab before setup. Chrome's debugger API needs an existing debuggee.
|
|
1386
|
+
|
|
1387
|
+
| Operation | Reverse API target (default) | Reverse debugger target |
|
|
1388
|
+
| --- | --- | --- |
|
|
1389
|
+
| Target selection and root routing | Register the stable tab ID without debugger attachment. | Attach the selected tab and register the same routing ID. |
|
|
1390
|
+
| Implicit initialization | No Runtime/Page/Network enabling, binding, focus emulation, background-service setup or resume command. | Apply the documented initialization options and resume after setup. |
|
|
1391
|
+
| `_.find`, `_.click` | Fixed native `chrome.scripting.executeScript` functions in the isolated world; shared DOM, no string evaluation. | `Runtime.evaluate` in the page world. |
|
|
1392
|
+
| Explicit session CDP, such as `Runtime.evaluate` or `Page.navigate` | Attach lazily and execute the exact CDP request. | Execute the CDP request. |
|
|
1393
|
+
| Explicit `chrome.*` method | Invoke the named native extension API with the supplied positional arguments. | Same native extension API dispatch. |
|
|
1394
|
+
|
|
1395
|
+
The target option is recorded on first use of its logical name, like the other
|
|
1396
|
+
target options. It belongs in `params.target`, not constructor/browser defaults.
|
|
1397
|
+
`runtime: true`, `network: true` or a binding name alone do not opt an API target
|
|
1398
|
+
into debugger setup. To retain the previous reverse convenience initialization,
|
|
1399
|
+
set `target.execution_mode: 'debugger'` explicitly. Direct CDP defaults are unchanged.
|
|
1400
|
+
As with the existing XPath helper, `_.find` does not wait for navigation or retry;
|
|
1401
|
+
wait for your page's readiness before querying it.
|
|
1402
|
+
|
|
1403
|
+
```js
|
|
1404
|
+
const found = await client.call({
|
|
1405
|
+
method: '_.find',
|
|
1406
|
+
params: {
|
|
1407
|
+
browser: {
|
|
1408
|
+
name: 'work',
|
|
1409
|
+
cdp_ext: true,
|
|
1410
|
+
},
|
|
1411
|
+
target: {
|
|
1412
|
+
name: 'search',
|
|
1413
|
+
create_params: {
|
|
1414
|
+
url: 'https://example.com/',
|
|
1415
|
+
},
|
|
1416
|
+
},
|
|
1417
|
+
xpath: '//h1',
|
|
1418
|
+
},
|
|
1419
|
+
})
|
|
1420
|
+
console.log(found.result.value)
|
|
1421
|
+
|
|
1422
|
+
// Select debugger execution for this helper call only.
|
|
1423
|
+
await client.call({
|
|
1424
|
+
method: '_.click',
|
|
1425
|
+
params: {
|
|
1426
|
+
browser: 'work',
|
|
1427
|
+
target: 'search',
|
|
1428
|
+
execution_mode: 'debugger',
|
|
1429
|
+
xpath: '//button',
|
|
1430
|
+
},
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
// Native CDP executes even though the target's default remains API mode.
|
|
1434
|
+
await client.call({
|
|
1435
|
+
method: 'Runtime.evaluate',
|
|
1436
|
+
params: {
|
|
1437
|
+
browser: 'work',
|
|
1438
|
+
target: 'search',
|
|
1439
|
+
expression: 'document.title',
|
|
1440
|
+
},
|
|
1441
|
+
})
|
|
1442
|
+
|
|
1443
|
+
// A separate target with the full convenience debugger setup.
|
|
1444
|
+
await client.call({
|
|
1445
|
+
method: 'Page.navigate',
|
|
1446
|
+
params: {
|
|
1447
|
+
browser: 'work',
|
|
1448
|
+
target: {
|
|
1449
|
+
name: 'instrumented',
|
|
1450
|
+
execution_mode: 'debugger',
|
|
1451
|
+
runtime: true,
|
|
1452
|
+
},
|
|
1453
|
+
url: 'https://example.com/',
|
|
1454
|
+
},
|
|
1455
|
+
})
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
For the built-in helpers and `evaluate_xpath`, `params.execution_mode` overrides
|
|
1459
|
+
the target's choice for that call only; it does not enable optional target setup
|
|
1460
|
+
or change the recorded configuration. Valid reverse values are `'api'` and
|
|
1461
|
+
`'debugger'`. Native CDP and `chrome.*` methods keep their explicit dispatch and
|
|
1462
|
+
ignore this helper override; the router removes it from native parameters.
|
|
1463
|
+
Custom handlers still receive the original parameters and choose their own behavior.
|
|
1464
|
+
|
|
1465
|
+
API selection does not detach a debugger previously attached by an explicit
|
|
1466
|
+
command. Use `Target.detachFromTarget` explicitly if needed. After detachment,
|
|
1467
|
+
an API target can be selected and used through API helpers again without
|
|
1468
|
+
reattachment. Native CDP attachment by itself does not enable domains or install
|
|
1469
|
+
bindings; send the desired commands explicitly or choose debugger target setup.
|
|
1470
|
+
Only debugger-attached targets expose flattened worker/iframe sessions. Browser
|
|
1471
|
+
API helpers operate on the registered root tab's main frame; extension permissions
|
|
1472
|
+
and Chrome's restricted-page rules still apply. There is no automatic fallback
|
|
1473
|
+
from API execution to debugger execution.
|
|
1474
|
+
|
|
1347
1475
|
### Named tab groups
|
|
1348
1476
|
|
|
1349
1477
|
With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
|
|
@@ -1416,6 +1544,10 @@ and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/t
|
|
|
1416
1544
|
|
|
1417
1545
|
### Target initialization
|
|
1418
1546
|
|
|
1547
|
+
The following CDP setup applies to direct mode and reverse targets configured
|
|
1548
|
+
with `execution_mode: 'debugger'`. Reverse API targets skip it entirely, including
|
|
1549
|
+
`Runtime.runIfWaitingForDebugger`; `initialize: false` does not opt into debugging.
|
|
1550
|
+
|
|
1419
1551
|
Domain options set to false skip setup commands; they do not actively disable a
|
|
1420
1552
|
domain already enabled by another caller. Likewise, `binding: false` does not
|
|
1421
1553
|
remove a previously installed binding. Use native CDP commands for immediate
|
|
@@ -1488,10 +1620,16 @@ await loaded
|
|
|
1488
1620
|
## XPath custom methods
|
|
1489
1621
|
|
|
1490
1622
|
Both built-ins require `params.target`; `params.browser` defaults to `main`
|
|
1491
|
-
through the normal router.
|
|
1623
|
+
through the normal router. In direct/debugger execution they use `Runtime.evaluate` with `returnByValue`,
|
|
1492
1624
|
`awaitPromise`, `silent` and `userGesture` set to true: serialize the result,
|
|
1493
1625
|
await async page code, suppress evaluation exception reporting, and evaluate
|
|
1494
|
-
with a user gesture respectively.
|
|
1626
|
+
with a user gesture respectively. In reverse API execution, fixed native scripting
|
|
1627
|
+
functions perform the same DOM operations in the isolated world, without eval,
|
|
1628
|
+
user-script permission or CSP changes. They keep `response.result.value` and the
|
|
1629
|
+
same click/find value shapes. API results contain `result.type` and `result.value`;
|
|
1630
|
+
they do not manufacture CDP remote-object metadata or `exceptionDetails`.
|
|
1631
|
+
Native injection errors reject; failed script execution also rejects. Debugger
|
|
1632
|
+
evaluation retains native `exceptionDetails` for page-side exceptions.
|
|
1495
1633
|
|
|
1496
1634
|
| Method | Parameters | Remote result in `response.result.value` |
|
|
1497
1635
|
| --- | --- | --- |
|
|
@@ -1536,10 +1674,19 @@ const text = await client.evaluate_xpath({
|
|
|
1536
1674
|
console.log(text.result.value)
|
|
1537
1675
|
```
|
|
1538
1676
|
|
|
1539
|
-
`params` supplies `browser`, `target
|
|
1677
|
+
`params` supplies `browser`, `target`, `xpath` and optional per-call
|
|
1678
|
+
`execution_mode`. `fn` is serialized and called in
|
|
1540
1679
|
the target as `fn(normalizedXPath, options)`; it cannot capture Node.js variables
|
|
1541
1680
|
or imported helpers. `options` must be JSON-serializable; omission becomes null
|
|
1542
|
-
in the argument array.
|
|
1681
|
+
in the argument array. Direct/debugger results retain native `exceptionDetails`
|
|
1682
|
+
if page code throws. In reverse API mode this arbitrary-function helper uses the
|
|
1683
|
+
existing string-function scripting adapter in the **MAIN** world and returns
|
|
1684
|
+
`{ result: { type, value } }`. Unlike the two fixed built-ins, arbitrary source
|
|
1685
|
+
evaluation requires the page CSP to allow eval. Injection errors or missing
|
|
1686
|
+
execution results reject; no debugger, CSP bypass or user-script toggle is enabled
|
|
1687
|
+
automatically. Choose `execution_mode: 'debugger'` explicitly when native CDP
|
|
1688
|
+
evaluation is required. Native `chrome.scripting.executeScript` calls retain their
|
|
1689
|
+
existing native world default and native result array.
|
|
1543
1690
|
|
|
1544
1691
|
### Register, replace or remove handlers
|
|
1545
1692
|
|
|
@@ -1645,6 +1792,7 @@ await client.call({
|
|
|
1645
1792
|
params: {
|
|
1646
1793
|
target: {
|
|
1647
1794
|
name: 'bridge',
|
|
1795
|
+
execution_mode: 'debugger',
|
|
1648
1796
|
binding: 'send_to_host',
|
|
1649
1797
|
runtime: true,
|
|
1650
1798
|
},
|
package/cdp.js
CHANGED
|
@@ -320,7 +320,7 @@ const custom_methods = {
|
|
|
320
320
|
if (i + 1 < options.attempts) await new Promise(resolve => setTimeout(resolve, options.interval_ms))
|
|
321
321
|
}
|
|
322
322
|
return false
|
|
323
|
-
}, options)
|
|
323
|
+
}, options, '_.click')
|
|
324
324
|
},
|
|
325
325
|
'_.find'(params) {
|
|
326
326
|
const limit = params.limit ?? 20
|
|
@@ -346,7 +346,7 @@ const custom_methods = {
|
|
|
346
346
|
count: nodes.snapshotLength,
|
|
347
347
|
items,
|
|
348
348
|
}
|
|
349
|
-
}, limit)
|
|
349
|
+
}, limit, '_.find')
|
|
350
350
|
},
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -472,20 +472,52 @@ export class cdp extends EventTarget {
|
|
|
472
472
|
})
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
evaluate_xpath(params, fn, options) {
|
|
475
|
+
async evaluate_xpath(params, fn, options, method) {
|
|
476
476
|
if (params.target == null) throw new Error('Custom method requires target')
|
|
477
477
|
const xpath = util.normalize_xpath(params.xpath)
|
|
478
|
-
|
|
478
|
+
const { record, entry } = await this._resolve(params)
|
|
479
|
+
const reverse = record.reverse || record.cdp_ext
|
|
480
|
+
const execution_mode = params.execution_mode ?? entry.execution_mode ?? 'api'
|
|
481
|
+
if (reverse && !['api', 'debugger'].includes(execution_mode)) throw new Error('execution_mode must be api or debugger')
|
|
482
|
+
if (reverse && execution_mode === 'api') {
|
|
483
|
+
if (method) return record.socket.req({
|
|
484
|
+
method,
|
|
485
|
+
params: {
|
|
486
|
+
xpath,
|
|
487
|
+
options,
|
|
488
|
+
},
|
|
489
|
+
sessionId: entry.sessionId,
|
|
490
|
+
})
|
|
491
|
+
const results = await record.socket.req({
|
|
492
|
+
method: 'chrome.scripting.executeScript',
|
|
493
|
+
params: {
|
|
494
|
+
args: [{
|
|
495
|
+
target: { tabId: Number(entry.targetId) },
|
|
496
|
+
world: 'MAIN',
|
|
497
|
+
func: 'async (...args) => ({ value: await (' + fn + ')(...args) })',
|
|
498
|
+
args: [xpath, options ?? null],
|
|
499
|
+
}],
|
|
500
|
+
},
|
|
501
|
+
})
|
|
502
|
+
const result = results[0]?.result
|
|
503
|
+
if (!result) throw new Error('Browser API evaluation failed; check page CSP and extension access. No debugger fallback was used.')
|
|
504
|
+
return {
|
|
505
|
+
result: {
|
|
506
|
+
type: typeof result.value,
|
|
507
|
+
value: result.value,
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return record.socket.req({
|
|
479
512
|
method: 'Runtime.evaluate',
|
|
480
513
|
params: {
|
|
481
|
-
browser: params.browser,
|
|
482
|
-
target: params.target,
|
|
483
514
|
expression: '(' + fn + ')(...' + JSON.stringify([xpath, options]) + ')',
|
|
484
515
|
returnByValue: true,
|
|
485
516
|
awaitPromise: true,
|
|
486
517
|
silent: true,
|
|
487
518
|
userGesture: true,
|
|
488
519
|
},
|
|
520
|
+
sessionId: entry.sessionId,
|
|
489
521
|
})
|
|
490
522
|
}
|
|
491
523
|
|
|
@@ -513,7 +545,7 @@ export class cdp extends EventTarget {
|
|
|
513
545
|
}
|
|
514
546
|
const info = browser.target_info[targetId]
|
|
515
547
|
if (message.method === 'Target.attachedToTarget') {
|
|
516
|
-
this._detach(browser, info?.sessionId)
|
|
548
|
+
if (info?.sessionId !== sessionId) this._detach(browser, info?.sessionId)
|
|
517
549
|
info.sessionId = sessionId
|
|
518
550
|
browser.session_targets[sessionId] = targetId
|
|
519
551
|
}
|
|
@@ -777,6 +809,8 @@ export class cdp extends EventTarget {
|
|
|
777
809
|
async _target(browser, target) {
|
|
778
810
|
const socket = browser.socket
|
|
779
811
|
const reverse = browser.reverse || browser.cdp_ext
|
|
812
|
+
const execution_mode = target.execution_mode ?? 'api'
|
|
813
|
+
if (reverse && !['api', 'debugger'].includes(execution_mode)) throw new Error('execution_mode must be api or debugger')
|
|
780
814
|
if (target.group != null && target.group !== false) {
|
|
781
815
|
const spec = typeof target.group === 'string' ? { name: target.group } : target.group
|
|
782
816
|
browser.groups[spec.name] ??= {
|
|
@@ -819,17 +853,23 @@ export class cdp extends EventTarget {
|
|
|
819
853
|
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
820
854
|
}
|
|
821
855
|
if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
|
|
822
|
-
if (!info.sessionId) {
|
|
856
|
+
if (!info.sessionId || (reverse && execution_mode === 'debugger')) {
|
|
823
857
|
info.sessionId = (await socket.req({
|
|
824
858
|
method: 'Target.attachToTarget',
|
|
825
859
|
params: {
|
|
826
860
|
targetId: target.targetId,
|
|
827
861
|
flatten: true,
|
|
862
|
+
...(reverse ? { execution_mode } : {}),
|
|
828
863
|
},
|
|
829
864
|
})).sessionId
|
|
830
865
|
browser.session_targets[info.sessionId] = target.targetId
|
|
831
866
|
}
|
|
832
867
|
const sessionId = info.sessionId
|
|
868
|
+
target.setup_errors = []
|
|
869
|
+
if (reverse && execution_mode === 'api') {
|
|
870
|
+
target.sessionId = sessionId
|
|
871
|
+
return
|
|
872
|
+
}
|
|
833
873
|
const steps = []
|
|
834
874
|
if (target.initialize !== false) {
|
|
835
875
|
for (const [option, domain] of Object.entries({
|
|
@@ -853,7 +893,6 @@ export class cdp extends EventTarget {
|
|
|
853
893
|
if ((target.runtime ?? 'bootstrap') === 'bootstrap') steps.push(['Runtime.disable'])
|
|
854
894
|
}
|
|
855
895
|
steps.push(['Runtime.runIfWaitingForDebugger'])
|
|
856
|
-
target.setup_errors = []
|
|
857
896
|
for (const [method, params] of steps) {
|
|
858
897
|
try {
|
|
859
898
|
await socket.req({
|
|
@@ -869,13 +908,7 @@ export class cdp extends EventTarget {
|
|
|
869
908
|
target.sessionId = sessionId
|
|
870
909
|
}
|
|
871
910
|
|
|
872
|
-
async
|
|
873
|
-
if (method.startsWith('_.')) {
|
|
874
|
-
const handler = this.custom_methods[method]
|
|
875
|
-
if (!handler) throw new Error('Unknown CDP extension: ' + method)
|
|
876
|
-
return handler.call(this, params)
|
|
877
|
-
}
|
|
878
|
-
const { browser = 'main', target, ...native } = params
|
|
911
|
+
async _resolve({ browser = 'main', target }) {
|
|
879
912
|
const spec = typeof browser === 'string' ? { name: browser } : browser
|
|
880
913
|
const record = this.browsers[spec.name] ??= {
|
|
881
914
|
user_data_dir: ((spec.reverse ?? this.options.reverse) || (spec.cdp_ext ?? this.options.cdp_ext)) ? null : path.join(this.base_path, spec.name),
|
|
@@ -933,6 +966,20 @@ export class cdp extends EventTarget {
|
|
|
933
966
|
await entry.connecting
|
|
934
967
|
}
|
|
935
968
|
}
|
|
969
|
+
return {
|
|
970
|
+
record,
|
|
971
|
+
entry,
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async call({ method, params = {} }) {
|
|
976
|
+
if (method.startsWith('_.')) {
|
|
977
|
+
const handler = this.custom_methods[method]
|
|
978
|
+
if (!handler) throw new Error('Unknown CDP extension: ' + method)
|
|
979
|
+
return handler.call(this, params)
|
|
980
|
+
}
|
|
981
|
+
const { browser, target, execution_mode, ...native } = params
|
|
982
|
+
const { record, entry } = await this._resolve(params)
|
|
936
983
|
if (method === 'Target.disposeBrowserContext' && target == null) {
|
|
937
984
|
const context = Object.values(record.contexts).find(value => value.browserContextId === native.browserContextId)
|
|
938
985
|
if (context) return this._dispose_context(record, context, native)
|
package/cdp_ext/cdp_ext.js
CHANGED
|
@@ -15,6 +15,18 @@ let connection_message
|
|
|
15
15
|
const debug_commands = []
|
|
16
16
|
const debug_ports = []
|
|
17
17
|
|
|
18
|
+
function debug_record(fields) {
|
|
19
|
+
const record = {
|
|
20
|
+
time: Date.now(),
|
|
21
|
+
...fields,
|
|
22
|
+
status: 'running',
|
|
23
|
+
}
|
|
24
|
+
debug_commands.unshift(record)
|
|
25
|
+
debug_commands.length = Math.min(debug_commands.length, 50)
|
|
26
|
+
debug_update()
|
|
27
|
+
return record
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
function debug_update() {
|
|
19
31
|
const state = {
|
|
20
32
|
status: connection_message ?? (config.browser ? 'Connecting to server' : 'Waiting for trigger'),
|
|
@@ -23,6 +35,7 @@ function debug_update() {
|
|
|
23
35
|
endpoint: config.websocket_url ?? '',
|
|
24
36
|
extension_id: chrome.runtime.id,
|
|
25
37
|
sessions: Object.keys(sessions).length,
|
|
38
|
+
debugger_tabs: Object.keys(attached).length,
|
|
26
39
|
commands: debug_commands,
|
|
27
40
|
}
|
|
28
41
|
for (const port of debug_ports) {
|
|
@@ -67,31 +80,62 @@ async function bootstrap(tab) {
|
|
|
67
80
|
await configured
|
|
68
81
|
while (bootstrapping) await bootstrapping
|
|
69
82
|
return bootstrapping = (async () => {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
const tabs = await chrome.tabs.query({})
|
|
84
|
+
if (!tabs.some(current => current.id === tab.id && (current.pendingUrl ?? current.url) === text)) return
|
|
85
|
+
const trigger = debug_commands.find(record => record.method === 'cdp_ext.trigger' &&
|
|
86
|
+
record.tabId === tab.id && record.browser === settings.browser && record.endpoint === settings.websocket_url) ?? debug_record({
|
|
87
|
+
method: 'cdp_ext.trigger',
|
|
88
|
+
tabId: tab.id,
|
|
89
|
+
browser: settings.browser,
|
|
90
|
+
endpoint: settings.websocket_url,
|
|
91
|
+
})
|
|
92
|
+
try {
|
|
93
|
+
const changed = ['browser', 'websocket_url', 'token'].some(key => settings[key] !== config[key])
|
|
94
|
+
if (changed && config.browser && socket?.readyState < WebSocket.CLOSING) {
|
|
95
|
+
const probe = new WebSocket(settings.websocket_url)
|
|
96
|
+
trigger.error = 'This Chrome profile is already connected as ' + config.browser
|
|
97
|
+
probe.onopen = () => probe.send(JSON.stringify({
|
|
98
|
+
browser: settings.browser,
|
|
99
|
+
extension_id: chrome.runtime.id,
|
|
100
|
+
token: settings.token,
|
|
101
|
+
error: trigger.error,
|
|
102
|
+
}))
|
|
103
|
+
} else {
|
|
104
|
+
if (changed) {
|
|
105
|
+
Object.assign(config, settings)
|
|
106
|
+
connection_message = undefined
|
|
107
|
+
debug_update()
|
|
108
|
+
await chrome.storage.session.set({ cdp_bootstrap: settings })
|
|
109
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
110
|
+
await new Promise(resolve => {
|
|
111
|
+
socket.addEventListener('close', resolve, { once: true })
|
|
112
|
+
socket.close()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
await disconnecting
|
|
116
|
+
}
|
|
117
|
+
await connect()
|
|
91
118
|
}
|
|
92
|
-
await
|
|
119
|
+
const current_tabs = await chrome.tabs.query({})
|
|
120
|
+
const current = current_tabs.find(current => current.id === tab.id)
|
|
121
|
+
if ((current?.pendingUrl ?? current?.url) === text) {
|
|
122
|
+
if (current_tabs.length === 1) {
|
|
123
|
+
await chrome.tabs.update(tab.id, { url: chrome.runtime.getURL('sidebar.html?connection') })
|
|
124
|
+
trigger.action = 'opened connection page'
|
|
125
|
+
} else {
|
|
126
|
+
await chrome.tabs.remove(tab.id)
|
|
127
|
+
trigger.action = 'closed tab'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
trigger.status = trigger.error ? 'error' : 'done'
|
|
131
|
+
} catch (error) {
|
|
132
|
+
trigger.status = 'error'
|
|
133
|
+
trigger.error = error.message
|
|
134
|
+
throw error
|
|
135
|
+
} finally {
|
|
136
|
+
trigger.duration_ms = Date.now() - trigger.time
|
|
137
|
+
debug_update()
|
|
93
138
|
}
|
|
94
|
-
await connect()
|
|
95
139
|
})().finally(() => {
|
|
96
140
|
bootstrapping = null
|
|
97
141
|
})
|
|
@@ -114,7 +158,7 @@ function target_info(tab) {
|
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
function detached(source) {
|
|
117
|
-
if (!
|
|
161
|
+
if (!sessions[String(source.tabId)]) return
|
|
118
162
|
delete attached[source.tabId]
|
|
119
163
|
for (const [id, session] of Object.entries(sessions)) {
|
|
120
164
|
if (session.tabId === source.tabId) delete sessions[id]
|
|
@@ -210,6 +254,12 @@ const methods = {
|
|
|
210
254
|
},
|
|
211
255
|
async 'Target.attachToTarget'(params) {
|
|
212
256
|
const tabId = Number(params.targetId)
|
|
257
|
+
if (params.execution_mode === 'api') {
|
|
258
|
+
await chrome.tabs.get(tabId)
|
|
259
|
+
sessions[String(tabId)] = { tabId }
|
|
260
|
+
debug_update()
|
|
261
|
+
return { sessionId: String(tabId) }
|
|
262
|
+
}
|
|
213
263
|
if (attaching[tabId]) return attaching[tabId]
|
|
214
264
|
if (attached[tabId]) return { sessionId: String(tabId) }
|
|
215
265
|
return attaching[tabId] = attach_tab(tabId).finally(() => delete attaching[tabId])
|
|
@@ -220,7 +270,7 @@ const methods = {
|
|
|
220
270
|
if (source.sessionId) {
|
|
221
271
|
await chrome.debugger.sendCommand({ tabId: source.tabId }, 'Target.detachFromTarget', { sessionId: source.sessionId })
|
|
222
272
|
} else {
|
|
223
|
-
await chrome.debugger.detach(source)
|
|
273
|
+
if (attached[source.tabId]) await chrome.debugger.detach(source)
|
|
224
274
|
detached(source)
|
|
225
275
|
}
|
|
226
276
|
},
|
|
@@ -266,6 +316,7 @@ chrome.tabs.onUpdated.addListener((id, changes, tab) => {
|
|
|
266
316
|
})
|
|
267
317
|
})
|
|
268
318
|
chrome.tabs.onRemoved.addListener(id => {
|
|
319
|
+
detached({ tabId: id })
|
|
269
320
|
if (discovering) send({
|
|
270
321
|
method: 'Target.targetDestroyed',
|
|
271
322
|
params: { targetId: String(id) },
|
|
@@ -333,16 +384,12 @@ async function connect() {
|
|
|
333
384
|
if (!event.data) return
|
|
334
385
|
const request = JSON.parse(event.data)
|
|
335
386
|
const { method, params = {}, sessionId, id } = request
|
|
336
|
-
const command = {
|
|
337
|
-
time: Date.now(),
|
|
387
|
+
const command = debug_record({
|
|
338
388
|
id,
|
|
339
389
|
method,
|
|
340
390
|
sessionId,
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
debug_commands.unshift(command)
|
|
344
|
-
debug_commands.length = Math.min(debug_commands.length, 50)
|
|
345
|
-
debug_update()
|
|
391
|
+
execution_mode: params.execution_mode,
|
|
392
|
+
})
|
|
346
393
|
let response
|
|
347
394
|
try {
|
|
348
395
|
let result
|
|
@@ -363,11 +410,62 @@ async function connect() {
|
|
|
363
410
|
else resolve(value)
|
|
364
411
|
}))
|
|
365
412
|
: await object[name](...args)
|
|
413
|
+
} else if (method === '_.click' || method === '_.find') {
|
|
414
|
+
const source = sessions[sessionId]
|
|
415
|
+
if (!source || source.sessionId) throw new Error('XPath browser API commands require a registered root tab')
|
|
416
|
+
const results = await chrome.scripting.executeScript({
|
|
417
|
+
target: { tabId: source.tabId },
|
|
418
|
+
args: [method, params.xpath, params.options],
|
|
419
|
+
func: async (method, xpath, options) => {
|
|
420
|
+
if (method === '_.click') {
|
|
421
|
+
for (let i = 0; i < options.attempts; i++) {
|
|
422
|
+
const node = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
|
423
|
+
if (node) {
|
|
424
|
+
if (typeof node.click !== 'function') return false
|
|
425
|
+
node.click()
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
if (i + 1 < options.attempts) await new Promise(resolve => setTimeout(resolve, options.interval_ms))
|
|
429
|
+
}
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
const nodes = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
|
433
|
+
const items = []
|
|
434
|
+
for (let i = 0; i < Math.min(nodes.snapshotLength, options); i++) {
|
|
435
|
+
const node = nodes.snapshotItem(i)
|
|
436
|
+
items.push({
|
|
437
|
+
node_type: node.nodeType,
|
|
438
|
+
node_name: node.nodeName,
|
|
439
|
+
text: String(node.innerText ?? node.textContent ?? '').trim().slice(0, 2000),
|
|
440
|
+
tag: node.tagName?.toLowerCase(),
|
|
441
|
+
id: node.id,
|
|
442
|
+
class: node.className == null ? undefined : String(node.className),
|
|
443
|
+
href: node.href,
|
|
444
|
+
value: node.value,
|
|
445
|
+
rect: node.getBoundingClientRect?.().toJSON(),
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
count: nodes.snapshotLength,
|
|
450
|
+
items,
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
if (!Object.hasOwn(results[0] ?? {}, 'result')) throw new Error('XPath browser API execution failed')
|
|
455
|
+
result = {
|
|
456
|
+
result: {
|
|
457
|
+
type: typeof results[0].result,
|
|
458
|
+
value: results[0].result,
|
|
459
|
+
},
|
|
460
|
+
}
|
|
366
461
|
} else {
|
|
367
462
|
if (sessionId == null && !methods[method]) throw new Error('Unsupported reverse browser command: ' + method)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
:
|
|
463
|
+
if (sessionId != null) {
|
|
464
|
+
const source = sessions[sessionId]
|
|
465
|
+
if (!source) throw new Error('Unknown debugger session: ' + sessionId)
|
|
466
|
+
await methods['Target.attachToTarget']({ targetId: String(source.tabId) })
|
|
467
|
+
result = await chrome.debugger.sendCommand(source, method, params)
|
|
468
|
+
} else result = await methods[method](params)
|
|
371
469
|
}
|
|
372
470
|
response = {
|
|
373
471
|
id,
|
|
@@ -391,10 +489,11 @@ async function connect() {
|
|
|
391
489
|
connection.onclose = () => {
|
|
392
490
|
report_connection('Waiting for CDP.js server at ' + endpoint + '; retrying automatically.')
|
|
393
491
|
discovering = false
|
|
394
|
-
disconnecting = Promise.all(Object.keys(
|
|
492
|
+
disconnecting = Promise.all(Object.keys(sessions).filter(id => !sessions[id].sessionId).map(id => {
|
|
395
493
|
const source = { tabId: Number(id) }
|
|
494
|
+
const was_attached = attached[id]
|
|
396
495
|
detached(source)
|
|
397
|
-
return chrome.debugger.detach(source).catch(() => {})
|
|
496
|
+
if (was_attached) return chrome.debugger.detach(source).catch(() => {})
|
|
398
497
|
})).finally(() => disconnecting = null)
|
|
399
498
|
}
|
|
400
499
|
}
|
package/cdp_ext/manifest.json
CHANGED
package/cdp_ext/sidebar.html
CHANGED
|
@@ -20,16 +20,18 @@
|
|
|
20
20
|
small { display: block; opacity: .7; margin-top: 5px; font-size: 12px; }
|
|
21
21
|
.error { color: light-dark(#b42318, #ff938a); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
22
22
|
</style>
|
|
23
|
-
<header><h1>CDP.js debug</h1><button id="clear" type="button" title="Clear
|
|
23
|
+
<header><h1>CDP.js debug</h1><button id="clear" type="button" title="Clear diagnostic history">Clear</button></header>
|
|
24
|
+
<p id="connection-message" hidden>Browser bridge configured. Live connection status is shown below.</p>
|
|
24
25
|
<p id="status" role="status">Connecting to extension…</p>
|
|
25
26
|
<dl>
|
|
26
27
|
<dt>Browser</dt><dd id="browser">—</dd>
|
|
27
28
|
<dt>Server</dt><dd id="endpoint">—</dd>
|
|
28
29
|
<dt>Extension</dt><dd id="extension">—</dd>
|
|
29
|
-
<dt>
|
|
30
|
+
<dt>Routing sessions</dt><dd id="sessions">0</dd>
|
|
31
|
+
<dt>Debugger tabs</dt><dd id="debugger_tabs">0</dd>
|
|
30
32
|
</dl>
|
|
31
|
-
<h2>Recent
|
|
32
|
-
<p id="empty">No
|
|
33
|
+
<h2>Recent activity <small>Latest 50 · memory only</small></h2>
|
|
34
|
+
<p id="empty">No activity received.</p>
|
|
33
35
|
<ol id="commands"></ol>
|
|
34
36
|
<script src="sidebar.js"></script>
|
|
35
37
|
</html>
|
package/cdp_ext/sidebar.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const port = chrome.runtime.connect({ name: 'cdp_debug' })
|
|
2
2
|
const status = document.querySelector('#status')
|
|
3
3
|
const heartbeat = setInterval(() => port.postMessage('ping'), 20000)
|
|
4
|
+
if (location.search === '?connection') {
|
|
5
|
+
document.title = 'CDP.js connection'
|
|
6
|
+
document.querySelector('h1').textContent = document.title
|
|
7
|
+
document.querySelector('#connection-message').hidden = false
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
port.onMessage.addListener(state => {
|
|
6
11
|
status.textContent = state.status
|
|
@@ -10,6 +15,7 @@ port.onMessage.addListener(state => {
|
|
|
10
15
|
endpoint: state.endpoint,
|
|
11
16
|
extension: state.extension_id,
|
|
12
17
|
sessions: state.sessions,
|
|
18
|
+
debugger_tabs: state.debugger_tabs ?? 0,
|
|
13
19
|
})) document.getElementById(id).textContent = value === '' ? '—' : value
|
|
14
20
|
document.querySelector('#empty').hidden = state.commands.length > 0
|
|
15
21
|
document.querySelector('#commands').replaceChildren(...state.commands.map(command => {
|
|
@@ -19,9 +25,14 @@ port.onMessage.addListener(state => {
|
|
|
19
25
|
const details = document.createElement('small')
|
|
20
26
|
details.textContent = [
|
|
21
27
|
new Date(command.time).toLocaleTimeString(),
|
|
22
|
-
|
|
28
|
+
command.id == null ? (command.method === 'cdp_ext.trigger' ? 'trigger' : 'notification') : '#' + command.id,
|
|
23
29
|
command.sessionId ? 'session ' + command.sessionId : 'browser',
|
|
24
30
|
command.status,
|
|
31
|
+
command.execution_mode,
|
|
32
|
+
command.tabId == null ? '' : 'tab ' + command.tabId,
|
|
33
|
+
command.browser,
|
|
34
|
+
command.endpoint,
|
|
35
|
+
command.action,
|
|
25
36
|
command.duration_ms == null ? '' : command.duration_ms + ' ms',
|
|
26
37
|
].filter(Boolean).join(' · ')
|
|
27
38
|
row.append(method, details)
|
package/package.json
CHANGED