mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: add error handling screen and logging functionality
This commit is contained in:
@@ -5,7 +5,7 @@ import { getRemainingDelayMs } from "../utils/timing";
|
||||
import { ApplicationPaths } from "./paths";
|
||||
import { createShutdownService, ShutdownService } from "./shutdown-service";
|
||||
|
||||
export type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
|
||||
type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
|
||||
|
||||
export type ApplicationWindow = {
|
||||
close: () => void;
|
||||
@@ -53,6 +53,7 @@ export type ApplicationController = {
|
||||
focusExistingWindow: () => void;
|
||||
getState: () => ApplicationState;
|
||||
launch: () => Promise<void>;
|
||||
showErrorScreen: (error: unknown) => Promise<void>;
|
||||
stopNodecgGracefully: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -60,7 +61,7 @@ export function createApplicationController({
|
||||
appConfig,
|
||||
appVersion,
|
||||
deps,
|
||||
isPackaged,
|
||||
isPackaged: _isPackaged,
|
||||
isWindows,
|
||||
paths,
|
||||
}: ApplicationControllerConfig): ApplicationController {
|
||||
@@ -176,7 +177,7 @@ export function createApplicationController({
|
||||
} catch (error) {
|
||||
state = "failed";
|
||||
launchPromise = null;
|
||||
closeLoadingWindow();
|
||||
await showErrorScreen(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -206,11 +207,31 @@ export function createApplicationController({
|
||||
state = "stopped";
|
||||
};
|
||||
|
||||
const showErrorScreen = async (error: unknown): Promise<void> => {
|
||||
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
||||
const encodedMsg = encodeURIComponent(`msg=${encodeURIComponent(message)}`);
|
||||
const errorUrl = `file://${paths.staticErrorHtmlPath}#${encodedMsg}`;
|
||||
|
||||
const targetWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : loadingWindow;
|
||||
|
||||
if (!targetWindow || targetWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await targetWindow.loadURL(errorUrl);
|
||||
targetWindow.show();
|
||||
} catch {
|
||||
// If even the error screen fails to load, nothing more can be done.
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
activate,
|
||||
focusExistingWindow,
|
||||
getState: () => state,
|
||||
launch,
|
||||
showErrorScreen,
|
||||
stopNodecgGracefully,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
|
||||
import { getRuntimeConfig, loadEnvFile, AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { showFatalError, log } from "../errors/error-handler";
|
||||
import { logger } from "../logging/logger";
|
||||
import { createNodecgProcessManager } from "../nodecg/process-manager";
|
||||
import { prepareUserNodecgRuntime } from "../nodecg/runtime-setup";
|
||||
import { scheduleUpdateCheck } from "../updates/update-service";
|
||||
@@ -97,8 +98,7 @@ export function bootstrap(): void {
|
||||
}
|
||||
|
||||
controller.launch().catch((error: unknown) => {
|
||||
showFatalError("No se pudo iniciar Scoreko.", error);
|
||||
app.exit(1);
|
||||
logger.error("launch-failed", { error: error instanceof Error ? error.stack : String(error) });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type ApplicationPaths = {
|
||||
mainDashboardUrl: string;
|
||||
loadingDashboardUrl: string;
|
||||
staticLoadingHtmlPath: string;
|
||||
staticErrorHtmlPath: string;
|
||||
};
|
||||
|
||||
export function getRootPath(isDev: boolean, compiledMainDir: string, resourcesPath: string): string {
|
||||
@@ -80,5 +81,6 @@ export function getApplicationPaths({
|
||||
mainDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.mainDashboardRoute),
|
||||
loadingDashboardUrl: getDashboardUrl(appConfig.nodecgPort, appConfig.bundleName, appConfig.loadingDashboardRoute),
|
||||
staticLoadingHtmlPath: path.join(rootPath, "static", "loading.html"),
|
||||
staticErrorHtmlPath: path.join(rootPath, "static", "error.html"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AppShutdownState = "running" | "stopping" | "stopped";
|
||||
type AppShutdownState = "running" | "stopping" | "stopped";
|
||||
|
||||
export type ShutdownService = {
|
||||
getState: () => AppShutdownState;
|
||||
|
||||
@@ -6,7 +6,7 @@ export function log(...args: unknown[]): void {
|
||||
logger.info("runtime", { args });
|
||||
}
|
||||
|
||||
export function formatErrorMessage(error: unknown): string {
|
||||
function formatErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const stack = error.stack?.trim();
|
||||
return stack && stack.length > 0 ? stack : error.message;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import electronLog from "electron-log";
|
||||
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
type LogContext = Record<string, unknown>;
|
||||
|
||||
// Configure electron-log: write to file and (in dev) also to console.
|
||||
electronLog.initialize();
|
||||
electronLog.transports.file.level = "debug";
|
||||
electronLog.transports.console.level = process.env["NODE_ENV"] === "development" ? "debug" : false;
|
||||
|
||||
function write(level: LogLevel, message: string, context?: LogContext): void {
|
||||
const payload = {
|
||||
ts: new Date().toISOString(),
|
||||
@@ -13,17 +20,7 @@ function write(level: LogLevel, message: string, context?: LogContext): void {
|
||||
|
||||
const line = JSON.stringify(payload);
|
||||
|
||||
if (level === "error") {
|
||||
console.error(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === "warn") {
|
||||
console.warn(line);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(line);
|
||||
electronLog[level](line);
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
|
||||
@@ -54,7 +54,7 @@ export type NodecgProcessManager = {
|
||||
getState: () => NodecgProcessState;
|
||||
};
|
||||
|
||||
export type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
||||
type NodecgProcessState = "idle" | "starting" | "running" | "stopping" | "stopped" | "failed";
|
||||
|
||||
export function createNodecgProcessManager({
|
||||
isDev,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
|
||||
|
||||
export type GiteaReleaseAsset = {
|
||||
type GiteaReleaseAsset = {
|
||||
name: string;
|
||||
browserDownloadUrl: string;
|
||||
size?: number;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions, shell } from "electron";
|
||||
import electronLog from "electron-log";
|
||||
|
||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||
import { DEFAULT_WINDOW_BACKGROUND, DEFAULT_WINDOW_SIZE, LOADING_WINDOW_SIZE } from "../constants";
|
||||
@@ -33,6 +34,12 @@ export function createMainWindow({
|
||||
});
|
||||
|
||||
window.webContents.on("will-navigate", (event, url) => {
|
||||
if (url.startsWith("app://open-logs")) {
|
||||
event.preventDefault();
|
||||
void shell.showItemInFolder(electronLog.transports.file.getFile().path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldAllowInternalNavigation(url, mainDashboardUrl)) {
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +74,7 @@ export function createLoadingWindow({
|
||||
return window;
|
||||
}
|
||||
|
||||
export function createWindowOptions({
|
||||
function createWindowOptions({
|
||||
allowDevTools,
|
||||
appConfig,
|
||||
rootPath,
|
||||
|
||||
@@ -92,6 +92,7 @@ test("ApplicationController preserves startup ordering and schedules updates aft
|
||||
mainDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html?standalone=true",
|
||||
loadingDashboardUrl: "http://localhost:9090/bundles/scoreko-dev/dashboard/loading/main.html?standalone=true",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
};
|
||||
|
||||
const controller = createApplicationController({
|
||||
@@ -143,7 +144,6 @@ test("ApplicationController preserves startup ordering and schedules updates aft
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
`loading:load:${paths.loadingDashboardUrl}`,
|
||||
`main:load:${paths.mainDashboardUrl}`,
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -166,6 +166,7 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => {
|
||||
@@ -205,7 +206,6 @@ test("ApplicationController directly launches packaged app after runtime install
|
||||
"create-manager",
|
||||
"start-nodecg",
|
||||
"wait-nodecg",
|
||||
"loading:load:http://localhost:9090/loading",
|
||||
"main:load:http://localhost:9090/main",
|
||||
"main:show",
|
||||
"loading:close",
|
||||
@@ -228,6 +228,7 @@ test("ApplicationController activation before readiness routes through launch",
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
@@ -269,6 +270,7 @@ test("ApplicationController shutdown is idempotent", async () => {
|
||||
mainDashboardUrl: "http://localhost:9090/main",
|
||||
loadingDashboardUrl: "http://localhost:9090/loading",
|
||||
staticLoadingHtmlPath: "/app/static/loading.html",
|
||||
staticErrorHtmlPath: "/app/static/error.html",
|
||||
},
|
||||
deps: {
|
||||
createLoadingWindow: () => new MockWindow("loading", events),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "../main/windows/navigation-policy";
|
||||
import { shouldAllowInternalNavigation, shouldOpenExternalNavigation } from "../main/windows/navigation";
|
||||
|
||||
const dashboardUrl = "http://localhost:9090/bundles/scoreko-dev/dashboard/main.html";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
|
||||
import { SpawnOptions } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
import { killProcessTree } from "../main/nodecg/platform-process-killer";
|
||||
import { killProcessTree } from "../main/nodecg/process-killer";
|
||||
|
||||
test("killProcessTree validates pid before building Windows taskkill command", () => {
|
||||
const spawnCalls: Array<{ command: string; args: string[]; options: SpawnOptions }> = [];
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-provisioner";
|
||||
import { prepareUserNodecgRuntime } from "../main/nodecg/runtime-setup";
|
||||
|
||||
type FakeFsState = {
|
||||
paths: Set<string>;
|
||||
@@ -45,7 +45,7 @@ function createFakeFs(initialPaths: string[] = [], initialFiles: Record<string,
|
||||
return normalized.endsWith("node_modules") || normalized.endsWith("bundles");
|
||||
},
|
||||
}),
|
||||
symlinkSync: (target: string, linkPath: string, type: string) => {
|
||||
symlinkSync: (target: string, linkPath: string, _type: string) => {
|
||||
state.copied.push({ from: path.normalize(target), to: path.normalize(linkPath) });
|
||||
state.paths.add(path.normalize(linkPath));
|
||||
if (target.endsWith("node_modules")) {
|
||||
|
||||
Reference in New Issue
Block a user