@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/sshPool.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import queue from './queue.js';
|
|
4
|
+
|
|
5
|
+
class SSHConnectionPool {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.pools = new Map(); // Map<serverAlias, Connection[]>
|
|
8
|
+
this.activeConnections = new Map(); // Map<connectionId, {conn, serverAlias, inUse, lastUsed}>
|
|
9
|
+
this.config = {
|
|
10
|
+
maxConnections: 5, // Max connexions par serveur
|
|
11
|
+
minConnections: 1, // Min connexions maintenues
|
|
12
|
+
idleTimeout: 300000, // 5 minutes avant fermeture si inactif
|
|
13
|
+
keepAliveInterval: 30000, // Keep-alive toutes les 30s
|
|
14
|
+
connectionTimeout: 20000,
|
|
15
|
+
retryAttempts: 3
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Nettoyage périodique des connexions inactives
|
|
19
|
+
this.startCleanupInterval();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Obtenir ou créer une connexion
|
|
23
|
+
async getConnection(serverAlias, serverConfig) {
|
|
24
|
+
// Chercher une connexion disponible
|
|
25
|
+
const pool = this.pools.get(serverAlias) || [];
|
|
26
|
+
|
|
27
|
+
for (const connId of pool) {
|
|
28
|
+
const connInfo = this.activeConnections.get(connId);
|
|
29
|
+
if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
|
|
30
|
+
connInfo.inUse = true;
|
|
31
|
+
connInfo.lastUsed = Date.now();
|
|
32
|
+
queue.log('info', `Réutilisation connexion SSH existante pour ${serverAlias}`);
|
|
33
|
+
return { id: connId, client: connInfo.conn };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Si pas de connexion disponible, en créer une nouvelle
|
|
38
|
+
if (pool.length < this.config.maxConnections) {
|
|
39
|
+
const newConn = await this.createConnection(serverAlias, serverConfig);
|
|
40
|
+
return newConn;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Si pool plein, attendre qu'une connexion se libère
|
|
44
|
+
queue.log('warn', `Pool SSH saturé pour ${serverAlias}, attente...`);
|
|
45
|
+
return await this.waitForConnection(serverAlias, serverConfig);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Créer une nouvelle connexion
|
|
49
|
+
async createConnection(serverAlias, serverConfig) {
|
|
50
|
+
const conn = new Client();
|
|
51
|
+
const connId = `${serverAlias}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
52
|
+
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let retries = 0;
|
|
55
|
+
|
|
56
|
+
const tryConnect = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const config = {
|
|
59
|
+
host: serverConfig.host,
|
|
60
|
+
port: 22,
|
|
61
|
+
username: serverConfig.user,
|
|
62
|
+
readyTimeout: this.config.connectionTimeout,
|
|
63
|
+
keepaliveInterval: this.config.keepAliveInterval,
|
|
64
|
+
keepaliveCountMax: 3
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (serverConfig.keyPath) {
|
|
68
|
+
config.privateKey = await fs.readFile(serverConfig.keyPath);
|
|
69
|
+
} else if (serverConfig.password) {
|
|
70
|
+
config.password = serverConfig.password;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
conn.on('ready', () => {
|
|
74
|
+
queue.log('info', `Nouvelle connexion SSH établie pour ${serverAlias}`);
|
|
75
|
+
|
|
76
|
+
// Ajouter au pool
|
|
77
|
+
if (!this.pools.has(serverAlias)) {
|
|
78
|
+
this.pools.set(serverAlias, []);
|
|
79
|
+
}
|
|
80
|
+
this.pools.get(serverAlias).push(connId);
|
|
81
|
+
|
|
82
|
+
// Enregistrer la connexion
|
|
83
|
+
this.activeConnections.set(connId, {
|
|
84
|
+
conn,
|
|
85
|
+
serverAlias,
|
|
86
|
+
inUse: true,
|
|
87
|
+
lastUsed: Date.now(),
|
|
88
|
+
config: serverConfig
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Ajouter un flag pour vérifier si la connexion est prête
|
|
92
|
+
conn.isReady = true;
|
|
93
|
+
|
|
94
|
+
resolve({ id: connId, client: conn });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
conn.on('error', (err) => {
|
|
98
|
+
conn.isReady = false;
|
|
99
|
+
if (retries < this.config.retryAttempts) {
|
|
100
|
+
retries++;
|
|
101
|
+
queue.log('warn', `Tentative ${retries}/${this.config.retryAttempts} de connexion à ${serverAlias}`);
|
|
102
|
+
setTimeout(tryConnect, 2000 * retries);
|
|
103
|
+
} else {
|
|
104
|
+
this.removeConnection(connId);
|
|
105
|
+
reject(new Error(`Impossible de se connecter à ${serverAlias}: ${err.message}`));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
conn.on('close', () => {
|
|
110
|
+
conn.isReady = false;
|
|
111
|
+
this.removeConnection(connId);
|
|
112
|
+
queue.log('info', `Connexion SSH fermée pour ${serverAlias}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
conn.connect(config);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
reject(err);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
tryConnect();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Libérer une connexion
|
|
126
|
+
releaseConnection(connId) {
|
|
127
|
+
const connInfo = this.activeConnections.get(connId);
|
|
128
|
+
if (connInfo) {
|
|
129
|
+
connInfo.inUse = false;
|
|
130
|
+
connInfo.lastUsed = Date.now();
|
|
131
|
+
queue.log('debug', `Connexion ${connId} libérée`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fermer une connexion spécifique
|
|
136
|
+
closeConnection(connId) {
|
|
137
|
+
const connInfo = this.activeConnections.get(connId);
|
|
138
|
+
if (connInfo) {
|
|
139
|
+
try {
|
|
140
|
+
connInfo.conn.end();
|
|
141
|
+
} catch (e) {
|
|
142
|
+
// Ignorer les erreurs de fermeture
|
|
143
|
+
}
|
|
144
|
+
this.removeConnection(connId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Retirer une connexion du pool
|
|
149
|
+
removeConnection(connId) {
|
|
150
|
+
const connInfo = this.activeConnections.get(connId);
|
|
151
|
+
if (connInfo) {
|
|
152
|
+
const pool = this.pools.get(connInfo.serverAlias);
|
|
153
|
+
if (pool) {
|
|
154
|
+
const index = pool.indexOf(connId);
|
|
155
|
+
if (index > -1) {
|
|
156
|
+
pool.splice(index, 1);
|
|
157
|
+
}
|
|
158
|
+
if (pool.length === 0) {
|
|
159
|
+
this.pools.delete(connInfo.serverAlias);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.activeConnections.delete(connId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Attendre qu'une connexion se libère
|
|
167
|
+
async waitForConnection(serverAlias, serverConfig, timeout = 30000) {
|
|
168
|
+
const startTime = Date.now();
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const checkInterval = setInterval(async () => {
|
|
172
|
+
// Vérifier le timeout
|
|
173
|
+
if (Date.now() - startTime > timeout) {
|
|
174
|
+
clearInterval(checkInterval);
|
|
175
|
+
reject(new Error(`Timeout en attendant une connexion pour ${serverAlias}`));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Essayer d'obtenir une connexion
|
|
180
|
+
const pool = this.pools.get(serverAlias) || [];
|
|
181
|
+
for (const connId of pool) {
|
|
182
|
+
const connInfo = this.activeConnections.get(connId);
|
|
183
|
+
if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
|
|
184
|
+
clearInterval(checkInterval);
|
|
185
|
+
connInfo.inUse = true;
|
|
186
|
+
connInfo.lastUsed = Date.now();
|
|
187
|
+
resolve({ id: connId, client: connInfo.conn });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, 500);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Nettoyer les connexions inactives
|
|
196
|
+
startCleanupInterval() {
|
|
197
|
+
setInterval(() => {
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
|
|
200
|
+
for (const [connId, connInfo] of this.activeConnections) {
|
|
201
|
+
// Fermer les connexions inactives depuis trop longtemps
|
|
202
|
+
if (!connInfo.inUse && (now - connInfo.lastUsed) > this.config.idleTimeout) {
|
|
203
|
+
const pool = this.pools.get(connInfo.serverAlias) || [];
|
|
204
|
+
|
|
205
|
+
// Garder au moins minConnections
|
|
206
|
+
if (pool.length > this.config.minConnections) {
|
|
207
|
+
queue.log('info', `Fermeture connexion inactive: ${connId}`);
|
|
208
|
+
this.closeConnection(connId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Vérifier que la connexion est toujours vivante
|
|
213
|
+
if (!connInfo.conn.isReady && !connInfo.inUse) {
|
|
214
|
+
this.removeConnection(connId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}, 60000); // Vérifier toutes les minutes
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Obtenir les statistiques du pool
|
|
221
|
+
getStats() {
|
|
222
|
+
const stats = {
|
|
223
|
+
totalConnections: this.activeConnections.size,
|
|
224
|
+
byServer: {}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (const [serverAlias, pool] of this.pools) {
|
|
228
|
+
const connections = pool.map(connId => {
|
|
229
|
+
const info = this.activeConnections.get(connId);
|
|
230
|
+
return {
|
|
231
|
+
id: connId,
|
|
232
|
+
inUse: info?.inUse || false,
|
|
233
|
+
ready: info?.conn?.isReady || false,
|
|
234
|
+
lastUsed: info?.lastUsed
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
stats.byServer[serverAlias] = {
|
|
239
|
+
total: connections.length,
|
|
240
|
+
inUse: connections.filter(c => c.inUse).length,
|
|
241
|
+
available: connections.filter(c => !c.inUse && c.ready).length,
|
|
242
|
+
connections
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return stats;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fermer toutes les connexions
|
|
250
|
+
closeAll() {
|
|
251
|
+
queue.log('info', 'Fermeture de toutes les connexions SSH...');
|
|
252
|
+
for (const connId of this.activeConnections.keys()) {
|
|
253
|
+
this.closeConnection(connId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Instance singleton
|
|
259
|
+
const sshPool = new SSHConnectionPool();
|
|
260
|
+
|
|
261
|
+
// Nettoyer à la fermeture du processus
|
|
262
|
+
process.on('SIGINT', () => {
|
|
263
|
+
sshPool.closeAll();
|
|
264
|
+
process.exit(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
process.on('SIGTERM', () => {
|
|
268
|
+
sshPool.closeAll();
|
|
269
|
+
process.exit(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
export default sshPool;
|
package/test_features.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import queue from './queue.js';
|
|
4
|
+
import sshPool from './sshPool.js';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
console.log('🧪 Test des fonctionnalités MCP Orchestrator v6.0\n');
|
|
9
|
+
|
|
10
|
+
// Test 1: Queue persistante
|
|
11
|
+
console.log('✅ Test Queue:');
|
|
12
|
+
const testJob = queue.addJob({
|
|
13
|
+
type: 'test',
|
|
14
|
+
alias: 'test_server',
|
|
15
|
+
cmd: 'echo "Test"',
|
|
16
|
+
status: 'pending'
|
|
17
|
+
});
|
|
18
|
+
console.log(` - Tâche créée: ${testJob.id}`);
|
|
19
|
+
console.log(` - Stats queue:`, queue.getStats());
|
|
20
|
+
|
|
21
|
+
// Test 2: Pool de connexions
|
|
22
|
+
console.log('\n✅ Test Pool SSH:');
|
|
23
|
+
const poolStats = sshPool.getStats();
|
|
24
|
+
console.log(` - Connexions totales: ${poolStats.totalConnections}`);
|
|
25
|
+
console.log(` - Serveurs actifs: ${Object.keys(poolStats.byServer).length}`);
|
|
26
|
+
|
|
27
|
+
// Test 3: Patterns Glob
|
|
28
|
+
console.log('\n✅ Test Patterns Glob:');
|
|
29
|
+
async function testGlob() {
|
|
30
|
+
const patterns = ['*.js', '*.json'];
|
|
31
|
+
for (const pattern of patterns) {
|
|
32
|
+
const files = await glob(pattern, { cwd: process.cwd() });
|
|
33
|
+
console.log(` - Pattern "${pattern}": ${files.length} fichiers trouvés`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Test 4: Détection de prompts interactifs
|
|
38
|
+
console.log('\n✅ Test Détection Prompts:');
|
|
39
|
+
const testPrompts = [
|
|
40
|
+
'Are you sure you want to continue? (yes/no)',
|
|
41
|
+
'Password:',
|
|
42
|
+
'Do you want to continue [Y/n]?',
|
|
43
|
+
'Save configuration?'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function detectPrompt(text) {
|
|
47
|
+
const patterns = {
|
|
48
|
+
'continue': /continue.*\?/i,
|
|
49
|
+
'password': /password:/i,
|
|
50
|
+
'yes/no': /\(yes\/no\)/i,
|
|
51
|
+
'y/n': /\[y\/n\]/i,
|
|
52
|
+
'save': /save.*\?/i
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const [name, regex] of Object.entries(patterns)) {
|
|
56
|
+
if (regex.test(text)) {
|
|
57
|
+
return name;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
testPrompts.forEach(prompt => {
|
|
64
|
+
const detected = detectPrompt(prompt);
|
|
65
|
+
console.log(` - "${prompt.substring(0, 30)}..." → ${detected || 'non détecté'}`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Test 5: Logs système
|
|
69
|
+
console.log('\n✅ Test Logs:');
|
|
70
|
+
queue.log('info', 'Test log info');
|
|
71
|
+
queue.log('warn', 'Test log warning');
|
|
72
|
+
queue.log('error', 'Test log error');
|
|
73
|
+
const logs = queue.getLogs({ limit: 3 });
|
|
74
|
+
console.log(` - ${logs.length} logs récupérés`);
|
|
75
|
+
|
|
76
|
+
// Test 6: Gestion des tâches crashées
|
|
77
|
+
console.log('\n✅ Test Gestion Crashes:');
|
|
78
|
+
const crashedJob = queue.addJob({
|
|
79
|
+
type: 'test_crash',
|
|
80
|
+
alias: 'test',
|
|
81
|
+
status: 'running'
|
|
82
|
+
});
|
|
83
|
+
queue.updateJobStatus(crashedJob.id, 'crashed', {
|
|
84
|
+
canRetry: true,
|
|
85
|
+
crashedAt: new Date()
|
|
86
|
+
});
|
|
87
|
+
const crashed = queue.getCrashedJobs();
|
|
88
|
+
console.log(` - Tâches crashées: ${crashed.length}`);
|
|
89
|
+
if (crashed.length > 0) {
|
|
90
|
+
console.log(` - Peut être relancée: ${crashed[0].canRetry}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Test 7: Statistiques
|
|
94
|
+
console.log('\n✅ Résumé des Statistiques:');
|
|
95
|
+
const finalStats = queue.getStats();
|
|
96
|
+
console.log(` - Total tâches: ${finalStats.total}`);
|
|
97
|
+
console.log(` - Par statut:`, finalStats.byStatus);
|
|
98
|
+
console.log(` - Par type:`, finalStats.byType);
|
|
99
|
+
console.log(` - Taux de succès: ${finalStats.successRate}%`);
|
|
100
|
+
|
|
101
|
+
// Exécuter les tests asynchrones
|
|
102
|
+
testGlob().then(() => {
|
|
103
|
+
console.log('\n✨ Tous les tests sont passés avec succès!');
|
|
104
|
+
console.log('📝 Note: Ce sont des tests unitaires, pas des tests de connexion réelle.');
|
|
105
|
+
|
|
106
|
+
// Nettoyer
|
|
107
|
+
queue.cleanOldJobs();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}).catch(err => {
|
|
110
|
+
console.error('❌ Erreur lors des tests:', err);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|