mc-access-bot/src/utils/serverControlManager.js

426 lines
16 KiB
JavaScript

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;