ezpm2gui 1.3.2 → 1.5.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 +295 -294
- package/bin/ezpm2gui.js +8 -8
- package/bin/ezpm2gui.ts +51 -51
- package/bin/generate-ecosystem.js +35 -35
- package/bin/generate-ecosystem.ts +56 -56
- package/dist/index.js +1 -1
- package/dist/server/config/project-configs.json +236 -236
- package/dist/server/index.js +256 -83
- package/dist/server/routes/deployApplication.js +6 -5
- package/dist/server/routes/logStreaming.js +20 -13
- package/dist/server/routes/modules.js +89 -69
- package/dist/server/routes/remoteConnections.js +279 -40
- package/dist/server/routes/updates.d.ts +3 -0
- package/dist/server/routes/updates.js +135 -0
- package/dist/server/utils/encryption.js +0 -12
- package/dist/server/utils/pm2-connection.d.ts +1 -1
- package/dist/server/utils/pm2-connection.js +1 -3
- package/dist/server/utils/remote-connection.d.ts +36 -3
- package/dist/server/utils/remote-connection.js +307 -79
- package/package.json +73 -69
- package/scripts/postinstall.js +36 -36
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/favicon.ico +2 -2
- package/src/client/build/index.html +1 -1
- package/src/client/build/logo192.svg +7 -7
- package/src/client/build/logo512.svg +7 -7
- package/src/client/build/manifest.json +24 -24
- package/src/client/build/static/css/main.2d095544.css +5 -0
- package/src/client/build/static/css/main.2d095544.css.map +1 -0
- package/src/client/build/static/js/main.17e17668.js +3 -0
- package/src/client/build/static/js/main.17e17668.js.map +1 -0
- package/dist/server/config/cron-jobs.json +0 -18
- package/dist/server/config/cron-scripts/6d8d5e1d-2bc8-463f-82a6-6c294f2b9dbe.sh +0 -2
- package/dist/server/config/remote-connections.json +0 -22
- package/dist/server/logs/deployment.log +0 -12
- package/dist/server/utils/dialog.d.ts +0 -1
- package/dist/server/utils/dialog.js +0 -16
- package/dist/server/utils/upload.d.ts +0 -3
- package/dist/server/utils/upload.js +0 -39
- package/src/client/build/static/css/main.d46bc75c.css +0 -5
- package/src/client/build/static/css/main.d46bc75c.css.map +0 -1
- package/src/client/build/static/js/main.b0e1c9b1.js +0 -3
- package/src/client/build/static/js/main.b0e1c9b1.js.map +0 -1
- /package/src/client/build/static/js/{main.b0e1c9b1.js.LICENSE.txt → main.17e17668.js.LICENSE.txt} +0 -0
|
@@ -1,106 +1,126 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const express_1 = require("express");
|
|
4
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const pm2_1 = __importDefault(require("pm2"));
|
|
9
|
+
const pm2_connection_1 = require("../utils/pm2-connection");
|
|
5
10
|
const router = (0, express_1.Router)();
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
17
|
-
pm2Command.on('close', (code) => {
|
|
11
|
+
// @group Utilities : Resolve pm2 binary path via shell
|
|
12
|
+
const runPM2CLI = (args) => new Promise((resolve, reject) => {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
// Use shell so the OS resolves the global pm2 binary from PATH
|
|
15
|
+
const child = (0, child_process_1.exec)(`pm2 ${args}`);
|
|
16
|
+
let out = '';
|
|
17
|
+
let err = '';
|
|
18
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => { out += d.toString(); });
|
|
19
|
+
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => { err += d.toString(); });
|
|
20
|
+
child.on('close', (code) => {
|
|
18
21
|
if (code !== 0) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
22
|
+
reject(new Error(err || `pm2 exited with code ${code}`));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
resolve({ stdout: out, stderr: err });
|
|
23
26
|
}
|
|
24
|
-
|
|
27
|
+
});
|
|
28
|
+
child.on('error', (e) => reject(e));
|
|
29
|
+
});
|
|
30
|
+
// @group APIEndpoints : List installed PM2 modules
|
|
31
|
+
router.get('/', async (_req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
// Use PM2 Node.js API — modules appear as regular processes with module metadata
|
|
34
|
+
const processList = await (0, pm2_connection_1.executePM2Command)((cb) => pm2_1.default.list(cb));
|
|
35
|
+
// PM2 modules have pm2_env.pmx_module === true
|
|
36
|
+
const modules = processList
|
|
37
|
+
.filter((proc) => { var _a; return ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.pmx_module) === true; })
|
|
38
|
+
.map((proc) => {
|
|
39
|
+
var _a, _b, _c;
|
|
40
|
+
return ({
|
|
41
|
+
name: proc.name,
|
|
42
|
+
version: ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.version) || ((_b = proc.pm2_env) === null || _b === void 0 ? void 0 : _b.MODULE_VERSION) || 'N/A',
|
|
43
|
+
status: ((_c = proc.pm2_env) === null || _c === void 0 ? void 0 : _c.status) || 'unknown',
|
|
44
|
+
pid: proc.pid,
|
|
45
|
+
pm_id: proc.pm_id,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
res.json(modules);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Fallback: parse pm2 module:list CLI output
|
|
25
52
|
try {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
53
|
+
const { stdout } = await runPM2CLI('module:list');
|
|
54
|
+
const moduleLines = stdout
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter(line => line.includes('│') && !line.includes('Module') && line.trim() !== '');
|
|
57
|
+
const modules = moduleLines
|
|
58
|
+
.map(line => {
|
|
28
59
|
const parts = line.split('│').map(part => part.trim()).filter(Boolean);
|
|
29
60
|
if (parts.length >= 3) {
|
|
30
|
-
return {
|
|
31
|
-
name: parts[0],
|
|
32
|
-
version: parts[1],
|
|
33
|
-
status: parts[2]
|
|
34
|
-
};
|
|
61
|
+
return { name: parts[0], version: parts[1], status: parts[2] };
|
|
35
62
|
}
|
|
36
63
|
return null;
|
|
37
|
-
})
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
38
66
|
res.json(modules);
|
|
39
67
|
}
|
|
40
|
-
catch (
|
|
68
|
+
catch (fallbackError) {
|
|
41
69
|
res.status(500).json({
|
|
42
|
-
error: 'Failed to
|
|
43
|
-
details:
|
|
70
|
+
error: 'Failed to list PM2 modules',
|
|
71
|
+
details: fallbackError.message
|
|
44
72
|
});
|
|
45
73
|
}
|
|
46
|
-
}
|
|
74
|
+
}
|
|
47
75
|
});
|
|
48
|
-
// Install a module
|
|
49
|
-
router.post('/install', (req, res) => {
|
|
76
|
+
// @group APIEndpoints : Install a PM2 module
|
|
77
|
+
router.post('/install', async (req, res) => {
|
|
50
78
|
const { moduleName } = req.body;
|
|
51
79
|
if (!moduleName) {
|
|
52
80
|
return res.status(400).json({ error: 'Module name is required' });
|
|
53
81
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
pm2Command.stderr.on('data', (data) => {
|
|
61
|
-
errorOutput += data.toString();
|
|
62
|
-
});
|
|
63
|
-
pm2Command.on('close', (code) => {
|
|
64
|
-
if (code !== 0) {
|
|
65
|
-
return res.status(500).json({
|
|
66
|
-
error: `Failed to install module: ${moduleName}`,
|
|
67
|
-
details: errorOutput
|
|
68
|
-
});
|
|
69
|
-
}
|
|
82
|
+
// Basic validation — only allow alphanumeric, @, /, -, .
|
|
83
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
84
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const { stdout } = await runPM2CLI(`install ${moduleName}`);
|
|
70
88
|
res.json({
|
|
71
89
|
success: true,
|
|
72
90
|
message: `Successfully installed module: ${moduleName}`,
|
|
73
|
-
details:
|
|
91
|
+
details: stdout
|
|
74
92
|
});
|
|
75
|
-
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
res.status(500).json({
|
|
96
|
+
error: `Failed to install module: ${moduleName}`,
|
|
97
|
+
details: error.message
|
|
98
|
+
});
|
|
99
|
+
}
|
|
76
100
|
});
|
|
77
|
-
// Uninstall a module
|
|
78
|
-
router.delete('/:moduleName', (req, res) => {
|
|
101
|
+
// @group APIEndpoints : Uninstall a PM2 module
|
|
102
|
+
router.delete('/:moduleName', async (req, res) => {
|
|
79
103
|
const { moduleName } = req.params;
|
|
80
104
|
if (!moduleName) {
|
|
81
105
|
return res.status(400).json({ error: 'Module name is required' });
|
|
82
106
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
pm2Command.stderr.on('data', (data) => {
|
|
90
|
-
errorOutput += data.toString();
|
|
91
|
-
});
|
|
92
|
-
pm2Command.on('close', (code) => {
|
|
93
|
-
if (code !== 0) {
|
|
94
|
-
return res.status(500).json({
|
|
95
|
-
error: `Failed to uninstall module: ${moduleName}`,
|
|
96
|
-
details: errorOutput
|
|
97
|
-
});
|
|
98
|
-
}
|
|
107
|
+
// Basic validation
|
|
108
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
109
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await runPM2CLI(`uninstall ${moduleName}`);
|
|
99
113
|
res.json({
|
|
100
114
|
success: true,
|
|
101
115
|
message: `Successfully uninstalled module: ${moduleName}`,
|
|
102
|
-
details:
|
|
116
|
+
details: stdout
|
|
103
117
|
});
|
|
104
|
-
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
res.status(500).json({
|
|
121
|
+
error: `Failed to uninstall module: ${moduleName}`,
|
|
122
|
+
details: error.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
105
125
|
});
|
|
106
126
|
exports.default = router;
|
|
@@ -6,6 +6,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const express_1 = __importDefault(require("express"));
|
|
7
7
|
const remote_connection_1 = require("../utils/remote-connection");
|
|
8
8
|
const router = express_1.default.Router();
|
|
9
|
+
// @group Security : Validate remote log file paths before shell interpolation
|
|
10
|
+
const SHELL_UNSAFE_CHARS = /['"`;$|&<>(){}\\\n\r\0]/;
|
|
11
|
+
const MAX_LOG_LINES = 10000;
|
|
12
|
+
const validateRemotePath = (filePath) => {
|
|
13
|
+
if (!filePath)
|
|
14
|
+
return false;
|
|
15
|
+
if (filePath.includes('..'))
|
|
16
|
+
return false;
|
|
17
|
+
if (SHELL_UNSAFE_CHARS.test(filePath))
|
|
18
|
+
return false;
|
|
19
|
+
if (!/\.(log|gz)$/i.test(filePath))
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
const safeLogLines = (raw) => {
|
|
24
|
+
const n = parseInt(raw || '200', 10);
|
|
25
|
+
if (!Number.isFinite(n) || n < 0)
|
|
26
|
+
return 200;
|
|
27
|
+
return Math.min(n, MAX_LOG_LINES);
|
|
28
|
+
};
|
|
9
29
|
/**
|
|
10
30
|
* Connect to an existing remote server
|
|
11
31
|
* POST /api/remote/:connectionId/connect
|
|
@@ -20,14 +40,11 @@ router.post('/:connectionId/connect', async (req, res) => {
|
|
|
20
40
|
error: 'Connection not found'
|
|
21
41
|
});
|
|
22
42
|
}
|
|
23
|
-
//
|
|
43
|
+
// Establish the SSH connection only — PM2 detection happens lazily
|
|
44
|
+
// when processes are first requested, avoiding a slow multi-command
|
|
45
|
+
// pre-check that causes browser timeouts on the connect button.
|
|
24
46
|
await connection.connect();
|
|
25
|
-
|
|
26
|
-
const isPM2Installed = await connection.checkPM2Installation();
|
|
27
|
-
res.json({
|
|
28
|
-
success: true,
|
|
29
|
-
isPM2Installed
|
|
30
|
-
});
|
|
47
|
+
res.json({ success: true });
|
|
31
48
|
}
|
|
32
49
|
catch (error) {
|
|
33
50
|
console.error('Connection error:', error);
|
|
@@ -481,29 +498,36 @@ router.get('/:connectionId/logs/:processId', async (req, res) => {
|
|
|
481
498
|
success: false,
|
|
482
499
|
error: 'Connection not established'
|
|
483
500
|
});
|
|
484
|
-
}
|
|
485
|
-
|
|
501
|
+
}
|
|
502
|
+
// Get log paths from PM2 process info — use the PATH-fallback executor so
|
|
503
|
+
// pm2 is found regardless of the remote shell environment (nvm, npm-global, etc.)
|
|
504
|
+
const processInfoResult = await connection.executePM2Command('jlist');
|
|
486
505
|
if (processInfoResult.code !== 0) {
|
|
487
506
|
return res.status(500).json({
|
|
488
507
|
success: false,
|
|
489
|
-
error: 'Failed to get process
|
|
508
|
+
error: 'Failed to get PM2 process list'
|
|
490
509
|
});
|
|
491
510
|
}
|
|
492
511
|
let processInfo;
|
|
493
512
|
try {
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
let raw = processInfoResult.stdout.trim();
|
|
514
|
+
const start = raw.indexOf('[');
|
|
515
|
+
const end = raw.lastIndexOf(']') + 1;
|
|
516
|
+
if (start !== -1 && end > 0)
|
|
517
|
+
raw = raw.substring(start, end);
|
|
518
|
+
const processList = JSON.parse(raw);
|
|
519
|
+
processInfo = processList.find((p) => p.pm_id === parseInt(processId, 10) || p.name === processId);
|
|
520
|
+
if (!processInfo) {
|
|
496
521
|
return res.status(404).json({
|
|
497
522
|
success: false,
|
|
498
523
|
error: 'Process not found'
|
|
499
524
|
});
|
|
500
525
|
}
|
|
501
|
-
processInfo = processInfo[0];
|
|
502
526
|
}
|
|
503
527
|
catch (parseError) {
|
|
504
528
|
return res.status(500).json({
|
|
505
529
|
success: false,
|
|
506
|
-
error: 'Failed to parse process
|
|
530
|
+
error: 'Failed to parse PM2 process list'
|
|
507
531
|
});
|
|
508
532
|
}
|
|
509
533
|
const outLogPath = (_a = processInfo.pm2_env) === null || _a === void 0 ? void 0 : _a.pm_out_log_path;
|
|
@@ -548,6 +572,246 @@ router.get('/:connectionId/logs/:processId', async (req, res) => {
|
|
|
548
572
|
});
|
|
549
573
|
}
|
|
550
574
|
});
|
|
575
|
+
// @group LogHistory : Helper — resolve log path for a given process on a remote connection
|
|
576
|
+
const resolveRemoteLogPath = async (connection, processId, logType) => {
|
|
577
|
+
var _a, _b;
|
|
578
|
+
const processInfoResult = await connection.executePM2Command('jlist');
|
|
579
|
+
if (processInfoResult.code !== 0)
|
|
580
|
+
return { logPath: null, error: 'Failed to get PM2 process list' };
|
|
581
|
+
try {
|
|
582
|
+
let raw = processInfoResult.stdout.trim();
|
|
583
|
+
const start = raw.indexOf('[');
|
|
584
|
+
const end = raw.lastIndexOf(']') + 1;
|
|
585
|
+
if (start !== -1 && end > 0)
|
|
586
|
+
raw = raw.substring(start, end);
|
|
587
|
+
const processList = JSON.parse(raw);
|
|
588
|
+
const proc = processList.find((p) => p.pm_id === parseInt(processId, 10) || p.name === processId);
|
|
589
|
+
if (!proc)
|
|
590
|
+
return { logPath: null, error: 'Process not found' };
|
|
591
|
+
const key = logType === 'out' ? 'pm_out_log_path' : 'pm_err_log_path';
|
|
592
|
+
return { logPath: (_b = (_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a[key]) !== null && _b !== void 0 ? _b : null };
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return { logPath: null, error: 'Failed to parse PM2 process list' };
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Get log lines from a specific log type on a remote process — ?lines=N (default 200, 0 = all)
|
|
600
|
+
* GET /api/remote/:connectionId/logs/:processId/:type
|
|
601
|
+
*/
|
|
602
|
+
router.get('/:connectionId/logs/:processId/:type', async (req, res) => {
|
|
603
|
+
var _a;
|
|
604
|
+
try {
|
|
605
|
+
const { connectionId, processId, type } = req.params;
|
|
606
|
+
const logType = type === 'err' ? 'err' : 'out';
|
|
607
|
+
const lines = safeLogLines(req.query.lines);
|
|
608
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
609
|
+
if (!connection)
|
|
610
|
+
return res.status(404).json({ success: false, error: 'Connection not found' });
|
|
611
|
+
if (!connection.isConnected())
|
|
612
|
+
return res.status(400).json({ success: false, error: 'Not connected' });
|
|
613
|
+
const { logPath, error } = await resolveRemoteLogPath(connection, processId, logType);
|
|
614
|
+
if (!logPath)
|
|
615
|
+
return res.status(404).json({ success: false, error: error || 'Log path not found' });
|
|
616
|
+
// tail -n 0 = all lines; use wc -l to get total count alongside
|
|
617
|
+
const lineArg = lines === 0 ? '+1' : `-${lines}`;
|
|
618
|
+
const cmd = `{ wc -l < "${logPath}" 2>/dev/null || echo 0; } && tail -n ${lineArg} "${logPath}" 2>/dev/null`;
|
|
619
|
+
let result = await connection.executeCommand(cmd);
|
|
620
|
+
// Fallback to sudo if the file is unreadable (root-owned logs)
|
|
621
|
+
if (result.code !== 0 || !result.stdout.trim()) {
|
|
622
|
+
result = await connection.executeCommand(`{ sudo wc -l < "${logPath}" 2>/dev/null || echo 0; } && sudo tail -n ${lineArg} "${logPath}" 2>/dev/null`);
|
|
623
|
+
}
|
|
624
|
+
const outputLines = result.stdout.split('\n');
|
|
625
|
+
const totalLines = parseInt(((_a = outputLines[0]) === null || _a === void 0 ? void 0 : _a.trim()) || '0', 10);
|
|
626
|
+
const logLines = outputLines.slice(1).filter((l) => l.trim() !== '');
|
|
627
|
+
res.json({ logs: logLines, logPath, totalLines });
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
console.error('Error fetching remote log history:', error);
|
|
631
|
+
res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
/**
|
|
635
|
+
* Download full log file from a remote process via SSH
|
|
636
|
+
* GET /api/remote/:connectionId/logs/:processId/:type/download
|
|
637
|
+
*/
|
|
638
|
+
router.get('/:connectionId/logs/:processId/:type/download', async (req, res) => {
|
|
639
|
+
try {
|
|
640
|
+
const { connectionId, processId, type } = req.params;
|
|
641
|
+
const logType = type === 'err' ? 'err' : 'out';
|
|
642
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
643
|
+
if (!connection)
|
|
644
|
+
return res.status(404).json({ success: false, error: 'Connection not found' });
|
|
645
|
+
if (!connection.isConnected())
|
|
646
|
+
return res.status(400).json({ success: false, error: 'Not connected' });
|
|
647
|
+
const { logPath, error } = await resolveRemoteLogPath(connection, processId, logType);
|
|
648
|
+
if (!logPath)
|
|
649
|
+
return res.status(404).json({ success: false, error: error || 'Log path not found' });
|
|
650
|
+
const fileName = `${processId}-${logType}.log`;
|
|
651
|
+
await connection.streamFileToResponse(logPath, res, fileName);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
console.error('Error downloading remote log:', error);
|
|
655
|
+
if (!res.headersSent) {
|
|
656
|
+
res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
/**
|
|
661
|
+
* List all log files (current + rotated) for a remote process
|
|
662
|
+
* GET /api/remote/:connectionId/log-files/:processId
|
|
663
|
+
* Uses /log-files/ prefix to avoid Express matching /:processId/:type with type='files'
|
|
664
|
+
*/
|
|
665
|
+
router.get('/:connectionId/log-files/:processId', async (req, res) => {
|
|
666
|
+
try {
|
|
667
|
+
const { connectionId, processId } = req.params;
|
|
668
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
669
|
+
if (!connection)
|
|
670
|
+
return res.status(404).json({ success: false, error: 'Connection not found' });
|
|
671
|
+
if (!connection.isConnected())
|
|
672
|
+
return res.status(400).json({ success: false, error: 'Not connected' });
|
|
673
|
+
// Resolve both log paths so we know the directory and base name
|
|
674
|
+
const { logPath: outPath } = await resolveRemoteLogPath(connection, processId, 'out');
|
|
675
|
+
const { logPath: errPath } = await resolveRemoteLogPath(connection, processId, 'err');
|
|
676
|
+
if (!outPath && !errPath) {
|
|
677
|
+
return res.status(404).json({ success: false, error: 'No log paths found for this process' });
|
|
678
|
+
}
|
|
679
|
+
// Derive log directory + base name from the out log path (or err if out missing)
|
|
680
|
+
const refPath = outPath || errPath;
|
|
681
|
+
const logDir = refPath.substring(0, refPath.lastIndexOf('/'));
|
|
682
|
+
const baseName = refPath.split('/').pop().replace(/-out\.log.*$/, '').replace(/-(error|err)\.log.*$/, '');
|
|
683
|
+
// List matching files in the log directory
|
|
684
|
+
const lsCmd = `ls -la "${logDir}" 2>/dev/null`;
|
|
685
|
+
let lsResult = await connection.executeCommand(lsCmd);
|
|
686
|
+
if (lsResult.code !== 0)
|
|
687
|
+
lsResult = await connection.executeCommand(`sudo ${lsCmd}`);
|
|
688
|
+
const files = [];
|
|
689
|
+
if (lsResult.code === 0) {
|
|
690
|
+
for (const line of lsResult.stdout.split('\n')) {
|
|
691
|
+
// ls -la line: permissions links owner group size month day time name
|
|
692
|
+
const parts = line.trim().split(/\s+/);
|
|
693
|
+
if (parts.length < 9)
|
|
694
|
+
continue;
|
|
695
|
+
const fileName = parts.slice(8).join(' ');
|
|
696
|
+
if (!fileName.startsWith(baseName))
|
|
697
|
+
continue;
|
|
698
|
+
const size = parseInt(parts[4], 10) || 0;
|
|
699
|
+
const month = parts[5];
|
|
700
|
+
const day = parts[6];
|
|
701
|
+
const timeOrYr = parts[7];
|
|
702
|
+
const modified = `${month} ${day} ${timeOrYr}`;
|
|
703
|
+
let type = 'unknown';
|
|
704
|
+
if (fileName.includes('-out'))
|
|
705
|
+
type = 'out';
|
|
706
|
+
else if (fileName.includes('-error') || fileName.includes('-err'))
|
|
707
|
+
type = 'err';
|
|
708
|
+
files.push({
|
|
709
|
+
name: fileName,
|
|
710
|
+
path: `${logDir}/${fileName}`,
|
|
711
|
+
size,
|
|
712
|
+
modified,
|
|
713
|
+
type,
|
|
714
|
+
compressed: fileName.endsWith('.gz'),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Sort: current files first (no date suffix), then rotated newest first
|
|
719
|
+
files.sort((a, b) => {
|
|
720
|
+
const aRot = a.name.includes('__') || /\d{4}-\d{2}/.test(a.name);
|
|
721
|
+
const bRot = b.name.includes('__') || /\d{4}-\d{2}/.test(b.name);
|
|
722
|
+
if (!aRot && bRot)
|
|
723
|
+
return -1;
|
|
724
|
+
if (aRot && !bRot)
|
|
725
|
+
return 1;
|
|
726
|
+
return b.name.localeCompare(a.name);
|
|
727
|
+
});
|
|
728
|
+
res.json({ files });
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
console.error('Error listing remote log files:', error);
|
|
732
|
+
res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
/**
|
|
736
|
+
* Read a specific remote log file by path — ?lines=N, handles .gz via zcat
|
|
737
|
+
* GET /api/remote/:connectionId/log-file?path=...&lines=N
|
|
738
|
+
* Uses /log-file top-level to avoid clashing with /logs/:processId routes
|
|
739
|
+
*/
|
|
740
|
+
router.get('/:connectionId/log-file', async (req, res) => {
|
|
741
|
+
var _a;
|
|
742
|
+
try {
|
|
743
|
+
const { connectionId } = req.params;
|
|
744
|
+
const filePath = req.query.path;
|
|
745
|
+
const lines = safeLogLines(req.query.lines);
|
|
746
|
+
if (!filePath)
|
|
747
|
+
return res.status(400).json({ error: 'path query parameter required' });
|
|
748
|
+
// Strict path validation — blocks traversal, shell metacharacters, and non-log extensions
|
|
749
|
+
if (!validateRemotePath(filePath)) {
|
|
750
|
+
return res.status(403).json({ error: 'Access denied: invalid or unsafe log file path' });
|
|
751
|
+
}
|
|
752
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
753
|
+
if (!connection)
|
|
754
|
+
return res.status(404).json({ success: false, error: 'Connection not found' });
|
|
755
|
+
if (!connection.isConnected())
|
|
756
|
+
return res.status(400).json({ success: false, error: 'Not connected' });
|
|
757
|
+
const isGz = filePath.endsWith('.gz');
|
|
758
|
+
const catCmd = isGz ? `zcat "${filePath}" 2>/dev/null` : `cat "${filePath}" 2>/dev/null`;
|
|
759
|
+
const lineArg = lines === 0 ? '' : `| tail -n ${lines}`;
|
|
760
|
+
const countCmd = isGz
|
|
761
|
+
? `zcat "${filePath}" 2>/dev/null | wc -l`
|
|
762
|
+
: `wc -l < "${filePath}" 2>/dev/null`;
|
|
763
|
+
const [contentResult, countResult] = await Promise.all([
|
|
764
|
+
connection.executeCommand(`${catCmd} ${lineArg}`).catch(() => ({ code: 1, stdout: '', stderr: '' })),
|
|
765
|
+
connection.executeCommand(countCmd).catch(() => ({ code: 1, stdout: '0', stderr: '' })),
|
|
766
|
+
]);
|
|
767
|
+
// Fallback to sudo on permission error
|
|
768
|
+
const content = contentResult.code === 0 && contentResult.stdout.trim()
|
|
769
|
+
? contentResult
|
|
770
|
+
: await connection.executeCommand(`sudo ${catCmd} ${lineArg}`);
|
|
771
|
+
const totalLines = parseInt(((_a = countResult.stdout) === null || _a === void 0 ? void 0 : _a.trim()) || '0', 10);
|
|
772
|
+
const logLines = (content.stdout || '').split('\n').filter((l) => l.trim() !== '');
|
|
773
|
+
res.json({ logs: logLines, totalLines });
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.error('Error reading remote log file:', error);
|
|
777
|
+
res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
/**
|
|
781
|
+
* Download a specific remote log file by path via SSH cat/zcat
|
|
782
|
+
* GET /api/remote/:connectionId/log-file/download?path=...
|
|
783
|
+
*/
|
|
784
|
+
router.get('/:connectionId/log-file/download', async (req, res) => {
|
|
785
|
+
try {
|
|
786
|
+
const { connectionId } = req.params;
|
|
787
|
+
const filePath = req.query.path;
|
|
788
|
+
if (!filePath)
|
|
789
|
+
return res.status(400).json({ error: 'path query parameter required' });
|
|
790
|
+
if (!validateRemotePath(filePath)) {
|
|
791
|
+
return res.status(403).json({ error: 'Access denied: invalid or unsafe log file path' });
|
|
792
|
+
}
|
|
793
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
794
|
+
if (!connection)
|
|
795
|
+
return res.status(404).json({ success: false, error: 'Connection not found' });
|
|
796
|
+
if (!connection.isConnected())
|
|
797
|
+
return res.status(400).json({ success: false, error: 'Not connected' });
|
|
798
|
+
// .gz files: stream via SFTP + local gunzip (no server-side memory buffering)
|
|
799
|
+
if (filePath.endsWith('.gz')) {
|
|
800
|
+
const gzName = filePath.split('/').pop().replace(/\.gz$/, '');
|
|
801
|
+
await connection.streamGzFileToResponse(filePath, res, gzName);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
// Plain files: SFTP stream
|
|
805
|
+
const fileName = filePath.split('/').pop();
|
|
806
|
+
await connection.streamFileToResponse(filePath, res, fileName);
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
console.error('Error downloading remote log file:', error);
|
|
810
|
+
if (!res.headersSent) {
|
|
811
|
+
res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
551
815
|
/**
|
|
552
816
|
* Get list of all remote connections with their status
|
|
553
817
|
* GET /api/remote/connections
|
|
@@ -561,7 +825,7 @@ router.get('/connections', async (req, res) => {
|
|
|
561
825
|
host: conn.host,
|
|
562
826
|
port: conn.port,
|
|
563
827
|
username: conn.username,
|
|
564
|
-
|
|
828
|
+
connected: conn.isConnected(),
|
|
565
829
|
isPM2Installed: conn.isPM2Installed
|
|
566
830
|
}));
|
|
567
831
|
res.json(connectionsList);
|
|
@@ -711,29 +975,4 @@ router.post('/:connectionId/install-pm2', async (req, res) => {
|
|
|
711
975
|
});
|
|
712
976
|
}
|
|
713
977
|
});
|
|
714
|
-
/**
|
|
715
|
-
* Delete a connection configuration
|
|
716
|
-
* DELETE /api/remote/connections/:connectionId
|
|
717
|
-
*/
|
|
718
|
-
router.delete('/connections/:connectionId', async (req, res) => {
|
|
719
|
-
try {
|
|
720
|
-
const { connectionId } = req.params;
|
|
721
|
-
// Disconnect if connected
|
|
722
|
-
await remote_connection_1.remoteConnectionManager.closeConnection(connectionId);
|
|
723
|
-
// Delete the connection from the manager
|
|
724
|
-
const success = remote_connection_1.remoteConnectionManager.deleteConnection(connectionId);
|
|
725
|
-
if (success) {
|
|
726
|
-
res.json({ success: true, message: 'Connection deleted' });
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
res.status(404).json({ success: false, error: 'Connection not found' });
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
catch (error) {
|
|
733
|
-
res.status(500).json({
|
|
734
|
-
success: false,
|
|
735
|
-
error: `Server error: ${error.message}`
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
978
|
exports.default = router;
|