94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
// Minimal CDP driver: node cdp_inspect.mjs <chromeBin> <url> [scriptFile]
|
|
// Launches headless Chromium, connects over the DevTools WebSocket (Node's
|
|
// built-in WebSocket — no dependencies), evaluates one async script, prints JSON.
|
|
import { spawn } from 'node:child_process'
|
|
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
const [chromeBin, targetUrl, scriptFile] = process.argv.slice(2)
|
|
const userDataDir = mkdtempSync(join(tmpdir(), 'cdp-'))
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
// Optional --window-size=WxH, so responsive behaviour can be checked at real sizes.
|
|
const windowSizeArg = process.argv.find((arg) => arg.startsWith('--window-size='))
|
|
const windowSize = windowSizeArg ? windowSizeArg.split('=')[1] : null
|
|
|
|
const chrome = spawn(chromeBin, [
|
|
'--headless=new', '--no-sandbox', '--disable-dev-shm-usage',
|
|
// Headless Chrome wants `--window-size=W,H`; a `WxH` string is accepted here too.
|
|
...(windowSize ? [`--window-size=${windowSize.replace('x', ',')}`] : []),
|
|
// Software WebGL2: headless has no GPU, and SwiftShader is the only way to
|
|
// exercise the renderer path in CI-like runs.
|
|
'--use-angle=swiftshader', '--use-gl=angle', '--enable-unsafe-swiftshader',
|
|
`--user-data-dir=${userDataDir}`,
|
|
'--remote-debugging-port=0',
|
|
targetUrl,
|
|
], { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
let stderr = ''
|
|
chrome.stderr.on('data', (d) => { stderr += d })
|
|
|
|
let port = null
|
|
for (let i = 0; i < 100; i++) {
|
|
await sleep(200)
|
|
try {
|
|
port = readFileSync(join(userDataDir, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0]
|
|
if (port) break
|
|
} catch {}
|
|
}
|
|
if (!port) { console.error('NO PORT', stderr.slice(-500)); process.exit(1) }
|
|
|
|
let page
|
|
for (let i = 0; i < 50; i++) {
|
|
await sleep(200)
|
|
try {
|
|
const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json()
|
|
page = list.find((x) => x.type === 'page')
|
|
if (page) break
|
|
} catch {}
|
|
}
|
|
if (!page) { console.error('NO PAGE', stderr.slice(-500)); process.exit(1) }
|
|
|
|
const ws = new WebSocket(page.webSocketDebuggerUrl)
|
|
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject })
|
|
let msgId = 0
|
|
const pending = new Map()
|
|
ws.onmessage = (event) => {
|
|
const message = JSON.parse(event.data)
|
|
if (message.id && pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id) }
|
|
}
|
|
const send = (method, params = {}) => new Promise((resolve) => {
|
|
const id = ++msgId
|
|
pending.set(id, resolve)
|
|
ws.send(JSON.stringify({ id, method, params }))
|
|
})
|
|
|
|
await send('Runtime.enable')
|
|
await send('Page.enable')
|
|
await sleep(3000)
|
|
|
|
const expression = scriptFile
|
|
? await readFile(scriptFile, 'utf8')
|
|
: `({ title: document.title, text: document.body.innerText.slice(0, 2000) })`
|
|
const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })
|
|
const shotIndex = process.argv.indexOf('--screenshot')
|
|
if (shotIndex !== -1) {
|
|
const shotPath = process.argv[shotIndex + 1]
|
|
const shot = await send('Page.captureScreenshot', { format: 'png' })
|
|
const data = shot.result?.data
|
|
if (typeof data === 'string') {
|
|
const { writeFileSync } = await import('node:fs')
|
|
writeFileSync(shotPath, Buffer.from(data, 'base64'))
|
|
console.error(`screenshot written: ${shotPath}`)
|
|
}
|
|
}
|
|
if (result.result?.exceptionDetails) {
|
|
console.error('EVAL ERROR', JSON.stringify(result.result.exceptionDetails).slice(0, 1500))
|
|
} else {
|
|
console.log(JSON.stringify(result.result?.result?.value, null, 2))
|
|
}
|
|
ws.close()
|
|
chrome.kill()
|
|
process.exit(0)
|