import { app, BrowserWindow, dialog } from 'electron'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import { get } from 'node:http'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const bundleRoot = resolve(__dirname, '..'); const nodecgRoot = process.env.NODECG_ROOT ? resolve(bundleRoot, process.env.NODECG_ROOT) : resolve(bundleRoot, '..', '..'); const startUrl = process.env.ELECTRON_START_URL ?? 'http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true'; const nodecgPort = Number(process.env.NODECG_PORT ?? 9090); const nodeBinary = process.env.NODE_BINARY ?? 'node'; const useElectronNodeForNodeCG = process.env.NODECG_USE_ELECTRON_NODE === '1'; /** @type {import('node:child_process').ChildProcess | undefined} */ let nodecgProcess; function waitForServer(url, timeoutMs = 30_000) { const started = Date.now(); return new Promise((resolvePromise, rejectPromise) => { const attempt = () => { const request = get(url, (response) => { response.resume(); if (response.statusCode && response.statusCode >= 200 && response.statusCode < 500) { resolvePromise(); return; } if (Date.now() - started > timeoutMs) { rejectPromise(new Error('NodeCG did not respond in time.')); return; } setTimeout(attempt, 500); }); request.on('error', () => { if (Date.now() - started > timeoutMs) { rejectPromise(new Error('Could not connect to NodeCG.')); return; } setTimeout(attempt, 500); }); }; attempt(); }); } function startNodeCG() { const runtimeBinary = useElectronNodeForNodeCG ? process.execPath : nodeBinary; nodecgProcess = spawn(runtimeBinary, ['index.js'], { cwd: nodecgRoot, stdio: 'inherit', env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? 'production', PORT: String(nodecgPort) } }); nodecgProcess.on('exit', (code) => { if (!app.isQuiting) { dialog.showErrorBox( 'NodeCG exited', `The NodeCG process ended unexpectedly with code ${code ?? 'unknown'}.` ); app.quit(); } }); nodecgProcess.on('error', (error) => { dialog.showErrorBox( 'Could not start NodeCG', `Could not run \"${runtimeBinary}\". ${useElectronNodeForNodeCG ? 'Disable NODECG_USE_ELECTRON_NODE or check your Electron setup.' : 'Set NODE_BINARY to your Node.js path.'}\n\nDetails: ${error.message}` ); app.quit(); }); } async function createWindow() { const win = new BrowserWindow({ width: 1920, height: 1080, autoHideMenuBar: true, backgroundColor: '#000000', webPreferences: { contextIsolation: true, sandbox: true } }); await win.loadURL(startUrl); } app.on('before-quit', () => { app.isQuiting = true; if (nodecgProcess && !nodecgProcess.killed) { nodecgProcess.kill('SIGTERM'); } }); app.whenReady().then(async () => { startNodeCG(); try { await waitForServer(`http://127.0.0.1:${nodecgPort}`); await createWindow(); } catch (error) { dialog.showErrorBox('Could not start', error instanceof Error ? error.message : String(error)); app.quit(); } }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } });