@mefistofelix/cdp.js 0.1.2 → 0.3.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 +872 -28
- package/cdp.js +379 -36
- package/cdp_ext/cdp_ext.js +263 -0
- package/cdp_ext/manifest.json +76 -0
- package/package.json +5 -3
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 = typeof url === 'string' || url instanceof URL ? super(url) : 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,112 @@ 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 && !globalThis.Bun) throw new Error('The reverse WebSocket server requires Deno or Bun')
|
|
359
|
+
if (this.server) throw new Error('Reverse server is already listening')
|
|
360
|
+
const accept = (socket, origin) => {
|
|
361
|
+
socket.addEventListener('message', event => {
|
|
362
|
+
const id = JSON.parse(event.data).browser
|
|
363
|
+
if (id !== origin.slice('chrome-extension://'.length) ||
|
|
364
|
+
this.reverse_sockets[id]?.readyState < WebSocket.CLOSING) {
|
|
365
|
+
return socket.close(1008, 'Invalid or already connected extension ID')
|
|
366
|
+
}
|
|
367
|
+
this.reverse_sockets[id] = socket
|
|
368
|
+
socket.addEventListener('close', () => {
|
|
369
|
+
if (this.reverse_sockets[id] === socket) delete this.reverse_sockets[id]
|
|
370
|
+
this._close_reverse_if_idle()
|
|
371
|
+
})
|
|
372
|
+
util.emit(this, 'reverse_' + encodeURIComponent(id), socket)
|
|
373
|
+
}, { once: true })
|
|
374
|
+
}
|
|
375
|
+
const fetch = (request, server) => {
|
|
376
|
+
const origin = request.headers.get('origin')
|
|
377
|
+
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
378
|
+
!origin?.startsWith('chrome-extension://')) {
|
|
379
|
+
return new Response(null, { status: 403 })
|
|
380
|
+
}
|
|
381
|
+
if (globalThis.Bun) {
|
|
382
|
+
if (server.upgrade(request, {
|
|
383
|
+
data: {
|
|
384
|
+
origin,
|
|
385
|
+
socket: new EventTarget(),
|
|
386
|
+
},
|
|
387
|
+
})) return
|
|
388
|
+
return new Response(null, { status: 400 })
|
|
389
|
+
}
|
|
390
|
+
const { socket, response } = Deno.upgradeWebSocket(request)
|
|
391
|
+
accept(socket, origin)
|
|
392
|
+
return response
|
|
393
|
+
}
|
|
394
|
+
if (globalThis.Bun) {
|
|
395
|
+
const server = this.server = Bun.serve({
|
|
396
|
+
hostname,
|
|
397
|
+
port,
|
|
398
|
+
fetch,
|
|
399
|
+
websocket: {
|
|
400
|
+
open(socket) {
|
|
401
|
+
const events = socket.data.socket
|
|
402
|
+
events.send = socket.send.bind(socket)
|
|
403
|
+
events.close = socket.close.bind(socket)
|
|
404
|
+
Object.defineProperty(events, 'readyState', { get: () => socket.readyState })
|
|
405
|
+
accept(events, socket.data.origin)
|
|
406
|
+
},
|
|
407
|
+
message(socket, data) {
|
|
408
|
+
socket.data.socket.dispatchEvent(new MessageEvent('message', { data }))
|
|
409
|
+
},
|
|
410
|
+
close(socket, code, reason) {
|
|
411
|
+
socket.data.socket.dispatchEvent(Object.assign(new Event('close'), {
|
|
412
|
+
code,
|
|
413
|
+
reason,
|
|
414
|
+
}))
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
server.addr = {
|
|
419
|
+
hostname,
|
|
420
|
+
port: server.port,
|
|
421
|
+
}
|
|
422
|
+
const finished = Promise.withResolvers()
|
|
423
|
+
server.finished = finished.promise
|
|
424
|
+
server.shutdown = () => server.stop().then(finished.resolve)
|
|
425
|
+
} else this.server = Deno.serve({
|
|
426
|
+
hostname,
|
|
427
|
+
port,
|
|
428
|
+
}, fetch)
|
|
429
|
+
this.reverse_server = {
|
|
430
|
+
hostname,
|
|
431
|
+
port: this.server.addr.port,
|
|
432
|
+
}
|
|
433
|
+
return this.server
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_close_reverse_if_idle() {
|
|
437
|
+
if (this.server_closing) return this.server_closing
|
|
438
|
+
if (!this.server || Object.values(this.reverse_sockets).some(socket => socket.readyState < WebSocket.CLOSING)) return
|
|
439
|
+
for (const record of Object.values(this.browsers)) {
|
|
440
|
+
if (!record.reverse && !record.cdp_ext) continue
|
|
441
|
+
if (record.connecting) return
|
|
442
|
+
if (record.proc?.exitCode === null && record.proc.signalCode === null) return
|
|
443
|
+
}
|
|
444
|
+
return this.server_closing = this.server.shutdown().finally(() => {
|
|
445
|
+
this.server = null
|
|
446
|
+
this.server_closing = null
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
296
450
|
evaluate_xpath(params, fn, options) {
|
|
297
451
|
if (params.target == null) throw new Error('Custom method requires target')
|
|
298
452
|
const xpath = util.normalize_xpath(params.xpath)
|
|
@@ -326,7 +480,8 @@ export class cdp extends EventTarget {
|
|
|
326
480
|
browser: browser.name,
|
|
327
481
|
}
|
|
328
482
|
const sessionId = params.sessionId || message.sessionId
|
|
329
|
-
const targetId = params.targetInfo?.targetId || params.targetId || browser.session_targets[sessionId]
|
|
483
|
+
const targetId = params.targetInfo?.targetId || params.targetId || browser.session_targets[sessionId] ||
|
|
484
|
+
(params.tabId >= 0 ? String(params.tabId) : undefined)
|
|
330
485
|
const target = Object.values(browser.targets).find(value => value.targetId === targetId)
|
|
331
486
|
if (params.targetInfo) {
|
|
332
487
|
Object.assign(browser.target_info[targetId] ??= {}, params.targetInfo)
|
|
@@ -347,26 +502,152 @@ export class cdp extends EventTarget {
|
|
|
347
502
|
if (['Target.targetDestroyed', 'Target.targetCrashed'].includes(message.method)) {
|
|
348
503
|
this._detach(browser, info?.sessionId)
|
|
349
504
|
delete browser.target_info[targetId]
|
|
505
|
+
this._close_contexts_if_idle(browser)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async _context(browser, spec) {
|
|
510
|
+
if (typeof spec === 'string') spec = { name: spec }
|
|
511
|
+
const context = browser.contexts[spec.name] ??= {
|
|
512
|
+
...spec,
|
|
513
|
+
browserContextId: null,
|
|
514
|
+
}
|
|
515
|
+
await context.closing
|
|
516
|
+
if (!context.browserContextId) {
|
|
517
|
+
context.connecting ??= browser.socket.req({
|
|
518
|
+
method: 'Target.createBrowserContext',
|
|
519
|
+
params: {
|
|
520
|
+
proxyServer: context.proxy === false ? 'direct://' : context.proxy,
|
|
521
|
+
...context.create_params,
|
|
522
|
+
},
|
|
523
|
+
}).then(result => {
|
|
524
|
+
context.browserContextId = result.browserContextId
|
|
525
|
+
}).finally(() => {
|
|
526
|
+
context.connecting = null
|
|
527
|
+
})
|
|
528
|
+
await context.connecting
|
|
529
|
+
}
|
|
530
|
+
return context
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_dispose_context(browser, context, params = { browserContextId: context.browserContextId }) {
|
|
534
|
+
const id = context.browserContextId
|
|
535
|
+
return context.closing ??= browser.socket.req({
|
|
536
|
+
method: 'Target.disposeBrowserContext',
|
|
537
|
+
params,
|
|
538
|
+
}).then(result => {
|
|
539
|
+
context.browserContextId = null
|
|
540
|
+
for (const [targetId, info] of Object.entries(browser.target_info)) {
|
|
541
|
+
if (info.browserContextId !== id) continue
|
|
542
|
+
this._detach(browser, info.sessionId)
|
|
543
|
+
delete browser.target_info[targetId]
|
|
544
|
+
}
|
|
545
|
+
util.emit(this, 'context_closed', {
|
|
546
|
+
browser: browser.name,
|
|
547
|
+
context: context.name,
|
|
548
|
+
browserContextId: id,
|
|
549
|
+
})
|
|
550
|
+
return result
|
|
551
|
+
}).finally(() => {
|
|
552
|
+
context.closing = null
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_close_contexts_if_idle(browser) {
|
|
557
|
+
if (browser.context_checking) {
|
|
558
|
+
browser.context_check_pending = true
|
|
559
|
+
return browser.context_checking
|
|
350
560
|
}
|
|
561
|
+
if (browser.connecting || browser.closing || browser.socket?.readyState !== WebSocket.OPEN) return
|
|
562
|
+
const contexts = Object.values(browser.contexts).filter(context =>
|
|
563
|
+
context.browserContextId && !context.connecting && !context.closing &&
|
|
564
|
+
!Object.values(browser.target_info).some(info =>
|
|
565
|
+
info.type !== 'browser_ui' && info.browserContextId === context.browserContextId,
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
if (!contexts.length) return
|
|
569
|
+
const socket = browser.socket
|
|
570
|
+
return browser.context_checking = (async () => {
|
|
571
|
+
do {
|
|
572
|
+
browser.context_check_pending = false
|
|
573
|
+
const { targetInfos } = await socket.req({ method: 'Target.getTargets' })
|
|
574
|
+
if (browser.socket !== socket || browser.closing) return
|
|
575
|
+
for (const context of Object.values(browser.contexts)) {
|
|
576
|
+
if (!context.browserContextId || context.connecting || context.closing) continue
|
|
577
|
+
const belongs = info => info.type !== 'browser_ui' && info.browserContextId === context.browserContextId
|
|
578
|
+
if (targetInfos.some(belongs) || Object.values(browser.target_info).some(belongs)) continue
|
|
579
|
+
if (Object.values(browser.targets).some(target =>
|
|
580
|
+
target.connecting && (target.context?.name ?? target.context) === context.name,
|
|
581
|
+
)) continue
|
|
582
|
+
await this._dispose_context(browser, context)
|
|
583
|
+
}
|
|
584
|
+
} while (browser.context_check_pending)
|
|
585
|
+
})().catch(error => util.emit(this, 'error', {
|
|
586
|
+
browser: browser.name,
|
|
587
|
+
error,
|
|
588
|
+
})).finally(() => {
|
|
589
|
+
browser.context_checking = null
|
|
590
|
+
})
|
|
351
591
|
}
|
|
352
592
|
|
|
353
593
|
async _connect(record) {
|
|
594
|
+
if (record.reverse || record.cdp_ext) {
|
|
595
|
+
await this.server_closing
|
|
596
|
+
if (!this.server) this.listen_reverse()
|
|
597
|
+
}
|
|
354
598
|
let url = record.websocket_url
|
|
355
|
-
if (!url && (record.http_url || record.port)) {
|
|
599
|
+
if (!record.reverse && !record.cdp_ext && !url && (record.http_url || record.port)) {
|
|
356
600
|
const discovery = new URL(record.http_url || 'http://' + (record.host || '127.0.0.1') + ':' + record.port)
|
|
357
601
|
if (!discovery.pathname.endsWith('/json/version')) discovery.pathname = discovery.pathname.replace(/\/$/, '') + '/json/version'
|
|
358
602
|
const response = await fetch(discovery, { signal: AbortSignal.timeout(record.connect_timeout_ms ?? 15000) })
|
|
359
603
|
if (!response.ok) throw new Error('CDP discovery: HTTP ' + response.status)
|
|
360
604
|
url = (await response.json()).webSocketDebuggerUrl
|
|
361
605
|
}
|
|
362
|
-
if (!url) {
|
|
363
|
-
if (record.proc?.exitCode === null && record.proc.signalCode === null) url = record.socket
|
|
606
|
+
if (record.cdp_ext || (!record.reverse && !url)) {
|
|
607
|
+
if (record.proc?.exitCode === null && record.proc.signalCode === null) url = record.socket?.url
|
|
364
608
|
else {
|
|
609
|
+
const args = browser.build_args(record)
|
|
610
|
+
if (record.cdp_ext) {
|
|
611
|
+
record.extension_path = path.resolve(
|
|
612
|
+
args['user-data-dir'] || path.join(this.base_path, record.name),
|
|
613
|
+
args['profile-directory'] || 'Default',
|
|
614
|
+
'cdp_ext',
|
|
615
|
+
)
|
|
616
|
+
await fs.mkdir(record.extension_path, { recursive: true })
|
|
617
|
+
record.extension_path = await fs.realpath(record.extension_path)
|
|
618
|
+
record.extension_id = browser.extension_id(record.extension_path)
|
|
619
|
+
const manifest = JSON.parse(await fs.readFile(new URL('./cdp_ext/manifest.json', import.meta.url), 'utf8'))
|
|
620
|
+
let host = this.server.addr.hostname
|
|
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
|
|
624
|
+
await fs.writeFile(path.join(record.extension_path, 'manifest.json'), JSON.stringify(manifest))
|
|
625
|
+
await fs.copyFile(new URL('./cdp_ext/cdp_ext.js', import.meta.url), path.join(record.extension_path, 'cdp_ext.js'))
|
|
626
|
+
}
|
|
627
|
+
if (args['user-data-dir'] && record.update_preferences !== false) {
|
|
628
|
+
await browser.update_profile_preferences(path.join(args['user-data-dir'], args['profile-directory'] || 'Default', 'Preferences'), {
|
|
629
|
+
'translate.enabled': record.translations === true,
|
|
630
|
+
'signin.allowed': record.login === true,
|
|
631
|
+
'signin.allowed_on_next_startup': record.login === true,
|
|
632
|
+
...record.preferences,
|
|
633
|
+
})
|
|
634
|
+
if (Object.keys(record.local_state).length) {
|
|
635
|
+
await browser.update_profile_preferences(path.join(args['user-data-dir'], 'Local State'), record.local_state)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
365
638
|
const launched = await browser.launch(record)
|
|
366
639
|
record.proc = launched.proc
|
|
640
|
+
if (record.cdp_ext) record.proc.once('exit', () => this._close_reverse_if_idle())
|
|
367
641
|
url = launched.url
|
|
368
642
|
}
|
|
369
643
|
}
|
|
644
|
+
if (record.reverse || record.cdp_ext) {
|
|
645
|
+
const id = record.extension_id ?? record.name
|
|
646
|
+
url = this.reverse_sockets[id]
|
|
647
|
+
if (url?.readyState !== WebSocket.OPEN) {
|
|
648
|
+
url = (await util.on_first(this, 'reverse_' + encodeURIComponent(id), record.connect_timeout_ms ?? 15000)).args[0].detail
|
|
649
|
+
}
|
|
650
|
+
}
|
|
370
651
|
const socket = record.socket = new jsrpc(url, record)
|
|
371
652
|
record.target_info = {}
|
|
372
653
|
record.session_targets = {}
|
|
@@ -380,7 +661,9 @@ export class cdp extends EventTarget {
|
|
|
380
661
|
code: event.code,
|
|
381
662
|
})
|
|
382
663
|
})
|
|
383
|
-
|
|
664
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
665
|
+
await util.on_first(socket, 'open error close', record.connect_timeout_ms ?? 15000)
|
|
666
|
+
}
|
|
384
667
|
await socket.req({
|
|
385
668
|
method: 'Target.setAutoAttach',
|
|
386
669
|
params: {
|
|
@@ -393,21 +676,50 @@ export class cdp extends EventTarget {
|
|
|
393
676
|
method: 'Target.setDiscoverTargets',
|
|
394
677
|
params: { discover: true },
|
|
395
678
|
})
|
|
679
|
+
if (Object.values(record.contexts).some(context => context.browserContextId)) {
|
|
680
|
+
const { browserContextIds } = await socket.req({ method: 'Target.getBrowserContexts' })
|
|
681
|
+
for (const context of Object.values(record.contexts)) {
|
|
682
|
+
if (!browserContextIds.includes(context.browserContextId)) context.browserContextId = null
|
|
683
|
+
}
|
|
684
|
+
}
|
|
396
685
|
}
|
|
397
686
|
|
|
398
687
|
async _target(browser, target) {
|
|
399
688
|
const socket = browser.socket
|
|
689
|
+
const reverse = browser.reverse || browser.cdp_ext
|
|
690
|
+
if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
|
|
691
|
+
target.context = 'incognito'
|
|
692
|
+
}
|
|
693
|
+
if (target.context != null && target.create_params?.browserContextId != null) {
|
|
694
|
+
throw new Error('Use target.context or create_params.browserContextId, not both')
|
|
695
|
+
}
|
|
696
|
+
const context = target.context == null ? null : await this._context(browser, target.context)
|
|
697
|
+
if (context) target.context = context.name
|
|
400
698
|
if (!browser.target_info[target.targetId]) {
|
|
401
699
|
target.targetId = (await socket.req({
|
|
402
700
|
method: 'Target.createTarget',
|
|
403
701
|
params: {
|
|
404
702
|
url: 'about:blank',
|
|
405
703
|
background: true,
|
|
704
|
+
browserContextId: context?.browserContextId,
|
|
705
|
+
...(reverse && target.incognito ? { incognito: true } : {}),
|
|
406
706
|
...target.create_params,
|
|
407
707
|
},
|
|
408
708
|
})).targetId
|
|
709
|
+
if (context) (browser.target_info[target.targetId] ??= {}).browserContextId = context.browserContextId
|
|
409
710
|
}
|
|
410
711
|
const info = browser.target_info[target.targetId] ??= {}
|
|
712
|
+
if (context && info.browserContextId !== context.browserContextId) {
|
|
713
|
+
throw new Error('Existing target belongs to a different browser context')
|
|
714
|
+
}
|
|
715
|
+
if (reverse && target.incognito && !info.incognito) {
|
|
716
|
+
const { targetInfo } = await socket.req({
|
|
717
|
+
method: 'Target.getTargetInfo',
|
|
718
|
+
params: { targetId: target.targetId },
|
|
719
|
+
})
|
|
720
|
+
Object.assign(info, targetInfo)
|
|
721
|
+
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
722
|
+
}
|
|
411
723
|
if (!info.sessionId) {
|
|
412
724
|
info.sessionId = (await socket.req({
|
|
413
725
|
method: 'Target.attachToTarget',
|
|
@@ -478,15 +790,25 @@ export class cdp extends EventTarget {
|
|
|
478
790
|
...this.options.preferences,
|
|
479
791
|
...spec.preferences,
|
|
480
792
|
},
|
|
793
|
+
local_state: {
|
|
794
|
+
...this.options.local_state,
|
|
795
|
+
...spec.local_state,
|
|
796
|
+
},
|
|
481
797
|
targets: Object.create(null),
|
|
798
|
+
contexts: Object.create(null),
|
|
799
|
+
}
|
|
800
|
+
if (target?.context != null && (record.reverse || record.cdp_ext)) {
|
|
801
|
+
throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
|
|
482
802
|
}
|
|
483
803
|
if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
|
|
484
804
|
record.connecting = this._connect(record).catch(error => {
|
|
485
805
|
record.socket?.close()
|
|
486
|
-
record.proc?.kill()
|
|
806
|
+
if (!record.cdp_ext) record.proc?.kill()
|
|
487
807
|
throw error
|
|
488
|
-
}).finally(() => {
|
|
808
|
+
}).finally(async () => {
|
|
489
809
|
record.connecting = null
|
|
810
|
+
await this._close_reverse_if_idle()
|
|
811
|
+
this._close_contexts_if_idle(record)
|
|
490
812
|
})
|
|
491
813
|
}
|
|
492
814
|
await record.connecting
|
|
@@ -503,10 +825,15 @@ export class cdp extends EventTarget {
|
|
|
503
825
|
if (!entry.sessionId) {
|
|
504
826
|
entry.connecting ??= this._target(record, entry).finally(() => {
|
|
505
827
|
entry.connecting = null
|
|
828
|
+
this._close_contexts_if_idle(record)
|
|
506
829
|
})
|
|
507
830
|
await entry.connecting
|
|
508
831
|
}
|
|
509
832
|
}
|
|
833
|
+
if (method === 'Target.disposeBrowserContext' && target == null) {
|
|
834
|
+
const context = Object.values(record.contexts).find(value => value.browserContextId === native.browserContextId)
|
|
835
|
+
if (context) return this._dispose_context(record, context, native)
|
|
836
|
+
}
|
|
510
837
|
const result = await record.socket.req({
|
|
511
838
|
method,
|
|
512
839
|
params: native,
|
|
@@ -516,23 +843,39 @@ export class cdp extends EventTarget {
|
|
|
516
843
|
if (method === 'Target.closeTarget' && result.success) {
|
|
517
844
|
this._detach(record, record.target_info[native.targetId]?.sessionId)
|
|
518
845
|
delete record.target_info[native.targetId]
|
|
846
|
+
await this._close_contexts_if_idle(record)
|
|
519
847
|
}
|
|
520
848
|
return result
|
|
521
849
|
}
|
|
522
850
|
|
|
523
851
|
async close() {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
852
|
+
try {
|
|
853
|
+
await Promise.all(Object.values(this.browsers).map(async browser => {
|
|
854
|
+
try {
|
|
855
|
+
await browser.connecting
|
|
856
|
+
browser.closing = true
|
|
857
|
+
await Promise.allSettled(Object.values(browser.targets).map(target => target.connecting))
|
|
858
|
+
await browser.context_checking
|
|
859
|
+
for (const context of Object.values(browser.contexts)) {
|
|
860
|
+
await context.closing
|
|
861
|
+
if (context.browserContextId && browser.socket?.readyState === WebSocket.OPEN) {
|
|
862
|
+
await this._dispose_context(browser, context)
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (browser.proc && !browser.cdp_ext && browser.socket?.readyState === WebSocket.OPEN) {
|
|
866
|
+
await browser.socket.req({ method: 'Browser.close' })
|
|
867
|
+
}
|
|
868
|
+
} finally {
|
|
869
|
+
browser.socket?.close()
|
|
870
|
+
browser.proc?.kill()
|
|
529
871
|
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
872
|
+
}))
|
|
873
|
+
} finally {
|
|
874
|
+
this.browsers = Object.create(null)
|
|
875
|
+
for (const socket of Object.values(this.reverse_sockets)) socket.close()
|
|
876
|
+
this.reverse_sockets = Object.create(null)
|
|
877
|
+
await this._close_reverse_if_idle()
|
|
878
|
+
}
|
|
536
879
|
}
|
|
537
880
|
}
|
|
538
881
|
|