mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
Async node_helper dont have to finish immediately in loadModules. So the start callback in the app.js with the config isnt called for some time. But the electron ready event can already be fired in the meantime. This lead to the electron app starting but without a config (which is provded by the node_helper callback) therefor crashing. This PR fixes #2487 by moving the callback call out of the loadModules block, therefor the config is provided in time. If any new async node_helper doesnt like this, we will see it :-) Co-authored-by: veeck <michael@veeck.de>
317 lines
8.9 KiB
JavaScript
317 lines
8.9 KiB
JavaScript
/* MagicMirror²
|
|
* The Core App (Server)
|
|
*
|
|
* By Michael Teeuw https://michaelteeuw.nl
|
|
* MIT Licensed.
|
|
*/
|
|
|
|
// Alias modules mentioned in package.js under _moduleAliases.
|
|
require("module-alias/register");
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const Log = require("logger");
|
|
const Server = require(`${__dirname}/server`);
|
|
const Utils = require(`${__dirname}/utils`);
|
|
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
|
|
|
// Get version number.
|
|
global.version = require(`${__dirname}/../package.json`).version;
|
|
Log.log("Starting MagicMirror: v" + global.version);
|
|
|
|
// global absolute root path
|
|
global.root_path = path.resolve(`${__dirname}/../`);
|
|
|
|
if (process.env.MM_CONFIG_FILE) {
|
|
global.configuration_file = process.env.MM_CONFIG_FILE;
|
|
}
|
|
|
|
// FIXME: Hotfix Pull Request
|
|
// https://github.com/MichMich/MagicMirror/pull/673
|
|
if (process.env.MM_PORT) {
|
|
global.mmPort = process.env.MM_PORT;
|
|
}
|
|
|
|
// The next part is here to prevent a major exception when there
|
|
// is no internet connection. This could probable be solved better.
|
|
process.on("uncaughtException", function (err) {
|
|
Log.error("Whoops! There was an uncaught exception...");
|
|
Log.error(err);
|
|
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
|
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
|
});
|
|
|
|
/**
|
|
* The core app.
|
|
*
|
|
* @class
|
|
*/
|
|
function App() {
|
|
let nodeHelpers = [];
|
|
let httpServer;
|
|
|
|
/**
|
|
* Loads the config file. Combines it with the defaults, and runs the
|
|
* callback with the found config as argument.
|
|
*
|
|
* @param {Function} callback Function to be called after loading the config
|
|
*/
|
|
function loadConfig(callback) {
|
|
Log.log("Loading config ...");
|
|
const defaults = require(`${__dirname}/defaults`);
|
|
|
|
// For this check proposed to TestSuite
|
|
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
|
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
|
|
|
try {
|
|
fs.accessSync(configFilename, fs.F_OK);
|
|
const c = require(configFilename);
|
|
checkDeprecatedOptions(c);
|
|
const config = Object.assign(defaults, c);
|
|
callback(config);
|
|
} catch (e) {
|
|
if (e.code === "ENOENT") {
|
|
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
|
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
|
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
|
|
} else {
|
|
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
|
}
|
|
callback(defaults);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks the config for deprecated options and throws a warning in the logs
|
|
* if it encounters one option from the deprecated.js list
|
|
*
|
|
* @param {object} userConfig The user config
|
|
*/
|
|
function checkDeprecatedOptions(userConfig) {
|
|
const deprecated = require(`${global.root_path}/js/deprecated`);
|
|
const deprecatedOptions = deprecated.configs;
|
|
|
|
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
|
if (usedDeprecated.length > 0) {
|
|
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a specific module.
|
|
*
|
|
* @param {string} module The name of the module (including subpath).
|
|
* @param {Function} callback Function to be called after loading
|
|
*/
|
|
function loadModule(module, callback) {
|
|
const elements = module.split("/");
|
|
const moduleName = elements[elements.length - 1];
|
|
let moduleFolder = `${__dirname}/../modules/${module}`;
|
|
|
|
if (defaultModules.includes(moduleName)) {
|
|
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
|
}
|
|
|
|
const moduleFile = `${moduleFolder}/${module}.js`;
|
|
|
|
try {
|
|
fs.accessSync(moduleFile, fs.R_OK);
|
|
} catch (e) {
|
|
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
|
}
|
|
|
|
const helperPath = `${moduleFolder}/node_helper.js`;
|
|
|
|
let loadHelper = true;
|
|
try {
|
|
fs.accessSync(helperPath, fs.R_OK);
|
|
} catch (e) {
|
|
loadHelper = false;
|
|
Log.log(`No helper found for module: ${moduleName}.`);
|
|
}
|
|
|
|
if (loadHelper) {
|
|
const Module = require(helperPath);
|
|
let m = new Module();
|
|
|
|
if (m.requiresVersion) {
|
|
Log.log(`Check MagicMirror² version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
|
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
|
Log.log("Version is ok!");
|
|
} else {
|
|
Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
m.setName(moduleName);
|
|
m.setPath(path.resolve(moduleFolder));
|
|
nodeHelpers.push(m);
|
|
|
|
m.loaded(callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads all modules.
|
|
*
|
|
* @param {Module[]} modules All modules to be loaded
|
|
* @param {Function} callback Function to be called after loading
|
|
*/
|
|
function loadModules(modules, callback) {
|
|
Log.log("Loading module helpers ...");
|
|
|
|
/**
|
|
*
|
|
*/
|
|
function loadNextModule() {
|
|
if (modules.length > 0) {
|
|
const nextModule = modules[0];
|
|
loadModule(nextModule, function () {
|
|
modules = modules.slice(1);
|
|
loadNextModule();
|
|
});
|
|
} else {
|
|
// All modules are loaded
|
|
Log.log("All module helpers loaded.");
|
|
callback();
|
|
}
|
|
}
|
|
|
|
loadNextModule();
|
|
}
|
|
|
|
/**
|
|
* Compare two semantic version numbers and return the difference.
|
|
*
|
|
* @param {string} a Version number a.
|
|
* @param {string} b Version number b.
|
|
* @returns {number} A positive number if a is larger than b, a negative
|
|
* number if a is smaller and 0 if they are the same
|
|
*/
|
|
function cmpVersions(a, b) {
|
|
let i, diff;
|
|
const regExStrip0 = /(\.0+)+$/;
|
|
const segmentsA = a.replace(regExStrip0, "").split(".");
|
|
const segmentsB = b.replace(regExStrip0, "").split(".");
|
|
const l = Math.min(segmentsA.length, segmentsB.length);
|
|
|
|
for (i = 0; i < l; i++) {
|
|
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
|
if (diff) {
|
|
return diff;
|
|
}
|
|
}
|
|
return segmentsA.length - segmentsB.length;
|
|
}
|
|
|
|
/**
|
|
* Start the core app.
|
|
*
|
|
* It loads the config, then it loads all modules. When it's done it
|
|
* executes the callback with the config as argument.
|
|
*
|
|
* @param {Function} callback Function to be called after start
|
|
*/
|
|
this.start = function (callback) {
|
|
loadConfig(function (c) {
|
|
config = c;
|
|
|
|
Log.setLogLevel(config.logLevel);
|
|
|
|
let modules = [];
|
|
|
|
for (const module of config.modules) {
|
|
if (!modules.includes(module.module) && !module.disabled) {
|
|
modules.push(module.module);
|
|
}
|
|
}
|
|
|
|
loadModules(modules, async function () {
|
|
httpServer = new Server(config);
|
|
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);
|
|
}
|
|
}
|
|
|
|
Promise.allSettled(nodePromises).then((results) => {
|
|
// 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 ...");
|
|
});
|
|
});
|
|
|
|
if (typeof callback === "function") {
|
|
callback(config);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Stops the core app. This calls each node_helper's STOP() function, if it
|
|
* exists.
|
|
*
|
|
* Added to fix #1056
|
|
*
|
|
* @param {Function} callback Function to be called after the app has stopped
|
|
*/
|
|
this.stop = function (callback) {
|
|
for (const nodeHelper of nodeHelpers) {
|
|
if (typeof nodeHelper.stop === "function") {
|
|
nodeHelper.stop();
|
|
}
|
|
}
|
|
httpServer.close().then(callback);
|
|
};
|
|
|
|
/**
|
|
* Listen for SIGINT signal and call stop() function.
|
|
*
|
|
* Added to fix #1056
|
|
* Note: this is only used if running `server-only`. Otherwise
|
|
* this.stop() is called by app.on("before-quit"... in `electron.js`
|
|
*/
|
|
process.on("SIGINT", () => {
|
|
Log.log("[SIGINT] Received. Shutting down server...");
|
|
setTimeout(() => {
|
|
process.exit(0);
|
|
}, 3000); // Force quit after 3 seconds
|
|
this.stop();
|
|
process.exit(0);
|
|
});
|
|
|
|
/**
|
|
* Listen to SIGTERM signals so we can stop everything when we
|
|
* are asked to stop by the OS.
|
|
*/
|
|
process.on("SIGTERM", () => {
|
|
Log.log("[SIGTERM] Received. Shutting down server...");
|
|
setTimeout(() => {
|
|
process.exit(0);
|
|
}, 3000); // Force quit after 3 seconds
|
|
this.stop();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
module.exports = new App();
|