mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 17:01:08 -04:00
3rd party modules updater for updatenotification (#3150)
Added my (modified) updater main core into updatenotification default module Missing: callback display in MM² (i will code it after) new part of configuration added: ``` updates: [ // array of module update commands { // with embed npm script "MMM-Test": "npm run update" }, { // with "complex" process "MMM-OtherSample": "rm -rf package-lock.json && git reset --hard && git pull && npm install" }, { // with git pull && npm install "MMM-OtherSample2": "git pull && npm install" }, { // with a simple git pull "MMM-OtherSample3": "git pull" } ], updateTimeout: 2 * 60 * 1000, // max update duration updateAutorestart: false // autoRestart MM when update done ? ``` @khassel: i need your help I don't use docker, maybe you can help me for this: How can i check if MM² is running inside a docker ? (from MM² main core) Actually, I check if we use pm2 or not. I have to check if docker is used or not too last time you tell me: "you can't use updater with docker", so I want to check and deny any update if docker used --------- Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
This commit is contained in:
parent
3fe5ad4b3d
commit
203e8647d4
10 changed files with 323 additions and 10 deletions
|
@ -11,6 +11,7 @@ _This release is scheduled to be released on 2024-01-01._
|
|||
|
||||
### Added
|
||||
|
||||
- Added updatenotification Updater (for 3rd party modules)
|
||||
- Added node 21 to the test matrix
|
||||
- Added transform object to calendar:customEvents
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const GitHelper = require("./git_helper");
|
||||
const UpdateHelper = require("./update_helper");
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
|
@ -11,6 +12,7 @@ module.exports = NodeHelper.create({
|
|||
updateProcessStarted: false,
|
||||
|
||||
gitHelper: new GitHelper(),
|
||||
updateHelper: null,
|
||||
|
||||
async configureModules(modules) {
|
||||
for (const moduleName of modules) {
|
||||
|
@ -28,6 +30,8 @@ module.exports = NodeHelper.create({
|
|||
switch (notification) {
|
||||
case "CONFIG":
|
||||
this.config = payload;
|
||||
this.updateHelper = new UpdateHelper(this.config);
|
||||
await this.updateHelper.check_PM2_Process();
|
||||
break;
|
||||
case "MODULES":
|
||||
// if this is the 1st time thru the update check process
|
||||
|
@ -51,12 +55,22 @@ module.exports = NodeHelper.create({
|
|||
const repos = await this.gitHelper.getRepos();
|
||||
|
||||
for (const repo of repos) {
|
||||
this.sendSocketNotification("STATUS", repo);
|
||||
this.sendSocketNotification("REPO_STATUS", repo);
|
||||
}
|
||||
|
||||
if (this.config.sendUpdatesNotifications) {
|
||||
const updates = await this.gitHelper.checkUpdates();
|
||||
if (updates.length) this.sendSocketNotification("UPDATES", updates);
|
||||
const updates = await this.gitHelper.checkUpdates();
|
||||
|
||||
if (this.config.sendUpdatesNotifications && updates.length) {
|
||||
this.sendSocketNotification("UPDATES", updates);
|
||||
}
|
||||
|
||||
if (updates.length) {
|
||||
const updateResult = await this.updateHelper.parse(updates);
|
||||
for (const update of updateResult) {
|
||||
if (update.inProgress) {
|
||||
this.sendSocketNotification("UPDATE_STATUS", update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
|
|
224
modules/default/updatenotification/update_helper.js
Normal file
224
modules/default/updatenotification/update_helper.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
const Exec = require("child_process").exec;
|
||||
const Spawn = require("child_process").spawn;
|
||||
const commandExists = require("command-exists");
|
||||
const Log = require("logger");
|
||||
|
||||
/* class Updater
|
||||
* Allow to self updating 3rd party modules from command defined in config
|
||||
*
|
||||
* [constructor] read value in config:
|
||||
* updates: [ // array of modules update commands
|
||||
* {
|
||||
* <module name>: <update command>
|
||||
* },
|
||||
* {
|
||||
* ...
|
||||
* }
|
||||
* ],
|
||||
* updateTimeout: 2 * 60 * 1000, // max update duration
|
||||
* updateAutorestart: false // autoRestart MM when update done ?
|
||||
*
|
||||
* [main command]: parse(<Array of modules>):
|
||||
* parse if module update is needed
|
||||
* --> Apply ONLY one update (first of the module list)
|
||||
* --> auto-restart MagicMirror or wait manual restart by user
|
||||
* return array with modules update state information for `updatenotification` module displayer information
|
||||
* [
|
||||
* {
|
||||
* name = <module-name>, // name of the module
|
||||
* updateCommand = <update command>, // update command (if found)
|
||||
* inProgress = <boolean>, // an update if in progress for this module
|
||||
* error = <boolean>, // an error if detected when updating
|
||||
* updated = <boolean>, // updated successfully
|
||||
* needRestart = <boolean> // manual restart of MagicMirror is required by user
|
||||
* },
|
||||
* {
|
||||
* ...
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
|
||||
class Updater {
|
||||
constructor(config) {
|
||||
this.updates = config.updates;
|
||||
this.timeout = config.updateTimeout;
|
||||
this.autoRestart = config.updateAutorestart;
|
||||
this.moduleList = {};
|
||||
this.updating = false;
|
||||
this.usePM2 = false;
|
||||
this.PM2 = null;
|
||||
this.version = global.version;
|
||||
this.root_path = global.root_path;
|
||||
Log.info("updatenotification: Updater Class Loaded!");
|
||||
}
|
||||
|
||||
// [main command] parse if module update is needed
|
||||
async parse(modules) {
|
||||
var parser = modules.map(async (module) => {
|
||||
if (this.moduleList[module.module] === undefined) {
|
||||
this.moduleList[module.module] = {};
|
||||
this.moduleList[module.module].name = module.module;
|
||||
this.moduleList[module.module].updateCommand = await this.applyCommand(module.module);
|
||||
this.moduleList[module.module].inProgress = false;
|
||||
this.moduleList[module.module].error = null;
|
||||
this.moduleList[module.module].updated = false;
|
||||
this.moduleList[module.module].needRestart = false;
|
||||
}
|
||||
if (!this.moduleList[module.module].inProgress) {
|
||||
if (!this.updating) {
|
||||
if (!this.moduleList[module.module].updateCommand) {
|
||||
this.updating = false;
|
||||
} else {
|
||||
this.updating = true;
|
||||
this.moduleList[module.module].inProgress = true;
|
||||
Object.assign(this.moduleList[module.module], await this.updateProcess(this.moduleList[module.module]));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(parser);
|
||||
let updater = Object.values(this.moduleList);
|
||||
Log.debug("updatenotification Update Result:", updater);
|
||||
return updater;
|
||||
}
|
||||
|
||||
// module updater with his proper command
|
||||
// return object as result
|
||||
//{
|
||||
// error: <boolean>, // if error detected
|
||||
// updated: <boolean>, // if updated successfully
|
||||
// needRestart: <boolean> // if magicmirror restart required
|
||||
//};
|
||||
updateProcess(module) {
|
||||
let Result = {
|
||||
error: false,
|
||||
updated: false,
|
||||
needRestart: false
|
||||
};
|
||||
let Command = null;
|
||||
const Path = `${this.root_path}/modules/`;
|
||||
const modulePath = Path + module.name;
|
||||
|
||||
if (module.updateCommand) {
|
||||
Command = module.updateCommand;
|
||||
} else {
|
||||
Log.warn(`updatenotification: Update of ${module.name} is not supported.`);
|
||||
return Result;
|
||||
}
|
||||
Log.info(`updatenotification: Updating ${module.name}...`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Exec(Command, { cwd: modulePath, timeout: this.timeout }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
Log.error(`updatenotification: exec error: ${error}`);
|
||||
Result.error = true;
|
||||
} else {
|
||||
Log.info(`updatenotification: Update logs of ${module.name}: ${stdout}`);
|
||||
Result.updated = true;
|
||||
if (this.autoRestart) {
|
||||
Log.info("updatenotification: Update done");
|
||||
setTimeout(() => this.restart(), 3000);
|
||||
} else {
|
||||
Log.info("updatenotification: Update done, don't forget to restart MagicMirror!");
|
||||
Result.needRestart = true;
|
||||
}
|
||||
}
|
||||
resolve(Result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// restart rules (pm2 or npm start)
|
||||
restart() {
|
||||
if (this.usePM2) this.pm2Restart();
|
||||
else this.npmRestart();
|
||||
}
|
||||
|
||||
// restart MagicMiror with "pm2"
|
||||
pm2Restart() {
|
||||
Log.info("updatenotification: PM2 will restarting MagicMirror...");
|
||||
Exec(`pm2 restart ${this.PM2}`, (err, std, sde) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification:[PM2] restart Error", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// restart MagicMiror with "npm start"
|
||||
npmRestart() {
|
||||
Log.info("updatenotification: Restarting MagicMirror...");
|
||||
const out = process.stdout;
|
||||
const err = process.stderr;
|
||||
const subprocess = Spawn("npm start", { cwd: this.root_path, shell: true, detached: true, stdio: ["ignore", out, err] });
|
||||
subprocess.unref();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// Check using pm2
|
||||
check_PM2_Process() {
|
||||
Log.info("updatenotification: Checking PM2 using...");
|
||||
return new Promise((resolve) => {
|
||||
commandExists("pm2")
|
||||
.then(async () => {
|
||||
var PM2_List = await this.PM2_GetList();
|
||||
if (!PM2_List) {
|
||||
Log.error("updatenotification: [PM2] Can't get process List!");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
PM2_List.forEach((pm) => {
|
||||
if (pm.pm2_env.version === this.version && pm.pm2_env.status === "online" && pm.pm2_env.PWD.includes(this.root_path)) {
|
||||
this.PM2 = pm.name;
|
||||
this.usePM2 = true;
|
||||
Log.info("updatenotification: You are using pm2 with", this.PM2);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
if (!this.PM2) {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the list of pm2 process
|
||||
PM2_GetList() {
|
||||
return new Promise((resolve) => {
|
||||
Exec("pm2 jlist", (err, std, sde) => {
|
||||
if (err) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
let result = JSON.parse(std);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// check if module is MagicMirror
|
||||
isMagicMirror(module) {
|
||||
if (module === "MagicMirror") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// search update module command
|
||||
applyCommand(module) {
|
||||
if (this.isMagicMirror(module.module)) return null;
|
||||
let command = null;
|
||||
this.updates.forEach((updater) => {
|
||||
if (updater[module]) command = updater[module];
|
||||
});
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Updater;
|
|
@ -9,11 +9,16 @@ Module.register("updatenotification", {
|
|||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: [],
|
||||
sendUpdatesNotifications: false
|
||||
sendUpdatesNotifications: false,
|
||||
updates: [],
|
||||
updateTimeout: 2 * 60 * 1000, // max update duration
|
||||
updateAutorestart: false // autoRestart MM when update done ?
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
needRestart: false,
|
||||
updates: {},
|
||||
|
||||
start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
@ -47,12 +52,15 @@ Module.register("updatenotification", {
|
|||
|
||||
socketNotificationReceived(notification, payload) {
|
||||
switch (notification) {
|
||||
case "STATUS":
|
||||
case "REPO_STATUS":
|
||||
this.updateUI(payload);
|
||||
break;
|
||||
case "UPDATES":
|
||||
this.sendNotification("UPDATES", payload);
|
||||
break;
|
||||
case "UPDATE_STATUS":
|
||||
this.updatesNotifier(payload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -65,7 +73,7 @@ Module.register("updatenotification", {
|
|||
},
|
||||
|
||||
getTemplateData() {
|
||||
return { moduleList: this.moduleList, suspended: this.suspended };
|
||||
return { moduleList: this.moduleList, updatesList: this.updates, suspended: this.suspended, needRestart: this.needRestart };
|
||||
},
|
||||
|
||||
updateUI(payload) {
|
||||
|
@ -96,5 +104,29 @@ Module.register("updatenotification", {
|
|||
const remoteRef = status.tracking.replace(/.*\//, "");
|
||||
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
});
|
||||
},
|
||||
|
||||
updatesNotifier(payload, done = true) {
|
||||
if (this.updates[payload.name] === undefined) {
|
||||
this.updates[payload.name] = {
|
||||
name: payload.name,
|
||||
done: done
|
||||
};
|
||||
|
||||
if (payload.error) {
|
||||
this.sendSocketNotification("UPDATE_ERROR", payload.name);
|
||||
this.updates[payload.name].done = false;
|
||||
} else {
|
||||
if (payload.updated) {
|
||||
delete this.moduleList[payload.name];
|
||||
this.updates[payload.name].done = true;
|
||||
}
|
||||
if (payload.needRestart) {
|
||||
this.needRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateDom(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
{% if not suspended %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
|
@ -12,4 +21,21 @@
|
|||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"console-stamp": "^3.1.2",
|
||||
"envsub": "^4.1.0",
|
||||
"eslint": "^8.52.0",
|
||||
|
@ -2860,6 +2861,11 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/command-exists": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
|
||||
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"console-stamp": "^3.1.2",
|
||||
"envsub": "^4.1.0",
|
||||
"eslint": "^8.52.0",
|
||||
|
|
|
@ -43,5 +43,8 @@
|
|||
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das Modul „{MODULE_NAME}“ verfügbar.",
|
||||
"UPDATE_INFO_SINGLE": "Die aktuelle Installation ist ein Commit hinter dem {BRANCH_NAME}-Branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commits hinter dem {BRANCH_NAME}-Branch."
|
||||
"UPDATE_INFO_MULTIPLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commits hinter dem {BRANCH_NAME}-Branch.",
|
||||
"UPDATE_NOTIFICATION_DONE": "Aktualisierung für das Modul {MODULE_NAME} abgeschlossen.",
|
||||
"UPDATE_NOTIFICATION_ERROR": "Fehler bei der Aktualisierung für das Modul {MODULE_NAME}.",
|
||||
"UPDATE_NOTIFICATION_NEED-RESTART": "MagicMirror muss neu gestartet werden."
|
||||
}
|
||||
|
|
|
@ -41,5 +41,8 @@
|
|||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
|
||||
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",
|
||||
"UPDATE_INFO_MULTIPLE": "The current installation is {COMMIT_COUNT} commits behind on the {BRANCH_NAME} branch."
|
||||
"UPDATE_INFO_MULTIPLE": "The current installation is {COMMIT_COUNT} commits behind on the {BRANCH_NAME} branch.",
|
||||
"UPDATE_NOTIFICATION_DONE": "Update done for {MODULE_NAME} module",
|
||||
"UPDATE_NOTIFICATION_ERROR": "Update error for {MODULE_NAME} module",
|
||||
"UPDATE_NOTIFICATION_NEED-RESTART": "Restarting of MagicMirror is required."
|
||||
}
|
||||
|
|
|
@ -43,5 +43,8 @@
|
|||
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "L'installation actuelle est {COMMIT_COUNT} commit en retard sur la branche {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "L'installation actuelle est {COMMIT_COUNT} commits en retard sur la branche {BRANCH_NAME}."
|
||||
"UPDATE_INFO_MULTIPLE": "L'installation actuelle est {COMMIT_COUNT} commits en retard sur la branche {BRANCH_NAME}.",
|
||||
"UPDATE_NOTIFICATION_DONE": "Mise à jour effectuée pour le module {MODULE_NAME}",
|
||||
"UPDATE_NOTIFICATION_ERROR": "Erreur lors de la mise à jour du module {MODULE_NAME}",
|
||||
"UPDATE_NOTIFICATION_NEED-RESTART": "Le redémarrage de MagicMirror est nécessaire."
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue