diff --git a/defaultmodules/updatenotification/update_helper.js b/defaultmodules/updatenotification/update_helper.js index 095f241e00..b83c22bcff 100644 --- a/defaultmodules/updatenotification/update_helper.js +++ b/defaultmodules/updatenotification/update_helper.js @@ -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; diff --git a/tests/unit/functions/update_helper_spec.js b/tests/unit/functions/update_helper_spec.js index ec8a231882..7b7635f47a 100644 --- a/tests/unit/functions/update_helper_spec.js +++ b/tests/unit/functions/update_helper_spec.js @@ -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); + }); + }); });