mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
refactor: run NodeCG from lib/scoreko-dev for Node 24 migration
This commit is contained in:
@@ -20,7 +20,6 @@ type NodecgProcessManagerDeps = {
|
||||
pathExists: (candidatePath: string) => boolean;
|
||||
fetchUrl: typeof fetch;
|
||||
platform: NodeJS.Platform;
|
||||
execPath: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
killProcess: (pid: number, signal: NodeJS.Signals) => void;
|
||||
setTimer: (handler: () => void, timeoutMs: number) => unknown;
|
||||
@@ -53,7 +52,6 @@ export function createNodecgProcessManager({
|
||||
let lastStderrLine: string | null = null;
|
||||
|
||||
const startNodecgProcess = async (): Promise<ChildProcess> => {
|
||||
// Fail fast with actionable errors before spawning child processes.
|
||||
validateNodecgInstall(
|
||||
nodecgRootPath,
|
||||
appConfig.bundleName,
|
||||
@@ -69,18 +67,17 @@ export function createNodecgProcessManager({
|
||||
);
|
||||
}
|
||||
|
||||
const indexPath = path.join(nodecgRootPath, "index.js");
|
||||
const child = resolvedDeps.spawnProcess(resolvedDeps.execPath, [indexPath], {
|
||||
const command = resolvedDeps.platform === "win32" ? "npx.cmd" : "npx";
|
||||
const child = resolvedDeps.spawnProcess(command, ["nodecg", "start"], {
|
||||
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: resolvedDeps.platform === "win32",
|
||||
shell: false,
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
@@ -108,7 +105,6 @@ export function createNodecgProcessManager({
|
||||
};
|
||||
|
||||
const waitForNodecgReady = async (startTime: number): Promise<void> => {
|
||||
// Poll the local NodeCG URL until it answers or we hit the configured timeout.
|
||||
while (Date.now() - startTime < appConfig.startupTimeoutMs) {
|
||||
if (!nodecgProcess) {
|
||||
const exitDetails = lastExit
|
||||
@@ -121,7 +117,7 @@ export function createNodecgProcessManager({
|
||||
exitDetails,
|
||||
stderrDetails,
|
||||
`NodeCG path: ${nodecgRootPath}`,
|
||||
"Check that lib/nodecg dependencies are installed and the bundle exists.",
|
||||
"Check that lib/scoreko-dev dependencies are installed and the bundle exists.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
@@ -142,7 +138,6 @@ export function createNodecgProcessManager({
|
||||
};
|
||||
|
||||
const stopNodecgProcessGracefully = (): Promise<void> => {
|
||||
// Reuse the same stop promise to avoid sending multiple kill signals during app shutdown.
|
||||
if (stopNodecgPromise) {
|
||||
return stopNodecgPromise;
|
||||
}
|
||||
@@ -204,7 +199,6 @@ function resolveDeps(deps?: Partial<NodecgProcessManagerDeps>): NodecgProcessMan
|
||||
pathExists: deps?.pathExists ?? fs.existsSync,
|
||||
fetchUrl: deps?.fetchUrl ?? fetch,
|
||||
platform: deps?.platform ?? process.platform,
|
||||
execPath: deps?.execPath ?? process.execPath,
|
||||
env: deps?.env ?? process.env,
|
||||
killProcess: deps?.killProcess ?? process.kill,
|
||||
setTimer: deps?.setTimer ?? setTimeout,
|
||||
@@ -221,39 +215,46 @@ function validateNodecgInstall(
|
||||
pathExists: (candidatePath: string) => boolean,
|
||||
hasReadWriteAccessToPath: (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);
|
||||
const packageJsonPath = path.join(nodecgRootPath, "package.json");
|
||||
const nodecgCliPath = path.join(
|
||||
nodecgRootPath,
|
||||
"node_modules",
|
||||
".bin",
|
||||
process.platform === "win32" ? "nodecg.cmd" : "nodecg",
|
||||
);
|
||||
const bundleAssetDirs = ["dashboard", "graphics", "extension", "extensions"].map((dir) =>
|
||||
path.join(nodecgRootPath, dir),
|
||||
);
|
||||
|
||||
if (!pathExists(nodecgRootPath)) {
|
||||
throw new Error(`NodeCG folder does not exist: ${nodecgRootPath}`);
|
||||
throw new Error(`Scoreko app folder does not exist: ${nodecgRootPath}`);
|
||||
}
|
||||
|
||||
if (!hasReadWriteAccessToPath(nodecgRootPath)) {
|
||||
throw new Error(`No read/write permissions on NodeCG: ${nodecgRootPath}`);
|
||||
throw new Error(`No read/write permissions on scoreko app folder: ${nodecgRootPath}`);
|
||||
}
|
||||
|
||||
if (!pathExists(indexPath)) {
|
||||
throw new Error(`${indexPath} was not found. Copy a full NodeCG installation into lib/nodecg.`);
|
||||
if (!pathExists(packageJsonPath)) {
|
||||
throw new Error(`${packageJsonPath} was not found. Expected a NodeCG bundle app at lib/scoreko-dev.`);
|
||||
}
|
||||
|
||||
if (!pathExists(nodecgBootstrapPath)) {
|
||||
if (!pathExists(nodecgCliPath)) {
|
||||
throw new Error(
|
||||
[
|
||||
"NodeCG is present but internal dependencies are missing.",
|
||||
`Not found: ${nodecgBootstrapPath}`,
|
||||
"Solution: enter lib/nodecg and install dependencies:",
|
||||
"NodeCG dependency is missing in lib/scoreko-dev.",
|
||||
`Not found: ${nodecgCliPath}`,
|
||||
"Solution: enter lib/scoreko-dev and install dependencies:",
|
||||
" npm install",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
if (!pathExists(bundlePath)) {
|
||||
if (!bundleAssetDirs.some((candidatePath) => pathExists(candidatePath))) {
|
||||
throw new Error(
|
||||
[
|
||||
`Bundle '${bundleName}' was not found.`,
|
||||
`Expected path: ${bundlePath}`,
|
||||
"Copy/clone your bundle inside lib/nodecg/bundles before running Electron.",
|
||||
`Bundle '${bundleName}' appears incomplete.`,
|
||||
`Expected one of: ${bundleAssetDirs.join(", ")}`,
|
||||
"Ensure extensions/dashboard/graphics assets exist inside lib/scoreko-dev.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
@@ -270,76 +271,49 @@ function hasReadWriteAccess(candidatePath: string): boolean {
|
||||
|
||||
function probePortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// A successful TCP connection means some process is already listening on the port.
|
||||
const socket = net.createConnection({ host: "127.0.0.1", port });
|
||||
let resolved = false;
|
||||
const server = net.createServer();
|
||||
|
||||
const complete = (isAvailable: boolean): void => {
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
resolved = true;
|
||||
socket.destroy();
|
||||
resolve(isAvailable);
|
||||
};
|
||||
|
||||
socket.setTimeout(1000);
|
||||
|
||||
socket.once("connect", () => {
|
||||
complete(false);
|
||||
server.once("error", () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.once("timeout", () => {
|
||||
complete(true);
|
||||
server.once("listening", () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
|
||||
socket.once("error", (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === "ECONNREFUSED" || error.code === "EHOSTUNREACH") {
|
||||
complete(true);
|
||||
return;
|
||||
}
|
||||
|
||||
complete(false);
|
||||
});
|
||||
server.listen(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
|
||||
function killNodecgProcessTree(
|
||||
pid: number,
|
||||
signal: NodeJS.Signals,
|
||||
log: (...args: unknown[]) => void,
|
||||
deps: Pick<NodecgProcessManagerDeps, "platform" | "spawnProcess" | "killProcess">,
|
||||
): boolean {
|
||||
if (deps.platform === "win32") {
|
||||
const force = signal === "SIGKILL" ? "/F" : "";
|
||||
const killer = deps.spawnProcess("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 {
|
||||
deps.killProcess(-pid, signal);
|
||||
return true;
|
||||
} catch {
|
||||
try {
|
||||
deps.killProcess(pid, signal);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number, setTimer: (handler: () => void, timeoutMs: number) => unknown): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimer(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
function killNodecgProcessTree(
|
||||
pid: number,
|
||||
signal: NodeJS.Signals,
|
||||
log: (...args: unknown[]) => void,
|
||||
deps: Pick<NodecgProcessManagerDeps, "platform" | "killProcess">,
|
||||
): void {
|
||||
if (deps.platform === "win32") {
|
||||
try {
|
||||
deps.killProcess(pid, signal);
|
||||
} catch (error) {
|
||||
log(`Error sending ${signal} to pid=${pid}`, error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deps.killProcess(-pid, signal);
|
||||
} catch {
|
||||
try {
|
||||
deps.killProcess(pid, signal);
|
||||
} catch (error) {
|
||||
log(`Error sending ${signal} to pid=${pid}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user