ezpm2gui 1.6.0 → 1.9.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 +40 -6
- package/dist/server/config/project-configs.json +3 -4
- package/dist/server/index.js +44 -5
- package/dist/server/routes/deployApplication.js +43 -41
- package/dist/server/routes/logStreaming.js +66 -28
- package/dist/server/routes/modules.js +55 -0
- package/dist/server/routes/pageAuth.js +3 -3
- package/dist/server/routes/remoteConnections.js +57 -21
- package/dist/server/routes/remoteMetrics.d.ts +3 -0
- package/dist/server/routes/remoteMetrics.js +84 -0
- package/dist/server/services/ProjectSetupService.d.ts +1 -1
- package/dist/server/services/ProjectSetupService.js +25 -9
- package/dist/server/utils/encryption.d.ts +22 -0
- package/dist/server/utils/encryption.js +53 -0
- package/dist/server/utils/metrics-history.d.ts +21 -0
- package/dist/server/utils/metrics-history.js +68 -0
- package/dist/server/utils/remote-connection.js +3 -3
- package/dist/server/utils/remote-metrics-db.d.ts +29 -0
- package/dist/server/utils/remote-metrics-db.js +134 -0
- package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
- package/dist/server/utils/remote-metrics-poller.js +67 -0
- package/package.json +15 -2
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/index.html +1 -1
- package/src/client/build/static/css/main.2836d066.css +5 -0
- package/src/client/build/static/css/main.2836d066.css.map +1 -0
- package/src/client/build/static/js/main.d5c19622.js +3 -0
- package/src/client/build/static/js/main.d5c19622.js.map +1 -0
- package/src/client/build/static/css/main.775772ee.css +0 -5
- package/src/client/build/static/css/main.775772ee.css.map +0 -1
- package/src/client/build/static/js/main.cbcb09c9.js +0 -3
- package/src/client/build/static/js/main.cbcb09c9.js.map +0 -1
- /package/src/client/build/static/js/{main.cbcb09c9.js.LICENSE.txt → main.d5c19622.js.LICENSE.txt} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# EZ PM2 GUI
|
|
2
2
|
|
|
3
|
+
[](https://discord.gg/ttgc2zqK7b)
|
|
4
|
+
|
|
3
5
|
A modern web-based graphical user interface for the PM2 process manager, built with TypeScript, Tailwind CSS, and React.
|
|
4
6
|
|
|
5
7
|
## Screenshots
|
|
@@ -12,6 +14,14 @@ A modern web-based graphical user interface for the PM2 process manager, built w
|
|
|
12
14
|
|
|
13
15
|

|
|
14
16
|
|
|
17
|
+
**Metrics (Live)** — rolling 1-hour sparklines per process, updated every 3 seconds:
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
**Metrics (History)** — SQLite-backed CPU and memory charts with selectable time range:
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
15
25
|
**Deploy App** — start new PM2 processes from a structured form:
|
|
16
26
|
|
|
17
27
|

|
|
@@ -30,18 +40,24 @@ A modern web-based graphical user interface for the PM2 process manager, built w
|
|
|
30
40
|
|
|
31
41
|
- **Real-time process monitoring** - Keep track of all your PM2 processes in real-time
|
|
32
42
|
- **Process management** - Start, stop, restart, and delete processes with one click
|
|
43
|
+
- **Sidebar quick-actions** *(v1.9.0)* - Per-process restart, start/stop, and logs buttons revealed on hover in the sidebar
|
|
33
44
|
- **System metrics dashboard** - Monitor CPU, memory usage, and uptime
|
|
45
|
+
- **Metrics page with live sparklines** - Per-process rolling 1-hour CPU and memory micro-graphs updated every 3s; switch to History tab for SQLite-backed long-term charts
|
|
34
46
|
- **Enhanced log streaming** - View and filter logs from multiple processes simultaneously
|
|
47
|
+
- **Log search highlighting** *(v1.9.0)* - Search terms are visually highlighted in the log viewer
|
|
48
|
+
- **Log timestamp range filter** *(v1.9.0)* - Filter log output by start/end timestamp with snapshot mode
|
|
49
|
+
- **Remote log polling** *(v1.9.0)* - Logs from remote servers fetched and displayed in real-time
|
|
35
50
|
- **WebSocket for live updates** - Get instant updates without refreshing
|
|
36
51
|
- **Process CPU and memory charts** - Visualize performance metrics over time
|
|
37
52
|
- **Filter processes by status or name** - Quickly find the processes you need
|
|
38
|
-
- **Dark/light mode** - Fully supported across all pages with Tailwind CSS
|
|
53
|
+
- **Dark/light mode** - Fully supported across all pages with Tailwind CSS; preference persisted across sessions
|
|
39
54
|
- **Cluster management** - Easily scale your Node.js applications
|
|
40
55
|
- **Application deployment** - Deploy new applications directly from the UI
|
|
41
56
|
- **Ecosystem configuration** - Create and manage your PM2 ecosystem files
|
|
42
57
|
- **PM2 modules support** - Manage and configure PM2 modules
|
|
43
58
|
- **Cron Jobs** - Schedule and manage automated tasks with visual cron expression builder
|
|
44
59
|
- **Remote Server Management** - Connect and manage PM2 on remote servers via SSH
|
|
60
|
+
- **End-to-end encrypted credentials** *(v1.9.0)* - Remote server passwords encrypted in-browser with RSA-OAEP + AES-256-GCM before transmission
|
|
45
61
|
- **Advanced Monitoring Dashboard** - Real-time performance charts with health scoring
|
|
46
62
|
- **Tailwind CSS UI** - Sleek, compact, and responsive design with consistent dark/light theming
|
|
47
63
|
- **Fully typed with TypeScript** - Robust and maintainable codebase
|
|
@@ -55,9 +71,9 @@ Monitor all your PM2 processes in real-time with detailed information on CPU usa
|
|
|
55
71
|
Connect to and manage PM2 processes on remote servers via secure SSH connections:
|
|
56
72
|
- Add multiple remote server connections with SSH credentials
|
|
57
73
|
- View and manage processes on remote servers
|
|
58
|
-
- Stream logs from remote processes in real-time
|
|
74
|
+
- Stream logs from remote processes in real-time with polling
|
|
59
75
|
- Execute PM2 commands on remote machines
|
|
60
|
-
-
|
|
76
|
+
- **End-to-end credential encryption** — passwords are encrypted client-side (RSA-OAEP + AES-256-GCM hybrid scheme) before transmission; the server never sees plaintext passwords in transit
|
|
61
77
|
|
|
62
78
|
### Cron Jobs
|
|
63
79
|
Schedule and automate tasks using PM2's cron restart feature:
|
|
@@ -91,10 +107,13 @@ Easily scale your Node.js applications with the cluster management interface. Ad
|
|
|
91
107
|
### Log Streaming
|
|
92
108
|
View and filter logs from multiple processes simultaneously with the enhanced log streaming interface. Features include:
|
|
93
109
|
- Real-time log updates via WebSocket
|
|
110
|
+
- **Search with visual highlighting** — matched terms are highlighted inline
|
|
111
|
+
- **Timestamp range filter** — narrow logs to a start/end time window with snapshot mode (polling pauses while filter is active)
|
|
94
112
|
- Filtering by process, log level, or content
|
|
95
113
|
- Pausing and resuming log streams
|
|
96
114
|
- Download logs for offline analysis
|
|
97
115
|
- Floating log panel for remote process logs
|
|
116
|
+
- Remote server log polling
|
|
98
117
|
|
|
99
118
|
### Ecosystem Configuration
|
|
100
119
|
Generate and manage PM2 ecosystem configuration files directly from the UI. This makes it easy to set up complex application deployments and share configurations across your team.
|
|
@@ -169,7 +188,7 @@ ezpm2gui.start({
|
|
|
169
188
|
Once started, open your browser and navigate to:
|
|
170
189
|
|
|
171
190
|
```
|
|
172
|
-
http://localhost:
|
|
191
|
+
http://localhost:3101
|
|
173
192
|
```
|
|
174
193
|
|
|
175
194
|
## Requirements
|
|
@@ -181,8 +200,23 @@ http://localhost:3001
|
|
|
181
200
|
|
|
182
201
|
EZ PM2 GUI uses environment variables for configuration:
|
|
183
202
|
|
|
184
|
-
- `PORT`: The port to run the server on (default:
|
|
185
|
-
- `HOST`: The host to bind to (default: localhost)
|
|
203
|
+
- `PORT`: The port to run the server on (default: `3101`)
|
|
204
|
+
- `HOST`: The host to bind to (default: `localhost`)
|
|
205
|
+
|
|
206
|
+
You can set these in a `.env` file at the project root (create it if it doesn't exist):
|
|
207
|
+
|
|
208
|
+
```env
|
|
209
|
+
# .env
|
|
210
|
+
PORT=3102
|
|
211
|
+
HOST=localhost
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For the React client to connect to the correct port during a production build, also set:
|
|
215
|
+
|
|
216
|
+
```env
|
|
217
|
+
# src/client/.env
|
|
218
|
+
REACT_APP_API_URL=http://localhost:3102
|
|
219
|
+
```
|
|
186
220
|
|
|
187
221
|
## Load Balancing with PM2
|
|
188
222
|
|
|
@@ -38,9 +38,7 @@
|
|
|
38
38
|
"workingDirectory": "project"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
|
-
"environment": {
|
|
42
|
-
"NODE_ENV": "production"
|
|
43
|
-
}
|
|
41
|
+
"environment": {}
|
|
44
42
|
},
|
|
45
43
|
"validation": {
|
|
46
44
|
"checks": [
|
|
@@ -50,7 +48,8 @@
|
|
|
50
48
|
},
|
|
51
49
|
{
|
|
52
50
|
"name": "node_modules exists after install",
|
|
53
|
-
"directory": "node_modules"
|
|
51
|
+
"directory": "node_modules",
|
|
52
|
+
"optional": true
|
|
54
53
|
}
|
|
55
54
|
]
|
|
56
55
|
},
|
package/dist/server/index.js
CHANGED
|
@@ -26,6 +26,9 @@ const pageAuth_1 = __importDefault(require("./routes/pageAuth"));
|
|
|
26
26
|
const logStreaming_1 = require("./routes/logStreaming");
|
|
27
27
|
const pm2_connection_1 = require("./utils/pm2-connection");
|
|
28
28
|
const remote_connection_1 = require("./utils/remote-connection");
|
|
29
|
+
const metrics_history_1 = require("./utils/metrics-history");
|
|
30
|
+
const remoteMetrics_1 = __importDefault(require("./routes/remoteMetrics"));
|
|
31
|
+
const remote_metrics_poller_1 = require("./utils/remote-metrics-poller");
|
|
29
32
|
/**
|
|
30
33
|
* Create and configure the express server
|
|
31
34
|
*/
|
|
@@ -38,15 +41,15 @@ function createServer() {
|
|
|
38
41
|
origin: '*',
|
|
39
42
|
methods: ['GET', 'POST']
|
|
40
43
|
},
|
|
41
|
-
// Increase ping timeout to prevent false positive disconnections
|
|
42
|
-
pingTimeout:
|
|
44
|
+
// Increase ping timeout to prevent false positive disconnections in VPN/remote scenarios
|
|
45
|
+
pingTimeout: 20000, // How long to wait for a pong response (increased from 10s to 20s)
|
|
43
46
|
pingInterval: 25000, // How often to send ping packets (default: 25000ms)
|
|
44
47
|
// Allow reconnection attempts
|
|
45
48
|
allowEIO3: true,
|
|
46
49
|
// Transport configuration
|
|
47
50
|
transports: ['websocket', 'polling'],
|
|
48
51
|
// Upgrade timeout
|
|
49
|
-
upgradeTimeout:
|
|
52
|
+
upgradeTimeout: 20000 // Increased from 10s to 20s for slow VPN connections
|
|
50
53
|
});
|
|
51
54
|
// Configure middleware
|
|
52
55
|
app.use(express_1.default.json());
|
|
@@ -68,6 +71,9 @@ function createServer() {
|
|
|
68
71
|
app.use('/api/cron-jobs', cronJobs_1.default);
|
|
69
72
|
app.use('/api/update', updates_1.default);
|
|
70
73
|
app.use('/api/auth', pageAuth_1.default);
|
|
74
|
+
app.use('/api/remote-metrics', remoteMetrics_1.default);
|
|
75
|
+
// Start remote metrics poller (30-second interval, polls all connected servers)
|
|
76
|
+
remote_metrics_poller_1.remoteMetricsPoller.start();
|
|
71
77
|
// Setup log streaming with Socket.IO
|
|
72
78
|
(0, logStreaming_1.setupLogStreaming)(io); // PM2 API endpoints
|
|
73
79
|
app.get('/api/processes', async (req, res) => {
|
|
@@ -137,6 +143,24 @@ function createServer() {
|
|
|
137
143
|
};
|
|
138
144
|
res.json(metrics);
|
|
139
145
|
});
|
|
146
|
+
// @group MetricsHistory : Return full history for all processes
|
|
147
|
+
app.get('/api/metrics/history', (_req, res) => {
|
|
148
|
+
res.json(metrics_history_1.metricsHistory.getAll());
|
|
149
|
+
});
|
|
150
|
+
// @group MetricsHistory : Return history for a single process by pm_id
|
|
151
|
+
app.get('/api/metrics/history/:processId', (req, res) => {
|
|
152
|
+
const pm_id = parseInt(req.params.processId, 10);
|
|
153
|
+
if (isNaN(pm_id)) {
|
|
154
|
+
res.status(400).json({ error: 'Invalid processId' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const entry = metrics_history_1.metricsHistory.getOne(pm_id);
|
|
158
|
+
if (!entry) {
|
|
159
|
+
res.status(404).json({ error: 'No history found for this process' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
res.json(entry);
|
|
163
|
+
});
|
|
140
164
|
// @group LogHistory : Resolve log path from PM2 process descriptor
|
|
141
165
|
const resolveLocalLogPath = async (id, logType) => {
|
|
142
166
|
var _a, _b, _c;
|
|
@@ -355,6 +379,19 @@ function createServer() {
|
|
|
355
379
|
res.status(500).json({ error: 'Failed to download file' });
|
|
356
380
|
}
|
|
357
381
|
});
|
|
382
|
+
// @group MetricsHistory : Server-level polling — records PM2 process metrics into the history store
|
|
383
|
+
// This runs once regardless of how many clients are connected so the ring buffer fills consistently.
|
|
384
|
+
const historyPollInterval = setInterval(async () => {
|
|
385
|
+
try {
|
|
386
|
+
const processList = await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
387
|
+
pm2_1.default.list(callback);
|
|
388
|
+
});
|
|
389
|
+
metrics_history_1.metricsHistory.record(processList);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// silent — best-effort recording; don't crash the server
|
|
393
|
+
}
|
|
394
|
+
}, 3000);
|
|
358
395
|
// WebSocket for real-time updates
|
|
359
396
|
io.on('connection', (socket) => {
|
|
360
397
|
console.log('Client connected');
|
|
@@ -403,15 +440,17 @@ function createServer() {
|
|
|
403
440
|
res.status(404).send('File not found. Please check server configuration.');
|
|
404
441
|
}
|
|
405
442
|
});
|
|
443
|
+
// Clean up history poll on server close
|
|
444
|
+
server.on('close', () => clearInterval(historyPollInterval));
|
|
406
445
|
// Return the server instance
|
|
407
446
|
return server;
|
|
408
447
|
}
|
|
409
448
|
// Only start the server if this file is run directly
|
|
410
449
|
if (require.main === module) {
|
|
411
|
-
const PORT = process.env.PORT || 3101;
|
|
450
|
+
const PORT = parseInt(process.env.PORT || '3101', 10);
|
|
412
451
|
const HOST = process.env.HOST || 'localhost';
|
|
413
452
|
const server = createServer();
|
|
414
|
-
server.listen(PORT, () => {
|
|
453
|
+
server.listen(PORT, HOST, () => {
|
|
415
454
|
console.log(`Server running on http://${HOST}:${PORT}`);
|
|
416
455
|
});
|
|
417
456
|
// Handle shutdown gracefully
|
|
@@ -9,60 +9,69 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const ProjectSetupService_1 = require("../services/ProjectSetupService");
|
|
11
11
|
const router = (0, express_1.Router)();
|
|
12
|
-
// Deploy a new application
|
|
12
|
+
// Deploy a new application (SSE streaming)
|
|
13
13
|
router.post('/', async (req, res) => {
|
|
14
14
|
const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
|
|
15
|
-
// Validate required fields
|
|
15
|
+
// Validate required fields before starting SSE
|
|
16
16
|
if (!name || !script) {
|
|
17
17
|
return res.status(400).json({ error: 'Name and script path are required' });
|
|
18
18
|
}
|
|
19
|
-
// Validate script path exists
|
|
20
19
|
if (!fs_1.default.existsSync(script)) {
|
|
21
20
|
return res.status(400).json({ error: `Script file not found: ${script}` });
|
|
22
21
|
}
|
|
22
|
+
// Switch to SSE
|
|
23
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
24
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
25
|
+
res.setHeader('Connection', 'keep-alive');
|
|
26
|
+
res.flushHeaders();
|
|
27
|
+
const send = (type, data) => {
|
|
28
|
+
if (!res.writableEnded) {
|
|
29
|
+
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const log = (message) => send('log', { message });
|
|
33
|
+
const done = (success, extra = {}) => {
|
|
34
|
+
send('done', { success, ...extra });
|
|
35
|
+
res.end();
|
|
36
|
+
};
|
|
23
37
|
try {
|
|
24
38
|
const projectPath = cwd || path_1.default.dirname(script);
|
|
25
|
-
let setupResult = null;
|
|
26
39
|
let finalEnv = env || {};
|
|
27
40
|
let interpreterPath = '';
|
|
28
|
-
|
|
41
|
+
let setupResult = null;
|
|
42
|
+
// Auto-detect project type
|
|
29
43
|
let detectedType = appType;
|
|
30
44
|
if (!detectedType) {
|
|
31
45
|
detectedType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
|
|
32
46
|
if (detectedType) {
|
|
33
|
-
|
|
47
|
+
log(`Auto-detected project type: ${detectedType}`);
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
|
-
// Run project setup
|
|
50
|
+
// Run project setup
|
|
37
51
|
if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
|
|
38
|
-
|
|
52
|
+
log(`Running ${detectedType} project setup...`);
|
|
39
53
|
try {
|
|
40
|
-
setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType);
|
|
54
|
+
setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType, log);
|
|
41
55
|
if (!setupResult.success) {
|
|
42
|
-
return
|
|
56
|
+
return done(false, {
|
|
43
57
|
error: 'Project setup failed',
|
|
44
58
|
details: setupResult.errors,
|
|
45
|
-
warnings: setupResult.warnings
|
|
46
|
-
steps: setupResult.steps
|
|
59
|
+
warnings: setupResult.warnings
|
|
47
60
|
});
|
|
48
61
|
}
|
|
49
|
-
// Merge environment variables from setup
|
|
50
62
|
finalEnv = { ...setupResult.environment, ...finalEnv };
|
|
51
|
-
// Set interpreter path for Python projects
|
|
52
63
|
if (setupResult.interpreterPath) {
|
|
53
64
|
interpreterPath = setupResult.interpreterPath;
|
|
54
65
|
}
|
|
55
|
-
console.log('Project setup completed successfully');
|
|
56
66
|
}
|
|
57
67
|
catch (setupError) {
|
|
58
|
-
|
|
59
|
-
return res.status(500).json({
|
|
68
|
+
return done(false, {
|
|
60
69
|
error: 'Project setup failed',
|
|
61
70
|
details: setupError instanceof Error ? setupError.message : 'Unknown setup error'
|
|
62
71
|
});
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
|
-
//
|
|
74
|
+
// Build PM2 config
|
|
66
75
|
const appConfig = {
|
|
67
76
|
name,
|
|
68
77
|
script,
|
|
@@ -75,45 +84,38 @@ router.post('/', async (req, res) => {
|
|
|
75
84
|
max_memory_restart: max_memory_restart || '150M',
|
|
76
85
|
env: finalEnv
|
|
77
86
|
};
|
|
78
|
-
// Set interpreter for Python projects
|
|
79
87
|
if (detectedType === 'python' && interpreterPath) {
|
|
80
88
|
appConfig.interpreter = interpreterPath;
|
|
81
89
|
}
|
|
82
90
|
else if (detectedType === 'dotnet') {
|
|
83
91
|
appConfig.interpreter = 'dotnet';
|
|
84
|
-
// For .NET projects, update script to point to the published DLL if available
|
|
85
92
|
const publishedDll = path_1.default.join(projectPath, 'publish', `${path_1.default.basename(projectPath)}.dll`);
|
|
86
93
|
if (fs_1.default.existsSync(publishedDll)) {
|
|
87
94
|
appConfig.script = publishedDll;
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
error: `Failed to deploy application: ${err.message || 'Unknown error'}`
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
res.json({
|
|
104
|
-
success: true,
|
|
105
|
-
message: `Application ${name} deployed successfully`,
|
|
106
|
-
setupResult: setupResult ? {
|
|
107
|
-
steps: setupResult.steps,
|
|
108
|
-
warnings: setupResult.warnings
|
|
109
|
-
} : null
|
|
97
|
+
log(`Starting PM2 process: ${name}`);
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
pm2_1.default.connect((err) => {
|
|
100
|
+
if (err)
|
|
101
|
+
return reject(err);
|
|
102
|
+
pm2_1.default.start(appConfig, (startErr) => {
|
|
103
|
+
pm2_1.default.disconnect();
|
|
104
|
+
if (startErr)
|
|
105
|
+
return reject(startErr);
|
|
106
|
+
resolve();
|
|
110
107
|
});
|
|
111
108
|
});
|
|
112
109
|
});
|
|
110
|
+
log(`Process "${name}" started successfully.`);
|
|
111
|
+
done(true, {
|
|
112
|
+
message: `Application ${name} deployed successfully`,
|
|
113
|
+
setupResult: setupResult ? { steps: setupResult.steps, warnings: setupResult.warnings } : null
|
|
114
|
+
});
|
|
113
115
|
}
|
|
114
116
|
catch (error) {
|
|
115
117
|
console.error('Deployment error:', error);
|
|
116
|
-
|
|
118
|
+
done(false, {
|
|
117
119
|
error: 'Deployment failed',
|
|
118
120
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
119
121
|
});
|
|
@@ -7,7 +7,6 @@ exports.setupLogStreaming = void 0;
|
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const pm2_1 = __importDefault(require("pm2"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
10
|
const remote_connection_1 = require("../utils/remote-connection");
|
|
12
11
|
const router = (0, express_1.Router)();
|
|
13
12
|
// This variable will hold references to active log streams
|
|
@@ -18,7 +17,7 @@ const getLogStream = (io, processId, logType) => {
|
|
|
18
17
|
const streamKey = `${processId}-${logType}`;
|
|
19
18
|
// If stream already exists, return it
|
|
20
19
|
if (activeStreams[streamKey]) {
|
|
21
|
-
return activeStreams[streamKey];
|
|
20
|
+
return Promise.resolve(activeStreams[streamKey]);
|
|
22
21
|
}
|
|
23
22
|
return new Promise((resolve, reject) => {
|
|
24
23
|
pm2_1.default.describe(processId, (err, processDesc) => {
|
|
@@ -32,30 +31,38 @@ const getLogStream = (io, processId, logType) => {
|
|
|
32
31
|
reject(new Error(`Log file not found: ${logPath}`));
|
|
33
32
|
return;
|
|
34
33
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
// Cross-platform tail using fs.watch — no Unix `tail` binary needed
|
|
35
|
+
let position = fs_1.default.statSync(logPath).size; // start at EOF, don't replay history
|
|
36
|
+
const watcher = fs_1.default.watch(logPath, (eventType) => {
|
|
37
|
+
if (eventType !== 'change')
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const stat = fs_1.default.statSync(logPath);
|
|
41
|
+
// Handle log rotation / truncation
|
|
42
|
+
if (stat.size < position)
|
|
43
|
+
position = 0;
|
|
44
|
+
if (stat.size === position)
|
|
45
|
+
return;
|
|
46
|
+
const length = stat.size - position;
|
|
47
|
+
const buffer = Buffer.alloc(length);
|
|
48
|
+
const fd = fs_1.default.openSync(logPath, 'r');
|
|
49
|
+
fs_1.default.readSync(fd, buffer, 0, length, position);
|
|
50
|
+
fs_1.default.closeSync(fd);
|
|
51
|
+
position = stat.size;
|
|
52
|
+
const lines = buffer.toString('utf8').split('\n').filter((l) => l.trim() !== '');
|
|
53
|
+
lines.forEach((line) => {
|
|
54
|
+
io.to(streamKey).emit('log-line', { processId, logType, line });
|
|
46
55
|
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
tail.on('close', (code) => {
|
|
53
|
-
console.log(`Tail process exited with code ${code}`);
|
|
54
|
-
delete activeStreams[streamKey];
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error('Error reading log file:', e);
|
|
59
|
+
}
|
|
55
60
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
watcher.on('error', (e) => console.error(`Watcher error for ${streamKey}:`, e));
|
|
62
|
+
// Expose a kill() so existing cleanup code works unchanged
|
|
63
|
+
const streamObj = { kill: () => { watcher.close(); delete activeStreams[streamKey]; } };
|
|
64
|
+
activeStreams[streamKey] = streamObj;
|
|
65
|
+
resolve(streamObj);
|
|
59
66
|
});
|
|
60
67
|
});
|
|
61
68
|
};
|
|
@@ -67,8 +74,19 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
67
74
|
return activeRemoteStreams[streamKey];
|
|
68
75
|
}
|
|
69
76
|
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
70
|
-
if (!connection
|
|
71
|
-
throw new Error('Connection not found
|
|
77
|
+
if (!connection) {
|
|
78
|
+
throw new Error('Connection not found');
|
|
79
|
+
}
|
|
80
|
+
// Ensure connection is active before starting log stream
|
|
81
|
+
if (!connection.isConnected()) {
|
|
82
|
+
console.log(`Connection ${connectionId} not active, attempting to connect...`);
|
|
83
|
+
try {
|
|
84
|
+
await connection.connect();
|
|
85
|
+
console.log(`Successfully connected to ${connectionId} for log streaming`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`Failed to connect: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
89
|
+
}
|
|
72
90
|
}
|
|
73
91
|
// Get process info using the multi-path fallback so pm2 is found regardless of PATH
|
|
74
92
|
console.log(`Getting process info for: ${processId}`);
|
|
@@ -178,9 +196,24 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
178
196
|
processName,
|
|
179
197
|
error: error.message
|
|
180
198
|
});
|
|
199
|
+
// Clean up on error
|
|
200
|
+
if (activeRemoteStreams[streamKey]) {
|
|
201
|
+
delete activeRemoteStreams[streamKey];
|
|
202
|
+
}
|
|
181
203
|
});
|
|
182
204
|
logStream.on('close', (code) => {
|
|
183
205
|
console.log(`PM2 logs stream closed for ${processName} with code:`, code);
|
|
206
|
+
// Notify clients that the stream has closed
|
|
207
|
+
io.to(streamKey).emit('remote-log-closed', {
|
|
208
|
+
connectionId,
|
|
209
|
+
processId,
|
|
210
|
+
processName,
|
|
211
|
+
code
|
|
212
|
+
});
|
|
213
|
+
// Clean up the stream reference
|
|
214
|
+
if (activeRemoteStreams[streamKey]) {
|
|
215
|
+
delete activeRemoteStreams[streamKey];
|
|
216
|
+
}
|
|
184
217
|
});
|
|
185
218
|
streams.combined = logStream;
|
|
186
219
|
}
|
|
@@ -267,10 +300,15 @@ const setupLogStreaming = (io) => {
|
|
|
267
300
|
const streams = activeRemoteStreams[streamKey];
|
|
268
301
|
if (streams) {
|
|
269
302
|
console.log(`Stopping remote log streams: ${streamKey}`);
|
|
270
|
-
|
|
303
|
+
// Kill the log stream properly
|
|
304
|
+
if (streams.combined && typeof streams.combined.kill === 'function') {
|
|
305
|
+
streams.combined.kill();
|
|
306
|
+
}
|
|
307
|
+
// Also handle legacy stdout/stderr streams if they exist
|
|
308
|
+
if (streams.stdout && typeof streams.stdout.kill === 'function') {
|
|
271
309
|
streams.stdout.kill();
|
|
272
310
|
}
|
|
273
|
-
if (streams.stderr && streams.stderr.kill) {
|
|
311
|
+
if (streams.stderr && typeof streams.stderr.kill === 'function') {
|
|
274
312
|
streams.stderr.kill();
|
|
275
313
|
}
|
|
276
314
|
delete activeRemoteStreams[streamKey];
|
|
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const express_1 = require("express");
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
8
11
|
const pm2_1 = __importDefault(require("pm2"));
|
|
9
12
|
const pm2_connection_1 = require("../utils/pm2-connection");
|
|
10
13
|
const router = (0, express_1.Router)();
|
|
@@ -123,4 +126,56 @@ router.delete('/:moduleName', async (req, res) => {
|
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
});
|
|
129
|
+
// @group Utilities : Resolve ~/.pm2/module_conf.json path
|
|
130
|
+
const moduleConfPath = () => path_1.default.join(os_1.default.homedir(), '.pm2', 'module_conf.json');
|
|
131
|
+
// @group APIEndpoints : Get config for a specific installed module
|
|
132
|
+
router.get('/:moduleName/config', (req, res) => {
|
|
133
|
+
var _a;
|
|
134
|
+
const { moduleName } = req.params;
|
|
135
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
136
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
137
|
+
}
|
|
138
|
+
const confFile = moduleConfPath();
|
|
139
|
+
if (!fs_1.default.existsSync(confFile)) {
|
|
140
|
+
return res.json({ config: {} });
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const raw = JSON.parse(fs_1.default.readFileSync(confFile, 'utf8'));
|
|
144
|
+
// PM2 stores module config under the module name key
|
|
145
|
+
const config = (_a = raw[moduleName]) !== null && _a !== void 0 ? _a : {};
|
|
146
|
+
res.json({ config });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
res.status(500).json({ error: 'Failed to read module configuration' });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// @group APIEndpoints : Set one or more config keys for an installed module
|
|
153
|
+
router.put('/:moduleName/config', async (req, res) => {
|
|
154
|
+
const { moduleName } = req.params;
|
|
155
|
+
const { config } = req.body;
|
|
156
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
157
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
158
|
+
}
|
|
159
|
+
if (!config || typeof config !== 'object') {
|
|
160
|
+
return res.status(400).json({ error: 'config object is required' });
|
|
161
|
+
}
|
|
162
|
+
// Validate all keys — no shell metacharacters
|
|
163
|
+
for (const key of Object.keys(config)) {
|
|
164
|
+
if (!/^[a-zA-Z0-9_\-.]+$/.test(key)) {
|
|
165
|
+
return res.status(400).json({ error: `Invalid config key: ${key}` });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
// Run pm2 set for each key sequentially
|
|
170
|
+
for (const [key, value] of Object.entries(config)) {
|
|
171
|
+
// Wrap value in quotes to handle spaces; strip any embedded quotes first
|
|
172
|
+
const safeValue = String(value).replace(/"/g, '');
|
|
173
|
+
await runPM2CLI(`set ${moduleName}:${key} "${safeValue}"`);
|
|
174
|
+
}
|
|
175
|
+
res.json({ success: true, message: `Configuration updated for ${moduleName}` });
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
res.status(500).json({ error: 'Failed to update configuration', details: error.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
126
181
|
exports.default = router;
|
|
@@ -96,14 +96,14 @@ router.post('/verify', (req, res) => {
|
|
|
96
96
|
const config = loadAuthConfig();
|
|
97
97
|
if (!config) {
|
|
98
98
|
// No password set — treat as unlocked
|
|
99
|
-
return res.json({ success: true });
|
|
99
|
+
return res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
100
100
|
}
|
|
101
101
|
const hash = hashPassword(password, config.salt);
|
|
102
102
|
const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
|
|
103
103
|
if (!match) {
|
|
104
104
|
return res.status(401).json({ success: false, error: 'Incorrect password' });
|
|
105
105
|
}
|
|
106
|
-
res.json({ success: true });
|
|
106
|
+
res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
107
107
|
});
|
|
108
108
|
// @group Endpoints : DELETE /api/auth/remove — remove the password (requires current password)
|
|
109
109
|
router.delete('/remove', (req, res) => {
|
|
@@ -153,7 +153,7 @@ router.post('/pin/verify', (req, res) => {
|
|
|
153
153
|
if (!match) {
|
|
154
154
|
return res.status(401).json({ success: false, error: 'Incorrect PIN' });
|
|
155
155
|
}
|
|
156
|
-
res.json({ success: true });
|
|
156
|
+
res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
157
157
|
});
|
|
158
158
|
// @group Endpoints : DELETE /api/auth/pin/remove — remove PIN (requires current password)
|
|
159
159
|
router.delete('/pin/remove', (req, res) => {
|