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
|
@@ -34,6 +34,8 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
34
34
|
super();
|
|
35
35
|
this._isConnected = false;
|
|
36
36
|
this.isPM2Installed = false;
|
|
37
|
+
/** Index into PM2_BUILDERS of the invocation that actually works on this host. -1 = not yet resolved. */
|
|
38
|
+
this.resolvedBuilderIndex = -1;
|
|
37
39
|
this.client = new ssh2_1.Client();
|
|
38
40
|
this.config = config;
|
|
39
41
|
// Initialize public properties
|
|
@@ -127,11 +129,10 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
127
129
|
}
|
|
128
130
|
// Apply sudo if configured or explicitly requested for this command
|
|
129
131
|
const useElevatedPrivileges = forceSudo || this.config.useSudo;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
132
|
+
// Use sudo -S so sudo reads the password from stdin instead of piping it in the shell command
|
|
133
|
+
const finalCommand = (useElevatedPrivileges && this.config.password)
|
|
134
|
+
? `sudo -S ${command}`
|
|
135
|
+
: command;
|
|
135
136
|
console.log(`Executing command: ${useElevatedPrivileges ? '[sudo] ' : ''}${command}`);
|
|
136
137
|
return new Promise((resolve, reject) => {
|
|
137
138
|
this.client.exec(finalCommand, (err, channel) => {
|
|
@@ -139,6 +140,11 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
139
140
|
console.error('Error executing command:', err);
|
|
140
141
|
return reject(err);
|
|
141
142
|
}
|
|
143
|
+
// Write password to sudo's stdin, then close stdin
|
|
144
|
+
if (useElevatedPrivileges && this.config.password) {
|
|
145
|
+
channel.stdin.write(this.config.password + '\n');
|
|
146
|
+
channel.stdin.end();
|
|
147
|
+
}
|
|
142
148
|
let stdout = '';
|
|
143
149
|
let stderr = '';
|
|
144
150
|
let exitCode = null;
|
|
@@ -152,11 +158,6 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
152
158
|
exitCode = code;
|
|
153
159
|
});
|
|
154
160
|
channel.on('close', () => {
|
|
155
|
-
// Remove sudo password from stdout/stderr if present
|
|
156
|
-
if (useElevatedPrivileges && this.config.password) {
|
|
157
|
-
stdout = stdout.replace(this.config.password, '[PASSWORD REDACTED]');
|
|
158
|
-
stderr = stderr.replace(this.config.password, '[PASSWORD REDACTED]');
|
|
159
|
-
}
|
|
160
161
|
resolve({
|
|
161
162
|
stdout,
|
|
162
163
|
stderr,
|
|
@@ -170,6 +171,170 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
170
171
|
});
|
|
171
172
|
});
|
|
172
173
|
}
|
|
174
|
+
static validateRemotePath(remotePath) {
|
|
175
|
+
if (!remotePath)
|
|
176
|
+
return false;
|
|
177
|
+
if (remotePath.includes('..'))
|
|
178
|
+
return false;
|
|
179
|
+
if (RemoteConnection.SHELL_UNSAFE.test(remotePath))
|
|
180
|
+
return false;
|
|
181
|
+
if (!/\.(log|gz)$/i.test(remotePath))
|
|
182
|
+
return false;
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
async streamFileToResponse(remotePath, res, fileName) {
|
|
186
|
+
// Validate path before any shell interpolation (fallback methods 2-4 use exec)
|
|
187
|
+
if (!RemoteConnection.validateRemotePath(remotePath)) {
|
|
188
|
+
if (!res.headersSent)
|
|
189
|
+
res.status(400).json({ success: false, error: 'Invalid log file path' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const setHeaders = () => {
|
|
193
|
+
if (!res.headersSent) {
|
|
194
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
195
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const escapeRemotePathForShell = (value) => {
|
|
199
|
+
if (/[\0\n\r]/.test(value)) {
|
|
200
|
+
throw new Error('Invalid remote path');
|
|
201
|
+
}
|
|
202
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
203
|
+
};
|
|
204
|
+
let escapedRemotePath;
|
|
205
|
+
try {
|
|
206
|
+
escapedRemotePath = escapeRemotePathForShell(remotePath);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
if (!res.headersSent) {
|
|
210
|
+
res.status(400).json({ success: false, error: 'Invalid remote path' });
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// @group StreamFileToResponse : Helper — pipe an exec channel stdout to response
|
|
215
|
+
const pipeExec = (cmd, sudoPassword) => new Promise((resolve) => {
|
|
216
|
+
this.client.exec(cmd, (err, channel) => {
|
|
217
|
+
if (err) {
|
|
218
|
+
resolve('error');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (sudoPassword) {
|
|
222
|
+
channel.stdin.write(sudoPassword + '\n');
|
|
223
|
+
}
|
|
224
|
+
channel.stdin.end();
|
|
225
|
+
let hasData = false;
|
|
226
|
+
channel.on('data', (chunk) => {
|
|
227
|
+
if (!hasData) {
|
|
228
|
+
setHeaders();
|
|
229
|
+
hasData = true;
|
|
230
|
+
}
|
|
231
|
+
res.write(chunk);
|
|
232
|
+
});
|
|
233
|
+
channel.on('close', (code) => {
|
|
234
|
+
if (hasData) {
|
|
235
|
+
res.end();
|
|
236
|
+
resolve('ok');
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
resolve(code === 0 ? 'empty' : 'error');
|
|
240
|
+
});
|
|
241
|
+
channel.on('error', () => resolve('error'));
|
|
242
|
+
channel.stderr.resume(); // Discard stderr so it doesn't block the channel
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
// ── 1. SFTP (preferred — real streaming, no exec overhead) ──────────
|
|
246
|
+
const sftpOk = await new Promise((resolve) => {
|
|
247
|
+
this.client.sftp((err, sftp) => {
|
|
248
|
+
if (err) {
|
|
249
|
+
resolve(false);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const stream = sftp.createReadStream(remotePath);
|
|
253
|
+
let hasData = false;
|
|
254
|
+
stream.on('data', (chunk) => {
|
|
255
|
+
if (!hasData) {
|
|
256
|
+
setHeaders();
|
|
257
|
+
hasData = true;
|
|
258
|
+
}
|
|
259
|
+
res.write(chunk);
|
|
260
|
+
});
|
|
261
|
+
stream.on('end', () => { if (hasData) {
|
|
262
|
+
res.end();
|
|
263
|
+
resolve(true);
|
|
264
|
+
}
|
|
265
|
+
else
|
|
266
|
+
resolve(false); });
|
|
267
|
+
stream.on('error', () => resolve(false));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
if (sftpOk)
|
|
271
|
+
return;
|
|
272
|
+
// ── 2. Plain cat ─────────────────────────────────────────────────────
|
|
273
|
+
if (await pipeExec(`cat -- ${escapedRemotePath}`) === 'ok')
|
|
274
|
+
return;
|
|
275
|
+
// ── 3. sudo -S cat (password auth) ───────────────────────────────────
|
|
276
|
+
if (this.config.password) {
|
|
277
|
+
if (await pipeExec(`sudo -S cat -- ${escapedRemotePath}`, this.config.password) === 'ok')
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// ── 4. sudo cat without -S (NOPASSWD / key-based auth) ───────────────
|
|
281
|
+
if (await pipeExec(`sudo cat -- ${escapedRemotePath}`) === 'ok')
|
|
282
|
+
return;
|
|
283
|
+
// All attempts failed
|
|
284
|
+
if (!res.headersSent) {
|
|
285
|
+
res.status(403).json({ success: false, error: 'Cannot read log file — check file permissions or sudo configuration' });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stream a remote .gz file to an HTTP response, decompressing it on the fly.
|
|
290
|
+
* Uses SFTP + local zlib.createGunzip() pipeline — no server-side buffering.
|
|
291
|
+
* Falls back to exec `zcat` if SFTP is unavailable.
|
|
292
|
+
*/
|
|
293
|
+
async streamGzFileToResponse(remotePath, res, fileName) {
|
|
294
|
+
if (!RemoteConnection.validateRemotePath(remotePath)) {
|
|
295
|
+
if (!res.headersSent)
|
|
296
|
+
res.status(400).json({ success: false, error: 'Invalid log file path' });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const zlib = require('zlib');
|
|
300
|
+
const setHeaders = () => {
|
|
301
|
+
if (!res.headersSent) {
|
|
302
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
303
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
// ── SFTP + local gunzip (preferred — no exec, streaming, no buffering) ──
|
|
307
|
+
const sftpOk = await new Promise((resolve) => {
|
|
308
|
+
this.client.sftp((err, sftp) => {
|
|
309
|
+
if (err) {
|
|
310
|
+
resolve(false);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const inputStream = sftp.createReadStream(remotePath);
|
|
314
|
+
const gunzip = zlib.createGunzip();
|
|
315
|
+
setHeaders();
|
|
316
|
+
inputStream.pipe(gunzip).pipe(res);
|
|
317
|
+
gunzip.on('finish', () => resolve(true));
|
|
318
|
+
gunzip.on('error', () => { inputStream.destroy(); resolve(false); });
|
|
319
|
+
inputStream.on('error', () => resolve(false));
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
if (sftpOk)
|
|
323
|
+
return;
|
|
324
|
+
// ── Fallback: exec zcat and buffer (path already validated) ──────────
|
|
325
|
+
if (!res.headersSent) {
|
|
326
|
+
const result = await this.executeCommand(`zcat -- "${remotePath}" 2>/dev/null`);
|
|
327
|
+
const decompressed = result.stdout ||
|
|
328
|
+
(await this.executeCommand(`sudo zcat -- "${remotePath}" 2>/dev/null`)).stdout;
|
|
329
|
+
if (decompressed) {
|
|
330
|
+
setHeaders();
|
|
331
|
+
res.send(decompressed);
|
|
332
|
+
}
|
|
333
|
+
else if (!res.headersSent) {
|
|
334
|
+
res.status(500).json({ success: false, error: 'Could not decompress file' });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
173
338
|
/**
|
|
174
339
|
* Check if PM2 is installed on the remote server
|
|
175
340
|
* Uses multiple detection methods for better reliability
|
|
@@ -246,91 +411,122 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
246
411
|
}
|
|
247
412
|
}
|
|
248
413
|
/**
|
|
249
|
-
* Execute a PM2 command with proper PATH handling
|
|
250
|
-
*
|
|
414
|
+
* Execute a PM2 command with proper PATH handling.
|
|
415
|
+
* The first successful invocation is cached so later calls (and streaming
|
|
416
|
+
* commands) reuse the same shell/binary path without re-probing.
|
|
251
417
|
*/
|
|
252
418
|
async executePM2Command(pm2Args) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`~/node_modules/.bin/pm2 ${pm2Args}`, // Local node_modules path
|
|
258
|
-
`/usr/local/bin/pm2 ${pm2Args}`, // System-wide installation
|
|
259
|
-
`npx pm2 ${pm2Args}` // Using npx as fallback
|
|
260
|
-
];
|
|
419
|
+
// Fast path: reuse the builder we already know works on this host
|
|
420
|
+
if (this.resolvedBuilderIndex >= 0) {
|
|
421
|
+
return this.executeCommand(RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args));
|
|
422
|
+
}
|
|
261
423
|
let lastError = null;
|
|
262
|
-
for (
|
|
424
|
+
for (let i = 0; i < RemoteConnection.PM2_BUILDERS.length; i++) {
|
|
425
|
+
const command = RemoteConnection.PM2_BUILDERS[i](pm2Args);
|
|
263
426
|
try {
|
|
264
427
|
const result = await this.executeCommand(command);
|
|
265
428
|
if (result.code === 0) {
|
|
429
|
+
this.resolvedBuilderIndex = i; // Cache for all future calls on this connection
|
|
430
|
+
console.log(`[executePM2Command] Resolved pm2 via builder[${i}]: ${command.substring(0, 60)}`);
|
|
266
431
|
return result;
|
|
267
432
|
}
|
|
268
|
-
lastError = new Error(`Command failed
|
|
433
|
+
lastError = new Error(`Command failed (code ${result.code}): ${result.stderr}`);
|
|
269
434
|
}
|
|
270
435
|
catch (error) {
|
|
271
436
|
lastError = error;
|
|
272
|
-
continue;
|
|
273
437
|
}
|
|
274
438
|
}
|
|
275
439
|
throw lastError || new Error(`Failed to execute PM2 command: ${pm2Args}`);
|
|
276
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Build a pm2 command string using the already-resolved invocation pattern.
|
|
443
|
+
* Falls back to bash -li -c if the resolver has not yet run.
|
|
444
|
+
* Use this when you need the raw command string for a streaming shell exec.
|
|
445
|
+
*/
|
|
446
|
+
buildPM2StreamCommand(pm2Args) {
|
|
447
|
+
if (this.resolvedBuilderIndex >= 0) {
|
|
448
|
+
return RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args);
|
|
449
|
+
}
|
|
450
|
+
// Default: login+interactive shell covers .profile AND .bashrc (catches nvm)
|
|
451
|
+
return `bash -li -c "pm2 ${pm2Args}"`;
|
|
452
|
+
}
|
|
277
453
|
/**
|
|
278
454
|
* Get PM2 processes from the remote server
|
|
279
455
|
*/
|
|
280
456
|
async getPM2Processes() {
|
|
457
|
+
if (!this._isConnected) {
|
|
458
|
+
await this.connect();
|
|
459
|
+
}
|
|
460
|
+
let result;
|
|
281
461
|
try {
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
await this.connect();
|
|
285
|
-
}
|
|
286
|
-
// Check if PM2 is installed (force re-check if not cached)
|
|
287
|
-
const isPM2Installed = await this.checkPM2Installation();
|
|
288
|
-
if (!isPM2Installed) {
|
|
289
|
-
throw new Error('PM2 is not installed on the remote server. Please install PM2 globally using: npm install -g pm2');
|
|
290
|
-
}
|
|
291
|
-
// Update the cached status
|
|
462
|
+
// executePM2Command tries all known paths/shells before giving up
|
|
463
|
+
result = await this.executePM2Command('jlist');
|
|
292
464
|
this.isPM2Installed = true;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
// All path attempts failed — PM2 is genuinely not found
|
|
468
|
+
console.error('Error getting PM2 processes:', err);
|
|
469
|
+
throw new Error('PM2 is not installed or not accessible on the remote server. Please install PM2 globally using: npm install -g pm2');
|
|
470
|
+
}
|
|
471
|
+
// Extract the JSON array from the output (pm2 jlist may include extra lines)
|
|
472
|
+
let cleanedOutput = result.stdout.trim();
|
|
473
|
+
const startIndex = cleanedOutput.indexOf('[');
|
|
474
|
+
const endIndex = cleanedOutput.lastIndexOf(']') + 1;
|
|
475
|
+
if (startIndex !== -1 && endIndex > 0) {
|
|
476
|
+
cleanedOutput = cleanedOutput.substring(startIndex, endIndex);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
console.log('Invalid PM2 jlist output, returning empty list. Raw:', result.stdout);
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const processList = JSON.parse(cleanedOutput);
|
|
484
|
+
// Return the full PM2 process shape (pm2_env + monit) so all frontend
|
|
485
|
+
// components work without modification when viewing a remote server.
|
|
486
|
+
return processList.map((proc) => {
|
|
487
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
488
|
+
return ({
|
|
489
|
+
pid: proc.pid || 0,
|
|
490
|
+
pm_id: proc.pm_id || 0,
|
|
491
|
+
name: proc.name || '',
|
|
492
|
+
monit: {
|
|
493
|
+
cpu: proc.monit ? (proc.monit.cpu || 0) : 0,
|
|
494
|
+
memory: proc.monit ? (proc.monit.memory || 0) : 0,
|
|
495
|
+
},
|
|
496
|
+
pm2_env: {
|
|
497
|
+
// identity
|
|
498
|
+
pm_id: proc.pm_id || 0,
|
|
499
|
+
name: proc.name || '',
|
|
500
|
+
namespace: ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.namespace) || 'default',
|
|
501
|
+
version: ((_b = proc.pm2_env) === null || _b === void 0 ? void 0 : _b.version) || '',
|
|
502
|
+
versioning: ((_c = proc.pm2_env) === null || _c === void 0 ? void 0 : _c.versioning) || null,
|
|
503
|
+
// timing
|
|
504
|
+
pm_uptime: ((_d = proc.pm2_env) === null || _d === void 0 ? void 0 : _d.pm_uptime) || 0,
|
|
505
|
+
created_at: ((_e = proc.pm2_env) === null || _e === void 0 ? void 0 : _e.created_at) || 0,
|
|
506
|
+
// paths
|
|
507
|
+
pm_cwd: ((_f = proc.pm2_env) === null || _f === void 0 ? void 0 : _f.pm_cwd) || '',
|
|
508
|
+
pm_exec_path: ((_g = proc.pm2_env) === null || _g === void 0 ? void 0 : _g.pm_exec_path) || '',
|
|
509
|
+
pm_out_log_path: ((_h = proc.pm2_env) === null || _h === void 0 ? void 0 : _h.pm_out_log_path) || '',
|
|
510
|
+
pm_err_log_path: ((_j = proc.pm2_env) === null || _j === void 0 ? void 0 : _j.pm_err_log_path) || '',
|
|
511
|
+
// runtime
|
|
512
|
+
exec_interpreter: ((_k = proc.pm2_env) === null || _k === void 0 ? void 0 : _k.exec_interpreter) || 'node',
|
|
513
|
+
exec_mode: ((_l = proc.pm2_env) === null || _l === void 0 ? void 0 : _l.exec_mode) || 'fork',
|
|
514
|
+
instances: ((_m = proc.pm2_env) === null || _m === void 0 ? void 0 : _m.instances) || 1,
|
|
515
|
+
node_args: ((_o = proc.pm2_env) === null || _o === void 0 ? void 0 : _o.node_args) || [],
|
|
516
|
+
status: ((_p = proc.pm2_env) === null || _p === void 0 ? void 0 : _p.status) || 'unknown',
|
|
517
|
+
restart_time: ((_q = proc.pm2_env) === null || _q === void 0 ? void 0 : _q.restart_time) || 0,
|
|
518
|
+
unstable_restarts: ((_r = proc.pm2_env) === null || _r === void 0 ? void 0 : _r.unstable_restarts) || 0,
|
|
519
|
+
autorestart: (_t = (_s = proc.pm2_env) === null || _s === void 0 ? void 0 : _s.autorestart) !== null && _t !== void 0 ? _t : true,
|
|
520
|
+
watch: ((_u = proc.pm2_env) === null || _u === void 0 ? void 0 : _u.watch) || false,
|
|
521
|
+
env: ((_v = proc.pm2_env) === null || _v === void 0 ? void 0 : _v.env) || {},
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
});
|
|
330
525
|
}
|
|
331
526
|
catch (error) {
|
|
332
|
-
console.error('Error
|
|
333
|
-
|
|
527
|
+
console.error('Error parsing PM2 process list:', error);
|
|
528
|
+
console.log('Raw output:', result.stdout);
|
|
529
|
+
return [];
|
|
334
530
|
}
|
|
335
531
|
}
|
|
336
532
|
/**
|
|
@@ -506,14 +702,11 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
506
702
|
if (!this._isConnected) {
|
|
507
703
|
throw new Error('Connection not established');
|
|
508
704
|
}
|
|
509
|
-
|
|
705
|
+
// Use sudo -S so sudo reads the password from stdin rather than the shell command line
|
|
706
|
+
const useSudoAuth = useSudo && this.config.useSudo && !!this.config.password;
|
|
707
|
+
const finalCommand = useSudoAuth ? `sudo -S ${command}` : command;
|
|
510
708
|
let isInitialized = false;
|
|
511
|
-
|
|
512
|
-
if (useSudo && this.config.useSudo && this.config.password) {
|
|
513
|
-
// Use echo to pipe the password to sudo for streaming commands
|
|
514
|
-
finalCommand = `echo '${this.config.password}' | sudo -S ${command}`;
|
|
515
|
-
}
|
|
516
|
-
console.log(`[createLogStream] Executing command: ${finalCommand.replace(this.config.password || '', '[PASSWORD]')}`);
|
|
709
|
+
console.log(`[createLogStream] Executing command: ${useSudoAuth ? '[sudo] ' : ''}${command}`);
|
|
517
710
|
return new Promise((resolve, reject) => {
|
|
518
711
|
this.client.exec(finalCommand, (err, stream) => {
|
|
519
712
|
var _a;
|
|
@@ -522,6 +715,10 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
522
715
|
reject(err);
|
|
523
716
|
return;
|
|
524
717
|
}
|
|
718
|
+
// Write password to sudo's stdin (do not close stdin — stream must stay open)
|
|
719
|
+
if (useSudoAuth && this.config.password) {
|
|
720
|
+
stream.stdin.write(this.config.password + '\n');
|
|
721
|
+
}
|
|
525
722
|
const logEmitter = new events_1.EventEmitter();
|
|
526
723
|
stream.on('data', (data) => {
|
|
527
724
|
const dataStr = data.toString();
|
|
@@ -566,6 +763,37 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
566
763
|
}
|
|
567
764
|
}
|
|
568
765
|
exports.RemoteConnection = RemoteConnection;
|
|
766
|
+
/**
|
|
767
|
+
* Stream a remote file directly into an HTTP response without buffering.
|
|
768
|
+
*
|
|
769
|
+
* Strategy (tried in order):
|
|
770
|
+
* 1. SFTP createReadStream — best; handles large files, uses file-transfer protocol
|
|
771
|
+
* 2. exec `cat <file>` — current SSH user, direct pipe to response
|
|
772
|
+
* 3. exec `sudo -S cat` — password-based sudo (when password auth is configured)
|
|
773
|
+
* 4. exec `sudo cat` — NOPASSWD sudo (key-based auth where sudo needs no password)
|
|
774
|
+
*/
|
|
775
|
+
// @group Security : Shell metacharacter allowlist for remote file paths
|
|
776
|
+
RemoteConnection.SHELL_UNSAFE = /['"`$|&;<>(){}\\\n\r\0]/;
|
|
777
|
+
/**
|
|
778
|
+
* Ordered list of command builders tried when resolving the pm2 binary.
|
|
779
|
+
* The index of the first successful builder is cached so that subsequent
|
|
780
|
+
* calls (including streaming commands) reuse the same invocation.
|
|
781
|
+
*/
|
|
782
|
+
RemoteConnection.PM2_BUILDERS = [
|
|
783
|
+
args => `pm2 ${args}`,
|
|
784
|
+
args => `bash -l -c "pm2 ${args}"`,
|
|
785
|
+
args => `bash -i -c "pm2 ${args}"`,
|
|
786
|
+
args => `bash -li -c "pm2 ${args}"`,
|
|
787
|
+
args => `/usr/local/bin/pm2 ${args}`,
|
|
788
|
+
args => `/usr/bin/pm2 ${args}`,
|
|
789
|
+
args => `~/.npm-global/bin/pm2 ${args}`,
|
|
790
|
+
args => `~/.npm/bin/pm2 ${args}`,
|
|
791
|
+
args => `~/node_modules/.bin/pm2 ${args}`,
|
|
792
|
+
args => `. ~/.nvm/nvm.sh && pm2 ${args}`,
|
|
793
|
+
args => `. ~/.nvm/nvm.sh && nvm use --lts && pm2 ${args}`,
|
|
794
|
+
args => `. /usr/share/nvm/init-nvm.sh && pm2 ${args}`,
|
|
795
|
+
args => `npx pm2 ${args}`,
|
|
796
|
+
];
|
|
569
797
|
/**
|
|
570
798
|
* Connection manager to handle multiple remote connections
|
|
571
799
|
*/
|
package/package.json
CHANGED
|
@@ -1,69 +1,73 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ezpm2gui",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"main": "dist/server/index.js",
|
|
5
|
-
"bin": {
|
|
6
|
-
"ezpm2gui": "bin/ezpm2gui.js",
|
|
7
|
-
"ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node dist/server/index.js",
|
|
11
|
-
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
|
|
12
|
-
"dev:server": "nodemon --exec ts-node src/server/index.ts",
|
|
13
|
-
"dev:client": "cd src/client && npm
|
|
14
|
-
"build": "node scripts/build.js",
|
|
15
|
-
"build:server": "tsc",
|
|
16
|
-
"build:client": "cd src/client && npm run build",
|
|
17
|
-
"build:bin": "tsc --project tsconfig.bin.json",
|
|
18
|
-
"prepare": "npm run build",
|
|
19
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
20
|
-
"postinstall": "node scripts/postinstall.js"
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"pm2",
|
|
24
|
-
"gui",
|
|
25
|
-
"monitor",
|
|
26
|
-
"process-manager",
|
|
27
|
-
"dashboard"
|
|
28
|
-
],
|
|
29
|
-
"author": "Chandan Bhagat",
|
|
30
|
-
"license": "ISC",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
|
|
34
|
-
},
|
|
35
|
-
"description": "A modern web-based GUI for PM2 process manager",
|
|
36
|
-
"files": [
|
|
37
|
-
"dist/",
|
|
38
|
-
"bin/",
|
|
39
|
-
"src/client/build/",
|
|
40
|
-
"scripts/postinstall.js"
|
|
41
|
-
],
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@tailwindcss/postcss": "^4.1.14",
|
|
44
|
-
"@types/node-cron": "^3.0.11",
|
|
45
|
-
"axios": "^1.9.0",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"react
|
|
53
|
-
"react-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"@types/
|
|
61
|
-
"@types/
|
|
62
|
-
"@types/
|
|
63
|
-
"@types/
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "ezpm2gui",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"main": "dist/server/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ezpm2gui": "bin/ezpm2gui.js",
|
|
7
|
+
"ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node dist/server/index.js",
|
|
11
|
+
"dev": "concurrently --kill-others-on-fail --names \"server,client\" --prefix-colors \"cyan,green\" \"npm run dev:server\" \"npm run dev:client\"",
|
|
12
|
+
"dev:server": "nodemon --exec ts-node src/server/index.ts",
|
|
13
|
+
"dev:client": "cd src/client && npm run dev",
|
|
14
|
+
"build": "node scripts/build.js",
|
|
15
|
+
"build:server": "tsc",
|
|
16
|
+
"build:client": "cd src/client && npm run build",
|
|
17
|
+
"build:bin": "tsc --project tsconfig.bin.json",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
20
|
+
"postinstall": "node scripts/postinstall.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pm2",
|
|
24
|
+
"gui",
|
|
25
|
+
"monitor",
|
|
26
|
+
"process-manager",
|
|
27
|
+
"dashboard"
|
|
28
|
+
],
|
|
29
|
+
"author": "Chandan Bhagat",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
|
|
34
|
+
},
|
|
35
|
+
"description": "A modern web-based GUI for PM2 process manager",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/",
|
|
38
|
+
"bin/",
|
|
39
|
+
"src/client/build/",
|
|
40
|
+
"scripts/postinstall.js"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@tailwindcss/postcss": "^4.1.14",
|
|
44
|
+
"@types/node-cron": "^3.0.11",
|
|
45
|
+
"axios": "^1.9.0",
|
|
46
|
+
"dotenv": "^16.5.0",
|
|
47
|
+
"chart.js": "^4.4.9",
|
|
48
|
+
"cron-parser": "^5.4.0",
|
|
49
|
+
"express": "^4.18.3",
|
|
50
|
+
"node-cron": "^4.2.1",
|
|
51
|
+
"pm2": "^6.0.5",
|
|
52
|
+
"react": "^19.1.0",
|
|
53
|
+
"react-dom": "^19.1.0",
|
|
54
|
+
"react-scripts": "^5.0.1",
|
|
55
|
+
"socket.io": "^4.8.1",
|
|
56
|
+
"ssh2": "^1.16.0",
|
|
57
|
+
"uuid": "^11.0.5"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/express": "^4.17.21",
|
|
61
|
+
"@types/node": "^22.15.17",
|
|
62
|
+
"@types/react": "^19.1.4",
|
|
63
|
+
"@types/react-dom": "^19.1.5",
|
|
64
|
+
"@types/socket.io": "^3.0.2",
|
|
65
|
+
"@types/ssh2": "^1.15.5",
|
|
66
|
+
"@types/uuid": "^10.0.0",
|
|
67
|
+
"concurrently": "^9.1.2",
|
|
68
|
+
"cross-env": "^10.1.0",
|
|
69
|
+
"nodemon": "^3.1.10",
|
|
70
|
+
"ts-node": "^10.9.2",
|
|
71
|
+
"typescript": "^5.8.3"
|
|
72
|
+
}
|
|
73
|
+
}
|