mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
refactor(main): extraer configuración, ventanas, procesos y errores
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import { ChildProcess, spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { NODE_RUNTIME_NAME } from "../constants";
|
||||
|
||||
type NodecgProcessManagerConfig = {
|
||||
isDev: boolean;
|
||||
nodecgPath: string;
|
||||
baseUrl: string;
|
||||
runtimeConfig: AppRuntimeConfig;
|
||||
log: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
export type NodecgProcessManager = {
|
||||
startNodeCG: () => ChildProcess;
|
||||
waitForNodeCGReady: (startTime: number) => Promise<void>;
|
||||
stopNodeCG: () => Promise<void>;
|
||||
getProcess: () => ChildProcess | null;
|
||||
};
|
||||
|
||||
export function createNodecgProcessManager({
|
||||
isDev,
|
||||
nodecgPath,
|
||||
baseUrl,
|
||||
runtimeConfig,
|
||||
log,
|
||||
}: NodecgProcessManagerConfig): NodecgProcessManager {
|
||||
let nodecgProcess: ChildProcess | null = null;
|
||||
let stopNodeCGPromise: Promise<void> | null = null;
|
||||
|
||||
const startNodeCG = (): ChildProcess => {
|
||||
validateNodeCGInstall(nodecgPath, runtimeConfig.bundleName);
|
||||
|
||||
const indexPath = path.join(nodecgPath, "index.js");
|
||||
const child = spawn(process.execPath, [indexPath], {
|
||||
cwd: nodecgPath,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: isDev ? "development" : "production",
|
||||
NODECG_PORT: runtimeConfig.nodecgPort,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
process.stdout.write(String(chunk));
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
process.stderr.write(String(chunk));
|
||||
});
|
||||
|
||||
log(`NodeCG started with pid=${child.pid} using ${NODE_RUNTIME_NAME}`);
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||
nodecgProcess = null;
|
||||
});
|
||||
|
||||
nodecgProcess = child;
|
||||
return child;
|
||||
};
|
||||
|
||||
const waitForNodeCGReady = async (startTime: number): Promise<void> => {
|
||||
while (Date.now() - startTime < runtimeConfig.startupTimeoutMs) {
|
||||
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} (${runtimeConfig.startupTimeoutMs}ms).`);
|
||||
};
|
||||
|
||||
const stopNodeCG = (): Promise<void> => {
|
||||
if (stopNodeCGPromise) {
|
||||
return stopNodeCGPromise;
|
||||
}
|
||||
|
||||
if (!nodecgProcess || nodecgProcess.killed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const processToStop = nodecgProcess;
|
||||
const pid = processToStop.pid;
|
||||
|
||||
if (typeof pid !== "number") {
|
||||
log("NodeCG pid unavailable, skipping graceful stop");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
log(`Stopping NodeCG pid=${pid}`);
|
||||
killNodeCGProcessTree(pid, "SIGTERM", log);
|
||||
|
||||
stopNodeCGPromise = new Promise((resolve) => {
|
||||
const complete = () => {
|
||||
if (nodecgProcess === processToStop) {
|
||||
nodecgProcess = null;
|
||||
}
|
||||
stopNodeCGPromise = null;
|
||||
resolve();
|
||||
};
|
||||
|
||||
processToStop.once("exit", () => {
|
||||
complete();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (processToStop.exitCode === null && processToStop.signalCode === null) {
|
||||
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
|
||||
killNodeCGProcessTree(pid, "SIGKILL", log);
|
||||
}
|
||||
}, Math.max(0, runtimeConfig.nodecgKillTimeoutMs));
|
||||
});
|
||||
|
||||
return stopNodeCGPromise;
|
||||
};
|
||||
|
||||
return {
|
||||
startNodeCG,
|
||||
waitForNodeCGReady,
|
||||
stopNodeCG,
|
||||
getProcess: () => nodecgProcess,
|
||||
};
|
||||
}
|
||||
|
||||
function validateNodeCGInstall(nodecgPath: string, bundleName: string): void {
|
||||
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", bundleName);
|
||||
|
||||
if (!fs.existsSync(nodecgPath)) {
|
||||
throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`);
|
||||
}
|
||||
|
||||
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 '${bundleName}'.`,
|
||||
`Ruta esperada: ${bundlePath}`,
|
||||
"Copia/clona tu bundle dentro de lib/nodecg/bundles antes de ejecutar Electron.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function killNodeCGProcessTree(pid: number, signal: NodeJS.Signals, log: (...args: unknown[]) => void): boolean {
|
||||
if (process.platform === "win32") {
|
||||
const force = signal === "SIGKILL" ? "/F" : "";
|
||||
const killer = spawn("taskkill", ["/pid", String(pid), "/T", ...(force ? [force] : [])], {
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
killer.on("error", (error) => {
|
||||
log(`taskkill error for pid=${pid}`, error);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
process.kill(-pid, signal);
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
process.kill(pid, signal);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user