Add Space Engineers update handler that works around broken upstream entrypoint
All checks were successful
Deploy GSM / deploy (push) Successful in 21s
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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user