From 710fea38c0834127df078e155ec8b85992e0d6e1 Mon Sep 17 00:00:00 2001 From: Pandipipas <62224708+Pandipipas@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:55:30 +0100 Subject: [PATCH] =?UTF-8?q?refactor(cleanup):=20completar=20fase=205=20con?= =?UTF-8?q?=20renombrados=20sem=C3=A1nticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/config/runtime-config.ts | 8 ++-- src/main/main.ts | 46 +++++++++--------- src/main/nodecg/process-manager.ts | 77 +++++++++++++++--------------- src/main/windows/icon-path.ts | 4 +- src/main/windows/window-factory.ts | 22 ++++----- src/tests/icon-path.test.ts | 16 +++---- src/tests/process-manager.test.ts | 54 ++++++++++----------- 7 files changed, 114 insertions(+), 113 deletions(-) diff --git a/src/main/config/runtime-config.ts b/src/main/config/runtime-config.ts index c1e555d..3c0512c 100644 --- a/src/main/config/runtime-config.ts +++ b/src/main/config/runtime-config.ts @@ -4,8 +4,8 @@ export type AppRuntimeConfig = { iconPathOverride?: string; nodecgPort: string; bundleName: string; - dashboardRoute: string; - loadingRoute: string; + mainDashboardRoute: string; + loadingDashboardRoute: string; loadDelayMs: number; startupTimeoutMs: number; nodecgKillTimeoutMs: number; @@ -21,8 +21,8 @@ export function getRuntimeConfig(): AppRuntimeConfig { iconPathOverride: getOptionalEnv("SCOREKO_APP_ICON_PATH"), nodecgPort: parseEnvPort("NODECG_PORT", "9090"), bundleName: getEnv("NODECG_BUNDLE_NAME", "scoreko-dev"), - dashboardRoute: getEnv("SCOREKO_DASHBOARD_ROUTE", "dashboard/scoreko-dev/main.html?standalone=true"), - loadingRoute: getEnv("SCOREKO_LOADING_ROUTE", "dashboard/loading/main.html?standalone=true"), + mainDashboardRoute: getEnv("SCOREKO_DASHBOARD_ROUTE", "dashboard/scoreko-dev/main.html?standalone=true"), + loadingDashboardRoute: getEnv("SCOREKO_LOADING_ROUTE", "dashboard/loading/main.html?standalone=true"), loadDelayMs: parseEnvIntInRange("ELECTRON_LOAD_DELAY_MS", 10000, 0, 600000), startupTimeoutMs: parseEnvIntInRange("NODECG_STARTUP_TIMEOUT_MS", 30000, 1000, 600000), nodecgKillTimeoutMs: parseEnvIntInRange("NODECG_KILL_TIMEOUT_MS", 2500, 0, 120000), diff --git a/src/main/main.ts b/src/main/main.ts index 3757bb5..f305694 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -7,20 +7,20 @@ import { createNodecgProcessManager } from "./nodecg/process-manager"; import { getRemainingDelayMs } from "./utils/timing"; import { createLoadingWindow, createMainWindow } from "./windows/window-factory"; -const runtimeConfig = getRuntimeConfig(); +const appConfig = getRuntimeConfig(); const isDev = !app.isPackaged; const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath; -const nodecgPath = path.resolve(rootPath, "lib", "nodecg"); -const dashboardUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.dashboardRoute}`; -const loadingUrl = `http://localhost:${runtimeConfig.nodecgPort}/bundles/${runtimeConfig.bundleName}/${runtimeConfig.loadingRoute}`; -const baseUrl = `http://127.0.0.1:${runtimeConfig.nodecgPort}`; +const nodecgRootPath = path.resolve(rootPath, "lib", "nodecg"); +const mainDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.mainDashboardRoute}`; +const loadingDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.loadingDashboardRoute}`; +const nodecgBaseUrl = `http://127.0.0.1:${appConfig.nodecgPort}`; const nodecgManager = createNodecgProcessManager({ isDev, - nodecgPath, - baseUrl, - runtimeConfig, + nodecgRootPath, + nodecgBaseUrl, + appConfig, log, }); @@ -30,19 +30,19 @@ let mainWindow: BrowserWindow | null = null; let loadingWindow: BrowserWindow | null = null; let shutdownState: AppShutdownState = "running"; -async function launch(): Promise { - mainWindow = createMainWindow({ runtimeConfig, rootPath, dashboardUrl }); - loadingWindow = createLoadingWindow({ runtimeConfig, rootPath }); +async function launchApplication(): Promise { + mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl }); + loadingWindow = createLoadingWindow({ appConfig, rootPath }); - nodecgManager.startNodeCG(); + nodecgManager.startNodecgProcess(); - await nodecgManager.waitForNodeCGReady(Date.now()); + await nodecgManager.waitForNodecgReady(Date.now()); if (!loadingWindow || loadingWindow.isDestroyed()) { return; } - await loadingWindow.loadURL(loadingUrl); + await loadingWindow.loadURL(loadingDashboardUrl); loadingWindow.show(); const loadingShownAt = Date.now(); @@ -51,9 +51,9 @@ async function launch(): Promise { return; } - await mainWindow.loadURL(dashboardUrl); + await mainWindow.loadURL(mainDashboardUrl); - const remainingLoadingDelay = getRemainingDelayMs(runtimeConfig.loadDelayMs, loadingShownAt); + const remainingLoadingDelay = getRemainingDelayMs(appConfig.loadDelayMs, loadingShownAt); if (remainingLoadingDelay > 0) { await sleep(remainingLoadingDelay); } @@ -83,24 +83,24 @@ function stopNodecgGracefully(): Promise { } if (shutdownState === "stopping") { - return nodecgManager.stopNodeCG(); + return nodecgManager.stopNodecgProcessGracefully(); } shutdownState = "stopping"; - return nodecgManager.stopNodeCG().finally(() => { + return nodecgManager.stopNodecgProcessGracefully().finally(() => { shutdownState = "stopped"; }); } app.on("ready", () => { - app.setName(runtimeConfig.title); + app.setName(appConfig.title); if (process.platform === "win32") { - app.setAppUserModelId(runtimeConfig.userModelId); + app.setAppUserModelId(appConfig.userModelId); } - launch().catch((error: unknown) => { + launchApplication().catch((error: unknown) => { showFatalError("No se pudo iniciar Scoreko.", error); closeLoadingWindow(); app.exit(1); @@ -109,8 +109,8 @@ app.on("ready", () => { app.on("activate", async () => { if (BrowserWindow.getAllWindows().length === 0) { - mainWindow = createMainWindow({ runtimeConfig, rootPath, dashboardUrl }); - await mainWindow.loadURL(dashboardUrl); + mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl }); + await mainWindow.loadURL(mainDashboardUrl); mainWindow.show(); } }); diff --git a/src/main/nodecg/process-manager.ts b/src/main/nodecg/process-manager.ts index 6bc6326..a95e02d 100644 --- a/src/main/nodecg/process-manager.ts +++ b/src/main/nodecg/process-manager.ts @@ -7,9 +7,9 @@ import { NODE_RUNTIME_NAME } from "../constants"; type NodecgProcessManagerConfig = { isDev: boolean; - nodecgPath: string; - baseUrl: string; - runtimeConfig: AppRuntimeConfig; + nodecgRootPath: string; + nodecgBaseUrl: string; + appConfig: AppRuntimeConfig; log: (...args: unknown[]) => void; deps?: Partial; }; @@ -28,35 +28,35 @@ type NodecgProcessManagerDeps = { }; export type NodecgProcessManager = { - startNodeCG: () => ChildProcess; - waitForNodeCGReady: (startTime: number) => Promise; - stopNodeCG: () => Promise; + startNodecgProcess: () => ChildProcess; + waitForNodecgReady: (startTime: number) => Promise; + stopNodecgProcessGracefully: () => Promise; getProcess: () => ChildProcess | null; }; export function createNodecgProcessManager({ isDev, - nodecgPath, - baseUrl, - runtimeConfig, + nodecgRootPath, + nodecgBaseUrl, + appConfig, log, deps, }: NodecgProcessManagerConfig): NodecgProcessManager { const resolvedDeps = resolveDeps(deps); let nodecgProcess: ChildProcess | null = null; - let stopNodeCGPromise: Promise | null = null; + let stopNodecgPromise: Promise | null = null; - const startNodeCG = (): ChildProcess => { - validateNodeCGInstall(nodecgPath, runtimeConfig.bundleName, resolvedDeps.pathExists); + const startNodecgProcess = (): ChildProcess => { + validateNodecgInstall(nodecgRootPath, appConfig.bundleName, resolvedDeps.pathExists); - const indexPath = path.join(nodecgPath, "index.js"); + const indexPath = path.join(nodecgRootPath, "index.js"); const child = resolvedDeps.spawnProcess(resolvedDeps.execPath, [indexPath], { - cwd: nodecgPath, + cwd: nodecgRootPath, env: { ...resolvedDeps.env, NODE_ENV: isDev ? "development" : "production", - NODECG_PORT: runtimeConfig.nodecgPort, + NODECG_PORT: appConfig.nodecgPort, ELECTRON_RUN_AS_NODE: "1", }, stdio: ["ignore", "pipe", "pipe"], @@ -83,14 +83,14 @@ export function createNodecgProcessManager({ return child; }; - const waitForNodeCGReady = async (startTime: number): Promise => { - while (Date.now() - startTime < runtimeConfig.startupTimeoutMs) { + const waitForNodecgReady = async (startTime: number): Promise => { + while (Date.now() - startTime < appConfig.startupTimeoutMs) { if (!nodecgProcess) { throw new Error("NodeCG terminó antes de estar listo."); } try { - const response = await resolvedDeps.fetchUrl(baseUrl, { method: "GET" }); + const response = await resolvedDeps.fetchUrl(nodecgBaseUrl, { method: "GET" }); if (response.ok || response.status === 404) { return; } @@ -101,12 +101,12 @@ export function createNodecgProcessManager({ await sleep(500, resolvedDeps.setTimer); } - throw new Error(`Timeout esperando NodeCG en ${baseUrl} (${runtimeConfig.startupTimeoutMs}ms).`); + throw new Error(`Timeout esperando NodeCG en ${nodecgBaseUrl} (${appConfig.startupTimeoutMs}ms).`); }; - const stopNodeCG = (): Promise => { - if (stopNodeCGPromise) { - return stopNodeCGPromise; + const stopNodecgProcessGracefully = (): Promise => { + if (stopNodecgPromise) { + return stopNodecgPromise; } if (!nodecgProcess || nodecgProcess.killed) { @@ -122,14 +122,15 @@ export function createNodecgProcessManager({ } log(`Stopping NodeCG pid=${pid}`); - killNodeCGProcessTree(pid, "SIGTERM", log, resolvedDeps); + killNodecgProcessTree(pid, "SIGTERM", log, resolvedDeps); - stopNodeCGPromise = new Promise((resolve) => { + stopNodecgPromise = new Promise((resolve) => { const complete = () => { if (nodecgProcess === processToStop) { nodecgProcess = null; } - stopNodeCGPromise = null; + + stopNodecgPromise = null; resolve(); }; @@ -140,18 +141,18 @@ export function createNodecgProcessManager({ resolvedDeps.setTimer(() => { if (processToStop.exitCode === null && processToStop.signalCode === null) { log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`); - killNodeCGProcessTree(pid, "SIGKILL", log, resolvedDeps); + killNodecgProcessTree(pid, "SIGKILL", log, resolvedDeps); } - }, Math.max(0, runtimeConfig.nodecgKillTimeoutMs)); + }, Math.max(0, appConfig.nodecgKillTimeoutMs)); }); - return stopNodeCGPromise; + return stopNodecgPromise; }; return { - startNodeCG, - waitForNodeCGReady, - stopNodeCG, + startNodecgProcess, + waitForNodecgReady, + stopNodecgProcessGracefully, getProcess: () => nodecgProcess, }; } @@ -171,13 +172,13 @@ function resolveDeps(deps?: Partial): NodecgProcessMan }; } -function validateNodeCGInstall(nodecgPath: string, bundleName: string, pathExists: (candidatePath: string) => boolean): 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); +function validateNodecgInstall(nodecgRootPath: string, bundleName: string, pathExists: (candidatePath: string) => boolean): void { + const indexPath = path.join(nodecgRootPath, "index.js"); + const nodecgBootstrapPath = path.join(nodecgRootPath, "node_modules", "nodecg", "dist", "server", "bootstrap.js"); + const bundlePath = path.join(nodecgRootPath, "bundles", bundleName); - if (!pathExists(nodecgPath)) { - throw new Error(`No existe la carpeta NodeCG: ${nodecgPath}`); + if (!pathExists(nodecgRootPath)) { + throw new Error(`No existe la carpeta NodeCG: ${nodecgRootPath}`); } if (!pathExists(indexPath)) { @@ -206,7 +207,7 @@ function validateNodeCGInstall(nodecgPath: string, bundleName: string, pathExist } } -function killNodeCGProcessTree( +function killNodecgProcessTree( pid: number, signal: NodeJS.Signals, log: (...args: unknown[]) => void, diff --git a/src/main/windows/icon-path.ts b/src/main/windows/icon-path.ts index e537ccc..97e2db9 100644 --- a/src/main/windows/icon-path.ts +++ b/src/main/windows/icon-path.ts @@ -4,12 +4,12 @@ import path from "node:path"; import { AppRuntimeConfig } from "../config/runtime-config"; export function resolveAppIconPath( - runtimeConfig: AppRuntimeConfig, + appConfig: AppRuntimeConfig, rootPath: string, pathExists: (candidatePath: string) => boolean = fs.existsSync, ): string | undefined { const iconCandidates = [ - runtimeConfig.iconPathOverride, + appConfig.iconPathOverride, path.join(rootPath, "static", "icons", "icon.ico"), path.join(rootPath, "static", "icons", "icon.png"), path.join(rootPath, "static", "icon.ico"), diff --git a/src/main/windows/window-factory.ts b/src/main/windows/window-factory.ts index 4d38d6e..8731dc7 100644 --- a/src/main/windows/window-factory.ts +++ b/src/main/windows/window-factory.ts @@ -5,13 +5,13 @@ import { resolveAppIconPath } from "./icon-path"; import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "./navigation-security"; type WindowFactoryDependencies = { - runtimeConfig: AppRuntimeConfig; + appConfig: AppRuntimeConfig; rootPath: string; - dashboardUrl: string; + mainDashboardUrl: string; }; -export function createMainWindow({ runtimeConfig, rootPath, dashboardUrl }: WindowFactoryDependencies): BrowserWindow { - const windowOptions = createWindowOptions({ runtimeConfig, rootPath, isLoadingWindow: false }); +export function createMainWindow({ appConfig, rootPath, mainDashboardUrl }: WindowFactoryDependencies): BrowserWindow { + const windowOptions = createWindowOptions({ appConfig, rootPath, isLoadingWindow: false }); const window = new BrowserWindow(windowOptions); window.setMenuBarVisibility(false); @@ -25,7 +25,7 @@ export function createMainWindow({ runtimeConfig, rootPath, dashboardUrl }: Wind }); window.webContents.on("will-navigate", (event, url) => { - if (shouldAllowInternalNavigation(url, dashboardUrl)) { + if (shouldAllowInternalNavigation(url, mainDashboardUrl)) { return; } @@ -43,8 +43,8 @@ export function createMainWindow({ runtimeConfig, rootPath, dashboardUrl }: Wind return window; } -export function createLoadingWindow({ runtimeConfig, rootPath }: Omit): BrowserWindow { - const window = new BrowserWindow(createWindowOptions({ runtimeConfig, rootPath, isLoadingWindow: true })); +export function createLoadingWindow({ appConfig, rootPath }: Omit): BrowserWindow { + const window = new BrowserWindow(createWindowOptions({ appConfig, rootPath, isLoadingWindow: true })); window.on("page-title-updated", (event) => { event.preventDefault(); @@ -54,19 +54,19 @@ export function createLoadingWindow({ runtimeConfig, rootPath }: Omit { - const runtimeConfig: AppRuntimeConfig = { + const appConfig: AppRuntimeConfig = { ...getBaseConfig(), iconPathOverride: "/custom/icon.ico", }; - const iconPath = resolveAppIconPath(runtimeConfig, "/app", (candidate) => candidate === "/custom/icon.ico"); + const iconPath = resolveAppIconPath(appConfig, "/app", (candidate) => candidate === "/custom/icon.ico"); assert.equal(iconPath, "/custom/icon.ico"); }); test("resolveAppIconPath cae al primer icono por defecto existente", () => { - const runtimeConfig = getBaseConfig(); + const appConfig = getBaseConfig(); - const iconPath = resolveAppIconPath(runtimeConfig, "/app", (candidate) => candidate === "/app/static/icons/icon.png"); + const iconPath = resolveAppIconPath(appConfig, "/app", (candidate) => candidate === "/app/static/icons/icon.png"); assert.equal(iconPath, "/app/static/icons/icon.png"); }); test("resolveAppIconPath devuelve undefined cuando no hay iconos", () => { - const runtimeConfig = getBaseConfig(); + const appConfig = getBaseConfig(); - const iconPath = resolveAppIconPath(runtimeConfig, "/app", () => false); + const iconPath = resolveAppIconPath(appConfig, "/app", () => false); assert.equal(iconPath, undefined); }); diff --git a/src/tests/process-manager.test.ts b/src/tests/process-manager.test.ts index 12595de..5bf127b 100644 --- a/src/tests/process-manager.test.ts +++ b/src/tests/process-manager.test.ts @@ -23,8 +23,8 @@ function getBaseConfig(): AppRuntimeConfig { userModelId: "com.scoreko.desktop", nodecgPort: "9090", bundleName: "scoreko-dev", - dashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true", - loadingRoute: "dashboard/loading/main.html?standalone=true", + mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true", + loadingDashboardRoute: "dashboard/loading/main.html?standalone=true", loadDelayMs: 10000, startupTimeoutMs: 100, nodecgKillTimeoutMs: 10, @@ -34,9 +34,9 @@ function getBaseConfig(): AppRuntimeConfig { test("startNodeCG valida instalación de NodeCG antes de arrancar", () => { const manager = createNodecgProcessManager({ isDev: true, - nodecgPath: "/fake/nodecg", - baseUrl: "http://127.0.0.1:9090", - runtimeConfig: getBaseConfig(), + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), log: () => undefined, deps: { pathExists: () => false, @@ -47,7 +47,7 @@ test("startNodeCG valida instalación de NodeCG antes de arrancar", () => { }); assert.throws(() => { - manager.startNodeCG(); + manager.startNodecgProcess(); }, /No existe la carpeta NodeCG/); }); @@ -55,9 +55,9 @@ test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () => const child = new MockChildProcess(4321); const manager = createNodecgProcessManager({ isDev: true, - nodecgPath: "/fake/nodecg", - baseUrl: "http://127.0.0.1:9090", - runtimeConfig: getBaseConfig(), + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), log: () => undefined, deps: { pathExists: () => true, @@ -72,9 +72,9 @@ test("waitForNodeCGReady resuelve cuando el endpoint responde 404", async () => }, }); - manager.startNodeCG(); + manager.startNodecgProcess(); await assert.doesNotReject(async () => { - await manager.waitForNodeCGReady(Date.now()); + await manager.waitForNodecgReady(Date.now()); }); }); @@ -85,9 +85,9 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async () const manager = createNodecgProcessManager({ isDev: true, - nodecgPath: "/fake/nodecg", - baseUrl: "http://127.0.0.1:9090", - runtimeConfig: getBaseConfig(), + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), log: () => undefined, deps: { pathExists: () => true, @@ -105,8 +105,8 @@ test("stopNodeCG envía SIGTERM y luego SIGKILL si el proceso no sale", async () }, }); - manager.startNodeCG(); - const stopPromise = manager.stopNodeCG(); + manager.startNodecgProcess(); + const stopPromise = manager.stopNodecgProcessGracefully(); assert.deepEqual(killSignals, [{ pid: -9999, signal: "SIGTERM" }]); @@ -129,9 +129,9 @@ test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async const manager = createNodecgProcessManager({ isDev: true, - nodecgPath: "/fake/nodecg", - baseUrl: "http://127.0.0.1:9090", - runtimeConfig: getBaseConfig(), + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: getBaseConfig(), log: () => undefined, deps: { pathExists: () => true, @@ -144,9 +144,9 @@ test("stopNodeCG reutiliza la misma promesa cuando se invoca en paralelo", async }, }); - manager.startNodeCG(); - const firstStop = manager.stopNodeCG(); - const secondStop = manager.stopNodeCG(); + manager.startNodecgProcess(); + const firstStop = manager.stopNodecgProcessGracefully(); + const secondStop = manager.stopNodecgProcessGracefully(); assert.equal(firstStop, secondStop); @@ -160,9 +160,9 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => { const manager = createNodecgProcessManager({ isDev: true, - nodecgPath: "/fake/nodecg", - baseUrl: "http://127.0.0.1:9090", - runtimeConfig: { + nodecgRootPath: "/fake/nodecg", + nodecgBaseUrl: "http://127.0.0.1:9090", + appConfig: { ...getBaseConfig(), nodecgKillTimeoutMs: -10, }, @@ -182,8 +182,8 @@ test("stopNodeCG normaliza timeout negativo a cero", async () => { }, }); - manager.startNodeCG(); - const stopPromise = manager.stopNodeCG(); + manager.startNodecgProcess(); + const stopPromise = manager.stopNodecgProcessGracefully(); assert.ok(timeouts.includes(0));