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
+234
View File
@@ -0,0 +1,234 @@
import { AppRuntimeConfig } from "../config/runtime-config";
import { NodecgProcessManager } from "../nodecg/process-manager";
import { PreparedNodecgRuntime } from "../nodecg/runtime-provisioner";
import { getRemainingDelayMs } from "../utils/timing";
import { createShutdownService, ShutdownService } from "./shutdown-service";
export type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
export type ApplicationWindow = {
close: () => void;
focus: () => void;
isDestroyed: () => boolean;
isMinimized: () => boolean;
loadURL: (url: string) => Promise<unknown>;
restore: () => void;
show: () => void;
};
export type ApplicationControllerConfig = {
appConfig: AppRuntimeConfig;
appVersion: string;
isPackaged: boolean;
isWindows: boolean;
paths: {
rootPath: string;
sourceNodecgRuntimePath: string;
userDataPath: string;
nodecgBaseUrl: string;
mainDashboardUrl: string;
loadingDashboardUrl: string;
};
deps: {
createLoadingWindow: () => ApplicationWindow;
createMainWindow: () => ApplicationWindow;
createNodecgProcessManager: (runtimePath: string) => NodecgProcessManager;
getAllWindows: () => ApplicationWindow[];
log: (...args: unknown[]) => void;
prepareRuntime: (config: {
sourceRuntimePath: string;
userDataPath: string;
appVersion: string;
bundleName: string;
log: (...args: unknown[]) => void;
}) => PreparedNodecgRuntime;
relaunch: () => void;
scheduleUpdateCheck: (config: {
getParentWindow: () => ApplicationWindow | null;
beforeInstall: () => Promise<void>;
}) => void;
setAppUserModelId: (userModelId: string) => void;
exit: (code: number) => void;
now?: () => number;
sleep?: (ms: number) => Promise<void>;
};
};
export type ApplicationController = {
activate: () => Promise<void>;
focusExistingWindow: () => void;
getState: () => ApplicationState;
launch: () => Promise<void>;
stopNodecgGracefully: () => Promise<void>;
};
export function createApplicationController({
appConfig,
appVersion,
deps,
isPackaged,
isWindows,
paths,
}: ApplicationControllerConfig): ApplicationController {
let state: ApplicationState = "idle";
let mainWindow: ApplicationWindow | null = null;
let loadingWindow: ApplicationWindow | null = null;
let nodecgManager: NodecgProcessManager | null = null;
let launchPromise: Promise<void> | null = null;
const shutdownService: ShutdownService = createShutdownService(async () => {
await (nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve());
});
const now = deps.now ?? Date.now;
const sleep = deps.sleep ?? defaultSleep;
const closeLoadingWindow = (): void => {
if (!loadingWindow || loadingWindow.isDestroyed()) {
return;
}
loadingWindow.close();
loadingWindow = null;
};
const focusExistingWindow = (): void => {
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : loadingWindow;
if (!targetWindow || targetWindow.isDestroyed()) {
return;
}
if (targetWindow.isMinimized()) {
targetWindow.restore();
}
targetWindow.show();
targetWindow.focus();
};
const startNodecg = async (): Promise<void> => {
if (!nodecgManager) {
throw new Error("NodeCG process manager is not initialized.");
}
await nodecgManager.startNodecgProcess();
await nodecgManager.waitForNodecgReady(now());
};
const launch = async (): Promise<void> => {
if (launchPromise) {
return launchPromise;
}
launchPromise = (async () => {
if (isWindows) {
deps.setAppUserModelId(appConfig.userModelId);
}
state = "preparing";
const preparedRuntime = deps.prepareRuntime({
sourceRuntimePath: paths.sourceNodecgRuntimePath,
userDataPath: paths.userDataPath,
appVersion,
bundleName: appConfig.bundleName,
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);
mainWindow = deps.createMainWindow();
loadingWindow = deps.createLoadingWindow();
state = "starting";
await startNodecg();
if (!loadingWindow || loadingWindow.isDestroyed()) {
state = "ready";
return;
}
await loadingWindow.loadURL(paths.loadingDashboardUrl);
loadingWindow.show();
const loadingShownAt = now();
if (!mainWindow) {
state = "ready";
return;
}
await mainWindow.loadURL(paths.mainDashboardUrl);
const remainingLoadingDelay = getRemainingDelayMs(appConfig.loadDelayMs, loadingShownAt, now());
if (remainingLoadingDelay > 0) {
await sleep(remainingLoadingDelay);
}
mainWindow.show();
closeLoadingWindow();
deps.scheduleUpdateCheck({
getParentWindow: () => mainWindow,
beforeInstall: stopNodecgGracefully,
});
state = "ready";
})();
try {
await launchPromise;
} catch (error) {
state = "failed";
launchPromise = null;
closeLoadingWindow();
throw error;
}
};
const activate = async (): Promise<void> => {
if (deps.getAllWindows().length > 0) {
focusExistingWindow();
return;
}
if (state !== "ready") {
await launch();
return;
}
mainWindow = deps.createMainWindow();
await mainWindow.loadURL(paths.mainDashboardUrl);
mainWindow.show();
};
const stopNodecgGracefully = async (): Promise<void> => {
if (shutdownService.getState() === "running") {
state = "stopping";
}
await shutdownService.stop();
state = "stopped";
};
return {
activate,
focusExistingWindow,
getState: () => state,
launch,
stopNodecgGracefully,
};
}
function defaultSleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
+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);
});
}
+60
View File
@@ -0,0 +1,60 @@
import path from "node:path";
import { AppRuntimeConfig } from "../config/runtime-config";
export type ApplicationPaths = {
rootPath: string;
sourceNodecgRuntimePath: string;
userDataPath: string;
nodecgBaseUrl: string;
mainDashboardUrl: string;
loadingDashboardUrl: string;
};
export function getRootPath(isDev: boolean, compiledMainDir: string, resourcesPath: string): string {
return isDev ? path.resolve(compiledMainDir, "../..") : resourcesPath;
}
export function getUserDataPath(appDataPath: string, userDataDirectoryName: string): string {
return path.join(appDataPath, userDataDirectoryName);
}
export function getSourceNodecgRuntimePath(rootPath: string): string {
return path.resolve(rootPath, "lib", "nodecg");
}
export function getNodecgBaseUrl(nodecgPort: string): string {
return `http://127.0.0.1:${nodecgPort}`;
}
export function getDashboardUrl(nodecgPort: string, bundleName: string, dashboardRoute: string): string {
return `http://localhost:${nodecgPort}/bundles/${bundleName}/${dashboardRoute}`;
}
export function getApplicationPaths({
appConfig,
appDataPath,
compiledMainDir,
isDev,
resourcesPath,
}: {
appConfig: Pick<
AppRuntimeConfig,
"bundleName" | "loadingDashboardRoute" | "mainDashboardRoute" | "nodecgPort" | "userDataDirectoryName"
>;
appDataPath: string;
compiledMainDir: string;
isDev: boolean;
resourcesPath: string;
}): ApplicationPaths {
const rootPath = getRootPath(isDev, compiledMainDir, resourcesPath);
return {
rootPath,
sourceNodecgRuntimePath: getSourceNodecgRuntimePath(rootPath),
userDataPath: getUserDataPath(appDataPath, appConfig.userDataDirectoryName),
nodecgBaseUrl: getNodecgBaseUrl(appConfig.nodecgPort),
mainDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.mainDashboardRoute),
loadingDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.loadingDashboardRoute),
};
}
+32
View File
@@ -0,0 +1,32 @@
export type AppShutdownState = "running" | "stopping" | "stopped";
export type ShutdownService = {
getState: () => AppShutdownState;
stop: () => Promise<void>;
};
export function createShutdownService(stopRuntime: () => Promise<void>): ShutdownService {
let state: AppShutdownState = "running";
let stopPromise: Promise<void> | null = null;
return {
getState: () => state,
stop: () => {
if (state === "stopped") {
return Promise.resolve();
}
if (stopPromise) {
return stopPromise;
}
state = "stopping";
stopPromise = stopRuntime().finally(() => {
state = "stopped";
stopPromise = null;
});
return stopPromise;
},
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { app, dialog } from "electron";
import { logger } from "./logger";
import { logger } from "../logging/logger";
export function log(...args: unknown[]): void {
logger.info("runtime", { args });
+2 -219
View File
@@ -1,220 +1,3 @@
import { app, BrowserWindow } from "electron";
import path from "node:path";
import { bootstrap } from "./app/bootstrap";
import { getRuntimeConfig } from "./config/runtime-config";
import { showFatalError, log } from "./errors/error-presenter";
import { createNodecgProcessManager, NodecgProcessManager } from "./nodecg/process-manager";
import { prepareUserNodecgRuntime } from "./nodecg/runtime-provisioner";
import { scheduleUpdateCheck } from "./updates/update-manager";
import { getRemainingDelayMs } from "./utils/timing";
import { createLoadingWindow, createMainWindow } from "./windows/window-factory";
const appConfig = getRuntimeConfig();
// Force a stable userData folder name; overridable via SCOREKO_APP_USER_DATA_DIRECTORY.
app.setName(appConfig.title);
app.setPath("userData", path.join(app.getPath("appData"), appConfig.userDataDirectoryName));
const isDev = !app.isPackaged;
const rootPath = isDev ? path.resolve(__dirname, "../..") : process.resourcesPath;
const sourceNodecgRuntimePath = path.resolve(rootPath, "lib", "nodecg");
const mainDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.mainDashboardRoute}`;
const loadingDashboardUrl = `http://localhost:${appConfig.nodecgPort}/bundles/${appConfig.bundleName}/${appConfig.loadingDashboardRoute}`;
const nodecgBaseUrl = `http://127.0.0.1:${appConfig.nodecgPort}`;
const hasSingleInstanceLock = app.requestSingleInstanceLock();
if (!hasSingleInstanceLock) {
app.quit();
}
type AppShutdownState = "running" | "stopping" | "stopped";
let mainWindow: BrowserWindow | null = null;
let loadingWindow: BrowserWindow | null = null;
let nodecgManager: NodecgProcessManager | null = null;
let shutdownState: AppShutdownState = "running";
function focusExistingWindow(): void {
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : loadingWindow;
if (!targetWindow || targetWindow.isDestroyed()) {
return;
}
if (targetWindow.isMinimized()) {
targetWindow.restore();
}
targetWindow.show();
targetWindow.focus();
}
async function launchApplication(): Promise<void> {
const preparedRuntime = prepareUserNodecgRuntime({
sourceRuntimePath: sourceNodecgRuntimePath,
userDataPath: app.getPath("userData"),
appVersion: app.getVersion(),
bundleName: appConfig.bundleName,
log,
});
if (preparedRuntime.installed && app.isPackaged) {
log("Runtime was installed or refreshed; relaunching Scoreko before starting NodeCG.");
app.relaunch();
app.exit(0);
return;
}
nodecgManager = createNodecgProcessManager({
isDev,
nodecgRootPath: preparedRuntime.runtimePath,
nodecgBaseUrl,
appConfig,
log,
});
// We create both windows early so startup feels instant while NodeCG is booting in the background.
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
loadingWindow = createLoadingWindow({ appConfig, rootPath });
await startNodecg();
if (!loadingWindow || loadingWindow.isDestroyed()) {
return;
}
await loadingWindow.loadURL(loadingDashboardUrl);
loadingWindow.show();
const loadingShownAt = Date.now();
if (!mainWindow) {
return;
}
await mainWindow.loadURL(mainDashboardUrl);
// Keep the loading overlay visible for a minimum amount of time to avoid abrupt flashes.
const remainingLoadingDelay = getRemainingDelayMs(appConfig.loadDelayMs, loadingShownAt);
if (remainingLoadingDelay > 0) {
await sleep(remainingLoadingDelay);
}
mainWindow.show();
closeLoadingWindow();
scheduleUpdateCheck({
appConfig,
rootPath,
getParentWindow: () => mainWindow,
beforeInstall: stopNodecgGracefully,
log,
});
}
async function startNodecg(): Promise<void> {
if (!nodecgManager) {
throw new Error("NodeCG process manager is not initialized.");
}
await nodecgManager.startNodecgProcess();
await nodecgManager.waitForNodecgReady(Date.now());
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function closeLoadingWindow(): void {
if (!loadingWindow || loadingWindow.isDestroyed()) {
return;
}
loadingWindow.close();
loadingWindow = null;
}
function stopNodecgGracefully(): Promise<void> {
if (shutdownState === "stopped") {
return Promise.resolve();
}
if (shutdownState === "stopping") {
return nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve();
}
shutdownState = "stopping";
return (nodecgManager?.stopNodecgProcessGracefully() ?? Promise.resolve()).finally(() => {
shutdownState = "stopped";
});
}
app.on("ready", () => {
if (!hasSingleInstanceLock) {
return;
}
if (process.platform === "win32") {
app.setAppUserModelId(appConfig.userModelId);
}
launchApplication().catch((error: unknown) => {
showFatalError("No se pudo iniciar Scoreko.", error);
closeLoadingWindow();
app.exit(1);
});
});
app.on("second-instance", () => {
focusExistingWindow();
});
app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
await mainWindow.loadURL(mainDashboardUrl);
mainWindow.show();
}
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("before-quit", (event) => {
if (shutdownState !== "running") {
return;
}
// Block the default quit flow until we ask NodeCG to stop cleanly.
event.preventDefault();
stopNodecgGracefully().finally(() => {
app.quit();
});
});
app.on("will-quit", () => {
if (shutdownState === "running") {
void stopNodecgGracefully();
}
});
process.on("exit", () => {
if (shutdownState === "running") {
void 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);
});
bootstrap();
@@ -0,0 +1,54 @@
import { ChildProcess, SpawnOptions } from "node:child_process";
export type PlatformProcessKillerDeps = {
platform: NodeJS.Platform;
spawnProcess: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
killProcess: (pid: number, signal: NodeJS.Signals) => void;
log: (...args: unknown[]) => void;
};
export function killProcessTree(pid: number, signal: NodeJS.Signals, deps: PlatformProcessKillerDeps): boolean {
if (!Number.isSafeInteger(pid) || pid <= 0) {
deps.log(`Invalid pid for process tree termination: ${pid}`);
return false;
}
if (deps.platform === "win32") {
return killWindowsProcessTree(pid, signal, deps);
}
return killPosixProcessTree(pid, signal, deps.killProcess);
}
function killWindowsProcessTree(
pid: number,
signal: NodeJS.Signals,
deps: Pick<PlatformProcessKillerDeps, "spawnProcess" | "log">,
): boolean {
const args = ["/pid", String(pid), "/T", ...(signal === "SIGKILL" ? ["/F"] : [])];
const killer = deps.spawnProcess("taskkill", args, {
stdio: "ignore",
shell: false,
windowsHide: true,
});
killer.on("error", (error) => {
deps.log(`taskkill error for pid=${pid}`, error);
});
return true;
}
function killPosixProcessTree(pid: number, signal: NodeJS.Signals, killProcess: PlatformProcessKillerDeps["killProcess"]): boolean {
try {
killProcess(-pid, signal);
return true;
} catch {
try {
killProcess(pid, signal);
return true;
} catch {
return false;
}
}
}
+13 -35
View File
@@ -5,6 +5,7 @@ import path from "node:path";
import { AppRuntimeConfig } from "../config/runtime-config";
import { NODE_RUNTIME_NAME } from "../constants";
import { killProcessTree } from "./platform-process-killer";
type NodecgProcessManagerConfig = {
isDev: boolean;
@@ -161,7 +162,12 @@ export function createNodecgProcessManager({
}
log(`Stopping NodeCG pid=${pid}`);
killNodecgProcessTree(pid, "SIGTERM", log, resolvedDeps);
killProcessTree(pid, "SIGTERM", {
platform: resolvedDeps.platform,
spawnProcess: resolvedDeps.spawnProcess,
killProcess: resolvedDeps.killProcess,
log,
});
stopNodecgPromise = new Promise((resolve) => {
let completed = false;
@@ -189,7 +195,12 @@ export function createNodecgProcessManager({
() => {
if (processToStop.exitCode === null && processToStop.signalCode === null) {
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
killNodecgProcessTree(pid, "SIGKILL", log, resolvedDeps);
killProcessTree(pid, "SIGKILL", {
platform: resolvedDeps.platform,
spawnProcess: resolvedDeps.spawnProcess,
killProcess: resolvedDeps.killProcess,
log,
});
complete();
}
},
@@ -315,39 +326,6 @@ function probePortAvailable(port: number): Promise<boolean> {
});
}
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);
@@ -1,19 +1,27 @@
import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
import { AppRuntimeConfig } from "../config/runtime-config";
import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants";
import { resolveAppIconPath } from "./icon-path";
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "./navigation-security";
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "./navigation-policy";
type WindowFactoryDependencies = {
type WindowServiceDependencies = {
appConfig: AppRuntimeConfig;
allowDevTools: boolean;
rootPath: string;
mainDashboardUrl: string;
};
export function createMainWindow({ appConfig, rootPath, mainDashboardUrl }: WindowFactoryDependencies): BrowserWindow {
const windowOptions = createWindowOptions({ appConfig, rootPath, isLoadingWindow: false });
export function createMainWindow({
allowDevTools,
appConfig,
rootPath,
mainDashboardUrl,
}: WindowServiceDependencies): BrowserWindow {
const windowOptions = createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: false });
const window = new BrowserWindow(windowOptions);
denyPermissionsByDefault(window);
window.setMenuBarVisibility(false);
window.webContents.setWindowOpenHandler(({ url }) => {
@@ -44,10 +52,13 @@ export function createMainWindow({ appConfig, rootPath, mainDashboardUrl }: Wind
}
export function createLoadingWindow({
allowDevTools,
appConfig,
rootPath,
}: Omit<WindowFactoryDependencies, "mainDashboardUrl">): BrowserWindow {
const window = new BrowserWindow(createWindowOptions({ appConfig, rootPath, isLoadingWindow: true }));
}: Omit<WindowServiceDependencies, "mainDashboardUrl">): BrowserWindow {
const window = new BrowserWindow(createWindowOptions({ allowDevTools, appConfig, rootPath, isLoadingWindow: true }));
denyPermissionsByDefault(window);
window.on("page-title-updated", (event) => {
event.preventDefault();
@@ -56,11 +67,13 @@ export function createLoadingWindow({
return window;
}
function createWindowOptions({
export function createWindowOptions({
allowDevTools,
appConfig,
rootPath,
isLoadingWindow,
}: {
allowDevTools: boolean;
appConfig: AppRuntimeConfig;
rootPath: string;
isLoadingWindow: boolean;
@@ -74,8 +87,10 @@ function createWindowOptions({
backgroundColor: DEFAULT_WINDOW_BACKGROUND,
webPreferences: {
contextIsolation: true,
devTools: allowDevTools,
nodeIntegration: false,
sandbox: true,
...(isLoadingWindow ? {} : { nodeIntegration: false }),
webSecurity: true,
},
};
@@ -100,3 +115,9 @@ function createWindowOptions({
minHeight: DEFAULT_WINDOW_SIZE.minHeight,
};
}
function denyPermissionsByDefault(window: BrowserWindow): void {
window.webContents.session.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false);
});
}