@jxa13/pm2ui 1.17.0 → 1.18.0
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.
- package/README.md +71 -11
- package/Services/pm2Service.js +54 -29
- package/changelog.md +27 -0
- package/frontend/dist/assets/index-B4aD3qEe.js +224 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +26 -4
- package/roadmap.md +46 -40
- package/server.js +202 -183
- package/user_manual.md +16 -7
- package/frontend/dist/assets/index-aVk9yHhV.js +0 -224
package/server.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const crypto = require('crypto');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
|
-
const { execFile
|
|
6
|
-
const { promisify } = require('util');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
7
|
const {
|
|
8
|
+
connectLogBus,
|
|
9
|
+
createProcess,
|
|
10
|
+
deleteProcess,
|
|
11
|
+
flushProcessLogs,
|
|
8
12
|
listProcesses,
|
|
13
|
+
savePm2State,
|
|
9
14
|
startProcess,
|
|
10
15
|
stopProcess,
|
|
11
16
|
restartProcess
|
|
@@ -13,21 +18,66 @@ const {
|
|
|
13
18
|
const { getSystemStats } = require('./Services/systemService');
|
|
14
19
|
|
|
15
20
|
const app = express();
|
|
16
|
-
const PORT = process.env.PORT ||
|
|
21
|
+
const PORT = process.env.PORT || 3210;
|
|
17
22
|
const DEFAULT_PROTECTED_PROCESS_NAMES = ['node-webui'];
|
|
18
|
-
const
|
|
23
|
+
const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
24
|
+
const apiSessionToken = crypto.randomBytes(32).toString('hex');
|
|
19
25
|
const reactDistPath = path.join(__dirname, 'frontend', 'dist');
|
|
20
26
|
const reactIndexPath = path.join(reactDistPath, 'index.html');
|
|
21
|
-
const
|
|
27
|
+
const pm2PackageVersion = (() => {
|
|
22
28
|
try {
|
|
23
|
-
return require
|
|
29
|
+
return require('pm2/package.json').version;
|
|
24
30
|
} catch (error) {
|
|
25
|
-
return '
|
|
31
|
+
return '';
|
|
26
32
|
}
|
|
27
33
|
})();
|
|
28
34
|
|
|
29
35
|
app.use(express.json());
|
|
30
36
|
|
|
37
|
+
function isSameOriginRequest(req) {
|
|
38
|
+
const origin = req.get('origin');
|
|
39
|
+
|
|
40
|
+
if (!origin) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const originUrl = new URL(origin);
|
|
46
|
+
return originUrl.host === req.get('host');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasValidApiSessionToken(req) {
|
|
53
|
+
const token = String(req.get('X-PM2UI-Token') || '');
|
|
54
|
+
const expectedToken = Buffer.from(apiSessionToken);
|
|
55
|
+
const receivedToken = Buffer.from(token);
|
|
56
|
+
|
|
57
|
+
if (!token || receivedToken.length !== expectedToken.length) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return crypto.timingSafeEqual(receivedToken, expectedToken);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
app.use((req, res, next) => {
|
|
65
|
+
if (!req.path.startsWith('/api/') || !UNSAFE_METHODS.has(req.method)) {
|
|
66
|
+
next();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!isSameOriginRequest(req) || !hasValidApiSessionToken(req)) {
|
|
71
|
+
res.status(403).json({
|
|
72
|
+
ok: false,
|
|
73
|
+
error: 'Invalid API session'
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
next();
|
|
79
|
+
});
|
|
80
|
+
|
|
31
81
|
function getProtectedProcessNames() {
|
|
32
82
|
const rawValue = process.env.PROTECTED_PM2_PROCESSES;
|
|
33
83
|
const names = rawValue
|
|
@@ -100,15 +150,6 @@ function readTextFile(filePath) {
|
|
|
100
150
|
}
|
|
101
151
|
}
|
|
102
152
|
|
|
103
|
-
async function getCommandOutput(command, args) {
|
|
104
|
-
try {
|
|
105
|
-
const { stdout } = await execFileAsync(command, args);
|
|
106
|
-
return String(stdout || '').trim();
|
|
107
|
-
} catch (error) {
|
|
108
|
-
return '';
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
153
|
async function getPm2RuntimeStatus(processes = []) {
|
|
113
154
|
const homeDirectory = os.homedir();
|
|
114
155
|
const pm2Home = process.env.PM2_HOME || path.join(homeDirectory, '.pm2');
|
|
@@ -121,21 +162,17 @@ async function getPm2RuntimeStatus(processes = []) {
|
|
|
121
162
|
'LaunchAgents',
|
|
122
163
|
userName ? `pm2.${userName}.plist` : 'pm2.plist'
|
|
123
164
|
));
|
|
124
|
-
const version = await getCommandOutput(pm2CliPath, ['-v']);
|
|
125
165
|
const daemonPid = daemonPidFile.exists ? readTextFile(daemonPidFile.path) : '';
|
|
126
|
-
const daemonUptime = daemonPid
|
|
127
|
-
? await getCommandOutput('ps', ['-p', daemonPid, '-o', 'etime='])
|
|
128
|
-
: '';
|
|
129
166
|
|
|
130
167
|
return {
|
|
131
168
|
home: pm2Home,
|
|
132
|
-
version,
|
|
169
|
+
version: pm2PackageVersion,
|
|
133
170
|
daemon: {
|
|
134
171
|
connected: true,
|
|
135
172
|
pid: daemonPid || '',
|
|
136
173
|
pidFile: daemonPidFile.path,
|
|
137
|
-
uptime:
|
|
138
|
-
uptimeSource:
|
|
174
|
+
uptime: '',
|
|
175
|
+
uptimeSource: ''
|
|
139
176
|
},
|
|
140
177
|
managedAppCount: processes.length,
|
|
141
178
|
saved: dumpFile.exists,
|
|
@@ -258,10 +295,11 @@ function normalizePm2Args(value) {
|
|
|
258
295
|
return [String(value)];
|
|
259
296
|
}
|
|
260
297
|
|
|
261
|
-
function parsePm2LogText(rawText, type, target) {
|
|
298
|
+
function parsePm2LogText(rawText, type, target, options = {}) {
|
|
262
299
|
const ansiRegex = /\u001b\[[0-9;]*m/g;
|
|
263
300
|
const prefixRegex = /^\d+\|[^|]+\|\s*/;
|
|
264
301
|
const timestampRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s*/;
|
|
302
|
+
const fallbackTimestamp = options.timestamp || new Date().toLocaleString();
|
|
265
303
|
|
|
266
304
|
return String(rawText || '')
|
|
267
305
|
.split(/\r?\n/)
|
|
@@ -275,15 +313,79 @@ function parsePm2LogText(rawText, type, target) {
|
|
|
275
313
|
const message = withoutPrefix;
|
|
276
314
|
|
|
277
315
|
return {
|
|
278
|
-
timestamp: timestamp ||
|
|
316
|
+
timestamp: timestamp || fallbackTimestamp,
|
|
279
317
|
type,
|
|
280
|
-
processId: null,
|
|
318
|
+
processId: options.processId ?? null,
|
|
281
319
|
appName: target,
|
|
282
320
|
message: message || withoutPrefix
|
|
283
321
|
};
|
|
284
322
|
});
|
|
285
323
|
}
|
|
286
324
|
|
|
325
|
+
async function readTailTextFile(filePath, maxBytes = 1024 * 1024 * 10) {
|
|
326
|
+
if (!filePath) {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const stats = await fs.promises.stat(filePath);
|
|
332
|
+
const length = Math.min(stats.size, maxBytes);
|
|
333
|
+
const start = Math.max(stats.size - length, 0);
|
|
334
|
+
const handle = await fs.promises.open(filePath, 'r');
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const buffer = Buffer.alloc(length);
|
|
338
|
+
await handle.read(buffer, 0, length, start);
|
|
339
|
+
return buffer.toString('utf8');
|
|
340
|
+
} finally {
|
|
341
|
+
await handle.close();
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error.code !== 'ENOENT') {
|
|
345
|
+
console.error('Read PM2 log file error:', error);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return '';
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getPm2ProcessLogFiles(process) {
|
|
353
|
+
const env = process?.pm2_env || {};
|
|
354
|
+
|
|
355
|
+
return [
|
|
356
|
+
{ type: 'stdout', path: env.pm_out_log_path || '' },
|
|
357
|
+
{ type: 'stderr', path: env.pm_err_log_path || '' }
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function getLogEntriesForTarget(target, lineCount) {
|
|
362
|
+
const processes = await listProcesses();
|
|
363
|
+
const process = findProcessByTarget(processes, target);
|
|
364
|
+
|
|
365
|
+
if (!process) {
|
|
366
|
+
const error = new Error('PM2 target was not found');
|
|
367
|
+
error.statusCode = 404;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const entriesByFile = await Promise.all(
|
|
372
|
+
getPm2ProcessLogFiles(process).map(async (file) => {
|
|
373
|
+
const rawText = await readTailTextFile(file.path);
|
|
374
|
+
return parsePm2LogText(rawText, file.type, process.name || target, {
|
|
375
|
+
processId: process.pm_id
|
|
376
|
+
});
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return entriesByFile
|
|
381
|
+
.flat()
|
|
382
|
+
.slice(-lineCount);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getLiveLogType(type) {
|
|
386
|
+
return type === 'err' ? 'stderr' : 'stdout';
|
|
387
|
+
}
|
|
388
|
+
|
|
287
389
|
function mapPm2Process(pm2Process) {
|
|
288
390
|
const env = pm2Process.pm2_env || {};
|
|
289
391
|
const processEnv = env.env || {};
|
|
@@ -381,6 +483,13 @@ app.get('/api/health', (req, res) => {
|
|
|
381
483
|
});
|
|
382
484
|
});
|
|
383
485
|
|
|
486
|
+
app.get('/api/session', (req, res) => {
|
|
487
|
+
res.json({
|
|
488
|
+
ok: true,
|
|
489
|
+
token: apiSessionToken
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
384
493
|
app.get('/api/dashboard', async (req, res) => {
|
|
385
494
|
try {
|
|
386
495
|
res.json(await getDashboardData());
|
|
@@ -446,22 +555,12 @@ app.get('/api/pm2/status', async (req, res) => {
|
|
|
446
555
|
|
|
447
556
|
app.post('/api/pm2/save', async (req, res) => {
|
|
448
557
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
});
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
res.json({
|
|
460
|
-
ok: true,
|
|
461
|
-
message: 'PM2 state saved',
|
|
462
|
-
output: stdout || '',
|
|
463
|
-
errors: stderr || ''
|
|
464
|
-
});
|
|
558
|
+
await savePm2State();
|
|
559
|
+
res.json({
|
|
560
|
+
ok: true,
|
|
561
|
+
message: 'PM2 state saved',
|
|
562
|
+
output: '',
|
|
563
|
+
errors: ''
|
|
465
564
|
});
|
|
466
565
|
} catch (error) {
|
|
467
566
|
console.error('Save PM2 state API error:', error);
|
|
@@ -532,61 +631,28 @@ app.post('/api/processes', async (req, res) => {
|
|
|
532
631
|
|
|
533
632
|
const parsedArgs = parseProcessArgsInput(argsInput);
|
|
534
633
|
const parsedNodeArgs = parseProcessArgsInput(nodeArgsInput);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (watch) {
|
|
550
|
-
pm2Args.push('--watch');
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (maxMemoryRestart) {
|
|
554
|
-
pm2Args.push('--max-memory-restart', maxMemoryRestart);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (outLogPath) {
|
|
558
|
-
pm2Args.push('--output', outLogPath);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (errorLogPath) {
|
|
562
|
-
pm2Args.push('--error', errorLogPath);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (parsedArgs.length > 0) {
|
|
566
|
-
pm2Args.push('--', ...parsedArgs);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
execFile(pm2CliPath, pm2Args, {
|
|
634
|
+
await createProcess({
|
|
635
|
+
script: scriptPath,
|
|
636
|
+
name: processName,
|
|
637
|
+
cwd,
|
|
638
|
+
...(interpreter ? { interpreter } : {}),
|
|
639
|
+
...(parsedNodeArgs.length > 0 ? { node_args: parsedNodeArgs } : {}),
|
|
640
|
+
...(instances ? { instances } : {}),
|
|
641
|
+
...(watch ? { watch: true } : {}),
|
|
642
|
+
...(maxMemoryRestart ? { max_memory_restart: maxMemoryRestart } : {}),
|
|
643
|
+
...(outLogPath ? { output: outLogPath } : {}),
|
|
644
|
+
...(errorLogPath ? { error: errorLogPath } : {}),
|
|
645
|
+
...(parsedArgs.length > 0 ? { args: parsedArgs } : {}),
|
|
570
646
|
env: {
|
|
571
647
|
...process.env,
|
|
572
648
|
...(nodeEnv ? { NODE_ENV: nodeEnv } : {})
|
|
573
649
|
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
});
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
res.json({
|
|
585
|
-
ok: true,
|
|
586
|
-
message: `Created process: ${processName}`,
|
|
587
|
-
output: stdout || '',
|
|
588
|
-
errors: stderr || ''
|
|
589
|
-
});
|
|
650
|
+
});
|
|
651
|
+
res.json({
|
|
652
|
+
ok: true,
|
|
653
|
+
message: `Created process: ${processName}`,
|
|
654
|
+
output: '',
|
|
655
|
+
errors: ''
|
|
590
656
|
});
|
|
591
657
|
} catch (error) {
|
|
592
658
|
console.error('Create process API error:', error);
|
|
@@ -607,23 +673,12 @@ app.delete('/api/processes/:target', async (req, res) => {
|
|
|
607
673
|
}
|
|
608
674
|
|
|
609
675
|
await assertProcessCanBeControlled(target, 'deleted');
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
error: stderr || error.message || 'Failed to delete process'
|
|
617
|
-
});
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
res.json({
|
|
622
|
-
ok: true,
|
|
623
|
-
message: `Deleted process PM2 ID: ${target}`,
|
|
624
|
-
output: stdout || '',
|
|
625
|
-
errors: stderr || ''
|
|
626
|
-
});
|
|
676
|
+
await deleteProcess(target);
|
|
677
|
+
res.json({
|
|
678
|
+
ok: true,
|
|
679
|
+
message: `Deleted process PM2 ID: ${target}`,
|
|
680
|
+
output: '',
|
|
681
|
+
errors: ''
|
|
627
682
|
});
|
|
628
683
|
} catch (error) {
|
|
629
684
|
console.error('Delete process API error:', error);
|
|
@@ -686,32 +741,14 @@ app.get('/api/processes/:target/logs', async (req, res) => {
|
|
|
686
741
|
? Math.min(Math.max(Math.trunc(requestedLines), 1), 500)
|
|
687
742
|
: 100;
|
|
688
743
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
console.error('Logs API error:', error);
|
|
694
|
-
res.status(500).json({
|
|
695
|
-
ok: false,
|
|
696
|
-
error: error.message || 'Failed to load logs'
|
|
697
|
-
});
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const entries = [
|
|
702
|
-
...parsePm2LogText(stdout, 'stdout', target),
|
|
703
|
-
...parsePm2LogText(stderr, 'stderr', target)
|
|
704
|
-
];
|
|
705
|
-
|
|
706
|
-
res.json({
|
|
707
|
-
ok: true,
|
|
708
|
-
target,
|
|
709
|
-
entries
|
|
710
|
-
});
|
|
744
|
+
res.json({
|
|
745
|
+
ok: true,
|
|
746
|
+
target,
|
|
747
|
+
entries: await getLogEntriesForTarget(target, lines)
|
|
711
748
|
});
|
|
712
749
|
} catch (error) {
|
|
713
750
|
console.error('Logs API error:', error);
|
|
714
|
-
res.status(500).json({
|
|
751
|
+
res.status(error.statusCode || 500).json({
|
|
715
752
|
ok: false,
|
|
716
753
|
error: error.message || 'Failed to load logs'
|
|
717
754
|
});
|
|
@@ -729,13 +766,7 @@ app.get('/api/processes/:target/logs/stream', async (req, res) => {
|
|
|
729
766
|
res.write('retry: 5000\n\n');
|
|
730
767
|
|
|
731
768
|
let isClosed = false;
|
|
732
|
-
|
|
733
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
734
|
-
});
|
|
735
|
-
const buffers = {
|
|
736
|
-
stdout: '',
|
|
737
|
-
stderr: ''
|
|
738
|
-
};
|
|
769
|
+
let logBus = null;
|
|
739
770
|
|
|
740
771
|
function writeEntries(rawText, type) {
|
|
741
772
|
if (isClosed) {
|
|
@@ -749,13 +780,6 @@ app.get('/api/processes/:target/logs/stream', async (req, res) => {
|
|
|
749
780
|
});
|
|
750
781
|
}
|
|
751
782
|
|
|
752
|
-
function handleChunk(type, chunk) {
|
|
753
|
-
buffers[type] += chunk.toString('utf8');
|
|
754
|
-
const lines = buffers[type].split(/\r?\n/);
|
|
755
|
-
buffers[type] = lines.pop() || '';
|
|
756
|
-
writeEntries(lines.join('\n'), type);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
783
|
const heartbeatIntervalId = setInterval(() => {
|
|
760
784
|
if (isClosed) {
|
|
761
785
|
return;
|
|
@@ -765,60 +789,55 @@ app.get('/api/processes/:target/logs/stream', async (req, res) => {
|
|
|
765
789
|
res.write(`data: ${JSON.stringify({ ok: true })}\n\n`);
|
|
766
790
|
}, 15000);
|
|
767
791
|
|
|
768
|
-
|
|
769
|
-
handleChunk('stdout', chunk);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
logProcess.stderr.on('data', (chunk) => {
|
|
773
|
-
handleChunk('stderr', chunk);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
logProcess.on('error', (error) => {
|
|
792
|
+
function handleStreamError(error) {
|
|
777
793
|
console.error('Logs stream API error:', error);
|
|
778
794
|
if (isClosed) {
|
|
779
795
|
return;
|
|
780
796
|
}
|
|
781
797
|
res.write(`event: logs-error\n`);
|
|
782
798
|
res.write(`data: ${JSON.stringify({ ok: false, error: error.message || 'Failed to stream logs' })}\n\n`);
|
|
783
|
-
}
|
|
799
|
+
}
|
|
784
800
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (!
|
|
790
|
-
|
|
801
|
+
try {
|
|
802
|
+
const processes = await listProcesses();
|
|
803
|
+
const process = findProcessByTarget(processes, target);
|
|
804
|
+
|
|
805
|
+
if (!process) {
|
|
806
|
+
throw Object.assign(new Error('PM2 target was not found'), { statusCode: 404 });
|
|
791
807
|
}
|
|
792
|
-
|
|
808
|
+
|
|
809
|
+
logBus = await connectLogBus();
|
|
810
|
+
logBus.bus.on('log:*', (type, packet) => {
|
|
811
|
+
const packetProcess = packet?.process || {};
|
|
812
|
+
const normalizedTarget = String(target || '').trim();
|
|
813
|
+
|
|
814
|
+
if (String(packetProcess.pm_id ?? '') !== normalizedTarget && String(packetProcess.name || '') !== normalizedTarget) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
writeEntries(String(packet?.data || ''), getLiveLogType(type));
|
|
819
|
+
});
|
|
820
|
+
} catch (error) {
|
|
821
|
+
handleStreamError(error);
|
|
822
|
+
}
|
|
793
823
|
|
|
794
824
|
req.on('close', () => {
|
|
795
825
|
isClosed = true;
|
|
796
826
|
clearInterval(heartbeatIntervalId);
|
|
797
|
-
|
|
827
|
+
logBus?.close();
|
|
798
828
|
});
|
|
799
829
|
});
|
|
800
830
|
|
|
801
831
|
app.post('/api/processes/:target/logs/clear', async (req, res) => {
|
|
802
832
|
try {
|
|
803
833
|
const target = String(req.params.target || '');
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
});
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
res.json({
|
|
816
|
-
ok: true,
|
|
817
|
-
target,
|
|
818
|
-
message: `Cleared logs for ${target}`,
|
|
819
|
-
output: stdout || '',
|
|
820
|
-
errors: stderr || ''
|
|
821
|
-
});
|
|
834
|
+
await flushProcessLogs(target);
|
|
835
|
+
res.json({
|
|
836
|
+
ok: true,
|
|
837
|
+
target,
|
|
838
|
+
message: `Cleared logs for ${target}`,
|
|
839
|
+
output: '',
|
|
840
|
+
errors: ''
|
|
822
841
|
});
|
|
823
842
|
} catch (error) {
|
|
824
843
|
console.error('Clear logs API error:', error);
|
package/user_manual.md
CHANGED
|
@@ -21,10 +21,9 @@ Node WebUI is a local web dashboard for monitoring and managing Node.js services
|
|
|
21
21
|
- macOS
|
|
22
22
|
- Node.js 18 or newer
|
|
23
23
|
- npm
|
|
24
|
-
- PM2
|
|
25
|
-
- One or more PM2-managed processes, unless you only want to create a process from the UI
|
|
24
|
+
- One or more PM2-managed processes, unless you only want to create one from the UI
|
|
26
25
|
|
|
27
|
-
Install PM2 globally if
|
|
26
|
+
Node WebUI includes PM2 as a package dependency for dashboard operations. Install PM2 globally only if you want to use the `pm2` command in your terminal:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
29
|
npm install -g pm2
|
|
@@ -52,6 +51,8 @@ Install dependencies:
|
|
|
52
51
|
npm install
|
|
53
52
|
```
|
|
54
53
|
|
|
54
|
+
This installs both runtime dependencies and development dependencies needed to rebuild the React frontend. The published npm package ships the built frontend bundle for normal CLI use.
|
|
55
|
+
|
|
55
56
|
Start Node WebUI:
|
|
56
57
|
|
|
57
58
|
```bash
|
|
@@ -61,7 +62,7 @@ npm start
|
|
|
61
62
|
The server binds to localhost by default:
|
|
62
63
|
|
|
63
64
|
```text
|
|
64
|
-
http://127.0.0.1:
|
|
65
|
+
http://127.0.0.1:3210
|
|
65
66
|
```
|
|
66
67
|
|
|
67
68
|
## Opening The App
|
|
@@ -69,10 +70,10 @@ http://127.0.0.1:3000
|
|
|
69
70
|
Node WebUI serves the React operations console as the default UI:
|
|
70
71
|
|
|
71
72
|
```text
|
|
72
|
-
http://127.0.0.1:
|
|
73
|
+
http://127.0.0.1:3210
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
`npm start` builds the React frontend before starting Express. The compatibility path `http://127.0.0.1:
|
|
76
|
+
`npm start` builds the React frontend before starting Express. The compatibility path `http://127.0.0.1:3210/react` also loads the same React UI.
|
|
76
77
|
|
|
77
78
|
## Optional Environment Variables
|
|
78
79
|
|
|
@@ -212,6 +213,8 @@ Per-process action buttons:
|
|
|
212
213
|
|
|
213
214
|
Some actions require confirmation before they run.
|
|
214
215
|
|
|
216
|
+
Process actions, create, delete, save, log clearing, and Finder reveal are sent through Node WebUI's local API with a same-origin session token. If the page was loaded from the Node WebUI server, this is handled automatically.
|
|
217
|
+
|
|
215
218
|
## Bulk Actions
|
|
216
219
|
|
|
217
220
|
Select one or more rows using the table checkboxes. The bulk action bar supports:
|
|
@@ -255,6 +258,8 @@ The logs workspace supports:
|
|
|
255
258
|
|
|
256
259
|
The row-level log action opens logs for that specific process.
|
|
257
260
|
|
|
261
|
+
Recent log history is read from the selected process stdout and stderr log files reported by PM2. Live log updates use PM2's Node API event bus. If a PM2 log file is missing or has been rotated away, that stream may show no recent entries until the process writes new output.
|
|
262
|
+
|
|
258
263
|
## Create PM2 App
|
|
259
264
|
|
|
260
265
|
Click `Create PM2 App` to register a new PM2 process.
|
|
@@ -407,7 +412,7 @@ npm start
|
|
|
407
412
|
|
|
408
413
|
### Process actions fail
|
|
409
414
|
|
|
410
|
-
Check
|
|
415
|
+
Check PM2 from the same shell/user that starts Node WebUI:
|
|
411
416
|
|
|
412
417
|
```bash
|
|
413
418
|
pm2 -v
|
|
@@ -421,6 +426,8 @@ pm2 list
|
|
|
421
426
|
|
|
422
427
|
If the process is protected, remove it from the protected list or adjust `PROTECTED_PM2_PROCESSES`.
|
|
423
428
|
|
|
429
|
+
If the browser tab has been open across a Node WebUI restart, refresh the page so it fetches a fresh local API session token.
|
|
430
|
+
|
|
424
431
|
### Open folder does not work
|
|
425
432
|
|
|
426
433
|
Open folder is designed for macOS Finder. It requires the process to have a valid script path or working directory.
|
|
@@ -456,3 +463,5 @@ Node WebUI is designed for local use. The server binds to:
|
|
|
456
463
|
```
|
|
457
464
|
|
|
458
465
|
Do not expose it directly to the public internet. If you need remote access, put it behind authentication and a reverse proxy.
|
|
466
|
+
|
|
467
|
+
Unsafe API methods require a same-origin session token generated by the local server. This reduces the risk of another website triggering local PM2 actions from your browser, but it is not full authentication and should not be treated as protection for LAN or internet exposure.
|