mcp-orchestrator-server
Version:
Serveur MCP pour l'orchestration de tâches distantes (SSH/SFTP) avec une file d'attente persistante.
275 lines (235 loc) • 10.3 kB
JavaScript
import { Client } from 'ssh2';
import fs from 'fs/promises';
import queue from './queue.js';
import config from './config.js';
class SSHConnectionPool {
constructor() {
this.pools = new Map(); // Map<serverAlias, Connection[]>
this.activeConnections = new Map(); // Map<connectionId, {conn, serverAlias, inUse, lastUsed}>
this.config = {
maxConnections: config.maxConnectionsPerServer,
minConnections: config.minConnectionsPerServer,
idleTimeout: config.idleTimeout,
keepAliveInterval: config.keepAliveInterval,
connectionTimeout: 20000,
retryAttempts: 3
};
// Nettoyage périodique des connexions inactives
this.startCleanupInterval();
}
// Obtenir ou créer une connexion
async getConnection(serverAlias, serverConfig) {
// Chercher une connexion disponible
const pool = this.pools.get(serverAlias) || [];
for (const connId of pool) {
const connInfo = this.activeConnections.get(connId);
if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
connInfo.inUse = true;
connInfo.lastUsed = Date.now();
queue.log('info', `Réutilisation connexion SSH existante pour ${serverAlias}`);
return { id: connId, client: connInfo.conn };
}
}
// Si pas de connexion disponible, en créer une nouvelle
if (pool.length < this.config.maxConnections) {
const newConn = await this.createConnection(serverAlias, serverConfig);
return newConn;
}
// Si pool plein, attendre qu'une connexion se libère
queue.log('warn', `Pool SSH saturé pour ${serverAlias}, attente...`);
return await this.waitForConnection(serverAlias, serverConfig);
}
// Créer une nouvelle connexion
async createConnection(serverAlias, serverConfig) {
const conn = new Client();
const connId = `${serverAlias}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
return new Promise((resolve, reject) => {
let retries = 0;
const tryConnect = async () => {
try {
const config = {
host: serverConfig.host,
port: 22,
username: serverConfig.user,
readyTimeout: this.config.connectionTimeout,
keepaliveInterval: this.config.keepAliveInterval,
keepaliveCountMax: 3
};
if (serverConfig.keyPath) {
config.privateKey = await fs.readFile(serverConfig.keyPath);
} else if (serverConfig.password) {
config.password = serverConfig.password;
}
conn.on('ready', () => {
queue.log('info', `Nouvelle connexion SSH établie pour ${serverAlias}`);
// Ajouter au pool
if (!this.pools.has(serverAlias)) {
this.pools.set(serverAlias, []);
}
this.pools.get(serverAlias).push(connId);
// Enregistrer la connexion
this.activeConnections.set(connId, {
conn,
serverAlias,
inUse: true,
lastUsed: Date.now(),
config: serverConfig
});
// Ajouter un flag pour vérifier si la connexion est prête
conn.isReady = true;
resolve({ id: connId, client: conn });
});
conn.on('error', (err) => {
conn.isReady = false;
if (retries < this.config.retryAttempts) {
retries++;
queue.log('warn', `Tentative ${retries}/${this.config.retryAttempts} de connexion à ${serverAlias}`);
setTimeout(tryConnect, 2000 * retries);
} else {
this.removeConnection(connId);
reject(new Error(`Impossible de se connecter à ${serverAlias}: ${err.message}`));
}
});
conn.on('close', () => {
conn.isReady = false;
this.removeConnection(connId);
queue.log('info', `Connexion SSH fermée pour ${serverAlias}`);
});
conn.connect(config);
} catch (err) {
reject(err);
}
};
tryConnect();
});
}
// Libérer une connexion
releaseConnection(connId) {
const connInfo = this.activeConnections.get(connId);
if (connInfo) {
connInfo.inUse = false;
connInfo.lastUsed = Date.now();
queue.log('debug', `Connexion ${connId} libérée`);
}
}
// Fermer une connexion spécifique
closeConnection(connId) {
const connInfo = this.activeConnections.get(connId);
if (connInfo) {
try {
connInfo.conn.end();
} catch (e) {
// Ignorer les erreurs de fermeture
}
this.removeConnection(connId);
}
}
// Retirer une connexion du pool
removeConnection(connId) {
const connInfo = this.activeConnections.get(connId);
if (connInfo) {
const pool = this.pools.get(connInfo.serverAlias);
if (pool) {
const index = pool.indexOf(connId);
if (index > -1) {
pool.splice(index, 1);
}
if (pool.length === 0) {
this.pools.delete(connInfo.serverAlias);
}
}
this.activeConnections.delete(connId);
}
}
// Attendre qu'une connexion se libère
async waitForConnection(serverAlias, serverConfig, timeout = 30000) {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
// Vérifier le timeout
if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
reject(new Error(`Timeout en attendant une connexion pour ${serverAlias}`));
return;
}
// Essayer d'obtenir une connexion
const pool = this.pools.get(serverAlias) || [];
for (const connId of pool) {
const connInfo = this.activeConnections.get(connId);
if (connInfo && !connInfo.inUse && connInfo.conn.isReady) {
clearInterval(checkInterval);
connInfo.inUse = true;
connInfo.lastUsed = Date.now();
resolve({ id: connId, client: connInfo.conn });
return;
}
}
}, 500);
});
}
// Nettoyer les connexions inactives
startCleanupInterval() {
setInterval(() => {
const now = Date.now();
for (const [connId, connInfo] of this.activeConnections) {
// Fermer les connexions inactives depuis trop longtemps
if (!connInfo.inUse && (now - connInfo.lastUsed) > this.config.idleTimeout) {
const pool = this.pools.get(connInfo.serverAlias) || [];
// Garder au moins minConnections
if (pool.length > this.config.minConnections) {
queue.log('info', `Fermeture connexion inactive: ${connId}`);
this.closeConnection(connId);
}
}
// Vérifier que la connexion est toujours vivante
if (!connInfo.conn.isReady && !connInfo.inUse) {
this.removeConnection(connId);
}
}
}, 60000); // Vérifier toutes les minutes
}
// Obtenir les statistiques du pool
getStats() {
const stats = {
totalConnections: this.activeConnections.size,
byServer: {}
};
for (const [serverAlias, pool] of this.pools) {
const connections = pool.map(connId => {
const info = this.activeConnections.get(connId);
return {
id: connId,
inUse: info?.inUse || false,
ready: info?.conn?.isReady || false,
lastUsed: info?.lastUsed
};
});
stats.byServer[serverAlias] = {
total: connections.length,
inUse: connections.filter(c => c.inUse).length,
available: connections.filter(c => !c.inUse && c.ready).length,
connections
};
}
return stats;
}
// Fermer toutes les connexions
closeAll() {
queue.log('info', 'Fermeture de toutes les connexions SSH...');
for (const connId of this.activeConnections.keys()) {
this.closeConnection(connId);
}
}
}
// Instance singleton
const sshPool = new SSHConnectionPool();
// Nettoyer à la fermeture du processus
process.on('SIGINT', () => {
sshPool.closeAll();
process.exit(0);
});
process.on('SIGTERM', () => {
sshPool.closeAll();
process.exit(0);
});
export default sshPool;