- 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
188 lines
6.7 KiB
JavaScript
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}` };
|
|
}
|
|
} |