const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const fs = require('fs').promises; class ServerControlManager { constructor() { this.serverPath = process.env.MINECRAFT_SERVER_PATH || '/opt/minecraft/server'; this.serviceName = process.env.MINECRAFT_SERVICE_NAME || 'minecraft'; this.screenName = 'minecraft'; // Nom de la session screen this.serverUser = 'minecraft'; // Utilisateur qui lance le serveur this.whitelistPath = process.env.MINECRAFT_WHITELIST_PATH; this.useScreen = process.env.USE_SCREEN === 'true'; // true pour screen, false pour systemctl // Détecter le type de serveur et le fichier JAR this.serverJar = this.detectServerJar(); this.javaArgs = process.env.JAVA_ARGS || '-Xmx2G -Xms1G'; } detectServerJar() { // Ordre de priorité pour détecter le fichier JAR const possibleJars = [ 'fabric-server.jar', 'fabric-server-launch.jar', 'server.jar', 'forge-server.jar', 'paper.jar', 'spigot.jar' ]; // Pour l'instant, on retourne fabric-server.jar puisque c'est ce que vous utilisez // Plus tard, on pourrait faire une détection automatique return process.env.SERVER_JAR || 'fabric-server.jar'; } async executeCommand(command, description = '') { try { console.log(`🔧 Exécution: ${description || command}`); const { stdout, stderr } = await execAsync(command); // Log des détails pour debug if (stdout) console.log(`📤 Sortie: ${stdout.trim()}`); if (stderr && !stderr.includes('Warning')) { console.warn(`⚠️ Erreur stderr: ${stderr.trim()}`); return { success: false, error: stderr.trim(), output: stdout }; } console.log(`✅ Succès: ${description || command}`); return { success: true, output: stdout, error: stderr }; } catch (error) { console.error(`❌ Erreur: ${description || command} - ${error.message}`); return { success: false, error: error.message, output: '' }; } } async reloadWhitelist() { console.log('🔄 Rechargement de la whitelist...'); if (this.useScreen) { // Méthode screen : commande directe const result = await this.executeCommand( `sudo -u ${this.serverUser} screen -S ${this.screenName} -p 0 -X stuff "whitelist reload\n"`, 'Rechargement whitelist via commande' ); if (result.success) { return { success: true, message: 'Whitelist rechargée via commande serveur' }; } } // Fallback systemctl : redémarrage const restartResult = await this.executeCommand( `sudo systemctl restart ${this.serviceName}`, 'Redémarrage pour appliquer la whitelist' ); if (restartResult.success) { return { success: true, message: 'Whitelist rechargée (serveur redémarré)' }; } return { success: false, message: 'Impossible de recharger la whitelist' }; } async getServerStatus() { if (this.useScreen) { // Vérifier si la session screen existe (nom exact ou suffixé) const screenResult = await this.executeCommand( `screen -ls | grep -w "${this.screenName}"`, 'Vérification session screen' ); const screenRunning = screenResult.success && screenResult.output.trim() !== ''; // Double vérification : chercher le processus Java avec le bon jar const javaResult = await this.executeCommand( `pgrep -f "${this.serverJar}" | wc -l`, 'Vérification processus Java Fabric' ); const javaRunning = javaResult.success && parseInt(javaResult.output.trim()) > 0; return { running: screenRunning && javaRunning, enabled: true, status: screenRunning ? (javaRunning ? 'Running in screen with Java process' : 'Screen session exists but no Java process') : 'No screen session found', details: { screen: screenRunning, java: javaRunning, screenOutput: screenResult.output.trim(), javaOutput: javaResult.output.trim() } }; } // Méthode systemctl avec vérification plus précise const result = await this.executeCommand( `sudo systemctl is-active ${this.serviceName}`, 'Vérification active systemctl' ); const isActive = result.success && result.output.trim() === 'active'; const enabledResult = await this.executeCommand( `sudo systemctl is-enabled ${this.serviceName}`, 'Vérification enabled systemctl' ); const isEnabled = enabledResult.success && enabledResult.output.trim() === 'enabled'; return { running: isActive, enabled: isEnabled, status: `Active: ${isActive ? 'active' : result.output.trim()}, Enabled: ${isEnabled ? 'enabled' : enabledResult.output.trim()}` }; } async startServer() { if (this.useScreen) { // Vérifier d'abord si le serveur n'est pas déjà lancé const status = await this.getServerStatus(); if (status.running) { return { success: false, message: 'Le serveur est déjà en cours d\'exécution' }; } // Forcer le nom de la session screen à "minecraft" const result = await this.executeCommand( `sudo -u ${this.serverUser} screen -dmS ${this.screenName} bash -c "cd ${this.serverPath} && java ${this.javaArgs} -jar ${this.serverJar} nogui"`, 'Démarrage du serveur avec screen (nom forcé)' ); if (result.success) { // Attendre un peu et vérifier que ça a vraiment démarré await new Promise(resolve => setTimeout(resolve, 3000)); const newStatus = await this.getServerStatus(); return { success: newStatus.running, message: newStatus.running ? 'Serveur démarré avec screen' : `Échec démarrage: ${result.error || 'Processus non trouvé'}` }; } return { success: false, message: `Erreur: ${result.error}` }; } // Méthode systemctl avec vérification const result = await this.executeCommand( `sudo systemctl start ${this.serviceName}`, 'Démarrage du serveur' ); if (result.success) { // Vérifier que ça a vraiment démarré await new Promise(resolve => setTimeout(resolve, 2000)); const status = await this.getServerStatus(); return { success: status.running, message: status.running ? 'Serveur démarré avec succès' : `Échec: ${status.status}` }; } return { success: false, message: `Erreur: ${result.error}` }; } async stopServer() { if (this.useScreen) { // Arrêter proprement avec la commande stop const stopResult = await this.executeCommand( `sudo -u ${this.serverUser} screen -S ${this.screenName} -p 0 -X stuff "stop\n"`, 'Arrêt propre du serveur' ); // Attendre un peu puis forcer si nécessaire await new Promise(resolve => setTimeout(resolve, 5000)); const killResult = await this.executeCommand( `sudo -u ${this.serverUser} screen -S ${this.screenName} -X quit`, 'Fermeture session screen' ); return { success: true, message: 'Serveur arrêté' }; } // Méthode systemctl const result = await this.executeCommand( `sudo systemctl stop ${this.serviceName}`, 'Arrêt du serveur' ); return { success: result.success, message: result.success ? 'Serveur arrêté avec succès' : `Erreur: ${result.error}` }; } async restartServer() { if (this.useScreen) { // Arrêter puis redémarrer avec screen console.log('🔄 Arrêt du serveur...'); await this.stopServer(); // Attendre un peu await new Promise(resolve => setTimeout(resolve, 3000)); console.log('🔄 Redémarrage du serveur...'); return await this.startServer(); } // Méthode systemctl const result = await this.executeCommand( `sudo systemctl restart ${this.serviceName}`, 'Redémarrage du serveur' ); return { success: result.success, message: result.success ? 'Serveur redémarré avec succès' : `Erreur: ${result.error}` }; } async getServerLogs(lines = 50) { const result = await this.executeCommand( `sudo journalctl -u ${this.serviceName} -n ${lines} --no-pager`, `Récupération des ${lines} dernières lignes de logs` ); return { success: result.success, logs: result.output, error: result.error }; } async sendServerCommand(command) { if (this.useScreen) { // Envoyer directement via screen const result = await this.executeCommand( `sudo -u ${this.serverUser} screen -S ${this.screenName} -p 0 -X stuff "${command}\n"`, `Envoi commande: ${command}` ); return { success: result.success, message: result.success ? `Commande "${command}" envoyée` : `Erreur: ${result.error}` }; } // Pour systemctl, impossible d'envoyer des commandes directes return { success: false, message: 'Envoi de commandes non supporté avec systemctl (utilisez screen)' }; } async backupWorld() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `/opt/minecraft/backups/world_${timestamp}.tar.gz`; const result = await this.executeCommand( `sudo mkdir -p /opt/minecraft/backups && sudo tar -czf ${backupPath} -C ${this.serverPath} world`, 'Sauvegarde du monde' ); return { success: result.success, message: result.success ? `Monde sauvegardé: ${backupPath}` : `Erreur: ${result.error}`, backupPath: result.success ? backupPath : null }; } async getWhitelistFromFile() { try { const data = await fs.readFile(this.whitelistPath, 'utf8'); return JSON.parse(data); } catch (error) { console.error('Erreur lecture whitelist:', error); return []; } } async saveWhitelistToFile(whitelist) { try { await fs.writeFile(this.whitelistPath, JSON.stringify(whitelist, null, 2)); return true; } catch (error) { console.error('Erreur écriture whitelist:', error); return false; } } async getServerProperties() { try { const propsPath = `${this.serverPath}/server.properties`; const data = await fs.readFile(propsPath, 'utf8'); const properties = {}; data.split('\n').forEach(line => { if (line.trim() && !line.startsWith('#')) { const [key, value] = line.split('='); if (key && value !== undefined) { properties[key.trim()] = value.trim(); } } }); return { success: true, properties }; } catch (error) { return { success: false, error: error.message }; } } async updateServerProperty(key, value) { try { const propsPath = `${this.serverPath}/server.properties`; let data = await fs.readFile(propsPath, 'utf8'); // Remplacer ou ajouter la propriété const regex = new RegExp(`^${key}=.*$`, 'm'); if (regex.test(data)) { data = data.replace(regex, `${key}=${value}`); } else { data += `\n${key}=${value}\n`; } await fs.writeFile(propsPath, data); return { success: true, message: `Propriété ${key} mise à jour: ${value}` }; } catch (error) { return { success: false, error: error.message }; } } async setupServerForBot() { console.log('🔧 Configuration automatique du serveur pour le bot...'); const results = []; // Vérifier si l'utilisateur minecraft existe déjà const userCheck = await this.executeCommand( 'id minecraft 2>/dev/null', 'Vérification utilisateur minecraft' ); if (!userCheck.success && this.useScreen) { const userResult = await this.executeCommand( 'sudo useradd -r -s /bin/bash minecraft', 'Création utilisateur minecraft' ); results.push(userResult.success ? '✅ Utilisateur minecraft créé' : '❌ Échec création utilisateur'); } else if (this.useScreen) { results.push('✅ Utilisateur minecraft existe déjà'); } // Vérifier et créer les dossiers try { await fs.access(this.serverPath); results.push('✅ Dossier serveur existe'); } catch (error) { const dirResult = await this.executeCommand( `sudo mkdir -p ${this.serverPath}`, 'Création dossier serveur' ); results.push(dirResult.success ? '✅ Dossier serveur créé' : '❌ Échec création dossier'); } // Configuration permissions si screen if (this.useScreen) { const permResult = await this.executeCommand( `sudo chown -R minecraft:minecraft /opt/minecraft`, 'Configuration permissions' ); results.push(permResult.success ? '✅ Permissions configurées' : '⚠️ Échec permissions'); } // Configuration server.properties const whitelistResult = await this.updateServerProperty('white-list', 'true'); const enforceResult = await this.updateServerProperty('enforce-whitelist', 'true'); results.push(whitelistResult.success ? '✅ Whitelist activée' : '⚠️ Échec whitelist'); results.push(enforceResult.success ? '✅ Enforce-whitelist activé' : '⚠️ Échec enforce-whitelist'); // Création dossier backups const backupResult = await this.executeCommand( 'sudo mkdir -p /opt/minecraft/backups', 'Création dossier backups' ); results.push(backupResult.success ? '✅ Dossier backups créé' : '⚠️ Échec dossier backups'); return { success: true, message: 'Configuration terminée', details: results }; } } module.exports = ServerControlManager;