Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3rd party modules updater for updatenotification #3150

Merged
merged 31 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34a9e7d
Merge pull request #1 from MichMich/develop
bugsounet Jun 7, 2023
6444af3
sendUpdatesNotifications feature
Jun 7, 2023
8f38ea7
missing const
Jun 7, 2023
7206f27
mismake, sorry
Jun 7, 2023
a03803e
set `sendUpdatesNotifications` feature to false
bugsounet Jun 8, 2023
4350b65
sync develop sources of MM²
bugsounet Jun 18, 2023
8dcb8f8
Remote force check update (#3127)
bugsounet Jun 19, 2023
3a96ea7
Update dependencies and fix dependabot issues (#3134)
bugsounet Jun 25, 2023
272c2d3
Develop sync
bugsounet Jul 14, 2023
777748a
Merge branch 'updates-notification' into develop-source
bugsounet Jul 14, 2023
2bfda92
Develop source sync
bugsounet Jul 14, 2023
c8d512d
add updater to updatenotification default module
bugsounet Jul 14, 2023
89ea2a3
correct for eslint
bugsounet Jul 14, 2023
ba28952
update package-lock
bugsounet Jul 14, 2023
c9e453f
correct pm2 restarting
bugsounet Jul 14, 2023
2990410
Develop
bugsounet Aug 2, 2023
d64aa4c
Merge branch 'updates-notification' into develop-source
bugsounet Aug 2, 2023
c001822
Develop source
bugsounet Aug 2, 2023
375311c
delete pm2 dependency and use child_process
bugsounet Aug 2, 2023
d14cb4c
screen callbacks of Updates status with translations
bugsounet Aug 2, 2023
beded0b
added de.json translation file (thx to @lxne)
bugsounet Aug 3, 2023
f16d7ef
rewrite updater in async
bugsounet Aug 27, 2023
e0a63d4
Merge branch 'develop' into updates-notification
bugsounet Aug 27, 2023
133e124
delete updatesCallbacks (not used)
bugsounet Aug 27, 2023
3a0eeee
Merge branch 'updates-notification' of https://github.com/bugsounet/M…
bugsounet Aug 27, 2023
5b11ae0
comment how this class work
bugsounet Sep 1, 2023
3274c2b
Merge branch 'develop' into updates-notification
bugsounet Sep 3, 2023
898c1d7
Merge branch 'develop' into updates-notification
bugsounet Oct 7, 2023
b7dffce
ChangeLog move to v2.26
bugsounet Oct 7, 2023
aec008b
Merge branch 'develop' into updates-notification
bugsounet Nov 4, 2023
bf1238d
force sync from develop
bugsounet Nov 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 18 additions & 4 deletions modules/default/updatenotification/node_helper.js
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,6 +12,7 @@ module.exports = NodeHelper.create({
updateProcessStarted: false,

gitHelper: new GitHelper(),
updateHelper: null,

async configureModules(modules) {
for (const moduleName of modules) {
Expand All @@ -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
Expand All @@ -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);
Expand Down
224 changes: 224 additions & 0 deletions modules/default/updatenotification/update_helper.js
Original file line number Diff line number Diff line change
@@ -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 {
bugsounet marked this conversation as resolved.
Show resolved Hide resolved
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;
38 changes: 35 additions & 3 deletions modules/default/updatenotification/updatenotification.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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;
}
},

Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}
}
});
26 changes: 26 additions & 0 deletions modules/default/updatenotification/updatenotification.njk
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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 %}
Loading