@mefistofelix/cdp.js 0.1.2 → 0.2.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 +855 -28
- package/cdp.js +335 -36
- package/cdp_ext/cdp_ext.js +263 -0
- package/cdp_ext/manifest.json +76 -0
- package/package.json +4 -2
package/cdp.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as child_process from 'node:child_process'
|
|
|
2
2
|
import * as fs from 'node:fs/promises'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import * as path from 'node:path'
|
|
5
|
+
import * as crypto from 'node:crypto'
|
|
5
6
|
import { once, setMaxListeners } from 'node:events'
|
|
6
7
|
|
|
7
8
|
export class util {
|
|
@@ -45,15 +46,19 @@ export class util {
|
|
|
45
46
|
|
|
46
47
|
export class jsrpc extends WebSocket {
|
|
47
48
|
constructor(url, options = {}) {
|
|
48
|
-
super(url)
|
|
49
|
-
setMaxListeners(0,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const socket = url instanceof WebSocket ? url : super(url)
|
|
50
|
+
setMaxListeners(0, socket)
|
|
51
|
+
socket.id = 0
|
|
52
|
+
socket.pending = {}
|
|
53
|
+
socket.timeout_ms = options.request_timeout_ms ?? 30000
|
|
54
|
+
socket.req = jsrpc.prototype.req
|
|
55
|
+
socket.notify = jsrpc.prototype.notify
|
|
56
|
+
socket.addEventListener('message', event => {
|
|
57
|
+
if (!event.data) return
|
|
54
58
|
const message = JSON.parse(event.data)
|
|
55
|
-
util.emit(
|
|
59
|
+
util.emit(socket, message.id === undefined ? 'notify' : 'rpc_' + message.id, message)
|
|
56
60
|
})
|
|
61
|
+
return socket
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
async req(request, timeout_ms = this.timeout_ms) {
|
|
@@ -91,6 +96,12 @@ export class jsrpc extends WebSocket {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
export class browser {
|
|
99
|
+
static extension_id(directory) {
|
|
100
|
+
const normalized = path.resolve(directory).replace(/^[a-z]:/, drive => drive.toUpperCase())
|
|
101
|
+
return crypto.createHash('sha256').update(normalized, process.platform === 'win32' ? 'utf16le' : 'utf8')
|
|
102
|
+
.digest('hex').slice(0, 32).replace(/./g, digit => 'abcdefghijklmnop'[parseInt(digit, 16)])
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
static find_executable_path() {
|
|
95
106
|
if (process.env.CDP_BROWSER) return process.env.CDP_BROWSER
|
|
96
107
|
const candidates = process.platform === 'win32'
|
|
@@ -167,11 +178,23 @@ export class browser {
|
|
|
167
178
|
args.headless = 'new'
|
|
168
179
|
args['window-size'] = '1440,900'
|
|
169
180
|
}
|
|
170
|
-
if (options.extensions
|
|
181
|
+
if (Array.isArray(options.extensions)) args['disable-extensions-except'] = options.extensions
|
|
182
|
+
if (options.extensions !== true && !Array.isArray(options.extensions)) {
|
|
171
183
|
args['disable-extensions'] = true
|
|
172
184
|
args['disable-component-extensions-with-background-pages'] = true
|
|
173
185
|
}
|
|
186
|
+
if (options.cdp_ext) {
|
|
187
|
+
args['remote-debugging-port'] = false
|
|
188
|
+
delete args['disable-extensions']
|
|
189
|
+
delete args['disable-component-extensions-with-background-pages']
|
|
190
|
+
if (options.extension_path) args['disable-extensions-except'] = [
|
|
191
|
+
options.extension_path,
|
|
192
|
+
...(args['disable-extensions-except'] ?? (options.extensions === true ? ['*'] : [])),
|
|
193
|
+
]
|
|
194
|
+
}
|
|
174
195
|
if (options.images === false) args['blink-settings'] = 'imagesEnabled=false'
|
|
196
|
+
if (options.proxy === false) args['no-proxy-server'] = true
|
|
197
|
+
else if (options.proxy != null) args['proxy-server'] = options.proxy
|
|
175
198
|
if (options.translations !== true) {
|
|
176
199
|
args['disable-features'].push('Translate', 'TranslateToast', 'EnableTranslatePdf')
|
|
177
200
|
}
|
|
@@ -180,7 +203,39 @@ export class browser {
|
|
|
180
203
|
args['disable-sync'] = true
|
|
181
204
|
args['disable-features'].push('DiceWebSigninInterception', 'SigninPromoOnAvatarPill')
|
|
182
205
|
}
|
|
183
|
-
|
|
206
|
+
Object.assign(args, options.args)
|
|
207
|
+
const extensions = args['disable-extensions-except']
|
|
208
|
+
if (extensions != null && extensions !== false) {
|
|
209
|
+
if (extensions === true) args['disable-extensions-except'] = []
|
|
210
|
+
else args['disable-extensions-except'] = Array.isArray(extensions) ? [...extensions] : String(extensions).split(',')
|
|
211
|
+
const features = args['disable-features']
|
|
212
|
+
args['disable-features'] = Array.isArray(features) ? [...features] : features ? String(features).split(',') : []
|
|
213
|
+
const feature = 'DisableDisableExtensionsExceptCommandLineSwitch'
|
|
214
|
+
if (!args['disable-features'].includes(feature)) args['disable-features'].push(feature)
|
|
215
|
+
}
|
|
216
|
+
return args
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static async profile_extension_paths(args) {
|
|
220
|
+
if (!args['user-data-dir']) throw new Error('Reading profile extensions requires user-data-dir')
|
|
221
|
+
const profile = path.join(args['user-data-dir'], args['profile-directory'] || 'Default')
|
|
222
|
+
const settings = {}
|
|
223
|
+
for (const filename of ['Preferences', 'Secure Preferences']) {
|
|
224
|
+
const content = await fs.readFile(path.join(profile, filename), 'utf8').catch(error => {
|
|
225
|
+
if (error.code !== 'ENOENT') throw error
|
|
226
|
+
return '{}'
|
|
227
|
+
})
|
|
228
|
+
Object.assign(settings, JSON.parse(content).extensions?.settings)
|
|
229
|
+
}
|
|
230
|
+
for (const [id, extension] of Object.entries(settings)) {
|
|
231
|
+
if (!extension.path || [5, 10].includes(extension.location)) delete settings[id]
|
|
232
|
+
else settings[id] = {
|
|
233
|
+
path: path.isAbsolute(extension.path) ? extension.path : path.join(profile, 'Extensions', extension.path),
|
|
234
|
+
enabled: extension.state !== 0 &&
|
|
235
|
+
!(Array.isArray(extension.disable_reasons) ? extension.disable_reasons.length : extension.disable_reasons),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return settings
|
|
184
239
|
}
|
|
185
240
|
|
|
186
241
|
static async update_profile_preferences(filename, preferences) {
|
|
@@ -201,19 +256,20 @@ export class browser {
|
|
|
201
256
|
|
|
202
257
|
static async launch(options) {
|
|
203
258
|
const args = browser.build_args(options)
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
'
|
|
209
|
-
|
|
210
|
-
|
|
259
|
+
const extensions = args['disable-extensions-except']
|
|
260
|
+
if (extensions && extensions.includes('*')) {
|
|
261
|
+
const installed = Object.values(await browser.profile_extension_paths(args))
|
|
262
|
+
.filter(extension => extension.enabled).map(extension => extension.path)
|
|
263
|
+
args['disable-extensions-except'] = [...new Set(extensions.flatMap(directory =>
|
|
264
|
+
directory === '*' ? installed : directory,
|
|
265
|
+
))]
|
|
266
|
+
}
|
|
211
267
|
const proc = child_process.spawn(options.executable_path || browser.find_executable_path(), [...util.args_to_strings(args), 'about:blank'], {
|
|
212
268
|
windowsHide: options.windowsHide ?? (args.headless != null && args.headless !== false),
|
|
213
269
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
214
270
|
})
|
|
215
271
|
let diagnostic = ''
|
|
216
|
-
const ready = util.on_first(proc, 'ready exit', options.connect_timeout_ms ?? 15000)
|
|
272
|
+
const ready = util.on_first(proc, options.cdp_ext ? 'spawn exit' : 'ready exit', options.connect_timeout_ms ?? 15000)
|
|
217
273
|
const data = chunk => {
|
|
218
274
|
diagnostic = (diagnostic + chunk).slice(-8192)
|
|
219
275
|
const match = diagnostic.match(/DevTools listening on (ws:\/\/\S+)/)
|
|
@@ -285,14 +341,68 @@ const custom_methods = {
|
|
|
285
341
|
}
|
|
286
342
|
|
|
287
343
|
export class cdp extends EventTarget {
|
|
288
|
-
constructor({ base_path = '.cdp', ...options } = {}) {
|
|
344
|
+
constructor({ base_path = '.cdp', reverse_server = {}, ...options } = {}) {
|
|
289
345
|
super()
|
|
290
346
|
this.base_path = path.resolve(base_path)
|
|
291
347
|
this.options = options
|
|
348
|
+
this.reverse_server = { ...reverse_server }
|
|
292
349
|
this.browsers = Object.create(null)
|
|
350
|
+
this.reverse_sockets = Object.create(null)
|
|
293
351
|
this.custom_methods = { ...custom_methods }
|
|
294
352
|
}
|
|
295
353
|
|
|
354
|
+
listen_reverse({
|
|
355
|
+
hostname = '127.0.0.1',
|
|
356
|
+
port = 9223,
|
|
357
|
+
} = this.reverse_server) {
|
|
358
|
+
if (!globalThis.Deno) throw new Error('The reverse WebSocket server requires Deno')
|
|
359
|
+
if (this.server) throw new Error('Reverse server is already listening')
|
|
360
|
+
this.server = Deno.serve({
|
|
361
|
+
hostname,
|
|
362
|
+
port,
|
|
363
|
+
}, request => {
|
|
364
|
+
const origin = request.headers.get('origin')
|
|
365
|
+
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
366
|
+
!origin?.startsWith('chrome-extension://')) {
|
|
367
|
+
return new Response(null, { status: 403 })
|
|
368
|
+
}
|
|
369
|
+
const { socket, response } = Deno.upgradeWebSocket(request)
|
|
370
|
+
socket.addEventListener('message', event => {
|
|
371
|
+
const id = JSON.parse(event.data).browser
|
|
372
|
+
if (id !== origin.slice('chrome-extension://'.length) ||
|
|
373
|
+
this.reverse_sockets[id]?.readyState < WebSocket.CLOSING) {
|
|
374
|
+
return socket.close(1008, 'Invalid or already connected extension ID')
|
|
375
|
+
}
|
|
376
|
+
this.reverse_sockets[id] = socket
|
|
377
|
+
socket.addEventListener('close', () => {
|
|
378
|
+
if (this.reverse_sockets[id] === socket) delete this.reverse_sockets[id]
|
|
379
|
+
this._close_reverse_if_idle()
|
|
380
|
+
})
|
|
381
|
+
util.emit(this, 'reverse_' + encodeURIComponent(id), socket)
|
|
382
|
+
}, { once: true })
|
|
383
|
+
return response
|
|
384
|
+
})
|
|
385
|
+
this.reverse_server = {
|
|
386
|
+
hostname,
|
|
387
|
+
port: this.server.addr.port,
|
|
388
|
+
}
|
|
389
|
+
return this.server
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_close_reverse_if_idle() {
|
|
393
|
+
if (this.server_closing) return this.server_closing
|
|
394
|
+
if (!this.server || Object.values(this.reverse_sockets).some(socket => socket.readyState < WebSocket.CLOSING)) return
|
|
395
|
+
for (const record of Object.values(this.browsers)) {
|
|
396
|
+
if (!record.reverse && !record.cdp_ext) continue
|
|
397
|
+
if (record.connecting) return
|
|
398
|
+
if (record.proc?.exitCode === null && record.proc.signalCode === null) return
|
|
399
|
+
}
|
|
400
|
+
return this.server_closing = this.server.shutdown().finally(() => {
|
|
401
|
+
this.server = null
|
|
402
|
+
this.server_closing = null
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
296
406
|
evaluate_xpath(params, fn, options) {
|
|
297
407
|
if (params.target == null) throw new Error('Custom method requires target')
|
|
298
408
|
const xpath = util.normalize_xpath(params.xpath)
|
|
@@ -326,7 +436,8 @@ export class cdp extends EventTarget {
|
|
|
326
436
|
browser: browser.name,
|
|
327
437
|
}
|
|
328
438
|
const sessionId = params.sessionId || message.sessionId
|
|
329
|
-
const targetId = params.targetInfo?.targetId || params.targetId || browser.session_targets[sessionId]
|
|
439
|
+
const targetId = params.targetInfo?.targetId || params.targetId || browser.session_targets[sessionId] ||
|
|
440
|
+
(params.tabId >= 0 ? String(params.tabId) : undefined)
|
|
330
441
|
const target = Object.values(browser.targets).find(value => value.targetId === targetId)
|
|
331
442
|
if (params.targetInfo) {
|
|
332
443
|
Object.assign(browser.target_info[targetId] ??= {}, params.targetInfo)
|
|
@@ -347,26 +458,152 @@ export class cdp extends EventTarget {
|
|
|
347
458
|
if (['Target.targetDestroyed', 'Target.targetCrashed'].includes(message.method)) {
|
|
348
459
|
this._detach(browser, info?.sessionId)
|
|
349
460
|
delete browser.target_info[targetId]
|
|
461
|
+
this._close_contexts_if_idle(browser)
|
|
350
462
|
}
|
|
351
463
|
}
|
|
352
464
|
|
|
465
|
+
async _context(browser, spec) {
|
|
466
|
+
if (typeof spec === 'string') spec = { name: spec }
|
|
467
|
+
const context = browser.contexts[spec.name] ??= {
|
|
468
|
+
...spec,
|
|
469
|
+
browserContextId: null,
|
|
470
|
+
}
|
|
471
|
+
await context.closing
|
|
472
|
+
if (!context.browserContextId) {
|
|
473
|
+
context.connecting ??= browser.socket.req({
|
|
474
|
+
method: 'Target.createBrowserContext',
|
|
475
|
+
params: {
|
|
476
|
+
proxyServer: context.proxy === false ? 'direct://' : context.proxy,
|
|
477
|
+
...context.create_params,
|
|
478
|
+
},
|
|
479
|
+
}).then(result => {
|
|
480
|
+
context.browserContextId = result.browserContextId
|
|
481
|
+
}).finally(() => {
|
|
482
|
+
context.connecting = null
|
|
483
|
+
})
|
|
484
|
+
await context.connecting
|
|
485
|
+
}
|
|
486
|
+
return context
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_dispose_context(browser, context, params = { browserContextId: context.browserContextId }) {
|
|
490
|
+
const id = context.browserContextId
|
|
491
|
+
return context.closing ??= browser.socket.req({
|
|
492
|
+
method: 'Target.disposeBrowserContext',
|
|
493
|
+
params,
|
|
494
|
+
}).then(result => {
|
|
495
|
+
context.browserContextId = null
|
|
496
|
+
for (const [targetId, info] of Object.entries(browser.target_info)) {
|
|
497
|
+
if (info.browserContextId !== id) continue
|
|
498
|
+
this._detach(browser, info.sessionId)
|
|
499
|
+
delete browser.target_info[targetId]
|
|
500
|
+
}
|
|
501
|
+
util.emit(this, 'context_closed', {
|
|
502
|
+
browser: browser.name,
|
|
503
|
+
context: context.name,
|
|
504
|
+
browserContextId: id,
|
|
505
|
+
})
|
|
506
|
+
return result
|
|
507
|
+
}).finally(() => {
|
|
508
|
+
context.closing = null
|
|
509
|
+
})
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_close_contexts_if_idle(browser) {
|
|
513
|
+
if (browser.context_checking) {
|
|
514
|
+
browser.context_check_pending = true
|
|
515
|
+
return browser.context_checking
|
|
516
|
+
}
|
|
517
|
+
if (browser.connecting || browser.closing || browser.socket?.readyState !== WebSocket.OPEN) return
|
|
518
|
+
const contexts = Object.values(browser.contexts).filter(context =>
|
|
519
|
+
context.browserContextId && !context.connecting && !context.closing &&
|
|
520
|
+
!Object.values(browser.target_info).some(info =>
|
|
521
|
+
info.type !== 'browser_ui' && info.browserContextId === context.browserContextId,
|
|
522
|
+
),
|
|
523
|
+
)
|
|
524
|
+
if (!contexts.length) return
|
|
525
|
+
const socket = browser.socket
|
|
526
|
+
return browser.context_checking = (async () => {
|
|
527
|
+
do {
|
|
528
|
+
browser.context_check_pending = false
|
|
529
|
+
const { targetInfos } = await socket.req({ method: 'Target.getTargets' })
|
|
530
|
+
if (browser.socket !== socket || browser.closing) return
|
|
531
|
+
for (const context of Object.values(browser.contexts)) {
|
|
532
|
+
if (!context.browserContextId || context.connecting || context.closing) continue
|
|
533
|
+
const belongs = info => info.type !== 'browser_ui' && info.browserContextId === context.browserContextId
|
|
534
|
+
if (targetInfos.some(belongs) || Object.values(browser.target_info).some(belongs)) continue
|
|
535
|
+
if (Object.values(browser.targets).some(target =>
|
|
536
|
+
target.connecting && (target.context?.name ?? target.context) === context.name,
|
|
537
|
+
)) continue
|
|
538
|
+
await this._dispose_context(browser, context)
|
|
539
|
+
}
|
|
540
|
+
} while (browser.context_check_pending)
|
|
541
|
+
})().catch(error => util.emit(this, 'error', {
|
|
542
|
+
browser: browser.name,
|
|
543
|
+
error,
|
|
544
|
+
})).finally(() => {
|
|
545
|
+
browser.context_checking = null
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
353
549
|
async _connect(record) {
|
|
550
|
+
if (record.reverse || record.cdp_ext) {
|
|
551
|
+
await this.server_closing
|
|
552
|
+
if (!this.server) this.listen_reverse()
|
|
553
|
+
}
|
|
354
554
|
let url = record.websocket_url
|
|
355
|
-
if (!url && (record.http_url || record.port)) {
|
|
555
|
+
if (!record.reverse && !record.cdp_ext && !url && (record.http_url || record.port)) {
|
|
356
556
|
const discovery = new URL(record.http_url || 'http://' + (record.host || '127.0.0.1') + ':' + record.port)
|
|
357
557
|
if (!discovery.pathname.endsWith('/json/version')) discovery.pathname = discovery.pathname.replace(/\/$/, '') + '/json/version'
|
|
358
558
|
const response = await fetch(discovery, { signal: AbortSignal.timeout(record.connect_timeout_ms ?? 15000) })
|
|
359
559
|
if (!response.ok) throw new Error('CDP discovery: HTTP ' + response.status)
|
|
360
560
|
url = (await response.json()).webSocketDebuggerUrl
|
|
361
561
|
}
|
|
362
|
-
if (!url) {
|
|
363
|
-
if (record.proc?.exitCode === null && record.proc.signalCode === null) url = record.socket
|
|
562
|
+
if (record.cdp_ext || (!record.reverse && !url)) {
|
|
563
|
+
if (record.proc?.exitCode === null && record.proc.signalCode === null) url = record.socket?.url
|
|
364
564
|
else {
|
|
565
|
+
const args = browser.build_args(record)
|
|
566
|
+
if (record.cdp_ext) {
|
|
567
|
+
record.extension_path = path.resolve(
|
|
568
|
+
args['user-data-dir'] || path.join(this.base_path, record.name),
|
|
569
|
+
args['profile-directory'] || 'Default',
|
|
570
|
+
'cdp_ext',
|
|
571
|
+
)
|
|
572
|
+
await fs.mkdir(record.extension_path, { recursive: true })
|
|
573
|
+
record.extension_path = await fs.realpath(record.extension_path)
|
|
574
|
+
record.extension_id = browser.extension_id(record.extension_path)
|
|
575
|
+
const manifest = JSON.parse(await fs.readFile(new URL('./cdp_ext/manifest.json', import.meta.url), 'utf8'))
|
|
576
|
+
let host = this.server.addr.hostname
|
|
577
|
+
if (host === '0.0.0.0') host = '127.0.0.1'
|
|
578
|
+
if (host === '::') host = '::1'
|
|
579
|
+
manifest.cdp.websocket_url = 'ws://' + (host.includes(':') ? '[' + host + ']' : host) + ':' + this.server.addr.port
|
|
580
|
+
await fs.writeFile(path.join(record.extension_path, 'manifest.json'), JSON.stringify(manifest))
|
|
581
|
+
await fs.copyFile(new URL('./cdp_ext/cdp_ext.js', import.meta.url), path.join(record.extension_path, 'cdp_ext.js'))
|
|
582
|
+
}
|
|
583
|
+
if (args['user-data-dir'] && record.update_preferences !== false) {
|
|
584
|
+
await browser.update_profile_preferences(path.join(args['user-data-dir'], args['profile-directory'] || 'Default', 'Preferences'), {
|
|
585
|
+
'translate.enabled': record.translations === true,
|
|
586
|
+
'signin.allowed': record.login === true,
|
|
587
|
+
'signin.allowed_on_next_startup': record.login === true,
|
|
588
|
+
...record.preferences,
|
|
589
|
+
})
|
|
590
|
+
if (Object.keys(record.local_state).length) {
|
|
591
|
+
await browser.update_profile_preferences(path.join(args['user-data-dir'], 'Local State'), record.local_state)
|
|
592
|
+
}
|
|
593
|
+
}
|
|
365
594
|
const launched = await browser.launch(record)
|
|
366
595
|
record.proc = launched.proc
|
|
596
|
+
if (record.cdp_ext) record.proc.once('exit', () => this._close_reverse_if_idle())
|
|
367
597
|
url = launched.url
|
|
368
598
|
}
|
|
369
599
|
}
|
|
600
|
+
if (record.reverse || record.cdp_ext) {
|
|
601
|
+
const id = record.extension_id ?? record.name
|
|
602
|
+
url = this.reverse_sockets[id]
|
|
603
|
+
if (url?.readyState !== WebSocket.OPEN) {
|
|
604
|
+
url = (await util.on_first(this, 'reverse_' + encodeURIComponent(id), record.connect_timeout_ms ?? 15000)).args[0].detail
|
|
605
|
+
}
|
|
606
|
+
}
|
|
370
607
|
const socket = record.socket = new jsrpc(url, record)
|
|
371
608
|
record.target_info = {}
|
|
372
609
|
record.session_targets = {}
|
|
@@ -380,7 +617,9 @@ export class cdp extends EventTarget {
|
|
|
380
617
|
code: event.code,
|
|
381
618
|
})
|
|
382
619
|
})
|
|
383
|
-
|
|
620
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
621
|
+
await util.on_first(socket, 'open error close', record.connect_timeout_ms ?? 15000)
|
|
622
|
+
}
|
|
384
623
|
await socket.req({
|
|
385
624
|
method: 'Target.setAutoAttach',
|
|
386
625
|
params: {
|
|
@@ -393,21 +632,50 @@ export class cdp extends EventTarget {
|
|
|
393
632
|
method: 'Target.setDiscoverTargets',
|
|
394
633
|
params: { discover: true },
|
|
395
634
|
})
|
|
635
|
+
if (Object.values(record.contexts).some(context => context.browserContextId)) {
|
|
636
|
+
const { browserContextIds } = await socket.req({ method: 'Target.getBrowserContexts' })
|
|
637
|
+
for (const context of Object.values(record.contexts)) {
|
|
638
|
+
if (!browserContextIds.includes(context.browserContextId)) context.browserContextId = null
|
|
639
|
+
}
|
|
640
|
+
}
|
|
396
641
|
}
|
|
397
642
|
|
|
398
643
|
async _target(browser, target) {
|
|
399
644
|
const socket = browser.socket
|
|
645
|
+
const reverse = browser.reverse || browser.cdp_ext
|
|
646
|
+
if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
|
|
647
|
+
target.context = 'incognito'
|
|
648
|
+
}
|
|
649
|
+
if (target.context != null && target.create_params?.browserContextId != null) {
|
|
650
|
+
throw new Error('Use target.context or create_params.browserContextId, not both')
|
|
651
|
+
}
|
|
652
|
+
const context = target.context == null ? null : await this._context(browser, target.context)
|
|
653
|
+
if (context) target.context = context.name
|
|
400
654
|
if (!browser.target_info[target.targetId]) {
|
|
401
655
|
target.targetId = (await socket.req({
|
|
402
656
|
method: 'Target.createTarget',
|
|
403
657
|
params: {
|
|
404
658
|
url: 'about:blank',
|
|
405
659
|
background: true,
|
|
660
|
+
browserContextId: context?.browserContextId,
|
|
661
|
+
...(reverse && target.incognito ? { incognito: true } : {}),
|
|
406
662
|
...target.create_params,
|
|
407
663
|
},
|
|
408
664
|
})).targetId
|
|
665
|
+
if (context) (browser.target_info[target.targetId] ??= {}).browserContextId = context.browserContextId
|
|
409
666
|
}
|
|
410
667
|
const info = browser.target_info[target.targetId] ??= {}
|
|
668
|
+
if (context && info.browserContextId !== context.browserContextId) {
|
|
669
|
+
throw new Error('Existing target belongs to a different browser context')
|
|
670
|
+
}
|
|
671
|
+
if (reverse && target.incognito && !info.incognito) {
|
|
672
|
+
const { targetInfo } = await socket.req({
|
|
673
|
+
method: 'Target.getTargetInfo',
|
|
674
|
+
params: { targetId: target.targetId },
|
|
675
|
+
})
|
|
676
|
+
Object.assign(info, targetInfo)
|
|
677
|
+
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
678
|
+
}
|
|
411
679
|
if (!info.sessionId) {
|
|
412
680
|
info.sessionId = (await socket.req({
|
|
413
681
|
method: 'Target.attachToTarget',
|
|
@@ -478,15 +746,25 @@ export class cdp extends EventTarget {
|
|
|
478
746
|
...this.options.preferences,
|
|
479
747
|
...spec.preferences,
|
|
480
748
|
},
|
|
749
|
+
local_state: {
|
|
750
|
+
...this.options.local_state,
|
|
751
|
+
...spec.local_state,
|
|
752
|
+
},
|
|
481
753
|
targets: Object.create(null),
|
|
754
|
+
contexts: Object.create(null),
|
|
755
|
+
}
|
|
756
|
+
if (target?.context != null && (record.reverse || record.cdp_ext)) {
|
|
757
|
+
throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
|
|
482
758
|
}
|
|
483
759
|
if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
|
|
484
760
|
record.connecting = this._connect(record).catch(error => {
|
|
485
761
|
record.socket?.close()
|
|
486
|
-
record.proc?.kill()
|
|
762
|
+
if (!record.cdp_ext) record.proc?.kill()
|
|
487
763
|
throw error
|
|
488
|
-
}).finally(() => {
|
|
764
|
+
}).finally(async () => {
|
|
489
765
|
record.connecting = null
|
|
766
|
+
await this._close_reverse_if_idle()
|
|
767
|
+
this._close_contexts_if_idle(record)
|
|
490
768
|
})
|
|
491
769
|
}
|
|
492
770
|
await record.connecting
|
|
@@ -503,10 +781,15 @@ export class cdp extends EventTarget {
|
|
|
503
781
|
if (!entry.sessionId) {
|
|
504
782
|
entry.connecting ??= this._target(record, entry).finally(() => {
|
|
505
783
|
entry.connecting = null
|
|
784
|
+
this._close_contexts_if_idle(record)
|
|
506
785
|
})
|
|
507
786
|
await entry.connecting
|
|
508
787
|
}
|
|
509
788
|
}
|
|
789
|
+
if (method === 'Target.disposeBrowserContext' && target == null) {
|
|
790
|
+
const context = Object.values(record.contexts).find(value => value.browserContextId === native.browserContextId)
|
|
791
|
+
if (context) return this._dispose_context(record, context, native)
|
|
792
|
+
}
|
|
510
793
|
const result = await record.socket.req({
|
|
511
794
|
method,
|
|
512
795
|
params: native,
|
|
@@ -516,23 +799,39 @@ export class cdp extends EventTarget {
|
|
|
516
799
|
if (method === 'Target.closeTarget' && result.success) {
|
|
517
800
|
this._detach(record, record.target_info[native.targetId]?.sessionId)
|
|
518
801
|
delete record.target_info[native.targetId]
|
|
802
|
+
await this._close_contexts_if_idle(record)
|
|
519
803
|
}
|
|
520
804
|
return result
|
|
521
805
|
}
|
|
522
806
|
|
|
523
807
|
async close() {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
808
|
+
try {
|
|
809
|
+
await Promise.all(Object.values(this.browsers).map(async browser => {
|
|
810
|
+
try {
|
|
811
|
+
await browser.connecting
|
|
812
|
+
browser.closing = true
|
|
813
|
+
await Promise.allSettled(Object.values(browser.targets).map(target => target.connecting))
|
|
814
|
+
await browser.context_checking
|
|
815
|
+
for (const context of Object.values(browser.contexts)) {
|
|
816
|
+
await context.closing
|
|
817
|
+
if (context.browserContextId && browser.socket?.readyState === WebSocket.OPEN) {
|
|
818
|
+
await this._dispose_context(browser, context)
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (browser.proc && !browser.cdp_ext && browser.socket?.readyState === WebSocket.OPEN) {
|
|
822
|
+
await browser.socket.req({ method: 'Browser.close' })
|
|
823
|
+
}
|
|
824
|
+
} finally {
|
|
825
|
+
browser.socket?.close()
|
|
826
|
+
browser.proc?.kill()
|
|
529
827
|
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
828
|
+
}))
|
|
829
|
+
} finally {
|
|
830
|
+
this.browsers = Object.create(null)
|
|
831
|
+
for (const socket of Object.values(this.reverse_sockets)) socket.close()
|
|
832
|
+
this.reverse_sockets = Object.create(null)
|
|
833
|
+
await this._close_reverse_if_idle()
|
|
834
|
+
}
|
|
536
835
|
}
|
|
537
836
|
}
|
|
538
837
|
|