feat: Implement application bootstrap and window management

- Added bootstrap functionality to initialize the Electron application.
- Created a new paths module to manage application paths and URLs.
- Introduced a shutdown service to handle graceful application shutdowns.
- Refactored error logging to use a dedicated logger module.
- Implemented process killing logic for NodeCG processes across platforms.
- Established navigation policies for internal and external URL handling in windows.
- Developed window service for creating and managing application windows.
- Added tests for application paths, application controller, navigation policies, process killer, and shutdown service.
This commit is contained in:
2026-05-24 16:14:23 +02:00
parent c168c3b84a
commit e3d3936156
17 changed files with 1067 additions and 264 deletions
+136
View File
@@ -0,0 +1,136 @@
import { app, BrowserWindow } from "electron";
import { getRuntimeConfig } from "../config/runtime-config";
import { showFatalError, log } from "../errors/error-presenter";
import { createNodecgProcessManager } from "../nodecg/process-manager";
import { prepareUserNodecgRuntime } from "../nodecg/runtime-provisioner";
import { scheduleUpdateCheck } from "../updates/update-manager";
import { createLoadingWindow, createMainWindow } from "../windows/window-service";
import { createApplicationController } from "./application-controller";
import { getApplicationPaths } from "./paths";
export function bootstrap(): void {
const appConfig = getRuntimeConfig();
const isDev = !app.isPackaged;
const paths = getApplicationPaths({
appConfig,
appDataPath: app.getPath("appData"),
compiledMainDir: __dirname,
isDev,
resourcesPath: process.resourcesPath,
});
app.setName(appConfig.title);
app.setPath("userData", paths.userDataPath);
const hasSingleInstanceLock = app.requestSingleInstanceLock();
if (!hasSingleInstanceLock) {
app.quit();
}
const controller = createApplicationController({
appConfig,
appVersion: app.getVersion(),
isPackaged: app.isPackaged,
isWindows: process.platform === "win32",
paths,
deps: {
createLoadingWindow: () =>
createLoadingWindow({
allowDevTools: isDev,
appConfig,
rootPath: paths.rootPath,
}),
createMainWindow: () =>
createMainWindow({
allowDevTools: isDev,
appConfig,
rootPath: paths.rootPath,
mainDashboardUrl: paths.mainDashboardUrl,
}),
createNodecgProcessManager: (runtimePath) =>
createNodecgProcessManager({
isDev,
nodecgRootPath: runtimePath,
nodecgBaseUrl: paths.nodecgBaseUrl,
appConfig,
log,
}),
getAllWindows: () => BrowserWindow.getAllWindows(),
log,
prepareRuntime: prepareUserNodecgRuntime,
relaunch: () => app.relaunch(),
scheduleUpdateCheck: ({ getParentWindow, beforeInstall }) => {
scheduleUpdateCheck({
appConfig,
rootPath: paths.rootPath,
getParentWindow: () => getParentWindow() as BrowserWindow | null,
beforeInstall,
log,
});
},
setAppUserModelId: (userModelId) => app.setAppUserModelId(userModelId),
exit: (code) => app.exit(code),
},
});
app.on("ready", () => {
if (!hasSingleInstanceLock) {
return;
}
controller.launch().catch((error: unknown) => {
showFatalError("No se pudo iniciar Scoreko.", error);
app.exit(1);
});
});
app.on("second-instance", () => {
controller.focusExistingWindow();
});
app.on("activate", () => {
controller.activate().catch((error: unknown) => {
showFatalError("No se pudo reactivar Scoreko.", error);
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("before-quit", (event) => {
if (controller.getState() === "stopping" || controller.getState() === "stopped") {
return;
}
event.preventDefault();
controller.stopNodecgGracefully().finally(() => {
app.quit();
});
});
app.on("will-quit", () => {
if (controller.getState() !== "stopping" && controller.getState() !== "stopped") {
void controller.stopNodecgGracefully();
}
});
process.on("exit", () => {
if (controller.getState() !== "stopping" && controller.getState() !== "stopped") {
void controller.stopNodecgGracefully();
}
});
process.on("uncaughtException", (error) => {
showFatalError("Unexpected error in Electron main process.", error);
});
process.on("unhandledRejection", (reason) => {
showFatalError("Unhandled promise in Electron main process.", reason);
});
}