@drocketxx/pm2me 1.1.36 → 1.1.38
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/backend/app.js +109 -63
- package/backend/public/assets/index-BBA5OG2n.css +1 -0
- package/backend/public/assets/index-OyOipAZ9.js +16 -0
- package/backend/public/index.html +2 -2
- package/backend/routes/api.js +239 -303
- package/backend/services/systemService.js +12 -18
- package/package.json +53 -53
- package/backend/public/assets/index-BDEN18Iq.css +0 -1
- package/backend/public/assets/index-DF5VxOtp.js +0 -22
- package/c:VsCodemyPM2Metmp_old_dashboard.vue +0 -390
package/backend/routes/api.js
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
import crypto from 'crypto';
|
|
11
|
-
import { exec } from 'child_process';
|
|
11
|
+
import { exec, execSync, execFile } from 'child_process';
|
|
12
12
|
import util from 'util';
|
|
13
13
|
import bcrypt from 'bcrypt';
|
|
14
14
|
import os from 'os';
|
|
@@ -16,9 +16,6 @@ import os from 'os';
|
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const getWorkspacesDir = () => db.data.settings?.appsPath || path.resolve(__dirname, '../../apps');
|
|
18
18
|
|
|
19
|
-
// Track active build processes to prevent concurrent builds
|
|
20
|
-
const activeBuildProcesses = new Map();
|
|
21
|
-
|
|
22
19
|
const router = express.Router();
|
|
23
20
|
|
|
24
21
|
// GET all PM2 apps
|
|
@@ -44,6 +41,7 @@ router.get('/system/stats', async (req, res) => {
|
|
|
44
41
|
|
|
45
42
|
// GET PM2 version and update check
|
|
46
43
|
const execAsync = util.promisify(exec);
|
|
44
|
+
const execFileAsync = util.promisify(execFile);
|
|
47
45
|
router.get('/pm2/version-check', async (req, res) => {
|
|
48
46
|
try {
|
|
49
47
|
// Check globally installed PM2 version via npm (avoids local node_modules/.bin/pm2)
|
|
@@ -107,11 +105,21 @@ router.post('/pm2/:action', async (req, res) => {
|
|
|
107
105
|
else if (action === 'reload') result = await pm2Service.reloadApp(nameOrId);
|
|
108
106
|
else if (action === 'delete') result = await pm2Service.deleteApp(nameOrId);
|
|
109
107
|
|
|
110
|
-
// Find app by name to emit system log
|
|
108
|
+
// Find app by name to emit system log and save to history
|
|
111
109
|
const app = db.data.apps.find(a => a.name === nameOrId);
|
|
112
|
-
if (app
|
|
110
|
+
if (app) {
|
|
113
111
|
const actionLabel = action.charAt(0).toUpperCase() + action.slice(1);
|
|
114
|
-
|
|
112
|
+
const logString = `[pm2me] Action: ${actionLabel} executed manually by user\n`;
|
|
113
|
+
|
|
114
|
+
if (req.io) {
|
|
115
|
+
req.io.emit(`deploy-log-${app.id}`, logString.trim());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Append to deploy log
|
|
119
|
+
const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
|
|
120
|
+
if (fs.existsSync(logFilePath)) {
|
|
121
|
+
fs.appendFileSync(logFilePath, logString);
|
|
122
|
+
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
res.json({ success: true, result });
|
|
@@ -153,18 +161,66 @@ router.get('/apps/:id/sync-status', async (req, res) => {
|
|
|
153
161
|
}
|
|
154
162
|
});
|
|
155
163
|
|
|
164
|
+
// GET native PM2 logs - reads directly from C:\Users\xxx\.pm2\logs\appname-out.log
|
|
165
|
+
router.get('/apps/:id/pm2-logs', async (req, res) => {
|
|
166
|
+
const { id } = req.params;
|
|
167
|
+
const appConfig = db.data.apps.find(a => a.id === id);
|
|
168
|
+
if (!appConfig) return res.status(404).send('App not found');
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await pm2Service.connectPM2();
|
|
172
|
+
const list = await pm2Service.listApps();
|
|
173
|
+
const pmApp = list.find(a => a.name === appConfig.name);
|
|
174
|
+
|
|
175
|
+
if (!pmApp?.pm2_env) {
|
|
176
|
+
return res.send(`App "${appConfig.name}" is not currently running in PM2.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const readTail = (filePath, maxLines) => {
|
|
180
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
181
|
+
const size = fs.statSync(filePath).size;
|
|
182
|
+
if (size === 0) return [];
|
|
183
|
+
const readBytes = Math.min(size, 100000);
|
|
184
|
+
const buf = Buffer.alloc(readBytes);
|
|
185
|
+
const fd = fs.openSync(filePath, 'r');
|
|
186
|
+
fs.readSync(fd, buf, 0, readBytes, size - readBytes);
|
|
187
|
+
fs.closeSync(fd);
|
|
188
|
+
return buf.toString('utf8').split('\n').filter(l => l.trim()).slice(-maxLines);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const outLines = readTail(pmApp.pm2_env.pm_out_log_path, 200);
|
|
192
|
+
const errLines = readTail(pmApp.pm2_env.pm_err_log_path, 50)
|
|
193
|
+
.map(l => `[stderr] ${l}`);
|
|
194
|
+
|
|
195
|
+
const all = [...outLines, ...errLines];
|
|
196
|
+
res.send(all.join('\n'));
|
|
197
|
+
} catch (err) {
|
|
198
|
+
res.status(500).send(`Error reading PM2 logs: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
|
|
156
203
|
router.put('/apps/:id', async (req, res) => {
|
|
157
204
|
const { id } = req.params;
|
|
158
205
|
const index = db.data.apps.findIndex(app => app.id === id);
|
|
159
206
|
if (index === -1) return res.status(404).json({ error: 'App not found' });
|
|
160
207
|
|
|
208
|
+
const oldApp = db.data.apps[index];
|
|
209
|
+
const newName = req.body.name;
|
|
210
|
+
const isNameChanged = newName && oldApp.name && newName !== oldApp.name;
|
|
211
|
+
|
|
161
212
|
// Merge new config, but preserve id and status
|
|
162
213
|
db.data.apps[index] = {
|
|
163
|
-
...
|
|
214
|
+
...oldApp,
|
|
164
215
|
...req.body,
|
|
165
|
-
id:
|
|
166
|
-
status:
|
|
216
|
+
id: oldApp.id,
|
|
217
|
+
status: oldApp.status
|
|
167
218
|
};
|
|
219
|
+
|
|
220
|
+
if (isNameChanged) {
|
|
221
|
+
db.data.apps[index].previousName = oldApp.name;
|
|
222
|
+
}
|
|
223
|
+
|
|
168
224
|
await db.write();
|
|
169
225
|
res.json(db.data.apps[index]);
|
|
170
226
|
});
|
|
@@ -268,6 +324,84 @@ router.get('/git/repositories', async (req, res) => {
|
|
|
268
324
|
}
|
|
269
325
|
});
|
|
270
326
|
|
|
327
|
+
const buildPM2StartOptions = (appConfig, targetPath, updateEnv = false) => {
|
|
328
|
+
let pmArgs = [];
|
|
329
|
+
if (appConfig.pm2Args) pmArgs = appConfig.pm2Args.split(' ').filter(Boolean);
|
|
330
|
+
|
|
331
|
+
let scriptName = appConfig.pm2Script || 'npm';
|
|
332
|
+
|
|
333
|
+
// To prevent Windows console popups caused by `npm` spawning sub-processes,
|
|
334
|
+
// we intercept 'npm run [script]' and extract the direct 'node' command from package.json
|
|
335
|
+
if (os.platform() === 'win32' && scriptName.toLowerCase() === 'npm') {
|
|
336
|
+
const isRun = pmArgs[0] === 'run';
|
|
337
|
+
const targetNpmScript = isRun ? pmArgs[1] : (pmArgs[0] === 'start' ? 'start' : null);
|
|
338
|
+
|
|
339
|
+
if (targetNpmScript) {
|
|
340
|
+
try {
|
|
341
|
+
const pkgPath = path.join(targetPath, 'package.json');
|
|
342
|
+
if (fs.existsSync(pkgPath)) {
|
|
343
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
344
|
+
if (pkg.scripts && pkg.scripts[targetNpmScript]) {
|
|
345
|
+
const rawCommand = pkg.scripts[targetNpmScript].trim();
|
|
346
|
+
// If it's a simple node command, bypass npm entirely
|
|
347
|
+
if (rawCommand.startsWith('node ')) {
|
|
348
|
+
const parsedArgs = rawCommand.substring(5).trim().split(' ');
|
|
349
|
+
scriptName = parsedArgs[0]; // e.g. dist/server.js
|
|
350
|
+
pmArgs = parsedArgs.slice(1);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
console.error('Failed to parse package.json for script resolution', e);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (os.platform() === 'win32') {
|
|
361
|
+
if (scriptName === 'npm') {
|
|
362
|
+
try {
|
|
363
|
+
const root = execSync('npm root -g', { windowsHide: true }).toString().trim();
|
|
364
|
+
const cliPath = path.join(root, 'npm', 'bin', 'npm-cli.js');
|
|
365
|
+
if (fs.existsSync(cliPath)) {
|
|
366
|
+
scriptName = cliPath;
|
|
367
|
+
} else {
|
|
368
|
+
scriptName += '.cmd';
|
|
369
|
+
}
|
|
370
|
+
} catch (e) {
|
|
371
|
+
scriptName += '.cmd';
|
|
372
|
+
}
|
|
373
|
+
} else if (['yarn', 'pnpm', 'npx'].includes(scriptName)) {
|
|
374
|
+
scriptName += '.cmd';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let startOptions = {
|
|
379
|
+
name: appConfig.name,
|
|
380
|
+
cwd: targetPath,
|
|
381
|
+
script: scriptName,
|
|
382
|
+
args: pmArgs,
|
|
383
|
+
env: appConfig.env || {}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
if (updateEnv) startOptions.updateEnv = true;
|
|
387
|
+
|
|
388
|
+
if (os.platform() === 'win32' && scriptName.endsWith('.cmd')) {
|
|
389
|
+
startOptions.interpreter = 'none';
|
|
390
|
+
startOptions.windowsHide = true;
|
|
391
|
+
} else if (!scriptName.endsWith('.js') && !scriptName.endsWith('.mjs')) {
|
|
392
|
+
startOptions.interpreter = 'none';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (os.platform() === 'win32') {
|
|
396
|
+
startOptions.windowsHide = true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (appConfig.ecosystemFile) {
|
|
400
|
+
return path.join(targetPath, appConfig.ecosystemFile);
|
|
401
|
+
}
|
|
402
|
+
return startOptions;
|
|
403
|
+
};
|
|
404
|
+
|
|
271
405
|
const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess, setPipelineState, logFilePath) => {
|
|
272
406
|
if (!appConfig.lastSuccessfulCommitHash || appConfig.lastSuccessfulCommitHash === appConfig.commitHash) {
|
|
273
407
|
logProcess('No stable version to rollback to or already on last good version.', true);
|
|
@@ -300,7 +434,7 @@ const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess,
|
|
|
300
434
|
.filter(Boolean)
|
|
301
435
|
.join(' && ');
|
|
302
436
|
await new Promise((resolve, reject) => {
|
|
303
|
-
const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024 });
|
|
437
|
+
const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
|
|
304
438
|
child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
|
|
305
439
|
child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
|
|
306
440
|
child.on('close', code => code !== 0 ? reject(new Error('Rollback build failed')) : resolve());
|
|
@@ -310,13 +444,7 @@ const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess,
|
|
|
310
444
|
// 5. Restart PM2 with original version
|
|
311
445
|
logProcess('Restarting PM2 with original version...', true);
|
|
312
446
|
await pm2Service.connectPM2();
|
|
313
|
-
const startOpts =
|
|
314
|
-
name: appConfig.name,
|
|
315
|
-
cwd: targetPath,
|
|
316
|
-
script: appConfig.pm2Script || 'npm',
|
|
317
|
-
updateEnv: true,
|
|
318
|
-
env: appConfig.env || {}
|
|
319
|
-
};
|
|
447
|
+
const startOpts = buildPM2StartOptions(appConfig, targetPath, true);
|
|
320
448
|
|
|
321
449
|
try {
|
|
322
450
|
await pm2Service.reloadApp(appConfig.name, startOpts);
|
|
@@ -363,7 +491,7 @@ export const performDeployment = async (appId, io) => {
|
|
|
363
491
|
fs.mkdirSync(wsDir, { recursive: true });
|
|
364
492
|
}
|
|
365
493
|
|
|
366
|
-
fs.
|
|
494
|
+
fs.appendFileSync(logFilePath, `\n\n--- Deployment Started at ${new Date().toISOString()} ---\n`);
|
|
367
495
|
|
|
368
496
|
const logProcess = (msg, isSystem = false) => {
|
|
369
497
|
const formattedMsg = isSystem ? `[pm2me] ${msg}` : msg;
|
|
@@ -399,24 +527,6 @@ export const performDeployment = async (appId, io) => {
|
|
|
399
527
|
fs.writeFileSync(path.join(targetPath, '.env'), envFileData);
|
|
400
528
|
|
|
401
529
|
if (appConfig.buildScript) {
|
|
402
|
-
// Kill any existing build process for this app
|
|
403
|
-
if (activeBuildProcesses.has(appId)) {
|
|
404
|
-
const existingProcess = activeBuildProcesses.get(appId);
|
|
405
|
-
logProcess('Killing existing build process...', true);
|
|
406
|
-
try {
|
|
407
|
-
existingProcess.kill('SIGTERM');
|
|
408
|
-
// Give it a moment to terminate gracefully
|
|
409
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
410
|
-
if (!existingProcess.killed) {
|
|
411
|
-
existingProcess.kill('SIGKILL');
|
|
412
|
-
}
|
|
413
|
-
activeBuildProcesses.delete(appId);
|
|
414
|
-
logProcess('Existing build process terminated', true);
|
|
415
|
-
} catch (killErr) {
|
|
416
|
-
logProcess(`Warning: Failed to kill existing build process: ${killErr.message}`, true);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
530
|
const normalizedScript = appConfig.buildScript.split('\n')
|
|
421
531
|
.map(s => s.trim())
|
|
422
532
|
.filter(Boolean)
|
|
@@ -427,22 +537,13 @@ export const performDeployment = async (appId, io) => {
|
|
|
427
537
|
logProcess('Executing Build Script', true);
|
|
428
538
|
await new Promise((resolve, reject) => {
|
|
429
539
|
const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
|
|
430
|
-
|
|
431
|
-
// Store the build process
|
|
432
|
-
activeBuildProcesses.set(appId, child);
|
|
433
|
-
|
|
434
540
|
child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
|
|
435
541
|
child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
|
|
436
542
|
child.on('close', code => {
|
|
437
|
-
// Remove from active processes when done
|
|
438
|
-
activeBuildProcesses.delete(appId);
|
|
439
543
|
if (code !== 0) reject(new Error(`Build script exited with code ${code}`));
|
|
440
544
|
else { logProcess('Build Script Completed', true); resolve(); }
|
|
441
545
|
});
|
|
442
|
-
child.on('error', err => {
|
|
443
|
-
activeBuildProcesses.delete(appId);
|
|
444
|
-
reject(err);
|
|
445
|
-
});
|
|
546
|
+
child.on('error', err => { reject(err); });
|
|
446
547
|
});
|
|
447
548
|
}
|
|
448
549
|
|
|
@@ -451,32 +552,47 @@ export const performDeployment = async (appId, io) => {
|
|
|
451
552
|
logProcess('Starting PM2', true);
|
|
452
553
|
await pm2Service.connectPM2();
|
|
453
554
|
|
|
454
|
-
|
|
455
|
-
if (appConfig.pm2Args) pmArgs = appConfig.pm2Args.split(' ').filter(Boolean);
|
|
555
|
+
const startOptions = buildPM2StartOptions(appConfig, targetPath, false);
|
|
456
556
|
|
|
457
|
-
|
|
458
|
-
name: appConfig.name,
|
|
459
|
-
cwd: targetPath,
|
|
460
|
-
script: appConfig.pm2Script || 'npm',
|
|
461
|
-
args: appConfig.pm2Script === 'npm' ? pmArgs : [],
|
|
462
|
-
env: appConfig.env || {}
|
|
463
|
-
};
|
|
557
|
+
logProcess('Updating Application in PM2', true);
|
|
464
558
|
|
|
465
|
-
if
|
|
559
|
+
// Delete previous process if the app was renamed
|
|
560
|
+
if (appConfig.previousName) {
|
|
561
|
+
logProcess(`App renamed. Removing old PM2 process: ${appConfig.previousName}`, true);
|
|
562
|
+
try {
|
|
563
|
+
await pm2Service.deleteApp(appConfig.previousName);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
// If it wasn't running or already deleted, ignore.
|
|
566
|
+
}
|
|
567
|
+
delete appConfig.previousName;
|
|
568
|
+
await db.write();
|
|
569
|
+
}
|
|
466
570
|
|
|
467
|
-
logProcess('Updating Application in PM2', true);
|
|
468
571
|
let exists = null;
|
|
469
572
|
try {
|
|
470
573
|
const list = await pm2Service.listApps();
|
|
471
574
|
exists = list.find(a => a.name === appConfig.name);
|
|
472
575
|
if (exists) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
576
|
+
const currentInterpreter = exists.pm2_env ? exists.pm2_env.exec_interpreter : null;
|
|
577
|
+
const currentScriptPath = exists.pm2_env ? exists.pm2_env.pm_exec_path : null;
|
|
578
|
+
const scriptBaseName = currentScriptPath ? path.basename(currentScriptPath) : null;
|
|
579
|
+
|
|
580
|
+
const interpreterChanged = startOptions.interpreter && currentInterpreter !== startOptions.interpreter;
|
|
581
|
+
const scriptChanged = scriptBaseName !== null && scriptBaseName !== startOptions.script;
|
|
582
|
+
|
|
583
|
+
if (interpreterChanged || scriptChanged) {
|
|
584
|
+
logProcess('Runtime configuration changed (Script or Interpreter). Recreating process...', true);
|
|
585
|
+
await pm2Service.deleteApp(appConfig.name);
|
|
586
|
+
await pm2Service.startApp(startOptions);
|
|
477
587
|
} else {
|
|
478
|
-
|
|
479
|
-
|
|
588
|
+
const updateOptions = { ...startOptions, updateEnv: true };
|
|
589
|
+
if (appConfig.zeroDowntime !== false) {
|
|
590
|
+
logProcess('Zero-downtime Reloading with new environment', true);
|
|
591
|
+
await pm2Service.reloadApp(appConfig.name, updateOptions);
|
|
592
|
+
} else {
|
|
593
|
+
logProcess('Restarting (Non Zero-downtime) with new environment', true);
|
|
594
|
+
await pm2Service.restartApp(appConfig.name, updateOptions);
|
|
595
|
+
}
|
|
480
596
|
}
|
|
481
597
|
} else {
|
|
482
598
|
logProcess('Fresh Starting', true);
|
|
@@ -562,6 +678,14 @@ export const performDeployment = async (appId, io) => {
|
|
|
562
678
|
// Build and Deploy an App
|
|
563
679
|
router.post('/deploy/:appId', async (req, res) => {
|
|
564
680
|
try {
|
|
681
|
+
const app = db.data.apps.find(a => a.id === req.params.appId);
|
|
682
|
+
if (app) {
|
|
683
|
+
const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
|
|
684
|
+
const deployString = `[pm2me] Action: Manual deployment triggered by user\n`;
|
|
685
|
+
if (fs.existsSync(logFilePath)) fs.appendFileSync(logFilePath, deployString);
|
|
686
|
+
if (req.io) req.io.emit(`deploy-log-${app.id}`, deployString.trim());
|
|
687
|
+
}
|
|
688
|
+
|
|
565
689
|
await performDeployment(req.params.appId, req.io);
|
|
566
690
|
res.json({ success: true });
|
|
567
691
|
} catch (err) {
|
|
@@ -575,50 +699,16 @@ router.get('/deploy/:appId/logs', async (req, res) => {
|
|
|
575
699
|
const appConfig = db.data.apps.find(a => a.id === appId);
|
|
576
700
|
if (!appConfig) return res.status(404).json({ error: 'App not found' });
|
|
577
701
|
|
|
578
|
-
const logFilePath = path.join(getWorkspacesDir(), `${appConfig.
|
|
702
|
+
const logFilePath = path.join(getWorkspacesDir(), `${appConfig.id}_deploy.log`);
|
|
579
703
|
let logs = '';
|
|
580
704
|
|
|
581
705
|
if (fs.existsSync(logFilePath)) {
|
|
582
706
|
logs += fs.readFileSync(logFilePath, 'utf8');
|
|
583
707
|
}
|
|
584
708
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const pmApp = list.find(app => app.name === appConfig.name);
|
|
589
|
-
if (pmApp && pmApp.pm2_env) {
|
|
590
|
-
const outPath = pmApp.pm2_env.pm_out_log_path;
|
|
591
|
-
const errPath = pmApp.pm2_env.pm_err_log_path;
|
|
592
|
-
|
|
593
|
-
const readTailLines = (filePath, prefix = '', maxLines = 100) => {
|
|
594
|
-
if (!filePath || !fs.existsSync(filePath)) return '';
|
|
595
|
-
const stats = fs.statSync(filePath);
|
|
596
|
-
const size = stats.size;
|
|
597
|
-
const maxBytes = 50000;
|
|
598
|
-
const start = Math.max(0, size - maxBytes);
|
|
599
|
-
const buffer = Buffer.alloc(Math.max(0, size - start));
|
|
600
|
-
if (buffer.length === 0) return '';
|
|
601
|
-
const fd = fs.openSync(filePath, 'r');
|
|
602
|
-
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
603
|
-
fs.closeSync(fd);
|
|
604
|
-
const text = buffer.toString('utf8');
|
|
605
|
-
const lines = text.split('\n').filter(l => l.trim());
|
|
606
|
-
return lines.slice(-maxLines).map(line => `${prefix} | ${line}`).join('\n');
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
const outLogs = readTailLines(outPath, `[out]`);
|
|
610
|
-
const errLogs = readTailLines(errPath, `[err]`);
|
|
611
|
-
|
|
612
|
-
if (outLogs || errLogs) {
|
|
613
|
-
logs += '\n\n--- 🔵 PM2 Execution Logs ---\n';
|
|
614
|
-
if (errLogs) logs += errLogs + '\n';
|
|
615
|
-
if (outLogs) logs += outLogs + '\n';
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
} catch (err) {
|
|
619
|
-
console.error('Failed to fetch PM2 logs', err);
|
|
620
|
-
}
|
|
621
|
-
|
|
709
|
+
// Return only deployment events - PM2 native logs are served separately via /pm2-logs
|
|
710
|
+
// Return just the historically preserved unified log file
|
|
711
|
+
// Realtime events are handled via PM2 Event bus in app.js and broadcasted automatically
|
|
622
712
|
res.send(logs);
|
|
623
713
|
});
|
|
624
714
|
|
|
@@ -698,6 +788,17 @@ router.post('/webhook', async (req, res) => {
|
|
|
698
788
|
for (const app of appsToDeploy) {
|
|
699
789
|
try {
|
|
700
790
|
await notificationService.notifyAll(app.name, 'Push event received, triggering auto-sync...');
|
|
791
|
+
|
|
792
|
+
// Append push event to Event Log history
|
|
793
|
+
const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
|
|
794
|
+
const pushLogString = `[pm2me] Auto-Sync triggered via GitHub Webhook (Branch: ${branch})\n`;
|
|
795
|
+
if (fs.existsSync(logFilePath)) {
|
|
796
|
+
fs.appendFileSync(logFilePath, pushLogString);
|
|
797
|
+
}
|
|
798
|
+
if (req.io) {
|
|
799
|
+
req.io.emit(`deploy-log-${app.id}`, pushLogString.trim());
|
|
800
|
+
}
|
|
801
|
+
|
|
701
802
|
// Trigger deployment directly
|
|
702
803
|
performDeployment(app.id, req.io).catch(err => {
|
|
703
804
|
console.error(`Auto-Sync failed for ${app.name}:`, err);
|
|
@@ -753,12 +854,15 @@ router.get('/nginx/info', async (req, res) => {
|
|
|
753
854
|
|
|
754
855
|
// Check if nginx binary exists / is installed
|
|
755
856
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
857
|
+
if (isWindows) {
|
|
858
|
+
const { stderr } = await execFileAsync(NGINX_BIN, ['-v'], { windowsHide: true });
|
|
859
|
+
const match = (stderr || '').match(/nginx\/([\d.]+)/);
|
|
860
|
+
info.version = match ? match[1] : 'unknown';
|
|
861
|
+
} else {
|
|
862
|
+
const { stderr } = await execAsync(`${NGINX_BIN} -v`, { windowsHide: true });
|
|
863
|
+
const match = (stderr || '').match(/nginx\/([\d.]+)/);
|
|
864
|
+
info.version = match ? match[1] : 'unknown';
|
|
865
|
+
}
|
|
762
866
|
info.installed = true;
|
|
763
867
|
} catch {
|
|
764
868
|
info.installed = false;
|
|
@@ -776,10 +880,10 @@ router.get('/nginx/status', async (req, res) => {
|
|
|
776
880
|
let running = false;
|
|
777
881
|
try {
|
|
778
882
|
if (isWindows) {
|
|
779
|
-
const { stdout } = await
|
|
883
|
+
const { stdout } = await execFileAsync('tasklist.exe', ['/FI', 'IMAGENAME eq nginx.exe', '/NH'], { windowsHide: true });
|
|
780
884
|
running = stdout.toLowerCase().includes('nginx.exe');
|
|
781
885
|
} else {
|
|
782
|
-
const { stdout } = await execAsync('pgrep -x nginx');
|
|
886
|
+
const { stdout } = await execAsync('pgrep -x nginx', { windowsHide: true });
|
|
783
887
|
running = stdout.trim().length > 0;
|
|
784
888
|
}
|
|
785
889
|
} catch {
|
|
@@ -867,7 +971,7 @@ router.post('/nginx/enable', async (req, res) => {
|
|
|
867
971
|
try {
|
|
868
972
|
const { filePath, name } = req.body;
|
|
869
973
|
const enabledPath = `/etc/nginx/sites-enabled/${name}`;
|
|
870
|
-
if (!fs.existsSync(enabledPath)) await execAsync(`sudo ln -s "${filePath}" "${enabledPath}"
|
|
974
|
+
if (!fs.existsSync(enabledPath)) await execAsync(`sudo ln -s "${filePath}" "${enabledPath}"`, { windowsHide: true });
|
|
871
975
|
res.json({ success: true });
|
|
872
976
|
} catch (err) {
|
|
873
977
|
res.status(500).json({ error: err.message });
|
|
@@ -880,7 +984,7 @@ router.post('/nginx/disable', async (req, res) => {
|
|
|
880
984
|
try {
|
|
881
985
|
const { name } = req.body;
|
|
882
986
|
const enabledPath = `/etc/nginx/sites-enabled/${name}`;
|
|
883
|
-
if (fs.existsSync(enabledPath)) await execAsync(`sudo rm "${enabledPath}"
|
|
987
|
+
if (fs.existsSync(enabledPath)) await execAsync(`sudo rm "${enabledPath}"`, { windowsHide: true });
|
|
884
988
|
res.json({ success: true });
|
|
885
989
|
} catch (err) {
|
|
886
990
|
res.status(500).json({ error: err.message });
|
|
@@ -925,7 +1029,7 @@ router.post('/nginx/action', async (req, res) => {
|
|
|
925
1029
|
|
|
926
1030
|
try {
|
|
927
1031
|
let cmd;
|
|
928
|
-
const nginxCwd = isWindows ?
|
|
1032
|
+
const nginxCwd = isWindows ? path.dirname(NGINX_BIN) : undefined;
|
|
929
1033
|
|
|
930
1034
|
if (isWindows) {
|
|
931
1035
|
const bin = `"${NGINX_BIN}"`;
|
|
@@ -948,7 +1052,7 @@ router.post('/nginx/action', async (req, res) => {
|
|
|
948
1052
|
cmd = cmdMap[action];
|
|
949
1053
|
}
|
|
950
1054
|
|
|
951
|
-
const { stdout, stderr } = await execAsync(cmd, { cwd: nginxCwd }).catch(err => ({
|
|
1055
|
+
const { stdout, stderr } = await execAsync(cmd, { cwd: nginxCwd, windowsHide: true }).catch(err => ({
|
|
952
1056
|
stdout: err.stdout || '',
|
|
953
1057
|
stderr: err.stderr || err.message,
|
|
954
1058
|
}));
|
|
@@ -993,7 +1097,7 @@ router.get('/setup/info', (req, res) => {
|
|
|
993
1097
|
const pathPresets = serverIsWindows
|
|
994
1098
|
? ['C:\\pm2me\\apps', 'C:\\Users\\apps', 'D:\\pm2me\\apps']
|
|
995
1099
|
: ['/opt/pm2me/apps', '/home/apps', '/var/pm2me/apps'];
|
|
996
|
-
|
|
1100
|
+
|
|
997
1101
|
// Read version from package.json
|
|
998
1102
|
let version = 'unknown';
|
|
999
1103
|
try {
|
|
@@ -1001,7 +1105,7 @@ router.get('/setup/info', (req, res) => {
|
|
|
1001
1105
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1002
1106
|
version = pkg.version || 'unknown';
|
|
1003
1107
|
} catch { }
|
|
1004
|
-
|
|
1108
|
+
|
|
1005
1109
|
res.json({
|
|
1006
1110
|
os: serverIsWindows ? 'windows' : 'linux',
|
|
1007
1111
|
isWindows: serverIsWindows,
|
|
@@ -1020,21 +1124,21 @@ router.post('/setup/complete', async (req, res) => {
|
|
|
1020
1124
|
}
|
|
1021
1125
|
// Hash with bcrypt (same as auth.js uses for verification)
|
|
1022
1126
|
const passwordHash = await bcrypt.hash(adminPassword, 10);
|
|
1023
|
-
|
|
1127
|
+
|
|
1024
1128
|
// Update db data
|
|
1025
1129
|
db.data.admin = { passwordHash };
|
|
1026
1130
|
db.data.settings = { ...db.data.settings, appsPath };
|
|
1027
1131
|
db.data.setupComplete = true;
|
|
1028
|
-
|
|
1132
|
+
|
|
1029
1133
|
// Force write to database
|
|
1030
1134
|
await db.write();
|
|
1031
|
-
|
|
1135
|
+
|
|
1032
1136
|
// Verify the data was written by reading it back
|
|
1033
1137
|
await db.read();
|
|
1034
|
-
|
|
1138
|
+
|
|
1035
1139
|
// Ensure apps dir exists
|
|
1036
1140
|
fs.mkdirSync(appsPath, { recursive: true });
|
|
1037
|
-
|
|
1141
|
+
|
|
1038
1142
|
console.log('Setup completed successfully. Database saved to:', db.data);
|
|
1039
1143
|
res.json({ success: true });
|
|
1040
1144
|
} catch (err) {
|
|
@@ -1051,218 +1155,50 @@ router.get('/system/version-check', async (req, res) => {
|
|
|
1051
1155
|
try {
|
|
1052
1156
|
// Read local version from root package.json (two levels up from backend/routes/)
|
|
1053
1157
|
let localVersion = 'unknown';
|
|
1054
|
-
let packageName = '@drocketxx/pm2me';
|
|
1055
1158
|
try {
|
|
1056
1159
|
const pkgPath = path.resolve(__dirname, '../../package.json');
|
|
1057
1160
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1058
1161
|
localVersion = pkg.version || 'unknown';
|
|
1059
|
-
packageName = pkg.name || packageName;
|
|
1060
1162
|
} catch { }
|
|
1061
1163
|
|
|
1062
|
-
// Fetch latest
|
|
1164
|
+
// Fetch latest release from GitHub
|
|
1063
1165
|
let latest = null, releaseUrl = null, changelog = '';
|
|
1064
1166
|
try {
|
|
1065
|
-
const response = await fetch(
|
|
1167
|
+
const response = await fetch('https://api.github.com/repos/drocketxx/PM2Me/releases/latest', {
|
|
1066
1168
|
headers: { 'User-Agent': 'pm2me-updater' }
|
|
1067
1169
|
});
|
|
1068
1170
|
if (response.ok) {
|
|
1069
1171
|
const data = await response.json();
|
|
1070
|
-
latest = data.
|
|
1071
|
-
releaseUrl = data.
|
|
1072
|
-
changelog = data.
|
|
1172
|
+
latest = data.tag_name?.replace(/^v/, '') || null;
|
|
1173
|
+
releaseUrl = data.html_url || null;
|
|
1174
|
+
changelog = data.body || '';
|
|
1073
1175
|
}
|
|
1074
|
-
} catch
|
|
1075
|
-
console.error('Failed to fetch npm version:', err);
|
|
1076
|
-
}
|
|
1176
|
+
} catch { }
|
|
1077
1177
|
|
|
1078
|
-
const hasUpdate = latest && localVersion !== 'unknown' && latest !== localVersion
|
|
1079
|
-
latest.localeCompare(localVersion, undefined, { numeric: true, sensitivity: 'base' }) > 0;
|
|
1178
|
+
const hasUpdate = latest && localVersion !== 'unknown' && latest !== localVersion;
|
|
1080
1179
|
res.json({ current: localVersion, latest, hasUpdate, releaseUrl, changelog });
|
|
1081
1180
|
} catch (err) {
|
|
1082
1181
|
res.status(500).json({ error: err.message });
|
|
1083
1182
|
}
|
|
1084
1183
|
});
|
|
1085
1184
|
|
|
1086
|
-
// GET update log
|
|
1087
|
-
router.get('/system/update-log', async (req, res) => {
|
|
1088
|
-
try {
|
|
1089
|
-
const logPath = isWindows ? path.join(os.tmpdir(), 'pm2me-update.log') : '/tmp/pm2me-update.log';
|
|
1090
|
-
if (!fs.existsSync(logPath)) {
|
|
1091
|
-
return res.json({ exists: false, content: '' });
|
|
1092
|
-
}
|
|
1093
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
1094
|
-
res.json({ exists: true, content });
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
res.status(500).json({ error: err.message });
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
1185
|
router.post('/system/update', async (req, res) => {
|
|
1101
1186
|
try {
|
|
1102
1187
|
// Global npm install if installed globally, otherwise git pull + rebuild
|
|
1103
1188
|
const isGlobal = !fs.existsSync(path.resolve(__dirname, '../../.git'));
|
|
1104
1189
|
let cmd, cwd;
|
|
1105
1190
|
if (isGlobal) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// Windows: Create PowerShell script and execute with Start-Process
|
|
1109
|
-
const scriptPath = path.join(os.tmpdir(), 'pm2me-update.ps1');
|
|
1110
|
-
const logPath = path.join(os.tmpdir(), 'pm2me-update.log');
|
|
1111
|
-
const scriptContent = `$LogPath = "${logPath}"
|
|
1112
|
-
function Log($msg) {
|
|
1113
|
-
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
1114
|
-
"[$timestamp] $msg" | Out-File -FilePath $LogPath -Append
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
Log "PM2Me Update Script Started"
|
|
1118
|
-
Start-Sleep -Seconds 3
|
|
1119
|
-
|
|
1120
|
-
Log "Cleaning npm cache..."
|
|
1121
|
-
npm cache clean --force 2>&1 | Out-File -FilePath $LogPath -Append
|
|
1122
|
-
|
|
1123
|
-
Log "Installing latest version from npm..."
|
|
1124
|
-
$installResult = npm install -g @drocketxx/pm2me@latest --force 2>&1
|
|
1125
|
-
$installResult | Out-File -FilePath $LogPath -Append
|
|
1126
|
-
|
|
1127
|
-
if ($LASTEXITCODE -eq 0) {
|
|
1128
|
-
Log "Successfully installed latest version"
|
|
1129
|
-
} else {
|
|
1130
|
-
Log "ERROR: npm install failed"
|
|
1131
|
-
exit 1
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
Log "Uninstalling old service..."
|
|
1135
|
-
pm2me service uninstall 2>&1 | Out-File -FilePath $LogPath -Append
|
|
1136
|
-
|
|
1137
|
-
Start-Sleep -Seconds 3
|
|
1138
|
-
|
|
1139
|
-
Log "Installing new service..."
|
|
1140
|
-
$serviceResult = pm2me service install 2>&1
|
|
1141
|
-
$serviceResult | Out-File -FilePath $LogPath -Append
|
|
1142
|
-
|
|
1143
|
-
if ($LASTEXITCODE -eq 0) {
|
|
1144
|
-
Log "Service installed successfully"
|
|
1145
|
-
Log "Update completed successfully!"
|
|
1146
|
-
} else {
|
|
1147
|
-
Log "ERROR: Service install failed"
|
|
1148
|
-
exit 1
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
Remove-Item -Path "${scriptPath}" -Force`;
|
|
1152
|
-
fs.writeFileSync(scriptPath, scriptContent);
|
|
1153
|
-
cmd = `powershell -Command "Start-Process -FilePath 'powershell' -ArgumentList '-ExecutionPolicy','Bypass','-File','${scriptPath}' -WindowStyle Hidden"`;
|
|
1154
|
-
|
|
1155
|
-
exec(cmd, { windowsHide: true });
|
|
1156
|
-
|
|
1157
|
-
res.json({
|
|
1158
|
-
success: true,
|
|
1159
|
-
output: `Update scheduled. Service will restart in ~30 seconds.\nLog: ${logPath}`,
|
|
1160
|
-
cmd
|
|
1161
|
-
});
|
|
1162
|
-
} else {
|
|
1163
|
-
// Linux: Get full paths to npm, pm2me, and node before running detached
|
|
1164
|
-
let npmPath, pm2mePath, nodePath, binDir;
|
|
1165
|
-
try {
|
|
1166
|
-
const npmResult = await execAsync('which npm');
|
|
1167
|
-
npmPath = npmResult.stdout.trim();
|
|
1168
|
-
binDir = path.dirname(npmPath); // e.g., /root/.nvm/versions/node/v22.21.1/bin
|
|
1169
|
-
} catch {
|
|
1170
|
-
npmPath = '/usr/bin/npm';
|
|
1171
|
-
binDir = '/usr/bin';
|
|
1172
|
-
}
|
|
1173
|
-
try {
|
|
1174
|
-
const pm2meResult = await execAsync('which pm2me');
|
|
1175
|
-
pm2mePath = pm2meResult.stdout.trim();
|
|
1176
|
-
} catch {
|
|
1177
|
-
pm2mePath = 'pm2me';
|
|
1178
|
-
}
|
|
1179
|
-
try {
|
|
1180
|
-
const nodeResult = await execAsync('which node');
|
|
1181
|
-
nodePath = nodeResult.stdout.trim();
|
|
1182
|
-
} catch {
|
|
1183
|
-
nodePath = path.join(binDir, 'node'); // Same dir as npm
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
const scriptPath = '/tmp/pm2me-update.sh';
|
|
1187
|
-
const logPath = '/tmp/pm2me-update.log';
|
|
1188
|
-
const scriptContent = `#!/bin/bash
|
|
1189
|
-
# Add Node.js bin directory to PATH
|
|
1190
|
-
export PATH="${binDir}:$PATH"
|
|
1191
|
-
|
|
1192
|
-
exec > ${logPath} 2>&1
|
|
1193
|
-
echo "[$(date)] PM2Me Update Script Started"
|
|
1194
|
-
echo "[$(date)] PATH: $PATH"
|
|
1195
|
-
echo "[$(date)] Using node: ${nodePath}"
|
|
1196
|
-
echo "[$(date)] Using npm: ${npmPath}"
|
|
1197
|
-
echo "[$(date)] Using pm2me: ${pm2mePath}"
|
|
1198
|
-
sleep 3
|
|
1199
|
-
|
|
1200
|
-
echo "[$(date)] Cleaning npm cache..."
|
|
1201
|
-
${npmPath} cache clean --force || echo "Cache clean failed (non-critical)"
|
|
1202
|
-
|
|
1203
|
-
echo "[$(date)] Installing latest version from npm..."
|
|
1204
|
-
if ${npmPath} install -g @drocketxx/pm2me@latest --force; then
|
|
1205
|
-
echo "[$(date)] Successfully installed latest version"
|
|
1206
|
-
else
|
|
1207
|
-
echo "[$(date)] ERROR: npm install failed - trying with sudo..."
|
|
1208
|
-
if sudo -n ${npmPath} install -g @drocketxx/pm2me@latest --force 2>/dev/null; then
|
|
1209
|
-
echo "[$(date)] Successfully installed with sudo"
|
|
1210
|
-
else
|
|
1211
|
-
echo "[$(date)] FATAL: npm install failed. Update aborted."
|
|
1212
|
-
exit 1
|
|
1213
|
-
fi
|
|
1214
|
-
fi
|
|
1215
|
-
|
|
1216
|
-
echo "[$(date)] Uninstalling old service..."
|
|
1217
|
-
${pm2mePath} service uninstall || echo "Service uninstall failed (might not exist)"
|
|
1218
|
-
|
|
1219
|
-
sleep 3
|
|
1220
|
-
|
|
1221
|
-
echo "[$(date)] Installing new service..."
|
|
1222
|
-
if ${pm2mePath} service install; then
|
|
1223
|
-
echo "[$(date)] Service installed successfully"
|
|
1224
|
-
else
|
|
1225
|
-
echo "[$(date)] ERROR: Service install failed"
|
|
1226
|
-
exit 1
|
|
1227
|
-
fi
|
|
1228
|
-
|
|
1229
|
-
echo "[$(date)] Update completed successfully!"
|
|
1230
|
-
rm -f ${scriptPath}`;
|
|
1231
|
-
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
1232
|
-
|
|
1233
|
-
// Try to use systemd-run first (most reliable), fallback to at, then to background process
|
|
1234
|
-
try {
|
|
1235
|
-
// Check if systemd-run is available
|
|
1236
|
-
await execAsync('which systemd-run');
|
|
1237
|
-
cmd = `systemd-run --user --on-active=3s ${scriptPath}`;
|
|
1238
|
-
} catch {
|
|
1239
|
-
try {
|
|
1240
|
-
// Check if at is available
|
|
1241
|
-
await execAsync('which at');
|
|
1242
|
-
cmd = `echo "${scriptPath}" | at now + 3 seconds 2>&1`;
|
|
1243
|
-
} catch {
|
|
1244
|
-
// Fallback to background process with setsid (detach from session)
|
|
1245
|
-
cmd = `(sleep 3 && ${scriptPath}) & disown`;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
exec(cmd, { shell: '/bin/bash', detached: true });
|
|
1250
|
-
|
|
1251
|
-
res.json({
|
|
1252
|
-
success: true,
|
|
1253
|
-
output: `Update scheduled. Service will restart in ~30 seconds.\nBin Dir: ${binDir}\nNode: ${nodePath}\nNpm: ${npmPath}\nPm2me: ${pm2mePath}\nLog: ${logPath}`,
|
|
1254
|
-
cmd
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1191
|
+
cmd = 'npm install -g pm2me@latest';
|
|
1192
|
+
cwd = '/';
|
|
1257
1193
|
} else {
|
|
1258
1194
|
cmd = 'git pull origin main && npm run build';
|
|
1259
1195
|
cwd = path.resolve(__dirname, '../..');
|
|
1260
|
-
const { stdout, stderr } = await execAsync(cmd, { cwd, windowsHide: true }).catch(err => ({
|
|
1261
|
-
stdout: '', stderr: err.message
|
|
1262
|
-
}));
|
|
1263
|
-
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
1264
|
-
res.json({ success: !stderr.includes('error'), output, cmd });
|
|
1265
1196
|
}
|
|
1197
|
+
const { stdout, stderr } = await execAsync(cmd, { cwd, windowsHide: true }).catch(err => ({
|
|
1198
|
+
stdout: '', stderr: err.message
|
|
1199
|
+
}));
|
|
1200
|
+
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
1201
|
+
res.json({ success: !stderr.includes('error'), output, cmd });
|
|
1266
1202
|
} catch (err) {
|
|
1267
1203
|
res.status(500).json({ error: err.message });
|
|
1268
1204
|
}
|