ezpm2gui 1.0.0 → 1.2.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 +202 -14
- package/bin/ezpm2gui.js +40 -44
- package/bin/ezpm2gui.ts +51 -0
- package/bin/generate-ecosystem.js +24 -22
- package/bin/generate-ecosystem.ts +56 -0
- package/dist/server/config/project-configs.json +236 -0
- package/dist/server/index.js +64 -19
- package/dist/server/logs/deployment.log +12 -0
- package/dist/server/routes/clusterManagement.d.ts +3 -0
- package/dist/server/routes/clusterManagement.js +152 -0
- package/dist/server/routes/deployApplication.d.ts +3 -0
- package/dist/server/routes/deployApplication.js +310 -0
- package/dist/server/routes/logStreaming.d.ts +5 -0
- package/dist/server/routes/logStreaming.js +276 -0
- package/dist/server/routes/modules.d.ts +3 -0
- package/dist/server/routes/modules.js +106 -0
- package/dist/server/routes/processConfig.d.ts +3 -0
- package/dist/server/routes/processConfig.js +118 -0
- package/dist/server/routes/remoteConnections.d.ts +3 -0
- package/dist/server/routes/remoteConnections.js +634 -0
- package/dist/server/services/ProjectSetupService.d.ts +72 -0
- package/dist/server/services/ProjectSetupService.js +327 -0
- package/dist/server/utils/dialog.d.ts +1 -0
- package/dist/server/utils/dialog.js +16 -0
- package/dist/server/utils/encryption.d.ts +12 -0
- package/dist/server/utils/encryption.js +72 -0
- package/dist/server/utils/pm2-connection.d.ts +16 -0
- package/dist/server/utils/pm2-connection.js +141 -0
- package/dist/server/utils/remote-connection.d.ts +152 -0
- package/dist/server/utils/remote-connection.js +590 -0
- package/dist/server/utils/upload.d.ts +3 -0
- package/dist/server/utils/upload.js +39 -0
- package/package.json +65 -49
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/favicon.ico +2 -0
- package/src/client/build/index.html +1 -1
- package/src/client/build/logo192.svg +7 -0
- package/src/client/build/logo512.svg +7 -0
- package/src/client/build/manifest.json +5 -6
- package/src/client/build/static/css/main.672b8f26.css +2 -0
- package/src/client/build/static/css/main.672b8f26.css.map +1 -0
- package/src/client/build/static/js/main.31323a04.js +156 -0
- package/src/client/build/static/js/{main.dde30e92.js.LICENSE.txt → main.31323a04.js.LICENSE.txt} +19 -0
- package/src/client/build/static/js/main.31323a04.js.map +1 -0
- package/ .npmignore +0 -39
- package/install.bat +0 -22
- package/install.sh +0 -23
- package/src/client/build/static/css/main.c1cbda3a.css +0 -2
- package/src/client/build/static/css/main.c1cbda3a.css.map +0 -1
- package/src/client/build/static/js/main.dde30e92.js +0 -3
- package/src/client/build/static/js/main.dde30e92.js.map +0 -1
- package/src/client/package-lock.json +0 -16192
- package/src/client/package.json +0 -39
- package/src/client/public/index.html +0 -20
- package/src/client/public/manifest.json +0 -25
- package/src/index.ts +0 -24
- package/src/server/index.ts +0 -240
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = require("express");
|
|
7
|
+
const pm2_1 = __importDefault(require("pm2"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const ProjectSetupService_1 = require("../services/ProjectSetupService");
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
// Deploy a new application
|
|
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;
|
|
15
|
+
// Validate required fields
|
|
16
|
+
if (!name || !script) {
|
|
17
|
+
return res.status(400).json({ error: 'Name and script path are required' });
|
|
18
|
+
}
|
|
19
|
+
// Validate script path exists
|
|
20
|
+
if (!fs_1.default.existsSync(script)) {
|
|
21
|
+
return res.status(400).json({ error: `Script file not found: ${script}` });
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const projectPath = cwd || path_1.default.dirname(script);
|
|
25
|
+
let setupResult = null;
|
|
26
|
+
let finalEnv = env || {};
|
|
27
|
+
let interpreterPath = '';
|
|
28
|
+
// Auto-detect project type if not provided
|
|
29
|
+
let detectedType = appType;
|
|
30
|
+
if (!detectedType) {
|
|
31
|
+
detectedType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
|
|
32
|
+
if (detectedType) {
|
|
33
|
+
console.log(`Auto-detected project type: ${detectedType}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Run project setup if auto-setup is enabled and project type is detected
|
|
37
|
+
if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
|
|
38
|
+
console.log(`Running setup for ${detectedType} project...`);
|
|
39
|
+
try {
|
|
40
|
+
setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType);
|
|
41
|
+
if (!setupResult.success) {
|
|
42
|
+
return res.status(500).json({
|
|
43
|
+
error: 'Project setup failed',
|
|
44
|
+
details: setupResult.errors,
|
|
45
|
+
warnings: setupResult.warnings,
|
|
46
|
+
steps: setupResult.steps
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Merge environment variables from setup
|
|
50
|
+
finalEnv = { ...setupResult.environment, ...finalEnv };
|
|
51
|
+
// Set interpreter path for Python projects
|
|
52
|
+
if (setupResult.interpreterPath) {
|
|
53
|
+
interpreterPath = setupResult.interpreterPath;
|
|
54
|
+
}
|
|
55
|
+
console.log('Project setup completed successfully');
|
|
56
|
+
}
|
|
57
|
+
catch (setupError) {
|
|
58
|
+
console.error('Setup error:', setupError);
|
|
59
|
+
return res.status(500).json({
|
|
60
|
+
error: 'Project setup failed',
|
|
61
|
+
details: setupError instanceof Error ? setupError.message : 'Unknown setup error'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Create deployment configuration
|
|
66
|
+
const appConfig = {
|
|
67
|
+
name,
|
|
68
|
+
script,
|
|
69
|
+
cwd: projectPath,
|
|
70
|
+
instances: parseInt(instances) || 1,
|
|
71
|
+
exec_mode: exec_mode || 'fork',
|
|
72
|
+
autorestart: autorestart !== undefined ? autorestart : true,
|
|
73
|
+
watch: watch || false,
|
|
74
|
+
max_memory_restart: max_memory_restart || '150M',
|
|
75
|
+
env: finalEnv
|
|
76
|
+
};
|
|
77
|
+
// Set interpreter for Python projects
|
|
78
|
+
if (detectedType === 'python' && interpreterPath) {
|
|
79
|
+
appConfig.interpreter = interpreterPath;
|
|
80
|
+
}
|
|
81
|
+
else if (detectedType === 'dotnet') {
|
|
82
|
+
appConfig.interpreter = 'dotnet';
|
|
83
|
+
// For .NET projects, update script to point to the published DLL if available
|
|
84
|
+
const publishedDll = path_1.default.join(projectPath, 'publish', `${path_1.default.basename(projectPath)}.dll`);
|
|
85
|
+
if (fs_1.default.existsSync(publishedDll)) {
|
|
86
|
+
appConfig.script = publishedDll;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pm2_1.default.connect((err) => {
|
|
90
|
+
if (err) {
|
|
91
|
+
console.error(err);
|
|
92
|
+
return res.status(500).json({ error: 'Failed to connect to PM2' });
|
|
93
|
+
}
|
|
94
|
+
pm2_1.default.start(appConfig, (err) => {
|
|
95
|
+
pm2_1.default.disconnect();
|
|
96
|
+
if (err) {
|
|
97
|
+
console.error('PM2 start error:', err);
|
|
98
|
+
return res.status(500).json({
|
|
99
|
+
error: `Failed to deploy application: ${err.message || 'Unknown error'}`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
res.json({
|
|
103
|
+
success: true,
|
|
104
|
+
message: `Application ${name} deployed successfully`,
|
|
105
|
+
setupResult: setupResult ? {
|
|
106
|
+
steps: setupResult.steps,
|
|
107
|
+
warnings: setupResult.warnings
|
|
108
|
+
} : null
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error('Deployment error:', error);
|
|
115
|
+
return res.status(500).json({
|
|
116
|
+
error: 'Deployment failed',
|
|
117
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Generate ecosystem.config.js file
|
|
122
|
+
router.post('/generate-ecosystem', (req, res) => {
|
|
123
|
+
pm2_1.default.connect(async (err) => {
|
|
124
|
+
if (err) {
|
|
125
|
+
console.error(err);
|
|
126
|
+
return res.status(500).json({ error: 'Failed to connect to PM2' });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
// Get all processes
|
|
130
|
+
const listPromise = new Promise((resolve, reject) => {
|
|
131
|
+
pm2_1.default.list((err, processList) => {
|
|
132
|
+
if (err) {
|
|
133
|
+
reject(err);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
resolve(processList);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
const processList = await listPromise;
|
|
140
|
+
// Filter processes if needed
|
|
141
|
+
let filteredProcesses = processList;
|
|
142
|
+
if (req.body.includeAllProcesses === false) {
|
|
143
|
+
filteredProcesses = processList.filter(proc => proc.pm2_env.status === 'online');
|
|
144
|
+
}
|
|
145
|
+
// Create ecosystem config
|
|
146
|
+
const apps = filteredProcesses.map(proc => {
|
|
147
|
+
const pm2Env = proc.pm2_env;
|
|
148
|
+
return {
|
|
149
|
+
name: proc.name,
|
|
150
|
+
script: pm2Env.pm_exec_path,
|
|
151
|
+
cwd: pm2Env.pm_cwd,
|
|
152
|
+
instances: pm2Env.instances || 1,
|
|
153
|
+
exec_mode: pm2Env.exec_mode === 'cluster_mode' ? 'cluster' : 'fork',
|
|
154
|
+
autorestart: pm2Env.autorestart,
|
|
155
|
+
watch: pm2Env.watch,
|
|
156
|
+
ignore_watch: pm2Env.ignore_watch || [],
|
|
157
|
+
max_memory_restart: pm2Env.max_memory_restart || '150M',
|
|
158
|
+
env: pm2Env.env || {}
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const ecosystemConfig = `module.exports = {
|
|
162
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
163
|
+
};`;
|
|
164
|
+
// Create the file (either at specified path or default location)
|
|
165
|
+
const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
|
|
166
|
+
fs_1.default.writeFileSync(filePath, ecosystemConfig);
|
|
167
|
+
pm2_1.default.disconnect();
|
|
168
|
+
res.json({
|
|
169
|
+
success: true,
|
|
170
|
+
message: 'Ecosystem file generated successfully',
|
|
171
|
+
path: filePath
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
pm2_1.default.disconnect();
|
|
176
|
+
res.status(500).json({ error: error.message || 'Failed to generate ecosystem file' });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// Preview ecosystem.config.js content
|
|
181
|
+
router.get('/generate-ecosystem-preview', (req, res) => {
|
|
182
|
+
pm2_1.default.connect(async (err) => {
|
|
183
|
+
if (err) {
|
|
184
|
+
console.error(err);
|
|
185
|
+
return res.status(500).json({ error: 'Failed to connect to PM2' });
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
// Get all processes
|
|
189
|
+
const listPromise = new Promise((resolve, reject) => {
|
|
190
|
+
pm2_1.default.list((err, processList) => {
|
|
191
|
+
if (err) {
|
|
192
|
+
reject(err);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
resolve(processList);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
const processList = await listPromise;
|
|
199
|
+
// Create ecosystem config
|
|
200
|
+
const apps = processList.map(proc => {
|
|
201
|
+
const pm2Env = proc.pm2_env;
|
|
202
|
+
return {
|
|
203
|
+
name: proc.name,
|
|
204
|
+
script: pm2Env.pm_exec_path,
|
|
205
|
+
cwd: pm2Env.pm_cwd,
|
|
206
|
+
instances: pm2Env.instances || 1,
|
|
207
|
+
exec_mode: pm2Env.exec_mode === 'cluster_mode' ? 'cluster' : 'fork',
|
|
208
|
+
autorestart: pm2Env.autorestart,
|
|
209
|
+
watch: pm2Env.watch,
|
|
210
|
+
ignore_watch: pm2Env.ignore_watch || [],
|
|
211
|
+
max_memory_restart: pm2Env.max_memory_restart || '150M',
|
|
212
|
+
env: pm2Env.env || {}
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
const ecosystemConfig = `module.exports = {
|
|
216
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
217
|
+
};`;
|
|
218
|
+
pm2_1.default.disconnect();
|
|
219
|
+
res.json({
|
|
220
|
+
success: true,
|
|
221
|
+
content: ecosystemConfig
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
pm2_1.default.disconnect();
|
|
226
|
+
res.status(500).json({ error: error.message || 'Failed to generate ecosystem preview' });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
// Detect project type
|
|
231
|
+
router.post('/detect-project', (req, res) => {
|
|
232
|
+
const { projectPath } = req.body;
|
|
233
|
+
if (!projectPath) {
|
|
234
|
+
return res.status(400).json({ error: 'Project path is required' });
|
|
235
|
+
}
|
|
236
|
+
if (!fs_1.default.existsSync(projectPath)) {
|
|
237
|
+
return res.status(400).json({ error: 'Project path does not exist' });
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const projectType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
|
|
241
|
+
const config = projectType ? ProjectSetupService_1.projectSetupService.getProjectConfig(projectType) : null;
|
|
242
|
+
res.json({
|
|
243
|
+
success: true,
|
|
244
|
+
projectType,
|
|
245
|
+
config: config ? {
|
|
246
|
+
name: config.name,
|
|
247
|
+
defaultConfig: config.defaultConfig,
|
|
248
|
+
setupSteps: config.setup.steps.length
|
|
249
|
+
} : null
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
res.status(500).json({
|
|
254
|
+
error: 'Failed to detect project type',
|
|
255
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
// Setup project (without deployment)
|
|
260
|
+
router.post('/setup-project', async (req, res) => {
|
|
261
|
+
const { projectPath, projectType } = req.body;
|
|
262
|
+
if (!projectPath || !projectType) {
|
|
263
|
+
return res.status(400).json({ error: 'Project path and type are required' });
|
|
264
|
+
}
|
|
265
|
+
if (!fs_1.default.existsSync(projectPath)) {
|
|
266
|
+
return res.status(400).json({ error: 'Project path does not exist' });
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, projectType);
|
|
270
|
+
res.json({
|
|
271
|
+
success: setupResult.success,
|
|
272
|
+
steps: setupResult.steps,
|
|
273
|
+
errors: setupResult.errors,
|
|
274
|
+
warnings: setupResult.warnings,
|
|
275
|
+
environment: setupResult.environment,
|
|
276
|
+
interpreterPath: setupResult.interpreterPath
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
res.status(500).json({
|
|
281
|
+
error: 'Project setup failed',
|
|
282
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
// Get supported project types
|
|
287
|
+
router.get('/project-types', (req, res) => {
|
|
288
|
+
try {
|
|
289
|
+
const types = ProjectSetupService_1.projectSetupService.getSupportedProjectTypes();
|
|
290
|
+
const typeConfigs = types.map(type => {
|
|
291
|
+
const config = ProjectSetupService_1.projectSetupService.getProjectConfig(type);
|
|
292
|
+
return {
|
|
293
|
+
type,
|
|
294
|
+
name: config === null || config === void 0 ? void 0 : config.name,
|
|
295
|
+
defaultConfig: config === null || config === void 0 ? void 0 : config.defaultConfig
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
res.json({
|
|
299
|
+
success: true,
|
|
300
|
+
types: typeConfigs
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
res.status(500).json({
|
|
305
|
+
error: 'Failed to get project types',
|
|
306
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
exports.default = router;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setupLogStreaming = void 0;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const pm2_1 = __importDefault(require("pm2"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const remote_connection_1 = require("../utils/remote-connection");
|
|
12
|
+
const router = (0, express_1.Router)();
|
|
13
|
+
// This variable will hold references to active log streams
|
|
14
|
+
const activeStreams = {};
|
|
15
|
+
const activeRemoteStreams = {};
|
|
16
|
+
// Get active stream (or create a new one)
|
|
17
|
+
const getLogStream = (io, processId, logType) => {
|
|
18
|
+
const streamKey = `${processId}-${logType}`;
|
|
19
|
+
// If stream already exists, return it
|
|
20
|
+
if (activeStreams[streamKey]) {
|
|
21
|
+
return activeStreams[streamKey];
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
pm2_1.default.describe(processId, (err, processDesc) => {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
if (err || !processDesc || processDesc.length === 0) {
|
|
27
|
+
reject(new Error('Process not found'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const logPath = (_b = (_a = processDesc[0]) === null || _a === void 0 ? void 0 : _a.pm2_env) === null || _b === void 0 ? void 0 : _b[`pm_${logType}_log_path`];
|
|
31
|
+
if (!logPath || !fs_1.default.existsSync(logPath)) {
|
|
32
|
+
reject(new Error(`Log file not found: ${logPath}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Create a tail process to stream the log file
|
|
36
|
+
const tail = (0, child_process_1.spawn)('tail', ['-f', logPath]);
|
|
37
|
+
// Setup event handlers
|
|
38
|
+
tail.stdout.on('data', (data) => {
|
|
39
|
+
const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
|
|
40
|
+
lines.forEach((line) => {
|
|
41
|
+
// Emit log line to all connected clients
|
|
42
|
+
io.emit('log-line', {
|
|
43
|
+
processId,
|
|
44
|
+
logType,
|
|
45
|
+
line
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
tail.stderr.on('data', (data) => {
|
|
50
|
+
console.error(`Tail error: ${data}`);
|
|
51
|
+
});
|
|
52
|
+
tail.on('close', (code) => {
|
|
53
|
+
console.log(`Tail process exited with code ${code}`);
|
|
54
|
+
delete activeStreams[streamKey];
|
|
55
|
+
});
|
|
56
|
+
// Store the tail process
|
|
57
|
+
activeStreams[streamKey] = tail;
|
|
58
|
+
resolve(tail);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
// Get remote log stream (or create a new one)
|
|
63
|
+
const getRemoteLogStream = async (io, connectionId, processId) => {
|
|
64
|
+
const streamKey = `${connectionId}-${processId}`;
|
|
65
|
+
// If stream already exists, return it
|
|
66
|
+
if (activeRemoteStreams[streamKey]) {
|
|
67
|
+
return activeRemoteStreams[streamKey];
|
|
68
|
+
}
|
|
69
|
+
const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
|
|
70
|
+
if (!connection || !connection.isConnected()) {
|
|
71
|
+
throw new Error('Connection not found or not connected');
|
|
72
|
+
}
|
|
73
|
+
// Get process info to find log paths
|
|
74
|
+
console.log(`Getting process info for: ${processId}`);
|
|
75
|
+
const processInfoResult = await connection.executeCommand(`pm2 jlist`, false); // Don't use sudo for listing
|
|
76
|
+
if (processInfoResult.code !== 0) {
|
|
77
|
+
throw new Error(`Failed to get process list: ${processInfoResult.stderr}`);
|
|
78
|
+
}
|
|
79
|
+
let processInfo;
|
|
80
|
+
try {
|
|
81
|
+
const processList = JSON.parse(processInfoResult.stdout);
|
|
82
|
+
console.log(`Found ${processList.length} processes`);
|
|
83
|
+
// Find the process by ID
|
|
84
|
+
processInfo = processList.find((proc) => proc.pm_id === parseInt(processId));
|
|
85
|
+
if (!processInfo) {
|
|
86
|
+
throw new Error(`Process with ID ${processId} not found`);
|
|
87
|
+
}
|
|
88
|
+
console.log(`Found process: ${processInfo.name} (ID: ${processInfo.pm_id})`);
|
|
89
|
+
}
|
|
90
|
+
catch (parseError) {
|
|
91
|
+
console.error('Parse error:', parseError);
|
|
92
|
+
throw new Error(`Failed to parse process list: ${parseError}`);
|
|
93
|
+
}
|
|
94
|
+
const processName = processInfo.name;
|
|
95
|
+
// Create streams using pm2 logs instead of tail for better permission handling
|
|
96
|
+
const streams = {};
|
|
97
|
+
try {
|
|
98
|
+
console.log(`Setting up pm2 logs stream for process: ${processName} (ID: ${processId})`);
|
|
99
|
+
// Use pm2 logs with --lines 0 --raw to stream only new logs
|
|
100
|
+
// Use sudo since PM2 processes are running as root
|
|
101
|
+
const pm2LogsCommand = `pm2 logs ${processId} --lines 0 --raw`;
|
|
102
|
+
console.log(`About to create pm2 log stream with command: ${pm2LogsCommand} (using sudo)`);
|
|
103
|
+
const logStream = await connection.createLogStream(pm2LogsCommand, true);
|
|
104
|
+
console.log(`Successfully created pm2 logs stream for ${processName}`);
|
|
105
|
+
logStream.on('data', (data) => {
|
|
106
|
+
console.log(`Raw pm2 logs data received for ${processName}:`, data);
|
|
107
|
+
const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
|
|
108
|
+
lines.forEach((line) => {
|
|
109
|
+
// Skip PM2 startup messages, system messages, and errors
|
|
110
|
+
if (line.trim() === '' ||
|
|
111
|
+
line.includes('PM2') ||
|
|
112
|
+
line.includes('---') ||
|
|
113
|
+
line.includes('watching') ||
|
|
114
|
+
line.includes('change detected') ||
|
|
115
|
+
line.includes('Runtime Edition') ||
|
|
116
|
+
line.includes('Production Process Manager') ||
|
|
117
|
+
line.includes('built-in Load Balancer') ||
|
|
118
|
+
line.includes('$ pm2') ||
|
|
119
|
+
line.includes('http://pm2.io') ||
|
|
120
|
+
line.includes('Start and Daemonize') ||
|
|
121
|
+
line.includes('Load Balance') ||
|
|
122
|
+
line.includes('Monitor in production') ||
|
|
123
|
+
line.includes('Make pm2 auto-boot') ||
|
|
124
|
+
line.includes('To go further') ||
|
|
125
|
+
line.includes('ENOENT') ||
|
|
126
|
+
line.includes('module_conf.json') ||
|
|
127
|
+
line.includes('pm2.log') ||
|
|
128
|
+
line.includes('node:fs:') ||
|
|
129
|
+
line.includes('at Object.') ||
|
|
130
|
+
line.includes('at Client.') ||
|
|
131
|
+
line.includes('at processTicksAndRejections') ||
|
|
132
|
+
line.includes('errno:') ||
|
|
133
|
+
line.includes('syscall:') ||
|
|
134
|
+
line.includes('Node.js v') ||
|
|
135
|
+
line.startsWith(' at ') ||
|
|
136
|
+
line.match(/^\s*\^?\s*$/) ||
|
|
137
|
+
line.match(/^[\s_\/\\|]+$/)) {
|
|
138
|
+
console.log(`Skipping PM2 system/error line: "${line}"`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Parse PM2 log format: timestamp | app-name | message
|
|
142
|
+
// PM2 logs typically come in format like: "2023-01-01T12:00:00: PM2 log: [TAILING]"
|
|
143
|
+
// or just the raw log content depending on version
|
|
144
|
+
let logType = 'stdout'; // Default to stdout
|
|
145
|
+
let cleanLine = line;
|
|
146
|
+
// Try to detect if this is stderr (PM2 usually prefixes with different indicators)
|
|
147
|
+
if (line.includes('ERROR') || line.includes('error') || line.includes('stderr')) {
|
|
148
|
+
logType = 'stderr';
|
|
149
|
+
}
|
|
150
|
+
// Clean up the line by removing PM2 prefixes if present
|
|
151
|
+
// PM2 log format can vary, but often includes timestamps and app names
|
|
152
|
+
const logMatch = line.match(/^.*?\|\s*(.+)$/);
|
|
153
|
+
if (logMatch) {
|
|
154
|
+
cleanLine = logMatch[1];
|
|
155
|
+
}
|
|
156
|
+
console.log(`Emitting remote-log-line for ${connectionId}-${processId} (${logType}):`, cleanLine);
|
|
157
|
+
io.emit('remote-log-line', {
|
|
158
|
+
connectionId,
|
|
159
|
+
processId,
|
|
160
|
+
processName,
|
|
161
|
+
logType,
|
|
162
|
+
line: cleanLine
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
logStream.on('error', (error) => {
|
|
167
|
+
console.error(`Error in pm2 logs stream for ${processName}:`, error);
|
|
168
|
+
io.emit('remote-log-error', {
|
|
169
|
+
connectionId,
|
|
170
|
+
processId,
|
|
171
|
+
processName,
|
|
172
|
+
error: error.message
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
logStream.on('close', (code) => {
|
|
176
|
+
console.log(`PM2 logs stream closed for ${processName} with code:`, code);
|
|
177
|
+
});
|
|
178
|
+
streams.combined = logStream;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
console.error(`Failed to create pm2 logs stream for ${processName}:`, error);
|
|
182
|
+
io.emit('remote-log-error', {
|
|
183
|
+
connectionId,
|
|
184
|
+
processId,
|
|
185
|
+
processName,
|
|
186
|
+
error: `Failed to start log streaming: ${error}`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Store the streams
|
|
190
|
+
activeRemoteStreams[streamKey] = streams;
|
|
191
|
+
return streams;
|
|
192
|
+
};
|
|
193
|
+
// Setup socket.io handlers for log streaming
|
|
194
|
+
const setupLogStreaming = (io) => {
|
|
195
|
+
io.on('connection', (socket) => {
|
|
196
|
+
console.log('Client connected for log streaming');
|
|
197
|
+
// Subscribe to log stream
|
|
198
|
+
socket.on('subscribe-logs', async ({ processId, logType }) => {
|
|
199
|
+
try {
|
|
200
|
+
const streamKey = `${processId}-${logType}`;
|
|
201
|
+
// Add socket to a room for this specific log stream
|
|
202
|
+
socket.join(streamKey);
|
|
203
|
+
console.log(`Client subscribed to logs: ${streamKey}`);
|
|
204
|
+
// Get or create the log stream
|
|
205
|
+
await getLogStream(io, processId, logType);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error('Error subscribing to logs:', error);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Unsubscribe from log stream
|
|
212
|
+
socket.on('unsubscribe-logs', ({ processId, logType }) => {
|
|
213
|
+
const streamKey = `${processId}-${logType}`;
|
|
214
|
+
// Remove socket from the room
|
|
215
|
+
socket.leave(streamKey);
|
|
216
|
+
console.log(`Client unsubscribed from logs: ${streamKey}`);
|
|
217
|
+
// If no more clients in this room, stop the stream
|
|
218
|
+
const room = io.sockets.adapter.rooms.get(streamKey);
|
|
219
|
+
if (!room || room.size === 0) {
|
|
220
|
+
const stream = activeStreams[streamKey];
|
|
221
|
+
if (stream) {
|
|
222
|
+
console.log(`Stopping log stream: ${streamKey}`);
|
|
223
|
+
stream.kill();
|
|
224
|
+
delete activeStreams[streamKey];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// Cleanup on disconnect
|
|
229
|
+
socket.on('disconnect', () => {
|
|
230
|
+
console.log('Client disconnected from log streaming');
|
|
231
|
+
}); // Subscribe to remote log stream
|
|
232
|
+
socket.on('subscribe-remote-logs', async ({ connectionId, processId }) => {
|
|
233
|
+
try {
|
|
234
|
+
console.log(`Subscribing to remote logs - Connection: ${connectionId}, Process: ${processId}`);
|
|
235
|
+
const streamKey = `${connectionId}-${processId}`;
|
|
236
|
+
// Add socket to a room for this specific log stream
|
|
237
|
+
socket.join(streamKey);
|
|
238
|
+
console.log(`Client subscribed to remote logs: ${streamKey}`);
|
|
239
|
+
// Get or create the remote log stream
|
|
240
|
+
await getRemoteLogStream(io, connectionId, processId);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error('Error subscribing to remote logs:', error);
|
|
244
|
+
socket.emit('remote-log-error', {
|
|
245
|
+
connectionId,
|
|
246
|
+
processId,
|
|
247
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// Unsubscribe from remote log stream
|
|
252
|
+
socket.on('unsubscribe-remote-logs', ({ connectionId, processId }) => {
|
|
253
|
+
const streamKey = `${connectionId}-${processId}`;
|
|
254
|
+
// Remove socket from the room
|
|
255
|
+
socket.leave(streamKey);
|
|
256
|
+
console.log(`Client unsubscribed from remote logs: ${streamKey}`);
|
|
257
|
+
// If no more clients in this room, stop the streams
|
|
258
|
+
const room = io.sockets.adapter.rooms.get(streamKey);
|
|
259
|
+
if (!room || room.size === 0) {
|
|
260
|
+
const streams = activeRemoteStreams[streamKey];
|
|
261
|
+
if (streams) {
|
|
262
|
+
console.log(`Stopping remote log streams: ${streamKey}`);
|
|
263
|
+
if (streams.stdout && streams.stdout.kill) {
|
|
264
|
+
streams.stdout.kill();
|
|
265
|
+
}
|
|
266
|
+
if (streams.stderr && streams.stderr.kill) {
|
|
267
|
+
streams.stderr.kill();
|
|
268
|
+
}
|
|
269
|
+
delete activeRemoteStreams[streamKey];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
exports.setupLogStreaming = setupLogStreaming;
|
|
276
|
+
exports.default = router;
|