Add Space Engineers update handler that works around broken upstream entrypoint
All checks were successful
Deploy GSM / deploy (push) Successful in 21s

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 20:07:13 +02:00
parent bb48f75b5d
commit 902aa04726
4 changed files with 119 additions and 3 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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) && (
<div className="card p-4">
<h3 className="text-sm font-medium text-neutral-300 mb-3">Server Update</h3>
<p className="text-neutral-500 text-sm mb-4">