| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import { spawn } from 'node:child_process';
- import { createHash, randomBytes } from 'node:crypto';
- import { mkdirSync, writeFileSync } from 'node:fs';
- import http from 'node:http';
- import net from 'node:net';
- import { dirname, resolve } from 'node:path';
- const chromePath = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
- const url = process.argv[2] || 'http://localhost:7456/';
- const out = resolve(process.argv[3] || 'cocos-preview.png');
- const port = Number(process.env.CDP_PORT || 9223);
- const userDataDir = process.env.CDP_USER_DATA_DIR || resolve('tmp/chrome-cdp');
- const waitMs = Number(process.env.CDP_WAIT_MS || 10000);
- const holdRightMs = Number(process.env.CDP_HOLD_RIGHT_MS || 0);
- const holdFireMs = Number(process.env.CDP_HOLD_FIRE_MS || 0);
- const evalExpression = process.env.CDP_EVAL || '';
- const windowSize = process.env.CDP_WINDOW_SIZE || '1280,720';
- function getJson(path) {
- return new Promise((resolvePromise, reject) => {
- const req = http.get({ hostname: '127.0.0.1', port, path }, (res) => {
- let data = '';
- res.on('data', (chunk) => (data += chunk));
- res.on('end', () => {
- try {
- resolvePromise(JSON.parse(data));
- } catch (error) {
- reject(error);
- }
- });
- });
- req.on('error', reject);
- });
- }
- async function waitForChrome() {
- for (let i = 0; i < 80; i++) {
- try {
- return await getJson('/json/version');
- } catch {
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
- }
- }
- throw new Error('Chrome DevTools endpoint did not start');
- }
- class CdpSocket {
- constructor(wsUrl) {
- const { hostname, port: wsPort, pathname } = new URL(wsUrl);
- this.host = hostname;
- this.port = Number(wsPort);
- this.path = pathname;
- this.nextId = 1;
- this.pending = new Map();
- this.buffer = Buffer.alloc(0);
- this.events = [];
- }
- connect() {
- return new Promise((resolvePromise, reject) => {
- const key = randomBytes(16).toString('base64');
- const expected = createHash('sha1')
- .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
- .digest('base64');
- this.socket = net.createConnection(this.port, this.host, () => {
- this.socket.write([
- `GET ${this.path} HTTP/1.1`,
- `Host: ${this.host}:${this.port}`,
- 'Upgrade: websocket',
- 'Connection: Upgrade',
- `Sec-WebSocket-Key: ${key}`,
- 'Sec-WebSocket-Version: 13',
- '',
- ''
- ].join('\r\n'));
- });
- let header = Buffer.alloc(0);
- const onData = (chunk) => {
- header = Buffer.concat([header, chunk]);
- const idx = header.indexOf('\r\n\r\n');
- if (idx === -1) return;
- const head = header.subarray(0, idx).toString('utf8');
- if (!head.includes('101') || !head.includes(expected)) {
- reject(new Error(`WebSocket handshake failed: ${head}`));
- return;
- }
- this.socket.off('data', onData);
- const rest = header.subarray(idx + 4);
- if (rest.length) this.onData(rest);
- this.socket.on('data', (data) => this.onData(data));
- resolvePromise();
- };
- this.socket.on('data', onData);
- this.socket.on('error', reject);
- });
- }
- onData(chunk) {
- this.buffer = Buffer.concat([this.buffer, chunk]);
- while (this.buffer.length >= 2) {
- const b1 = this.buffer[0];
- const b2 = this.buffer[1];
- let len = b2 & 0x7f;
- let offset = 2;
- if (len === 126) {
- if (this.buffer.length < 4) return;
- len = this.buffer.readUInt16BE(2);
- offset = 4;
- } else if (len === 127) {
- if (this.buffer.length < 10) return;
- len = Number(this.buffer.readBigUInt64BE(2));
- offset = 10;
- }
- const masked = Boolean(b2 & 0x80);
- const maskOffset = masked ? 4 : 0;
- if (this.buffer.length < offset + maskOffset + len) return;
- let payload = this.buffer.subarray(offset + maskOffset, offset + maskOffset + len);
- if (masked) {
- const mask = this.buffer.subarray(offset, offset + 4);
- payload = Buffer.from(payload.map((value, index) => value ^ mask[index % 4]));
- }
- this.buffer = this.buffer.subarray(offset + maskOffset + len);
- if ((b1 & 0x0f) === 1) {
- const msg = JSON.parse(payload.toString('utf8'));
- if (msg.id && this.pending.has(msg.id)) {
- this.pending.get(msg.id)(msg);
- this.pending.delete(msg.id);
- } else if (msg.method) {
- this.events.push(msg);
- }
- }
- }
- }
- send(method, params = {}) {
- const id = this.nextId++;
- const payload = Buffer.from(JSON.stringify({ id, method, params }), 'utf8');
- let header;
- if (payload.length < 126) {
- header = Buffer.alloc(2);
- header[1] = 0x80 | payload.length;
- } else {
- header = Buffer.alloc(4);
- header[1] = 0x80 | 126;
- header.writeUInt16BE(payload.length, 2);
- }
- header[0] = 0x81;
- const mask = randomBytes(4);
- const masked = Buffer.from(payload.map((value, index) => value ^ mask[index % 4]));
- const frame = Buffer.concat([header, mask, masked]);
- return new Promise((resolvePromise) => {
- this.pending.set(id, resolvePromise);
- this.socket.write(frame);
- }).then((msg) => {
- if (msg.error) throw new Error(`${method}: ${msg.error.message}`);
- return msg.result;
- });
- }
- close() {
- this.socket?.end();
- }
- }
- mkdirSync(dirname(out), { recursive: true });
- const chrome = spawn(chromePath, [
- '--headless',
- '--disable-gpu',
- '--no-sandbox',
- '--disable-dev-shm-usage',
- `--remote-debugging-port=${port}`,
- `--user-data-dir=${resolve(userDataDir)}`,
- `--window-size=${windowSize}`,
- url
- ], { stdio: 'ignore' });
- try {
- await waitForChrome();
- let page;
- for (let i = 0; i < 40; i++) {
- const pages = await getJson('/json/list');
- page = pages.find((item) => item.type === 'page' && item.url.includes('localhost:7456')) || pages.find((item) => item.type === 'page');
- if (page?.webSocketDebuggerUrl) break;
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
- }
- if (!page?.webSocketDebuggerUrl) throw new Error('No debuggable page found');
- const cdp = new CdpSocket(page.webSocketDebuggerUrl);
- await cdp.connect();
- await cdp.send('Page.enable');
- await cdp.send('Runtime.enable');
- if (holdRightMs > 0) {
- await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'd', code: 'KeyD', windowsVirtualKeyCode: 68 });
- await new Promise((resolvePromise) => setTimeout(resolvePromise, holdRightMs));
- await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'd', code: 'KeyD', windowsVirtualKeyCode: 68 });
- }
- if (holdFireMs > 0) {
- await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'j', code: 'KeyJ', windowsVirtualKeyCode: 74 });
- await new Promise((resolvePromise) => setTimeout(resolvePromise, holdFireMs));
- await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'j', code: 'KeyJ', windowsVirtualKeyCode: 74 });
- }
- await new Promise((resolvePromise) => setTimeout(resolvePromise, waitMs));
- let evalResult = null;
- if (evalExpression) {
- evalResult = await cdp.send('Runtime.evaluate', { expression: evalExpression, returnByValue: true });
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
- }
- const title = await cdp.send('Runtime.evaluate', { expression: 'document.title', returnByValue: true });
- const canvasInfo = await cdp.send('Runtime.evaluate', {
- expression: `(() => {
- const c = document.querySelector('canvas');
- return c ? { width: c.width, height: c.height, clientWidth: c.clientWidth, clientHeight: c.clientHeight } : null;
- })()`,
- returnByValue: true
- });
- const screenshot = await cdp.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
- writeFileSync(out, Buffer.from(screenshot.data, 'base64'));
- cdp.close();
- console.log(JSON.stringify({
- ok: true,
- url,
- output: out,
- title: title.result.value,
- canvas: canvasInfo.result.value,
- evalResult: evalResult?.result?.value ?? null,
- recentEvents: cdp.events.slice(-20).map((event) => ({
- method: event.method,
- type: event.params?.type,
- text: event.params?.args?.map((arg) => arg.value || arg.description).filter(Boolean).join(' ')
- || event.params?.exceptionDetails?.text
- || ''
- }))
- }, null, 2));
- } finally {
- chrome.kill();
- }
|