fix(updatenotification): don't spawn a child process when running under PM2 (#4166)

Previously, `nodeRestart()` would spawn a detached child and exit. Under
PM2 that's a problem: PM2 also respawns on exit, so both race to bind
the same port.

The fix: When `process.env.pm_id` is set, just exit and let PM2 handle
the restart.

The spawn logic is moved into its own method so it can be tested
cleanly.

Partially fixes #4165
This commit is contained in:
Kristjan ESPERANTO
2026-05-21 22:25:47 +02:00
committed by GitHub
parent e55b11be5d
commit 03e4eef3d1
2 changed files with 60 additions and 9 deletions

View File

@@ -130,21 +130,43 @@ class Updater {
});
}
// restart MagicMirror with the same start command as the current process
/**
* Restart the current MagicMirror process after a successful auto-update.
* Under PM2 just exit and let PM2 respawn — spawning a child here would
* race against PM2 and cause an EADDRINUSE conflict (see issue #4165).
* Standalone: spawn a detached clone of the current process, then exit.
*/
nodeRestart () {
Log.info("Restarting MagicMirror...");
const out = process.stdout;
const err = process.stderr;
// Restart with the same binary and arguments as the current process
const binary = process.argv[0];
const args = process.argv.slice(1);
const options = { cwd: this.root_path, detached: true, stdio: ["ignore", out, err] };
const subprocess = Spawn(binary, args, options);
subprocess.unref(); // allow the current process to exit without waiting for the subprocess
const isManagedByPm2 = process.env.pm_id !== undefined;
if (isManagedByPm2) {
Log.info("Running under PM2 — exiting for PM2 to respawn.");
process.exit(0);
return;
}
this._spawnDetachedSelf();
process.exit();
}
/**
* Spawn a detached clone of the current Node process (same binary + argv),
* wired to the parent's stdout/stderr.
*/
_spawnDetachedSelf () {
const nodeBinary = process.argv[0];
const nodeArgs = process.argv.slice(1);
const spawnOptions = {
cwd: this.root_path,
detached: true,
stdio: ["ignore", process.stdout, process.stderr]
};
const child = Spawn(nodeBinary, nodeArgs, spawnOptions);
child.unref();
}
// check if module is MagicMirror
isMagicMirror (module) {
if (module === "MagicMirror") return true;

View File

@@ -87,4 +87,33 @@ describe("UpdateHelper", () => {
vi.advanceTimersByTime(3000);
expect(nodeRestartSpy).toHaveBeenCalledTimes(1);
});
describe("nodeRestart", () => {
it("exits without spawning when running under PM2", async () => {
process.env.pm_id = "0";
const updater = await createUpdater();
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {});
const spawnSpy = vi.spyOn(updater, "_spawnDetachedSelf").mockImplementation(() => {});
updater.nodeRestart();
expect(exitSpy).toHaveBeenCalledWith(0);
expect(spawnSpy).not.toHaveBeenCalled();
});
it("spawns a detached child process when not running under PM2", async () => {
delete process.env.pm_id;
const updater = await createUpdater();
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {});
const spawnSpy = vi.spyOn(updater, "_spawnDetachedSelf").mockImplementation(() => {});
updater.nodeRestart();
expect(spawnSpy).toHaveBeenCalledOnce();
expect(exitSpy).toHaveBeenCalledOnce();
expect(exitSpy).not.toHaveBeenCalledWith(0);
});
});
});