diff --git a/js/app.js b/js/app.js index e2089170ff..af8310992a 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); - - global.defaultModulesDir = config.defaultModulesDir; - defaultModules = require(`${global.root_path}/${global.defaultModulesDir}/defaultmodules`); - - 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!"); + 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`); + + 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!"); + } } - } - // 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); + // 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); + } + } 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 } })); + setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } })); - await loadModules(modules); + await loadModules(modules); - httpServer = new Server(configObj); - const { app, io } = await httpServer.open(); - Log.log("Server started ..."); + 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); + 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); + 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); + 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 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 ..."); + 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); + } - return global.config; + 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); + } }; /** @@ -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 23955af848..ed868864d1 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 ef771bd5ef..ee90e880fe 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(() => {