import Docker from 'dockerode'; import { promisify } from 'util'; import { exec } from 'child_process'; import path from 'path'; const execPromise = promisify(exec); export function setupDocker() { return new Docker({ socketPath: process.env.DOCKER_SOCKET_PATH }); } export async function getContainerStats(docker, containerName) { try { const container = docker.getContainer(containerName); const [containers, info, stats] = await Promise.all([ docker.listContainers({ all: true }), container.inspect(), container.stats({ stream: false }) ]); if (!containers.some(c => c.Names.includes(`/${containerName}`))) { return { error: `Container ${containerName} not found` }; } const memoryUsage = stats.memory_stats.usage / 1024 / 1024; const memoryLimit = stats.memory_stats.limit / 1024 / 1024 / 1024; const memoryPercent = ((memoryUsage / (memoryLimit * 1024)) * 100).toFixed(2); const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats.cpu_usage?.total_usage || 0); const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats.system_cpu_usage || 0); const cpuPercent = systemDelta > 0 ? ((cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100).toFixed(2) : 0; return { status: info.State.Status, memory: { raw: `${memoryUsage.toFixed(2)}MiB / ${memoryLimit.toFixed(2)}GiB`, percent: memoryPercent }, cpu: cpuPercent }; } catch (error) { console.error(`Docker stats error for ${containerName}:`, error.message); return { error: `Failed to fetch stats for ${containerName}: ${error.message}` }; } } export async function streamContainerLogs(docker, ws, containerName, client) { let isStreaming = true; let isStartingStream = false; const startLogStream = async () => { if (isStartingStream) return false; isStartingStream = true; try { const container = docker.getContainer(containerName); const [containers, inspect] = await Promise.all([ docker.listContainers({ all: true }), container.inspect() ]); if (!containers.some(c => c.Names.includes(`/${containerName}`))) { if (isStreaming) ws.send(JSON.stringify({ type: 'docker-logs', error: `Container ${containerName} not found` })); return false; } if (inspect.State.Status !== 'running') { if (isStreaming) ws.send(JSON.stringify({ type: 'docker-logs', error: `Container ${containerName} is not running` })); return false; } if (client.logStream) { client.logStream.removeAllListeners(); client.logStream.destroy(); client.logStream = null; } const logStream = await container.logs({ follow: true, stdout: true, stderr: true, tail: parseInt(process.env.LOG_STREAM_TAIL_LINES, 10), timestamps: true }); logStream.on('data', (chunk) => { if (isStreaming && client.logStream === logStream) { ws.send(JSON.stringify({ type: 'docker-logs', data: { log: chunk.toString('utf8') } })); } }); logStream.on('error', (error) => { if (isStreaming) ws.send(JSON.stringify({ type: 'docker-logs', error: `Log stream error: ${error.message}` })); }); client.logStream = logStream; return true; } catch (error) { if (isStreaming) ws.send(JSON.stringify({ type: 'docker-logs', error: `Failed to stream logs: ${error.message}` })); return false; } finally { isStartingStream = false; } }; const monitorContainer = async () => { try { const container = docker.getContainer(containerName); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { if (client.logStream) { client.logStream.removeAllListeners(); client.logStream.destroy(); client.logStream = null; } return false; } return true; } catch (error) { return false; } }; if (!(await startLogStream())) { const monitorInterval = setInterval(async () => { if (!isStreaming) return clearInterval(monitorInterval); if (await monitorContainer() && !client.logStream && !isStartingStream) { await startLogStream(); } }, parseInt(process.env.LOG_STREAM_MONITOR_INTERVAL_MS, 10)); ws.on('close', () => { isStreaming = false; clearInterval(monitorInterval); if (client.logStream) { client.logStream.removeAllListeners(); client.logStream.destroy(); client.logStream = null; } }); return; } const monitorInterval = setInterval(async () => { if (!isStreaming) return clearInterval(monitorInterval); if (await monitorContainer() && !client.logStream && !isStartingStream) { await startLogStream(); } }, parseInt(process.env.LOG_STREAM_MONITOR_INTERVAL_MS, 10)); ws.on('close', () => { isStreaming = false; clearInterval(monitorInterval); if (client.logStream) { client.logStream.removeAllListeners(); client.logStream.destroy(); client.logStream = null; } }); } export async function readServerProperties(docker, containerName) { try { const container = docker.getContainer(containerName); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { return { error: `Container ${containerName} is not running` }; } const { stdout, stderr } = await execPromise(`docker exec ${containerName} bash -c "cat ${process.env.SERVER_PROPERTIES_PATH}"`); if (stderr) return { error: 'Failed to read server.properties' }; return { content: stdout }; } catch (error) { return { error: `Failed to read server.properties: ${error.message}` }; } } export async function writeServerProperties(docker, containerName, content) { try { const { randomBytes } = await import('crypto'); const tmpDir = process.env.TEMP_DIR; const randomId = randomBytes(parseInt(process.env.TEMP_FILE_RANDOM_ID_BYTES, 10)).toString('hex'); const tmpFile = path.join(tmpDir, `server_properties_${randomId}.tmp`); const containerFilePath = `${process.env.CONTAINER_TEMP_FILE_PREFIX}${randomId}.tmp`; await (await import('fs')).promises.writeFile(tmpFile, content); await execPromise(`docker cp ${tmpFile} ${containerName}:${containerFilePath}`); await execPromise(`docker exec ${containerName} bash -c "mv ${containerFilePath} ${process.env.SERVER_PROPERTIES_PATH} && chown mc:mc ${process.env.SERVER_PROPERTIES_PATH}"`); await (await import('fs')).promises.unlink(tmpFile).catch(err => console.error(`Error deleting temp file: ${err.message}`)); return { message: 'Server properties updated' }; } catch (error) { return { error: `Failed to write server.properties: ${error.message}` }; } } export async function updateMods(docker, containerName) { try { const container = docker.getContainer(containerName); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { return { error: `Container ${containerName} is not running` }; } const { stdout, stderr } = await execPromise(`docker exec ${containerName} bash -c 'cd /home/mc/minecraft && mod-manager update'`); if (stderr) return { output: stderr }; return { output: stdout || 'Mod update completed successfully.' }; } catch (error) { return { error: `Failed to update mods: ${error.message}` }; } } export async function createBackup(docker, containerName) { try { const container = docker.getContainer(containerName); const inspect = await container.inspect(); if (inspect.State.Status !== 'running') { return { error: `Container ${containerName} is not running` }; } const command = `docker exec -t ${containerName} bash -c "/home/backup.sh | grep export"`; const { stdout, stderr } = await execPromise(command); if (stderr) return { error: stderr }; // Extract the URL using a regular expression const urlRegex = /(https:\/\/[^\s]+)/; const match = stdout.match(urlRegex); if (!match) return { error: 'No download URL found in backup output' }; const downloadURL = match[0]; return { output: 'Backup completed successfully', downloadURL }; } catch (error) { return { error: `Failed to create backup: ${error.message}` }; } }