mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
Merge pull request #2 from Pandipipas/create-electron-wrapper-for-scoreko-dev-b16002
fix: detect native-module ABI mismatch and wait for NodeCG readiness
This commit is contained in:
@@ -16,6 +16,7 @@ scoreko-electron-dev/
|
|||||||
├─ lib/
|
├─ lib/
|
||||||
│ └─ nodecg/
|
│ └─ nodecg/
|
||||||
│ ├─ index.js
|
│ ├─ index.js
|
||||||
|
│ ├─ node_modules/
|
||||||
│ └─ bundles/
|
│ └─ bundles/
|
||||||
│ └─ scoreko-dev/ # clonado/copiado desde tu repo scoreko-dev
|
│ └─ scoreko-dev/ # clonado/copiado desde tu repo scoreko-dev
|
||||||
├─ src/main/main.ts
|
├─ src/main/main.ts
|
||||||
@@ -27,19 +28,51 @@ scoreko-electron-dev/
|
|||||||
|
|
||||||
1. Copia o clona tu instalación de NodeCG en `lib/nodecg`.
|
1. Copia o clona tu instalación de NodeCG en `lib/nodecg`.
|
||||||
2. Copia tu bundle `scoreko-dev` a `lib/nodecg/bundles/scoreko-dev`.
|
2. Copia tu bundle `scoreko-dev` a `lib/nodecg/bundles/scoreko-dev`.
|
||||||
3. Instala dependencias de NodeCG dentro de `lib/nodecg` (si aplica).
|
3. **Instala dependencias de NodeCG dentro de `lib/nodecg`**.
|
||||||
4. Instala dependencias del wrapper:
|
4. Instala dependencias del wrapper:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
cd lib/nodecg
|
||||||
|
npm install
|
||||||
|
cd ../..
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Si no haces `npm install` dentro de `lib/nodecg`, verás errores como `Cannot find module ... node_modules/nodecg/dist/server/bootstrap.js`.
|
||||||
|
|
||||||
|
|
||||||
|
## Error típico: NODE_MODULE_VERSION (Node 22 vs Electron Node 20)
|
||||||
|
|
||||||
|
Si ves un error como `better-sqlite3 ... NODE_MODULE_VERSION`, tienes módulos nativos compilados para una versión distinta de Node.
|
||||||
|
|
||||||
|
1. Entra a `lib/nodecg`.
|
||||||
|
2. Reinstala/recompila dependencias nativas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd lib/nodecg
|
||||||
|
npm install
|
||||||
|
npm rebuild better-sqlite3 --update-binary
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Si persiste, elimina `node_modules` del workspace que falla y vuelve a instalar.
|
||||||
|
|
||||||
## Variables de entorno opcionales
|
## Variables de entorno opcionales
|
||||||
|
|
||||||
|
Estas variables se leen en `src/main/main.ts`.
|
||||||
|
|
||||||
- `NODECG_PORT` (default: `9090`)
|
- `NODECG_PORT` (default: `9090`)
|
||||||
- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`)
|
- `NODECG_BUNDLE_NAME` (default: `scoreko-dev`)
|
||||||
- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/index.html`)
|
- `SCOREKO_DASHBOARD_ROUTE` (default: `dashboard/index.html`)
|
||||||
- `ELECTRON_LOAD_DELAY_MS` (default: `5000`)
|
- `ELECTRON_LOAD_DELAY_MS` (default: `2500`)
|
||||||
|
- `NODECG_STARTUP_TIMEOUT_MS` (default: `30000`)
|
||||||
|
|
||||||
|
Ejemplo (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:NODECG_PORT="9191"
|
||||||
|
$env:NODECG_BUNDLE_NAME="scoreko-dev"
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
|
|||||||
+128
-19
@@ -1,13 +1,14 @@
|
|||||||
import { app, BrowserWindow, shell } from "electron";
|
import { app, BrowserWindow, dialog, shell } from "electron";
|
||||||
import { fork, ChildProcess } from "node:child_process";
|
import { ChildProcess, fork } from "node:child_process";
|
||||||
import path from "node:path";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
const APP_TITLE = "Scoreko";
|
const APP_TITLE = "Scoreko";
|
||||||
const DEFAULT_NODECG_PORT = process.env.NODECG_PORT ?? "9090";
|
const DEFAULT_NODECG_PORT = process.env.NODECG_PORT ?? "9090";
|
||||||
const DEFAULT_BUNDLE_NAME = process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev";
|
const DEFAULT_BUNDLE_NAME = process.env.NODECG_BUNDLE_NAME ?? "scoreko-dev";
|
||||||
const DEFAULT_DASHBOARD_ROUTE = process.env.SCOREKO_DASHBOARD_ROUTE ?? "dashboard/index.html";
|
const DEFAULT_DASHBOARD_ROUTE = process.env.SCOREKO_DASHBOARD_ROUTE ?? "dashboard/index.html";
|
||||||
const LOAD_DELAY_MS = Number.parseInt(process.env.ELECTRON_LOAD_DELAY_MS ?? "5000", 10);
|
const LOAD_DELAY_MS = Number.parseInt(process.env.ELECTRON_LOAD_DELAY_MS ?? "2500", 10);
|
||||||
|
const STARTUP_TIMEOUT_MS = Number.parseInt(process.env.NODECG_STARTUP_TIMEOUT_MS ?? "30000", 10);
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
|
||||||
@@ -15,10 +16,12 @@ const nodecgPath = path.resolve(rootPath, "lib", "nodecg");
|
|||||||
const loadingPath = path.resolve(rootPath, "static", "loading.html");
|
const loadingPath = path.resolve(rootPath, "static", "loading.html");
|
||||||
|
|
||||||
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
const dashboardUrl = `http://localhost:${DEFAULT_NODECG_PORT}/bundles/${DEFAULT_BUNDLE_NAME}/${DEFAULT_DASHBOARD_ROUTE}`;
|
||||||
|
const baseUrl = `http://127.0.0.1:${DEFAULT_NODECG_PORT}`;
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let loadingWindow: BrowserWindow | null = null;
|
let loadingWindow: BrowserWindow | null = null;
|
||||||
let nodecgProcess: ChildProcess | null = null;
|
let nodecgProcess: ChildProcess | null = null;
|
||||||
|
let lastNodeCGOutput = "";
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createMainWindow(): BrowserWindow {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
@@ -87,14 +90,65 @@ function createLoadingWindow(): BrowserWindow {
|
|||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNodeCG(): ChildProcess {
|
function validateNodeCGInstall(): void {
|
||||||
const indexPath = path.join(nodecgPath, "index.js");
|
const indexPath = path.join(nodecgPath, "index.js");
|
||||||
|
const nodecgBootstrapPath = path.join(nodecgPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js");
|
||||||
|
const bundlePath = path.join(nodecgPath, "bundles", DEFAULT_BUNDLE_NAME);
|
||||||
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
if (!fs.existsSync(nodecgPath)) {
|
||||||
throw new Error(`NodeCG entrypoint not found: ${indexPath}`);
|
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const child = fork(indexPath, {
|
if (!fs.existsSync(indexPath)) {
|
||||||
|
throw new Error(`No se encontró ${indexPath}. Copia una instalación completa de NodeCG en lib/nodecg.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(nodecgBootstrapPath)) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
"NodeCG está presente pero faltan dependencias internas.",
|
||||||
|
`No existe: ${nodecgBootstrapPath}`,
|
||||||
|
"Solución: entra a lib/nodecg e instala dependencias:",
|
||||||
|
" npm install",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(bundlePath)) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`No se encontró el bundle '${DEFAULT_BUNDLE_NAME}'.`,
|
||||||
|
`Ruta esperada: ${bundlePath}`,
|
||||||
|
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichNodeCGFailureMessage(baseMessage: string): string {
|
||||||
|
if (lastNodeCGOutput.includes("NODE_MODULE_VERSION")) {
|
||||||
|
return [
|
||||||
|
baseMessage,
|
||||||
|
"",
|
||||||
|
"Detectado error de módulos nativos compilados para otra versión de Node/Electron (NODE_MODULE_VERSION).",
|
||||||
|
"Esto suele pasar cuando NodeCG/bundles fueron instalados con Node 22 pero Electron corre con Node 20 (o viceversa).",
|
||||||
|
"",
|
||||||
|
"Solución recomendada (en lib/nodecg):",
|
||||||
|
" 1) borrar node_modules y lockfile del workspace que falla (si aplica)",
|
||||||
|
" 2) npm install",
|
||||||
|
" 3) npm rebuild better-sqlite3 --update-binary",
|
||||||
|
"",
|
||||||
|
"También puedes alinear versiones: usar una versión de Electron cuyo Node interno coincida con el usado para compilar addons.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNodeCG(): ChildProcess {
|
||||||
|
validateNodeCGInstall();
|
||||||
|
|
||||||
|
const child = fork(path.join(nodecgPath, "index.js"), {
|
||||||
cwd: nodecgPath,
|
cwd: nodecgPath,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -102,7 +156,19 @@ function startNodeCG(): ChildProcess {
|
|||||||
NODE_ENV: isDev ? "development" : "production",
|
NODE_ENV: isDev ? "development" : "production",
|
||||||
NODECG_PORT: DEFAULT_NODECG_PORT,
|
NODECG_PORT: DEFAULT_NODECG_PORT,
|
||||||
},
|
},
|
||||||
stdio: "inherit",
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout?.on("data", (chunk) => {
|
||||||
|
const text = String(chunk);
|
||||||
|
process.stdout.write(text);
|
||||||
|
lastNodeCGOutput = `${lastNodeCGOutput}${text}`.slice(-20000);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on("data", (chunk) => {
|
||||||
|
const text = String(chunk);
|
||||||
|
process.stderr.write(text);
|
||||||
|
lastNodeCGOutput = `${lastNodeCGOutput}${text}`.slice(-20000);
|
||||||
});
|
});
|
||||||
|
|
||||||
log(`NodeCG started with pid=${child.pid}`);
|
log(`NodeCG started with pid=${child.pid}`);
|
||||||
@@ -115,6 +181,33 @@ function startNodeCG(): ChildProcess {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForNodeCGReady(startTime: number): Promise<void> {
|
||||||
|
while (Date.now() - startTime < STARTUP_TIMEOUT_MS) {
|
||||||
|
if (!nodecgProcess) {
|
||||||
|
throw new Error("NodeCG terminó antes de estar listo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(baseUrl, { method: "GET" });
|
||||||
|
if (response.ok || response.status === 404) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// retry until timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timeout esperando NodeCG en ${baseUrl} (${STARTUP_TIMEOUT_MS}ms).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function launch(): Promise<void> {
|
async function launch(): Promise<void> {
|
||||||
mainWindow = createMainWindow();
|
mainWindow = createMainWindow();
|
||||||
loadingWindow = createLoadingWindow();
|
loadingWindow = createLoadingWindow();
|
||||||
@@ -122,21 +215,27 @@ async function launch(): Promise<void> {
|
|||||||
await loadingWindow.loadFile(loadingPath);
|
await loadingWindow.loadFile(loadingPath);
|
||||||
loadingWindow.show();
|
loadingWindow.show();
|
||||||
|
|
||||||
|
lastNodeCGOutput = "";
|
||||||
nodecgProcess = startNodeCG();
|
nodecgProcess = startNodeCG();
|
||||||
|
|
||||||
setTimeout(async () => {
|
await sleep(Math.max(0, LOAD_DELAY_MS));
|
||||||
if (!mainWindow) {
|
await waitForNodeCGReady(Date.now());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await mainWindow.loadURL(dashboardUrl);
|
await mainWindow.loadURL(dashboardUrl);
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`No se pudo cargar el dashboard en ${dashboardUrl}. ${String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (loadingWindow && !loadingWindow.isDestroyed()) {
|
if (loadingWindow && !loadingWindow.isDestroyed()) {
|
||||||
loadingWindow.close();
|
loadingWindow.close();
|
||||||
loadingWindow = null;
|
loadingWindow = null;
|
||||||
}
|
}
|
||||||
}, LOAD_DELAY_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopNodeCG(): void {
|
function stopNodeCG(): void {
|
||||||
@@ -153,8 +252,18 @@ function log(...args: unknown[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.on("ready", () => {
|
app.on("ready", () => {
|
||||||
launch().catch((error: unknown) => {
|
launch().catch(async (error: unknown) => {
|
||||||
console.error("Failed to launch Scoreko wrapper", error);
|
console.error("Failed to launch Scoreko wrapper", error);
|
||||||
|
|
||||||
|
const detail = enrichNodeCGFailureMessage(error instanceof Error ? error.message : String(error));
|
||||||
|
|
||||||
|
await dialog.showMessageBox({
|
||||||
|
type: "error",
|
||||||
|
title: "No se pudo iniciar Scoreko",
|
||||||
|
message: "Fallo al iniciar NodeCG",
|
||||||
|
detail,
|
||||||
|
});
|
||||||
|
|
||||||
app.exit(1);
|
app.exit(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user