magalh 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/bin/mgl.js +350 -214
  2. package/msh.js +1 -1
  3. package/package.json +1 -1
  4. package/bin/msh.js +0 -53
package/bin/mgl.js CHANGED
@@ -8,255 +8,391 @@ const { spawn, execFileSync } = require("child_process");
8
8
 
9
9
  const args = process.argv.slice(2);
10
10
  const rootDir = path.resolve(__dirname, "..");
11
+ const entryFile = path.join(rootDir, "msh.js");
12
+
13
+ const SERVICE_NAME = "magalh";
14
+ const SERVICE_DISPLAY_NAME = "Magala Media Server";
15
+ const SERVICE_DESCRIPTION = "Magala self-hosted media streaming server";
16
+
17
+ function hasArg(name) {
18
+ return args.includes(name);
19
+ }
20
+
21
+ function log(message) {
22
+ console.log(`[magalh] ${message}`);
23
+ }
24
+
25
+ function warn(message) {
26
+ console.warn(`[magalh] ${message}`);
27
+ }
28
+
29
+ function fail(message, exitCode = 1) {
30
+ try {
31
+ process.stderr.write(`[magalh] ${message}\n`);
32
+ } catch {
33
+ console.error(`[magalh] ${message}`);
34
+ }
35
+ process.exit(exitCode);
36
+ }
37
+
38
+ function isRootOnLinux() {
39
+ return typeof process.getuid === "function" && process.getuid() === 0;
40
+ }
41
+
42
+ function isWindowsAdminLike() {
43
+ try {
44
+ fs.accessSync(
45
+ path.join(process.env.SystemRoot || "C:\\Windows", "System32"),
46
+ fs.constants.W_OK,
47
+ );
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function fileExists(filePath) {
55
+ try {
56
+ fs.accessSync(filePath, fs.constants.F_OK);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function runCommand(command, commandArgs, options = {}) {
64
+ return execFileSync(command, commandArgs, {
65
+ stdio: "inherit",
66
+ ...options,
67
+ });
68
+ }
11
69
 
12
- if (args.includes("--help") || args.includes("-h")) {
70
+ function runCommandQuiet(command, commandArgs, options = {}) {
71
+ try {
72
+ return execFileSync(command, commandArgs, {
73
+ stdio: "pipe",
74
+ encoding: "utf8",
75
+ ...options,
76
+ });
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ function printHelp() {
13
83
  process.stdout.write(
14
84
  [
15
85
  "magalh - Magala Media Server",
16
86
  "",
17
87
  "Usage:",
18
- " magalh [--help]",
88
+ " magalh",
89
+ " magalh --help",
19
90
  " magalh --install-service",
20
- " magalh --upgrade-node",
91
+ " magalh --uninstall-service",
92
+ " magalh --service-status",
93
+ "",
94
+ "Notes:",
95
+ " - The server starts normally with: magalh",
96
+ " - Service installation is explicit only; it is never done automatically.",
21
97
  "",
22
98
  ].join("\n"),
23
99
  );
24
- process.exit(0);
25
100
  }
26
101
 
27
- if (args.includes("--upgrade-node")) {
28
- if (process.platform !== "linux") {
29
- console.log("[magalh] --upgrade-node is only supported on Linux.");
30
- process.exit(0);
102
+ function getCliBin() {
103
+ return path.join(rootDir, "bin", "mgl.js");
104
+ }
105
+
106
+ function getLinuxServiceFile() {
107
+ return `/etc/systemd/system/${SERVICE_NAME}.service`;
108
+ }
109
+
110
+ function ensureEntryFileExists() {
111
+ if (!fileExists(entryFile)) {
112
+ fail(`Entry file not found: ${entryFile}`);
31
113
  }
114
+ }
32
115
 
33
- var MIN_NODE_MAJOR = 20;
34
- var current = parseInt(process.versions.node.split(".")[0], 10);
116
+ function getLinuxServiceContent() {
117
+ const nodeBin = process.execPath;
118
+ const cliBin = getCliBin();
35
119
 
36
- if (current >= MIN_NODE_MAJOR) {
37
- console.log("[magalh] Node.js v" + process.versions.node + " is already v" + MIN_NODE_MAJOR + "+. No upgrade needed.");
38
- process.exit(0);
120
+ return [
121
+ "[Unit]",
122
+ `Description=${SERVICE_DISPLAY_NAME}`,
123
+ "After=network-online.target",
124
+ "Wants=network-online.target",
125
+ "",
126
+ "[Service]",
127
+ "Type=simple",
128
+ `WorkingDirectory=${rootDir}`,
129
+ `ExecStart=${nodeBin} ${cliBin}`,
130
+ "Restart=on-failure",
131
+ "RestartSec=5",
132
+ "StandardOutput=journal",
133
+ "StandardError=journal",
134
+ `SyslogIdentifier=${SERVICE_NAME}`,
135
+ "",
136
+ "[Install]",
137
+ "WantedBy=multi-user.target",
138
+ "",
139
+ ].join("\n");
140
+ }
141
+
142
+ function isLinuxServiceInstalled() {
143
+ return fileExists(getLinuxServiceFile());
144
+ }
145
+
146
+ function isWindowsServiceInstalled() {
147
+ const out = runCommandQuiet("sc", ["query", SERVICE_NAME]);
148
+ return typeof out === "string" && out.includes("SERVICE_NAME");
149
+ }
150
+
151
+ function installLinuxService() {
152
+ if (!isRootOnLinux()) {
153
+ fail("Linux service installation requires root. Run: sudo magalh --install-service");
154
+ }
155
+
156
+ const systemdDir = "/etc/systemd/system";
157
+ if (!fileExists(systemdDir)) {
158
+ fail("systemd directory not found. Service install is not supported on this Linux system.");
39
159
  }
40
160
 
41
- console.log("[magalh] Node.js v" + process.versions.node + " is too old. Upgrading to v" + MIN_NODE_MAJOR + " via nvm...");
161
+ const cliBin = getCliBin();
162
+ if (!fileExists(cliBin)) {
163
+ fail(`CLI file not found: ${cliBin}`);
164
+ }
165
+
166
+ const serviceFile = getLinuxServiceFile();
167
+ const content = getLinuxServiceContent();
168
+
169
+ try {
170
+ fs.writeFileSync(serviceFile, content, { mode: 0o644 });
171
+ log(`Service file written -> ${serviceFile}`);
42
172
 
43
- var nvmDir = (process.env.NVM_DIR) ||
44
- (process.env.HOME ? path.join(process.env.HOME, ".nvm") : "");
45
- var nvmScript = nvmDir ? path.join(nvmDir, "nvm.sh") : "";
173
+ runCommand("systemctl", ["daemon-reload"]);
174
+ runCommand("systemctl", ["enable", SERVICE_NAME]);
175
+ runCommand("systemctl", ["restart", SERVICE_NAME]);
176
+
177
+ log(
178
+ "systemd service enabled and started.\n" +
179
+ ` Status : systemctl status ${SERVICE_NAME}\n` +
180
+ ` Logs : journalctl -u ${SERVICE_NAME} -f`,
181
+ );
182
+ } catch (error) {
183
+ fail(`Linux service installation failed: ${error.message || String(error)}`);
184
+ }
185
+ }
186
+
187
+ function uninstallLinuxService() {
188
+ if (!isRootOnLinux()) {
189
+ fail("Linux service removal requires root. Run: sudo magalh --uninstall-service");
190
+ }
191
+
192
+ const serviceFile = getLinuxServiceFile();
193
+
194
+ try {
195
+ runCommandQuiet("systemctl", ["stop", SERVICE_NAME]);
196
+ runCommandQuiet("systemctl", ["disable", SERVICE_NAME]);
197
+
198
+ if (fileExists(serviceFile)) {
199
+ fs.unlinkSync(serviceFile);
200
+ log(`Removed service file -> ${serviceFile}`);
201
+ } else {
202
+ log("Service file does not exist. Nothing to remove.");
203
+ }
204
+
205
+ runCommand("systemctl", ["daemon-reload"]);
206
+ log("Linux service removed.");
207
+ } catch (error) {
208
+ fail(`Linux service removal failed: ${error.message || String(error)}`);
209
+ }
210
+ }
211
+
212
+ function printLinuxServiceStatus() {
213
+ if (!isLinuxServiceInstalled()) {
214
+ log("Linux service is not installed.");
215
+ process.exit(0);
216
+ }
46
217
 
47
- var installCmd;
48
- if (nvmScript && fs.existsSync(nvmScript)) {
49
- installCmd = [
50
- "source " + nvmScript,
51
- "nvm install " + MIN_NODE_MAJOR,
52
- "nvm alias default " + MIN_NODE_MAJOR,
53
- "nvm use " + MIN_NODE_MAJOR,
54
- ].join(" && ");
218
+ log(`Linux service file exists -> ${getLinuxServiceFile()}`);
219
+ const status = runCommandQuiet("systemctl", ["status", SERVICE_NAME, "--no-pager"]);
220
+ if (status) {
221
+ process.stdout.write(`${status}\n`);
55
222
  } else {
56
- installCmd = [
57
- 'if curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh -o /tmp/_nvm_setup.sh 2>/dev/null && bash /tmp/_nvm_setup.sh; then',
58
- ' export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm install ' + MIN_NODE_MAJOR + ' && nvm alias default ' + MIN_NODE_MAJOR,
59
- 'else',
60
- ' echo "[magalh] curl failed, trying npm n..." && npm install -g n && n ' + MIN_NODE_MAJOR,
61
- 'fi',
62
- ].join('\n');
223
+ warn("Could not read systemd status output.");
224
+ }
225
+ }
226
+
227
+ function installWindowsService() {
228
+ if (!isWindowsAdminLike()) {
229
+ fail("Windows service installation requires Administrator privileges.");
63
230
  }
64
231
 
232
+ const cliBin = getCliBin();
233
+ if (!fileExists(cliBin)) {
234
+ fail(`CLI file not found: ${cliBin}`);
235
+ }
236
+
237
+ const nodeBin = process.execPath;
238
+ const binPath = `"${nodeBin}" "${cliBin}"`;
239
+
65
240
  try {
66
- execFileSync("bash", ["-c", installCmd], { stdio: "inherit" });
67
- console.log("[magalh] Node.js v" + MIN_NODE_MAJOR + " installed. Run: npm install -g magalh");
68
- } catch (err) {
69
- console.error("[magalh] Upgrade failed: " + ((err && err.message) || String(err)));
70
- process.exit(1);
241
+ runCommandQuiet("sc", ["stop", SERVICE_NAME]);
242
+ runCommandQuiet("sc", ["delete", SERVICE_NAME]);
243
+
244
+ runCommand("sc", [
245
+ "create",
246
+ SERVICE_NAME,
247
+ "binPath=",
248
+ binPath,
249
+ "start=",
250
+ "auto",
251
+ "DisplayName=",
252
+ SERVICE_DISPLAY_NAME,
253
+ ]);
254
+
255
+ runCommand("sc", [
256
+ "description",
257
+ SERVICE_NAME,
258
+ SERVICE_DESCRIPTION,
259
+ ]);
260
+
261
+ runCommand("sc", [
262
+ "failure",
263
+ SERVICE_NAME,
264
+ "reset=",
265
+ "60",
266
+ "actions=",
267
+ "restart/5000/restart/5000/restart/5000",
268
+ ]);
269
+
270
+ runCommand("sc", ["start", SERVICE_NAME]);
271
+
272
+ log(
273
+ "Windows service created and started.\n" +
274
+ ` Status : sc query ${SERVICE_NAME}\n` +
275
+ ` Stop : sc stop ${SERVICE_NAME}\n` +
276
+ " Logs : Event Viewer -> Windows Logs -> Application",
277
+ );
278
+ } catch (error) {
279
+ fail(`Windows service installation failed: ${error.message || String(error)}`);
71
280
  }
72
- process.exit(0);
73
281
  }
74
282
 
75
- if (args.includes("--install-service")) {
76
- const SERVICE_NAME = "magalh";
77
- const nodeBin = process.execPath;
78
- const mglBin = path.join(rootDir, "bin", "mgl.js");
79
-
80
- function installLinux() {
81
- if (typeof process.getuid !== "function" || process.getuid() !== 0) {
82
- console.log(
83
- "[magalh] Skipping systemd setup (not running as root).\n" +
84
- " To install manually: sudo npm install -g magalh",
85
- );
86
- return;
87
- }
88
- const SYSTEMD_DIR = "/etc/systemd/system";
89
- if (!fs.existsSync(SYSTEMD_DIR)) {
90
- console.log("[magalh] Skipping systemd setup (systemd not found).");
91
- return;
92
- }
93
- const serviceFile = path.join(SYSTEMD_DIR, `${SERVICE_NAME}.service`);
94
- const content = [
95
- "[Unit]",
96
- "Description=Magala Media Server",
97
- "After=network-online.target",
98
- "Wants=network-online.target",
99
- "",
100
- "[Service]",
101
- "Type=simple",
102
- `WorkingDirectory=${rootDir}`,
103
- `ExecStart=${nodeBin} ${mglBin}`,
104
- "Restart=on-failure",
105
- "RestartSec=5",
106
- "StandardOutput=journal",
107
- "StandardError=journal",
108
- `SyslogIdentifier=${SERVICE_NAME}`,
109
- "",
110
- "[Install]",
111
- "WantedBy=multi-user.target",
112
- "",
113
- ].join("\n");
114
- try {
115
- fs.writeFileSync(serviceFile, content, { mode: 0o644 });
116
- console.log(`[magalh] Service file written -> ${serviceFile}`);
117
- execFileSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
118
- execFileSync("systemctl", ["enable", SERVICE_NAME], { stdio: "inherit" });
119
- execFileSync("systemctl", ["restart", SERVICE_NAME], { stdio: "inherit" });
120
- console.log(
121
- `[magalh] systemd service enabled and started.\n` +
122
- ` Status : systemctl status ${SERVICE_NAME}\n` +
123
- ` Logs : journalctl -u ${SERVICE_NAME} -f`,
124
- );
125
- } catch (err) {
126
- console.warn("[magalh] Warning: systemd setup failed: " + ((err && err.message) || String(err)));
127
- }
283
+ function uninstallWindowsService() {
284
+ if (!isWindowsAdminLike()) {
285
+ fail("Windows service removal requires Administrator privileges.");
128
286
  }
129
287
 
130
- function installWindows() {
131
- try {
132
- fs.accessSync(
133
- path.join(process.env.SystemRoot || "C:\\Windows", "System32"),
134
- fs.constants.W_OK,
135
- );
136
- } catch (_e) {
137
- console.log(
138
- "[magalh] Skipping Windows service setup (not running as Administrator).\n" +
139
- " To install manually: run npm install -g magalh as Administrator.",
140
- );
141
- return;
142
- }
143
- const binPath = `"${nodeBin}" "${mglBin}"`;
144
- try {
145
- try { execFileSync("sc", ["stop", SERVICE_NAME], { stdio: "pipe" }); } catch (_e) { }
146
- try { execFileSync("sc", ["delete", SERVICE_NAME], { stdio: "pipe" }); } catch (_e) { }
147
- execFileSync("sc", [
148
- "create", SERVICE_NAME,
149
- "binPath=", binPath,
150
- "start=", "auto",
151
- "DisplayName=", "Magala Media Server",
152
- ], { stdio: "inherit" });
153
- execFileSync("sc", [
154
- "description", SERVICE_NAME,
155
- "Magala self-hosted media streaming server",
156
- ], { stdio: "inherit" });
157
- execFileSync("sc", [
158
- "failure", SERVICE_NAME,
159
- "reset=", "60",
160
- "actions=", "restart/5000/restart/5000/restart/5000",
161
- ], { stdio: "inherit" });
162
- execFileSync("sc", ["start", SERVICE_NAME], { stdio: "inherit" });
163
- console.log(
164
- `[magalh] Windows service created and started.\n` +
165
- ` Status : sc query ${SERVICE_NAME}\n` +
166
- ` Stop : sc stop ${SERVICE_NAME}\n` +
167
- ` Logs : Event Viewer -> Windows Logs -> Application`,
168
- );
169
- } catch (err) {
170
- console.warn("[magalh] Warning: Windows service setup failed: " + ((err && err.message) || String(err)));
171
- }
288
+ try {
289
+ runCommandQuiet("sc", ["stop", SERVICE_NAME]);
290
+ runCommandQuiet("sc", ["delete", SERVICE_NAME]);
291
+ log("Windows service removed.");
292
+ } catch (error) {
293
+ fail(`Windows service removal failed: ${error.message || String(error)}`);
294
+ }
295
+ }
296
+
297
+ function printWindowsServiceStatus() {
298
+ if (!isWindowsServiceInstalled()) {
299
+ log("Windows service is not installed.");
300
+ process.exit(0);
172
301
  }
173
302
 
174
- if (process.platform === "linux") installLinux();
175
- else if (process.platform === "win32") installWindows();
176
- process.exit(0);
303
+ const status = runCommandQuiet("sc", ["query", SERVICE_NAME]);
304
+ if (status) {
305
+ process.stdout.write(`${status}\n`);
306
+ } else {
307
+ warn("Could not read Windows service status output.");
308
+ }
177
309
  }
178
310
 
179
- if (process.platform === "linux" && !process.env.MAGALH_UPGRADED) {
180
- var _MIN = 20;
181
- var _curMaj = parseInt(process.versions.node.split(".")[0], 10);
182
- if (_curMaj < _MIN) {
183
- console.log("[magalh] Node.js v" + process.versions.node + " detected (need v" + _MIN + "+). Auto-upgrading...");
184
- var _nvmHome = process.env.NVM_DIR || path.join(process.env.HOME || "/root", ".nvm");
185
- var _nvmSh = path.join(_nvmHome, "nvm.sh");
186
- var _upCmd = [
187
- 'export NVM_DIR="' + _nvmHome + '"',
188
- 'if [ -s "' + _nvmSh + '" ]; then',
189
- ' source "' + _nvmSh + '" && nvm install ' + _MIN + ' && nvm alias default ' + _MIN,
190
- 'elif curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh -o /tmp/_nvm_setup.sh 2>/dev/null && bash /tmp/_nvm_setup.sh && [ -s "' + _nvmSh + '" ]; then',
191
- ' source "' + _nvmSh + '" && nvm install ' + _MIN + ' && nvm alias default ' + _MIN,
192
- 'else',
193
- ' echo "[magalh] Falling back to npm n..." && npm install -g n && n ' + _MIN,
194
- 'fi',
195
- ].join('\n');
196
- var _newNode = null;
197
- try {
198
- execFileSync("bash", ["-c", _upCmd], { stdio: "inherit" });
199
- if (fs.existsSync(_nvmSh)) {
200
- try {
201
- _newNode = execFileSync("bash", ["-c", 'source "' + _nvmSh + '" && nvm which ' + _MIN], { encoding: "utf8" }).trim();
202
- } catch (_e) {}
203
- }
204
- if (!_newNode) {
205
- var _cands = ["/usr/local/bin/node", "/usr/bin/node"];
206
- for (var _ci = 0; _ci < _cands.length; _ci++) {
207
- if (fs.existsSync(_cands[_ci])) {
208
- try {
209
- var _nv = execFileSync(_cands[_ci], ["--version"], { encoding: "utf8" }).trim();
210
- if (parseInt(_nv.replace("v", "").split(".")[0], 10) >= _MIN) {
211
- _newNode = _cands[_ci];
212
- break;
213
- }
214
- } catch (_e) {}
215
- }
216
- }
217
- }
218
- } catch (_e) {
219
- console.warn("[magalh] Auto-upgrade failed. Run manually: magalh --upgrade-node");
220
- }
221
- if (_newNode && fs.existsSync(_newNode)) {
222
- var _newNpm = path.join(path.dirname(_newNode), "npm");
223
- if (fs.existsSync(_newNpm)) {
224
- console.log("[magalh] Reinstalling magalh with Node v" + _MIN + " (rebuilds native modules)...");
225
- try {
226
- execFileSync(_newNpm, ["install", "-g", "magalh"], { stdio: "inherit" });
227
- } catch (_re) {
228
- console.warn("[magalh] Reinstall warning: " + String(_re));
229
- }
230
- }
231
- console.log("[magalh] Restarting with Node.js v" + _MIN + "...");
232
- var _env = Object.assign({}, process.env, { MAGALH_UPGRADED: "1" });
233
- var _rs = spawn(_newNode, [__filename].concat(args), { cwd: rootDir, env: _env, stdio: "inherit" });
234
- _rs.on("exit", function(c, s) { process.exit(s ? 1 : (typeof c === "number" ? c : 0)); });
235
- _rs.on("error", function(e) { process.stderr.write(String(e) + "\n"); process.exit(1); });
236
- return;
237
- }
238
- console.error("[magalh] Cannot start: requires Node.js v" + _MIN + "+. Run: magalh --upgrade-node");
239
- process.exit(1);
311
+ function installService() {
312
+ if (process.platform === "linux") {
313
+ installLinuxService();
314
+ return;
315
+ }
316
+
317
+ if (process.platform === "win32") {
318
+ installWindowsService();
319
+ return;
240
320
  }
321
+
322
+ fail(`Service installation is not supported on platform: ${process.platform}`);
241
323
  }
242
324
 
243
- const entryFile = path.join(rootDir, "msh.js");
325
+ function uninstallService() {
326
+ if (process.platform === "linux") {
327
+ uninstallLinuxService();
328
+ return;
329
+ }
330
+
331
+ if (process.platform === "win32") {
332
+ uninstallWindowsService();
333
+ return;
334
+ }
244
335
 
245
- const child = spawn(process.execPath, [entryFile, ...args], {
246
- cwd: rootDir,
247
- env: process.env,
248
- stdio: "inherit",
249
- windowsHide: true,
250
- });
336
+ fail(`Service removal is not supported on platform: ${process.platform}`);
337
+ }
251
338
 
252
- child.on("exit", (code, signal) => {
253
- if (signal) process.exit(1);
254
- process.exit(typeof code === "number" ? code : 0);
255
- });
339
+ function printServiceStatus() {
340
+ if (process.platform === "linux") {
341
+ printLinuxServiceStatus();
342
+ return;
343
+ }
256
344
 
257
- child.on("error", (err) => {
258
- try {
259
- process.stderr.write("Failed to start msh: " + ((err && err.message) || String(err)) + "\n");
260
- } catch (_e) { }
261
- process.exit(1);
262
- });
345
+ if (process.platform === "win32") {
346
+ printWindowsServiceStatus();
347
+ return;
348
+ }
349
+
350
+ log(`Service status is not supported on platform: ${process.platform}`);
351
+ }
352
+
353
+ function startMainProcess() {
354
+ ensureEntryFileExists();
355
+
356
+ const child = spawn(process.execPath, [entryFile, ...args], {
357
+ cwd: rootDir,
358
+ env: process.env,
359
+ stdio: "inherit",
360
+ windowsHide: true,
361
+ });
362
+
363
+ child.on("exit", (code, signal) => {
364
+ if (signal) {
365
+ process.exit(1);
366
+ }
367
+ });
368
+
369
+ child.on("error", (error) => {
370
+ fail(`Failed to start msh: ${error.message || String(error)}`);
371
+ });
372
+ }
373
+
374
+ function main() {
375
+ if (hasArg("--help") || hasArg("-h")) {
376
+ printHelp();
377
+ process.exit(0);
378
+ }
379
+
380
+ if (hasArg("--install-service")) {
381
+ installService();
382
+ process.exit(0);
383
+ }
384
+
385
+ if (hasArg("--uninstall-service")) {
386
+ uninstallService();
387
+ process.exit(0);
388
+ }
389
+
390
+ if (hasArg("--service-status")) {
391
+ printServiceStatus();
392
+ process.exit(0);
393
+ }
394
+
395
+ startMainProcess();
396
+ }
397
+
398
+ main();