From 902aa047265b8e40a433b4c2c4b5b029969dbbd9 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Sun, 10 May 2026 20:07:13 +0200 Subject: [PATCH] Add Space Engineers update handler that works around broken upstream entrypoint The mmmaxwwwell SE image calls steamcmd with +login before +force_install_dir, which Steam rejects ("Please use force_install_dir before logon!"), so container restarts never actually update the game. The new handler runs SteamCMD in a sibling one-shot container with the correct argument order, mounting the same volumes as the live container, then leaves the server stopped for the operator to start. The version cache is invalidated after a successful update so the UI shows the new build immediately. Co-Authored-By: Claude Opus 4.7 (1M context) --- gsm-backend/routes/servers.js | 3 +- gsm-backend/services/serverUpdates.js | 113 +++++++++++++++++++++++- gsm-backend/services/serverVersions.js | 4 + gsm-frontend/src/pages/ServerDetail.jsx | 2 +- 4 files changed, 119 insertions(+), 3 deletions(-) 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) && (

Server Update