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:
Bugsounet - Cédric 2023-11-10 12:43:34 +01:00 committed by GitHub
parent 3fe5ad4b3d
commit 203e8647d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 323 additions and 10 deletions

View file

@ -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

View file

@ -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);

View 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;

View file

@ -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);
}
}
});

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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."
}