Release 2.33.0 (#3903)

This commit is contained in:
Kristjan ESPERANTO
2025-09-30 18:02:22 +02:00
committed by GitHub
parent 62b0f7f26e
commit b0c5924019
77 changed files with 2811 additions and 2654 deletions

View File

@@ -6,26 +6,26 @@ const path = require("node:path");
const envsub = require("envsub");
const Log = require("logger");
// global absolute root path
global.root_path = path.resolve(`${__dirname}/../`);
const Server = require(`${__dirname}/server`);
const Utils = require(`${__dirname}/utils`);
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
// used to control fetch timeout for node_helpers
const { setGlobalDispatcher, Agent } = require("undici");
const { getEnvVarsAsObj } = require("#server_functions");
// common timeout value, provide environment override in case
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
// Get version number.
global.version = require(`${__dirname}/../package.json`).version;
global.version = require(`${global.root_path}/package.json`).version;
global.mmTestMode = process.env.mmTestMode === "true";
Log.log(`Starting MagicMirror: v${global.version}`);
// Log system information.
Utils.logSystemInformation();
// global absolute root path
global.root_path = path.resolve(`${__dirname}/../`);
Utils.logSystemInformation(global.version);
if (process.env.MM_CONFIG_FILE) {
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
@@ -181,10 +181,10 @@ function App () {
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
const env = getEnvVarsAsObj();
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
let moduleFolder = path.resolve(`${global.root_path}/${env.modulesDir}`, module);
if (defaultModules.includes(moduleName)) {
const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module);
const defaultModuleFolder = path.resolve(`${global.root_path}/modules/default/`, module);
if (process.env.JEST_WORKER_ID === undefined) {
moduleFolder = defaultModuleFolder;
} else {

View File

@@ -90,7 +90,7 @@ const MM = (function () {
/**
* Send a notification to all modules.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
* @param {Module} [sendTo] The (optional) module to send the notification to.
*/
@@ -262,7 +262,7 @@ const MM = (function () {
* Hide the module.
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
const hideModule = function (module, speed, callback, options = {}) {
@@ -347,7 +347,7 @@ const MM = (function () {
* Show the module.
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
const showModule = function (module, speed, callback, options = {}) {
@@ -552,7 +552,7 @@ const MM = (function () {
/**
* Walks thru a collection of modules and executes the callback with the module as an argument.
* @param {Function} callback The function to execute with the module as an argument.
* @param {module} callback The function to execute with the module as an argument.
*/
const enumerate = function (callback) {
modules.map(function (module) {
@@ -629,7 +629,7 @@ const MM = (function () {
/**
* Send a notification to all modules.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
*/
sendNotification (notification, payload, sender) {
@@ -688,7 +688,7 @@ const MM = (function () {
* Hide the module.
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
hideModule (module, speed, callback, options) {
@@ -700,7 +700,7 @@ const MM = (function () {
* Show the module.
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
showModule (module, speed, callback, options) {

View File

@@ -68,7 +68,7 @@ const Module = Class.extend({
* Returns a map of translation files the module requires to be loaded.
*
* return Map<String, String> -
* @returns {*} A map with langKeys and filenames.
* @returns {Map} A map with langKeys and filenames.
*/
getTranslations () {
return false;
@@ -140,7 +140,7 @@ const Module = Class.extend({
/**
* Called by the MagicMirror² core when a notification arrives.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
*/
notificationReceived (notification, payload, sender) {
@@ -176,7 +176,7 @@ const Module = Class.extend({
/**
* Called when a socket notification arrives.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
*/
socketNotificationReceived (notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
@@ -344,7 +344,7 @@ const Module = Class.extend({
/**
* Send a notification to all modules.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
*/
sendNotification (notification, payload) {
MM.sendNotification(notification, payload, this);
@@ -353,7 +353,7 @@ const Module = Class.extend({
/**
* Send a socket notification to the node helper.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
*/
sendSocketNotification (notification, payload) {
this.socket().sendNotification(notification, payload);
@@ -362,7 +362,7 @@ const Module = Class.extend({
/**
* Hide this module.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
hide (speed, callback, options = {}) {
@@ -389,7 +389,7 @@ const Module = Class.extend({
/**
* Show this module.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {Promise} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
show (speed, callback, options) {

18
js/module_functions.js Normal file
View File

@@ -0,0 +1,18 @@
/**
* Schedule the timer for the next update
* @param {object} timer The timer of the module
* @param {bigint} intervalMS interval in milliseconds
* @param {Promise} callback function to call when the timer expires
*/
const scheduleTimer = function (timer, intervalMS, callback) {
if (process.env.JEST_WORKER_ID === undefined) {
// only set timer when not running in jest
let tmr = timer;
clearTimeout(tmr);
tmr = setTimeout(function () {
callback();
}, intervalMS);
}
};
module.exports = { scheduleTimer };

View File

@@ -27,7 +27,7 @@ const NodeHelper = Class.extend({
/**
* This method is called when a socket notification arrives.
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {object} payload The payload of the notification.
*/
socketNotificationReceived (notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);

View File

@@ -7,7 +7,7 @@ const ipfilter = require("express-ipfilter").IpFilter;
const helmet = require("helmet");
const socketio = require("socket.io");
const Log = require("logger");
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("#server_functions");
const vendor = require(`${__dirname}/vendor`);
@@ -42,7 +42,9 @@ function Server (config) {
origin: /.*$/,
credentials: true
},
allowEIO3: true
allowEIO3: true,
pingInterval: 120000, // server → client ping every 2 mins
pingTimeout: 120000 // wait up to 2 mins for client pong
});
server.on("connection", (socket) => {
@@ -53,6 +55,29 @@ function Server (config) {
});
Log.log(`Starting server on port ${port} ... `);
// Add explicit error handling BEFORE calling listen so we can give user-friendly feedback
server.once("error", (err) => {
if (err && err.code === "EADDRINUSE") {
const bindAddr = config.address || "localhost";
const portInUseMessage = [
"",
"────────────────────────────────────────────────────────────────",
` PORT IN USE: ${bindAddr}:${port}`,
"",
" Another process (most likely another MagicMirror instance)",
" is already using this port.",
"",
" Stop the other process (free the port) or use a different port.",
"────────────────────────────────────────────────────────────────"
].join("\n");
Log.error(portInUseMessage);
return;
}
Log.error("Failed to start server:", err);
});
server.listen(port, config.address || "localhost");
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {

View File

@@ -69,7 +69,7 @@ async function cors (req, res) {
* @returns {object} An object specifying name and value of the headers.
*/
function getHeadersToSend (url) {
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
const headersToSend = { "User-Agent": getUserAgent() };
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
if (headersToSendMatch) {
const headers = headersToSendMatch[1].split(",");
@@ -129,6 +129,27 @@ function getVersion (req, res) {
res.send(global.version);
}
/**
* Gets the preferred `User-Agent`
* @returns {string} `User-Agent` to be used
*/
function getUserAgent () {
const defaultUserAgent = `Mozilla/5.0 (Node.js ${Number(process.version.match(/^v(\d+\.\d+)/)[1])}) MagicMirror/${global.version}`;
if (typeof config === "undefined") {
return defaultUserAgent;
}
switch (typeof config.userAgent) {
case "function":
return config.userAgent();
case "string":
return config.userAgent;
default:
return defaultUserAgent;
}
}
/**
* Gets environment variables needed in the browser.
* @returns {object} environment variables key: values
@@ -155,4 +176,4 @@ function getEnvVars (req, res) {
res.send(obj);
}
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent };

View File

@@ -13,7 +13,9 @@ const MMSocket = function (moduleName) {
base = config.basePath;
}
this.socket = io(`/${this.moduleName}`, {
path: `${base}socket.io`
path: `${base}socket.io`,
pingInterval: 120000, // send pings every 2 mins
pingTimeout: 120000 // wait up to 2 mins for a pong
});
let notificationCallback = function () {};

View File

@@ -1,4 +1,3 @@
const execSync = require("node:child_process").execSync;
const path = require("node:path");
const rootPath = path.resolve(`${__dirname}/../`);
@@ -14,27 +13,34 @@ const discoveredPositionsJSFilename = "js/positions.js";
module.exports = {
async logSystemInformation () {
async logSystemInformation (mirrorVersion) {
try {
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
const system = await si.system();
const osInfo = await si.osInfo();
const versions = await si.versions();
const staticData = await si.get({
system: "manufacturer, model, virtual",
osInfo: "platform, distro, release, arch",
versions: "kernel, node, npm, pm2"
});
let systemDataString = `System information:
### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}
### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}
### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}
### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
.replace(/\t/g, "");
const usedNodeVersion = process.version.replace("v", "");
const installedNodeVersion = versions.node;
const totalRam = (os.totalmem() / 1024 / 1024).toFixed(2);
const freeRam = (os.freemem() / 1024 / 1024).toFixed(2);
const usedRam = ((os.totalmem() - os.freemem()) / 1024 / 1024).toFixed(2);
let systemDataString = [
"\n#### System Information ####",
`- SYSTEM: manufacturer: ${system.manufacturer}; model: ${system.model}; virtual: ${system.virtual}; MM: ${mirrorVersion}`,
`- OS: platform: ${osInfo.platform}; distro: ${osInfo.distro}; release: ${osInfo.release}; arch: ${osInfo.arch}; kernel: ${versions.kernel}`,
`- VERSIONS: electron: ${process.versions.electron}; used node: ${usedNodeVersion}; installed node: ${installedNodeVersion}; npm: ${versions.npm}; pm2: ${versions.pm2}`,
`- ENV: XDG_SESSION_TYPE: ${process.env.XDG_SESSION_TYPE}; MM_CONFIG_FILE: ${process.env.MM_CONFIG_FILE}`,
` WAYLAND_DISPLAY: ${process.env.WAYLAND_DISPLAY}; DISPLAY: ${process.env.DISPLAY}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`,
`- RAM: total: ${totalRam} MB; free: ${freeRam} MB; used: ${usedRam} MB`,
`- OTHERS: uptime: ${Math.floor(os.uptime() / 60)} minutes; timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`
].join("\n");
Log.info(systemDataString);
// Return is currently only for jest
return systemDataString;
} catch (e) {
Log.error(e);
} catch (error) {
Log.error(error);
}
},