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.
- 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 +3 -1
- 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 +185 -15
- 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
|