Various changes in order to make startup and error handling more robust.

This commit is contained in:
vollstock committed 2026-09-15 15:00:24 +02:00
1 parent e248801ba6
commit cc88570ba8
3 files changed
+152 -33

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline</title>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
background: #000;
}
</style>
</head>
<body></body>
</html>
+3 -1
View File
@@ -30,6 +30,7 @@
line-height: 1; line-height: 1;
margin: 1rem 0 1.333rem 0; margin: 1rem 0 1.333rem 0;
} }
h2 { h2 {
margin-bottom: 2.25rem; margin-bottom: 2.25rem;
} }
@@ -113,7 +114,8 @@
} }
td, th { td,
th {
margin: 0; margin: 0;
padding: 1rem; padding: 1rem;
border-top: 1px solid lightblue; border-top: 1px solid lightblue;
+128 -32
View File
@@ -1,9 +1,10 @@
const { app, BrowserWindow, nativeTheme, Menu, MenuItem } = require('electron/main'); const { app, BrowserWindow, nativeTheme, Menu, MenuItem } = require('electron/main');
const path = require('node:path'); const path = require('node:path');
const { fileURLToPath } = require('node:url');
const fs = require('fs'); const fs = require('fs');
const log = require('electron-log/main'); const log = require('electron-log/main');
const isReachable = require('is-reachable'); const { default: isReachable } = require('is-reachable');
const { setInterval } = require('node:timers/promises'); const { setTimeout: delay } = require('node:timers/promises');
const config = require('./js/config.js'); const config = require('./js/config.js');
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 win;
let menu; 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'; const isMac = process.platform === 'darwin';
@@ -56,7 +68,34 @@ function createMainWindow() {
} }
return { action: 'deny' }; 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()); win.on("page-title-updated", (event) => event.preventDefault());
// Load page from config file // 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 * Creates all available keyboard shortcuts
@@ -117,14 +163,17 @@ function createMenu() {
* @returns `true` if the url is allowed, `false` otherwise * @returns `true` if the url is allowed, `false` otherwise
*/ */
function validateDomain(event) { function validateDomain(event) {
if (config == undefined) return true; if (config == undefined || config.data == undefined) return true;
let url = new URL(event.url); let url = new URL(event.url);
// allow local urls // allow local urls
if (['file:', 'file'].includes(url.protocol)) return true; 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(); event.preventDefault();
log.info("Navigation to " + event.url + " prevented"); log.info("Navigation to " + event.url + " prevented");
return false; return false;
@@ -140,47 +189,96 @@ function validateDomain(event) {
* @param {string} testurl * @param {string} testurl
*/ */
async function loadUrlAsync(testurl) { async function loadUrlAsync(testurl) {
const request = ++navigationRequest;
navigationController?.abort();
navigationController = new AbortController();
const { signal } = navigationController;
try { try {
let url = new URL(testurl); let url = new URL(testurl);
// If it’s an online url, test whether it’s reachable // If it’s an online url, test whether it’s reachable
if (['https:', 'http:', 'https', 'http'].includes(url.protocol)) { if (['https:', 'http:', 'https', 'http'].includes(url.protocol)) {
let fallbackShown = false;
reachable = await isReachable(url.href); while (!signal.aborted && request === navigationRequest) {
if (reachable) { const timeoutSignal = AbortSignal.timeout(3000);
log.info("Successfull access to " + url.hostname); const reachable = await isReachable(url.href, {
log.info("Loading " + config.data.url); signal: AbortSignal.any([signal, timeoutSignal])
win.loadURL(config.data.url); });
} else {
log.warn("Could not reach " + url.hostname);
for await (_ of setInterval(1000)) { if (signal.aborted || request !== navigationRequest) return;
reachable = await isReachable(url.href);
if (reachable) { if (reachable) {
log.info("Successfull access to " + url.hostname); log.info("Successful access to " + url.hostname);
log.info("Loading " + config.data.url); log.info("Loading " + testurl);
win.loadURL(config.data.url); if (!win.isDestroyed()) win.loadURL(testurl);
// break the interval return;
break;
} else {
log.warn("Could not reach " + url.hostname);
}
} }
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 { else {
win.loadURL(config.data.url); if (!signal.aborted && request === navigationRequest && !win.isDestroyed()) {
win.loadURL(testurl);
}
} }
} catch (err) { } catch (err) {
if (signal.aborted || request !== navigationRequest) return;
log.error(err.message); log.error(err.message);
// Load fallback page // 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 // Init electron app
@@ -195,12 +293,11 @@ app.whenReady().then(() => {
config.load(); config.load();
log.initialize(); log.initialize();
if (config.data != undefined && 'logPath' in config.data) { log.transports.file.resolvePathFn = resolveLogPath;
log.transports.file.resolvePathFn = () => config.data.logPath;
}
log.info('----------------------------------------------'); log.info('----------------------------------------------');
log.info('Starting Tooloop Kiosk Browser...'); log.info('Starting Tooloop Kiosk Browser...');
log.info('Logging to ' + log.transports.file.resolvePathFn());
createMenu(); createMenu();
createMainWindow(); createMainWindow();
@@ -221,6 +318,5 @@ app.on('quit', () => {
}); });
// prevent error dialogs in case of exceptions // prevent error dialogs in case of exceptions
process.on('uncaughtException', function (error) { process.on('uncaughtException', restartAfterFatalError);
log.error(error); process.on('unhandledRejection', restartAfterFatalError);
});