2022-01-26 23:09:26 +01:00
/ * M a g i c M i r r o r ²
2016-04-08 22:16:22 +02:00
* The Core App ( Server )
*
2020-04-28 23:05:28 +02:00
* By Michael Teeuw https : //michaelteeuw.nl
2016-04-08 22:16:22 +02:00
* MIT Licensed .
* /
2021-02-18 19:14:53 +01:00
// Alias modules mentioned in package.js under _moduleAliases.
require ( "module-alias/register" ) ;
2021-01-05 19:35:11 +01:00
const fs = require ( "fs" ) ;
const path = require ( "path" ) ;
2021-02-18 19:14:53 +01:00
const Log = require ( "logger" ) ;
2021-01-05 19:35:11 +01:00
const Server = require ( ` ${ _ _dirname } /server ` ) ;
const Utils = require ( ` ${ _ _dirname } /utils ` ) ;
const defaultModules = require ( ` ${ _ _dirname } /../modules/default/defaultmodules ` ) ;
2023-02-12 22:34:57 +01:00
const envsub = require ( "envsub" ) ;
2016-04-08 22:16:22 +02:00
2016-10-13 16:42:15 +02:00
// Get version number.
2021-01-05 19:35:11 +01:00
global . version = require ( ` ${ _ _dirname } /../package.json ` ) . version ;
2023-03-19 14:32:23 +01:00
Log . log ( ` Starting MagicMirror: v ${ global . version } ` ) ;
2016-10-13 16:42:15 +02:00
2016-12-27 14:31:35 -03:00
// global absolute root path
2021-01-05 19:35:11 +01:00
global . root _path = path . resolve ( ` ${ _ _dirname } /../ ` ) ;
2016-12-27 14:31:35 -03:00
2017-01-24 02:59:20 -03:00
if ( process . env . MM _CONFIG _FILE ) {
global . configuration _file = process . env . MM _CONFIG _FILE ;
}
2017-02-24 18:52:33 -03:00
// FIXME: Hotfix Pull Request
// https://github.com/MichMich/MagicMirror/pull/673
2017-01-31 21:58:46 -03:00
if ( process . env . MM _PORT ) {
global . mmPort = process . env . MM _PORT ;
}
2016-04-19 11:25:50 +02:00
// The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better.
2016-05-03 19:09:38 -04:00
process . on ( "uncaughtException" , function ( err ) {
2020-06-01 16:40:20 +02:00
Log . error ( "Whoops! There was an uncaught exception..." ) ;
2020-05-11 07:25:42 +02:00
Log . error ( err ) ;
2022-01-26 23:47:51 +01:00
Log . error ( "MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?" ) ;
2020-06-01 16:40:20 +02:00
Log . error ( "If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues" ) ;
2016-04-19 11:25:50 +02:00
} ) ;
2020-07-27 20:10:31 +02:00
/ * *
* The core app .
*
* @ class
2016-04-08 22:16:22 +02:00
* /
2021-01-05 19:35:11 +01:00
function App ( ) {
let nodeHelpers = [ ] ;
2021-09-15 21:09:31 +02:00
let httpServer ;
2016-04-08 22:16:22 +02:00
2020-07-27 20:10:31 +02:00
/ * *
2023-02-12 22:34:57 +01:00
* Loads the config file . Combines it with the defaults and returns the config
2023-02-22 18:58:00 +01:00
*
* @ async
* @ returns { Promise < object > } the loaded config or the defaults if something goes wrong
2016-04-08 22:16:22 +02:00
* /
2023-02-12 22:34:57 +01:00
async function loadConfig ( ) {
2020-05-11 07:25:42 +02:00
Log . log ( "Loading config ..." ) ;
2021-01-05 19:35:11 +01:00
const defaults = require ( ` ${ _ _dirname } /defaults ` ) ;
2017-01-24 02:59:20 -03:00
// For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
2021-01-05 19:39:31 +01:00
const configFilename = path . resolve ( global . configuration _file || ` ${ global . root _path } /config/config.js ` ) ;
2023-03-19 14:32:23 +01:00
let templateFile = ` ${ configFilename } .template ` ;
2023-02-12 22:34:57 +01:00
// check if templateFile exists
try {
fs . accessSync ( templateFile , fs . F _OK ) ;
} catch ( err ) {
templateFile = null ;
Log . debug ( "config template file not exists, no envsubst" ) ;
}
if ( templateFile ) {
// save current config.js
try {
if ( fs . existsSync ( configFilename ) ) {
2023-03-19 14:32:23 +01:00
fs . copyFileSync ( configFilename , ` ${ configFilename } _ ${ Date . now ( ) } ` ) ;
2023-02-12 22:34:57 +01:00
}
} catch ( err ) {
2023-03-19 14:32:23 +01:00
Log . warn ( ` Could not copy ${ configFilename } : ${ err . message } ` ) ;
2023-02-12 22:34:57 +01:00
}
// check if config.env exists
const envFiles = [ ] ;
2023-03-19 14:32:23 +01:00
const configEnvFile = ` ${ configFilename . substr ( 0 , configFilename . lastIndexOf ( "." ) ) } .env ` ;
2023-02-12 22:34:57 +01:00
try {
if ( fs . existsSync ( configEnvFile ) ) {
envFiles . push ( configEnvFile ) ;
}
} catch ( err ) {
2023-03-19 14:32:23 +01:00
Log . debug ( ` ${ configEnvFile } does not exist. ${ err . message } ` ) ;
2023-02-12 22:34:57 +01:00
}
let options = {
all : true ,
diff : false ,
envFiles : envFiles ,
protect : false ,
syntax : "default" ,
system : true
} ;
// envsubst variables in templateFile and create new config.js
// naming for envsub must be templateFile and outputFile
const outputFile = configFilename ;
try {
await envsub ( { templateFile , outputFile , options } ) ;
} catch ( err ) {
2023-03-19 14:32:23 +01:00
Log . error ( ` Could not envsubst variables: ${ err . message } ` ) ;
2023-02-12 22:34:57 +01:00
}
}
2017-01-24 02:59:20 -03:00
2016-04-08 22:16:22 +02:00
try {
fs . accessSync ( configFilename , fs . F _OK ) ;
2021-01-05 19:35:11 +01:00
const c = require ( configFilename ) ;
2016-12-14 18:54:44 +01:00
checkDeprecatedOptions ( c ) ;
2023-02-22 18:58:00 +01:00
return Object . assign ( defaults , c ) ;
2016-04-08 22:16:22 +02:00
} catch ( e ) {
2019-06-05 08:48:22 +02:00
if ( e . code === "ENOENT" ) {
2020-05-11 07:25:42 +02:00
Log . error ( Utils . colors . error ( "WARNING! Could not find config file. Please create one. Starting with default configuration." ) ) ;
2016-08-05 19:18:31 +02:00
} else if ( e instanceof ReferenceError || e instanceof SyntaxError ) {
2021-01-05 19:35:11 +01:00
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 } ` ) ) ;
2016-08-05 19:18:31 +02:00
} else {
2021-01-05 19:35:11 +01:00
Log . error ( Utils . colors . error ( ` WARNING! Could not load config file. Starting with default configuration. Error found: ${ e } ` ) ) ;
2016-08-05 19:18:31 +02:00
}
2016-04-08 22:16:22 +02:00
}
2023-02-22 18:58:00 +01:00
return defaults ;
2021-01-05 19:35:11 +01:00
}
2016-04-08 22:16:22 +02:00
2020-07-27 20:10:31 +02:00
/ * *
* 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
* /
2021-01-05 19:35:11 +01:00
function checkDeprecatedOptions ( userConfig ) {
const deprecated = require ( ` ${ global . root _path } /js/deprecated ` ) ;
const deprecatedOptions = deprecated . configs ;
2016-12-14 18:54:44 +01:00
2021-01-05 20:04:02 +01:00
const usedDeprecated = deprecatedOptions . filter ( ( option ) => userConfig . hasOwnProperty ( option ) ) ;
2016-12-14 18:54:44 +01:00
if ( usedDeprecated . length > 0 ) {
2021-01-05 19:35:11 +01:00
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. ` ) ) ;
2016-12-14 18:54:44 +01:00
}
2021-01-05 19:35:11 +01:00
}
2016-12-14 18:54:44 +01:00
2020-07-27 20:10:31 +02:00
/ * *
2016-04-08 22:16:22 +02:00
* Loads a specific module .
*
2020-07-27 20:10:31 +02:00
* @ param { string } module The name of the module ( including subpath ) .
2016-04-08 22:16:22 +02:00
* /
2023-02-26 18:36:47 +01:00
function loadModule ( module ) {
2021-01-05 19:35:11 +01:00
const elements = module . split ( "/" ) ;
const moduleName = elements [ elements . length - 1 ] ;
let moduleFolder = ` ${ _ _dirname } /../modules/ ${ module } ` ;
2016-04-08 22:16:22 +02:00
2021-01-05 19:35:11 +01:00
if ( defaultModules . includes ( moduleName ) ) {
moduleFolder = ` ${ _ _dirname } /../modules/default/ ${ module } ` ;
2016-04-08 22:16:22 +02:00
}
2023-01-26 12:45:17 +01:00
const moduleFile = ` ${ moduleFolder } / ${ module } .js ` ;
try {
fs . accessSync ( moduleFile , fs . R _OK ) ;
} catch ( e ) {
Log . warn ( ` No ${ moduleFile } found for module: ${ moduleName } . ` ) ;
}
2021-01-05 19:35:11 +01:00
const helperPath = ` ${ moduleFolder } /node_helper.js ` ;
2016-04-08 22:16:22 +02:00
2021-01-05 19:35:11 +01:00
let loadHelper = true ;
2016-04-08 22:16:22 +02:00
try {
fs . accessSync ( helperPath , fs . R _OK ) ;
} catch ( e ) {
2021-01-05 19:35:11 +01:00
loadHelper = false ;
Log . log ( ` No helper found for module: ${ moduleName } . ` ) ;
2016-04-08 22:16:22 +02:00
}
2021-01-05 19:35:11 +01:00
if ( loadHelper ) {
const Module = require ( helperPath ) ;
let m = new Module ( ) ;
2016-10-13 16:42:15 +02:00
if ( m . requiresVersion ) {
2022-01-26 23:47:51 +01:00
Log . log ( ` Check MagicMirror² version for node helper ' ${ moduleName } ' - Minimum version: ${ m . requiresVersion } - Current version: ${ global . version } ` ) ;
2016-10-13 16:42:15 +02:00
if ( cmpVersions ( global . version , m . requiresVersion ) >= 0 ) {
2020-05-11 07:25:42 +02:00
Log . log ( "Version is ok!" ) ;
2016-10-13 16:42:15 +02:00
} else {
2021-01-05 19:35:11 +01:00
Log . warn ( ` Version is incorrect. Skip module: ' ${ moduleName } ' ` ) ;
2016-10-13 16:42:15 +02:00
return ;
}
}
2016-04-08 22:16:22 +02:00
m . setName ( moduleName ) ;
m . setPath ( path . resolve ( moduleFolder ) ) ;
nodeHelpers . push ( m ) ;
2016-12-07 17:19:05 +01:00
2023-02-26 18:36:47 +01:00
m . loaded ( ) ;
2016-04-08 22:16:22 +02:00
}
2021-01-05 19:35:11 +01:00
}
2016-04-08 22:16:22 +02:00
2020-07-27 20:10:31 +02:00
/ * *
2016-04-08 22:16:22 +02:00
* Loads all modules .
*
2023-02-26 18:36:47 +01:00
* @ param { string [ ] } modules All modules to be loaded
2016-04-08 22:16:22 +02:00
* /
2023-02-26 18:36:47 +01:00
async function loadModules ( modules ) {
return new Promise ( ( resolve ) => {
Log . log ( "Loading module helpers ..." ) ;
/ * *
*
* /
function loadNextModule ( ) {
if ( modules . length > 0 ) {
const nextModule = modules [ 0 ] ;
loadModule ( nextModule ) ;
2016-12-07 17:19:05 +01:00
modules = modules . slice ( 1 ) ;
loadNextModule ( ) ;
2023-02-26 18:36:47 +01:00
} else {
// All modules are loaded
Log . log ( "All module helpers loaded." ) ;
resolve ( ) ;
}
2016-12-07 17:19:05 +01:00
}
2016-04-08 22:16:22 +02:00
2023-02-26 18:36:47 +01:00
loadNextModule ( ) ;
} ) ;
2021-01-05 19:35:11 +01:00
}
2016-04-08 22:16:22 +02:00
2020-07-27 14:24:30 +02:00
/ * *
2019-06-04 09:33:53 +02:00
* Compare two semantic version numbers and return the difference .
2016-10-13 16:42:15 +02:00
*
2020-07-27 14:24:30 +02:00
* @ param { string } a Version number a .
* @ param { string } b Version number b .
2020-07-27 20:10:31 +02:00
* @ 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
2016-10-13 16:42:15 +02:00
* /
function cmpVersions ( a , b ) {
2021-01-05 19:35:11 +01:00
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 ) ;
2016-10-13 16:42:15 +02:00
for ( i = 0 ; i < l ; i ++ ) {
diff = parseInt ( segmentsA [ i ] , 10 ) - parseInt ( segmentsB [ i ] , 10 ) ;
if ( diff ) {
return diff ;
}
}
return segmentsA . length - segmentsB . length ;
}
2020-07-27 20:10:31 +02:00
/ * *
* Start the core app .
2016-04-08 22:16:22 +02:00
*
2023-02-22 18:58:00 +01:00
* It loads the config , then it loads all modules .
2020-07-27 20:10:31 +02:00
*
2023-02-22 18:58:00 +01:00
* @ async
* @ returns { Promise < object > } the config used
2016-04-08 22:16:22 +02:00
* /
2023-02-22 18:58:00 +01:00
this . start = async function ( ) {
config = await loadConfig ( ) ;
2016-04-08 22:16:22 +02:00
2023-02-22 18:58:00 +01:00
Log . setLogLevel ( config . logLevel ) ;
2020-07-04 22:02:39 +02:00
2023-02-22 18:58:00 +01:00
let modules = [ ] ;
for ( const module of config . modules ) {
if ( ! modules . includes ( module . module ) && ! module . disabled ) {
modules . push ( module . module ) ;
2016-04-08 22:16:22 +02:00
}
2023-02-22 18:58:00 +01:00
}
2023-02-26 18:36:47 +01:00
await loadModules ( modules ) ;
2016-04-08 22:16:22 +02:00
2023-02-26 18:36:47 +01:00
httpServer = new Server ( config ) ;
const { app , io } = await httpServer . open ( ) ;
Log . log ( "Server started ..." ) ;
2016-04-08 22:16:22 +02:00
2023-02-26 18:36:47 +01:00
const nodePromises = [ ] ;
for ( let nodeHelper of nodeHelpers ) {
nodeHelper . setExpressApp ( app ) ;
nodeHelper . setSocketIO ( io ) ;
2022-10-19 21:40:43 +02:00
2023-02-26 18:36:47 +01:00
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 ) ;
2023-01-26 19:44:54 +01:00
2023-02-26 18:36:47 +01:00
// Log errors that happened during async node_helper startup
results . forEach ( ( result ) => {
if ( result . status === "rejected" ) {
Log . error ( result . reason ) ;
}
2016-04-08 22:16:22 +02:00
} ) ;
2023-02-22 18:58:00 +01:00
2023-02-26 18:36:47 +01:00
Log . log ( "Sockets connected & modules started ..." ) ;
2023-02-22 18:58:00 +01:00
return config ;
2021-01-05 20:04:02 +01:00
} ;
2017-10-13 16:43:11 -05:00
2020-07-27 20:10:31 +02:00
/ * *
* Stops the core app . This calls each node _helper ' s STOP ( ) function , if it
* exists .
*
2017-10-13 16:43:11 -05:00
* Added to fix # 1056
2022-10-29 22:34:17 +02:00
*
2023-02-22 18:58:00 +01:00
* @ returns { Promise } A promise that is resolved when all node _helpers and
* the http server has been closed
2017-10-13 16:43:11 -05:00
* /
2023-02-22 18:58:00 +01:00
this . stop = async function ( ) {
const nodePromises = [ ] ;
for ( let nodeHelper of nodeHelpers ) {
try {
if ( typeof nodeHelper . stop === "function" ) {
nodePromises . push ( nodeHelper . stop ( ) ) ;
}
} catch ( error ) {
Log . error ( ` Error when stopping node_helper for module ${ nodeHelper . name } : ` ) ;
console . error ( error ) ;
2017-10-13 16:43:11 -05:00
}
}
2023-02-22 18:58:00 +01:00
const results = await Promise . allSettled ( nodePromises ) ;
// Log errors that happened during async node_helper stopping
results . forEach ( ( result ) => {
if ( result . status === "rejected" ) {
Log . error ( result . reason ) ;
}
} ) ;
Log . log ( "Node_helpers stopped ..." ) ;
// To be able to stop the app even if it hasn't been started (when
// running with Electron against another server)
if ( ! httpServer ) {
return Promise . resolve ( ) ;
}
return httpServer . close ( ) ;
2017-10-13 16:43:11 -05:00
} ;
2020-07-27 20:10:31 +02:00
/ * *
* Listen for SIGINT signal and call stop ( ) function .
2017-10-13 16:43:11 -05:00
*
* 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 `
* /
2023-02-22 18:58:00 +01:00
process . on ( "SIGINT" , async ( ) => {
2020-05-11 07:25:42 +02:00
Log . log ( "[SIGINT] Received. Shutting down server..." ) ;
2020-05-25 18:57:15 +02:00
setTimeout ( ( ) => {
process . exit ( 0 ) ;
} , 3000 ) ; // Force quit after 3 seconds
2023-02-22 18:58:00 +01:00
await this . stop ( ) ;
2017-10-13 16:43:11 -05:00
process . exit ( 0 ) ;
} ) ;
2019-04-07 16:41:05 +02:00
2020-07-27 20:10:31 +02:00
/ * *
* Listen to SIGTERM signals so we can stop everything when we
* are asked to stop by the OS .
2019-04-06 20:50:54 +02:00
* /
2023-02-22 18:58:00 +01:00
process . on ( "SIGTERM" , async ( ) => {
2020-05-11 07:25:42 +02:00
Log . log ( "[SIGTERM] Received. Shutting down server..." ) ;
2020-05-25 18:57:15 +02:00
setTimeout ( ( ) => {
process . exit ( 0 ) ;
} , 3000 ) ; // Force quit after 3 seconds
2023-02-22 18:58:00 +01:00
await this . stop ( ) ;
2019-04-06 20:50:54 +02:00
process . exit ( 0 ) ;
} ) ;
2021-01-05 19:35:11 +01:00
}
2016-04-08 22:16:22 +02:00
2016-11-14 14:43:30 -03:00
module . exports = new App ( ) ;