FFXIVLauncher-Netmaui
502 строки · 17.9 Кб
1using System;2using System.Collections.Generic;3using System.IO;4using System.IO.Compression;5using System.Linq;6using System.Net;7using System.Net.Http;8using System.Net.Http.Headers;9using System.Security.Cryptography;10using System.Threading.Tasks;11using LibDalamud.Common.Util;12using Newtonsoft.Json;13using Serilog;14using XIVLauncher.Common.PlatformAbstractions;15using XIVLauncher.Common.Util;16
17namespace LibDalamud.Common.Dalamud18{
19public class DalamudUpdater20{21private readonly DirectoryInfo addonDirectory;22private readonly DirectoryInfo runtimeDirectory;23private readonly DirectoryInfo assetDirectory;24private readonly DirectoryInfo configDirectory;25private readonly IUniqueIdCache? cache;26
27private readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(15);28
29private bool forceProxy = false;30
31public DownloadState State { get; private set; } = DownloadState.Unknown;32public bool IsStaging { get; private set; } = false;33
34private FileInfo runnerInternal;35
36public FileInfo Runner37{38get39{40if (RunnerOverride != null)41return RunnerOverride;42
43return runnerInternal;44}45private set => runnerInternal = value;46}47
48public DirectoryInfo Runtime => this.runtimeDirectory;49
50public FileInfo RunnerOverride { get; set; }51
52public DirectoryInfo AssetDirectory { get; private set; }53
54public IDalamudLoadingOverlay Overlay { get; set; }55
56public string RolloutBucket { get; set; }57
58public enum DownloadState59{60Unknown,61Done,62Failed,63NoIntegrity
64}65
66public DalamudUpdater(DirectoryInfo addonDirectory, DirectoryInfo runtimeDirectory, DirectoryInfo assetDirectory, DirectoryInfo configDirectory, IUniqueIdCache? cache, string? dalamudRolloutBucket)67{68this.addonDirectory = addonDirectory;69this.runtimeDirectory = runtimeDirectory;70this.assetDirectory = assetDirectory;71this.configDirectory = configDirectory;72this.cache = cache;73
74this.RolloutBucket = dalamudRolloutBucket;75
76if (this.RolloutBucket == null)77{78var rng = new Random();79this.RolloutBucket = rng.Next(0, 9) >= 7 ? "Canary" : "Control";80}81}82
83public void SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep progress)84{85Overlay.SetStep(progress);86}87
88public void ShowOverlay()89{90Overlay.SetVisible();91}92
93public void CloseOverlay()94{95Overlay.SetInvisible();96}97
98private void ReportOverlayProgress(long? size, long downloaded, double? progress)99{100Overlay.ReportProgress(size, downloaded, progress);101}102
103public void Run()104{105Log.Information("[DUPDATE] Starting...");106
107Task.Run(async () =>108{109const int MAX_TRIES = 10;110
111for (var tries = 0; tries < MAX_TRIES; tries++)112{113try114{115await UpdateDalamud().ConfigureAwait(true);116break;117}118catch (Exception ex)119{120Log.Error(ex, "[DUPDATE] Update failed, try {TryCnt}/{MaxTries}...", tries, MAX_TRIES);121this.forceProxy = true;122}123}124
125if (this.State != DownloadState.Done) this.State = DownloadState.Failed;126});127}128
129private static string GetBetaTrackName(DalamudSettings settings) =>130string.IsNullOrEmpty(settings.DalamudBetaKind) ? "staging" : settings.DalamudBetaKind;131
132private async Task<(DalamudVersionInfo release, DalamudVersionInfo? staging)> GetVersionInfo(DalamudSettings settings)133{134using var client = new HttpClient135{136Timeout = this.defaultTimeout,137};138
139client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue140{141NoCache = true,142};143
144var versionInfoJsonRelease = await client.GetStringAsync(DalamudLauncher.REMOTE_BASE + $"release&bucket={this.RolloutBucket}").ConfigureAwait(false);145
146DalamudVersionInfo versionInfoRelease = JsonConvert.DeserializeObject<DalamudVersionInfo>(versionInfoJsonRelease);147
148DalamudVersionInfo? versionInfoStaging = null;149
150if (!string.IsNullOrEmpty(settings.DalamudBetaKey))151{152var versionInfoJsonStaging = await client.GetAsync(DalamudLauncher.REMOTE_BASE + GetBetaTrackName(settings)).ConfigureAwait(false);153
154if (versionInfoJsonStaging.StatusCode != HttpStatusCode.BadRequest)155versionInfoStaging = JsonConvert.DeserializeObject<DalamudVersionInfo>(await versionInfoJsonStaging.Content.ReadAsStringAsync().ConfigureAwait(false));156}157
158return (versionInfoRelease, versionInfoStaging);159}160
161private async Task UpdateDalamud()162{163var settings = DalamudSettings.GetSettings(this.configDirectory);164
165// GitHub requires TLS 1.2, we need to hardcode this for Windows 7166ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;167
168var (versionInfoRelease, versionInfoStaging) = await GetVersionInfo(settings).ConfigureAwait(false);169
170var remoteVersionInfo = versionInfoRelease;171
172if (versionInfoStaging?.Key != null && versionInfoStaging.Key == settings.DalamudBetaKey)173{174remoteVersionInfo = versionInfoStaging;175IsStaging = true;176Log.Information("[DUPDATE] Using staging version {Kind} with key {Key} ({Hash})", settings.DalamudBetaKind, settings.DalamudBetaKey, remoteVersionInfo.AssemblyVersion);177}178else179{180Log.Information("[DUPDATE] Using release version ({Hash})", remoteVersionInfo.AssemblyVersion);181}182
183var versionInfoJson = JsonConvert.SerializeObject(remoteVersionInfo);184
185var addonPath = new DirectoryInfo(Path.Combine(this.addonDirectory.FullName, "Hooks"));186var currentVersionPath = new DirectoryInfo(Path.Combine(addonPath.FullName, remoteVersionInfo.AssemblyVersion));187var runtimePaths = new DirectoryInfo[]188{189new(Path.Combine(this.runtimeDirectory.FullName, "host", "fxr", remoteVersionInfo.RuntimeVersion)),190new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.NETCore.App", remoteVersionInfo.RuntimeVersion)),191new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.WindowsDesktop.App", remoteVersionInfo.RuntimeVersion)),192};193
194if (!currentVersionPath.Exists || !IsIntegrity(currentVersionPath))195{196Log.Information("[DUPDATE] Not found, redownloading");197
198SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Dalamud);199
200try201{202await DownloadDalamud(currentVersionPath, remoteVersionInfo).ConfigureAwait(true);203CleanUpOld(addonPath, remoteVersionInfo.AssemblyVersion);204
205// This is a good indicator that we should clear the UID cache206cache?.Reset();207}208catch (Exception ex)209{210Log.Error(ex, "[DUPDATE] Could not download dalamud");211
212State = DownloadState.NoIntegrity;213return;214}215}216
217if (remoteVersionInfo.RuntimeRequired || settings.DoDalamudRuntime)218{219Log.Information("[DUPDATE] Now starting for .NET Runtime {0}", remoteVersionInfo.RuntimeVersion);220
221var versionFile = new FileInfo(Path.Combine(this.runtimeDirectory.FullName, "version"));222var localVersion = "5.0.6"; // This is the version we first shipped. We didn't write out a version file, so we can't check it.223if (versionFile.Exists)224localVersion = File.ReadAllText(versionFile.FullName);225
226if (!this.runtimeDirectory.Exists)227Directory.CreateDirectory(this.runtimeDirectory.FullName);228
229var integrity = await CheckRuntimeHashes(runtimeDirectory, localVersion).ConfigureAwait(false);230
231if (runtimePaths.Any(p => !p.Exists) || localVersion != remoteVersionInfo.RuntimeVersion || !integrity)232{233Log.Information("[DUPDATE] Not found, outdated or no integrity: {LocalVer} - {RemoteVer}", localVersion, remoteVersionInfo.RuntimeVersion);234
235SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Runtime);236
237try238{239await DownloadRuntime(this.runtimeDirectory, remoteVersionInfo.RuntimeVersion).ConfigureAwait(false);240File.WriteAllText(versionFile.FullName, remoteVersionInfo.RuntimeVersion);241}242catch (Exception ex)243{244Log.Error(ex, "[DUPDATE] Could not download runtime");245
246State = DownloadState.Failed;247return;248}249}250}251
252try253{254this.SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Assets);255this.ReportOverlayProgress(null, 0, null);256AssetDirectory = await AssetManager.EnsureAssets(this.assetDirectory, this.forceProxy).ConfigureAwait(true);257}258catch (Exception ex)259{260Log.Error(ex, "[DUPDATE] Asset ensurement error, bailing out...");261State = DownloadState.Failed;262return;263}264
265if (!IsIntegrity(currentVersionPath))266{267Log.Error("[DUPDATE] Integrity check failed after ensurement.");268
269State = DownloadState.NoIntegrity;270return;271}272
273WriteVersionJson(currentVersionPath, versionInfoJson);274
275Log.Information("[DUPDATE] All set for " + remoteVersionInfo.SupportedGameVer);276
277Runner = new FileInfo(Path.Combine(currentVersionPath.FullName, "Dalamud.Injector.exe"));278
279State = DownloadState.Done;280SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Starting);281}282
283private static bool CanRead(FileInfo info)284{285try286{287using var stream = info.OpenRead();288stream.ReadByte();289}290catch291{292return false;293}294
295return true;296}297
298public static bool IsIntegrity(DirectoryInfo addonPath)299{300var files = addonPath.GetFiles();301
302try303{304if (!CanRead(files.First(x => x.Name == "Dalamud.Injector.exe"))305|| !CanRead(files.First(x => x.Name == "Dalamud.dll"))306|| !CanRead(files.First(x => x.Name == "ImGuiScene.dll")))307{308Log.Error("[DUPDATE] Can't open files for read");309return false;310}311
312var hashesPath = Path.Combine(addonPath.FullName, "hashes.json");313
314if (!File.Exists(hashesPath))315{316Log.Error("[DUPDATE] No hashes.json");317return false;318}319
320return CheckIntegrity(addonPath, File.ReadAllText(hashesPath));321}322catch (Exception ex)323{324Log.Error(ex, "[DUPDATE] No dalamud integrity");325return false;326}327}328
329private static bool CheckIntegrity(DirectoryInfo directory, string hashesJson)330{331try332{333Log.Verbose("[DUPDATE] Checking integrity of {Directory}", directory.FullName);334
335var hashes = JsonConvert.DeserializeObject<Dictionary<string, string>>(hashesJson);336
337foreach (var hash in hashes)338{339var file = Path.Combine(directory.FullName, hash.Key.Replace("\\", "/"));340using var fileStream = File.OpenRead(file);341using var md5 = MD5.Create();342
343var hashed = BitConverter.ToString(md5.ComputeHash(fileStream)).ToUpperInvariant().Replace("-", string.Empty);344
345if (hashed != hash.Value)346{347Log.Error("[DUPDATE] Integrity check failed for {0} ({1} - {2})", file, hash.Value, hashed);348return false;349}350
351Log.Verbose("[DUPDATE] Integrity check OK for {0} ({1})", file, hashed);352}353}354catch (Exception ex)355{356Log.Error(ex, "[DUPDATE] Integrity check failed");357return false;358}359
360return true;361}362
363private static void CleanUpOld(DirectoryInfo addonPath, string currentVer)364{365if (!addonPath.Exists)366return;367
368foreach (var directory in addonPath.GetDirectories())369{370if (directory.Name == "dev" || directory.Name == currentVer) continue;371
372try373{374directory.Delete(true);375}376catch377{378// ignored379}380}381}382
383private static void WriteVersionJson(DirectoryInfo addonPath, string info)384{385File.WriteAllText(Path.Combine(addonPath.FullName, "version.json"), info);386}387
388private async Task DownloadDalamud(DirectoryInfo addonPath, DalamudVersionInfo version)389{390// Ensure directory exists391if (!addonPath.Exists)392addonPath.Create();393else394{395addonPath.Delete(true);396addonPath.Create();397}398
399var downloadPath = PlatformHelpers.GetTempFileName();400
401if (File.Exists(downloadPath))402File.Delete(downloadPath);403
404await this.DownloadFile(version.DownloadUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);405ZipFile.ExtractToDirectory(downloadPath, addonPath.FullName);406
407File.Delete(downloadPath);408
409try410{411var devPath = new DirectoryInfo(Path.Combine(addonPath.FullName, "..", "dev"));412
413if (!devPath.Exists)414devPath.Create();415else416{417devPath.Delete(true);418devPath.Create();419}420
421foreach (var fileInfo in addonPath.GetFiles())422{423fileInfo.CopyTo(Path.Combine(devPath.FullName, fileInfo.Name));424}425}426catch (Exception ex)427{428Log.Error(ex, "[DUPDATE] Could not copy to dev folder.");429}430}431
432private async Task<bool> CheckRuntimeHashes(DirectoryInfo runtimePath, string version)433{434#if DEBUG435Log.Warning("Debug build, ignoring runtime hash check");436return true;437#endif438
439var hashesFile = new FileInfo(Path.Combine(runtimePath.FullName, $"hashes-{version}.json"));440string? runtimeHashes = null;441
442if (!hashesFile.Exists)443{444Log.Verbose("Hashes file does not exist, redownloading...");445
446using var client = new HttpClient();447runtimeHashes = await client.GetStringAsync($"https://kamori.goats.dev/Dalamud/Release/Runtime/Hashes/{version}").ConfigureAwait(false);448
449File.WriteAllText(hashesFile.FullName, runtimeHashes);450}451else452{453runtimeHashes = File.ReadAllText(hashesFile.FullName);454}455
456return CheckIntegrity(runtimePath, runtimeHashes);457}458
459private async Task DownloadRuntime(DirectoryInfo runtimePath, string version)460{461// Ensure directory exists462if (!runtimePath.Exists)463{464runtimePath.Create();465}466else467{468runtimePath.Delete(true);469runtimePath.Create();470}471
472var dotnetUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/DotNet/{version}";473var desktopUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/WindowsDesktop/{version}";474
475var downloadPath = PlatformHelpers.GetTempFileName();476
477if (File.Exists(downloadPath))478File.Delete(downloadPath);479
480await this.DownloadFile(dotnetUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);481ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName);482
483await this.DownloadFile(desktopUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);484ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName);485
486File.Delete(downloadPath);487}488
489private async Task DownloadFile(string url, string path, TimeSpan timeout)490{491if (this.forceProxy && url.Contains("/File/Get/"))492{493url = url.Replace("/File/Get/", "/File/GetProxy/");494}495
496using var downloader = new HttpClientDownloadWithProgress(url, path);497downloader.ProgressChanged += this.ReportOverlayProgress;498
499await downloader.Download(timeout).ConfigureAwait(false);500}501}502}