ezpm2gui 1.2.1 → 1.3.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.
@@ -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