This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
EveElectron/capture/main.mjs

94 lines
3.7 KiB
JavaScript

import { app, BrowserWindow, shell } from "electron";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { buildCaptureDocument, writeCaptureWitness } from "../src/capture-model.mjs";
import { createEveElectronWindow } from "../src/window-host.mjs";
const options = parseArguments(process.argv.slice(2));
const tracePath = `${options.witness}.trace.log`;
mkdirSync(path.dirname(tracePath), { recursive: true });
trace("starting");
process.on("uncaughtException", error => fail(error));
process.on("unhandledRejection", error => fail(error));
app.on("ready", () => trace("ready-event"));
app.on("will-quit", () => trace("will-quit"));
void app.whenReady().then(runCapture, fail);
async function runCapture() {
trace("electron-ready");
const started = performance.now();
let window;
try {
const advertisement = parseJson(readFileSync(options.advertisement, "utf8"));
const surface = parseJson(readFileSync(options.surface, "utf8"));
const { projection, html } = buildCaptureDocument(advertisement, surface, options.surfaceId);
trace("projection-built");
mkdirSync(path.dirname(options.output), { recursive: true });
window = createEveElectronWindow({
width: 1280,
height: 800,
show: false,
frame: true,
title: `EveElectron ${projection.surfaceId}`,
webPreferences: { offscreen: true },
}, { BrowserWindow, shell });
const imagePromise = nextPaint(window, 10000);
await window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
trace("window-loaded");
await window.webContents.executeJavaScript("document.fonts.ready.then(() => true)");
window.webContents.invalidate();
const image = await imagePromise;
trace("page-captured");
if (image.isEmpty()) throw new Error("Electron capture produced an empty native image.");
const png = image.toPNG();
if (png.length < 1024) throw new Error(`Electron capture is suspiciously small: ${png.length} bytes.`);
writeFileSync(options.output, png);
const size = image.getSize();
writeCaptureWitness({
outputPath: options.witness,
imagePath: options.output,
imageSize: size,
durationMs: Math.round((performance.now() - started) * 1000) / 1000,
advertisement,
surface,
projection,
});
console.log(options.witness);
trace("complete");
} finally {
window?.destroy();
app.exit(0);
}
}
function parseArguments(args) {
const values = {};
for (let index = 0; index < args.length; index += 2) values[args[index].slice(2)] = path.resolve(args[index + 1]);
for (const field of ["advertisement", "surface", "surface-id", "output", "witness"]) {
if (!values[field]) throw new Error(`Missing --${field}.`);
}
return { ...values, surfaceId: args[args.indexOf("--surface-id") + 1] };
}
function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); }
function trace(value) { writeFileSync(tracePath, `${new Date().toISOString()} ${value}\n`, { encoding: "utf8", flag: "a" }); }
function nextPaint(window, timeoutMs) {
return new Promise((resolve, reject) => {
const onPaint = (_event, _dirty, image) => {
if (image.isEmpty() || image.toPNG().length < 1024) return;
clearTimeout(timer);
window.webContents.off("paint", onPaint);
resolve(image);
};
const timer = setTimeout(() => {
window.webContents.off("paint", onPaint);
reject(new Error(`Electron offscreen paint timed out after ${timeoutMs}ms.`));
}, timeoutMs);
window.webContents.on("paint", onPaint);
});
}
function fail(error) {
trace(`failed ${error instanceof Error ? error.stack || error.message : String(error)}`);
app.exit(1);
}