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