88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
// Minimal CDP helper with HARD timeouts so scripts can never hang.
|
|
// Usage:
|
|
// import { connectTab, screenshot, close } from './cdp.mjs';
|
|
// const c = await connectTab(); // finds a booted game tab on :8080
|
|
// const v = await c.eval('1+1');
|
|
// await c.screenshot('/tmp/x.png');
|
|
// c.close();
|
|
const CDP = '127.0.0.1:9333';
|
|
const GLOBAL_MS = 45000;
|
|
const watchdog = setTimeout(() => {
|
|
console.error('[cdp] GLOBAL TIMEOUT — force exiting');
|
|
process.exit(130);
|
|
}, GLOBAL_MS);
|
|
|
|
function toTimeout(promise, ms, label) {
|
|
return Promise.race([
|
|
promise,
|
|
new Promise((_, rej) => setTimeout(() => rej(new Error(`timeout: ${label} (${ms}ms)`)), ms)),
|
|
]);
|
|
}
|
|
|
|
async function listTargets() {
|
|
const r = await toTimeout(fetch(`http://${CDP}/json`), 5000, 'fetch /json');
|
|
return r.json();
|
|
}
|
|
|
|
export async function connectTab() {
|
|
const list = await listTargets();
|
|
const tabs = list.filter(x => x.type === 'page' && x.url.includes(':8080'));
|
|
if (!tabs.length) throw new Error('no :8080 page target');
|
|
for (const cand of tabs) {
|
|
let ws = null;
|
|
try {
|
|
ws = new WebSocket(cand.webSocketDebuggerUrl);
|
|
await toTimeout(new Promise((res, rej) => {
|
|
ws.onopen = res;
|
|
ws.onerror = () => rej(new Error('ws open error'));
|
|
}), 3000, 'ws open');
|
|
|
|
let seq = 0;
|
|
const pending = new Map();
|
|
ws.onmessage = m => {
|
|
const q = JSON.parse(m.data);
|
|
if (q.id && pending.has(q.id)) {
|
|
const o = pending.get(q.id); pending.delete(q.id);
|
|
q.error ? o.rej(new Error(q.error.message)) : o.res(q.result);
|
|
}
|
|
};
|
|
const send = (method, params = {}) =>
|
|
toTimeout(new Promise((res, rej) => {
|
|
const id = ++seq;
|
|
pending.set(id, { res, rej });
|
|
ws.send(JSON.stringify({ id, method, params }));
|
|
setTimeout(() => {
|
|
if (pending.has(id)) { pending.delete(id); rej(new Error('send timeout ' + method)); }
|
|
}, 8000);
|
|
}), 12000, 'send ' + method);
|
|
|
|
await send('Runtime.enable');
|
|
const probe = await send('Runtime.evaluate', {
|
|
expression: 'Boolean(window.__ORBIT_COMBAT__ && window.game?.scene?.getScene("GameScene")?.ship)',
|
|
returnByValue: true,
|
|
});
|
|
if (!probe.result?.value) { ws.close(); continue; }
|
|
|
|
return {
|
|
ws,
|
|
async eval(expression) {
|
|
const r = await send('Runtime.evaluate', { expression, returnByValue: true });
|
|
if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || 'eval error');
|
|
return r.result?.value;
|
|
},
|
|
async screenshot(path) {
|
|
const r = await send('Page.captureScreenshot', { format: 'png' });
|
|
const fs = await import('node:fs');
|
|
fs.writeFileSync(path, Buffer.from(r.data, 'base64'));
|
|
return path;
|
|
},
|
|
close() { try { ws.close(); } catch {} },
|
|
};
|
|
} catch (e) {
|
|
try { ws?.close(); } catch {}
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error('no booted game tab found: ' + tabs.map(t => t.url).join(' | '));
|
|
}
|