@fkom13/mcp-sftp-orchestrator 6.0.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/.env.example +22 -0
- package/README.md +102 -0
- package/apis.js +88 -0
- package/config.js +34 -0
- package/history.js +44 -0
- package/package.json +40 -0
- package/publish1github.md +216 -0
- package/queue.js +377 -0
- package/server.js +773 -0
- package/servers.js +58 -0
- package/sftp.js +248 -0
- package/ssh.js +492 -0
- package/sshPool.js +272 -0
- package/test_features.js +112 -0
package/ssh.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import queue from './queue.js';
|
|
4
|
+
import serverManager from './servers.js';
|
|
5
|
+
import sshPool from './sshPool.js';
|
|
6
|
+
import config from './config.js';
|
|
7
|
+
|
|
8
|
+
const STREAMING_COMMANDS = [
|
|
9
|
+
/^pm2\s+logs?\b/i,
|
|
10
|
+
/^docker\s+logs?\s+(-f|--follow)/i,
|
|
11
|
+
/^tail\s+-f/i,
|
|
12
|
+
/^journalctl\s+-f/i,
|
|
13
|
+
/^watch\b/i,
|
|
14
|
+
/^top\b/i,
|
|
15
|
+
/^htop\b/i,
|
|
16
|
+
/^less\b/i,
|
|
17
|
+
/^more\b/i,
|
|
18
|
+
/^vim?\b/i,
|
|
19
|
+
/^nano\b/i
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function isStreamingCommand(cmd) {
|
|
23
|
+
return STREAMING_COMMANDS.some(pattern => pattern.test(cmd.trim()));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Configuration des réponses automatiques pour les prompts interactifs
|
|
27
|
+
const INTERACTIVE_RESPONSES = {
|
|
28
|
+
// Patterns courants et leurs réponses
|
|
29
|
+
'continue connecting': 'yes',
|
|
30
|
+
'Are you sure': 'yes',
|
|
31
|
+
'password:': null, // Sera géré séparément
|
|
32
|
+
'Password:': null,
|
|
33
|
+
'Do you want to continue': 'y',
|
|
34
|
+
'Overwrite': 'y',
|
|
35
|
+
'Save': 'y'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Détecte si une sortie contient un prompt interactif
|
|
39
|
+
function detectInteractivePrompt(output) {
|
|
40
|
+
const lastLine = output.split('\n').pop().toLowerCase();
|
|
41
|
+
|
|
42
|
+
for (const [pattern, response] of Object.entries(INTERACTIVE_RESPONSES)) {
|
|
43
|
+
if (lastLine.includes(pattern.toLowerCase())) {
|
|
44
|
+
return { pattern, response, needsInput: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Vérifier les patterns qui attendent une entrée
|
|
49
|
+
if (lastLine.endsWith(':') || lastLine.endsWith('?') ||
|
|
50
|
+
lastLine.includes('[y/n]') || lastLine.includes('(yes/no)')) {
|
|
51
|
+
return { pattern: 'generic', response: null, needsInput: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { needsInput: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Exécution de commande avec pool de connexions
|
|
58
|
+
async function executeCommand(jobId) {
|
|
59
|
+
const job = queue.getJob(jobId);
|
|
60
|
+
if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`);
|
|
61
|
+
|
|
62
|
+
let connection = null;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const serverConfig = await serverManager.getServer(job.alias);
|
|
66
|
+
queue.updateJobStatus(jobId, 'running');
|
|
67
|
+
|
|
68
|
+
// Utiliser le pool de connexions si pas de mode interactif
|
|
69
|
+
const usePool = !job.interactive && job.persistent !== false;
|
|
70
|
+
|
|
71
|
+
if (usePool) {
|
|
72
|
+
// Obtenir une connexion du pool
|
|
73
|
+
connection = await sshPool.getConnection(job.alias, serverConfig);
|
|
74
|
+
await executeWithPooledConnection(connection, job, jobId);
|
|
75
|
+
} else {
|
|
76
|
+
// Créer une connexion dédiée pour les commandes interactives
|
|
77
|
+
await executeWithNewConnection(serverConfig, job, jobId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
} catch (err) {
|
|
81
|
+
queue.updateJobStatus(jobId, 'failed', { error: err.message });
|
|
82
|
+
} finally {
|
|
83
|
+
// Libérer la connexion si elle vient du pool
|
|
84
|
+
if (connection) {
|
|
85
|
+
sshPool.releaseConnection(connection.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Exécution avec une connexion du pool
|
|
91
|
+
async function executeWithPooledConnection(connection, job, jobId) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const { client } = connection;
|
|
94
|
+
|
|
95
|
+
const isStreaming = job.streaming || isStreamingCommand(job.cmd);
|
|
96
|
+
let cmdToExecute = job.cmd;
|
|
97
|
+
|
|
98
|
+
if (isStreaming) {
|
|
99
|
+
const maxLines = job.maxLines || 100;
|
|
100
|
+
const streamTimeout = job.streamTimeout || 10;
|
|
101
|
+
if (/^pm2\s+logs?\b/i.test(cmdToExecute)) {
|
|
102
|
+
cmdToExecute = cmdToExecute.replace(/\s+--lines\s+\d+/gi, '').replace(/\s+--nostream/gi, '');
|
|
103
|
+
cmdToExecute += ` --lines ${maxLines} --nostream`;
|
|
104
|
+
} else if (/^docker\s+logs?\b/i.test(cmdToExecute)) {
|
|
105
|
+
cmdToExecute = cmdToExecute.replace(/\s+-f\b|--follow\b/gi, '').replace(/\s+--tail\s+\d+/gi, '');
|
|
106
|
+
cmdToExecute += ` --tail ${maxLines}`;
|
|
107
|
+
} else if (/^tail\s+-f/i.test(cmdToExecute)) {
|
|
108
|
+
cmdToExecute = `timeout ${streamTimeout} ` + cmdToExecute.replace(/-f\b/gi, '') + ` | head -n ${maxLines}`;
|
|
109
|
+
} else if (/^journalctl\s+-f/i.test(cmdToExecute)) {
|
|
110
|
+
cmdToExecute = cmdToExecute.replace(/-f\b/gi, '') + ` -n ${maxLines}`;
|
|
111
|
+
} else {
|
|
112
|
+
cmdToExecute = `timeout ${streamTimeout} ${cmdToExecute}`;
|
|
113
|
+
}
|
|
114
|
+
queue.log('info', `Commande streaming transformée: ${cmdToExecute}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
client.exec(cmdToExecute, { pty: job.pty }, (err, stream) => {
|
|
118
|
+
if (err) {
|
|
119
|
+
reject(err);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let output = '';
|
|
124
|
+
let stderr = '';
|
|
125
|
+
let lineCount = 0;
|
|
126
|
+
const startTime = Date.now();
|
|
127
|
+
const maxLines = job.maxLines || 1000;
|
|
128
|
+
const timeout = job.timeout || config.defaultCommandTimeout;
|
|
129
|
+
|
|
130
|
+
const timeoutId = setTimeout(() => {
|
|
131
|
+
stream.write('\x03'); // Envoyer Ctrl+C
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
stream.close();
|
|
134
|
+
const duration = Date.now() - startTime;
|
|
135
|
+
const result = { output: output.trim(), stderr: stderr.trim(), exitCode: 124, duration, lineCount, timedOut: true };
|
|
136
|
+
queue.updateJobStatus(jobId, 'completed', result);
|
|
137
|
+
resolve(result);
|
|
138
|
+
}, 500);
|
|
139
|
+
}, timeout);
|
|
140
|
+
|
|
141
|
+
stream.on('close', (code, signal) => {
|
|
142
|
+
clearTimeout(timeoutId);
|
|
143
|
+
const duration = Date.now() - startTime;
|
|
144
|
+
const result = { output: output.trim(), stderr: stderr.trim(), exitCode: code, signal, duration, lineCount };
|
|
145
|
+
if (code === 0 || code === 124) {
|
|
146
|
+
queue.updateJobStatus(jobId, 'completed', result);
|
|
147
|
+
resolve(result);
|
|
148
|
+
} else {
|
|
149
|
+
queue.updateJobStatus(jobId, 'failed', { ...result, error: `Commande terminée avec code ${code}` });
|
|
150
|
+
reject(new Error(`Exit code: ${code}`));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
stream.on('data', (data) => {
|
|
155
|
+
const chunk = data.toString();
|
|
156
|
+
output += chunk;
|
|
157
|
+
lineCount += (chunk.match(/\n/g) || []).length;
|
|
158
|
+
if (isStreaming && lineCount >= maxLines) {
|
|
159
|
+
stream.write('\x03');
|
|
160
|
+
setTimeout(() => stream.close(), 500);
|
|
161
|
+
}
|
|
162
|
+
if (job.autoRespond) {
|
|
163
|
+
const prompt = detectInteractivePrompt(output);
|
|
164
|
+
if (prompt.needsInput && prompt.response) {
|
|
165
|
+
stream.write(prompt.response + '\n');
|
|
166
|
+
queue.log('info', `Réponse automatique au prompt: ${prompt.pattern}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
stream.stderr.on('data', (data) => {
|
|
172
|
+
stderr += data.toString();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Exécution avec une nouvelle connexion (pour commandes interactives)
|
|
179
|
+
async function executeWithNewConnection(serverConfig, job, jobId) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const conn = new Client();
|
|
182
|
+
|
|
183
|
+
conn.on('ready', () => {
|
|
184
|
+
conn.shell((err, stream) => {
|
|
185
|
+
if (err) {
|
|
186
|
+
conn.end();
|
|
187
|
+
return reject(err);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let output = '';
|
|
191
|
+
const responses = job.responses || {};
|
|
192
|
+
const END_MARKER = `GEMINI_TASK_COMPLETE_MARKER_${job.id}`;
|
|
193
|
+
let responseSent = false;
|
|
194
|
+
|
|
195
|
+
const cleanupAndResolve = (status = 'completed') => {
|
|
196
|
+
clearTimeout(jobTimeout);
|
|
197
|
+
output = output.replace(END_MARKER, '').trim();
|
|
198
|
+
const result = { output, exitCode: 0 };
|
|
199
|
+
queue.updateJobStatus(jobId, status, result);
|
|
200
|
+
try {
|
|
201
|
+
stream.end();
|
|
202
|
+
conn.end();
|
|
203
|
+
} catch(e) {/* ignore */}
|
|
204
|
+
resolve(result);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const jobTimeout = setTimeout(() => {
|
|
208
|
+
cleanupAndResolve('failed');
|
|
209
|
+
}, (job.timeout ? job.timeout * 1000 : config.interactiveCommandTimeout));
|
|
210
|
+
|
|
211
|
+
stream.on('close', () => {
|
|
212
|
+
if (!output.includes(END_MARKER)) {
|
|
213
|
+
cleanupAndResolve('failed');
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
stream.on('data', (data) => {
|
|
218
|
+
const chunk = data.toString();
|
|
219
|
+
output += chunk;
|
|
220
|
+
|
|
221
|
+
if (output.includes(END_MARKER)) {
|
|
222
|
+
cleanupAndResolve();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!responseSent && job.autoRespond) {
|
|
227
|
+
const lowerChunk = chunk.toLowerCase();
|
|
228
|
+
let responseToSend = null;
|
|
229
|
+
|
|
230
|
+
// Chercher une réponse personnalisée
|
|
231
|
+
for (const [pattern, response] of Object.entries(responses)) {
|
|
232
|
+
if (lowerChunk.includes(pattern.toLowerCase())) {
|
|
233
|
+
responseToSend = response;
|
|
234
|
+
queue.log('info', `Réponse interactive: ${pattern} -> ${response}`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sinon, chercher une réponse par défaut
|
|
240
|
+
if (!responseToSend) {
|
|
241
|
+
for (const [pattern, response] of Object.entries(INTERACTIVE_RESPONSES)) {
|
|
242
|
+
if (response && lowerChunk.includes(pattern.toLowerCase())) {
|
|
243
|
+
responseToSend = response;
|
|
244
|
+
queue.log('info', `Réponse auto: ${pattern} -> ${response}`);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Si une réponse a été trouvée, l'envoyer
|
|
251
|
+
if (responseToSend) {
|
|
252
|
+
stream.write(responseToSend + '\n');
|
|
253
|
+
responseSent = true;
|
|
254
|
+
// Envoyer le marqueur de fin juste après
|
|
255
|
+
setTimeout(() => stream.write(`echo "${END_MARKER}"; exit\n`), 100);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
stream.stderr.on('data', (data) => {
|
|
261
|
+
output += `[STDERR] ${data.toString()}`;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Envoyer uniquement la commande initiale
|
|
265
|
+
stream.write(job.cmd + '\n');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
conn.on('error', (err) => {
|
|
270
|
+
reject(err);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const connectConfig = {
|
|
274
|
+
host: serverConfig.host,
|
|
275
|
+
port: 22,
|
|
276
|
+
username: serverConfig.user,
|
|
277
|
+
readyTimeout: 20000
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
(async () => {
|
|
281
|
+
if (serverConfig.keyPath) {
|
|
282
|
+
try {
|
|
283
|
+
connectConfig.privateKey = await fs.readFile(serverConfig.keyPath);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
reject(new Error(`Impossible de lire la clé SSH: ${err.message}`));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
} else if (serverConfig.password) {
|
|
289
|
+
connectConfig.password = serverConfig.password;
|
|
290
|
+
} else {
|
|
291
|
+
reject(new Error("Aucune méthode d'authentification configurée"));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
conn.connect(connectConfig);
|
|
296
|
+
})();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
// Nouvelle fonction pour exécuter plusieurs commandes en séquence
|
|
302
|
+
async function executeCommandSequence(jobId) {
|
|
303
|
+
const job = queue.getJob(jobId);
|
|
304
|
+
if (!job) return queue.log('error', `Tâche introuvable: ${jobId}`);
|
|
305
|
+
|
|
306
|
+
const results = [];
|
|
307
|
+
let connection = null;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const serverConfig = await serverManager.getServer(job.alias);
|
|
311
|
+
queue.updateJobStatus(jobId, 'running');
|
|
312
|
+
|
|
313
|
+
// Obtenir une connexion du pool
|
|
314
|
+
connection = await sshPool.getConnection(job.alias, serverConfig);
|
|
315
|
+
|
|
316
|
+
// Exécuter chaque commande en séquence
|
|
317
|
+
for (let i = 0; i < job.commands.length; i++) {
|
|
318
|
+
const cmd = job.commands[i];
|
|
319
|
+
const stepJob = {
|
|
320
|
+
...job,
|
|
321
|
+
cmd: typeof cmd === 'string' ? cmd : cmd.command,
|
|
322
|
+
timeout: cmd.timeout || job.timeout,
|
|
323
|
+
continueOnError: cmd.continueOnError || job.continueOnError
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
queue.log('info', `Exécution étape ${i+1}/${job.commands.length}: ${stepJob.cmd}`);
|
|
328
|
+
const result = await executeWithPooledConnection(connection, stepJob, `${jobId}_step_${i}`);
|
|
329
|
+
results.push({
|
|
330
|
+
step: i + 1,
|
|
331
|
+
command: stepJob.cmd,
|
|
332
|
+
success: true,
|
|
333
|
+
...result
|
|
334
|
+
});
|
|
335
|
+
} catch (err) {
|
|
336
|
+
results.push({
|
|
337
|
+
step: i + 1,
|
|
338
|
+
command: stepJob.cmd,
|
|
339
|
+
success: false,
|
|
340
|
+
error: err.message
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (!stepJob.continueOnError) {
|
|
344
|
+
throw new Error(`Échec à l'étape ${i+1}: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
queue.updateJobStatus(jobId, 'completed', { results });
|
|
350
|
+
|
|
351
|
+
} catch (err) {
|
|
352
|
+
queue.updateJobStatus(jobId, 'failed', {
|
|
353
|
+
error: err.message,
|
|
354
|
+
results
|
|
355
|
+
});
|
|
356
|
+
} finally {
|
|
357
|
+
if (connection) {
|
|
358
|
+
sshPool.releaseConnection(connection.id);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Obtenir les stats du pool
|
|
364
|
+
function getPoolStats() {
|
|
365
|
+
return sshPool.getStats();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function parseSystemResources(output) {
|
|
369
|
+
const resources = {
|
|
370
|
+
load_average: null,
|
|
371
|
+
memory: null,
|
|
372
|
+
disk: null
|
|
373
|
+
};
|
|
374
|
+
const lines = output.split('\n');
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const uptimeLine = lines.find(line => line.includes('load average:'));
|
|
378
|
+
if (uptimeLine) {
|
|
379
|
+
const parts = uptimeLine.split('load average:')[1];
|
|
380
|
+
resources.load_average = parts.split(',').map(s => s.trim());
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const memLine = lines.find(line => line.trim().startsWith('Mem:'));
|
|
384
|
+
if (memLine) {
|
|
385
|
+
const parts = memLine.trim().split(/\s+/);
|
|
386
|
+
resources.memory = {
|
|
387
|
+
total: parts[1],
|
|
388
|
+
used: parts[2],
|
|
389
|
+
free: parts[3],
|
|
390
|
+
available: parts[6]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const diskLine = lines.find(line => line.startsWith('/dev/'));
|
|
395
|
+
if (diskLine) {
|
|
396
|
+
const parts = diskLine.trim().split(/\s+/);
|
|
397
|
+
resources.disk = {
|
|
398
|
+
filesystem: parts[0],
|
|
399
|
+
total: parts[1],
|
|
400
|
+
used: parts[2],
|
|
401
|
+
available: parts[3],
|
|
402
|
+
use_percent: parts[4]
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {
|
|
406
|
+
// En cas d'erreur de parsing, retourner les données brutes
|
|
407
|
+
return { raw_output: output, parsing_error: e.message };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return resources;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseServicesStatus(output) {
|
|
414
|
+
const services = {
|
|
415
|
+
systemd: [],
|
|
416
|
+
docker: [],
|
|
417
|
+
pm2: []
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const sections = output.split(/---DOCKER---|---PM2---/);
|
|
422
|
+
const systemdRaw = sections[0] || '';
|
|
423
|
+
const dockerRaw = sections[1] || '';
|
|
424
|
+
const pm2Raw = sections[2] || '';
|
|
425
|
+
|
|
426
|
+
// Parse systemd
|
|
427
|
+
if (systemdRaw) {
|
|
428
|
+
const lines = systemdRaw.trim().split('\n');
|
|
429
|
+
lines.forEach(line => {
|
|
430
|
+
const match = line.match(/^(\S+)\s+loaded\s+active\s+running/);
|
|
431
|
+
if (match && match[1]) {
|
|
432
|
+
services.systemd.push({ name: match[1], status: 'running' });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Parse Docker
|
|
438
|
+
if (dockerRaw) {
|
|
439
|
+
const lines = dockerRaw.trim().split('\n').filter(Boolean);
|
|
440
|
+
lines.forEach(line => {
|
|
441
|
+
const parts = line.split(': ');
|
|
442
|
+
if (parts.length >= 2) {
|
|
443
|
+
services.docker.push({ name: parts[0].trim(), status: parts.slice(1).join(': ').trim() });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Parse PM2
|
|
449
|
+
if (pm2Raw) {
|
|
450
|
+
const lines = pm2Raw.trim().split('\n');
|
|
451
|
+
const tableStartIndex = lines.findIndex(line => line.includes('│ id │'));
|
|
452
|
+
if (tableStartIndex !== -1) {
|
|
453
|
+
const tableBody = lines.slice(tableStartIndex + 2, -1); // Skip header, separator, and footer
|
|
454
|
+
tableBody.forEach(line => {
|
|
455
|
+
const columns = line.split('│').map(s => s.trim()).filter(Boolean);
|
|
456
|
+
if (columns.length >= 4) { // id, name, namespace, status
|
|
457
|
+
services.pm2.push({ id: columns[0], name: columns[1], status: columns[3] });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { raw_output: output, parsing_error: e.message };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return services;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseApiHealth(output) {
|
|
470
|
+
try {
|
|
471
|
+
const [codeStr, timeStr] = output.split(':');
|
|
472
|
+
const http_code = parseInt(codeStr, 10);
|
|
473
|
+
const response_time_ms = parseFloat(timeStr) * 1000;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
status: http_code >= 200 && http_code < 300 ? 'UP' : 'DOWN',
|
|
477
|
+
http_code: http_code,
|
|
478
|
+
response_time_ms: Math.round(response_time_ms)
|
|
479
|
+
};
|
|
480
|
+
} catch (e) {
|
|
481
|
+
return { status: 'ERROR', http_code: 0, response_time_ms: 0, parsing_error: e.message, raw_output: output };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export default {
|
|
486
|
+
executeCommand,
|
|
487
|
+
executeCommandSequence,
|
|
488
|
+
getPoolStats,
|
|
489
|
+
parseSystemResources,
|
|
490
|
+
parseServicesStatus,
|
|
491
|
+
parseApiHealth
|
|
492
|
+
};
|