Merge pull request #41 from Pandipipas/add-lightweight-implementation-ideas

Expose and run preflight checks for NodeCG process manager; add sanity script and test
This commit is contained in:
Pandipipas
2026-03-03 00:08:11 +01:00
committed by GitHub
5 changed files with 37 additions and 1 deletions
+1
View File
@@ -30,6 +30,7 @@ Desktop app (Electron + TypeScript) to run and package `scoreko-dev` (Electron h
### Quality and diagnostics ### Quality and diagnostics
- `npm run test`: build and tests (`node:test`). - `npm run test`: build and tests (`node:test`).
- `npm run sanity`: runs `typecheck`, `lint`, and `test` as a quick pre-release gate.
- `npm run doctor`: environment/configuration diagnostics. - `npm run doctor`: environment/configuration diagnostics.
- `npm run lint`: lint with ESLint. - `npm run lint`: lint with ESLint.
- `npm run lint:fix`: lint with auto-fix. - `npm run lint:fix`: lint with auto-fix.
+1
View File
@@ -21,6 +21,7 @@
"rebuild:native": "node scripts/rebuild-nodecg-native.mjs", "rebuild:native": "node scripts/rebuild-nodecg-native.mjs",
"rebuild:better-sqlite3": "electron-rebuild --version 40.6.1 --module-dir lib/scoreko-dev/workspaces/database-adapter-sqlite-legacy --only better-sqlite3 -f", "rebuild:better-sqlite3": "electron-rebuild --version 40.6.1 --module-dir lib/scoreko-dev/workspaces/database-adapter-sqlite-legacy --only better-sqlite3 -f",
"test": "npm run build && node --test dist/tests/**/*.test.js", "test": "npm run build && node --test dist/tests/**/*.test.js",
"sanity": "npm run typecheck && npm run lint && npm run test",
"doctor": "node scripts/doctor.mjs", "doctor": "node scripts/doctor.mjs",
"lint": "eslint . --ext .ts,.js,.mjs", "lint": "eslint . --ext .ts,.js,.mjs",
"lint:fix": "npm run lint -- --fix", "lint:fix": "npm run lint -- --fix",
+2
View File
@@ -56,6 +56,8 @@ function focusExistingWindow(): void {
} }
async function launchApplication(): Promise<void> { async function launchApplication(): Promise<void> {
await nodecgManager.runPreflightChecks();
// We create both windows early so startup feels instant while NodeCG is booting in the background. // We create both windows early so startup feels instant while NodeCG is booting in the background.
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl }); mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
loadingWindow = createLoadingWindow({ appConfig, rootPath }); loadingWindow = createLoadingWindow({ appConfig, rootPath });
+7 -1
View File
@@ -30,6 +30,7 @@ type NodecgProcessManagerDeps = {
}; };
export type NodecgProcessManager = { export type NodecgProcessManager = {
runPreflightChecks: () => Promise<void>;
startNodecgProcess: () => Promise<ChildProcess>; startNodecgProcess: () => Promise<ChildProcess>;
waitForNodecgReady: (startTime: number) => Promise<void>; waitForNodecgReady: (startTime: number) => Promise<void>;
stopNodecgProcessGracefully: () => Promise<void>; stopNodecgProcessGracefully: () => Promise<void>;
@@ -51,7 +52,7 @@ export function createNodecgProcessManager({
let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null; let lastExit: { code: number | null; signal: NodeJS.Signals | null } | null = null;
let lastStderrLine: string | null = null; let lastStderrLine: string | null = null;
const startNodecgProcess = async (): Promise<ChildProcess> => { const runPreflightChecks = async (): Promise<void> => {
validateNodecgInstall( validateNodecgInstall(
nodecgRootPath, nodecgRootPath,
resolvedDeps.platform, resolvedDeps.platform,
@@ -66,6 +67,10 @@ export function createNodecgProcessManager({
`Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`, `Port ${appConfig.nodecgPort} is already in use. Stop the process using it or set NODECG_PORT before starting.`,
); );
} }
};
const startNodecgProcess = async (): Promise<ChildProcess> => {
await runPreflightChecks();
const command = resolvedDeps.platform === "win32" ? "npx.cmd" : "npx"; const command = resolvedDeps.platform === "win32" ? "npx.cmd" : "npx";
const child = resolvedDeps.spawnProcess(command, ["nodecg", "start"], { const child = resolvedDeps.spawnProcess(command, ["nodecg", "start"], {
@@ -186,6 +191,7 @@ export function createNodecgProcessManager({
}; };
return { return {
runPreflightChecks,
startNodecgProcess, startNodecgProcess,
waitForNodecgReady, waitForNodecgReady,
stopNodecgProcessGracefully, stopNodecgProcessGracefully,
+26
View File
@@ -70,6 +70,32 @@ test("startNodeCG fails when there are no read/write permissions", async () => {
}, /No read\/write permissions on scoreko app folder/); }, /No read\/write permissions on scoreko app folder/);
}); });
test("runPreflightChecks validates install and port availability without starting NodeCG", async () => {
let spawnCalls = 0;
const manager = createNodecgProcessManager({
isDev: true,
nodecgRootPath: "/fake/scoreko-dev",
nodecgBaseUrl: "http://127.0.0.1:9090",
appConfig: getBaseConfig(),
log: () => undefined,
deps: {
pathExists: () => true,
hasReadWriteAccess: () => true,
probePortAvailable: async () => true,
spawnProcess: () => {
spawnCalls += 1;
throw new Error("should not spawn during preflight");
},
},
});
await assert.doesNotReject(async () => {
await manager.runPreflightChecks();
});
assert.equal(spawnCalls, 0);
});
test("waitForNodeCGReady resolves when endpoint returns 404", async () => { test("waitForNodeCGReady resolves when endpoint returns 404", async () => {
const child = new MockChildProcess(4321); const child = new MockChildProcess(4321);
const manager = createNodecgProcessManager({ const manager = createNodecgProcessManager({