From 109e67bd5e28fc394dfa0ae750c4739d9349b32e Mon Sep 17 00:00:00 2001 From: Owen de Bree Date: Mon, 22 Jun 2026 20:09:33 +0200 Subject: [PATCH] Fix DS version number checks --- Docs/LinuxDeploymentAndUpdates.md | 5 +- Docs/QuasarArchitecture.md | 14 +- Docs/Reference/data/manifest.json | 4 +- ...anagedDedicatedServerRuntimeResolver.cs.md | 4 +- Docs/WindowsDeploymentAndUpdates.md | 5 +- .../ManagedDedicatedServerRuntimeResolver.cs | 121 +++++++++++++++++- 6 files changed, 135 insertions(+), 18 deletions(-) diff --git a/Docs/LinuxDeploymentAndUpdates.md b/Docs/LinuxDeploymentAndUpdates.md index 0756aea..e84a28b 100644 --- a/Docs/LinuxDeploymentAndUpdates.md +++ b/Docs/LinuxDeploymentAndUpdates.md @@ -131,8 +131,9 @@ preparation and injects the bundled deployable DLL before relaunch. The Updates page always shows the currently installed Quasar, Bootstrap, Magnetar, and Space Engineers Dedicated Server versions when Quasar can resolve -them from release metadata or executable file versions. It also shows the -managed runtime install paths and the most recent managed-runtime check time. +them from release metadata, Dedicated Server `SE_VERSION` assembly metadata, or +non-placeholder executable file versions. It also shows the managed runtime +install paths and the most recent managed-runtime check time. Quasar UI worker and Bootstrap checks use the Quasar release checker interval (15 minutes by default) and the page's **Check Quasar** button. Managed Magnetar diff --git a/Docs/QuasarArchitecture.md b/Docs/QuasarArchitecture.md index 22642fb..74c2ba2 100644 --- a/Docs/QuasarArchitecture.md +++ b/Docs/QuasarArchitecture.md @@ -543,12 +543,14 @@ self-update path for that requested release immediately. The Updates page also shows installed managed-runtime versions independently of Quasar self-update state: Quasar UI/Bootstrap, Magnetar, and the Space Engineers -Dedicated Server can all be inspected there. Quasar release checks run on the -configured update interval (15 minutes by default) and can be triggered by the -Quasar check button. Managed Magnetar is checked on startup and every hour after -startup, with a separate manual Magnetar check button. The managed Dedicated -Server is checked during startup readiness and can be forced through its own -manual check button; the action runs SteamCMD `app_update 298740 validate`. +Dedicated Server can all be inspected there. The DS version is resolved from the +server's `SpaceEngineers.Game.dll` `SE_VERSION` metadata first, with +non-placeholder file versions only as fallbacks. Quasar release checks run on +the configured update interval (15 minutes by default) and can be triggered by +the Quasar check button. Managed Magnetar is checked on startup and every hour +after startup, with a separate manual Magnetar check button. The managed +Dedicated Server is checked during startup readiness and can be forced through +its own manual check button; the action runs SteamCMD `app_update 298740 validate`. ### Future proxy update flow diff --git a/Docs/Reference/data/manifest.json b/Docs/Reference/data/manifest.json index 2723f9d..50040f2 100644 --- a/Docs/Reference/data/manifest.json +++ b/Docs/Reference/data/manifest.json @@ -1734,8 +1734,8 @@ "path": "Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs", "name": "ManagedDedicatedServerRuntimeResolver.cs", "ext": ".cs", - "size": 77042, - "sha256": "92beddebc90450f953395900c227a50d7d0fdcceeddba141329b03d1f96e70c6", + "size": 81027, + "sha256": "72d495fc130099179afc801d59264176bcb1c9dd92d0ab432d555695e189c472", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" diff --git a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md index 165ce81..1b58fe2 100644 --- a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md +++ b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md @@ -41,7 +41,7 @@ Namespace: `Quasar.Services` **`ManagedRuntimeReadiness`** — sealed record returned by startup readiness checks: readiness bool, SteamCMD path, SteamCMD runtime path, DedicatedServer64 path, and failure message. -**`ManagedRuntimeVersionSnapshot`** — sealed record carrying installed SteamCMD, Magnetar, and Dedicated Server paths plus version strings derived from marker metadata or executable/assembly file versions. +**`ManagedRuntimeVersionSnapshot`** — sealed record carrying installed SteamCMD, Magnetar, and Dedicated Server paths plus version strings derived from marker metadata, Dedicated Server `SE_VERSION` assembly metadata, or non-placeholder executable/assembly file versions. **`ManagedRuntimeInstallProgress`** — sealed record emitted to readiness progress sinks with component (`SteamCmd` / `Magnetar` / `DedicatedServer`), phase (`Pending`, `Checking`, `Downloading`, `Installing`, `Ready`, `Failed`), message, optional percent, path, and version. @@ -58,4 +58,4 @@ Internal enum `ArchiveKind` (`Unknown`, `Zip`, `TarGz`, `SevenZip`). Private Mag ## Notes -Each install operation has its own `SemaphoreSlim(1,1)` so multiple servers starting at once cannot trigger duplicate installs. Magnetar checks always attempt to resolve the current configured archive source unless a successful GitHub release resolution is still inside its five-minute cooldown; if the installed marker already matches the latest GitHub release tag + asset name, the archive is not downloaded again. Dedicated Server checks use SteamCMD `app_update 298740 validate` and report the detected DS executable/assembly version when available. If the latest check fails while a launcher already exists, Quasar logs the failure and continues with the installed runtime instead of blocking a server start. On Linux the Magnetar launcher is resolved to the actual apphost binary under `Bin/` rather than the wrapper script, so Quasar starts it directly (Bin/ as working directory) and the tracked PID is the server's own — essential for cross-restart adoption. The two OS layouts differ: Windows extracts a single `Magnetar/` folder holding both launcher exes plus a `Libraries/` subfolder; Linux stages the Interim build behind a top-level wrapper with the apphost under `Bin/`. On Windows the per-server `ManagedRuntime` selects `MagnetarInterim.exe` (.NET 10) or `MagnetarLegacy.exe` (.NET Framework 4.8); on non-Windows hosts a `NetFramework48` selection is silently downgraded to `DotNet10`. On Linux/macOS, SteamCMD uses `+@sSteamCmdForcePlatformType windows` to fetch the Windows DS binaries, and exec bits are applied via `File.SetUnixFileMode`; Quasar-managed SteamCMD's `linux64/` runtime is prepared during startup readiness and preferred for `NativeLibrarySearchPaths`. `DedicatedServer64` validation requires the launcher plus core assemblies (`SpaceEngineers.Game.dll`, `VRage.dll`, `Sandbox.Game.dll`) so thin or corrupt DS folders are rejected earlier. Archive entries that resolve outside the extraction root are rejected. +Each install operation has its own `SemaphoreSlim(1,1)` so multiple servers starting at once cannot trigger duplicate installs. Magnetar checks always attempt to resolve the current configured archive source unless a successful GitHub release resolution is still inside its five-minute cooldown; if the installed marker already matches the latest GitHub release tag + asset name, the archive is not downloaded again. Dedicated Server checks use SteamCMD `app_update 298740 validate` and report the detected DS version when available. The DS version is read first from `SpaceEngineers.Game.SpaceEngineersGame.SE_VERSION` inside `SpaceEngineers.Game.dll`, formatted the same way as the game build number (for example `1.209.024`), and includes the server build suffix when `SERVER_BUILD_NUMBER` is present. File-version fallbacks ignore placeholder assembly versions such as `1.0.0`. If the latest check fails while a launcher already exists, Quasar logs the failure and continues with the installed runtime instead of blocking a server start. On Linux the Magnetar launcher is resolved to the actual apphost binary under `Bin/` rather than the wrapper script, so Quasar starts it directly (Bin/ as working directory) and the tracked PID is the server's own — essential for cross-restart adoption. The two OS layouts differ: Windows extracts a single `Magnetar/` folder holding both launcher exes plus a `Libraries/` subfolder; Linux stages the Interim build behind a top-level wrapper with the apphost under `Bin/`. On Windows the per-server `ManagedRuntime` selects `MagnetarInterim.exe` (.NET 10) or `MagnetarLegacy.exe` (.NET Framework 4.8); on non-Windows hosts a `NetFramework48` selection is silently downgraded to `DotNet10`. On Linux/macOS, SteamCMD uses `+@sSteamCmdForcePlatformType windows` to fetch the Windows DS binaries, and exec bits are applied via `File.SetUnixFileMode`; Quasar-managed SteamCMD's `linux64/` runtime is prepared during startup readiness and preferred for `NativeLibrarySearchPaths`. `DedicatedServer64` validation requires the launcher plus core assemblies (`SpaceEngineers.Game.dll`, `VRage.dll`, `Sandbox.Game.dll`) so thin or corrupt DS folders are rejected earlier. Archive entries that resolve outside the extraction root are rejected. diff --git a/Docs/WindowsDeploymentAndUpdates.md b/Docs/WindowsDeploymentAndUpdates.md index 8b9b346..a1a2da1 100644 --- a/Docs/WindowsDeploymentAndUpdates.md +++ b/Docs/WindowsDeploymentAndUpdates.md @@ -108,8 +108,9 @@ launch with the same base settings. The Updates page always shows the currently installed Quasar, Bootstrap, Magnetar, and Space Engineers Dedicated Server versions when Quasar can resolve -them from release metadata or executable file versions. It also shows the -managed runtime install paths and the most recent managed-runtime check time. +them from release metadata, Dedicated Server `SE_VERSION` assembly metadata, or +non-placeholder executable file versions. It also shows the managed runtime +install paths and the most recent managed-runtime check time. Quasar UI worker and Bootstrap checks use the Quasar release checker interval (15 minutes by default) and the page's **Check Quasar** button. Managed Magnetar diff --git a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs index 79c6e04..7eadd9a 100644 --- a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs +++ b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs @@ -1,6 +1,9 @@ using System.Diagnostics; +using System.Globalization; using System.IO.Compression; using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -756,10 +759,120 @@ private static string GetDedicatedServerVersion(string dedicatedServer64Path) return string.Empty; return FirstNonEmpty( - GetFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName + ".exe")), - GetFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName)), - GetFileVersion(Path.Combine(dedicatedServer64Path, "SpaceEngineers.Game.dll")), - GetFileVersion(Path.Combine(dedicatedServer64Path, "Sandbox.Game.dll"))); + GetSpaceEngineersGameVersion(dedicatedServer64Path), + GetNonPlaceholderFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName + ".exe")), + GetNonPlaceholderFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName)), + GetNonPlaceholderFileVersion(Path.Combine(dedicatedServer64Path, "SpaceEngineers.Game.dll")), + GetNonPlaceholderFileVersion(Path.Combine(dedicatedServer64Path, "Sandbox.Game.dll"))); + } + + private static string GetSpaceEngineersGameVersion(string dedicatedServer64Path) + { + var gameAssemblyPath = Path.Combine(dedicatedServer64Path, "SpaceEngineers.Game.dll"); + if (!TryReadInt32Constant( + gameAssemblyPath, + "SpaceEngineers.Game", + "SpaceEngineersGame", + "SE_VERSION", + out var gameVersion)) + { + return string.Empty; + } + + var version = FormatSpaceEngineersVersion(gameVersion); + if (string.IsNullOrWhiteSpace(version)) + return string.Empty; + + return TryReadInt32Constant( + gameAssemblyPath, + "SpaceEngineers.Game", + "SpaceEngineersGame", + "SERVER_BUILD_NUMBER", + out var serverBuildNumber) && + serverBuildNumber > 0 + ? $"{version} b{serverBuildNumber}" + : version; + } + + private static bool TryReadInt32Constant( + string assemblyPath, + string typeNamespace, + string typeName, + string fieldName, + out int value) + { + value = 0; + if (string.IsNullOrWhiteSpace(assemblyPath) || !File.Exists(assemblyPath)) + return false; + + try + { + using var stream = File.OpenRead(assemblyPath); + using var peReader = new PEReader(stream); + if (!peReader.HasMetadata) + return false; + + var metadata = peReader.GetMetadataReader(); + foreach (var typeHandle in metadata.TypeDefinitions) + { + var type = metadata.GetTypeDefinition(typeHandle); + if (!string.Equals(metadata.GetString(type.Namespace), typeNamespace, StringComparison.Ordinal) || + !string.Equals(metadata.GetString(type.Name), typeName, StringComparison.Ordinal)) + { + continue; + } + + foreach (var fieldHandle in type.GetFields()) + { + var field = metadata.GetFieldDefinition(fieldHandle); + if (!string.Equals(metadata.GetString(field.Name), fieldName, StringComparison.Ordinal) || + (field.Attributes & FieldAttributes.Literal) == 0) + { + continue; + } + + var constantHandle = field.GetDefaultValue(); + if (constantHandle.IsNil) + return false; + + var constant = metadata.GetConstant(constantHandle); + if (constant.TypeCode != ConstantTypeCode.Int32) + return false; + + value = metadata.GetBlobReader(constant.Value).ReadInt32(); + return true; + } + + return false; + } + } + catch + { + } + + return false; + } + + private static string FormatSpaceEngineersVersion(int version) + { + if (version <= 0) + return string.Empty; + + var text = version.ToString(CultureInfo.InvariantCulture).PadLeft(7, '0'); + return $"{text[..1]}.{text.Substring(1, 3)}.{text.Substring(4, 3)}"; + } + + private static string GetNonPlaceholderFileVersion(string path) + { + var version = GetFileVersion(path); + return IsPlaceholderAssemblyVersion(version) ? string.Empty : version; + } + + private static bool IsPlaceholderAssemblyVersion(string version) + { + var normalized = version.Trim(); + return string.Equals(normalized, "1.0.0", StringComparison.Ordinal) || + string.Equals(normalized, "1.0.0.0", StringComparison.Ordinal); } private static string GetFileVersion(string path)