Skip to content

Commit 1ebc12f

Browse files
refactor: remove PM2 dependency, add universal process manager support
Replace PM2-specific restart/stop logic with universal approach: - Desktop mode (Electron): app.relaunch() + app.quit() - Server mode: process.exit(0) for clean restart, exit(1) for stop - Wait for HTTP response via res.on('finish') before exit Changes: - Add handleRestart() and handleStop() replacing controlPm2() - Fix shutdown/reboot: detect sudo password errors, show helpful message - Update translations: RESTART→REBOOT, add CONFIRM_RESTARTMM - Improve MINIMIZE hint: "restart to restore" instead of "cannot undo" - Add missing translation keys (FULLSCREEN, MINIMIZE, DEVTOOLS) - Update docs (README, FAQ with sudo setup guide, swagger.json) - Remove all PM2 references from code and documentation - Add error logging in catch blocks for better debugging The `pm2ProcessName` config option is no longer used (silently ignored). Restart now works automatically with any process manager (systemd, PM2, Docker, etc.) configured to restart on exit code 0.
1 parent d998dcf commit 1ebc12f

24 files changed

+226
-149
lines changed

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ The module also includes a **RESTful API** for controlling all aspects of your m
8989
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.0.1/120", "192.168.0.1/24"],
9090
```
9191
92-
4. Restart your MagicMirror² (i.e. `pm2 restart MagicMirror`).
92+
4. Restart MagicMirror².
9393
9494
5. Access the remote interface on [http://192.168.xxx.xxx:8080/remote.html](http://192.168.xxx.xxx:8080/remote.html) (replace with IP address of your RaspberryPi).
9595
@@ -149,15 +149,15 @@ For example you can use [MMM-ModuleScheduler](https://forum.magicmirror.builders
149149
150150
### Examples
151151
152-
- Example for a REST API GET request to trigger a RaspberryPi restart:
152+
- Example for a REST API GET request to trigger a system reboot:
153153
154-
`http://192.168.xxx.xxx:8080/api/restart`
154+
`http://192.168.xxx.xxx:8080/api/reboot`
155155
156-
- Example to trigger a RaspberryPi restart in your module:
156+
- Example to trigger MagicMirror² restart in your module:
157157
158-
```js
159-
this.sendNotification("REMOTE_ACTION", { action: "RESTART" });
160-
```
158+
```js
159+
this.sendNotification("REMOTE_ACTION", { action: "RESTART" });
160+
```
161161
162162
See the [Examples Guide](docs/guide/examples.md) for more integration examples with MMM-ModuleScheduler, MMM-Navigate, Home Assistant, and more.
163163
@@ -178,19 +178,20 @@ See the [Examples Guide](docs/guide/examples.md) for more integration examples w
178178
179179
| Action | Description |
180180
| :--------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181-
| RESTART | Restart your MagicMirror² |
181+
| RESTART | Restart MagicMirror² (Electron relaunch or clean process exit for process managers like systemd/PM2/Docker) |
182+
| STOP | Stop MagicMirror² without restarting (exits with error code, process managers won't auto-restart) |
182183
| REFRESH | Refresh mirror page |
183184
| UPDATE | Update MagicMirror² and any of it's modules |
184185
| SAVE | Save the current configuration (show and hide status of modules, and brightness), will be applied after the mirror starts |
185186
| BRIGHTNESS | Change mirror brightness, with the new value specified by `value`. `100` equals the default (full brightness), possible range is between `0` (black) and `100`. |
186187
187188
#### MagicMirror² Electron Browser Window Control
188189
189-
| Action | Description |
190-
| :--------------: | ---------------------------------- |
191-
| MINIMIZE | Minimize the browser window. |
192-
| TOGGLEFULLSCREEN | Toggle fullscreen mode on and off. |
193-
| DEVTOOLS | Open the DevTools console window. |
190+
| Action | Description |
191+
| :--------------: | ------------------------------------------------------------------------------------------- |
192+
| MINIMIZE | Minimize the browser window. **Warning:** Cannot be undone via remote (Wayland limitation). |
193+
| TOGGLEFULLSCREEN | Toggle fullscreen mode on and off. |
194+
| DEVTOOLS | Open the DevTools console window. |
194195
195196
#### Module Control
196197

cspell.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"navicon",
5353
"newsfeed",
5454
"newsitems",
55+
"NOPASSWD",
5556
"Norsk",
5657
"openmeteo",
5758
"passwordless",

docs/guide/configuration.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,6 @@ config: {
9292
},
9393
```
9494

95-
### pm2ProcessName
96-
97-
If your PM2 process name is not the default `mm`:
98-
99-
```js
100-
config: {
101-
pm2ProcessName: 'MagicMirror',
102-
},
103-
```
104-
10595
## Position
10696

10797
Setting a `position` displays the mirror's IP address on screen. You can hide it later from the module menu.

docs/guide/faq.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,39 @@
22

33
## System Issues
44

5-
### RPi not shutting down? Getting "Interactive Authorization" error
5+
### Shutdown or Reboot not working? "System requires password" error
66

7-
You need passwordless sudo for shutdown commands. See [this guide](https://askubuntu.com/questions/168879/shutdown-from-terminal-without-entering-password).
7+
Your system requires a password for shutdown/reboot commands. To fix this:
88

9-
### MagicMirror instance isn't restarting
9+
1. Open the sudoers file (safely):
10+
11+
```bash
12+
sudo visudo
13+
```
1014

11-
You probably don't have PM2 installed, or the process name is different.
15+
2. Add this line at the end (replace `pi` with your username):
1216

13-
1. Check if PM2 is running: `pm2 list`
14-
2. If your process name isn't `mm`, add to config:
15-
```js
16-
config: {
17-
pm2ProcessName: 'MagicMirror', // your process name
18-
}
17+
```bash
18+
pi ALL=(ALL) NOPASSWD: /sbin/shutdown
1919
```
2020

21-
See [MagicMirror Docs](https://docs.magicmirror.builders/configuration/autostart.html) for PM2 setup.
21+
3. Save and exit (Ctrl+X, then Y, then Enter)
22+
23+
Now shutdown and reboot should work without password prompts.
24+
25+
See also: [Ubuntu guide on passwordless shutdown](https://askubuntu.com/questions/168879/shutdown-from-terminal-without-entering-password)
26+
27+
### MagicMirror instance isn't restarting
28+
29+
The restart function depends on your process manager setup.
30+
31+
1. In Desktop mode (with Electron), restart uses `app.relaunch()` and `app.quit()`
32+
2. In Server mode, the process exits cleanly and your process manager (systemd, PM2, Docker, etc.) should restart it
33+
3. Make sure your process manager is configured to restart on clean exit (exit code 0)
34+
35+
For example, with systemd set `Restart=on-success`, or with PM2 use the default auto-restart behavior.
36+
37+
See [MagicMirror Docs](https://docs.magicmirror.builders/configuration/autostart.html) for setup instructions.
2238

2339
---
2440

docs/swagger.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@
459459
"/api/restart": {
460460
"get": {
461461
"tags": ["Mirror Control", "API"],
462-
"summary": "Restart your MagicMirror² instance if using PM2 to control it.",
462+
"summary": "Restart your MagicMirror² instance (Electron relaunch or process manager restart).",
463463
"security": [
464464
{
465465
"apikey": []
@@ -623,11 +623,11 @@
623623
"MONITOROFF": "Turn monitor OFF",
624624
"MONITORON": "Turn monitor ON",
625625
"MONITORTIMED": "Turn monitor ON briefly",
626-
"RESTART": "Restart",
626+
"REBOOT": "Reboot System",
627627
"RESTARTMM": "Restart MagicMirror²",
628628
"SHUTDOWN": "Shutdown",
629629
"FULLSCREEN": "Toggle Fullscreen",
630-
"MINIMIZE": "Minimize Browser",
630+
"MINIMIZE": "Minimize Browser (restart to restore)",
631631
"DEVTOOLS": "Open DevTools",
632632
"SHOWALL": "All",
633633
"HIDEALL": "All",
@@ -658,7 +658,8 @@
658658
"PANIC": "Never mind.",
659659
"NO_RISK_NO_FUN": "No risk no fun!",
660660
"CONFIRM_SHUTDOWN": "The system will shut down.",
661-
"CONFIRM_RESTART": "The system will restart.",
661+
"CONFIRM_REBOOT": "The system will restart.",
662+
"CONFIRM_RESTARTMM": "MagicMirror² will restart.",
662663
"LOAD_ERROR": "If you see this message, an error occurred when loading the javascript file. Please go to the following link and see if this a known problem with your browser:",
663664
"ISSUE_LINK": "Github issue page",
664665
"DONE": "Done.",

node_helper.js

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -790,18 +790,46 @@ module.exports = NodeHelper.create({
790790
}
791791
},
792792

793-
shutdownControl (action, options) {
793+
shutdownControl (action, options, res) {
794794
const shutdownCommand = this.initialized && "shutdownCommand" in this.thisConfig.customCommand
795795
? this.thisConfig.customCommand.shutdownCommand
796796
: "sudo shutdown -h now";
797797
const rebootCommand = this.initialized && "rebootCommand" in this.thisConfig.customCommand
798798
? this.thisConfig.customCommand.rebootCommand
799799
: "sudo shutdown -r now";
800800
if (action === "SHUTDOWN") {
801-
exec(shutdownCommand, options, (error, stdout, stderr, res) => { this.checkForExecError(error, stdout, stderr, res); });
801+
this.sendResponse(res, undefined, {action: "SHUTDOWN", info: "Shutting down system..."});
802+
Log.log(`Executing shutdown command: ${shutdownCommand}`);
803+
exec(shutdownCommand, options, (error, stdout, stderr) => {
804+
if (error) {
805+
// Check for sudo password requirement
806+
if (error.killed && error.signal === "SIGTERM") {
807+
Log.error("Shutdown failed: System requires password for shutdown.");
808+
Log.error("See setup guide: https://github.com/Jopyth/MMM-Remote-Control#faq");
809+
return;
810+
}
811+
Log.error(`Shutdown error: ${stderr || error.message}`);
812+
} else {
813+
Log.log("Shutdown command executed successfully - system should be shutting down...");
814+
}
815+
});
802816
}
803817
if (action === "REBOOT") {
804-
exec(rebootCommand, options, (error, stdout, stderr, res) => { this.checkForExecError(error, stdout, stderr, res); });
818+
this.sendResponse(res, undefined, {action: "REBOOT", info: "Rebooting system..."});
819+
Log.log(`Executing reboot command: ${rebootCommand}`);
820+
exec(rebootCommand, options, (error, stdout, stderr) => {
821+
if (error) {
822+
// Check for sudo password requirement
823+
if (error.killed && error.signal === "SIGTERM") {
824+
Log.error("Reboot failed: System requires password for reboot.");
825+
Log.error("See setup guide: https://github.com/Jopyth/MMM-Remote-Control#faq");
826+
return;
827+
}
828+
Log.error(`Reboot error: ${stderr || error.message}`);
829+
} else {
830+
Log.log("Reboot command executed successfully - system should be rebooting...");
831+
}
832+
});
805833
}
806834
},
807835

@@ -915,6 +943,49 @@ module.exports = NodeHelper.create({
915943
}
916944
},
917945

946+
handleRestart (query, res) {
947+
try {
948+
const {app} = require("electron");
949+
if (!app) { throw "Could not get Electron app instance."; }
950+
this.sendResponse(res, undefined, {action: "RESTART", info: "Restarting Electron..."});
951+
app.relaunch();
952+
app.quit();
953+
} catch (error) {
954+
// Electron not available (server mode) - exit cleanly and let process manager restart
955+
Log.log(`Electron not available (${error?.message || "server mode"}), exiting process for restart by process manager...`);
956+
this.sendResponse(res, undefined, {action: "RESTART", info: "Exiting process for restart..."});
957+
958+
// Wait for response to be sent before exiting
959+
if (res && res.on) {
960+
res.on("finish", () => {
961+
// eslint-disable-next-line unicorn/no-process-exit
962+
process.exit(0);
963+
});
964+
} else {
965+
// Fallback if res doesn't have event emitter (socket response)
966+
// eslint-disable-next-line unicorn/no-process-exit
967+
setTimeout(() => process.exit(0), 1000);
968+
}
969+
}
970+
},
971+
972+
handleStop (query, res) {
973+
Log.log("Stopping MagicMirror...");
974+
this.sendResponse(res, undefined, {action: "STOP", info: "Stopping process..."});
975+
976+
// Wait for response to be sent before exiting
977+
if (res && res.on) {
978+
res.on("finish", () => {
979+
// eslint-disable-next-line unicorn/no-process-exit
980+
process.exit(1);
981+
});
982+
} else {
983+
// Fallback if res doesn't have event emitter (socket response)
984+
// eslint-disable-next-line unicorn/no-process-exit
985+
setTimeout(() => process.exit(1), 1000);
986+
}
987+
},
988+
918989
handleCommand (query, res) {
919990
const options = {timeout: 15_000};
920991
if (this.thisConfig.customCommand && this.thisConfig.customCommand[query.command]) {
@@ -990,8 +1061,8 @@ module.exports = NodeHelper.create({
9901061
GET_CHANGELOG: (q, r) => this.answerGetChangelog(q, r),
9911062
SHUTDOWN: (q, r) => this.shutdownControl(q.action, options, r),
9921063
REBOOT: (q, r) => this.shutdownControl(q.action, options, r),
993-
RESTART: (q, r) => this.controlPm2(r, q),
994-
STOP: (q, r) => this.controlPm2(r, q),
1064+
RESTART: (q, r) => this.handleRestart(q, r),
1065+
STOP: (q, r) => this.handleStop(q, r),
9951066
COMMAND: (q, r) => this.handleCommand(q, r),
9961067
USER_PRESENCE: (q, r) => this.handleUserPresence(q, r),
9971068
MONITORON: (q, r) => this.monitorControl(q.action, options, r),
@@ -1158,57 +1229,6 @@ module.exports = NodeHelper.create({
11581229
this.sendResponse(res, error, data);
11591230
},
11601231

1161-
controlPm2 (res, query) {
1162-
const actionName = query.action.toLowerCase();
1163-
1164-
// Check if PM2 is available
1165-
try {
1166-
require.resolve("pm2");
1167-
} catch {
1168-
// PM2 not installed
1169-
const message = `MagicMirror² is not running under PM2. Please ${actionName} manually.`;
1170-
Log.log(`${message}`);
1171-
this.sendResponse(res, undefined, {action: actionName, info: message, status: "info"});
1172-
return;
1173-
}
1174-
1175-
const pm2 = require("pm2");
1176-
const processName = query.processName || this.thisConfig.pm2ProcessName || "mm";
1177-
1178-
pm2.connect((error) => {
1179-
if (error) {
1180-
pm2.disconnect();
1181-
const message = `MagicMirror² is not running under PM2. Please ${actionName} manually.`;
1182-
Log.log(`${message}`);
1183-
this.sendResponse(res, undefined, {action: actionName, info: message, status: "info"});
1184-
return;
1185-
}
1186-
1187-
// Check if process is running in PM2
1188-
pm2.list((error, list) => {
1189-
if (error || !list.some((proc) => proc.name === processName && proc.pm2_env.status === "online")) {
1190-
pm2.disconnect();
1191-
const message = `MagicMirror² is not running under PM2. Please ${actionName} manually.`;
1192-
Log.log(`${message}`);
1193-
this.sendResponse(res, undefined, {action: actionName, info: message, status: "info"});
1194-
return;
1195-
}
1196-
1197-
// Process is running in PM2, perform action
1198-
pm2[actionName](processName, (error) => {
1199-
pm2.disconnect();
1200-
if (error) {
1201-
Log.error(`PM2 ${actionName} error:`, error);
1202-
this.sendResponse(res, error);
1203-
} else {
1204-
Log.log(`PM2 ${actionName}: ${processName}`);
1205-
this.sendResponse(res, undefined, {action: actionName, processName});
1206-
}
1207-
});
1208-
});
1209-
});
1210-
},
1211-
12121232
translate (data) {
12131233
for (const t of Object.keys(this.translation)) {
12141234
const pattern = `%%TRANSLATE:${t}%%`;

remote.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
</div>
121121
<div id="restart-button" class="button" role="button" tabindex="0">
122122
<span class="fa fa-fw fa-refresh" aria-hidden="true"></span>
123-
<span class="text">%%TRANSLATE:RESTART%%</span>
123+
<span class="text">%%TRANSLATE:REBOOT%%</span>
124124
</div>
125125
<div
126126
id="restart-mm-button"

0 commit comments

Comments
 (0)