ezpm2gui 1.2.2 → 1.3.1
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 +45 -41
- package/dist/server/config/cron-jobs.json +18 -0
- package/dist/server/config/cron-scripts/6d8d5e1d-2bc8-463f-82a6-6c294f2b9dbe.sh +2 -0
- package/dist/server/config/remote-connections.json +22 -0
- package/dist/server/index.js +2 -0
- package/dist/server/routes/cronJobs.d.ts +7 -0
- package/dist/server/routes/cronJobs.js +189 -0
- package/dist/server/routes/remoteConnections.js +105 -0
- package/dist/server/services/CronJobService.d.ts +74 -0
- package/dist/server/services/CronJobService.js +407 -0
- package/dist/server/utils/remote-connection.d.ts +17 -0
- package/dist/server/utils/remote-connection.js +183 -14
- package/package.json +5 -1
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/index.html +1 -1
- package/src/client/build/static/css/main.d46bc75c.css +5 -0
- package/src/client/build/static/css/main.d46bc75c.css.map +1 -0
- package/src/client/build/static/js/main.b0e1c9b1.js +3 -0
- package/src/client/build/static/js/{main.31323a04.js.LICENSE.txt → main.b0e1c9b1.js.LICENSE.txt} +11 -0
- package/src/client/build/static/js/main.b0e1c9b1.js.map +1 -0
- package/src/client/build/static/css/main.672b8f26.css +0 -2
- package/src/client/build/static/css/main.672b8f26.css.map +0 -1
- package/src/client/build/static/js/main.31323a04.js +0 -156
- package/src/client/build/static/js/main.31323a04.js.map +0 -1
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cron Job Service - Manages PM2 processes with cron_restart
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.CronJobService = void 0;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const uuid_1 = require("uuid");
|
|
43
|
+
const pm2_connection_1 = require("../utils/pm2-connection");
|
|
44
|
+
const cron_parser_1 = require("cron-parser");
|
|
45
|
+
const CRON_CONFIG_FILE = path.join(__dirname, '../config/cron-jobs.json');
|
|
46
|
+
const CRON_SCRIPTS_DIR = path.join(__dirname, '../config/cron-scripts');
|
|
47
|
+
class CronJobService {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.ensureConfigFile();
|
|
50
|
+
}
|
|
51
|
+
static getInstance() {
|
|
52
|
+
if (!CronJobService.instance) {
|
|
53
|
+
CronJobService.instance = new CronJobService();
|
|
54
|
+
}
|
|
55
|
+
return CronJobService.instance;
|
|
56
|
+
}
|
|
57
|
+
ensureConfigFile() {
|
|
58
|
+
const dir = path.dirname(CRON_CONFIG_FILE);
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
if (!fs.existsSync(CRON_CONFIG_FILE)) {
|
|
63
|
+
fs.writeFileSync(CRON_CONFIG_FILE, JSON.stringify([], null, 2));
|
|
64
|
+
}
|
|
65
|
+
// Ensure scripts directory exists for inline scripts
|
|
66
|
+
if (!fs.existsSync(CRON_SCRIPTS_DIR)) {
|
|
67
|
+
fs.mkdirSync(CRON_SCRIPTS_DIR, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
readConfig() {
|
|
71
|
+
try {
|
|
72
|
+
const data = fs.readFileSync(CRON_CONFIG_FILE, 'utf-8');
|
|
73
|
+
return JSON.parse(data);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('Error reading cron config:', error);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
writeConfig(configs) {
|
|
81
|
+
fs.writeFileSync(CRON_CONFIG_FILE, JSON.stringify(configs, null, 2));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate cron expression
|
|
85
|
+
*/
|
|
86
|
+
validateCronExpression(expression) {
|
|
87
|
+
try {
|
|
88
|
+
const interval = cron_parser_1.CronExpressionParser.parse(expression);
|
|
89
|
+
return {
|
|
90
|
+
valid: true,
|
|
91
|
+
nextRun: interval.next().toDate()
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
error: error.message
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get human-readable description of cron expression
|
|
103
|
+
*/
|
|
104
|
+
getCronDescription(expression) {
|
|
105
|
+
try {
|
|
106
|
+
const parts = expression.split(' ');
|
|
107
|
+
if (parts.length !== 5) {
|
|
108
|
+
return 'Invalid cron expression';
|
|
109
|
+
}
|
|
110
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
111
|
+
// Simple descriptions for common patterns
|
|
112
|
+
if (expression === '* * * * *')
|
|
113
|
+
return 'Every minute';
|
|
114
|
+
if (expression === '0 * * * *')
|
|
115
|
+
return 'Every hour';
|
|
116
|
+
if (expression === '0 0 * * *')
|
|
117
|
+
return 'Daily at midnight';
|
|
118
|
+
if (expression === '0 0 * * 0')
|
|
119
|
+
return 'Weekly on Sunday at midnight';
|
|
120
|
+
if (expression === '0 0 1 * *')
|
|
121
|
+
return 'Monthly on the 1st at midnight';
|
|
122
|
+
let desc = 'At ';
|
|
123
|
+
if (minute === '*')
|
|
124
|
+
desc += 'every minute';
|
|
125
|
+
else
|
|
126
|
+
desc += `minute ${minute}`;
|
|
127
|
+
if (hour !== '*')
|
|
128
|
+
desc += ` past hour ${hour}`;
|
|
129
|
+
if (dayOfMonth !== '*')
|
|
130
|
+
desc += ` on day ${dayOfMonth}`;
|
|
131
|
+
if (month !== '*')
|
|
132
|
+
desc += ` in month ${month}`;
|
|
133
|
+
if (dayOfWeek !== '*')
|
|
134
|
+
desc += ` on day ${dayOfWeek} of week`;
|
|
135
|
+
return desc;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return 'Invalid expression';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get script path for inline scripts (creates temp file)
|
|
143
|
+
*/
|
|
144
|
+
getScriptPath(config) {
|
|
145
|
+
if (config.scriptMode === 'file') {
|
|
146
|
+
return config.scriptPath;
|
|
147
|
+
}
|
|
148
|
+
// For inline scripts, write to temp file
|
|
149
|
+
const extension = this.getScriptExtension(config.scriptType);
|
|
150
|
+
const scriptFileName = `${config.id}${extension}`;
|
|
151
|
+
const scriptPath = path.join(CRON_SCRIPTS_DIR, scriptFileName);
|
|
152
|
+
if (config.inlineScript) {
|
|
153
|
+
fs.writeFileSync(scriptPath, config.inlineScript, 'utf8');
|
|
154
|
+
// Make executable for shell scripts
|
|
155
|
+
if (config.scriptType === 'shell') {
|
|
156
|
+
try {
|
|
157
|
+
fs.chmodSync(scriptPath, '755');
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn('Could not set execute permission:', err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return scriptPath;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get file extension for script type
|
|
168
|
+
*/
|
|
169
|
+
getScriptExtension(scriptType) {
|
|
170
|
+
switch (scriptType) {
|
|
171
|
+
case 'node':
|
|
172
|
+
return '.js';
|
|
173
|
+
case 'python':
|
|
174
|
+
return '.py';
|
|
175
|
+
case 'shell':
|
|
176
|
+
return '.sh';
|
|
177
|
+
case 'dotnet':
|
|
178
|
+
return '.cs';
|
|
179
|
+
default:
|
|
180
|
+
return '.txt';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Convert CronJobConfig to PM2 start options
|
|
185
|
+
*/
|
|
186
|
+
toPM2Options(config) {
|
|
187
|
+
const scriptPath = this.getScriptPath(config);
|
|
188
|
+
const options = {
|
|
189
|
+
name: `cron-${config.id}`,
|
|
190
|
+
script: scriptPath,
|
|
191
|
+
cron_restart: config.cronExpression,
|
|
192
|
+
autorestart: false, // Don't auto-restart on crash, only on cron schedule
|
|
193
|
+
watch: false,
|
|
194
|
+
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
195
|
+
combine_logs: true
|
|
196
|
+
};
|
|
197
|
+
// Set interpreter based on script type
|
|
198
|
+
switch (config.scriptType) {
|
|
199
|
+
case 'node':
|
|
200
|
+
options.interpreter = 'node';
|
|
201
|
+
break;
|
|
202
|
+
case 'python':
|
|
203
|
+
options.interpreter = 'python';
|
|
204
|
+
break;
|
|
205
|
+
case 'dotnet':
|
|
206
|
+
options.interpreter = 'none';
|
|
207
|
+
options.script = 'dotnet';
|
|
208
|
+
options.args = ['run', '--project', config.scriptPath];
|
|
209
|
+
break;
|
|
210
|
+
case 'shell':
|
|
211
|
+
options.interpreter = 'bash';
|
|
212
|
+
options.interpreter_args = '-c';
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (config.args && config.args.length > 0) {
|
|
216
|
+
options.args = config.args;
|
|
217
|
+
}
|
|
218
|
+
if (config.env) {
|
|
219
|
+
options.env = config.env;
|
|
220
|
+
}
|
|
221
|
+
if (config.cwd) {
|
|
222
|
+
options.cwd = config.cwd;
|
|
223
|
+
}
|
|
224
|
+
return options;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Create a new cron job
|
|
228
|
+
*/
|
|
229
|
+
async createCronJob(config) {
|
|
230
|
+
const validation = this.validateCronExpression(config.cronExpression);
|
|
231
|
+
if (!validation.valid) {
|
|
232
|
+
throw new Error(`Invalid cron expression: ${validation.error}`);
|
|
233
|
+
}
|
|
234
|
+
const newConfig = {
|
|
235
|
+
...config,
|
|
236
|
+
id: (0, uuid_1.v4)(),
|
|
237
|
+
createdAt: new Date().toISOString(),
|
|
238
|
+
updatedAt: new Date().toISOString()
|
|
239
|
+
};
|
|
240
|
+
const configs = this.readConfig();
|
|
241
|
+
configs.push(newConfig);
|
|
242
|
+
this.writeConfig(configs);
|
|
243
|
+
// Start the PM2 process if enabled
|
|
244
|
+
if (newConfig.enabled) {
|
|
245
|
+
await this.startCronJob(newConfig.id);
|
|
246
|
+
}
|
|
247
|
+
return newConfig;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get all cron jobs
|
|
251
|
+
*/
|
|
252
|
+
getCronJobs() {
|
|
253
|
+
return this.readConfig();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get a single cron job by ID
|
|
257
|
+
*/
|
|
258
|
+
getCronJob(id) {
|
|
259
|
+
const configs = this.readConfig();
|
|
260
|
+
return configs.find(c => c.id === id);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Update a cron job
|
|
264
|
+
*/
|
|
265
|
+
async updateCronJob(id, updates) {
|
|
266
|
+
if (updates.cronExpression) {
|
|
267
|
+
const validation = this.validateCronExpression(updates.cronExpression);
|
|
268
|
+
if (!validation.valid) {
|
|
269
|
+
throw new Error(`Invalid cron expression: ${validation.error}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const configs = this.readConfig();
|
|
273
|
+
const index = configs.findIndex(c => c.id === id);
|
|
274
|
+
if (index === -1) {
|
|
275
|
+
throw new Error('Cron job not found');
|
|
276
|
+
}
|
|
277
|
+
const wasEnabled = configs[index].enabled;
|
|
278
|
+
configs[index] = {
|
|
279
|
+
...configs[index],
|
|
280
|
+
...updates,
|
|
281
|
+
id, // Ensure ID doesn't change
|
|
282
|
+
updatedAt: new Date().toISOString()
|
|
283
|
+
};
|
|
284
|
+
this.writeConfig(configs);
|
|
285
|
+
// Handle PM2 process state changes
|
|
286
|
+
if (wasEnabled && !configs[index].enabled) {
|
|
287
|
+
await this.stopCronJob(id);
|
|
288
|
+
}
|
|
289
|
+
else if (!wasEnabled && configs[index].enabled) {
|
|
290
|
+
await this.startCronJob(id);
|
|
291
|
+
}
|
|
292
|
+
else if (configs[index].enabled) {
|
|
293
|
+
// Restart if enabled and config changed
|
|
294
|
+
await this.stopCronJob(id);
|
|
295
|
+
await this.startCronJob(id);
|
|
296
|
+
}
|
|
297
|
+
return configs[index];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Delete a cron job
|
|
301
|
+
*/
|
|
302
|
+
async deleteCronJob(id) {
|
|
303
|
+
const configs = this.readConfig();
|
|
304
|
+
const job = configs.find(c => c.id === id);
|
|
305
|
+
if (!job) {
|
|
306
|
+
throw new Error('Cron job not found');
|
|
307
|
+
}
|
|
308
|
+
// Stop PM2 process if running
|
|
309
|
+
if (job.enabled) {
|
|
310
|
+
await this.stopCronJob(id);
|
|
311
|
+
}
|
|
312
|
+
// Delete inline script file if it exists
|
|
313
|
+
if (job.scriptMode === 'inline') {
|
|
314
|
+
const extension = this.getScriptExtension(job.scriptType);
|
|
315
|
+
const scriptPath = path.join(CRON_SCRIPTS_DIR, `${id}${extension}`);
|
|
316
|
+
if (fs.existsSync(scriptPath)) {
|
|
317
|
+
fs.unlinkSync(scriptPath);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const filtered = configs.filter(c => c.id !== id);
|
|
321
|
+
this.writeConfig(filtered);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Start a cron job (start PM2 process with cron_restart)
|
|
325
|
+
*/
|
|
326
|
+
async startCronJob(id) {
|
|
327
|
+
const config = this.getCronJob(id);
|
|
328
|
+
if (!config) {
|
|
329
|
+
throw new Error('Cron job not found');
|
|
330
|
+
}
|
|
331
|
+
const pm2Options = this.toPM2Options(config);
|
|
332
|
+
const pm2 = require('pm2');
|
|
333
|
+
await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
334
|
+
pm2.start(pm2Options, callback);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Stop a cron job (delete PM2 process)
|
|
339
|
+
*/
|
|
340
|
+
async stopCronJob(id) {
|
|
341
|
+
const processName = `cron-${id}`;
|
|
342
|
+
const pm2 = require('pm2');
|
|
343
|
+
try {
|
|
344
|
+
await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
345
|
+
pm2.delete(processName, (err) => {
|
|
346
|
+
// PM2 delete returns undefined on success, pass empty object
|
|
347
|
+
if (err && !err.message.includes('not found')) {
|
|
348
|
+
callback(err);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
callback(null, {});
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
// Ignore not found errors
|
|
358
|
+
if (!error.message.includes('not found')) {
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get status of all cron jobs (including PM2 process info)
|
|
365
|
+
*/
|
|
366
|
+
async getCronJobsStatus() {
|
|
367
|
+
const configs = this.readConfig();
|
|
368
|
+
const pm2 = require('pm2');
|
|
369
|
+
const list = await (0, pm2_connection_1.executePM2Command)((callback) => {
|
|
370
|
+
pm2.list(callback);
|
|
371
|
+
});
|
|
372
|
+
const statuses = configs.map(config => {
|
|
373
|
+
var _a;
|
|
374
|
+
const processName = `cron-${config.id}`;
|
|
375
|
+
const pm2Process = list.find((p) => p.name === processName);
|
|
376
|
+
let nextExecution;
|
|
377
|
+
if (config.enabled) {
|
|
378
|
+
try {
|
|
379
|
+
const interval = cron_parser_1.CronExpressionParser.parse(config.cronExpression);
|
|
380
|
+
nextExecution = interval.next().toDate().toISOString();
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
// Ignore
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
config,
|
|
388
|
+
pm2Process,
|
|
389
|
+
isRunning: ((_a = pm2Process === null || pm2Process === void 0 ? void 0 : pm2Process.pm2_env) === null || _a === void 0 ? void 0 : _a.status) === 'online',
|
|
390
|
+
nextExecution
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
return statuses;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Toggle cron job enabled state
|
|
397
|
+
*/
|
|
398
|
+
async toggleCronJob(id) {
|
|
399
|
+
const config = this.getCronJob(id);
|
|
400
|
+
if (!config) {
|
|
401
|
+
throw new Error('Cron job not found');
|
|
402
|
+
}
|
|
403
|
+
return this.updateCronJob(id, { enabled: !config.enabled });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
exports.CronJobService = CronJobService;
|
|
407
|
+
exports.default = CronJobService.getInstance();
|
|
@@ -56,8 +56,14 @@ export declare class RemoteConnection extends EventEmitter {
|
|
|
56
56
|
executeCommand(command: string, forceSudo?: boolean): Promise<CommandResult>;
|
|
57
57
|
/**
|
|
58
58
|
* Check if PM2 is installed on the remote server
|
|
59
|
+
* Uses multiple detection methods for better reliability
|
|
59
60
|
*/
|
|
60
61
|
checkPM2Installation(): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Execute a PM2 command with proper PATH handling
|
|
64
|
+
* Tries different methods to find and execute PM2
|
|
65
|
+
*/
|
|
66
|
+
private executePM2Command;
|
|
61
67
|
/**
|
|
62
68
|
* Get PM2 processes from the remote server
|
|
63
69
|
*/
|
|
@@ -85,6 +91,10 @@ export declare class RemoteConnection extends EventEmitter {
|
|
|
85
91
|
* Delete a PM2 process
|
|
86
92
|
*/
|
|
87
93
|
deletePM2Process(processName: string): Promise<CommandResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Install PM2 on the remote server
|
|
96
|
+
*/
|
|
97
|
+
installPM2(): Promise<CommandResult>;
|
|
88
98
|
/**
|
|
89
99
|
* Get system information from the remote server
|
|
90
100
|
*/
|
|
@@ -116,6 +126,13 @@ export declare class RemoteConnectionManager {
|
|
|
116
126
|
* @returns The connection ID
|
|
117
127
|
*/
|
|
118
128
|
createConnection(config: RemoteConnectionConfig): string;
|
|
129
|
+
/**
|
|
130
|
+
* Update an existing remote connection
|
|
131
|
+
* @param connectionId The connection ID
|
|
132
|
+
* @param config New connection configuration
|
|
133
|
+
* @returns True if the connection was updated, false if it didn't exist
|
|
134
|
+
*/
|
|
135
|
+
updateConnection(connectionId: string, config: RemoteConnectionConfig): Promise<boolean>;
|
|
119
136
|
/**
|
|
120
137
|
* Get a connection by ID
|
|
121
138
|
* @param connectionId The connection ID
|
|
@@ -172,18 +172,108 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
174
|
* Check if PM2 is installed on the remote server
|
|
175
|
+
* Uses multiple detection methods for better reliability
|
|
175
176
|
*/
|
|
176
177
|
async checkPM2Installation() {
|
|
177
178
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
// Method 1: Try running pm2 --version directly (most reliable)
|
|
180
|
+
try {
|
|
181
|
+
const versionResult = await this.executeCommand('pm2 --version');
|
|
182
|
+
if (versionResult.code === 0 && versionResult.stdout.trim()) {
|
|
183
|
+
console.log(`PM2 detected via version check: ${versionResult.stdout.trim()}`);
|
|
184
|
+
this.isPM2Installed = true;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.log('PM2 version check failed, trying alternative methods');
|
|
190
|
+
}
|
|
191
|
+
// Method 2: Check common installation paths
|
|
192
|
+
const pathChecks = [
|
|
193
|
+
'which pm2',
|
|
194
|
+
'command -v pm2',
|
|
195
|
+
'ls -la /usr/local/bin/pm2',
|
|
196
|
+
'ls -la ~/.npm-global/bin/pm2',
|
|
197
|
+
'ls -la ~/node_modules/.bin/pm2',
|
|
198
|
+
'find /usr -name "pm2" -type f -executable 2>/dev/null | head -1'
|
|
199
|
+
];
|
|
200
|
+
for (const pathCheck of pathChecks) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await this.executeCommand(pathCheck);
|
|
203
|
+
if (result.code === 0 && result.stdout.trim() && !result.stdout.includes('not found')) {
|
|
204
|
+
console.log(`PM2 found via: ${pathCheck} -> ${result.stdout.trim()}`);
|
|
205
|
+
this.isPM2Installed = true;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
// Continue to next method
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Method 3: Try with full environment loading
|
|
215
|
+
try {
|
|
216
|
+
const envResult = await this.executeCommand('bash -l -c "pm2 --version"');
|
|
217
|
+
if (envResult.code === 0 && envResult.stdout.trim()) {
|
|
218
|
+
console.log(`PM2 detected via bash login shell: ${envResult.stdout.trim()}`);
|
|
219
|
+
this.isPM2Installed = true;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
console.log('PM2 not found via bash login shell');
|
|
225
|
+
}
|
|
226
|
+
// Method 4: Try npm ls -g pm2
|
|
227
|
+
try {
|
|
228
|
+
const npmResult = await this.executeCommand('npm ls -g pm2 --depth=0');
|
|
229
|
+
if (npmResult.code === 0 && !npmResult.stdout.includes('(empty)')) {
|
|
230
|
+
console.log('PM2 detected via npm global list');
|
|
231
|
+
this.isPM2Installed = true;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.log('PM2 not found via npm global list');
|
|
237
|
+
}
|
|
238
|
+
console.log('PM2 not detected via any method');
|
|
239
|
+
this.isPM2Installed = false;
|
|
240
|
+
return false;
|
|
181
241
|
}
|
|
182
242
|
catch (error) {
|
|
243
|
+
console.error('Error during PM2 installation check:', error);
|
|
183
244
|
this.isPM2Installed = false;
|
|
184
245
|
return false;
|
|
185
246
|
}
|
|
186
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Execute a PM2 command with proper PATH handling
|
|
250
|
+
* Tries different methods to find and execute PM2
|
|
251
|
+
*/
|
|
252
|
+
async executePM2Command(pm2Args) {
|
|
253
|
+
const commands = [
|
|
254
|
+
`pm2 ${pm2Args}`, // Direct PM2 call
|
|
255
|
+
`bash -l -c "pm2 ${pm2Args}"`, // With login shell
|
|
256
|
+
`~/.npm-global/bin/pm2 ${pm2Args}`, // Common global npm path
|
|
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
|
+
];
|
|
261
|
+
let lastError = null;
|
|
262
|
+
for (const command of commands) {
|
|
263
|
+
try {
|
|
264
|
+
const result = await this.executeCommand(command);
|
|
265
|
+
if (result.code === 0) {
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
lastError = new Error(`Command failed with exit code ${result.code}: ${result.stderr}`);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
lastError = error;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw lastError || new Error(`Failed to execute PM2 command: ${pm2Args}`);
|
|
276
|
+
}
|
|
187
277
|
/**
|
|
188
278
|
* Get PM2 processes from the remote server
|
|
189
279
|
*/
|
|
@@ -193,14 +283,14 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
193
283
|
if (!this._isConnected) {
|
|
194
284
|
await this.connect();
|
|
195
285
|
}
|
|
196
|
-
// Check if PM2 is installed
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw new Error('PM2 is not installed on the remote server');
|
|
201
|
-
}
|
|
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');
|
|
202
290
|
}
|
|
203
|
-
|
|
291
|
+
// Update the cached status
|
|
292
|
+
this.isPM2Installed = true;
|
|
293
|
+
const result = await this.executePM2Command('jlist');
|
|
204
294
|
// Clean the output to ensure it's valid JSON
|
|
205
295
|
// Sometimes pm2 jlist can include non-JSON data at the beginning or end
|
|
206
296
|
let cleanedOutput = result.stdout.trim();
|
|
@@ -289,25 +379,72 @@ class RemoteConnection extends events_1.EventEmitter {
|
|
|
289
379
|
* Start a PM2 process
|
|
290
380
|
*/
|
|
291
381
|
async startPM2Process(processName) {
|
|
292
|
-
return await this.
|
|
382
|
+
return await this.executePM2Command(`start ${processName}`);
|
|
293
383
|
}
|
|
294
384
|
/**
|
|
295
385
|
* Stop a PM2 process
|
|
296
386
|
*/
|
|
297
387
|
async stopPM2Process(processName) {
|
|
298
|
-
return await this.
|
|
388
|
+
return await this.executePM2Command(`stop ${processName}`);
|
|
299
389
|
}
|
|
300
390
|
/**
|
|
301
391
|
* Restart a PM2 process
|
|
302
392
|
*/
|
|
303
393
|
async restartPM2Process(processName) {
|
|
304
|
-
return await this.
|
|
394
|
+
return await this.executePM2Command(`restart ${processName}`);
|
|
305
395
|
}
|
|
306
396
|
/**
|
|
307
397
|
* Delete a PM2 process
|
|
308
398
|
*/
|
|
309
399
|
async deletePM2Process(processName) {
|
|
310
|
-
return await this.
|
|
400
|
+
return await this.executePM2Command(`delete ${processName}`);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Install PM2 on the remote server
|
|
404
|
+
*/
|
|
405
|
+
async installPM2() {
|
|
406
|
+
try {
|
|
407
|
+
console.log('Attempting to install PM2 on remote server...');
|
|
408
|
+
// Try different installation methods
|
|
409
|
+
const installCommands = [
|
|
410
|
+
'npm install -g pm2', // Standard global install
|
|
411
|
+
'sudo npm install -g pm2', // With sudo if needed
|
|
412
|
+
'npm install -g pm2 --unsafe-perm=true', // With unsafe permissions
|
|
413
|
+
];
|
|
414
|
+
let lastResult = null;
|
|
415
|
+
for (const command of installCommands) {
|
|
416
|
+
try {
|
|
417
|
+
const result = await this.executeCommand(command, command.includes('sudo'));
|
|
418
|
+
if (result.code === 0) {
|
|
419
|
+
console.log(`PM2 installed successfully with: ${command}`);
|
|
420
|
+
// Verify installation
|
|
421
|
+
const isPM2Installed = await this.checkPM2Installation();
|
|
422
|
+
if (isPM2Installed) {
|
|
423
|
+
this.isPM2Installed = true;
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
lastResult = result;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
console.log(`Failed to install PM2 with command: ${command}`);
|
|
431
|
+
lastResult = {
|
|
432
|
+
stdout: '',
|
|
433
|
+
stderr: error instanceof Error ? error.message : 'Unknown error',
|
|
434
|
+
code: 1
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return lastResult || {
|
|
439
|
+
stdout: '',
|
|
440
|
+
stderr: 'Failed to install PM2 with any method',
|
|
441
|
+
code: 1
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.error('Error installing PM2:', error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
311
448
|
}
|
|
312
449
|
/**
|
|
313
450
|
* Get system information from the remote server
|
|
@@ -455,6 +592,38 @@ class RemoteConnectionManager {
|
|
|
455
592
|
this.saveConnectionsToDisk();
|
|
456
593
|
return connectionId;
|
|
457
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Update an existing remote connection
|
|
597
|
+
* @param connectionId The connection ID
|
|
598
|
+
* @param config New connection configuration
|
|
599
|
+
* @returns True if the connection was updated, false if it didn't exist
|
|
600
|
+
*/
|
|
601
|
+
async updateConnection(connectionId, config) {
|
|
602
|
+
const existingConnection = this.connections.get(connectionId);
|
|
603
|
+
if (!existingConnection) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
// Close the existing connection if it's active
|
|
607
|
+
if (existingConnection.isConnected()) {
|
|
608
|
+
await existingConnection.disconnect();
|
|
609
|
+
}
|
|
610
|
+
// Get the old config to preserve password if not provided in update
|
|
611
|
+
const oldConfig = existingConnection.getFullConfig();
|
|
612
|
+
// If password is not provided in the update, keep the old one
|
|
613
|
+
const updatedConfig = {
|
|
614
|
+
...config,
|
|
615
|
+
password: config.password || oldConfig.password,
|
|
616
|
+
privateKey: config.privateKey || oldConfig.privateKey,
|
|
617
|
+
passphrase: config.passphrase || oldConfig.passphrase
|
|
618
|
+
};
|
|
619
|
+
// Create a new connection with updated config
|
|
620
|
+
const newConnection = new RemoteConnection(updatedConfig);
|
|
621
|
+
// Replace the old connection with the new one
|
|
622
|
+
this.connections.set(connectionId, newConnection);
|
|
623
|
+
// Save the updated connections to disk
|
|
624
|
+
this.saveConnectionsToDisk();
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
458
627
|
/**
|
|
459
628
|
* Get a connection by ID
|
|
460
629
|
* @param connectionId The connection ID
|