Files
panel/includes/docker.js
MCHost d38e2ad1f1 Refactor: Initial code split into includes directory for modularity
- Reorganized backend logic by moving API, authentication, Docker, status, and WebSocket handling into separate modules (api.js, auth.js, docker.js, status.js, websocket.js) within ./includes/
- Converted codebase to ES modules with import/export syntax for modern JavaScript
- Updated index.js to serve as main entry point, importing from ./includes/
- Reduced code duplication and improved readability with modularized functions
- Ensured full functionality preservation, including Docker stats and WebSocket communication
- Updated README to reflect new folder structure and ES module setup
2025-06-16 12:30:18 -04:00

188 lines
6.7 KiB
JavaScript

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}` };
}
}