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
package/dist/server/index.js
CHANGED
|
@@ -4,10 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createServer = createServer;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
// @group Configuration : Load .env.local first (local overrides), then .env (defaults)
|
|
9
|
+
// Must happen before any code reads process.env
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const dotenv = require('dotenv');
|
|
12
|
+
dotenv.config({ path: path_1.default.resolve(process.cwd(), '.env.local') });
|
|
13
|
+
dotenv.config({ path: path_1.default.resolve(process.cwd(), '.env') });
|
|
7
14
|
const express_1 = __importDefault(require("express"));
|
|
8
15
|
const http_1 = __importDefault(require("http"));
|
|
9
16
|
const socket_io_1 = require("socket.io");
|
|
10
|
-
const path_1 = __importDefault(require("path"));
|
|
11
17
|
const pm2_1 = __importDefault(require("pm2"));
|
|
12
18
|
const os_1 = __importDefault(require("os"));
|
|
13
19
|
const processConfig_1 = __importDefault(require("./routes/processConfig"));
|
|
@@ -15,6 +21,7 @@ const deployApplication_1 = __importDefault(require("./routes/deployApplication"
|
|
|
15
21
|
const modules_1 = __importDefault(require("./routes/modules"));
|
|
16
22
|
const remoteConnections_1 = __importDefault(require("./routes/remoteConnections"));
|
|
17
23
|
const cronJobs_1 = __importDefault(require("./routes/cronJobs"));
|
|
24
|
+
const updates_1 = __importDefault(require("./routes/updates"));
|
|
18
25
|
const logStreaming_1 = require("./routes/logStreaming");
|
|
19
26
|
const pm2_connection_1 = require("./utils/pm2-connection");
|
|
20
27
|
const remote_connection_1 = require("./utils/remote-connection");
|
|
@@ -29,7 +36,16 @@ function createServer() {
|
|
|
29
36
|
cors: {
|
|
30
37
|
origin: '*',
|
|
31
38
|
methods: ['GET', 'POST']
|
|
32
|
-
}
|
|
39
|
+
},
|
|
40
|
+
// Increase ping timeout to prevent false positive disconnections
|
|
41
|
+
pingTimeout: 10000, // How long to wait for a pong response (default: 5000ms)
|
|
42
|
+
pingInterval: 25000, // How often to send ping packets (default: 25000ms)
|
|
43
|
+
// Allow reconnection attempts
|
|
44
|
+
allowEIO3: true,
|
|
45
|
+
// Transport configuration
|
|
46
|
+
transports: ['websocket', 'polling'],
|
|
47
|
+
// Upgrade timeout
|
|
48
|
+
upgradeTimeout: 10000
|
|
33
49
|
});
|
|
34
50
|
// Configure middleware
|
|
35
51
|
app.use(express_1.default.json());
|
|
@@ -49,6 +65,7 @@ function createServer() {
|
|
|
49
65
|
app.use('/api/modules', modules_1.default);
|
|
50
66
|
app.use('/api/remote', remoteConnections_1.default);
|
|
51
67
|
app.use('/api/cron-jobs', cronJobs_1.default);
|
|
68
|
+
app.use('/api/update', updates_1.default);
|
|
52
69
|
// Setup log streaming with Socket.IO
|
|
53
70
|
(0, logStreaming_1.setupLogStreaming)(io); // PM2 API endpoints
|
|
54
71
|
app.get('/api/processes', async (req, res) => {
|
|
@@ -73,42 +90,36 @@ function createServer() {
|
|
|
73
90
|
}
|
|
74
91
|
});
|
|
75
92
|
// Action endpoints (start, stop, restart, delete)
|
|
76
|
-
app.post('/api/process/:id/:action', (req, res) => {
|
|
93
|
+
app.post('/api/process/:id/:action', async (req, res) => {
|
|
77
94
|
const { id, action } = req.params;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
switch (
|
|
95
|
+
const validActions = ['start', 'stop', 'restart', 'delete'];
|
|
96
|
+
if (!validActions.includes(action)) {
|
|
97
|
+
res.status(400).json({ error: 'Invalid action' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
102
|
+
switch (action) {
|
|
86
103
|
case 'start':
|
|
87
|
-
pm2_1.default.start(id,
|
|
104
|
+
pm2_1.default.start(id, callback);
|
|
88
105
|
break;
|
|
89
106
|
case 'stop':
|
|
90
|
-
pm2_1.default.stop(id,
|
|
107
|
+
pm2_1.default.stop(id, callback);
|
|
91
108
|
break;
|
|
92
109
|
case 'restart':
|
|
93
|
-
pm2_1.default.restart(id,
|
|
110
|
+
pm2_1.default.restart(id, callback);
|
|
94
111
|
break;
|
|
95
112
|
case 'delete':
|
|
96
|
-
pm2_1.default.delete(id,
|
|
113
|
+
pm2_1.default.delete(id, callback);
|
|
97
114
|
break;
|
|
98
|
-
default:
|
|
99
|
-
cb(new Error('Invalid action'));
|
|
100
115
|
}
|
|
101
|
-
};
|
|
102
|
-
processAction(action, (err) => {
|
|
103
|
-
pm2_1.default.disconnect();
|
|
104
|
-
if (err) {
|
|
105
|
-
console.error(err);
|
|
106
|
-
res.status(500).json({ error: `Failed to ${action} process` });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
res.json({ success: true, message: `Process ${id} ${action} request received` });
|
|
110
116
|
});
|
|
111
|
-
|
|
117
|
+
res.json({ success: true, message: `Process ${id} ${action} request received` });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(err);
|
|
121
|
+
res.status(500).json({ error: `Failed to ${action} process` });
|
|
122
|
+
}
|
|
112
123
|
});
|
|
113
124
|
// Get system metrics
|
|
114
125
|
app.get('/api/metrics', (req, res) => {
|
|
@@ -124,76 +135,238 @@ function createServer() {
|
|
|
124
135
|
};
|
|
125
136
|
res.json(metrics);
|
|
126
137
|
});
|
|
127
|
-
//
|
|
128
|
-
|
|
138
|
+
// @group LogHistory : Resolve log path from PM2 process descriptor
|
|
139
|
+
const resolveLocalLogPath = async (id, logType) => {
|
|
140
|
+
var _a, _b, _c;
|
|
141
|
+
const processDesc = await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
142
|
+
pm2_1.default.describe(id, callback);
|
|
143
|
+
});
|
|
144
|
+
if (!processDesc || processDesc.length === 0)
|
|
145
|
+
return null;
|
|
146
|
+
return (_c = (_b = (_a = processDesc[0]) === null || _a === void 0 ? void 0 : _a.pm2_env) === null || _b === void 0 ? void 0 : _b[`pm_${logType}_log_path`]) !== null && _c !== void 0 ? _c : null;
|
|
147
|
+
};
|
|
148
|
+
// @group LogHistory : Get log lines — ?lines=N (default 200, 0 = all)
|
|
149
|
+
app.get('/api/logs/:id/:type', async (req, res) => {
|
|
129
150
|
const { id, type } = req.params;
|
|
130
151
|
const logType = type === 'err' ? 'err' : 'out';
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
const lines = parseInt(req.query.lines || '200', 10);
|
|
153
|
+
try {
|
|
154
|
+
const logPath = await resolveLocalLogPath(id, logType);
|
|
155
|
+
if (!logPath) {
|
|
156
|
+
res.status(404).json({ error: 'Process not found or log path unavailable' });
|
|
135
157
|
return;
|
|
136
158
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
const fs = require('fs');
|
|
160
|
+
if (!fs.existsSync(logPath)) {
|
|
161
|
+
res.json({ logs: [], logPath });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const { lines: result, total } = await streamTailLines(fs.createReadStream(logPath), lines);
|
|
165
|
+
res.json({ logs: result, logPath, totalLines: total });
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.error(`Error reading log file: ${err}`);
|
|
169
|
+
res.status(500).json({ error: 'Failed to read log file' });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// @group LogHistory : Download full log file
|
|
173
|
+
app.get('/api/logs/:id/:type/download', async (req, res) => {
|
|
174
|
+
const { id, type } = req.params;
|
|
175
|
+
const logType = type === 'err' ? 'err' : 'out';
|
|
176
|
+
try {
|
|
177
|
+
const logPath = await resolveLocalLogPath(id, logType);
|
|
178
|
+
if (!logPath) {
|
|
179
|
+
res.status(404).json({ error: 'Process not found or log path unavailable' });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const fs = require('fs');
|
|
183
|
+
if (!fs.existsSync(logPath)) {
|
|
184
|
+
res.status(404).json({ error: 'Log file does not exist yet' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const fileName = `${id}-${logType}.log`;
|
|
188
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
189
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
190
|
+
fs.createReadStream(logPath).pipe(res);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(`Error downloading log file: ${err}`);
|
|
194
|
+
res.status(500).json({ error: 'Failed to download log file' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// @group LogHistory : List all log files (current + rotated) for a process
|
|
198
|
+
// Uses /api/log-files/:id to avoid Express matching /:id/:type with type='files'
|
|
199
|
+
app.get('/api/log-files/:id', async (req, res) => {
|
|
200
|
+
const { id } = req.params;
|
|
201
|
+
try {
|
|
202
|
+
const fs = require('fs');
|
|
203
|
+
const nodePath = require('path');
|
|
204
|
+
const [outPath, errPath] = await Promise.all([
|
|
205
|
+
resolveLocalLogPath(id, 'out'),
|
|
206
|
+
resolveLocalLogPath(id, 'err'),
|
|
207
|
+
]);
|
|
208
|
+
if (!outPath && !errPath) {
|
|
209
|
+
res.status(404).json({ error: 'Process not found or no log paths available' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Derive the process base name from the log path (strip -out.log suffix)
|
|
213
|
+
const baseName = outPath
|
|
214
|
+
? nodePath.basename(outPath).replace(/-out\.log.*$/, '')
|
|
215
|
+
: nodePath.basename(errPath).replace(/-(error|err)\.log.*$/, '');
|
|
216
|
+
const logDirs = new Set();
|
|
217
|
+
if (outPath)
|
|
218
|
+
logDirs.add(nodePath.dirname(outPath));
|
|
219
|
+
if (errPath)
|
|
220
|
+
logDirs.add(nodePath.dirname(errPath));
|
|
221
|
+
const files = [];
|
|
222
|
+
for (const dir of logDirs) {
|
|
223
|
+
if (!fs.existsSync(dir))
|
|
224
|
+
continue;
|
|
225
|
+
for (const fileName of fs.readdirSync(dir)) {
|
|
226
|
+
if (!fileName.startsWith(baseName))
|
|
227
|
+
continue;
|
|
228
|
+
const filePath = nodePath.join(dir, fileName);
|
|
229
|
+
const stat = fs.statSync(filePath);
|
|
230
|
+
if (!stat.isFile())
|
|
231
|
+
continue;
|
|
232
|
+
let type = 'unknown';
|
|
233
|
+
if (fileName.includes('-out'))
|
|
234
|
+
type = 'out';
|
|
235
|
+
else if (fileName.includes('-error') || fileName.includes('-err'))
|
|
236
|
+
type = 'err';
|
|
237
|
+
files.push({
|
|
238
|
+
name: fileName,
|
|
239
|
+
path: filePath,
|
|
240
|
+
size: stat.size,
|
|
241
|
+
modified: stat.mtime.toISOString(),
|
|
242
|
+
type,
|
|
243
|
+
compressed: fileName.endsWith('.gz'),
|
|
244
|
+
});
|
|
143
245
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
246
|
+
}
|
|
247
|
+
files.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
248
|
+
res.json({ files });
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error('Error listing log files:', err);
|
|
252
|
+
res.status(500).json({ error: 'Failed to list log files' });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// @group LogHistory : Security helper — block path traversal; allow any absolute log file path
|
|
256
|
+
const isAllowedLogPath = (filePath) => {
|
|
257
|
+
const nodePath = require('path');
|
|
258
|
+
const norm = nodePath.normalize(filePath);
|
|
259
|
+
// Require absolute path; reject shell-dangerous characters; require log extension
|
|
260
|
+
const SHELL_UNSAFE = /['"`;$|&<>(){}\\\n\r\0]/;
|
|
261
|
+
return (nodePath.isAbsolute(norm) &&
|
|
262
|
+
!norm.includes('..') &&
|
|
263
|
+
!SHELL_UNSAFE.test(norm) &&
|
|
264
|
+
/\.(log|gz)$/i.test(norm));
|
|
265
|
+
};
|
|
266
|
+
// @group LogHistory : Stream last N lines from a readable stream (ring buffer, no full-file load)
|
|
267
|
+
const streamTailLines = (inputStream, maxLines) => {
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const rl = require('readline').createInterface({ input: inputStream, crlfDelay: Infinity });
|
|
270
|
+
const buffer = [];
|
|
271
|
+
let total = 0;
|
|
272
|
+
rl.on('line', (line) => {
|
|
273
|
+
if (line.trim() === '')
|
|
148
274
|
return;
|
|
275
|
+
total++;
|
|
276
|
+
if (maxLines > 0) {
|
|
277
|
+
buffer.push(line);
|
|
278
|
+
if (buffer.length > maxLines)
|
|
279
|
+
buffer.shift();
|
|
149
280
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const fs = require('fs');
|
|
153
|
-
let logContent = '';
|
|
154
|
-
if (fs.existsSync(logPath)) {
|
|
155
|
-
// Read the last part of the file (up to 10KB to avoid huge responses)
|
|
156
|
-
const stats = fs.statSync(logPath);
|
|
157
|
-
const fileSize = stats.size;
|
|
158
|
-
const readSize = Math.min(fileSize, 10 * 1024); // 10KB max
|
|
159
|
-
const position = Math.max(0, fileSize - readSize);
|
|
160
|
-
const buffer = Buffer.alloc(readSize);
|
|
161
|
-
const fd = fs.openSync(logPath, 'r');
|
|
162
|
-
fs.readSync(fd, buffer, 0, readSize, position);
|
|
163
|
-
fs.closeSync(fd);
|
|
164
|
-
logContent = buffer.toString('utf8');
|
|
165
|
-
}
|
|
166
|
-
const logs = logContent.split('\n').filter((line) => line.trim() !== '');
|
|
167
|
-
pm2_1.default.disconnect();
|
|
168
|
-
res.json({ logs });
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
console.error(`Error reading log file: ${error}`);
|
|
172
|
-
pm2_1.default.disconnect();
|
|
173
|
-
res.status(500).json({ error: 'Failed to read log file' });
|
|
281
|
+
else {
|
|
282
|
+
buffer.push(line);
|
|
174
283
|
}
|
|
175
284
|
});
|
|
285
|
+
rl.on('close', () => resolve({ lines: buffer, total }));
|
|
286
|
+
rl.on('error', reject);
|
|
287
|
+
inputStream.on('error', reject);
|
|
176
288
|
});
|
|
289
|
+
};
|
|
290
|
+
// @group LogHistory : Read a specific log file by path — ?lines=N, supports .gz
|
|
291
|
+
// Uses /api/log-file (singular, top-level) to avoid clashing with /api/logs/:id/:type
|
|
292
|
+
app.get('/api/log-file', async (req, res) => {
|
|
293
|
+
const filePath = req.query.path;
|
|
294
|
+
const lines = parseInt(req.query.lines || '200', 10);
|
|
295
|
+
if (!filePath) {
|
|
296
|
+
res.status(400).json({ error: 'path query parameter required' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!isAllowedLogPath(filePath)) {
|
|
300
|
+
res.status(403).json({ error: 'Access denied: path is outside PM2 log directories' });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const fs = require('fs');
|
|
305
|
+
const zlib = require('zlib');
|
|
306
|
+
if (!fs.existsSync(filePath)) {
|
|
307
|
+
res.status(404).json({ error: 'File not found' });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const inputStream = filePath.endsWith('.gz')
|
|
311
|
+
? fs.createReadStream(filePath).pipe(zlib.createGunzip())
|
|
312
|
+
: fs.createReadStream(filePath);
|
|
313
|
+
const { lines: result, total } = await streamTailLines(inputStream, lines);
|
|
314
|
+
res.json({ logs: result, totalLines: total });
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
console.error('Error reading log file:', err);
|
|
318
|
+
res.status(500).json({ error: 'Failed to read log file' });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// @group LogHistory : Download a specific log file by path (streams .gz as-is)
|
|
322
|
+
app.get('/api/log-file/download', async (req, res) => {
|
|
323
|
+
const filePath = req.query.path;
|
|
324
|
+
if (!filePath) {
|
|
325
|
+
res.status(400).json({ error: 'path query parameter required' });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!isAllowedLogPath(filePath)) {
|
|
329
|
+
res.status(403).json({ error: 'Access denied' });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const fs = require('fs');
|
|
334
|
+
const zlib = require('zlib');
|
|
335
|
+
const nodePath = require('path');
|
|
336
|
+
if (!fs.existsSync(filePath)) {
|
|
337
|
+
res.status(404).json({ error: 'File not found' });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Decompress .gz server-side so the download is always plain text
|
|
341
|
+
const baseName = nodePath.basename(filePath).replace(/\.gz$/i, '');
|
|
342
|
+
res.setHeader('Content-Disposition', `attachment; filename="${baseName}"`);
|
|
343
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
344
|
+
if (filePath.endsWith('.gz')) {
|
|
345
|
+
fs.createReadStream(filePath).pipe(zlib.createGunzip()).pipe(res);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
fs.createReadStream(filePath).pipe(res);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
console.error('Error downloading log file:', err);
|
|
353
|
+
res.status(500).json({ error: 'Failed to download file' });
|
|
354
|
+
}
|
|
177
355
|
});
|
|
178
356
|
// WebSocket for real-time updates
|
|
179
357
|
io.on('connection', (socket) => {
|
|
180
358
|
console.log('Client connected');
|
|
181
|
-
// Send process updates every 3 seconds
|
|
182
|
-
const processInterval = setInterval(() => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
pm2_1.default.list((err, processList) => {
|
|
189
|
-
pm2_1.default.disconnect();
|
|
190
|
-
if (err) {
|
|
191
|
-
console.error(err);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
socket.emit('processes', processList);
|
|
359
|
+
// Send process updates every 3 seconds using shared pooled connection
|
|
360
|
+
const processInterval = setInterval(async () => {
|
|
361
|
+
try {
|
|
362
|
+
const processList = await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
363
|
+
pm2_1.default.list(callback);
|
|
195
364
|
});
|
|
196
|
-
|
|
365
|
+
socket.emit('processes', processList);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
console.error('Failed to list PM2 processes:', err);
|
|
369
|
+
}
|
|
197
370
|
}, 3000);
|
|
198
371
|
// Send system metrics every 2 seconds
|
|
199
372
|
const metricsInterval = setInterval(() => {
|
|
@@ -233,7 +406,7 @@ function createServer() {
|
|
|
233
406
|
}
|
|
234
407
|
// Only start the server if this file is run directly
|
|
235
408
|
if (require.main === module) {
|
|
236
|
-
const PORT = process.env.PORT ||
|
|
409
|
+
const PORT = process.env.PORT || 3101;
|
|
237
410
|
const HOST = process.env.HOST || 'localhost';
|
|
238
411
|
const server = createServer();
|
|
239
412
|
server.listen(PORT, () => {
|
|
@@ -11,7 +11,7 @@ const ProjectSetupService_1 = require("../services/ProjectSetupService");
|
|
|
11
11
|
const router = (0, express_1.Router)();
|
|
12
12
|
// Deploy a new application
|
|
13
13
|
router.post('/', async (req, res) => {
|
|
14
|
-
const { name, script, cwd, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
|
|
14
|
+
const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
|
|
15
15
|
// Validate required fields
|
|
16
16
|
if (!name || !script) {
|
|
17
17
|
return res.status(400).json({ error: 'Name and script path are required' });
|
|
@@ -67,6 +67,7 @@ router.post('/', async (req, res) => {
|
|
|
67
67
|
name,
|
|
68
68
|
script,
|
|
69
69
|
cwd: projectPath,
|
|
70
|
+
namespace: namespace || 'default',
|
|
70
71
|
instances: parseInt(instances) || 1,
|
|
71
72
|
exec_mode: exec_mode || 'fork',
|
|
72
73
|
autorestart: autorestart !== undefined ? autorestart : true,
|
|
@@ -158,8 +159,8 @@ router.post('/generate-ecosystem', (req, res) => {
|
|
|
158
159
|
env: pm2Env.env || {}
|
|
159
160
|
};
|
|
160
161
|
});
|
|
161
|
-
const ecosystemConfig = `module.exports = {
|
|
162
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
162
|
+
const ecosystemConfig = `module.exports = {
|
|
163
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
163
164
|
};`;
|
|
164
165
|
// Create the file (either at specified path or default location)
|
|
165
166
|
const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
|
|
@@ -212,8 +213,8 @@ router.get('/generate-ecosystem-preview', (req, res) => {
|
|
|
212
213
|
env: pm2Env.env || {}
|
|
213
214
|
};
|
|
214
215
|
});
|
|
215
|
-
const ecosystemConfig = `module.exports = {
|
|
216
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
216
|
+
const ecosystemConfig = `module.exports = {
|
|
217
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
217
218
|
};`;
|
|
218
219
|
pm2_1.default.disconnect();
|
|
219
220
|
res.json({
|
|
@@ -38,8 +38,8 @@ const getLogStream = (io, processId, logType) => {
|
|
|
38
38
|
tail.stdout.on('data', (data) => {
|
|
39
39
|
const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
|
|
40
40
|
lines.forEach((line) => {
|
|
41
|
-
// Emit
|
|
42
|
-
io.emit('log-line', {
|
|
41
|
+
// Emit only to clients subscribed to this specific log stream
|
|
42
|
+
io.to(streamKey).emit('log-line', {
|
|
43
43
|
processId,
|
|
44
44
|
logType,
|
|
45
45
|
line
|
|
@@ -70,17 +70,23 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
70
70
|
if (!connection || !connection.isConnected()) {
|
|
71
71
|
throw new Error('Connection not found or not connected');
|
|
72
72
|
}
|
|
73
|
-
// Get process info
|
|
73
|
+
// Get process info using the multi-path fallback so pm2 is found regardless of PATH
|
|
74
74
|
console.log(`Getting process info for: ${processId}`);
|
|
75
|
-
const processInfoResult = await connection.
|
|
75
|
+
const processInfoResult = await connection.executePM2Command('jlist');
|
|
76
76
|
if (processInfoResult.code !== 0) {
|
|
77
77
|
throw new Error(`Failed to get process list: ${processInfoResult.stderr}`);
|
|
78
78
|
}
|
|
79
|
+
// Extract JSON from output (pm2 jlist may prefix with non-JSON lines)
|
|
80
|
+
let rawOutput = processInfoResult.stdout.trim();
|
|
81
|
+
const startIdx = rawOutput.indexOf('[');
|
|
82
|
+
const endIdx = rawOutput.lastIndexOf(']') + 1;
|
|
83
|
+
if (startIdx !== -1 && endIdx > 0) {
|
|
84
|
+
rawOutput = rawOutput.substring(startIdx, endIdx);
|
|
85
|
+
}
|
|
79
86
|
let processInfo;
|
|
80
87
|
try {
|
|
81
|
-
const processList = JSON.parse(
|
|
88
|
+
const processList = JSON.parse(rawOutput);
|
|
82
89
|
console.log(`Found ${processList.length} processes`);
|
|
83
|
-
// Find the process by ID
|
|
84
90
|
processInfo = processList.find((proc) => proc.pm_id === parseInt(processId));
|
|
85
91
|
if (!processInfo) {
|
|
86
92
|
throw new Error(`Process with ID ${processId} not found`);
|
|
@@ -96,10 +102,10 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
96
102
|
const streams = {};
|
|
97
103
|
try {
|
|
98
104
|
console.log(`Setting up pm2 logs stream for process: ${processName} (ID: ${processId})`);
|
|
99
|
-
// Use pm2
|
|
100
|
-
//
|
|
101
|
-
const pm2LogsCommand = `
|
|
102
|
-
console.log(`About to create pm2 log stream with command: ${pm2LogsCommand}
|
|
105
|
+
// Use the resolved pm2 invocation (cached from the jlist call above) so the
|
|
106
|
+
// same shell/path that successfully ran pm2 jlist is reused here.
|
|
107
|
+
const pm2LogsCommand = connection.buildPM2StreamCommand(`logs ${processId} --lines 0 --raw`);
|
|
108
|
+
console.log(`About to create pm2 log stream with command: ${pm2LogsCommand}`);
|
|
103
109
|
const logStream = await connection.createLogStream(pm2LogsCommand, true);
|
|
104
110
|
console.log(`Successfully created pm2 logs stream for ${processName}`);
|
|
105
111
|
logStream.on('data', (data) => {
|
|
@@ -154,7 +160,8 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
154
160
|
cleanLine = logMatch[1];
|
|
155
161
|
}
|
|
156
162
|
console.log(`Emitting remote-log-line for ${connectionId}-${processId} (${logType}):`, cleanLine);
|
|
157
|
-
|
|
163
|
+
// Emit only to clients subscribed to this specific remote log stream
|
|
164
|
+
io.to(streamKey).emit('remote-log-line', {
|
|
158
165
|
connectionId,
|
|
159
166
|
processId,
|
|
160
167
|
processName,
|
|
@@ -165,7 +172,7 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
165
172
|
});
|
|
166
173
|
logStream.on('error', (error) => {
|
|
167
174
|
console.error(`Error in pm2 logs stream for ${processName}:`, error);
|
|
168
|
-
io.emit('remote-log-error', {
|
|
175
|
+
io.to(streamKey).emit('remote-log-error', {
|
|
169
176
|
connectionId,
|
|
170
177
|
processId,
|
|
171
178
|
processName,
|
|
@@ -179,7 +186,7 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
|
179
186
|
}
|
|
180
187
|
catch (error) {
|
|
181
188
|
console.error(`Failed to create pm2 logs stream for ${processName}:`, error);
|
|
182
|
-
io.emit('remote-log-error', {
|
|
189
|
+
io.to(streamKey).emit('remote-log-error', {
|
|
183
190
|
connectionId,
|
|
184
191
|
processId,
|
|
185
192
|
processName,
|