From 7abbe62cb0a1f9a38f158e0e11c3dab605cdd2e4 Mon Sep 17 00:00:00 2001 From: Karsten Hassel Date: Wed, 20 May 2026 00:03:15 +0200 Subject: [PATCH] systeminformation thread not ending: move error handling from utils to app (#4160) --- js/app.js | 153 +++++++++++++++---------------- js/utils.js | 22 +++-- tests/unit/classes/utils_spec.js | 12 +-- 3 files changed, 96 insertions(+), 91 deletions(-) diff --git a/js/app.js b/js/app.js index e2089170..af831099 100644 --- a/js/app.js +++ b/js/app.js @@ -14,6 +14,7 @@ const { setGlobalDispatcher, Agent } = require("undici"); const Server = require("./server"); const Utils = require("./utils"); +const { ConfigError } = require("./utils"); const { getEnvVarsAsObj } = require("#server_functions"); // common timeout value, provide environment override in case @@ -183,80 +184,92 @@ function App () { * @returns {Promise} the config used */ this.start = async function () { - const configObj = Utils.loadConfig(); - global.config = configObj.fullConf; - const config = global.config; - Utils.checkConfigFile(configObj); + try { + const configObj = Utils.loadConfig(); + global.config = configObj.fullConf; + const config = global.config; + Utils.checkConfigFile(configObj); - global.defaultModulesDir = config.defaultModulesDir; - defaultModules = require(`${global.root_path}/${global.defaultModulesDir}/defaultmodules`); + global.defaultModulesDir = config.defaultModulesDir; + defaultModules = require(`${global.root_path}/${global.defaultModulesDir}/defaultmodules`); - Log.setLogLevel(config.logLevel); + Log.setLogLevel(config.logLevel); - env = getEnvVarsAsObj(); - // check for deprecated css/custom.css and move it to new location - if ((!fs.existsSync(`${global.root_path}/${env.customCss}`)) && (fs.existsSync(`${global.root_path}/css/custom.css`))) { - try { - fs.renameSync(`${global.root_path}/css/custom.css`, `${global.root_path}/${env.customCss}`); - Log.warn(`WARNING! Your custom css file was moved from ${global.root_path}/css/custom.css to ${global.root_path}/${env.customCss}`); - } catch { - Log.warn("WARNING! Your custom css file is currently located in the css folder. Please move it to the config folder!"); + env = getEnvVarsAsObj(); + // check for deprecated css/custom.css and move it to new location + if ((!fs.existsSync(`${global.root_path}/${env.customCss}`)) && (fs.existsSync(`${global.root_path}/css/custom.css`))) { + try { + fs.renameSync(`${global.root_path}/css/custom.css`, `${global.root_path}/${env.customCss}`); + Log.warn(`WARNING! Your custom css file was moved from ${global.root_path}/css/custom.css to ${global.root_path}/${env.customCss}`); + } catch { + Log.warn("WARNING! Your custom css file is currently located in the css folder. Please move it to the config folder!"); + } } - } - // get the used module positions - Utils.getModulePositions(); + // get the used module positions + Utils.getModulePositions(); - let modules = []; - for (const module of config.modules) { - if (module.disabled) continue; - if (module.module) { - if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") { - // Only add this module to be loaded if it is not a duplicate (repeated instance of the same module) - if (!modules.includes(module.module)) { - modules.push(module.module); + let modules = []; + for (const module of config.modules) { + if (module.disabled) continue; + if (module.module) { + if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") { + // Only add this module to be loaded if it is not a duplicate (repeated instance of the same module) + if (!modules.includes(module.module)) { + modules.push(module.module); + } + } else { + Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); } } else { - Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); + Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); } - } else { - Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`); } + + setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } })); + + await loadModules(modules); + + httpServer = new Server(configObj); + const { app, io } = await httpServer.open(); + Log.log("Server started ..."); + + const nodePromises = []; + for (let nodeHelper of nodeHelpers) { + nodeHelper.setExpressApp(app); + nodeHelper.setSocketIO(io); + + try { + nodePromises.push(nodeHelper.start()); + } catch (error) { + Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`); + Log.error(error); + } + } + + const results = await Promise.allSettled(nodePromises); + + // Log errors that happened during async node_helper startup + results.forEach((result) => { + if (result.status === "rejected") { + Log.error(result.reason); + } + }); + + Log.log("Sockets connected & modules started ..."); + + return global.config; + } catch (err) { + // planned ConfigErrors already logged their message before throwing + if (!(err instanceof ConfigError)) { + Log.error("Unexpected error during startup:", err); + } + + const int32 = new Int32Array(new SharedArrayBuffer(4)); + // wait 1000ms before exiting so that child processes (e.g. systeminformation) have some additional time + Atomics.wait(int32, 0, 0, 1000); + process.exit(1); } - - setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } })); - - await loadModules(modules); - - httpServer = new Server(configObj); - const { app, io } = await httpServer.open(); - Log.log("Server started ..."); - - const nodePromises = []; - for (let nodeHelper of nodeHelpers) { - nodeHelper.setExpressApp(app); - nodeHelper.setSocketIO(io); - - try { - nodePromises.push(nodeHelper.start()); - } catch (error) { - Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`); - Log.error(error); - } - } - - const results = await Promise.allSettled(nodePromises); - - // Log errors that happened during async node_helper startup - results.forEach((result) => { - if (result.status === "rejected") { - Log.error(result.reason); - } - }); - - Log.log("Sockets connected & modules started ..."); - - return global.config; }; /** @@ -328,20 +341,6 @@ function App () { await this.stop(); process.exit(0); }); - - /** - * - * @param {number} ms milliseconds to wait - */ - function blockingSleep (ms) { - const int32 = new Int32Array(new SharedArrayBuffer(4)); - Atomics.wait(int32, 0, 0, ms); - } - - process.on("exit", () => { - // wait before exiting so that child processes (e.g. systeminformation) have some additional time - blockingSleep(1000); - }); } module.exports = new App(); diff --git a/js/utils.js b/js/utils.js index 23955af8..ed868864 100644 --- a/js/utils.js +++ b/js/utils.js @@ -14,6 +14,13 @@ const { getConfigFilePath } = require("#server_functions"); const linter = new Linter({ configType: "flat" }); +class ConfigError extends Error { + constructor (message) { + super(message); + this.name = "ConfigError"; + } +} + const requireFromString = (src) => { const m = new module.constructor(); m._compile(src, ""); @@ -172,9 +179,8 @@ const loadConfig = () => { } else { Log.error(`Cannot access config file: ${configFilename}\n${error.message}`); } - process.exit(1); + throw new ConfigError(""); } - return {}; }; /** @@ -220,7 +226,7 @@ const checkConfigFile = (configObject) => { errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`; } Log.error(errorMessage); - process.exit(1); + throw new ConfigError(""); } }; @@ -242,7 +248,7 @@ const validateModulePositions = (data) => { // `modules` always exists (defaults.js provides a default array), but guard against it being overridden with a non-array value if (data.modules !== undefined && !Array.isArray(data.modules)) { Log.error("This module configuration contains errors:\nmodules must be an array"); - process.exit(1); + throw new ConfigError(""); } // Validate each module entry @@ -250,19 +256,19 @@ const validateModulePositions = (data) => { // Each module entry must be an object so we can safely inspect its fields if (mod === null || typeof mod !== "object" || Array.isArray(mod)) { Log.error(`This module configuration contains errors:\n${JSON.stringify(mod, null, 2)}\nmodule entry must be an object`); - process.exit(1); + throw new ConfigError(""); } // `module` (the module name) is required and must be a string if (typeof mod.module !== "string") { Log.error(`This module configuration contains errors:\n${JSON.stringify(mod, null, 2)}\nmodule: must be a string`); - process.exit(1); + throw new ConfigError(""); } // `position` is optional, but must be a string when provided if (mod.position !== undefined && typeof mod.position !== "string") { Log.error(`This module configuration contains errors:\n${JSON.stringify(mod, null, 2)}\nposition: must be a string`); - process.exit(1); + throw new ConfigError(""); } // `position` is optional, but when set it must match a known region @@ -275,4 +281,4 @@ const validateModulePositions = (data) => { Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)")); }; -module.exports = { loadConfig, getModulePositions, moduleHasValidPosition, getAvailableModulePositions, checkConfigFile }; +module.exports = { loadConfig, getModulePositions, moduleHasValidPosition, getAvailableModulePositions, checkConfigFile, ConfigError }; diff --git a/tests/unit/classes/utils_spec.js b/tests/unit/classes/utils_spec.js index ef771bd5..ee90e880 100644 --- a/tests/unit/classes/utils_spec.js +++ b/tests/unit/classes/utils_spec.js @@ -1,7 +1,7 @@ const fs = require("node:fs"); const Log = require("../../../js/logger"); -const { checkConfigFile } = require("../../../js/utils"); +const { checkConfigFile, ConfigError } = require("../../../js/utils"); const createConfigObject = (modules) => ({ configFilename: "config.js", @@ -14,13 +14,13 @@ const runCheck = (modules) => { }; const expectExitForModules = (modules) => { - vi.spyOn(process, "exit").mockImplementation((code) => { - throw new Error(`process.exit:${code}`); + vi.spyOn(process, "exit").mockImplementation(() => { + throw new ConfigError(""); }); expect(() => { runCheck(modules); - }).toThrow("process.exit:1"); + }).toThrow(ConfigError); }; describe("utils", () => { @@ -69,8 +69,8 @@ describe("utils", () => { }); it("warns for unknown positions without exiting", () => { - const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => { - throw new Error(`process.exit:${code}`); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { + throw new ConfigError(""); }); expect(() => {