diff --git a/gsm-backend/routes/servers.js b/gsm-backend/routes/servers.js index c7bec2d..09437f7 100644 --- a/gsm-backend/routes/servers.js +++ b/gsm-backend/routes/servers.js @@ -6,7 +6,7 @@ import { EmbedBuilder } from 'discord.js'; import { authenticateToken, optionalAuth, requireRole, rejectGuest } from '../middleware/auth.js'; import { getServerStatus, startServer, stopServer, restartServer, getConsoleLog, getProcessUptime, listFactorioSaves, createFactorioWorld, deleteFactorioSave, getFactorioCurrentSave, isHostFailed, listZomboidConfigs, readZomboidConfig, writeZomboidConfig, listPalworldConfigs, readPalworldConfig, writePalworldConfig, readTerrariaConfig, writeTerrariaConfig, readOpenTTDConfig, writeOpenTTDConfig, readHytaleConfig, writeHytaleConfig } from '../services/ssh.js'; import { supportsUpdate, checkForUpdate, performUpdate } from '../services/serverUpdates.js'; -import { supportsVersionInfo, getVersionInfo } from '../services/serverVersions.js'; +import { supportsVersionInfo, getVersionInfo, invalidateVersionCache } from '../services/serverVersions.js'; import { sendRconCommand, getPlayers, getPlayerList } from '../services/rcon.js'; import { getServerMetricsHistory, getCurrentMetrics } from '../services/prometheus.js'; import { initWhitelistCache, getCachedWhitelist, setCachedWhitelist, initFactorioTemplates, getFactorioTemplates, createFactorioTemplate, deleteFactorioTemplate, initFactorioWorldSettings, getFactorioWorldSettings, saveFactorioWorldSettings, deleteFactorioWorldSettings, initAutoShutdownSettings, getAutoShutdownSettings, setAutoShutdownSettings, initActivityLog, logActivity, getActivityLog, initServerDisplaySettings, getServerDisplaySettings, getAllServerDisplaySettings, setServerDisplaySettings, initGuildSettings } from '../db/init.js'; @@ -836,6 +836,7 @@ router.post('/:id/update', authenticateToken, requireRole('moderator'), async (r try { const sendDiscord = req.body?.sendDiscord !== false; // Default true const result = await performUpdate(server, sendDiscord); + invalidateVersionCache(server.id); logActivity(req.user.id, req.user.username, 'server_update', server.id, result.newVersion || 'latest', req.user.discordId, req.user.avatar); res.json(result); } catch (err) { diff --git a/gsm-backend/services/serverUpdates.js b/gsm-backend/services/serverUpdates.js index fa78fd8..e2c3156 100644 --- a/gsm-backend/services/serverUpdates.js +++ b/gsm-backend/services/serverUpdates.js @@ -20,7 +20,8 @@ const serverLogos = { palworld: 'https://gsm.zeasy.dev/palworld.png', terraria: 'https://gsm.zeasy.dev/terraria.png', openttd: 'https://gsm.zeasy.dev/openttd.png', - hytale: 'https://gsm.zeasy.dev/hytale.png' + hytale: 'https://gsm.zeasy.dev/hytale.png', + spaceengineers: 'https://gsm.zeasy.dev/spaceengineers.png' }; // Steam App IDs @@ -411,6 +412,116 @@ const updateHandlers = { gameName: "Terraria (tModLoader)" }), + // Space Engineers (Docker image with broken entrypoint - run SteamCMD ourselves + // in a one-shot sibling container with correct arg order: force_install_dir BEFORE login) + spaceengineers: { + supportsUpdate: true, + gameName: "Space Engineers", + + async checkForUpdate(server) { + const ssh = await getConnection(server.host, server.sshUser); + const appId = 298740; + const manifestPath = `${server.workDir}/appdata/space-engineers/bins/SpaceEngineersDedicated/steamapps/appmanifest_${appId}.acf`; + + const manifestResult = await ssh.execCommand(`cat ${manifestPath} 2>/dev/null`); + const manifest = manifestResult.stdout || ""; + const buildidMatch = manifest.match(/"buildid"\s*"([^"]+)"/); + const targetMatch = manifest.match(/"TargetBuildID"\s*"([^"]+)"/); + + if (!buildidMatch) { + return { hasUpdate: false, error: "appmanifest nicht gefunden" }; + } + + const currentBuildId = buildidMatch[1]; + const targetBuildId = targetMatch ? targetMatch[1] : null; + + // If Steam already flagged a newer build via prior login attempt, trust that + if (targetBuildId && targetBuildId !== "0" && targetBuildId !== currentBuildId) { + return { + hasUpdate: true, + currentVersion: currentBuildId, + newVersion: targetBuildId, + message: "Neues Update verfügbar!" + }; + } + + // Otherwise query Steam fresh via a one-shot container + console.log("[Update] Querying Steam for SE build info..."); + const queryCmd = `docker run --rm --entrypoint /bin/bash mmmaxwwwell/space-engineers-dedicated-docker-linux:latest -c "runuser -l wine bash -c 'steamcmd +@sSteamCmdForcePlatformType windows +login anonymous +app_info_update 1 +app_info_print ${appId} +quit' 2>/dev/null"`; + const queryResult = await ssh.execCommand(queryCmd, { execOptions: { timeout: 120000 } }); + const publicSection = queryResult.stdout.match(/"public"[\s\S]*?"buildid"\s*"([^"]+)"/); + const latestBuildId = publicSection ? publicSection[1] : null; + + if (!latestBuildId) { + return { hasUpdate: false, error: "Konnte Steam-Version nicht abrufen" }; + } + + const hasUpdate = currentBuildId !== latestBuildId; + return { + hasUpdate, + currentVersion: currentBuildId, + newVersion: latestBuildId, + message: hasUpdate ? "Neues Update verfügbar!" : "Server ist auf dem neuesten Stand" + }; + }, + + async performUpdate(server) { + const ssh = await getConnection(server.host, server.sshUser); + const appId = 298740; + const manifestPath = `${server.workDir}/appdata/space-engineers/bins/SpaceEngineersDedicated/steamapps/appmanifest_${appId}.acf`; + const installHostDir = `${server.workDir}/appdata/space-engineers/bins/SpaceEngineersDedicated`; + const steamHostDir = `${server.workDir}/appdata/space-engineers/bins/steamcmd`; + + // Verify container stopped + const statusResult = await ssh.execCommand( + `docker inspect --format='{{.State.Status}}' ${server.containerName} 2>/dev/null` + ); + if (statusResult.stdout.trim() === "running") { + throw new Error("Server muss gestoppt sein um zu updaten"); + } + + // Old build id + const oldManifest = await ssh.execCommand(`cat ${manifestPath} 2>/dev/null`); + const oldMatch = (oldManifest.stdout || "").match(/"buildid"\s*"([^"]+)"/); + const oldBuildId = oldMatch ? oldMatch[1] : null; + + console.log("[Update] Starting Space Engineers SteamCMD update (one-shot container)..."); + + // One-shot container: same image + same volumes, but our own steamcmd call + // with force_install_dir BEFORE login (the upstream entrypoint has them swapped) + const updateCmd = [ + `docker run --rm`, + `--entrypoint /bin/bash`, + `-v ${installHostDir}:/appdata/space-engineers/SpaceEngineersDedicated`, + `-v ${steamHostDir}:/home/wine/.steam`, + `mmmaxwwwell/space-engineers-dedicated-docker-linux:latest`, + `-c "runuser -l wine bash -c 'steamcmd +@sSteamCmdForcePlatformType windows +force_install_dir /appdata/space-engineers/SpaceEngineersDedicated +login anonymous +app_update ${appId} +quit'"` + ].join(" "); + + const updateResult = await ssh.execCommand(updateCmd, { execOptions: { timeout: 1200000 } }); // 20 min + console.log("[Update] SteamCMD output (tail):", updateResult.stdout.slice(-2000)); + + const out = updateResult.stdout || ""; + const success = out.includes("Success! App '298740'") || out.includes("fully installed"); + if (!success) { + const errLine = out.match(/Error![^\n]*/)?.[0] || updateResult.stderr || "Unbekannter Fehler"; + throw new Error("Update fehlgeschlagen: " + errLine); + } + + // New build id + const newManifest = await ssh.execCommand(`cat ${manifestPath} 2>/dev/null`); + const newMatch = (newManifest.stdout || "").match(/"buildid"\s*"([^"]+)"/); + const newBuildId = newMatch ? newMatch[1] : null; + + return { + success: true, + message: "Update erfolgreich! Server kann jetzt gestartet werden.", + oldVersion: oldBuildId || "unbekannt", + newVersion: newBuildId || "aktuell" + }; + } + }, + // Minecraft Biohazard - Manual update required (modpack) minecraft: { supportsUpdate: false, // Modpacks need manual update diff --git a/gsm-backend/services/serverVersions.js b/gsm-backend/services/serverVersions.js index 205c76f..af9cde1 100644 --- a/gsm-backend/services/serverVersions.js +++ b/gsm-backend/services/serverVersions.js @@ -58,6 +58,10 @@ export function supportsVersionInfo(serverType) { return !!versionHandlers[serverType]; } +export function invalidateVersionCache(serverId) { + versionCache.delete(serverId); +} + export async function getVersionInfo(server) { const handler = versionHandlers[server.type]; if (!handler) return null; diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx index bd73aa8..a04f5a9 100644 --- a/gsm-frontend/src/pages/ServerDetail.jsx +++ b/gsm-frontend/src/pages/ServerDetail.jsx @@ -549,7 +549,7 @@ const formatUptime = (seconds) => { )} {/* Server Update - for supported server types */} - {isModerator && ['factorio', 'vrising', 'palworld', 'zomboid', 'terraria'].includes(server.type) && ( + {isModerator && ['factorio', 'vrising', 'palworld', 'zomboid', 'terraria', 'spaceengineers'].includes(server.type) && (