diff --git a/src/main/main.ts b/src/main/main.ts index b4e5e83..ac349c6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -25,15 +25,17 @@ let mainWindow: BrowserWindow | null = null; let loadingWindow: BrowserWindow | null = null; let nodecgProcess: ChildProcess | null = null; let lastNodeCGOutput = ""; +let stopNodeCGPromise: Promise | null = null; +let isQuitting = false; function createMainWindow(): BrowserWindow { const win = new BrowserWindow({ show: false, title: APP_TITLE, - width: 1440, - height: 900, - minWidth: 960, - minHeight: 640, + width: 1280, + height: 800, + minWidth: 800, + minHeight: 500, backgroundColor: "#0f0f0f", webPreferences: { contextIsolation: true, @@ -73,8 +75,8 @@ function createLoadingWindow(): BrowserWindow { show: false, frame: false, title: APP_TITLE, - width: 420, - height: 280, + width: 300, + height: 300, resizable: false, movable: true, minimizable: false, @@ -180,6 +182,7 @@ function startNodeCG(): ChildProcess { ...(USE_SYSTEM_NODE ? {} : { ELECTRON_RUN_AS_NODE: "1" }), }, stdio: ["ignore", "pipe", "pipe"], + detached: process.platform !== "win32", shell: process.platform === "win32", }); @@ -277,23 +280,76 @@ async function launch(): Promise { } } -function stopNodeCG(): void { +function killNodeCGProcessTree(pid: number, signal: NodeJS.Signals): 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 stopNodeCG(): Promise { + if (stopNodeCGPromise) { + return stopNodeCGPromise; + } + if (!nodecgProcess || nodecgProcess.killed) { - return; + return Promise.resolve(); } const processToStop = nodecgProcess; const pid = processToStop.pid; - log(`Stopping NodeCG pid=${pid}`); - processToStop.kill("SIGTERM"); + if (typeof pid !== "number") { + log("NodeCG pid unavailable, skipping graceful stop"); + return Promise.resolve(); + } - setTimeout(() => { - if (processToStop.exitCode === null && processToStop.signalCode === null) { - log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`); - processToStop.kill("SIGKILL"); - } - }, Math.max(0, NODECG_KILL_TIMEOUT_MS)); + log(`Stopping NodeCG pid=${pid}`); + killNodeCGProcessTree(pid, "SIGTERM"); + + 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"); + } + }, Math.max(0, NODECG_KILL_TIMEOUT_MS)); + }); + + return stopNodeCGPromise; } function log(...args: unknown[]): void { @@ -331,8 +387,17 @@ app.on("window-all-closed", () => { } }); -app.on("before-quit", () => { - stopNodeCG(); +app.on("before-quit", (event) => { + if (isQuitting) { + return; + } + + event.preventDefault(); + isQuitting = true; + + stopNodeCG().finally(() => { + app.quit(); + }); }); app.on("will-quit", () => {