@mefistofelix/cdp.js 0.1.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 +835 -0
- package/cdp.js +544 -0
- package/package.json +32 -0
package/cdp.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import * as child_process from 'node:child_process'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import { once, setMaxListeners } from 'node:events'
|
|
6
|
+
|
|
7
|
+
export class util {
|
|
8
|
+
static emit(target, name, detail) {
|
|
9
|
+
return target.dispatchEvent(new CustomEvent(name, { detail }))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static async on_first(target, names, timeout_ms = 30000) {
|
|
13
|
+
const controller = new AbortController()
|
|
14
|
+
const timer = setTimeout(() => controller.abort(new Error('Timed out waiting for ' + names)), timeout_ms)
|
|
15
|
+
try {
|
|
16
|
+
return await Promise.race(names.split(' ').map(type =>
|
|
17
|
+
once(target, type, { signal: controller.signal }).then(args => ({
|
|
18
|
+
type,
|
|
19
|
+
args,
|
|
20
|
+
})),
|
|
21
|
+
))
|
|
22
|
+
} finally {
|
|
23
|
+
clearTimeout(timer)
|
|
24
|
+
controller.abort()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static normalize_xpath(xpath) {
|
|
29
|
+
return xpath
|
|
30
|
+
.replace(/ends-with\(([^,]+),([^\)]+)\)/g, '(substring($1, string-length($1)-string-length($2)+1) = $2)')
|
|
31
|
+
.replace(/icontains\(([^,]+),([^\)]+)\)/g,
|
|
32
|
+
"contains(translate($1,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),translate($2,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'))")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static args_to_strings(args) {
|
|
36
|
+
const result = []
|
|
37
|
+
for (const [name, value] of Object.entries(args)) {
|
|
38
|
+
if (value === false || value == null) continue
|
|
39
|
+
const text = Array.isArray(value) ? value.join(',') : value
|
|
40
|
+
result.push('--' + name + (value === true ? '' : '=' + text))
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class jsrpc extends WebSocket {
|
|
47
|
+
constructor(url, options = {}) {
|
|
48
|
+
super(url)
|
|
49
|
+
setMaxListeners(0, this)
|
|
50
|
+
this.id = 0
|
|
51
|
+
this.pending = {}
|
|
52
|
+
this.timeout_ms = options.request_timeout_ms ?? 30000
|
|
53
|
+
this.addEventListener('message', event => {
|
|
54
|
+
const message = JSON.parse(event.data)
|
|
55
|
+
util.emit(this, message.id === undefined ? 'notify' : 'rpc_' + message.id, message)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async req(request, timeout_ms = this.timeout_ms) {
|
|
60
|
+
if (this.readyState !== WebSocket.OPEN) throw new Error('WebSocket is not open')
|
|
61
|
+
const id = ++this.id
|
|
62
|
+
const data = JSON.stringify({
|
|
63
|
+
...request,
|
|
64
|
+
id,
|
|
65
|
+
})
|
|
66
|
+
this.pending[id] = util.on_first(this, 'rpc_' + id + ' close error', timeout_ms)
|
|
67
|
+
try {
|
|
68
|
+
this.send(data)
|
|
69
|
+
const event = await this.pending[id]
|
|
70
|
+
if (event.type !== 'rpc_' + id) throw new Error('WebSocket connection closed')
|
|
71
|
+
const response = event.args[0].detail
|
|
72
|
+
if (response.error) {
|
|
73
|
+
const error = new Error(response.error.message, {
|
|
74
|
+
cause: {
|
|
75
|
+
req: request,
|
|
76
|
+
ret: response,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
error.cdp = response
|
|
80
|
+
throw error
|
|
81
|
+
}
|
|
82
|
+
return response.result
|
|
83
|
+
} finally {
|
|
84
|
+
delete this.pending[id]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
notify(request) {
|
|
89
|
+
this.send(JSON.stringify(request))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class browser {
|
|
94
|
+
static find_executable_path() {
|
|
95
|
+
if (process.env.CDP_BROWSER) return process.env.CDP_BROWSER
|
|
96
|
+
const candidates = process.platform === 'win32'
|
|
97
|
+
? [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA]
|
|
98
|
+
.filter(Boolean).flatMap(root => [
|
|
99
|
+
path.join(root, 'Google/Chrome/Application/chrome.exe'),
|
|
100
|
+
path.join(root, 'Microsoft/Edge/Application/msedge.exe'),
|
|
101
|
+
])
|
|
102
|
+
: [
|
|
103
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
104
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
105
|
+
'/usr/bin/google-chrome',
|
|
106
|
+
'/usr/bin/google-chrome-stable',
|
|
107
|
+
'/usr/bin/chromium',
|
|
108
|
+
'/usr/bin/chromium-browser',
|
|
109
|
+
'/usr/bin/microsoft-edge',
|
|
110
|
+
]
|
|
111
|
+
const executable = candidates.find(existsSync)
|
|
112
|
+
if (!executable) throw new Error('Set executable_path or CDP_BROWSER to a Chromium executable')
|
|
113
|
+
return executable
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static build_args(options = {}) {
|
|
117
|
+
const args = {
|
|
118
|
+
'enable-automation': true,
|
|
119
|
+
'mute-audio': true,
|
|
120
|
+
'disable-blink-features': 'AutomationControlled',
|
|
121
|
+
'hide-crash-restore-bubble': true,
|
|
122
|
+
'disable-field-trial-config': true,
|
|
123
|
+
'disable-background-networking': true,
|
|
124
|
+
'enable-features': ['NetworkService', 'NetworkServiceInProcess'],
|
|
125
|
+
'disable-background-timer-throttling': true,
|
|
126
|
+
'disable-backgrounding-occluded-windows': true,
|
|
127
|
+
'disable-renderer-backgrounding': true,
|
|
128
|
+
'disable-back-forward-cache': true,
|
|
129
|
+
'disable-breakpad': true,
|
|
130
|
+
'disable-client-side-phishing-detection': true,
|
|
131
|
+
'disable-component-update': true,
|
|
132
|
+
'no-default-browser-check': true,
|
|
133
|
+
'disable-default-apps': true,
|
|
134
|
+
'disable-dev-shm-usage': true,
|
|
135
|
+
'allow-pre-commit-input': true,
|
|
136
|
+
'disable-hang-monitor': true,
|
|
137
|
+
'disable-ipc-flooding-protection': true,
|
|
138
|
+
'disable-popup-blocking': true,
|
|
139
|
+
'disable-prompt-on-repost': true,
|
|
140
|
+
'force-color-profile': 'srgb',
|
|
141
|
+
'metrics-recording-only': true,
|
|
142
|
+
'no-first-run': true,
|
|
143
|
+
'password-store': 'basic',
|
|
144
|
+
'use-mock-keychain': true,
|
|
145
|
+
'no-service-autorun': true,
|
|
146
|
+
'export-tagged-pdf': true,
|
|
147
|
+
'disable-search-engine-choice-screen': true,
|
|
148
|
+
'disable-features': [
|
|
149
|
+
'InfiniteSessionRestore',
|
|
150
|
+
'ImprovedCookieControls',
|
|
151
|
+
'LazyFrameLoading',
|
|
152
|
+
'GlobalMediaControls',
|
|
153
|
+
'DestroyProfileOnBrowserClose',
|
|
154
|
+
'MediaRouter',
|
|
155
|
+
'DialMediaRouteProvider',
|
|
156
|
+
'AcceptCHFrame',
|
|
157
|
+
'AutoExpandDetailsElement',
|
|
158
|
+
'CertificateTransparencyComponentUpdater',
|
|
159
|
+
'AvoidUnnecessaryBeforeUnloadCheckSync',
|
|
160
|
+
'HttpsUpgrades',
|
|
161
|
+
'PaintHolding',
|
|
162
|
+
],
|
|
163
|
+
'user-data-dir': options.user_data_dir,
|
|
164
|
+
'remote-debugging-port': 0,
|
|
165
|
+
}
|
|
166
|
+
if (options.headless) {
|
|
167
|
+
args.headless = 'new'
|
|
168
|
+
args['window-size'] = '1440,900'
|
|
169
|
+
}
|
|
170
|
+
if (options.extensions !== true) {
|
|
171
|
+
args['disable-extensions'] = true
|
|
172
|
+
args['disable-component-extensions-with-background-pages'] = true
|
|
173
|
+
}
|
|
174
|
+
if (options.images === false) args['blink-settings'] = 'imagesEnabled=false'
|
|
175
|
+
if (options.translations !== true) {
|
|
176
|
+
args['disable-features'].push('Translate', 'TranslateToast', 'EnableTranslatePdf')
|
|
177
|
+
}
|
|
178
|
+
if (options.login !== true) {
|
|
179
|
+
args['disable-signin-promo-on-avatar-pill-for-testing'] = true
|
|
180
|
+
args['disable-sync'] = true
|
|
181
|
+
args['disable-features'].push('DiceWebSigninInterception', 'SigninPromoOnAvatarPill')
|
|
182
|
+
}
|
|
183
|
+
return Object.assign(args, options.args)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static async update_profile_preferences(filename, preferences) {
|
|
187
|
+
await fs.mkdir(path.dirname(filename), { recursive: true })
|
|
188
|
+
const content = await fs.readFile(filename, 'utf8').catch(error => {
|
|
189
|
+
if (error.code !== 'ENOENT') throw error
|
|
190
|
+
return '{}'
|
|
191
|
+
})
|
|
192
|
+
const prefs = JSON.parse(content)
|
|
193
|
+
for (const [key, value] of Object.entries(preferences)) {
|
|
194
|
+
const parts = key.split('.')
|
|
195
|
+
let object = prefs
|
|
196
|
+
for (const part of parts.slice(0, -1)) object = object[part] ??= {}
|
|
197
|
+
object[parts.at(-1)] = value
|
|
198
|
+
}
|
|
199
|
+
await fs.writeFile(filename, JSON.stringify(prefs))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static async launch(options) {
|
|
203
|
+
const args = browser.build_args(options)
|
|
204
|
+
const filename = path.join(args['user-data-dir'], args['profile-directory'] || 'Default', 'Preferences')
|
|
205
|
+
await browser.update_profile_preferences(filename, {
|
|
206
|
+
'translate.enabled': options.translations === true,
|
|
207
|
+
'signin.allowed': options.login === true,
|
|
208
|
+
'signin.allowed_on_next_startup': options.login === true,
|
|
209
|
+
...options.preferences,
|
|
210
|
+
})
|
|
211
|
+
const proc = child_process.spawn(options.executable_path || browser.find_executable_path(), [...util.args_to_strings(args), 'about:blank'], {
|
|
212
|
+
windowsHide: true,
|
|
213
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
214
|
+
})
|
|
215
|
+
let diagnostic = ''
|
|
216
|
+
const ready = util.on_first(proc, 'ready exit', options.connect_timeout_ms ?? 15000)
|
|
217
|
+
const data = chunk => {
|
|
218
|
+
diagnostic = (diagnostic + chunk).slice(-8192)
|
|
219
|
+
const match = diagnostic.match(/DevTools listening on (ws:\/\/\S+)/)
|
|
220
|
+
if (match) proc.emit('ready', match[1])
|
|
221
|
+
}
|
|
222
|
+
proc.stderr.on('data', data)
|
|
223
|
+
try {
|
|
224
|
+
const event = await ready
|
|
225
|
+
if (event.type === 'exit') throw new Error('Browser exited: ' + diagnostic)
|
|
226
|
+
return {
|
|
227
|
+
proc,
|
|
228
|
+
url: event.args[0],
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
proc.kill()
|
|
232
|
+
throw error
|
|
233
|
+
} finally {
|
|
234
|
+
proc.stderr.removeListener('data', data)
|
|
235
|
+
proc.stderr.resume()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const custom_methods = {
|
|
241
|
+
'_.click'(params) {
|
|
242
|
+
const options = {
|
|
243
|
+
attempts: params.attempts ?? 5,
|
|
244
|
+
interval_ms: params.interval_ms ?? 300,
|
|
245
|
+
}
|
|
246
|
+
return this.evaluate_xpath(params, async (xpath, options) => {
|
|
247
|
+
for (let i = 0; i < options.attempts; i++) {
|
|
248
|
+
const node = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
|
249
|
+
if (node) {
|
|
250
|
+
if (typeof node.click !== 'function') return false
|
|
251
|
+
node.click()
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
if (i + 1 < options.attempts) await new Promise(resolve => setTimeout(resolve, options.interval_ms))
|
|
255
|
+
}
|
|
256
|
+
return false
|
|
257
|
+
}, options)
|
|
258
|
+
},
|
|
259
|
+
'_.find'(params) {
|
|
260
|
+
const limit = params.limit ?? 20
|
|
261
|
+
return this.evaluate_xpath(params, (xpath, limit) => {
|
|
262
|
+
const nodes = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
|
263
|
+
const items = []
|
|
264
|
+
for (let i = 0; i < Math.min(nodes.snapshotLength, limit); i++) {
|
|
265
|
+
const node = nodes.snapshotItem(i)
|
|
266
|
+
const text = String(node.innerText ?? node.textContent ?? '').trim().slice(0, 2000)
|
|
267
|
+
items.push({
|
|
268
|
+
node_type: node.nodeType,
|
|
269
|
+
node_name: node.nodeName,
|
|
270
|
+
text,
|
|
271
|
+
tag: node.tagName?.toLowerCase(),
|
|
272
|
+
id: node.id,
|
|
273
|
+
class: node.className == null ? undefined : String(node.className),
|
|
274
|
+
href: node.href,
|
|
275
|
+
value: node.value,
|
|
276
|
+
rect: node.getBoundingClientRect?.().toJSON(),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
count: nodes.snapshotLength,
|
|
281
|
+
items,
|
|
282
|
+
}
|
|
283
|
+
}, limit)
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class cdp extends EventTarget {
|
|
288
|
+
constructor({ base_path = '.cdp', ...options } = {}) {
|
|
289
|
+
super()
|
|
290
|
+
this.base_path = path.resolve(base_path)
|
|
291
|
+
this.options = options
|
|
292
|
+
this.browsers = Object.create(null)
|
|
293
|
+
this.custom_methods = { ...custom_methods }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
evaluate_xpath(params, fn, options) {
|
|
297
|
+
if (params.target == null) throw new Error('Custom method requires target')
|
|
298
|
+
const xpath = util.normalize_xpath(params.xpath)
|
|
299
|
+
return this.call({
|
|
300
|
+
method: 'Runtime.evaluate',
|
|
301
|
+
params: {
|
|
302
|
+
browser: params.browser,
|
|
303
|
+
target: params.target,
|
|
304
|
+
expression: '(' + fn + ')(...' + JSON.stringify([xpath, options]) + ')',
|
|
305
|
+
returnByValue: true,
|
|
306
|
+
awaitPromise: true,
|
|
307
|
+
silent: true,
|
|
308
|
+
userGesture: true,
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_detach(browser, sessionId) {
|
|
314
|
+
const targetId = browser.session_targets[sessionId]
|
|
315
|
+
delete browser.session_targets[sessionId]
|
|
316
|
+
const info = browser.target_info[targetId]
|
|
317
|
+
if (info && info.sessionId === sessionId) delete info.sessionId
|
|
318
|
+
for (const target of Object.values(browser.targets)) {
|
|
319
|
+
if (target.sessionId === sessionId) target.sessionId = null
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
_notify(browser, message) {
|
|
324
|
+
const params = {
|
|
325
|
+
...message.params,
|
|
326
|
+
browser: browser.name,
|
|
327
|
+
}
|
|
328
|
+
const sessionId = params.sessionId || message.sessionId
|
|
329
|
+
const targetId = params.targetInfo?.targetId || params.targetId || browser.session_targets[sessionId]
|
|
330
|
+
const target = Object.values(browser.targets).find(value => value.targetId === targetId)
|
|
331
|
+
if (params.targetInfo) {
|
|
332
|
+
Object.assign(browser.target_info[targetId] ??= {}, params.targetInfo)
|
|
333
|
+
}
|
|
334
|
+
const info = browser.target_info[targetId]
|
|
335
|
+
if (message.method === 'Target.attachedToTarget') {
|
|
336
|
+
this._detach(browser, info?.sessionId)
|
|
337
|
+
info.sessionId = sessionId
|
|
338
|
+
browser.session_targets[sessionId] = targetId
|
|
339
|
+
}
|
|
340
|
+
if (target) params.target = target.name
|
|
341
|
+
util.emit(this, 'notify', {
|
|
342
|
+
...message,
|
|
343
|
+
params,
|
|
344
|
+
})
|
|
345
|
+
util.emit(this, message.method, params)
|
|
346
|
+
if (message.method === 'Target.detachedFromTarget') this._detach(browser, sessionId)
|
|
347
|
+
if (['Target.targetDestroyed', 'Target.targetCrashed'].includes(message.method)) {
|
|
348
|
+
this._detach(browser, info?.sessionId)
|
|
349
|
+
delete browser.target_info[targetId]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async _connect(record) {
|
|
354
|
+
let url = record.websocket_url
|
|
355
|
+
if (!url && (record.http_url || record.port)) {
|
|
356
|
+
const discovery = new URL(record.http_url || 'http://' + (record.host || '127.0.0.1') + ':' + record.port)
|
|
357
|
+
if (!discovery.pathname.endsWith('/json/version')) discovery.pathname = discovery.pathname.replace(/\/$/, '') + '/json/version'
|
|
358
|
+
const response = await fetch(discovery, { signal: AbortSignal.timeout(record.connect_timeout_ms ?? 15000) })
|
|
359
|
+
if (!response.ok) throw new Error('CDP discovery: HTTP ' + response.status)
|
|
360
|
+
url = (await response.json()).webSocketDebuggerUrl
|
|
361
|
+
}
|
|
362
|
+
if (!url) {
|
|
363
|
+
if (record.proc?.exitCode === null && record.proc.signalCode === null) url = record.socket.url
|
|
364
|
+
else {
|
|
365
|
+
const launched = await browser.launch(record)
|
|
366
|
+
record.proc = launched.proc
|
|
367
|
+
url = launched.url
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const socket = record.socket = new jsrpc(url, record)
|
|
371
|
+
record.target_info = {}
|
|
372
|
+
record.session_targets = {}
|
|
373
|
+
for (const target of Object.values(record.targets)) target.sessionId = null
|
|
374
|
+
socket.addEventListener('notify', event => this._notify(record, event.detail))
|
|
375
|
+
socket.addEventListener('close', event => {
|
|
376
|
+
if (record.socket !== socket) return
|
|
377
|
+
for (const sessionId of Object.keys(record.session_targets)) this._detach(record, sessionId)
|
|
378
|
+
util.emit(this, 'close', {
|
|
379
|
+
browser: record.name,
|
|
380
|
+
code: event.code,
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
await util.on_first(socket, 'open error close', record.connect_timeout_ms ?? 15000)
|
|
384
|
+
await socket.req({
|
|
385
|
+
method: 'Target.setAutoAttach',
|
|
386
|
+
params: {
|
|
387
|
+
autoAttach: true,
|
|
388
|
+
flatten: true,
|
|
389
|
+
waitForDebuggerOnStart: false,
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
await socket.req({
|
|
393
|
+
method: 'Target.setDiscoverTargets',
|
|
394
|
+
params: { discover: true },
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async _target(browser, target) {
|
|
399
|
+
const socket = browser.socket
|
|
400
|
+
if (!browser.target_info[target.targetId]) {
|
|
401
|
+
target.targetId = (await socket.req({
|
|
402
|
+
method: 'Target.createTarget',
|
|
403
|
+
params: {
|
|
404
|
+
url: 'about:blank',
|
|
405
|
+
background: true,
|
|
406
|
+
...target.create_params,
|
|
407
|
+
},
|
|
408
|
+
})).targetId
|
|
409
|
+
}
|
|
410
|
+
const info = browser.target_info[target.targetId] ??= {}
|
|
411
|
+
if (!info.sessionId) {
|
|
412
|
+
info.sessionId = (await socket.req({
|
|
413
|
+
method: 'Target.attachToTarget',
|
|
414
|
+
params: {
|
|
415
|
+
targetId: target.targetId,
|
|
416
|
+
flatten: true,
|
|
417
|
+
},
|
|
418
|
+
})).sessionId
|
|
419
|
+
browser.session_targets[info.sessionId] = target.targetId
|
|
420
|
+
}
|
|
421
|
+
const sessionId = info.sessionId
|
|
422
|
+
const steps = []
|
|
423
|
+
if (target.initialize !== false) {
|
|
424
|
+
for (const [option, domain] of Object.entries({
|
|
425
|
+
runtime: 'Runtime',
|
|
426
|
+
page: 'Page',
|
|
427
|
+
network: 'Network',
|
|
428
|
+
service_worker: 'ServiceWorker',
|
|
429
|
+
})) {
|
|
430
|
+
if (target[option] !== false) steps.push([domain + '.enable'])
|
|
431
|
+
}
|
|
432
|
+
if (target.focus_emulation !== false) steps.push(['Emulation.setFocusEmulationEnabled', { enabled: true }])
|
|
433
|
+
if (target.binding) steps.push(['Runtime.addBinding', { name: target.binding }])
|
|
434
|
+
if (target.background_service !== false) steps.push(
|
|
435
|
+
['BackgroundService.clearEvents', { service: 'pushMessaging' }],
|
|
436
|
+
['BackgroundService.startObserving', { service: 'pushMessaging' }],
|
|
437
|
+
['BackgroundService.setRecording', {
|
|
438
|
+
service: 'pushMessaging',
|
|
439
|
+
shouldRecord: true,
|
|
440
|
+
}],
|
|
441
|
+
)
|
|
442
|
+
if ((target.runtime ?? 'bootstrap') === 'bootstrap') steps.push(['Runtime.disable'])
|
|
443
|
+
}
|
|
444
|
+
steps.push(['Runtime.runIfWaitingForDebugger'])
|
|
445
|
+
target.setup_errors = []
|
|
446
|
+
for (const [method, params] of steps) {
|
|
447
|
+
try {
|
|
448
|
+
await socket.req({
|
|
449
|
+
method,
|
|
450
|
+
params,
|
|
451
|
+
sessionId,
|
|
452
|
+
})
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (!error.cdp) throw error
|
|
455
|
+
target.setup_errors.push(error.message)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
target.sessionId = sessionId
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async call({ method, params = {} }) {
|
|
462
|
+
if (method.startsWith('_.')) {
|
|
463
|
+
const handler = this.custom_methods[method]
|
|
464
|
+
if (!handler) throw new Error('Unknown CDP extension: ' + method)
|
|
465
|
+
return handler.call(this, params)
|
|
466
|
+
}
|
|
467
|
+
const { browser = 'main', target, ...native } = params
|
|
468
|
+
const spec = typeof browser === 'string' ? { name: browser } : browser
|
|
469
|
+
const record = this.browsers[spec.name] ??= {
|
|
470
|
+
user_data_dir: path.join(this.base_path, spec.name),
|
|
471
|
+
...this.options,
|
|
472
|
+
...spec,
|
|
473
|
+
args: {
|
|
474
|
+
...this.options.args,
|
|
475
|
+
...spec.args,
|
|
476
|
+
},
|
|
477
|
+
preferences: {
|
|
478
|
+
...this.options.preferences,
|
|
479
|
+
...spec.preferences,
|
|
480
|
+
},
|
|
481
|
+
targets: Object.create(null),
|
|
482
|
+
}
|
|
483
|
+
if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
|
|
484
|
+
record.connecting = this._connect(record).catch(error => {
|
|
485
|
+
record.socket?.close()
|
|
486
|
+
record.proc?.kill()
|
|
487
|
+
throw error
|
|
488
|
+
}).finally(() => {
|
|
489
|
+
record.connecting = null
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
await record.connecting
|
|
493
|
+
let entry
|
|
494
|
+
if (target != null) {
|
|
495
|
+
const spec = typeof target === 'string' ? { name: target } : target
|
|
496
|
+
entry = record.targets[spec.name] ??= {
|
|
497
|
+
targetId: null,
|
|
498
|
+
sessionId: null,
|
|
499
|
+
binding: '_send_to_cdp',
|
|
500
|
+
...spec,
|
|
501
|
+
}
|
|
502
|
+
if (entry.binding === true) entry.binding = '_send_to_cdp'
|
|
503
|
+
if (!entry.sessionId) {
|
|
504
|
+
entry.connecting ??= this._target(record, entry).finally(() => {
|
|
505
|
+
entry.connecting = null
|
|
506
|
+
})
|
|
507
|
+
await entry.connecting
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const result = await record.socket.req({
|
|
511
|
+
method,
|
|
512
|
+
params: native,
|
|
513
|
+
sessionId: entry?.sessionId,
|
|
514
|
+
})
|
|
515
|
+
if (method === 'Target.detachFromTarget') this._detach(record, native.sessionId)
|
|
516
|
+
if (method === 'Target.closeTarget' && result.success) {
|
|
517
|
+
this._detach(record, record.target_info[native.targetId]?.sessionId)
|
|
518
|
+
delete record.target_info[native.targetId]
|
|
519
|
+
}
|
|
520
|
+
return result
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async close() {
|
|
524
|
+
await Promise.all(Object.values(this.browsers).map(async browser => {
|
|
525
|
+
try {
|
|
526
|
+
await browser.connecting
|
|
527
|
+
if (browser.proc && browser.socket?.readyState === WebSocket.OPEN) {
|
|
528
|
+
await browser.socket.req({ method: 'Browser.close' })
|
|
529
|
+
}
|
|
530
|
+
} finally {
|
|
531
|
+
browser.socket?.close()
|
|
532
|
+
browser.proc?.kill()
|
|
533
|
+
}
|
|
534
|
+
}))
|
|
535
|
+
this.browsers = Object.create(null)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export default {
|
|
540
|
+
util,
|
|
541
|
+
jsrpc,
|
|
542
|
+
browser,
|
|
543
|
+
cdp,
|
|
544
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mefistofelix/cdp.js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Low-level Chromium DevTools Protocol client for Node.js, with no runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./cdp.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"cdp.js"
|
|
9
|
+
],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.19.0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cdp",
|
|
15
|
+
"chrome",
|
|
16
|
+
"chromium",
|
|
17
|
+
"devtools",
|
|
18
|
+
"browser",
|
|
19
|
+
"automation"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/mefistofelix/cdp.js.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/mefistofelix/cdp.js#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/mefistofelix/cdp.js/issues"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|