refactor(updatenotification): replace pm2 usage with node logic (#4134)

We can rely on PM2's native restart-on-exit behavior instead of the
programmatic pm2 API.

Fixes
https://github.com/MagicMirrorOrg/MagicMirror/security/dependabot/82 by
removing pm2.

Note: Originally this PR was intended to update pm2, but after
discussion, we decided to replace it instead. See the discussion below.
This commit is contained in:
Kristjan ESPERANTO
2026-05-03 09:51:08 +02:00
committed by GitHub
parent 0905d66a91
commit f2759ad4f6
5 changed files with 112 additions and 1407 deletions
@@ -49,7 +49,6 @@ module.exports = NodeHelper.create({
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
@@ -1,6 +1,5 @@
const Exec = require("node:child_process").exec;
const Spawn = require("node:child_process").spawn;
const fs = require("node:fs");
const Log = require("logger");
@@ -47,8 +46,6 @@ class Updater {
this.autoRestart = config.updateAutorestart;
this.moduleList = {};
this.updating = false;
this.usePM2 = false; // don't use pm2 by default
this.PM2Id = null; // pm2 process number
this.version = global.version;
this.root_path = global.root_path;
Log.info("Updater Class Loaded!");
@@ -122,7 +119,7 @@ class Updater {
Result.updated = true;
if (this.autoRestart) {
Log.info("Update done");
setTimeout(() => this.restart(), 3000);
setTimeout(() => this.nodeRestart(), 3000);
} else {
Log.info("Update done, don't forget to restart MagicMirror!");
Result.needRestart = true;
@@ -133,23 +130,6 @@ class Updater {
});
}
// restart rules (pm2 or node --run start)
restart () {
if (this.usePM2) this.pm2Restart();
else this.nodeRestart();
}
// restart MagicMirror with "pm2": use PM2Id for restart it
pm2Restart () {
Log.info("[PM2] restarting MagicMirror...");
const pm2 = require("pm2");
pm2.restart(this.PM2Id, (err) => {
if (err) {
Log.error("[PM2] restart Error", err);
}
});
}
// restart MagicMirror with "node --run start"
nodeRestart () {
Log.info("Restarting MagicMirror...");
@@ -160,58 +140,6 @@ class Updater {
process.exit();
}
// Check using pm2
check_PM2_Process () {
Log.info("Checking PM2 using...");
return new Promise((resolve) => {
if (fs.existsSync("/.dockerenv")) {
Log.info("[PM2] Running in docker container, not using PM2 ...");
resolve(false);
return;
}
if (process.env.unique_id === undefined) {
Log.info("[PM2] You are not using pm2");
resolve(false);
return;
}
Log.debug(`[PM2] Search for pm2 id: ${process.env.pm_id} -- name: ${process.env.name} -- unique_id: ${process.env.unique_id}`);
const pm2 = require("pm2");
pm2.connect((err) => {
if (err) {
Log.error("[PM2]", err);
resolve(false);
return;
}
pm2.list((err, list) => {
if (err) {
Log.error("[PM2] Can't get process List!");
resolve(false);
return;
}
list.forEach((pm) => {
Log.debug(`[PM2] found pm2 process id: ${pm.pm_id} -- name: ${pm.name} -- unique_id: ${pm.pm2_env.unique_id}`);
if (pm.pm2_env.status === "online" && process.env.name === pm.name && +process.env.pm_id === +pm.pm_id && process.env.unique_id === pm.pm2_env.unique_id) {
this.PM2Id = pm.pm_id;
this.usePM2 = true;
Log.info(`[PM2] You are using pm2 with id: ${this.PM2Id} (${pm.name})`);
resolve(true);
} else {
Log.debug(`[PM2] pm2 process id: ${pm.pm_id} don't match...`);
}
});
pm2.disconnect();
if (!this.usePM2) {
Log.info("[PM2] You are not using pm2");
resolve(false);
}
});
});
});
}
// check if module is MagicMirror
isMagicMirror (module) {
if (module === "MagicMirror") return true;
+21 -1332
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -105,7 +105,6 @@
"moment-timezone": "^0.6.2",
"node-ical": "^0.26.0",
"nunjucks": "^3.2.4",
"pm2": "^6.0.14",
"socket.io": "^4.8.3",
"suncalc": "^1.9.0",
"systeminformation": "^5.31.5",
@@ -0,0 +1,90 @@
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("UpdateHelper", () => {
const originalEnv = { ...process.env };
const tempRoots = [];
beforeEach(() => {
vi.resetModules();
process.env = { ...originalEnv };
global.version = "test";
global.root_path = process.cwd();
});
afterEach(() => {
process.env = originalEnv;
vi.useRealTimers();
vi.restoreAllMocks();
for (const tempRoot of tempRoots) {
rmSync(tempRoot, { recursive: true, force: true });
}
tempRoots.length = 0;
});
/**
* Creates a temporary MagicMirror-like root with a module directory.
* @param {string} moduleName - Name of the module directory to create.
* @returns {{ root: string, modulePath: string }} Created paths.
*/
function createTempModuleRoot (moduleName) {
const root = mkdtempSync(join(tmpdir(), "mm-updater-"));
const modulePath = join(root, "modules", moduleName);
mkdirSync(modulePath, { recursive: true });
tempRoots.push(root);
return { root, modulePath };
}
/**
* Creates a fresh UpdateHelper instance for testing.
* @param {object} config - Optional config overrides.
* @returns {Promise<object>} Resolved UpdateHelper instance.
*/
async function createUpdater (config = {}) {
const updateHelperModule = await import("../../../defaultmodules/updatenotification/update_helper");
const UpdateHelper = updateHelperModule.default || updateHelperModule;
return new UpdateHelper({ updates: [], updateTimeout: 1000, updateAutorestart: false, ...config });
}
it("marks update as requiring manual restart when autoRestart is disabled", async () => {
const moduleName = "MMM-Test";
const { root } = createTempModuleRoot(moduleName);
global.root_path = root;
const updater = await createUpdater({ updateAutorestart: false });
const result = await updater.updateProcess({
name: moduleName,
updateCommand: `"${process.execPath}" -p 1`
});
expect(result.error).toBe(false);
expect(result.updated).toBe(true);
expect(result.needRestart).toBe(true);
});
it("schedules node restart when autoRestart is enabled", async () => {
vi.useFakeTimers();
const moduleName = "MMM-Test";
const { root } = createTempModuleRoot(moduleName);
global.root_path = root;
const updater = await createUpdater({ updateAutorestart: true });
const nodeRestartSpy = vi.spyOn(updater, "nodeRestart").mockImplementation(() => {});
const result = await updater.updateProcess({
name: moduleName,
updateCommand: `"${process.execPath}" -p 1`
});
expect(result.error).toBe(false);
expect(result.updated).toBe(true);
expect(result.needRestart).toBe(false);
expect(nodeRestartSpy).not.toHaveBeenCalled();
vi.advanceTimersByTime(3000);
expect(nodeRestartSpy).toHaveBeenCalledTimes(1);
});
});