Skip to content

Commit 03e4eef

Browse files
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
1 parent e55b11b commit 03e4eef

2 files changed

Lines changed: 61 additions & 10 deletions

File tree

defaultmodules/updatenotification/update_helper.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,21 +130,43 @@ class Updater {
130130
});
131131
}
132132

133-
// restart MagicMirror with the same start command as the current process
133+
/**
134+
* Restart the current MagicMirror process after a successful auto-update.
135+
* Under PM2 just exit and let PM2 respawn — spawning a child here would
136+
* race against PM2 and cause an EADDRINUSE conflict (see issue #4165).
137+
* Standalone: spawn a detached clone of the current process, then exit.
138+
*/
134139
nodeRestart () {
135140
Log.info("Restarting MagicMirror...");
136-
const out = process.stdout;
137-
const err = process.stderr;
138-
139-
// Restart with the same binary and arguments as the current process
140-
const binary = process.argv[0];
141-
const args = process.argv.slice(1);
142-
const options = { cwd: this.root_path, detached: true, stdio: ["ignore", out, err] };
143-
const subprocess = Spawn(binary, args, options);
144-
subprocess.unref(); // allow the current process to exit without waiting for the subprocess
141+
142+
const isManagedByPm2 = process.env.pm_id !== undefined;
143+
if (isManagedByPm2) {
144+
Log.info("Running under PM2 — exiting for PM2 to respawn.");
145+
process.exit(0);
146+
return;
147+
}
148+
149+
this._spawnDetachedSelf();
145150
process.exit();
146151
}
147152

153+
/**
154+
* Spawn a detached clone of the current Node process (same binary + argv),
155+
* wired to the parent's stdout/stderr.
156+
*/
157+
_spawnDetachedSelf () {
158+
const nodeBinary = process.argv[0];
159+
const nodeArgs = process.argv.slice(1);
160+
const spawnOptions = {
161+
cwd: this.root_path,
162+
detached: true,
163+
stdio: ["ignore", process.stdout, process.stderr]
164+
};
165+
166+
const child = Spawn(nodeBinary, nodeArgs, spawnOptions);
167+
child.unref();
168+
}
169+
148170
// check if module is MagicMirror
149171
isMagicMirror (module) {
150172
if (module === "MagicMirror") return true;

tests/unit/functions/update_helper_spec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,33 @@ describe("UpdateHelper", () => {
8787
vi.advanceTimersByTime(3000);
8888
expect(nodeRestartSpy).toHaveBeenCalledTimes(1);
8989
});
90+
91+
describe("nodeRestart", () => {
92+
it("exits without spawning when running under PM2", async () => {
93+
process.env.pm_id = "0";
94+
95+
const updater = await createUpdater();
96+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {});
97+
const spawnSpy = vi.spyOn(updater, "_spawnDetachedSelf").mockImplementation(() => {});
98+
99+
updater.nodeRestart();
100+
101+
expect(exitSpy).toHaveBeenCalledWith(0);
102+
expect(spawnSpy).not.toHaveBeenCalled();
103+
});
104+
105+
it("spawns a detached child process when not running under PM2", async () => {
106+
delete process.env.pm_id;
107+
108+
const updater = await createUpdater();
109+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {});
110+
const spawnSpy = vi.spyOn(updater, "_spawnDetachedSelf").mockImplementation(() => {});
111+
112+
updater.nodeRestart();
113+
114+
expect(spawnSpy).toHaveBeenCalledOnce();
115+
expect(exitSpy).toHaveBeenCalledOnce();
116+
expect(exitSpy).not.toHaveBeenCalledWith(0);
117+
});
118+
});
90119
});

0 commit comments

Comments
 (0)