xd
This commit is contained in:
286
gsm-backend/services/ssh.js
Normal file
286
gsm-backend/services/ssh.js
Normal file
@@ -0,0 +1,286 @@
|
||||
import { NodeSSH } from 'node-ssh';
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const sshConnections = new Map();
|
||||
|
||||
function loadConfig() {
|
||||
return JSON.parse(readFileSync(join(__dirname, '..', 'config.json'), 'utf-8'));
|
||||
}
|
||||
|
||||
async function getConnection(host, username = "root") {
|
||||
if (sshConnections.has(username + "@" + host)) {
|
||||
const conn = sshConnections.get(username + "@" + host);
|
||||
if (conn.isConnected()) return conn;
|
||||
}
|
||||
|
||||
const ssh = new NodeSSH();
|
||||
await ssh.connect({
|
||||
host,
|
||||
username: username,
|
||||
privateKeyPath: '/root/.ssh/id_ed25519'
|
||||
});
|
||||
|
||||
sshConnections.set(username + "@" + host, ssh);
|
||||
return ssh;
|
||||
}
|
||||
|
||||
// Returns: "online", "starting", "stopping", "offline"
|
||||
export async function getServerStatus(server) {
|
||||
try {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === 'docker') {
|
||||
const result = await ssh.execCommand(`docker inspect --format='{{.State.Status}}' ${server.containerName} 2>/dev/null`);
|
||||
const status = result.stdout.trim();
|
||||
if (status === 'running') return 'online';
|
||||
if (status === 'restarting' || status === 'created') return 'starting';
|
||||
if (status === 'removing' || status === 'paused') return 'stopping';
|
||||
return 'offline';
|
||||
} else if (server.runtime === 'systemd') {
|
||||
const result = await ssh.execCommand(`systemctl is-active ${server.serviceName}`);
|
||||
const status = result.stdout.trim();
|
||||
if (status === 'active') return 'online';
|
||||
if (status === 'activating' || status === 'reloading') return 'starting';
|
||||
if (status === 'deactivating') return 'stopping';
|
||||
return 'offline';
|
||||
} else {
|
||||
const result = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
|
||||
if (result.code === 0) {
|
||||
const uptimeResult = await ssh.execCommand(`ps -o etimes= -p $(screen -ls | grep "\\.${server.screenName}" | awk '{print $1}' | cut -d. -f1) 2>/dev/null | head -1`);
|
||||
const uptime = parseInt(uptimeResult.stdout.trim()) || 999;
|
||||
if (uptime < 60) return 'starting';
|
||||
return 'online';
|
||||
}
|
||||
return 'offline';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to get status for ${server.id}:`, err.message);
|
||||
return 'offline';
|
||||
}
|
||||
}
|
||||
|
||||
export async function startServer(server, options = {}) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === 'docker') {
|
||||
// Factorio with specific save
|
||||
if (server.type === 'factorio' && options.save) {
|
||||
const saveName = options.save.endsWith('.zip') ? options.save.replace('.zip', '') : options.save;
|
||||
// Stop and remove existing container
|
||||
await ssh.execCommand(`docker stop ${server.containerName} 2>/dev/null || true`);
|
||||
await ssh.execCommand(`docker rm ${server.containerName} 2>/dev/null || true`);
|
||||
// Start with specific save
|
||||
await ssh.execCommand(`
|
||||
docker run -d \
|
||||
--name ${server.containerName} \
|
||||
-p 34197:34197/udp \
|
||||
-p 27015:27015/tcp \
|
||||
-v /srv/docker/factorio/data:/factorio \
|
||||
-e SAVE_NAME=${saveName} -e LOAD_LATEST_SAVE=false \
|
||||
-e TZ=Europe/Berlin \
|
||||
-e PUID=845 \
|
||||
-e PGID=845 \
|
||||
--restart=unless-stopped \
|
||||
factoriotools/factorio
|
||||
`);
|
||||
} else {
|
||||
await ssh.execCommand(`docker start ${server.containerName}`);
|
||||
}
|
||||
} else if (server.runtime === 'systemd') {
|
||||
await ssh.execCommand(`systemctl start ${server.serviceName}`);
|
||||
} else {
|
||||
await ssh.execCommand(`screen -S ${server.screenName} -X quit 2>/dev/null`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await ssh.execCommand(`cd ${server.workDir} && screen -dmS ${server.screenName} ${server.startCmd}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopServer(server) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === 'docker') {
|
||||
await ssh.execCommand(`docker stop ${server.containerName}`);
|
||||
} else if (server.runtime === 'systemd') {
|
||||
await ssh.execCommand(`systemctl stop ${server.serviceName}`);
|
||||
} else {
|
||||
// Different stop commands per server type
|
||||
const stopCmd = server.type === 'zomboid' ? 'quit' : 'stop';
|
||||
await ssh.execCommand(`screen -S ${server.screenName} -p 0 -X stuff '${stopCmd}\n'`);
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
const check = await ssh.execCommand(`screen -ls | grep -E "\\.${server.screenName}[[:space:]]"`);
|
||||
if (check.code !== 0) {
|
||||
console.log(`Server ${server.id} stopped after ${(i + 1) * 2} seconds`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Force killing ${server.id} after timeout`);
|
||||
await ssh.execCommand(`screen -S ${server.screenName} -X quit`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartServer(server) {
|
||||
await stopServer(server);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await startServer(server);
|
||||
}
|
||||
|
||||
export async function getConsoleLog(server, lines = 50) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === 'docker') {
|
||||
const result = await ssh.execCommand(`/usr/local/bin/docker-logs-tz ${server.containerName} ${lines}`);
|
||||
return result.stdout || result.stderr;
|
||||
} else if (server.runtime === 'systemd') {
|
||||
const result = await ssh.execCommand(`tail -n ${lines} ${server.workDir}/logs/VRisingServer.log 2>/dev/null || journalctl -u ${server.serviceName} -n ${lines} --no-pager`);
|
||||
return result.stdout || result.stderr;
|
||||
} else if (server.logFile) {
|
||||
const result = await ssh.execCommand(`tail -n ${lines} ${server.logFile} 2>/dev/null || echo No log file found`);
|
||||
return result.stdout;
|
||||
} else {
|
||||
const result = await ssh.execCommand(`tail -n ${lines} ${server.workDir}/logs/latest.log 2>/dev/null || echo No log file found`);
|
||||
return result.stdout;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProcessUptime(server) {
|
||||
try {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
if (server.runtime === "docker") {
|
||||
const result = await ssh.execCommand(`docker inspect --format="{{.State.StartedAt}}" ${server.containerName} 2>/dev/null`);
|
||||
if (result.stdout.trim()) {
|
||||
const startTime = new Date(result.stdout.trim());
|
||||
if (!isNaN(startTime.getTime())) {
|
||||
return Math.floor((Date.now() - startTime.getTime()) / 1000);
|
||||
}
|
||||
}
|
||||
} else if (server.runtime === "systemd") {
|
||||
const result = await ssh.execCommand(`systemctl show ${server.serviceName} --property=ActiveEnterTimestamp | cut -d= -f2 | xargs -I{} date -d "{}" +%s 2>/dev/null`);
|
||||
const startEpoch = parseInt(result.stdout.trim());
|
||||
if (!isNaN(startEpoch)) {
|
||||
return Math.floor(Date.now() / 1000) - startEpoch;
|
||||
}
|
||||
} else {
|
||||
const result = await ssh.execCommand(`ps -o etimes= -p $(pgrep -f "SCREEN.*${server.screenName}") 2>/dev/null | head -1`);
|
||||
const uptime = parseInt(result.stdout.trim());
|
||||
if (!isNaN(uptime)) return uptime;
|
||||
}
|
||||
return 0;
|
||||
} catch (err) {
|
||||
console.error(`Failed to get uptime for ${server.id}:`, err.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ FACTORIO-SPECIFIC FUNCTIONS ============
|
||||
|
||||
export async function listFactorioSaves(server) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
const result = await ssh.execCommand('ls -la /srv/docker/factorio/data/saves/*.zip 2>/dev/null');
|
||||
|
||||
if (result.code !== 0 || !result.stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const saves = [];
|
||||
const lines = result.stdout.trim().split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+\s+\d+\s+[\d:]+)\s+(.+)$/);
|
||||
if (match) {
|
||||
const filename = match[7].split('/').pop();
|
||||
// Skip autosaves
|
||||
if (filename.startsWith('_autosave')) continue;
|
||||
|
||||
const size = match[5];
|
||||
const modified = match[6];
|
||||
|
||||
saves.push({
|
||||
name: filename.replace('.zip', ''),
|
||||
filename,
|
||||
size,
|
||||
modified
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return saves;
|
||||
}
|
||||
|
||||
export async function deleteFactorioSave(server, saveName) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
const filename = saveName.endsWith('.zip') ? saveName : `${saveName}.zip`;
|
||||
|
||||
// Security check - prevent path traversal
|
||||
if (filename.includes('/') || filename.includes('..')) {
|
||||
throw new Error('Invalid save name');
|
||||
}
|
||||
|
||||
const result = await ssh.execCommand(`rm /srv/docker/factorio/data/saves/${filename}`);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(result.stderr || 'Failed to delete save');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFactorioWorld(server, saveName, settings) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
// Security check
|
||||
if (saveName.includes("/") || saveName.includes("..") || saveName.includes(" ")) {
|
||||
throw new Error("Invalid save name - no spaces or special characters allowed");
|
||||
}
|
||||
|
||||
// Write map-gen-settings.json
|
||||
const settingsJson = JSON.stringify(settings, null, 2);
|
||||
await ssh.execCommand(`cat > /srv/docker/factorio/data/config/map-gen-settings.json << 'SETTINGSEOF'
|
||||
${settingsJson}
|
||||
SETTINGSEOF`);
|
||||
|
||||
// Create new world using --create flag (correct method)
|
||||
console.log(`Creating new Factorio world: ${saveName}`);
|
||||
const createResult = await ssh.execCommand(`
|
||||
docker run --rm \
|
||||
-v /srv/docker/factorio/data:/factorio \
|
||||
factoriotools/factorio \
|
||||
/opt/factorio/bin/x64/factorio \
|
||||
--create /factorio/saves/${saveName}.zip \
|
||||
--map-gen-settings /factorio/config/map-gen-settings.json
|
||||
`, { execOptions: { timeout: 120000 } });
|
||||
|
||||
console.log("World creation output:", createResult.stdout, createResult.stderr);
|
||||
|
||||
// Verify save was created
|
||||
const checkResult = await ssh.execCommand(`ls /srv/docker/factorio/data/saves/${saveName}.zip`);
|
||||
if (checkResult.code !== 0) {
|
||||
throw new Error("World creation failed - save file not found. Output: " + createResult.stderr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getFactorioCurrentSave(server) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
// Check if container has SAVE_NAME env set
|
||||
const envResult = await ssh.execCommand(`docker inspect --format="{{range .Config.Env}}{{println .}}{{end}}" ${server.containerName} 2>/dev/null | grep "^SAVE_NAME="`);
|
||||
if (envResult.stdout.trim()) {
|
||||
const saveName = envResult.stdout.trim().replace("SAVE_NAME=", "");
|
||||
return { save: saveName, source: "configured" };
|
||||
}
|
||||
|
||||
// Otherwise find newest save file
|
||||
const result = await ssh.execCommand("ls -t /srv/docker/factorio/data/saves/*.zip 2>/dev/null | head -1");
|
||||
if (result.stdout.trim()) {
|
||||
const filename = result.stdout.trim().split("/").pop().replace(".zip", "");
|
||||
return { save: filename, source: "newest" };
|
||||
}
|
||||
|
||||
return { save: null, source: null };
|
||||
}
|
||||
Reference in New Issue
Block a user