Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Docs/LinuxDeploymentAndUpdates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions Docs/QuasarArchitecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Docs/Reference/data/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
5 changes: 3 additions & 2 deletions Docs/WindowsDeploymentAndUpdates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 117 additions & 4 deletions Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Loading