Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Multiple Engines #11

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
43 changes: 36 additions & 7 deletions SS14.Launcher/ConfigConstants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using SS14.Launcher.Utility;

namespace SS14.Launcher;
Expand Down Expand Up @@ -48,18 +49,46 @@ public static class ConfigConstants
public const string NewsFeedUrl = "https://spacestation14.com/post/index.xml";
public const string TranslateUrl = "https://docs.spacestation14.com/en/general-development/contributing-translations.html";

private static readonly UrlFallbackSet RobustBuildsBaseUrl = new([
"https://robust-builds.cdn.spacestation14.com/",
"https://robust-builds.fallback.cdn.spacestation14.com/",
]);
public static readonly Dictionary<string, UrlFallbackSet> EngineBuildsUrl = new()
{
{
"Robust",
new UrlFallbackSet([
"https://robust-builds.cdn.spacestation14.com/manifest.json",
"https://robust-builds.fallback.cdn.spacestation14.com/manifest.json",
])
},
{
"Multiverse",
new UrlFallbackSet([
"https://cdn.spacestationmultiverse.com/ssmv-engine-manifest",
])
},
};

public static readonly Dictionary<string, UrlFallbackSet> EngineModulesUrl = new()
{
{
"Robust",
new UrlFallbackSet([
"https://robust-builds.cdn.spacestation14.com/modules.json",
"https://robust-builds.fallback.cdn.spacestation14.com/modules.json",
])
},
{
"Multiverse",
new UrlFallbackSet([
// Same as Robust for now
"https://robust-builds.cdn.spacestation14.com/modules.json",
"https://robust-builds.fallback.cdn.spacestation14.com/modules.json",
])
},
};

private static readonly UrlFallbackSet LauncherDataBaseUrl = new([
"http://assets.simplestation.org/launcher/",
]);

public static readonly UrlFallbackSet RobustBuildsManifest = RobustBuildsBaseUrl + "manifest.json";
public static readonly UrlFallbackSet RobustModulesManifest = RobustBuildsBaseUrl + "modules.json";

// How long to keep cached copies of Robust manifests.
// TODO: Take this from Cache-Control header responses instead.
public static readonly TimeSpan RobustManifestCacheTime = TimeSpan.FromMinutes(15);
Expand Down
8 changes: 7 additions & 1 deletion SS14.Launcher/LauncherPaths.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

Expand Down Expand Up @@ -31,7 +32,12 @@ public static class LauncherPaths
public static readonly string PathClientMacLog = Path.Combine(DirLogs, ClientMacLogName);
public static readonly string PathClientStdoutLog = Path.Combine(DirLogs, ClientStdoutLogName);
public static readonly string PathClientStderrLog = Path.Combine(DirLogs, ClientStderrLogName);
public static readonly string PathPublicKey = Path.Combine(DirLauncherInstall, "signing_key");
public static readonly string PathPublicKey = Path.Combine(DirLauncherInstall, "signing_key_Robust"); // Used as a fallback
public static readonly Dictionary<string, string> PathPublicKeys = new()
{
{ "Robust", PathPublicKey },
{ "Multiverse", Path.Combine(DirLauncherInstall, "signing_key_Multiverse") },
};
public static readonly string PathContentDb = Path.Combine(DirLocalData, "content.db");
public static readonly string PathOverrideAssetsDb = Path.Combine(DirLocalData, "override_assets.db");

Expand Down
30 changes: 17 additions & 13 deletions SS14.Launcher/Models/Connector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ private async Task LaunchClientWrap(
Status = ConnectionStatus.ClientExited;
}

private async Task<Process?> ConnectLaunchClient(ContentLaunchInfo launchInfo,
private async Task<Process?> ConnectLaunchClient(
ContentLaunchInfo launchInfo,
ServerInfo? info,
ServerBuildInformation? serverBuildInformation,
Uri? connectAddress,
Expand Down Expand Up @@ -297,7 +298,7 @@ void BuildCVar(string name, string? value)
}

// Launch client.
return await LaunchClient(launchInfo, args, cVars);
return await LaunchClient(launchInfo, serverBuildInformation?.EngineType ?? "Robust", args, cVars);
}
catch (Exception e)
{
Expand Down Expand Up @@ -410,11 +411,15 @@ private async Task<ContentLaunchInfo> InstallContentBundleAsync(

private async Task<Process?> LaunchClient(
ContentLaunchInfo launchInfo,
string engine,
IEnumerable<string> extraArgs,
List<(string, string)> env)
{
var pubKey = LauncherPaths.PathPublicKey;
var engineVersion = launchInfo.ModuleInfo.Single(x => x.Module == "Robust").Version;
Log.Information("Launching client with engine {Engine}", engine);
Log.Debug($"Engine has modules {string.Join(", ", launchInfo.ModuleInfo)}");

var pubKey = LauncherPaths.PathPublicKeys!.GetValueOrDefault(engine, null) ?? LauncherPaths.PathPublicKey;
var engineVersion = launchInfo.ModuleInfo.Single(x => x.Module == engine).Version;
var binPath = _engineManager.GetEnginePath(engineVersion);
var sig = _engineManager.GetEngineSignature(engineVersion);

Expand All @@ -433,17 +438,15 @@ private async Task<ContentLaunchInfo> InstallContentBundleAsync(
EnvVar("SS14_LOADER_CONTENT_VERSION", launchInfo.Version.ToString());

// Env vars for engine modules.
foreach (var (moduleName, moduleVersion) in launchInfo.ModuleInfo)
{
foreach (var (moduleName, moduleVersion) in launchInfo.ModuleInfo)
{
if (moduleName == "Robust")
continue;
if (moduleName == engine)
continue;

var modulePath = _engineManager.GetEngineModule(moduleName, moduleVersion);
var modulePath = _engineManager.GetEngineModule(moduleName, moduleVersion);

var envVar = $"ROBUST_MODULE_{moduleName.ToUpperInvariant().Replace('.', '_')}";
EnvVar(envVar, modulePath);
}
var envVar = $"ROBUST_MODULE_{moduleName.ToUpperInvariant().Replace('.', '_')}";
EnvVar(envVar, modulePath);
}

if (_cfg.GetCVar(CVars.DisableSigning))
Expand Down Expand Up @@ -696,7 +699,8 @@ public ConnectException(ConnectionStatus status, Exception inner)
public sealed record ContentBundleMetadata(
[property: JsonPropertyName("server_gc")] bool? ServerGC,
[property: JsonPropertyName("engine_version")] string EngineVersion,
[property: JsonPropertyName("base_build")] ContentBundleBaseBuild? BaseBuild
[property: JsonPropertyName("base_build")] ContentBundleBaseBuild? BaseBuild,
[property: JsonPropertyName("engine")] string Engine = "Robust"
);

public sealed record ContentBundleBaseBuild(
Expand Down
1 change: 0 additions & 1 deletion SS14.Launcher/Models/ContentLaunchInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@
/// Information loaded by the updater that we need to launch the game.
/// </summary>
public sealed record ContentLaunchInfo(long Version, (string Module, string Version)[] ModuleInfo, bool ServerGC = false);

66 changes: 38 additions & 28 deletions SS14.Launcher/Models/EngineManager/EngineManagerDynamic.Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using SS14.Launcher.Utility;

namespace SS14.Launcher.Models.EngineManager;

Expand All @@ -16,7 +17,7 @@ public sealed partial class EngineManagerDynamic
private readonly SemaphoreSlim _manifestSemaphore = new(1);
private readonly Stopwatch _manifestStopwatch = Stopwatch.StartNew();

private Dictionary<string, VersionInfo>? _cachedRobustVersionInfo;
private readonly Dictionary<string, Dictionary<string, VersionInfo>?> _cachedEngineVersionInfo = new();
private TimeSpan _robustCacheValidUntil;

/// <summary>
Expand All @@ -31,13 +32,14 @@ public sealed partial class EngineManagerDynamic
/// </returns>
private async ValueTask<FoundVersionInfo?> GetVersionInfo(
string version,
string engine,
bool followRedirects = true,
CancellationToken cancel = default)
{
await _manifestSemaphore.WaitAsync(cancel);
try
{
return await GetVersionInfoCore(version, followRedirects, cancel);
return await GetVersionInfoCore(version, followRedirects, cancel, engine);
}
finally
{
Expand All @@ -48,51 +50,59 @@ public sealed partial class EngineManagerDynamic
private async ValueTask<FoundVersionInfo?> GetVersionInfoCore(
string version,
bool followRedirects,
CancellationToken cancel)
CancellationToken cancel,
string engine)
{
// If we have a cached copy, and it's not expired, we check it.
if (_cachedRobustVersionInfo != null && _robustCacheValidUntil > _manifestStopwatch.Elapsed)
{
// Check the version. If this fails, we immediately re-request the manifest as it may have changed.
// (Connecting to a freshly-updated server with a new Robust version, within the cache window.)
if (FindVersionInfoInCached(version, followRedirects) is { } foundVersionInfo)
return foundVersionInfo;
}

await UpdateBuildManifest(cancel);

return FindVersionInfoInCached(version, followRedirects);
// First, check if we have a cached copy of the manifest.
if (_cachedEngineVersionInfo.TryGetValue(engine, out var versionInfo)
&& versionInfo != null
&& _robustCacheValidUntil > _manifestStopwatch.Elapsed)
return FindVersionInfoInCached(version, followRedirects, engine);

// If we don't have a cached copy, or it's expired, we re-request the manifest.
await UpdateBuildManifest(cancel, engine);
return FindVersionInfoInCached(version, followRedirects, engine);
}

private async Task UpdateBuildManifest(CancellationToken cancel)
private async Task UpdateBuildManifest(CancellationToken cancel, string name)
{
// TODO: If-Modified-Since and If-None-Match request conditions.

Log.Debug("Loading manifest from {manifestUrl}...", ConfigConstants.RobustBuildsManifest);
_cachedRobustVersionInfo =
await ConfigConstants.RobustBuildsManifest.GetFromJsonAsync<Dictionary<string, VersionInfo>>(
_http, cancel);
if (ConfigConstants.EngineBuildsUrl.TryGetValue(name, out var urlSet))
foreach (var url in urlSet.Urls)
{
try
{
_cachedEngineVersionInfo.Remove(name);
_cachedEngineVersionInfo.Add(name, await new UrlFallbackSet([url]).GetFromJsonAsync<Dictionary<string, VersionInfo>>(_http, cancel));
break;
}
catch (Exception e)
{
Log.Error(e, "Failed to download manifest from {url}", url);
}
}

_robustCacheValidUntil = _manifestStopwatch.Elapsed + ConfigConstants.RobustManifestCacheTime;
}

private FoundVersionInfo? FindVersionInfoInCached(string version, bool followRedirects)
private FoundVersionInfo? FindVersionInfoInCached(string version, bool followRedirects, string name)
{
Debug.Assert(_cachedRobustVersionInfo != null);

if (!_cachedRobustVersionInfo.TryGetValue(version, out var versionInfo))
if (!_cachedEngineVersionInfo.TryGetValue(name, out var versionInfo))
Debug.Assert(false);
if (versionInfo == null || !versionInfo.TryGetValue(version, out var info))
return null;

if (followRedirects)
{
while (versionInfo.RedirectVersion != null)
while (info.RedirectVersion != null)
{
version = versionInfo.RedirectVersion;
versionInfo = _cachedRobustVersionInfo[versionInfo.RedirectVersion];
if (!versionInfo.TryGetValue(info.RedirectVersion, out info))
return null;
}
}

return new FoundVersionInfo(version, versionInfo);
return new FoundVersionInfo(version, info);
}

private sealed record FoundVersionInfo(string Version, VersionInfo Info);
Expand Down
43 changes: 20 additions & 23 deletions SS14.Launcher/Models/EngineManager/EngineManagerDynamic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,8 @@ public sealed partial class EngineManagerDynamic : IEngineManager
{
public const string OverrideVersionName = "_OVERRIDE_";

private readonly DataManager _cfg;
private readonly HttpClient _http;

public EngineManagerDynamic()
{
_cfg = Locator.Current.GetRequiredService<DataManager>();
_http = Locator.Current.GetRequiredService<HttpClient>();
}
private readonly DataManager _cfg = Locator.Current.GetRequiredService<DataManager>();
private readonly HttpClient _http = Locator.Current.GetRequiredService<HttpClient>();

public string GetEnginePath(string engineVersion)
{
Expand Down Expand Up @@ -72,7 +66,8 @@ public string GetEngineSignature(string engineVersion)
}

public async Task<EngineInstallationResult> DownloadEngineIfNecessary(
string engineVersion,
string version,
string engine,
Helpers.DownloadProgressCallback? progress = null,
CancellationToken cancel = default)
{
Expand All @@ -81,21 +76,18 @@ public async Task<EngineInstallationResult> DownloadEngineIfNecessary(
{
// Engine override means we don't need to download anything, we have it locally!
// At least, if we don't, we'll just blame the developer that enabled it.
return new EngineInstallationResult(engineVersion, false);
return new EngineInstallationResult(version, false);
}
#endif

var foundVersion = await GetVersionInfo(engineVersion, cancel: cancel);
var foundVersion = await GetVersionInfo(version, engine, true, cancel);
if (foundVersion == null)
throw new UpdateException("Unable to find engine version in manifest!");

if (foundVersion.Info.Insecure)
throw new UpdateException("Specified engine version is insecure!");

Log.Debug(
"Requested engine version was {RequestedEngien}, redirected to {FoundVersion}",
engineVersion,
foundVersion.Version);
Log.Debug($"Requested engine version was {version}, redirected to {foundVersion.Version}");

if (_cfg.EngineInstallations.Lookup(foundVersion.Version).HasValue)
{
Expand Down Expand Up @@ -203,7 +195,7 @@ public async Task<bool> DownloadModuleIfNecessary(
// Verify signature.
tempFile.Seek(0, SeekOrigin.Begin);

if (!VerifyModuleSignature(tempFile, platformData.Sig))
if (!VerifyModuleSignature(tempFile, moduleName, platformData.Sig))
{
#if DEBUG
if (_cfg.GetCVar(CVars.DisableSigning))
Expand Down Expand Up @@ -291,7 +283,7 @@ private static void ExtractModule(string moduleName, string moduleVersionDiskPat
}
}

private static unsafe bool VerifyModuleSignature(FileStream stream, string signature)
private static unsafe bool VerifyModuleSignature(FileStream stream, string module, string signature)
{
if (stream.Length > int.MaxValue)
throw new InvalidOperationException("Unable to handle files larger than 2 GiB");
Expand All @@ -315,7 +307,7 @@ private static unsafe bool VerifyModuleSignature(FileStream stream, string signa

var pubKey = PublicKey.Import(
SignatureAlgorithm.Ed25519,
File.ReadAllBytes(LauncherPaths.PathPublicKey),
File.ReadAllBytes(LauncherPaths.PathPublicKeys!.GetValueOrDefault(module, null) ?? LauncherPaths.PathPublicKey),
KeyBlobFormat.PkixPublicKeyText);

var sigBytes = Convert.FromHexString(signature);
Expand All @@ -328,10 +320,15 @@ private static unsafe bool VerifyModuleSignature(FileStream stream, string signa
}
}

public async Task<EngineModuleManifest> GetEngineModuleManifest(CancellationToken cancel = default)
public async Task<EngineModuleManifest> GetEngineModuleManifest(string engine, CancellationToken cancel = default)
{
return await ConfigConstants.RobustModulesManifest.GetFromJsonAsync<EngineModuleManifest>(_http, cancel) ??
throw new InvalidDataException();
if (!ConfigConstants.EngineBuildsUrl.TryGetValue(engine, out var urls))
throw new InvalidOperationException("No manifest URL for engine module");

if (await urls.GetFromJsonAsync<EngineModuleManifest>(_http, cancel) is { } manifest)
return manifest;

throw new InvalidOperationException("Failed to download engine module manifest");
}

public async Task DoEngineCullMaybeAsync(SqliteConnection contenCon)
Expand All @@ -350,7 +347,7 @@ public async Task DoEngineCullMaybeAsync(SqliteConnection contenCon)
var modulesUsed = new HashSet<(string, string)>();
foreach (var (name, version) in origModulesUsed)
{
if (name == "Robust" && await GetVersionInfo(version) is { } redirect)
if (name == "Robust" && await GetVersionInfo(version, name) is { } redirect)
{
modulesUsed.Add(("Robust", redirect.Version));
}
Expand All @@ -360,7 +357,7 @@ public async Task DoEngineCullMaybeAsync(SqliteConnection contenCon)
}
}

var toCull = _cfg.EngineInstallations.Items.Where(i => !modulesUsed.Contains(("Robust", i.Version))).ToArray();
var toCull = _cfg.EngineInstallations.Items.Where(i => !modulesUsed.Any(m => m.Item2 == i.Version)).ToArray();

foreach (var installation in toCull)
{
Expand Down
Loading
Loading