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.
Files changed (58) hide show
  1. package/README.md +202 -14
  2. package/bin/ezpm2gui.js +40 -44
  3. package/bin/ezpm2gui.ts +51 -0
  4. package/bin/generate-ecosystem.js +24 -22
  5. package/bin/generate-ecosystem.ts +56 -0
  6. package/dist/server/config/project-configs.json +236 -0
  7. package/dist/server/index.js +64 -19
  8. package/dist/server/logs/deployment.log +12 -0
  9. package/dist/server/routes/clusterManagement.d.ts +3 -0
  10. package/dist/server/routes/clusterManagement.js +152 -0
  11. package/dist/server/routes/deployApplication.d.ts +3 -0
  12. package/dist/server/routes/deployApplication.js +310 -0
  13. package/dist/server/routes/logStreaming.d.ts +5 -0
  14. package/dist/server/routes/logStreaming.js +276 -0
  15. package/dist/server/routes/modules.d.ts +3 -0
  16. package/dist/server/routes/modules.js +106 -0
  17. package/dist/server/routes/processConfig.d.ts +3 -0
  18. package/dist/server/routes/processConfig.js +118 -0
  19. package/dist/server/routes/remoteConnections.d.ts +3 -0
  20. package/dist/server/routes/remoteConnections.js +634 -0
  21. package/dist/server/services/ProjectSetupService.d.ts +72 -0
  22. package/dist/server/services/ProjectSetupService.js +327 -0
  23. package/dist/server/utils/dialog.d.ts +1 -0
  24. package/dist/server/utils/dialog.js +16 -0
  25. package/dist/server/utils/encryption.d.ts +12 -0
  26. package/dist/server/utils/encryption.js +72 -0
  27. package/dist/server/utils/pm2-connection.d.ts +16 -0
  28. package/dist/server/utils/pm2-connection.js +141 -0
  29. package/dist/server/utils/remote-connection.d.ts +152 -0
  30. package/dist/server/utils/remote-connection.js +590 -0
  31. package/dist/server/utils/upload.d.ts +3 -0
  32. package/dist/server/utils/upload.js +39 -0
  33. package/package.json +65 -49
  34. package/src/client/build/asset-manifest.json +6 -6
  35. package/src/client/build/favicon.ico +2 -0
  36. package/src/client/build/index.html +1 -1
  37. package/src/client/build/logo192.svg +7 -0
  38. package/src/client/build/logo512.svg +7 -0
  39. package/src/client/build/manifest.json +5 -6
  40. package/src/client/build/static/css/main.672b8f26.css +2 -0
  41. package/src/client/build/static/css/main.672b8f26.css.map +1 -0
  42. package/src/client/build/static/js/main.31323a04.js +156 -0
  43. package/src/client/build/static/js/{main.dde30e92.js.LICENSE.txt → main.31323a04.js.LICENSE.txt} +19 -0
  44. package/src/client/build/static/js/main.31323a04.js.map +1 -0
  45. package/ .npmignore +0 -39
  46. package/install.bat +0 -22
  47. package/install.sh +0 -23
  48. package/src/client/build/static/css/main.c1cbda3a.css +0 -2
  49. package/src/client/build/static/css/main.c1cbda3a.css.map +0 -1
  50. package/src/client/build/static/js/main.dde30e92.js +0 -3
  51. package/src/client/build/static/js/main.dde30e92.js.map +0 -1
  52. package/src/client/package-lock.json +0 -16192
  53. package/src/client/package.json +0 -39
  54. package/src/client/public/index.html +0 -20
  55. package/src/client/public/manifest.json +0 -25
  56. package/src/index.ts +0 -24
  57. package/src/server/index.ts +0 -240
  58. 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,5 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ declare const setupLogStreaming: (io: any) => void;
4
+ export { setupLogStreaming };
5
+ export 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;
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ export default router;