75 lines
2.5 KiB
JavaScript
75 lines
2.5 KiB
JavaScript
// Minimal Chrome DevTools Protocol client (Node 22 global WebSocket, no npm deps).
|
|||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
class CDP {
|
||
|
|
constructor(wsUrl) {
|
||
|
|
this.wsUrl = wsUrl;
|
||
|
|
this.id = 0;
|
||
|
|
this.pending = new Map();
|
||
|
|
this.closed = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
connect(timeoutMs = 10000) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const ws = new WebSocket(this.wsUrl);
|
||
|
|
this.ws = ws;
|
||
|
|
const timer = setTimeout(() => { try { ws.close(); } catch {} reject(new Error('connect timeout')); }, timeoutMs);
|
||
|
|
ws.onopen = () => { clearTimeout(timer); resolve(this); };
|
||
|
|
ws.onerror = () => { clearTimeout(timer); reject(new Error('ws error')); };
|
||
|
|
ws.onclose = () => {
|
||
|
|
this.closed = true;
|
||
|
|
for (const [, p] of this.pending) p.rej(new Error('ws closed'));
|
||
|
|
this.pending.clear();
|
||
|
|
};
|
||
|
|
ws.onmessage = (ev) => {
|
||
|
|
let m;
|
||
|
|
try { m = JSON.parse(ev.data); } catch { return; }
|
||
|
|
if (m.id && this.pending.has(m.id)) {
|
||
|
|
const p = this.pending.get(m.id);
|
||
|
|
this.pending.delete(m.id);
|
||
|
|
if (m.error) p.rej(new Error(m.error.message || 'cdp error'));
|
||
|
|
else p.res(m.result);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
send(method, params = {}, timeoutMs = 15000) {
|
||
|
|
if (this.closed) return Promise.reject(new Error('ws closed'));
|
||
|
|
const id = ++this.id;
|
||
|
|
return new Promise((res, rej) => {
|
||
|
|
this.pending.set(id, { res, rej });
|
||
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
||
|
|
setTimeout(() => {
|
||
|
|
if (this.pending.has(id)) { this.pending.delete(id); rej(new Error('cdp timeout: ' + method)); }
|
||
|
|
}, timeoutMs);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async eval(expression, timeoutMs = 15000) {
|
||
|
|
const r = await this.send('Runtime.evaluate', {
|
||
|
|
expression,
|
||
|
|
returnByValue: true,
|
||
|
|
awaitPromise: true,
|
||
|
|
userGesture: true
|
||
|
|
}, timeoutMs);
|
||
|
|
if (r.exceptionDetails) {
|
||
|
|
throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text || 'eval failed');
|
||
|
|
}
|
||
|
|
return r.result ? r.result.value : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
close() { try { this.ws.close(); } catch {} this.closed = true; }
|
||
|
|
}
|
||
|
|
|
||
|
|
async function listPages(port, urlMatch) {
|
||
|
|
const res = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(5000) });
|
||
|
|
const all = await res.json();
|
||
|
|
return all.filter(t => t.type === 'page' && typeof t.url === 'string'
|
||
|
|
&& t.url.includes(urlMatch) && !t.url.startsWith('devtools://'));
|
||
|
|
}
|
||
|
|
|
||
|
|
const EXPR = fs.readFileSync(path.join(__dirname, 'probe-expression.js'), 'utf8');
|
||
|
|
|
||
|
|
module.exports = { CDP, listPages, EXPR };
|