Files
MrMahoandClaude Opus 5.5 905d191807 Panel: pulsing dot on Update when a new server version is available
The dot replaces the small 'new' badge; the first update check now starts as
soon as the panel asks for the update status instead of 60 s after start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:56:00 +02:00

649 lines
28 KiB
C#

using System.Diagnostics;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace MSCO.Server;
/// <summary>
/// Server-Updates aus dem Git-Repository (git.mbm.wtf). Prüft regelmäßig, ob auf "main" eine neuere Version
/// (&lt;Version&gt; in MSCO.Server.csproj) liegt. Installieren im Panel: Quellcode laden, mit dem .NET SDK bauen
/// (dotnet publish) und nach DATA/app/current legen, dann neu starten. Beim Start lädt das Programm von dort die neuere
/// Version (siehe RunInstalled) - so klappt es auch in Docker, wo /app zum Image gehört. Ein neueres Image
/// (docker compose up --build) gewinnt wieder gegen ein älteres installiertes Update.
/// </summary>
public sealed class Updater
{
/// <summary>Exit-Code "neu starten für das Update" - Docker (restart: unless-stopped) und systemd starten neu.</summary>
public const int RestartExitCode = 75;
const int MaxStartAttempts = 3;
static readonly TimeSpan CheckInterval = TimeSpan.FromHours(6);
static readonly TimeSpan BuildTimeout = TimeSpan.FromMinutes(15);
const int MaxLogLines = 400;
/// <summary>Quelle der Updates (für Forks/Tests per MSCO_UPDATE_SOURCE änderbar).</summary>
public static string Source => (Environment.GetEnvironmentVariable("MSCO_UPDATE_SOURCE") ?? "https://git.mbm.wtf/MrMaho/MSCO-Dedicated-Server").TrimEnd('/');
public static string Branch => Environment.GetEnvironmentVariable("MSCO_UPDATE_BRANCH") ?? "main";
static string Api
{
get
{
var u = new Uri(Source);
return $"{u.Scheme}://{u.Authority}/api/v1/repos{u.AbsolutePath.TrimEnd('/')}";
}
}
static readonly HttpClient Http = CreateHttp();
static HttpClient CreateHttp()
{
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.UserAgent.ParseAdd("MSCO-Server/" + CurrentVersion);
return http;
}
// ---------------------------------------------------------------- eigene Version
static string Informational =>
typeof(Updater).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "0.0.0";
/// <summary>Version dieses Programms (aus MSCO.Server.csproj), z.B. "2.9.0".</summary>
public static string CurrentVersion => Informational.Split('+')[0];
/// <summary>Git-Commit dieses Builds (nur bei per Update gebauten Versionen bekannt).</summary>
public static string CurrentCommit
{
get
{
int plus = Informational.IndexOf('+');
return plus < 0 ? null : Informational[(plus + 1)..];
}
}
public static bool IsNewer(string candidate, string than) =>
Version.TryParse(candidate, out var a) && Version.TryParse(than, out var b) && a > b;
/// <summary>&lt;Version&gt; aus einer .csproj-Datei.</summary>
public static string ParseCsprojVersion(string csproj)
{
var m = Regex.Match(csproj ?? "", @"<Version>\s*([0-9][0-9.]*)\s*</Version>");
return m.Success ? m.Groups[1].Value : null;
}
/// <summary>Mod-Version (Protokoll) aus GameServer.cs.</summary>
public static string ParseModVersion(string source)
{
var m = Regex.Match(source ?? "", @"ModVersion\s*=\s*""([^""]+)""");
return m.Success ? m.Groups[1].Value : null;
}
// ---------------------------------------------------------------- Zustand fürs Panel
public enum Step { Idle, Downloading, Building, Installing, Restarting, Failed }
public sealed class Commit
{
public string Sha, Title, Date;
}
sealed class Latest
{
public string Version, Commit, Date, Message, ModVersion;
public List<Commit> Changes = new();
}
readonly ServerHost host;
readonly string dataDir;
readonly object gate = new();
readonly List<string> log = new();
Latest latest;
DateTime? lastCheckUtc;
string checkError;
Step step = Step.Idle;
int progress;
string message, error, target, startedBy;
DateTime stepSinceUtc = DateTime.UtcNow;
bool notified;
(bool ok, string reason)? sdk;
public Updater(ServerHost host, string dataDir)
{
this.host = host;
this.dataDir = dataDir;
}
string AppDir => Path.Combine(dataDir, "app");
string WorkDir => Path.Combine(dataDir, "update");
/// <summary>Hintergrund: kurz nach dem Start und dann alle 6 Stunden prüfen; neu installierte Version als gut markieren.</summary>
public void StartBackground()
{
_ = Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(60));
MarkHealthy(); // eine Minute ohne Absturz: das installierte Update läuft
while (true)
{
try { await CheckAsync(); } catch { }
await Task.Delay(CheckInterval);
}
});
}
bool firstCheckStarted;
public object Status()
{
// Noch nie geprüft (gerade erst gestartet): jetzt im Hintergrund prüfen, damit das Panel den Punkt bald zeigt
if (lastCheckUtc == null && !firstCheckStarted)
{
firstCheckStarted = true;
_ = Task.Run(async () => { try { await CheckAsync(); } catch { } });
}
lock (gate)
{
var sdkInfo = sdk;
return new
{
current = CurrentVersion,
currentCommit = CurrentCommit,
modVersion = GameServer.ModVersion,
source = Source,
branch = Branch,
latest = latest?.Version,
latestCommit = latest?.Commit,
latestDate = latest?.Date,
latestMessage = latest?.Message,
latestModVersion = latest?.ModVersion,
available = latest != null && IsNewer(latest.Version, CurrentVersion),
changes = latest?.Changes.Select(c => new { sha = c.Sha, title = c.Title, date = c.Date }),
lastCheck = lastCheckUtc?.ToString("o"),
checkError,
canInstall = sdkInfo?.ok,
cannotInstall = sdkInfo is { ok: false } ? sdkInfo.Value.reason : null,
supervised = Supervised,
step = step.ToString().ToLowerInvariant(),
progress,
message,
error,
target,
startedBy,
stepSince = stepSinceUtc.ToString("o"),
log = log.ToArray()
};
}
}
public bool Busy
{
get { lock (gate) return step is Step.Downloading or Step.Building or Step.Installing or Step.Restarting; }
}
void SetStep(Step s, int percent, string text)
{
lock (gate)
{
if (s != step) stepSinceUtc = DateTime.UtcNow;
step = s;
progress = percent;
message = text;
}
AddLog("== " + text);
}
void SetProgress(int percent, string text = null)
{
lock (gate)
{
progress = Math.Max(progress, Math.Clamp(percent, 0, 100));
if (text != null) message = text;
}
}
void AddLog(string line)
{
lock (gate)
{
log.Add(line);
if (log.Count > MaxLogLines) log.RemoveRange(0, log.Count - MaxLogLines);
}
}
// ---------------------------------------------------------------- Prüfen
/// <summary>Neueste Version auf dem Branch abfragen.</summary>
public async Task CheckAsync()
{
CheckSdk();
try
{
using var branchDoc = JsonDocument.Parse(await Http.GetStringAsync($"{Api}/branches/{Branch}"));
var commit = branchDoc.RootElement.GetProperty("commit");
string sha = commit.GetProperty("id").GetString();
string msg = commit.TryGetProperty("message", out var m) ? m.GetString() : "";
string date = commit.TryGetProperty("timestamp", out var t) ? t.GetString() : null;
string version = ParseCsprojVersion(await Http.GetStringAsync($"{Source}/raw/commit/{sha}/MSCO.Server.csproj"))
?? throw new Exception("No <Version> in MSCO.Server.csproj");
string modVersion = null;
try { modVersion = ParseModVersion(await Http.GetStringAsync($"{Source}/raw/commit/{sha}/GameServer.cs")); } catch { }
var found = new Latest
{
Version = version, Commit = sha, Date = date, ModVersion = modVersion,
Message = (msg ?? "").Split('\n')[0].Trim()
};
if (IsNewer(version, CurrentVersion))
{
try { found.Changes = await ChangesAsync(sha); } catch { }
}
bool newlyAvailable;
lock (gate)
{
newlyAvailable = IsNewer(version, CurrentVersion) && (latest == null || latest.Version != version || !notified);
latest = found;
lastCheckUtc = DateTime.UtcNow;
checkError = null;
if (newlyAvailable) notified = true;
}
if (newlyAvailable)
GameServer.Log($"Update available: MSCO server {version} (you have {CurrentVersion}) - install it in the web panel (Update)" +
(modVersion != null && modVersion != GameServer.ModVersion ? $". Players then need MSCO mod {modVersion}." : ""));
}
catch (Exception e)
{
lock (gate)
{
lastCheckUtc = DateTime.UtcNow;
checkError = "Could not check for updates: " + Short(e);
}
}
}
/// <summary>Commits seit der eigenen Version (bzw. die letzten, wenn der eigene Commit unbekannt ist).</summary>
async Task<List<Commit>> ChangesAsync(string sha)
{
using var doc = JsonDocument.Parse(await Http.GetStringAsync(
$"{Api}/commits?sha={sha}&limit=30&stat=false&verification=false&files=false"));
var list = new List<Commit>();
string own = CurrentCommit;
foreach (var c in doc.RootElement.EnumerateArray())
{
string id = c.GetProperty("sha").GetString();
if (own != null && id.StartsWith(own, StringComparison.OrdinalIgnoreCase)) break;
var inner = c.GetProperty("commit");
string text = inner.GetProperty("message").GetString() ?? "";
string date = inner.TryGetProperty("author", out var a) && a.TryGetProperty("date", out var d) ? d.GetString() : null;
list.Add(new Commit { Sha = id[..Math.Min(8, id.Length)], Title = text.Split('\n')[0].Trim(), Date = date });
if (list.Count >= (own != null ? 30 : 10)) break;
}
return list;
}
/// <summary>Ist ein .NET SDK zum Bauen da? (Docker-Image ab 2.9 enthält es.)</summary>
void CheckSdk()
{
if (sdk != null) return;
try
{
var psi = new ProcessStartInfo(DotnetPath(), "--list-sdks") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
using var p = Process.Start(psi);
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit(15000);
int need = Environment.Version.Major;
bool ok = output.Split('\n').Any(l => int.TryParse(l.Split('.')[0].Trim(), out int major) && major >= need);
lock (gate) sdk = ok ? (true, null) : (false, $"No .NET {need} SDK found - it is needed to build the update. " +
"With Docker: pull the new version once (git pull && docker compose up -d --build), its image contains the SDK.");
}
catch (Exception e)
{
lock (gate) sdk = (false, "The dotnet command was not found (" + Short(e) + ") - install the .NET SDK to update from the panel.");
}
}
static string DotnetPath()
{
string self = Environment.ProcessPath;
string name = Path.GetFileNameWithoutExtension(self ?? "");
return name.Equals("dotnet", StringComparison.OrdinalIgnoreCase) ? self : "dotnet";
}
// ---------------------------------------------------------------- Installieren
/// <summary>Update im Hintergrund laden, bauen, installieren und neu starten. null = gestartet, sonst Fehlertext.</summary>
public string Install(string by)
{
lock (gate)
{
if (step is Step.Downloading or Step.Building or Step.Installing or Step.Restarting) return "An update is already running";
if (latest == null || !IsNewer(latest.Version, CurrentVersion)) return "No update available - check again first";
if (sdk is { ok: false }) return sdk.Value.reason;
log.Clear();
error = null;
target = latest.Version;
startedBy = by;
step = Step.Downloading;
progress = 0;
stepSinceUtc = DateTime.UtcNow;
}
GameServer.Log($"Server update to {target} started by {by}");
_ = Task.Run(InstallAsync);
return null;
}
async Task InstallAsync()
{
try
{
// 1) Quellcode des neuesten Commits laden
SetStep(Step.Downloading, 2, "Checking the newest version");
await CheckAsync();
Latest want;
lock (gate) want = latest;
if (want == null || !IsNewer(want.Version, CurrentVersion)) throw new Exception(checkError ?? "No update available anymore");
lock (gate) target = want.Version;
Directory.CreateDirectory(WorkDir);
string zip = Path.Combine(WorkDir, "source.zip"), src = Path.Combine(WorkDir, "src");
SetStep(Step.Downloading, 5, $"Downloading MSCO server {want.Version} ({want.Commit[..8]})");
await DownloadAsync($"{Source}/archive/{want.Commit}.zip", zip);
if (Directory.Exists(src)) Directory.Delete(src, true);
ZipFile.ExtractToDirectory(zip, src);
string csproj = Directory.GetFiles(src, "MSCO.Server.csproj", SearchOption.AllDirectories)
.OrderBy(p => p.Length).FirstOrDefault()
?? throw new Exception("MSCO.Server.csproj not found in the download");
SetProgress(25, "Download complete");
// 2) Bauen
string build = Path.Combine(AppDir, "build");
if (Directory.Exists(build)) Directory.Delete(build, true);
SetStep(Step.Building, 27, "Building (dotnet publish) - this takes a minute");
await BuildAsync(csproj, build, want.Commit);
string dll = Path.Combine(build, "MSCO.Server.dll");
if (!File.Exists(dll)) throw new Exception("The build did not produce MSCO.Server.dll");
// 3) Installieren: build -> current (die bisherige bleibt als previous)
SetStep(Step.Installing, 92, "Installing");
WriteInfo(build, new InstallInfo { Version = want.Version, Commit = want.Commit, InstalledUtc = DateTime.UtcNow, By = startedBy });
string current = Path.Combine(AppDir, "current"), previous = Path.Combine(AppDir, "previous");
if (Directory.Exists(previous)) Directory.Delete(previous, true);
if (Directory.Exists(current)) Directory.Move(current, previous);
Directory.Move(build, current);
try { Directory.Delete(src, true); File.Delete(zip); } catch { }
AddLog("Installed to " + current);
// 4) Neu starten - die Spieler verbinden sich danach automatisch wieder
SetStep(Step.Restarting, 96, $"Restarting with version {want.Version}");
var server = host.Server;
if (server != null && host.State == HostState.Running)
{
// im Server-Thread: Spieler zählen und vorwarnen
var warn = server.Invoke(() => server.JoinedCount > 0
&& server.PanelSay("Server update - restarting in 5 seconds, you reconnect automatically", "Server") != null);
if (await Task.WhenAny(warn, Task.Delay(3000)) == warn && warn.Result) await Task.Delay(5000);
}
await Task.Delay(1500); // Panel: "Neustart" noch anzeigen lassen
GameServer.Log($"Update {want.Version} installed - restarting");
host.ExitForUpdate();
}
catch (Exception e)
{
lock (gate)
{
step = Step.Failed;
error = Short(e);
message = "Update failed";
stepSinceUtc = DateTime.UtcNow;
}
AddLog("FAILED: " + Short(e));
GameServer.Log("Server update failed: " + Short(e) + " - the server keeps running the old version");
}
}
async Task DownloadAsync(string url, string file)
{
using var res = await Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
res.EnsureSuccessStatusCode();
long? total = res.Content.Headers.ContentLength;
await using var input = await res.Content.ReadAsStreamAsync();
await using var output = File.Create(file);
var buffer = new byte[81920];
long done = 0;
int read;
while ((read = await input.ReadAsync(buffer)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read));
done += read;
string mb = (done / 1048576.0).ToString("0.0", System.Globalization.CultureInfo.InvariantCulture);
if (total > 0) SetProgress(5 + (int)(18 * done / total.Value), $"Downloading … {mb} MB");
else SetProgress(Math.Min(22, 5 + (int)(done / 100_000)), $"Downloading … {mb} MB");
}
AddLog($"Downloaded {done / 1024} KB");
}
async Task BuildAsync(string csproj, string output, string commit)
{
var psi = new ProcessStartInfo(DotnetPath())
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = Path.GetDirectoryName(csproj)
};
foreach (string a in new[] { "publish", csproj, "-c", "Release", "-o", output, "--nologo", "--disable-build-servers",
"-p:SourceRevisionId=" + commit, "-p:UseAppHost=" + (OperatingSystem.IsWindows() ? "true" : "false") })
psi.ArgumentList.Add(a);
// Alles Schreibbare in den Datenordner (in Docker ist HOME nur /tmp)
psi.Environment["DOTNET_CLI_HOME"] = Path.Combine(WorkDir, "home");
psi.Environment["NUGET_PACKAGES"] = Path.Combine(WorkDir, "nuget");
psi.Environment["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
psi.Environment["DOTNET_NOLOGO"] = "1";
psi.Environment["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
psi.Environment["MSBUILDDISABLENODEREUSE"] = "1";
using var p = new Process { StartInfo = psi };
void OnLine(string line)
{
if (string.IsNullOrWhiteSpace(line)) return;
AddLog(line.TrimEnd());
if (line.Contains("Restored ") || line.Contains("All projects are up-to-date")) SetProgress(45, "Building - packages restored");
else if (line.Contains(" -> ") && line.Contains(".dll")) SetProgress(80, "Building - compiled");
else if (line.Contains(" -> ")) SetProgress(90, "Building - published");
}
p.OutputDataReceived += (_, e) => { if (e.Data != null) OnLine(e.Data); };
p.ErrorDataReceived += (_, e) => { if (e.Data != null) OnLine(e.Data); };
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();
// Fortschritt langsam weiterlaufen lassen, solange das Bauen dauert
var started = DateTime.UtcNow;
while (!p.HasExited)
{
await Task.Delay(1000);
if (DateTime.UtcNow - started > BuildTimeout)
{
try { p.Kill(true); } catch { }
throw new Exception("The build took longer than " + BuildTimeout.TotalMinutes + " minutes");
}
lock (gate) if (progress < 88) progress++;
}
p.WaitForExit();
if (p.ExitCode != 0)
{
string lastError;
lock (gate) lastError = log.LastOrDefault(l => l.Contains("error", StringComparison.OrdinalIgnoreCase)) ?? "see the log";
throw new Exception($"The build failed (exit code {p.ExitCode}): {lastError}");
}
SetProgress(91, "Build complete");
}
static string Short(Exception e)
{
while (e.InnerException != null && e is AggregateException or TargetInvocationException) e = e.InnerException;
return e.Message;
}
// ---------------------------------------------------------------- Neu starten
/// <summary>Läuft das Programm unter Docker/systemd (die starten nach dem Beenden neu)?</summary>
public static bool Supervised =>
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true"
|| Environment.GetEnvironmentVariable("INVOCATION_ID") != null // systemd
|| Environment.GetEnvironmentVariable("MSCO_SUPERVISED") == "1";
/// <summary>Nach dem Beenden für ein Update: ohne Docker/systemd startet sich das Programm selbst neu.</summary>
public static void Relaunch(string[] args)
{
if (Supervised)
{
GameServer.Log("Exiting for the update - Docker/systemd starts the server again");
return;
}
try
{
string self = Environment.ProcessPath;
var all = new List<string>();
if (Path.GetFileNameWithoutExtension(self).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
all.Add(Environment.GetCommandLineArgs()[0]); // die gestartete MSCO.Server.dll
all.AddRange(args);
var psi = new ProcessStartInfo(self)
{
Arguments = string.Join(" ", all.Select(Quote)),
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = OperatingSystem.IsWindows() // Windows: neues Konsolenfenster
};
Process.Start(psi);
GameServer.Log("Started the updated server");
}
catch (Exception e)
{
GameServer.Log("Could not restart automatically (" + e.Message + ") - please start the server again");
}
}
static string Quote(string a) => a.Length > 0 && a.IndexOfAny(new[] { ' ', '"', '\t' }) < 0 ? a : "\"" + a.Replace("\"", "\\\"") + "\"";
// ---------------------------------------------------------------- installierte Version starten
sealed class InstallInfo
{
public string Version { get; set; }
public string Commit { get; set; }
public DateTime InstalledUtc { get; set; }
public string By { get; set; }
public int Attempts { get; set; }
public bool Verified { get; set; }
}
static InstallInfo ReadInfo(string dir)
{
try { return JsonSerializer.Deserialize<InstallInfo>(File.ReadAllText(Path.Combine(dir, "msco-update.json"))); }
catch { return null; }
}
static void WriteInfo(string dir, InstallInfo info) =>
File.WriteAllText(Path.Combine(dir, "msco-update.json"), JsonSerializer.Serialize(info, new JsonSerializerOptions { WriteIndented = true }));
/// <summary>Läuft gerade eine installierte Update-Version? Dann nach einer Minute als funktionierend merken.</summary>
static void MarkHealthy()
{
string dir = Environment.GetEnvironmentVariable("MSCO_UPDATE_DIR");
if (dir == null) return;
var info = ReadInfo(dir);
if (info == null || info.Verified) return;
info.Verified = true;
info.Attempts = 0;
try { WriteInfo(dir, info); } catch { }
}
/// <summary>
/// Ganz am Anfang von Program: liegt in DATA/app/current eine neuere Version, diese statt der eigenen ausführen
/// (im selben Prozess, eigener AssemblyLoadContext - Signale, Konsole und Docker bleiben gleich). Gibt den Exit-Code
/// zurück oder null, wenn die eigene Version laufen soll.
/// </summary>
public static int? RunInstalled(string[] args, string dataDir)
{
if (Environment.GetEnvironmentVariable("MSCO_UPDATE_DIR") != null) return null; // wir SIND die installierte Version
if (Environment.GetEnvironmentVariable("MSCO_NO_UPDATES") == "1") return null;
string dir = Path.Combine(dataDir, "app", "current");
var info = ReadInfo(dir);
if (info == null) return null;
string dll = Path.Combine(dir, "MSCO.Server.dll");
if (!File.Exists(dll)) return null;
if (!IsNewer(info.Version, CurrentVersion))
{
// Das Programm selbst ist gleich neu oder neuer (z.B. neues Docker-Image) - altes Update aufräumen
Console.WriteLine($"Installed update {info.Version} is not newer than this version ({CurrentVersion}) - removing it");
try { Directory.Delete(Path.Combine(dataDir, "app"), true); } catch { }
return null;
}
if (!info.Verified && info.Attempts >= MaxStartAttempts)
{
Console.WriteLine($"The installed update {info.Version} failed to start {info.Attempts} times - running the built-in version {CurrentVersion}");
try
{
string failed = Path.Combine(dataDir, "app", "failed");
if (Directory.Exists(failed)) Directory.Delete(failed, true);
Directory.Move(dir, failed);
}
catch { }
return null;
}
if (!info.Verified)
{
info.Attempts++;
try { WriteInfo(dir, info); } catch { }
}
MethodInfo entry;
try
{
var context = new UpdateLoadContext(dll);
entry = context.LoadFromAssemblyPath(dll).EntryPoint ?? throw new Exception("no entry point");
}
catch (Exception e)
{
Console.WriteLine($"Could not load the installed update {info.Version}: {e.Message} - running the built-in version {CurrentVersion}");
return null;
}
Console.WriteLine($"Starting installed update {info.Version} (built-in version {CurrentVersion})");
Environment.SetEnvironmentVariable("MSCO_UPDATE_DIR", dir);
try
{
object result = entry.Invoke(null, entry.GetParameters().Length == 0 ? null : new object[] { args });
if (result is Task<int> task) return task.GetAwaiter().GetResult();
if (result is Task plain) { plain.GetAwaiter().GetResult(); return 0; }
return result is int code ? code : 0;
}
catch (TargetInvocationException e) when (e.InnerException != null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw();
throw;
}
}
/// <summary>Lädt die installierte Version samt ihren Abhängigkeiten (z.B. SharpCompress) aus ihrem Ordner.</summary>
sealed class UpdateLoadContext : AssemblyLoadContext
{
readonly AssemblyDependencyResolver resolver;
public UpdateLoadContext(string mainAssembly) : base("msco-update") => resolver = new AssemblyDependencyResolver(mainAssembly);
protected override Assembly Load(AssemblyName name)
{
string path = resolver.ResolveAssemblyToPath(name);
return path != null ? LoadFromAssemblyPath(path) : null; // Framework (ASP.NET ...) aus dem Standard-Kontext
}
protected override IntPtr LoadUnmanagedDll(string name)
{
string path = resolver.ResolveUnmanagedDllToPath(name);
return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero;
}
}
}