From cc88570ba86118e0d8e63139324a0cfdd817842f Mon Sep 17 00:00:00 2001 From: Daniel Stock Date: Tue, 15 Sep 2026 15:00:24 +0200 Subject: [PATCH] Various changes in order to make startup and error handling more robust. --- html/offline.html | 21 ++++++ html/onboarding.html | 4 +- main.js | 160 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 152 insertions(+), 33 deletions(-) create mode 100644 html/offline.html diff --git a/html/offline.html b/html/offline.html new file mode 100644 index 0000000..afca62c --- /dev/null +++ b/html/offline.html @@ -0,0 +1,21 @@ + + + + + + + Offline + + + + + + \ No newline at end of file diff --git a/html/onboarding.html b/html/onboarding.html index 3a50f6b..b02f598 100644 --- a/html/onboarding.html +++ b/html/onboarding.html @@ -30,6 +30,7 @@ line-height: 1; margin: 1rem 0 1.333rem 0; } + h2 { margin-bottom: 2.25rem; } @@ -113,7 +114,8 @@ } - td, th { + td, + th { margin: 0; padding: 1rem; border-top: 1px solid lightblue; diff --git a/main.js b/main.js index 20c3938..ee6a8f9 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,10 @@ const { app, BrowserWindow, nativeTheme, Menu, MenuItem } = require('electron/main'); const path = require('node:path'); +const { fileURLToPath } = require('node:url'); const fs = require('fs'); const log = require('electron-log/main'); -const isReachable = require('is-reachable'); -const { setInterval } = require('node:timers/promises'); +const { default: isReachable } = require('is-reachable'); +const { setTimeout: delay } = require('node:timers/promises'); const config = require('./js/config.js'); const { Config } = require('./js/config.js'); @@ -14,6 +15,17 @@ const { Config } = require('./js/config.js'); let win; let menu; +let navigationController; +let navigationRequest = 0; +let restartScheduled = false; +let rendererRecoveryScheduled = false; +let restartTimer; + +const restartArgument = '--tooloop-restart-count='; +const restartCount = Number( + process.argv.find(argument => argument.startsWith(restartArgument))?.slice(restartArgument.length) || 0 +); +const maxAutomaticRestarts = 3; const isMac = process.platform === 'darwin'; @@ -56,7 +68,34 @@ function createMainWindow() { } return { action: 'deny' }; }); - win.webContents.on("did-fail-load", (event) => log.error(event)); + win.webContents.on("did-fail-load", (event) => { + log.error(event); + if (event.isMainFrame && event.errorCode !== -3 && config.data?.url) { + loadOfflinePage(); + } + }); + win.webContents.on("render-process-gone", (_event, details) => { + rendererRecoveryScheduled = false; + log.error('Renderer process gone: ' + JSON.stringify(details)); + loadOfflinePage(); + }); + win.webContents.on("unresponsive", () => { + log.error('Renderer became unresponsive'); + if (rendererRecoveryScheduled || win.isDestroyed()) return; + + rendererRecoveryScheduled = true; + try { + win.webContents.forcefullyCrashRenderer(); + } catch (error) { + rendererRecoveryScheduled = false; + log.error('Could not restart renderer: ' + (error?.stack || error)); + loadOfflinePage(); + } + }); + win.webContents.on("responsive", () => { + rendererRecoveryScheduled = false; + log.info('Renderer became responsive'); + }); win.on("page-title-updated", (event) => event.preventDefault()); // Load page from config file @@ -74,6 +113,13 @@ function createMainWindow() { } } +function loadOfflinePage() { + log.warn('Loading offline page'); + if (win && !win.isDestroyed()) { + win.loadFile(path.join(__dirname, 'html/offline.html')); + } +} + /** * Creates all available keyboard shortcuts @@ -117,14 +163,17 @@ function createMenu() { * @returns `true` if the url is allowed, `false` otherwise */ function validateDomain(event) { - if (config == undefined) return true; + if (config == undefined || config.data == undefined) return true; let url = new URL(event.url); // allow local urls if (['file:', 'file'].includes(url.protocol)) return true; - if ('whitelist' in config && !config.whitelist.includes(url.hostname)) { + if ('whitelist' in config.data && + Array.isArray(config.data.whitelist) && + config.data.whitelist.length > 0 && + !config.data.whitelist.includes(url.hostname)) { event.preventDefault(); log.info("Navigation to " + event.url + " prevented"); return false; @@ -140,47 +189,96 @@ function validateDomain(event) { * @param {string} testurl */ async function loadUrlAsync(testurl) { + const request = ++navigationRequest; + navigationController?.abort(); + navigationController = new AbortController(); + const { signal } = navigationController; + try { let url = new URL(testurl); // If it’s an online url, test whether it’s reachable if (['https:', 'http:', 'https', 'http'].includes(url.protocol)) { + let fallbackShown = false; - reachable = await isReachable(url.href); - if (reachable) { - log.info("Successfull access to " + url.hostname); - log.info("Loading " + config.data.url); - win.loadURL(config.data.url); - } else { - log.warn("Could not reach " + url.hostname); + while (!signal.aborted && request === navigationRequest) { + const timeoutSignal = AbortSignal.timeout(3000); + const reachable = await isReachable(url.href, { + signal: AbortSignal.any([signal, timeoutSignal]) + }); - for await (_ of setInterval(1000)) { - reachable = await isReachable(url.href); - if (reachable) { - log.info("Successfull access to " + url.hostname); - log.info("Loading " + config.data.url); - win.loadURL(config.data.url); - // break the interval - break; - } else { - log.warn("Could not reach " + url.hostname); - } + if (signal.aborted || request !== navigationRequest) return; + + if (reachable) { + log.info("Successful access to " + url.hostname); + log.info("Loading " + testurl); + if (!win.isDestroyed()) win.loadURL(testurl); + return; } + log.warn("Could not reach " + url.hostname); + if (!fallbackShown && !win.isDestroyed()) { + loadOfflinePage(); + fallbackShown = true; + } + await delay(1000, undefined, { signal }); } } - // Load other protocols (i. e. offline) immedately + // Load local files and other protocols immediately + else if (url.protocol === 'file:') { + if (!signal.aborted && request === navigationRequest && !win.isDestroyed()) { + await win.loadFile(fileURLToPath(url), { + search: url.search, + hash: url.hash + }); + } + } else { - win.loadURL(config.data.url); + if (!signal.aborted && request === navigationRequest && !win.isDestroyed()) { + win.loadURL(testurl); + } } } catch (err) { + if (signal.aborted || request !== navigationRequest) return; log.error(err.message); // Load fallback page - win.loadFile('./html/onboarding.html'); + loadOfflinePage(); } } +function resolveLogPath() { + if (config.data != undefined && 'logPath' in config.data) { + if (fs.existsSync(config.data.logPath)) { + return config.data.logPath; + } else { + return "tooloop-kiosk-browser.log"; + } + } +} + + +function restartAfterFatalError(error) { + log.error('Fatal main-process error: ' + (error?.stack || error)); + if (restartScheduled) return; + restartScheduled = true; + + if (restartCount >= maxAutomaticRestarts) { + log.error('Automatic restart limit reached; exiting'); + app.exit(1); + return; + } + + const relaunchArgs = process.argv.slice(1) + .filter(argument => !argument.startsWith(restartArgument)); + relaunchArgs.push(restartArgument + (restartCount + 1)); + log.warn('Restarting application in 5 seconds (attempt ' + (restartCount + 1) + '/' + maxAutomaticRestarts + ')'); + restartTimer = setTimeout(() => { + app.relaunch({ args: relaunchArgs }); + app.exit(1); + }, 5000); +} + //------------------------------------------------------------------------------ // Init electron app @@ -195,12 +293,11 @@ app.whenReady().then(() => { config.load(); log.initialize(); - if (config.data != undefined && 'logPath' in config.data) { - log.transports.file.resolvePathFn = () => config.data.logPath; - } + log.transports.file.resolvePathFn = resolveLogPath; log.info('----------------------------------------------'); log.info('Starting Tooloop Kiosk Browser...'); + log.info('Logging to ' + log.transports.file.resolvePathFn()); createMenu(); createMainWindow(); @@ -221,6 +318,5 @@ app.on('quit', () => { }); // prevent error dialogs in case of exceptions -process.on('uncaughtException', function (error) { - log.error(error); -}); \ No newline at end of file +process.on('uncaughtException', restartAfterFatalError); +process.on('unhandledRejection', restartAfterFatalError); \ No newline at end of file