orbit/dev/cdp.mjs

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(' | '));
}