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;
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;
+128 -32
View File
@@ -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);
});
process.on('uncaughtException', restartAfterFatalError);
process.on('unhandledRejection', restartAfterFatalError);