mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: Enhance NodeCG process management and add IPC security tests
This commit is contained in:
@@ -32,12 +32,14 @@ type NodecgProcessManagerDeps = {
|
||||
};
|
||||
|
||||
export type NodecgProcessManager = {
|
||||
startNodecgProcess: () => Promise<ChildProcess>;
|
||||
startNodecgProcess: () => Promise<void>;
|
||||
waitForNodecgReady: (startTime: number) => Promise<void>;
|
||||
stopNodecgProcessGracefully: () => Promise<void>;
|
||||
getProcess: () => ChildProcess | null;
|
||||
getState: () => NodecgProcessState;
|
||||
};
|
||||
|
||||
export type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
||||
|
||||
export function createNodecgProcessManager({
|
||||
isDev,
|
||||
nodecgRootPath,
|
||||
@@ -49,64 +51,97 @@ export function createNodecgProcessManager({
|
||||
const resolvedDeps = resolveDeps(deps);
|
||||
|
||||
let nodecgProcess: ChildProcess | null = null;
|
||||
let nodecgState: NodecgProcessState = "idle";
|
||||
let startNodecgPromise: Promise<void> | null = null;
|
||||
let stopNodecgPromise: Promise<void> | null = null;
|
||||
let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
|
||||
let lastStderrLine: string | null = null;
|
||||
|
||||
const startNodecgProcess = async (): Promise<ChildProcess> => {
|
||||
// Fail fast with actionable errors before spawning child processes.
|
||||
validateNodecgInstall(
|
||||
nodecgRootPath,
|
||||
appConfig.bundleName,
|
||||
resolvedDeps.pathExists,
|
||||
resolvedDeps.hasReadWriteAccess,
|
||||
);
|
||||
|
||||
const portAsNumber = Number.parseInt(appConfig.nodecgPort, 10);
|
||||
const isPortAvailable = await resolvedDeps.probePortAvailable(portAsNumber);
|
||||
if (!isPortAvailable) {
|
||||
throw new Error(
|
||||
`Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`,
|
||||
);
|
||||
const startNodecgProcess = (): Promise<void> => {
|
||||
if (nodecgProcess && nodecgState === "running") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const indexPath = path.join(nodecgRootPath, "index.js");
|
||||
const child = resolvedDeps.spawnProcess(resolvedDeps.execPath, [indexPath], {
|
||||
cwd: nodecgRootPath,
|
||||
env: {
|
||||
...resolvedDeps.env,
|
||||
NODE_ENV: isDev ? "development" : "production",
|
||||
NODECG_PORT: appConfig.nodecgPort,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: resolvedDeps.platform !== "win32",
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
if (startNodecgPromise) {
|
||||
return startNodecgPromise;
|
||||
}
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
resolvedDeps.stdoutWrite(String(chunk));
|
||||
});
|
||||
if (nodecgState === "stopping") {
|
||||
return Promise.reject(new Error("Cannot start NodeCG while shutdown is in progress."));
|
||||
}
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
const line = String(chunk);
|
||||
lastStderrLine = line.trim().length > 0 ? line.trim() : lastStderrLine;
|
||||
resolvedDeps.stderrWrite(line);
|
||||
});
|
||||
nodecgState = "starting";
|
||||
startNodecgPromise = (async () => {
|
||||
// Fail fast with actionable errors before spawning child processes.
|
||||
validateNodecgInstall(
|
||||
nodecgRootPath,
|
||||
appConfig.bundleName,
|
||||
resolvedDeps.pathExists,
|
||||
resolvedDeps.hasReadWriteAccess,
|
||||
);
|
||||
|
||||
log(`NodeCG started with pid=${child.pid} using ${NODE_RUNTIME_NAME}`);
|
||||
const portAsNumber = Number.parseInt(appConfig.nodecgPort, 10);
|
||||
const isPortAvailable = await resolvedDeps.probePortAvailable(portAsNumber);
|
||||
if (!isPortAvailable) {
|
||||
throw new Error(
|
||||
`Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`,
|
||||
);
|
||||
}
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||
lastExit = { code, signal };
|
||||
nodecgProcess = null;
|
||||
});
|
||||
const indexPath = path.join(nodecgRootPath, "index.js");
|
||||
const child = resolvedDeps.spawnProcess(resolvedDeps.execPath, [indexPath], {
|
||||
cwd: nodecgRootPath,
|
||||
env: {
|
||||
...resolvedDeps.env,
|
||||
NODE_ENV: isDev ? "development" : "production",
|
||||
NODECG_PORT: appConfig.nodecgPort,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: resolvedDeps.platform !== "win32",
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
lastExit = null;
|
||||
lastStderrLine = null;
|
||||
nodecgProcess = child;
|
||||
return child;
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
resolvedDeps.stdoutWrite(String(chunk));
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
const line = String(chunk);
|
||||
lastStderrLine = line.trim().length > 0 ? line.trim() : lastStderrLine;
|
||||
resolvedDeps.stderrWrite(line);
|
||||
});
|
||||
|
||||
log(`NodeCG started with pid=${child.pid} using ${NODE_RUNTIME_NAME}`);
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
log(`NodeCG exited code=${code} signal=${signal ?? "none"}`);
|
||||
lastExit = { code, signal };
|
||||
|
||||
if (nodecgProcess === child) {
|
||||
nodecgProcess = null;
|
||||
}
|
||||
|
||||
if (nodecgState !== "stopping") {
|
||||
nodecgState = code === 0 ? "stopped" : "failed";
|
||||
}
|
||||
});
|
||||
|
||||
lastExit = null;
|
||||
lastStderrLine = null;
|
||||
nodecgProcess = child;
|
||||
nodecgState = "running";
|
||||
})()
|
||||
.catch((error: unknown) => {
|
||||
nodecgState = "failed";
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
startNodecgPromise = null;
|
||||
});
|
||||
|
||||
return startNodecgPromise;
|
||||
};
|
||||
|
||||
const waitForNodecgReady = async (startTime: number): Promise<void> => {
|
||||
@@ -150,6 +185,7 @@ export function createNodecgProcessManager({
|
||||
}
|
||||
|
||||
if (!nodecgProcess || nodecgProcess.killed) {
|
||||
nodecgState = "stopped";
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -158,9 +194,12 @@ export function createNodecgProcessManager({
|
||||
|
||||
if (typeof pid !== "number") {
|
||||
log("NodeCG pid unavailable, skipping graceful stop");
|
||||
nodecgProcess = null;
|
||||
nodecgState = "stopped";
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
nodecgState = "stopping";
|
||||
log(`Stopping NodeCG pid=${pid}`);
|
||||
killProcessTree(pid, "SIGTERM", {
|
||||
platform: resolvedDeps.platform,
|
||||
@@ -183,6 +222,7 @@ export function createNodecgProcessManager({
|
||||
nodecgProcess = null;
|
||||
}
|
||||
|
||||
nodecgState = "stopped";
|
||||
stopNodecgPromise = null;
|
||||
resolve();
|
||||
};
|
||||
@@ -215,7 +255,7 @@ export function createNodecgProcessManager({
|
||||
startNodecgProcess,
|
||||
waitForNodecgReady,
|
||||
stopNodecgProcessGracefully,
|
||||
getProcess: () => nodecgProcess,
|
||||
getState: () => nodecgState,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user