Investigating Electron Startup Failures

This commit is contained in:
2026-05-31 16:24:14 +02:00
parent 33665ed896
commit 42a298925b
5 changed files with 28 additions and 27 deletions
+4 -5
View File
@@ -5,11 +5,10 @@
1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`. 1. `src/main/main.ts` loads `appConfig` from `config/runtime-config.ts`.
2. Installs or refreshes the packaged NodeCG runtime in user data when needed (`nodecg/runtime-provisioner.ts`). 2. Installs or refreshes the packaged NodeCG runtime in user data when needed (`nodecg/runtime-provisioner.ts`).
3. Creates windows (`windows/window-factory.ts`). 3. Creates windows (`windows/window-factory.ts`).
4. In packaged builds, relaunches once after a fresh runtime install so NodeCG starts from a settled user-data runtime. 4. Starts NodeCG with `nodecg/process-manager.ts`.
5. Starts NodeCG with `nodecg/process-manager.ts`. 5. Waits for HTTP readiness and shows loading -> main dashboard.
6. Waits for HTTP readiness and shows loading -> main dashboard. 6. Checks the configured Gitea latest-release endpoint for optional updates.
7. Checks the configured Gitea latest-release endpoint for optional updates. 7. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
8. On shutdown, runs a single graceful-stop flow to avoid orphan processes.
## Main modules ## Main modules
+1 -1
View File
@@ -74,7 +74,7 @@
], ],
"icon": "static/icons/icon.ico", "icon": "static/icons/icon.ico",
"executableName": "scoreko", "executableName": "scoreko",
"signAndEditExecutable": false "signAndEditExecutable": true
}, },
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
-8
View File
@@ -36,7 +36,6 @@ export type ApplicationControllerConfig = {
bundleName: string; bundleName: string;
log: (...args: unknown[]) => void; log: (...args: unknown[]) => void;
}) => PreparedNodecgRuntime; }) => PreparedNodecgRuntime;
relaunch: () => void;
scheduleUpdateCheck: (config: { scheduleUpdateCheck: (config: {
getParentWindow: () => ApplicationWindow | null; getParentWindow: () => ApplicationWindow | null;
beforeInstall: () => Promise<void>; beforeInstall: () => Promise<void>;
@@ -129,13 +128,6 @@ export function createApplicationController({
log: deps.log, log: deps.log,
}); });
if (preparedRuntime.installed && isPackaged) {
deps.log("Runtime was installed or refreshed; relaunching Scoreko before starting NodeCG.");
deps.relaunch();
deps.exit(0);
state = "stopped";
return;
}
nodecgManager = deps.createNodecgProcessManager(preparedRuntime.runtimePath); nodecgManager = deps.createNodecgProcessManager(preparedRuntime.runtimePath);
-1
View File
@@ -61,7 +61,6 @@ export function bootstrap(): void {
getAllWindows: () => BrowserWindow.getAllWindows(), getAllWindows: () => BrowserWindow.getAllWindows(),
log, log,
prepareRuntime: prepareUserNodecgRuntime, prepareRuntime: prepareUserNodecgRuntime,
relaunch: () => app.relaunch(),
scheduleUpdateCheck: ({ getParentWindow, beforeInstall }) => { scheduleUpdateCheck: ({ getParentWindow, beforeInstall }) => {
scheduleUpdateCheck({ scheduleUpdateCheck({
appConfig, appConfig,
+23 -12
View File
@@ -114,7 +114,6 @@ test("ApplicationController preserves startup ordering and schedules updates aft
events.push("prepare-runtime"); events.push("prepare-runtime");
return { runtimePath: "/user-data/scoreko/nodecg", installed: false }; return { runtimePath: "/user-data/scoreko/nodecg", installed: false };
}, },
relaunch: () => events.push("relaunch"),
scheduleUpdateCheck: () => events.push("schedule-update"), scheduleUpdateCheck: () => events.push("schedule-update"),
setAppUserModelId: () => events.push("set-app-user-model-id"), setAppUserModelId: () => events.push("set-app-user-model-id"),
exit: (code) => events.push(`exit:${code}`), exit: (code) => events.push(`exit:${code}`),
@@ -145,7 +144,7 @@ test("ApplicationController preserves startup ordering and schedules updates aft
]); ]);
}); });
test("ApplicationController relaunches packaged app after runtime install before starting NodeCG", async () => { test("ApplicationController directly launches packaged app after runtime install without relaunching", async () => {
const events: string[] = []; const events: string[] = [];
const controller = createApplicationController({ const controller = createApplicationController({
appConfig: getBaseConfig(), appConfig: getBaseConfig(),
@@ -162,31 +161,45 @@ test("ApplicationController relaunches packaged app after runtime install before
}, },
deps: { deps: {
createLoadingWindow: () => { createLoadingWindow: () => {
throw new Error("window creation should wait until after relaunch decisions"); events.push("create-loading");
return new MockWindow("loading", events);
}, },
createMainWindow: () => { createMainWindow: () => {
throw new Error("window creation should wait until after relaunch decisions"); events.push("create-main");
return new MockWindow("main", events);
}, },
createNodecgProcessManager: () => { createNodecgProcessManager: () => {
throw new Error("NodeCG should not start before relaunch"); events.push("create-manager");
return createMockManager(events);
}, },
getAllWindows: () => [], getAllWindows: () => [],
log: (...args) => events.push(String(args[0])), log: (...args) => events.push(String(args[0])),
prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: true }), prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: true }),
relaunch: () => events.push("relaunch"),
scheduleUpdateCheck: () => events.push("schedule-update"), scheduleUpdateCheck: () => events.push("schedule-update"),
setAppUserModelId: () => events.push("set-app-user-model-id"), setAppUserModelId: () => events.push("set-app-user-model-id"),
exit: (code) => events.push(`exit:${code}`), exit: (code) => events.push(`exit:${code}`),
now: () => 0,
sleep: async (ms) => {
events.push(`sleep:${ms}`);
},
}, },
}); });
await controller.launch(); await controller.launch();
assert.equal(controller.getState(), "stopped"); assert.equal(controller.getState(), "ready");
assert.deepEqual(events, [ assert.deepEqual(events, [
"Runtime was installed or refreshed; relaunching Scoreko before starting NodeCG.", "create-manager",
"relaunch", "create-main",
"exit:0", "create-loading",
"start-nodecg",
"wait-nodecg",
"loading:load:http://localhost:9090/loading",
"loading:show",
"main:load:http://localhost:9090/main",
"main:show",
"loading:close",
"schedule-update",
]); ]);
}); });
@@ -215,7 +228,6 @@ test("ApplicationController activation before readiness routes through launch",
events.push("prepare-runtime"); events.push("prepare-runtime");
return { runtimePath: "/user-data/scoreko/nodecg", installed: false }; return { runtimePath: "/user-data/scoreko/nodecg", installed: false };
}, },
relaunch: () => events.push("relaunch"),
scheduleUpdateCheck: () => events.push("schedule-update"), scheduleUpdateCheck: () => events.push("schedule-update"),
setAppUserModelId: () => events.push("set-app-user-model-id"), setAppUserModelId: () => events.push("set-app-user-model-id"),
exit: (code) => events.push(`exit:${code}`), exit: (code) => events.push(`exit:${code}`),
@@ -253,7 +265,6 @@ test("ApplicationController shutdown is idempotent", async () => {
getAllWindows: () => [], getAllWindows: () => [],
log: () => undefined, log: () => undefined,
prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: false }), prepareRuntime: () => ({ runtimePath: "/user-data/scoreko/nodecg", installed: false }),
relaunch: () => events.push("relaunch"),
scheduleUpdateCheck: () => events.push("schedule-update"), scheduleUpdateCheck: () => events.push("schedule-update"),
setAppUserModelId: () => events.push("set-app-user-model-id"), setAppUserModelId: () => events.push("set-app-user-model-id"),
exit: (code) => events.push(`exit:${code}`), exit: (code) => events.push(`exit:${code}`),