exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

cdp.mjs (4576B)


      1 // Pilotage d'un Chromium headless par le protocole DevTools, sans dépendance (pas de Playwright sur hotrats).
      2 import { spawn } from 'node:child_process';
      3 import { writeFileSync } from 'node:fs';
      4 
      5 export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
      6 
      7 export async function open(url, { port = Number(process.env.CDP_PORT ?? 9334), width = 1000, height = 1400 } = {}) {
      8   const chrome = spawn(process.env.CHROME ?? 'chromium', [
      9     '--headless=new', '--no-sandbox', '--hide-scrollbars',
     10     '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader',
     11     `--remote-debugging-port=${port}`, `--window-size=${width},${height}`, 'about:blank',
     12   ], { stdio: 'ignore' });
     13 
     14   let target;
     15   for (let i = 0; i < 50 && !target; i++) {
     16     await sleep(200);
     17     try {
     18       const r = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' });
     19       target = await r.json();
     20     } catch { /* pas encore prêt */ }
     21   }
     22   if (!target) { chrome.kill(); throw new Error('Chromium ne répond pas'); }
     23 
     24   const ws = new WebSocket(target.webSocketDebuggerUrl);
     25   await new Promise((r) => ws.addEventListener('open', r));
     26   let id = 0;
     27   const pending = new Map();
     28   const logs = [];
     29   ws.addEventListener('message', (ev) => {
     30     const m = JSON.parse(ev.data);
     31     if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? m.error); pending.delete(m.id); }
     32     else if (m.method === 'Runtime.consoleAPICalled') logs.push(`[${m.params.type}] ${m.params.args.map((a) => a.value ?? a.description).join(' ')}`);
     33     else if (m.method === 'Runtime.exceptionThrown') logs.push(`[exception] ${m.params.exceptionDetails.exception?.description ?? m.params.exceptionDetails.text}`);
     34   });
     35   const send = (method, params = {}) => new Promise((r) => { const i = ++id; pending.set(i, r); ws.send(JSON.stringify({ id: i, method, params })); });
     36   const evaluate = async (expression) => (await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })).result?.value;
     37   const screenshot = async (path) => {
     38     const png = (await send('Page.captureScreenshot', { format: 'png' })).data;
     39     writeFileSync(path, Buffer.from(png, 'base64'));
     40     return path;
     41   };
     42   // Coordonnées de la page pour un point de l'écran Smalltalk (640×480 mis à l'échelle CSS)
     43   const canvasPoint = async (x, y) => {
     44     const r = await evaluate("(() => { const c = document.getElementById('canvas').getBoundingClientRect(); return [c.left, c.top, c.width, c.height]; })()");
     45     const [left, top, w, h] = r;
     46     return { x: left + (x * w) / 640, y: top + (y * h) / 480 };
     47   };
     48   const mouse = async (type, x, y, button = 'none', clickCount = 1) =>
     49     send('Input.dispatchMouseEvent', { type, x, y, button, clickCount, buttons: button === 'left' ? 1 : button === 'right' ? 2 : button === 'middle' ? 4 : 0 });
     50   const click = async (sx, sy, button = 'left') => {
     51     const p = await canvasPoint(sx, sy);
     52     await mouse('mouseMoved', p.x, p.y);
     53     await sleep(100);
     54     await mouse('mousePressed', p.x, p.y, button);
     55     await sleep(120);
     56     await mouse('mouseReleased', p.x, p.y, button);
     57   };
     58   // Frappe : chaque touche porte un `code` physique et un keyCode, sinon SDL ne la reconnaît pas
     59   // (symbole 0) ; les caractères passent par le keypress (texte), Entrée par le keydown seul.
     60   const keyOf = (ch) => {
     61     if (ch === '\r') return { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, text: '\r' };
     62     if (/[a-z]/i.test(ch)) return { key: ch, code: `Key${ch.toUpperCase()}`, windowsVirtualKeyCode: ch.toUpperCase().charCodeAt(0), text: ch };
     63     if (/[0-9]/.test(ch)) return { key: ch, code: `Digit${ch}`, windowsVirtualKeyCode: ch.charCodeAt(0), text: ch };
     64     if (ch === ' ') return { key: ' ', code: 'Space', windowsVirtualKeyCode: 32, text: ' ' };
     65     return { key: ch, code: 'Unidentified', windowsVirtualKeyCode: 0, text: ch };
     66   };
     67   const type = async (text) => {
     68     for (const ch of text) {
     69       const k = keyOf(ch);
     70       await send('Input.dispatchKeyEvent', { type: 'keyDown', key: k.key, code: k.code, windowsVirtualKeyCode: k.windowsVirtualKeyCode, text: k.text, unmodifiedText: k.text });
     71       await send('Input.dispatchKeyEvent', { type: 'keyUp', key: k.key, code: k.code, windowsVirtualKeyCode: k.windowsVirtualKeyCode });
     72       await sleep(40);
     73     }
     74   };
     75   const close = () => { ws.close(); chrome.kill(); };
     76 
     77   await send('Runtime.enable');
     78   await send('Page.enable');
     79   await send('Page.navigate', { url });
     80   return { send, evaluate, logs, screenshot, canvasPoint, mouse, click, type, close };
     81 }