@mefistofelix/cdp.js 0.3.0 → 0.5.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 +179 -80
- package/cdp.js +124 -20
- package/cdp_ext/cdp_ext.js +68 -4
- package/cdp_ext/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ import api, { util, jsrpc, browser, cdp } from '@mefistofelix/cdp.js'
|
|
|
25
25
|
The npm package exports the same file and API. Direct file imports in the examples
|
|
26
26
|
below remain supported. Publication metadata is isolated in `npm/`.
|
|
27
27
|
The package includes both `cdp_ext/` files. With `cdp_ext: true`, the manager
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
prepares and loads the extension automatically with a custom user-data directory.
|
|
29
|
+
The system profile uses an already installed extension by default. Reverse mode still
|
|
30
30
|
requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
31
31
|
In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above.
|
|
32
32
|
|
|
@@ -37,6 +37,7 @@ In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above
|
|
|
37
37
|
- Browser proxies and named direct-mode contexts with independent proxies/storage
|
|
38
38
|
and automatic cleanup after their last target closes.
|
|
39
39
|
- Incognito targets in direct and reverse modes.
|
|
40
|
+
- Named, colored tab groups through the reverse Chrome extension.
|
|
40
41
|
- Attachment through WebSocket, HTTP discovery or a debugging port.
|
|
41
42
|
- Optional Chrome extension reverse connections through one native Deno/Bun server,
|
|
42
43
|
without browser remote-debugging arguments.
|
|
@@ -188,7 +189,7 @@ belong in `params.target` and do not inherit from the constructor.
|
|
|
188
189
|
|
|
189
190
|
For a new browser record, configuration is assembled in this order:
|
|
190
191
|
|
|
191
|
-
1. Default `user_data_dir
|
|
192
|
+
1. Default `user_data_dir`: unset in extension/passive reverse mode, otherwise derived from `base_path` and the browser name.
|
|
192
193
|
2. Constructor browser options.
|
|
193
194
|
3. The `params.browser` configuration object.
|
|
194
195
|
|
|
@@ -224,7 +225,7 @@ local configuration.
|
|
|
224
225
|
| `base_path` | string, `'.cdp'` | Constructor-only profile root, resolved against the working directory. |
|
|
225
226
|
| `reverse_server` | object, `{}` | Constructor-only settings for the shared lazy listener: `hostname` defaults to `'127.0.0.1'`, `port` to `9223`. Independent of browser attachment `host`/`port`. |
|
|
226
227
|
| `name` | string, `'main'` when routing is omitted | Required in a browser configuration object; dictionary key and default profile directory label. |
|
|
227
|
-
| `user_data_dir` | absolute path string or `null`, `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. Explicit paths should be absolute. `null` omits the switch and skips manager preference-file writes; Chromium selects its system directory. |
|
|
228
|
+
| `user_data_dir` | absolute path string or `null`, `null` in extension/passive reverse mode, otherwise `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. Explicit paths should be absolute. `null` omits the switch and skips manager preference-file writes; Chromium selects its system directory. |
|
|
228
229
|
| `executable_path` | string, automatic discovery | Executable to spawn; takes precedence over `CDP_BROWSER`. |
|
|
229
230
|
| `headless` | boolean, `false` | When true, add `headless: 'new'` and default `window-size: '1440,900'`. |
|
|
230
231
|
| `windowsHide` | boolean, effective headless mode | Hide the spawned process's console on Windows. Defaults to true when the final arguments include `--headless`, false otherwise; an explicit value wins. Does not hide the parent terminal. |
|
|
@@ -241,9 +242,10 @@ local configuration.
|
|
|
241
242
|
| `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
|
|
242
243
|
| `waitForDebuggerOnStart` | boolean, `false` | Ask Chromium to pause newly auto-attached targets. Applied to `Target.setAutoAttach` when connecting; not a CLI argument or target option. |
|
|
243
244
|
| `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
|
|
244
|
-
| `cdp_ext` | boolean, `false` |
|
|
245
|
+
| `cdp_ext` | boolean, `false` | Use the extension transport under Deno/Bun: start the shared reverse listener and open a local bootstrap URL containing the logical browser name and WebSocket endpoint. No extension ID is required. |
|
|
246
|
+
| `cdp_ext_install` | boolean, presence of an effective user-data directory | Prepare an extension copy and startup loading arguments. Defaults on for an explicit custom directory, off for the system profile. `false` uses an already installed extension. Only applies with `cdp_ext: true`. |
|
|
245
247
|
| `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno or Bun. |
|
|
246
|
-
| `extension_id` | string, browser name for
|
|
248
|
+
| `extension_id` | string, browser name for passive `reverse: true` attachment | Optional legacy incoming extension identity. Not needed with `cdp_ext: true`; observed automatically from the connection. |
|
|
247
249
|
| `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
|
|
248
250
|
| `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
|
|
249
251
|
| `host` | string, `'127.0.0.1'` | Host used with `port`. |
|
|
@@ -491,8 +493,8 @@ can configure its native incognito scope. See
|
|
|
491
493
|
[cdp_ext/manifest.json](cdp_ext/manifest.json) and
|
|
492
494
|
[cdp_ext/cdp_ext.js](cdp_ext/cdp_ext.js) form a Manifest V3 extension for Chrome
|
|
493
495
|
125+. The extension connects out to one WebSocket server. The server serves all
|
|
494
|
-
browser instances on the same port;
|
|
495
|
-
|
|
496
|
+
browser instances on the same port; bootstrapped extensions announce the logical
|
|
497
|
+
browser name and native extension ID. No remote-debugging argument is needed.
|
|
496
498
|
|
|
497
499
|
Run the server under Deno using its native
|
|
498
500
|
[WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
|
|
@@ -512,6 +514,7 @@ const result = await client.call({
|
|
|
512
514
|
browser: {
|
|
513
515
|
name: 'work',
|
|
514
516
|
cdp_ext: true,
|
|
517
|
+
user_data_dir: '/absolute/path/to/work-profile',
|
|
515
518
|
headless: true,
|
|
516
519
|
},
|
|
517
520
|
target: 'demo',
|
|
@@ -520,65 +523,84 @@ const result = await client.call({
|
|
|
520
523
|
})
|
|
521
524
|
```
|
|
522
525
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
`update_preferences` rules.
|
|
529
|
-
The generated launch arguments omit remote debugging and enable this extension.
|
|
530
|
-
An `extensions` array also selects those directories; `extensions: true` keeps
|
|
531
|
-
normally enabled profile extensions through `'*'`. Raw `args` still win and can
|
|
532
|
-
prevent the bridge from loading or connecting, causing a connection timeout.
|
|
533
|
-
|
|
534
|
-
For external attachment, load unpacked `cdp_ext/` through `chrome://extensions`
|
|
535
|
-
or the [local-extension argument options](#local-extensions-at-startup).
|
|
536
|
-
Use its ID shown in Chrome, or `browser.extension_id(directory)`, as the
|
|
537
|
-
browser option `extension_id`. The logical `name` stays in the manager.
|
|
538
|
-
|
|
539
|
-
The extension configuration is read from `chrome.runtime.getManifest()`:
|
|
540
|
-
|
|
541
|
-
| Manifest `cdp` key | Default | Meaning |
|
|
542
|
-
| --- | --- | --- |
|
|
543
|
-
| `websocket_url` | `ws://127.0.0.1:9223` | The shared server endpoint. Port 9223 avoids the usual direct CDP port 9222. |
|
|
544
|
-
| `reconnect_ms` | `5000` | Connection retry interval while the extension worker is awake; also the empty-message heartbeat interval while connected. Use a positive value below 30000. |
|
|
545
|
-
|
|
546
|
-
The first message uses `chrome.runtime.id`, not a configured logical browser name.
|
|
547
|
-
For an unpacked extension without a manifest `key`, Chromium derives this ID from
|
|
548
|
-
its directory. Managed launches use a separate directory for each profile, so
|
|
549
|
-
the ID is stable across reconnects and restarts, and the manager knows which
|
|
550
|
-
browser it belongs to. External instances must likewise use distinct extension
|
|
551
|
-
directories. Moving the directory changes the ID. The server rejects duplicate
|
|
552
|
-
live IDs and announcements that differ from the WebSocket's `chrome-extension://`
|
|
553
|
-
origin. This is routing information, not an authentication credential.
|
|
526
|
+
With just `cdp_ext: true`, Chrome uses its system user-data directory. No
|
|
527
|
+
`user-data-dir` switch is added. An already installed and enabled bridge reads
|
|
528
|
+
the local URL and connects to the waiting manager. The user enters no extension
|
|
529
|
+
ID or WebSocket address. Load the package's `cdp_ext/` once through
|
|
530
|
+
`chrome://extensions` (developer mode, Load unpacked); reload it after upgrading.
|
|
554
531
|
|
|
555
532
|
```js
|
|
556
|
-
import { browser, cdp } from './cdp.js'
|
|
557
|
-
|
|
558
533
|
const client = new cdp({
|
|
559
|
-
|
|
560
|
-
connect_timeout_ms: 60000,
|
|
561
|
-
reverse_server: {
|
|
562
|
-
hostname: '127.0.0.1',
|
|
563
|
-
port: 9223,
|
|
564
|
-
},
|
|
534
|
+
cdp_ext: true,
|
|
565
535
|
})
|
|
566
|
-
|
|
567
|
-
method: '
|
|
568
|
-
params: {
|
|
569
|
-
browser: {
|
|
570
|
-
name: 'other',
|
|
571
|
-
reverse: true,
|
|
572
|
-
extension_id: browser.extension_id('/absolute/path/to/cdp_ext'),
|
|
573
|
-
},
|
|
574
|
-
target: 'demo',
|
|
575
|
-
expression: 'document.title',
|
|
576
|
-
},
|
|
536
|
+
await client.call({
|
|
537
|
+
method: 'Browser.getVersion',
|
|
538
|
+
params: { browser: 'work' },
|
|
577
539
|
})
|
|
578
|
-
console.log(result.result.value)
|
|
579
|
-
await client.close()
|
|
580
540
|
```
|
|
581
541
|
|
|
542
|
+
Provide `user_data_dir` or raw `args['user-data-dir']` to choose a custom
|
|
543
|
+
directory. An effective custom directory makes `cdp_ext_install` default to true:
|
|
544
|
+
the manager copies the two extension files into
|
|
545
|
+
`<user-data-dir>/<profile-directory>/cdp_ext/` (`Default` when unset), sets the
|
|
546
|
+
shared server URL in that manifest and adds startup loading flags. Normal launch
|
|
547
|
+
options and profile preferences then apply. Set `cdp_ext_install: false` to use
|
|
548
|
+
an extension already installed in that profile, or true to force preparation and
|
|
549
|
+
loading. A null/false raw directory override makes automatic installation default off.
|
|
550
|
+
|
|
551
|
+
Forced installation without a directory places only the extension copy under
|
|
552
|
+
`<base_path>/<name>/<profile-directory>/cdp_ext/`; Chrome still selects its system
|
|
553
|
+
profile. The manager never infers system profile paths for preference writes.
|
|
554
|
+
Loading switches apply only at Chrome startup. An already running Chrome cannot
|
|
555
|
+
load a new extension from a forwarded command line: install it once or restart
|
|
556
|
+
that Chrome before forcing installation.
|
|
557
|
+
|
|
558
|
+
With installation disabled, opening the link skips the automation launch preset
|
|
559
|
+
so normal Chrome can forward the URL into its existing instance. High-level
|
|
560
|
+
headless, proxy, image, translation, login and extension options still generate
|
|
561
|
+
their flags, followed by raw overrides and the same enabled-only wildcard
|
|
562
|
+
expansion. The manager does not claim ownership of Chrome. Preference writes are
|
|
563
|
+
independent of extension installation: `update_preferences` controls them and
|
|
564
|
+
they require an explicit directory. A running instance does not apply new flags.
|
|
565
|
+
With installation
|
|
566
|
+
enabled, the normal launch preset applies, generated flags omit remote debugging
|
|
567
|
+
and enable the bridge. An `extensions` array adds its directories;
|
|
568
|
+
`extensions: true` retains enabled profile extensions through `'*'`. Raw `args`
|
|
569
|
+
still win and can prevent loading or connection.
|
|
570
|
+
|
|
571
|
+
The manager opens an actual local HTTP URL on the shared listener, for example
|
|
572
|
+
`http://127.0.0.1:9223/cdp_ext?browser=work&websocket_url=...&token=...`.
|
|
573
|
+
The port is the effective listening port, including an automatically allocated
|
|
574
|
+
port for `reverse_server.port: 0`. Name, WebSocket URL and generated binding token
|
|
575
|
+
are internal bootstrap parameters, not additional user options. The extension
|
|
576
|
+
observes startup tabs, tab creation and URL changes. Bootstrap accepts only
|
|
577
|
+
loopback HTTP URLs with a WebSocket endpoint on the same host/port. The bootstrap
|
|
578
|
+
tab remains open.
|
|
579
|
+
|
|
580
|
+
The first announcement contains `{ browser, extension_id, token }`. The browser
|
|
581
|
+
field is the logical label; `extension_id` is `chrome.runtime.id` and must match
|
|
582
|
+
the upgrade origin. The token must match the pending manager record. One extension
|
|
583
|
+
directory and native ID can therefore serve multiple profiles with distinct
|
|
584
|
+
logical labels. No logical name is written into a manifest. Bootstrap settings
|
|
585
|
+
use `chrome.storage.session` to survive worker restarts; they disappear on browser
|
|
586
|
+
restart or extension reload. Manager browser/target/session mappings remain in memory.
|
|
587
|
+
|
|
588
|
+
A profile has one active logical connection. A link requesting another name or
|
|
589
|
+
manager while connected returns an explicit error, preserving the existing
|
|
590
|
+
connection. After disconnection, a new bootstrap can rebind it.
|
|
591
|
+
|
|
592
|
+
Passive external attachment remains available through `reverse: true`: it opens
|
|
593
|
+
no URL and waits for `extension_id` (or the browser label). Without bootstrap,
|
|
594
|
+
the extension announces its native ID and uses its manifest endpoint. This
|
|
595
|
+
compatibility mode is optional; `cdp_ext: true` needs neither `reverse` nor an ID.
|
|
596
|
+
|
|
597
|
+
The manifest's `cdp` configuration supplies initial connection defaults:
|
|
598
|
+
|
|
599
|
+
| Manifest `cdp` key | Default | Meaning |
|
|
600
|
+
| --- | --- | --- |
|
|
601
|
+
| `websocket_url` | `ws://127.0.0.1:9223` | Initial shared endpoint; URL bootstrap overrides it. |
|
|
602
|
+
| `reconnect_ms` | `5000` | Awake retry and connected heartbeat interval; use a positive value below 30000. |
|
|
603
|
+
|
|
582
604
|
Construction and direct CDP calls do not start a listener. The first browser call
|
|
583
605
|
using `reverse: true` or `cdp_ext: true` starts the shared server lazily, with the
|
|
584
606
|
constructor's `reverse_server` settings. To mix direct and reverse browsers, leave the
|
|
@@ -615,7 +637,8 @@ Native Bun WebSocket limits still apply.
|
|
|
615
637
|
Reverse calls reuse the normal target setup, custom handlers, mappings, routed
|
|
616
638
|
events, request correlation, timeout and error behavior. `reverse: true` alone
|
|
617
639
|
attaches externally: the manager neither launches nor updates that browser.
|
|
618
|
-
`
|
|
640
|
+
`cdp_ext_install: true` owns the launched process and applies normal launch preferences.
|
|
641
|
+
With installation off, Chrome is external and remains open on manager close.
|
|
619
642
|
Closing the manager closes owned processes and the server, while external
|
|
620
643
|
browsers and tabs remain open. The extension retries when the server returns;
|
|
621
644
|
the next call rebuilds discovery and sessions, preserving surviving target IDs.
|
|
@@ -627,20 +650,11 @@ for that extension to reconnect; they do not spawn another process. Only the
|
|
|
627
650
|
child process's confirmed exit permits a new launch. Explicit manager `close()`
|
|
628
651
|
still terminates owned browsers.
|
|
629
652
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
Chrome's [process singleton](https://github.com/chromium/chromium/blob/main/chrome/browser/process_singleton.h)
|
|
637
|
-
uses the user-data directory to decide whether to forward a new launch to an
|
|
638
|
-
existing instance. `no-default-browser-check` only suppresses the default-browser
|
|
639
|
-
prompt. The current Chromium [notification callback](https://github.com/chromium/chromium/blob/main/chrome/browser/chrome_browser_main.cc)
|
|
640
|
-
rejects forwarding when either command line enables automation or uses headless
|
|
641
|
-
mode. This library generates `enable-automation` by default; a normal browser
|
|
642
|
-
started with `args['enable-automation']: false` can behave differently. Do not
|
|
643
|
-
infer ownership of the real browser from a short-lived forwarding process.
|
|
653
|
+
Chrome selects an existing instance by user-data directory. The minimal link
|
|
654
|
+
opener preserves this normal behavior. Automation/headless flags can prevent
|
|
655
|
+
forwarding; `no-default-browser-check` only suppresses a prompt. The short-lived
|
|
656
|
+
launcher is `record.opener`; its exit is not proof of browser exit. Installed
|
|
657
|
+
launches use `record.proc` instead.
|
|
644
658
|
|
|
645
659
|
The [service-worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle)
|
|
646
660
|
allows termination while offline. A 30-second Chrome alarm wakes the worker for
|
|
@@ -650,8 +664,8 @@ These are transport messages, not event buffering or polling of browser state.
|
|
|
650
664
|
|
|
651
665
|
### Reverse protocol and supported commands
|
|
652
666
|
|
|
653
|
-
The first
|
|
654
|
-
messages use
|
|
667
|
+
The first announcement contains `{ browser, extension_id, token }` for bootstrap,
|
|
668
|
+
or the native extension ID for passive attachment. Subsequent messages use CDP envelopes:
|
|
655
669
|
|
|
656
670
|
- Request: `{ id, method, params, sessionId? }`.
|
|
657
671
|
- Response: `{ id, result }` or `{ id, error: { code, message } }`.
|
|
@@ -1273,6 +1287,7 @@ await client.call({
|
|
|
1273
1287
|
| `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
|
|
1274
1288
|
| `context` | unset | Direct-mode context name or first-use configuration object; see named contexts above. Reverse mode rejects this option. |
|
|
1275
1289
|
| `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. |
|
|
1290
|
+
| `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
|
|
1276
1291
|
| `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
|
|
1277
1292
|
| `initialize` | `true` | Run the convenience setup sequence; false skips it. |
|
|
1278
1293
|
| `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
|
|
@@ -1283,6 +1298,78 @@ await client.call({
|
|
|
1283
1298
|
| `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
|
|
1284
1299
|
| `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
|
|
1285
1300
|
|
|
1301
|
+
### Named tab groups
|
|
1302
|
+
|
|
1303
|
+
With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
|
|
1304
|
+
configuring a target:
|
|
1305
|
+
|
|
1306
|
+
```js
|
|
1307
|
+
await client.call({
|
|
1308
|
+
method: 'Page.navigate',
|
|
1309
|
+
params: {
|
|
1310
|
+
browser: {
|
|
1311
|
+
name: 'work',
|
|
1312
|
+
cdp_ext: true,
|
|
1313
|
+
},
|
|
1314
|
+
target: {
|
|
1315
|
+
name: 'search',
|
|
1316
|
+
group: {
|
|
1317
|
+
name: 'Research',
|
|
1318
|
+
color: 'blue',
|
|
1319
|
+
},
|
|
1320
|
+
},
|
|
1321
|
+
url: 'https://example.com/',
|
|
1322
|
+
},
|
|
1323
|
+
})
|
|
1324
|
+
await client.call({
|
|
1325
|
+
method: 'Page.navigate',
|
|
1326
|
+
params: {
|
|
1327
|
+
browser: 'work',
|
|
1328
|
+
target: {
|
|
1329
|
+
name: 'notes',
|
|
1330
|
+
group: 'Research',
|
|
1331
|
+
},
|
|
1332
|
+
url: 'https://example.com/notes',
|
|
1333
|
+
},
|
|
1334
|
+
})
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
| Group option | Default | Meaning |
|
|
1338
|
+
| --- | --- | --- |
|
|
1339
|
+
| `name` | required in an object | Logical name in `record.groups`; also the visible group title. A string reference creates a default group if this name is new. |
|
|
1340
|
+
| `color` | unset | Chrome's native color selection for new groups; otherwise retain the current color. Explicit values: `grey`, `blue`, `red`, `yellow`, `green`, `pink`, `purple`, `cyan`, `orange`. |
|
|
1341
|
+
|
|
1342
|
+
The first use of a group name records its configuration. Concurrent target
|
|
1343
|
+
creation shares that definition and serializes group operations. Names are scoped
|
|
1344
|
+
to the logical browser. Each window has its own native group for that name:
|
|
1345
|
+
tabs stay in their current window, including separate regular/incognito windows.
|
|
1346
|
+
Groups are created by the library, not adopted from unrelated groups that happen
|
|
1347
|
+
to have the same visible title. Incognito still requires the bridge's user grant.
|
|
1348
|
+
|
|
1349
|
+
Grouping happens before optional CDP setup, including with `initialize: false`.
|
|
1350
|
+
It also works with `targetId` selecting an existing tab. As with other target
|
|
1351
|
+
options, passing a new group on a target label that was already configured does
|
|
1352
|
+
not reconfigure that label. Use another label with its physical `targetId`, or
|
|
1353
|
+
edit the public record and reinitialize the target.
|
|
1354
|
+
|
|
1355
|
+
Reconnection reapplies the name/color and rejoins a surviving group. If its last
|
|
1356
|
+
tab was closed or ungrouped, Chrome removes the native group; later target setup
|
|
1357
|
+
creates a new one. `record.groups[name].window_groups[windowId]` stores the last
|
|
1358
|
+
known native group ID. These IDs are checked against Chrome during setup and can
|
|
1359
|
+
remain stale between operations; no extra event subscriptions or persistence are
|
|
1360
|
+
added. In reverse target info, `groupId` is the current native ID, or `-1` when
|
|
1361
|
+
ungrouped. Generic `chrome.tabs` / `chrome.tabGroups` calls remain available for
|
|
1362
|
+
immediate native updates. Native errors, including invalid colors, reject setup
|
|
1363
|
+
with the usual RPC error context and can be retried after correcting the record.
|
|
1364
|
+
|
|
1365
|
+
Direct CDP does not expose browser tab-group commands. A non-false `group` option
|
|
1366
|
+
therefore rejects before connecting or launching in direct mode. The bridge's
|
|
1367
|
+
existing `tabGroups` permission is sufficient; no extra permission is added.
|
|
1368
|
+
See [Chrome tab groups](https://developer.chrome.com/docs/extensions/reference/api/tabGroups)
|
|
1369
|
+
and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/tabs#method-group).
|
|
1370
|
+
|
|
1371
|
+
### Target initialization
|
|
1372
|
+
|
|
1286
1373
|
Domain options set to false skip setup commands; they do not actively disable a
|
|
1287
1374
|
domain already enabled by another caller. Likewise, `binding: false` does not
|
|
1288
1375
|
remove a previously installed binding. Use native CDP commands for immediate
|
|
@@ -1544,8 +1631,8 @@ dictionary containers have a null prototype.
|
|
|
1544
1631
|
| `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
|
|
1545
1632
|
| `client.custom_methods[name]` | Handler function. |
|
|
1546
1633
|
| `client.browsers[name]` | Browser configuration fields plus live state. |
|
|
1547
|
-
| `client.reverse_sockets[
|
|
1548
|
-
| `record.extension_id`, `record.extension_path` |
|
|
1634
|
+
| `client.reverse_sockets[browser_name]` | Announced socket (native extension ID for passive attachment); native on Deno, an EventTarget adapter on Bun. |
|
|
1635
|
+
| `record.extension_id`, `record.extension_path` | Observed native extension identity and, when installed by the manager, the copy's canonical directory. |
|
|
1549
1636
|
| `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
|
|
1550
1637
|
| `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
|
|
1551
1638
|
| `record.socket` | Current `jsrpc` connection once created. |
|
|
@@ -1554,6 +1641,7 @@ dictionary containers have a null prototype.
|
|
|
1554
1641
|
| `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
|
|
1555
1642
|
| `record.contexts[name]` | Recorded context options, native `browserContextId` or null, and pending `connecting`/`closing` promises. Direct mode only. |
|
|
1556
1643
|
| `record.context_checking` | Pending automatic empty-context cleanup, otherwise unset/null. |
|
|
1644
|
+
| `record.groups[name]` | Recorded tab-group options, per-window native IDs in `window_groups`, and a pending `connecting` operation. Reverse only. |
|
|
1557
1645
|
| `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
|
|
1558
1646
|
| `record.session_targets[sessionId]` | Physical `targetId` for a session. |
|
|
1559
1647
|
| `socket.pending[id]` | Response-wait promise, removed on completion. |
|
|
@@ -1602,7 +1690,10 @@ try {
|
|
|
1602
1690
|
`browser.launch` builds arguments, starts the process, reads the
|
|
1603
1691
|
DevTools WebSocket URL from stderr and returns `{ proc, url }`. With `cdp_ext: true`,
|
|
1604
1692
|
it waits for process spawn instead and returns `url: undefined`; the caller must
|
|
1605
|
-
prepare the extension and supply its directory as `extension_path
|
|
1693
|
+
prepare the extension and supply its directory as `extension_path` when loading
|
|
1694
|
+
it. With `cdp_ext_install: false`, the helper skips the automation preset, uses
|
|
1695
|
+
the same option/argument/wildcard handling, and returns an unreferenced launcher.
|
|
1696
|
+
`bootstrap_url` is the optional startup URL, defaulting to `about:blank`.
|
|
1606
1697
|
It does not open a CDP connection or write preference files; `update_preferences`, `preferences`
|
|
1607
1698
|
and `local_state` are manager options, ignored by this helper.
|
|
1608
1699
|
To prepare files independently, call
|
|
@@ -1641,6 +1732,7 @@ labels, target labels, custom methods or reconnection policy in this layer.
|
|
|
1641
1732
|
| Failed discovery or local launch | Reject the call establishing the connection. |
|
|
1642
1733
|
| Named context in reverse mode | Reject explicitly; no separate process emulation. |
|
|
1643
1734
|
| Reverse incognito without the extension's user grant | Reject with an instruction to enable Allow in incognito. |
|
|
1735
|
+
| Tab group in direct mode | Reject before connecting; tab groups require the reverse extension APIs. |
|
|
1644
1736
|
| Automatic context cleanup fails | Emit the manager's `error` event with browser label and original error. |
|
|
1645
1737
|
|
|
1646
1738
|
```js
|
|
@@ -1688,6 +1780,8 @@ The shared Deno/Bun reverse suite loads the real extension into two headless Chr
|
|
|
1688
1780
|
server port without remote-debugging arguments. It checks native/custom calls,
|
|
1689
1781
|
Network events, webRequest notifications without debugger attachment, bindings,
|
|
1690
1782
|
child sessions, reconnect, detach/recreate, incognito and isolation.
|
|
1783
|
+
Tab-group checks cover concurrent creation, existing tabs, title/color, per-window
|
|
1784
|
+
incognito separation, reconnection, recreation and native validation/retry.
|
|
1691
1785
|
Node coverage also checks actual browser/context proxy routing, shared private
|
|
1692
1786
|
storage, concurrent context creation, automatic disposal, unselected targets and
|
|
1693
1787
|
context ownership in externally attached browsers.
|
|
@@ -1706,5 +1800,10 @@ Managed-launch coverage checks concurrent first use, profile-specific identity,
|
|
|
1706
1800
|
connection timeout without process replacement, lazy listener startup/shutdown
|
|
1707
1801
|
independent of direct browsers, and relaunch after confirmed exit. External reverse
|
|
1708
1802
|
timeouts release the listening port; other live reverse connections retain it.
|
|
1803
|
+
URL bootstrap also uses disposable normal Chrome profiles to check reuse of an
|
|
1804
|
+
already running instance, startup when absent, one shared extension directory,
|
|
1805
|
+
logical names, binding tokens, conflicting names and preservation on manager close.
|
|
1806
|
+
Installation defaults, raw directory overrides, profile writes independent of
|
|
1807
|
+
installation and shared wildcard expansion are covered without opening personal profiles.
|
|
1709
1808
|
The npm tarball contains `package.json`, `cdp.js`, `README.md` and the two extension
|
|
1710
1809
|
files required by managed reverse launches.
|
package/cdp.js
CHANGED
|
@@ -125,7 +125,10 @@ export class browser {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
static build_args(options = {}) {
|
|
128
|
-
const args = {
|
|
128
|
+
const args = options.cdp_ext && options.cdp_ext_install === false ? {
|
|
129
|
+
'user-data-dir': options.user_data_dir,
|
|
130
|
+
'disable-features': [],
|
|
131
|
+
} : {
|
|
129
132
|
'enable-automation': true,
|
|
130
133
|
'mute-audio': true,
|
|
131
134
|
'disable-blink-features': 'AutomationControlled',
|
|
@@ -264,10 +267,17 @@ export class browser {
|
|
|
264
267
|
directory === '*' ? installed : directory,
|
|
265
268
|
))]
|
|
266
269
|
}
|
|
267
|
-
const
|
|
270
|
+
const external = options.cdp_ext && options.cdp_ext_install === false
|
|
271
|
+
const proc = child_process.spawn(options.executable_path || browser.find_executable_path(), [...util.args_to_strings(args), options.bootstrap_url ?? 'about:blank'], {
|
|
268
272
|
windowsHide: options.windowsHide ?? (args.headless != null && args.headless !== false),
|
|
269
|
-
|
|
273
|
+
detached: external,
|
|
274
|
+
stdio: external ? 'ignore' : ['ignore', 'ignore', 'pipe'],
|
|
270
275
|
})
|
|
276
|
+
if (external) {
|
|
277
|
+
await once(proc, 'spawn')
|
|
278
|
+
proc.unref()
|
|
279
|
+
return { proc }
|
|
280
|
+
}
|
|
271
281
|
let diagnostic = ''
|
|
272
282
|
const ready = util.on_first(proc, options.cdp_ext ? 'spawn exit' : 'ready exit', options.connect_timeout_ms ?? 15000)
|
|
273
283
|
const data = chunk => {
|
|
@@ -359,11 +369,23 @@ export class cdp extends EventTarget {
|
|
|
359
369
|
if (this.server) throw new Error('Reverse server is already listening')
|
|
360
370
|
const accept = (socket, origin) => {
|
|
361
371
|
socket.addEventListener('message', event => {
|
|
362
|
-
const
|
|
363
|
-
|
|
372
|
+
const message = JSON.parse(event.data)
|
|
373
|
+
const id = message.browser
|
|
374
|
+
const extension_id = message.extension_id ?? id
|
|
375
|
+
const record = this.browsers[id]
|
|
376
|
+
if (typeof id !== 'string' || extension_id !== origin.slice('chrome-extension://'.length) ||
|
|
377
|
+
(record?.reverse_token && message.token !== record.reverse_token) ||
|
|
378
|
+
(id !== extension_id && !record?.reverse_token) ||
|
|
364
379
|
this.reverse_sockets[id]?.readyState < WebSocket.CLOSING) {
|
|
365
|
-
return socket.close(1008, 'Invalid or already connected
|
|
380
|
+
return socket.close(1008, 'Invalid or already connected reverse browser')
|
|
381
|
+
}
|
|
382
|
+
if (message.error) {
|
|
383
|
+
const error = new Error(message.error)
|
|
384
|
+
if (record) record.reverse_error = error
|
|
385
|
+
util.emit(this, 'reverse_' + encodeURIComponent(id), error)
|
|
386
|
+
return socket.close(1008, 'Chrome profile already connected')
|
|
366
387
|
}
|
|
388
|
+
if (record) record.extension_id = extension_id
|
|
367
389
|
this.reverse_sockets[id] = socket
|
|
368
390
|
socket.addEventListener('close', () => {
|
|
369
391
|
if (this.reverse_sockets[id] === socket) delete this.reverse_sockets[id]
|
|
@@ -373,6 +395,9 @@ export class cdp extends EventTarget {
|
|
|
373
395
|
}, { once: true })
|
|
374
396
|
}
|
|
375
397
|
const fetch = (request, server) => {
|
|
398
|
+
if (request.method === 'GET' && new URL(request.url).pathname === '/cdp_ext') {
|
|
399
|
+
return new Response('CDP.js browser connection')
|
|
400
|
+
}
|
|
376
401
|
const origin = request.headers.get('origin')
|
|
377
402
|
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
378
403
|
!origin?.startsWith('chrome-extension://')) {
|
|
@@ -594,6 +619,21 @@ export class cdp extends EventTarget {
|
|
|
594
619
|
if (record.reverse || record.cdp_ext) {
|
|
595
620
|
await this.server_closing
|
|
596
621
|
if (!this.server) this.listen_reverse()
|
|
622
|
+
let host = this.server.addr.hostname
|
|
623
|
+
if (host === '0.0.0.0') host = '127.0.0.1'
|
|
624
|
+
if (host === '::') host = '::1'
|
|
625
|
+
const address = (host.includes(':') ? '[' + host + ']' : host) + ':' + this.server.addr.port
|
|
626
|
+
record.reverse_url = 'ws://' + address
|
|
627
|
+
if (record.cdp_ext) {
|
|
628
|
+
if (!['127.0.0.1', 'localhost', '::1'].includes(host)) throw new Error('URL bootstrap requires a loopback reverse listener')
|
|
629
|
+
record.reverse_token ??= crypto.randomUUID()
|
|
630
|
+
const bootstrap = new URL('http://' + address + '/cdp_ext')
|
|
631
|
+
bootstrap.searchParams.set('browser', record.name)
|
|
632
|
+
bootstrap.searchParams.set('websocket_url', record.reverse_url)
|
|
633
|
+
bootstrap.searchParams.set('token', record.reverse_token)
|
|
634
|
+
record.bootstrap_url = bootstrap.href
|
|
635
|
+
}
|
|
636
|
+
delete record.reverse_error
|
|
597
637
|
}
|
|
598
638
|
let url = record.websocket_url
|
|
599
639
|
if (!record.reverse && !record.cdp_ext && !url && (record.http_url || record.port)) {
|
|
@@ -604,10 +644,12 @@ export class cdp extends EventTarget {
|
|
|
604
644
|
url = (await response.json()).webSocketDebuggerUrl
|
|
605
645
|
}
|
|
606
646
|
if (record.cdp_ext || (!record.reverse && !url)) {
|
|
607
|
-
|
|
647
|
+
const args = browser.build_args(record)
|
|
648
|
+
if (record.cdp_ext) record.cdp_ext_install ??= !!args['user-data-dir']
|
|
649
|
+
if ((record.proc?.exitCode === null && record.proc.signalCode === null) ||
|
|
650
|
+
(record.opener?.exitCode === null && record.opener.signalCode === null)) url = record.socket?.url
|
|
608
651
|
else {
|
|
609
|
-
|
|
610
|
-
if (record.cdp_ext) {
|
|
652
|
+
if (record.cdp_ext && record.cdp_ext_install) {
|
|
611
653
|
record.extension_path = path.resolve(
|
|
612
654
|
args['user-data-dir'] || path.join(this.base_path, record.name),
|
|
613
655
|
args['profile-directory'] || 'Default',
|
|
@@ -617,10 +659,7 @@ export class cdp extends EventTarget {
|
|
|
617
659
|
record.extension_path = await fs.realpath(record.extension_path)
|
|
618
660
|
record.extension_id = browser.extension_id(record.extension_path)
|
|
619
661
|
const manifest = JSON.parse(await fs.readFile(new URL('./cdp_ext/manifest.json', import.meta.url), 'utf8'))
|
|
620
|
-
|
|
621
|
-
if (host === '0.0.0.0') host = '127.0.0.1'
|
|
622
|
-
if (host === '::') host = '::1'
|
|
623
|
-
manifest.cdp.websocket_url = 'ws://' + (host.includes(':') ? '[' + host + ']' : host) + ':' + this.server.addr.port
|
|
662
|
+
manifest.cdp.websocket_url = record.reverse_url
|
|
624
663
|
await fs.writeFile(path.join(record.extension_path, 'manifest.json'), JSON.stringify(manifest))
|
|
625
664
|
await fs.copyFile(new URL('./cdp_ext/cdp_ext.js', import.meta.url), path.join(record.extension_path, 'cdp_ext.js'))
|
|
626
665
|
}
|
|
@@ -635,17 +674,28 @@ export class cdp extends EventTarget {
|
|
|
635
674
|
await browser.update_profile_preferences(path.join(args['user-data-dir'], 'Local State'), record.local_state)
|
|
636
675
|
}
|
|
637
676
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
677
|
+
if (!record.cdp_ext || record.cdp_ext_install) {
|
|
678
|
+
const launched = await browser.launch(record)
|
|
679
|
+
record.proc = launched.proc
|
|
680
|
+
if (record.cdp_ext) record.proc.once('exit', () => this._close_reverse_if_idle())
|
|
681
|
+
url = launched.url
|
|
682
|
+
}
|
|
642
683
|
}
|
|
643
684
|
}
|
|
644
685
|
if (record.reverse || record.cdp_ext) {
|
|
645
|
-
const id = record.extension_id ?? record.name
|
|
686
|
+
const id = record.cdp_ext ? record.name : record.extension_id ?? record.name
|
|
646
687
|
url = this.reverse_sockets[id]
|
|
647
688
|
if (url?.readyState !== WebSocket.OPEN) {
|
|
648
|
-
|
|
689
|
+
if (record.cdp_ext && !record.cdp_ext_install &&
|
|
690
|
+
!(record.opener?.exitCode === null && record.opener.signalCode === null)) {
|
|
691
|
+
record.opener = (await browser.launch(record)).proc
|
|
692
|
+
}
|
|
693
|
+
if (record.reverse_error) throw record.reverse_error
|
|
694
|
+
url = this.reverse_sockets[id]
|
|
695
|
+
if (url?.readyState !== WebSocket.OPEN) {
|
|
696
|
+
url = (await util.on_first(this, 'reverse_' + encodeURIComponent(id), record.connect_timeout_ms ?? 15000)).args[0].detail
|
|
697
|
+
}
|
|
698
|
+
if (url instanceof Error) throw url
|
|
649
699
|
}
|
|
650
700
|
}
|
|
651
701
|
const socket = record.socket = new jsrpc(url, record)
|
|
@@ -684,9 +734,58 @@ export class cdp extends EventTarget {
|
|
|
684
734
|
}
|
|
685
735
|
}
|
|
686
736
|
|
|
737
|
+
async _group(browser, target) {
|
|
738
|
+
const socket = browser.socket
|
|
739
|
+
const group = browser.groups[target.group]
|
|
740
|
+
while (group.connecting) await group.connecting
|
|
741
|
+
return group.connecting = (async () => {
|
|
742
|
+
const tab = await socket.req({
|
|
743
|
+
method: 'chrome.tabs.get',
|
|
744
|
+
params: { args: [Number(target.targetId)] },
|
|
745
|
+
})
|
|
746
|
+
const groups = await socket.req({
|
|
747
|
+
method: 'chrome.tabGroups.query',
|
|
748
|
+
params: { args: [{ windowId: tab.windowId }] },
|
|
749
|
+
})
|
|
750
|
+
const groupId = group.window_groups[tab.windowId]
|
|
751
|
+
const existing = groups.some(value => value.id === groupId)
|
|
752
|
+
const id = await socket.req({
|
|
753
|
+
method: 'chrome.tabs.group',
|
|
754
|
+
params: {
|
|
755
|
+
args: [{
|
|
756
|
+
tabIds: tab.id,
|
|
757
|
+
groupId: existing ? groupId : undefined,
|
|
758
|
+
createProperties: existing ? undefined : { windowId: tab.windowId },
|
|
759
|
+
}],
|
|
760
|
+
},
|
|
761
|
+
})
|
|
762
|
+
group.window_groups[tab.windowId] = id
|
|
763
|
+
await socket.req({
|
|
764
|
+
method: 'chrome.tabGroups.update',
|
|
765
|
+
params: {
|
|
766
|
+
args: [id, {
|
|
767
|
+
title: group.name,
|
|
768
|
+
color: group.color,
|
|
769
|
+
}],
|
|
770
|
+
},
|
|
771
|
+
})
|
|
772
|
+
return id
|
|
773
|
+
})().finally(() => {
|
|
774
|
+
group.connecting = null
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
687
778
|
async _target(browser, target) {
|
|
688
779
|
const socket = browser.socket
|
|
689
780
|
const reverse = browser.reverse || browser.cdp_ext
|
|
781
|
+
if (target.group != null && target.group !== false) {
|
|
782
|
+
const spec = typeof target.group === 'string' ? { name: target.group } : target.group
|
|
783
|
+
browser.groups[spec.name] ??= {
|
|
784
|
+
...spec,
|
|
785
|
+
window_groups: Object.create(null),
|
|
786
|
+
}
|
|
787
|
+
target.group = spec.name
|
|
788
|
+
}
|
|
690
789
|
if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
|
|
691
790
|
target.context = 'incognito'
|
|
692
791
|
}
|
|
@@ -720,6 +819,7 @@ export class cdp extends EventTarget {
|
|
|
720
819
|
Object.assign(info, targetInfo)
|
|
721
820
|
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
722
821
|
}
|
|
822
|
+
if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
|
|
723
823
|
if (!info.sessionId) {
|
|
724
824
|
info.sessionId = (await socket.req({
|
|
725
825
|
method: 'Target.attachToTarget',
|
|
@@ -779,7 +879,7 @@ export class cdp extends EventTarget {
|
|
|
779
879
|
const { browser = 'main', target, ...native } = params
|
|
780
880
|
const spec = typeof browser === 'string' ? { name: browser } : browser
|
|
781
881
|
const record = this.browsers[spec.name] ??= {
|
|
782
|
-
user_data_dir: path.join(this.base_path, spec.name),
|
|
882
|
+
user_data_dir: ((spec.reverse ?? this.options.reverse) || (spec.cdp_ext ?? this.options.cdp_ext)) ? null : path.join(this.base_path, spec.name),
|
|
783
883
|
...this.options,
|
|
784
884
|
...spec,
|
|
785
885
|
args: {
|
|
@@ -796,10 +896,14 @@ export class cdp extends EventTarget {
|
|
|
796
896
|
},
|
|
797
897
|
targets: Object.create(null),
|
|
798
898
|
contexts: Object.create(null),
|
|
899
|
+
groups: Object.create(null),
|
|
799
900
|
}
|
|
800
901
|
if (target?.context != null && (record.reverse || record.cdp_ext)) {
|
|
801
902
|
throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
|
|
802
903
|
}
|
|
904
|
+
if (target?.group != null && target.group !== false && !record.reverse && !record.cdp_ext) {
|
|
905
|
+
throw new Error('Tab groups require the reverse Chrome extension (cdp_ext or reverse)')
|
|
906
|
+
}
|
|
803
907
|
if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
|
|
804
908
|
record.connecting = this._connect(record).catch(error => {
|
|
805
909
|
record.socket?.close()
|
package/cdp_ext/cdp_ext.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const config = chrome.runtime.getManifest().cdp ?? {}
|
|
2
|
+
const configured = chrome.storage.session.get('cdp_bootstrap').then(value => Object.assign(config, value.cdp_bootstrap))
|
|
2
3
|
const sessions = {}
|
|
3
4
|
const attached = {}
|
|
4
5
|
const attaching = {}
|
|
@@ -6,6 +7,50 @@ let socket
|
|
|
6
7
|
let disconnecting
|
|
7
8
|
let discovering = false
|
|
8
9
|
let auto_attach = {}
|
|
10
|
+
let bootstrapping
|
|
11
|
+
|
|
12
|
+
async function bootstrap(tab) {
|
|
13
|
+
const text = tab.pendingUrl ?? tab.url ?? ''
|
|
14
|
+
if (!/^http:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?\/cdp_ext\?/.test(text)) return
|
|
15
|
+
const url = new URL(text)
|
|
16
|
+
const settings = {
|
|
17
|
+
browser: url.searchParams.get('browser'),
|
|
18
|
+
websocket_url: url.searchParams.get('websocket_url'),
|
|
19
|
+
token: url.searchParams.get('token'),
|
|
20
|
+
}
|
|
21
|
+
if (!settings.browser || !settings.websocket_url || !settings.token) return
|
|
22
|
+
const endpoint = new URL(settings.websocket_url)
|
|
23
|
+
if (endpoint.protocol !== 'ws:' || endpoint.host !== url.host) return
|
|
24
|
+
await configured
|
|
25
|
+
while (bootstrapping) await bootstrapping
|
|
26
|
+
return bootstrapping = (async () => {
|
|
27
|
+
const changed = ['browser', 'websocket_url', 'token'].some(key => settings[key] !== config[key])
|
|
28
|
+
if (changed && config.browser && socket?.readyState < WebSocket.CLOSING) {
|
|
29
|
+
const probe = new WebSocket(settings.websocket_url)
|
|
30
|
+
probe.onopen = () => probe.send(JSON.stringify({
|
|
31
|
+
browser: settings.browser,
|
|
32
|
+
extension_id: chrome.runtime.id,
|
|
33
|
+
token: settings.token,
|
|
34
|
+
error: 'This Chrome profile is already connected as ' + config.browser,
|
|
35
|
+
}))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
if (changed) {
|
|
39
|
+
Object.assign(config, settings)
|
|
40
|
+
await chrome.storage.session.set({ cdp_bootstrap: settings })
|
|
41
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
42
|
+
await new Promise(resolve => {
|
|
43
|
+
socket.addEventListener('close', resolve, { once: true })
|
|
44
|
+
socket.close()
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
await disconnecting
|
|
48
|
+
}
|
|
49
|
+
await connect()
|
|
50
|
+
})().finally(() => {
|
|
51
|
+
bootstrapping = null
|
|
52
|
+
})
|
|
53
|
+
}
|
|
9
54
|
|
|
10
55
|
function send(message) {
|
|
11
56
|
if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message))
|
|
@@ -19,6 +64,7 @@ function target_info(tab) {
|
|
|
19
64
|
url: tab.url ?? tab.pendingUrl ?? '',
|
|
20
65
|
attached: !!attached[tab.id],
|
|
21
66
|
incognito: tab.incognito,
|
|
67
|
+
groupId: tab.groupId,
|
|
22
68
|
}
|
|
23
69
|
}
|
|
24
70
|
|
|
@@ -159,12 +205,14 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
159
205
|
})
|
|
160
206
|
chrome.debugger.onDetach.addListener(detached)
|
|
161
207
|
chrome.tabs.onCreated.addListener(tab => {
|
|
208
|
+
bootstrap(tab)
|
|
162
209
|
if (discovering) send({
|
|
163
210
|
method: 'Target.targetCreated',
|
|
164
211
|
params: { targetInfo: target_info(tab) },
|
|
165
212
|
})
|
|
166
213
|
})
|
|
167
214
|
chrome.tabs.onUpdated.addListener((id, changes, tab) => {
|
|
215
|
+
bootstrap(tab)
|
|
168
216
|
if (discovering) send({
|
|
169
217
|
method: 'Target.targetInfoChanged',
|
|
170
218
|
params: { targetInfo: target_info(tab) },
|
|
@@ -195,12 +243,17 @@ for (const [name, options] of Object.entries({
|
|
|
195
243
|
}), { urls: ['<all_urls>'] }, options)
|
|
196
244
|
}
|
|
197
245
|
|
|
198
|
-
function connect() {
|
|
246
|
+
async function connect() {
|
|
247
|
+
await configured
|
|
199
248
|
if (disconnecting) return
|
|
200
249
|
if (socket?.readyState === WebSocket.CONNECTING) return
|
|
201
250
|
if (socket?.readyState === WebSocket.OPEN) return socket.send('')
|
|
202
251
|
const connection = socket = new WebSocket(config.websocket_url ?? 'ws://127.0.0.1:9223')
|
|
203
|
-
connection.onopen = () => connection.send(JSON.stringify({
|
|
252
|
+
connection.onopen = () => connection.send(JSON.stringify({
|
|
253
|
+
browser: config.browser ?? chrome.runtime.id,
|
|
254
|
+
extension_id: chrome.runtime.id,
|
|
255
|
+
token: config.token,
|
|
256
|
+
}))
|
|
204
257
|
connection.onmessage = async event => {
|
|
205
258
|
if (!event.data) return
|
|
206
259
|
const request = JSON.parse(event.data)
|
|
@@ -256,8 +309,19 @@ function connect() {
|
|
|
256
309
|
}
|
|
257
310
|
}
|
|
258
311
|
|
|
259
|
-
|
|
312
|
+
async function start() {
|
|
313
|
+
await configured
|
|
314
|
+
if (!config.browser) {
|
|
315
|
+
for (const tab of (await chrome.tabs.query({})).reverse()) {
|
|
316
|
+
if (config.browser) break
|
|
317
|
+
await bootstrap(tab)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
await connect()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
chrome.runtime.onStartup.addListener(start)
|
|
260
324
|
chrome.alarms.onAlarm.addListener(connect)
|
|
261
325
|
chrome.alarms.create('cdp-reconnect', { periodInMinutes: 0.5 })
|
|
262
326
|
setInterval(connect, config.reconnect_ms ?? 5000)
|
|
263
|
-
|
|
327
|
+
start()
|
package/cdp_ext/manifest.json
CHANGED
package/package.json
CHANGED