FFXIVLauncher-Netmaui

Форк
0
502 строки · 17.9 Кб
1
using System;
2
using System.Collections.Generic;
3
using System.IO;
4
using System.IO.Compression;
5
using System.Linq;
6
using System.Net;
7
using System.Net.Http;
8
using System.Net.Http.Headers;
9
using System.Security.Cryptography;
10
using System.Threading.Tasks;
11
using LibDalamud.Common.Util;
12
using Newtonsoft.Json;
13
using Serilog;
14
using XIVLauncher.Common.PlatformAbstractions;
15
using XIVLauncher.Common.Util;
16

17
namespace LibDalamud.Common.Dalamud
18
{
19
    public class DalamudUpdater
20
    {
21
        private readonly DirectoryInfo addonDirectory;
22
        private readonly DirectoryInfo runtimeDirectory;
23
        private readonly DirectoryInfo assetDirectory;
24
        private readonly DirectoryInfo configDirectory;
25
        private readonly IUniqueIdCache? cache;
26

27
        private readonly TimeSpan defaultTimeout = TimeSpan.FromMinutes(15);
28

29
        private bool forceProxy = false;
30

31
        public DownloadState State { get; private set; } = DownloadState.Unknown;
32
        public bool IsStaging { get; private set; } = false;
33

34
        private FileInfo runnerInternal;
35

36
        public FileInfo Runner
37
        {
38
            get
39
            {
40
                if (RunnerOverride != null)
41
                    return RunnerOverride;
42

43
                return runnerInternal;
44
            }
45
            private set => runnerInternal = value;
46
        }
47

48
        public DirectoryInfo Runtime => this.runtimeDirectory;
49

50
        public FileInfo RunnerOverride { get; set; }
51

52
        public DirectoryInfo AssetDirectory { get; private set; }
53

54
        public IDalamudLoadingOverlay Overlay { get; set; }
55

56
        public string RolloutBucket { get; set; }
57

58
        public enum DownloadState
59
        {
60
            Unknown,
61
            Done,
62
            Failed,
63
            NoIntegrity
64
        }
65

66
        public DalamudUpdater(DirectoryInfo addonDirectory, DirectoryInfo runtimeDirectory, DirectoryInfo assetDirectory, DirectoryInfo configDirectory, IUniqueIdCache? cache, string? dalamudRolloutBucket)
67
        {
68
            this.addonDirectory = addonDirectory;
69
            this.runtimeDirectory = runtimeDirectory;
70
            this.assetDirectory = assetDirectory;
71
            this.configDirectory = configDirectory;
72
            this.cache = cache;
73

74
            this.RolloutBucket = dalamudRolloutBucket;
75

76
            if (this.RolloutBucket == null)
77
            {
78
                var rng = new Random();
79
                this.RolloutBucket = rng.Next(0, 9) >= 7 ? "Canary" : "Control";
80
            }
81
        }
82

83
        public void SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep progress)
84
        {
85
            Overlay.SetStep(progress);
86
        }
87

88
        public void ShowOverlay()
89
        {
90
            Overlay.SetVisible();
91
        }
92

93
        public void CloseOverlay()
94
        {
95
            Overlay.SetInvisible();
96
        }
97

98
        private void ReportOverlayProgress(long? size, long downloaded, double? progress)
99
        {
100
            Overlay.ReportProgress(size, downloaded, progress);
101
        }
102

103
        public void Run()
104
        {
105
            Log.Information("[DUPDATE] Starting...");
106

107
            Task.Run(async () =>
108
            {
109
                const int MAX_TRIES = 10;
110

111
                for (var tries = 0; tries < MAX_TRIES; tries++)
112
                {
113
                    try
114
                    {
115
                        await UpdateDalamud().ConfigureAwait(true);
116
                        break;
117
                    }
118
                    catch (Exception ex)
119
                    {
120
                        Log.Error(ex, "[DUPDATE] Update failed, try {TryCnt}/{MaxTries}...", tries, MAX_TRIES);
121
                        this.forceProxy = true;
122
                    }
123
                }
124

125
                if (this.State != DownloadState.Done) this.State = DownloadState.Failed;
126
            });
127
        }
128

129
        private static string GetBetaTrackName(DalamudSettings settings) =>
130
            string.IsNullOrEmpty(settings.DalamudBetaKind) ? "staging" : settings.DalamudBetaKind;
131

132
        private async Task<(DalamudVersionInfo release, DalamudVersionInfo? staging)> GetVersionInfo(DalamudSettings settings)
133
        {
134
            using var client = new HttpClient
135
            {
136
                Timeout = this.defaultTimeout,
137
            };
138

139
            client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue
140
            {
141
                NoCache = true,
142
            };
143

144
            var versionInfoJsonRelease = await client.GetStringAsync(DalamudLauncher.REMOTE_BASE + $"release&bucket={this.RolloutBucket}").ConfigureAwait(false);
145

146
            DalamudVersionInfo versionInfoRelease = JsonConvert.DeserializeObject<DalamudVersionInfo>(versionInfoJsonRelease);
147

148
            DalamudVersionInfo? versionInfoStaging = null;
149

150
            if (!string.IsNullOrEmpty(settings.DalamudBetaKey))
151
            {
152
                var versionInfoJsonStaging = await client.GetAsync(DalamudLauncher.REMOTE_BASE + GetBetaTrackName(settings)).ConfigureAwait(false);
153

154
                if (versionInfoJsonStaging.StatusCode != HttpStatusCode.BadRequest)
155
                    versionInfoStaging = JsonConvert.DeserializeObject<DalamudVersionInfo>(await versionInfoJsonStaging.Content.ReadAsStringAsync().ConfigureAwait(false));
156
            }
157

158
            return (versionInfoRelease, versionInfoStaging);
159
        }
160

161
        private async Task UpdateDalamud()
162
        {
163
            var settings = DalamudSettings.GetSettings(this.configDirectory);
164

165
            // GitHub requires TLS 1.2, we need to hardcode this for Windows 7
166
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
167

168
            var (versionInfoRelease, versionInfoStaging) = await GetVersionInfo(settings).ConfigureAwait(false);
169

170
            var remoteVersionInfo = versionInfoRelease;
171

172
            if (versionInfoStaging?.Key != null && versionInfoStaging.Key == settings.DalamudBetaKey)
173
            {
174
                remoteVersionInfo = versionInfoStaging;
175
                IsStaging = true;
176
                Log.Information("[DUPDATE] Using staging version {Kind} with key {Key} ({Hash})", settings.DalamudBetaKind, settings.DalamudBetaKey, remoteVersionInfo.AssemblyVersion);
177
            }
178
            else
179
            {
180
                Log.Information("[DUPDATE] Using release version ({Hash})", remoteVersionInfo.AssemblyVersion);
181
            }
182

183
            var versionInfoJson = JsonConvert.SerializeObject(remoteVersionInfo);
184

185
            var addonPath = new DirectoryInfo(Path.Combine(this.addonDirectory.FullName, "Hooks"));
186
            var currentVersionPath = new DirectoryInfo(Path.Combine(addonPath.FullName, remoteVersionInfo.AssemblyVersion));
187
            var runtimePaths = new DirectoryInfo[]
188
            {
189
                new(Path.Combine(this.runtimeDirectory.FullName, "host", "fxr", remoteVersionInfo.RuntimeVersion)),
190
                new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.NETCore.App", remoteVersionInfo.RuntimeVersion)),
191
                new(Path.Combine(this.runtimeDirectory.FullName, "shared", "Microsoft.WindowsDesktop.App", remoteVersionInfo.RuntimeVersion)),
192
            };
193

194
            if (!currentVersionPath.Exists || !IsIntegrity(currentVersionPath))
195
            {
196
                Log.Information("[DUPDATE] Not found, redownloading");
197

198
                SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Dalamud);
199

200
                try
201
                {
202
                    await DownloadDalamud(currentVersionPath, remoteVersionInfo).ConfigureAwait(true);
203
                    CleanUpOld(addonPath, remoteVersionInfo.AssemblyVersion);
204

205
                    // This is a good indicator that we should clear the UID cache
206
                    cache?.Reset();
207
                }
208
                catch (Exception ex)
209
                {
210
                    Log.Error(ex, "[DUPDATE] Could not download dalamud");
211

212
                    State = DownloadState.NoIntegrity;
213
                    return;
214
                }
215
            }
216

217
            if (remoteVersionInfo.RuntimeRequired || settings.DoDalamudRuntime)
218
            {
219
                Log.Information("[DUPDATE] Now starting for .NET Runtime {0}", remoteVersionInfo.RuntimeVersion);
220

221
                var versionFile = new FileInfo(Path.Combine(this.runtimeDirectory.FullName, "version"));
222
                var 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.
223
                if (versionFile.Exists)
224
                    localVersion = File.ReadAllText(versionFile.FullName);
225

226
                if (!this.runtimeDirectory.Exists)
227
                    Directory.CreateDirectory(this.runtimeDirectory.FullName);
228

229
                var integrity = await CheckRuntimeHashes(runtimeDirectory, localVersion).ConfigureAwait(false);
230

231
                if (runtimePaths.Any(p => !p.Exists) || localVersion != remoteVersionInfo.RuntimeVersion || !integrity)
232
                {
233
                    Log.Information("[DUPDATE] Not found, outdated or no integrity: {LocalVer} - {RemoteVer}", localVersion, remoteVersionInfo.RuntimeVersion);
234

235
                    SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Runtime);
236

237
                    try
238
                    {
239
                        await DownloadRuntime(this.runtimeDirectory, remoteVersionInfo.RuntimeVersion).ConfigureAwait(false);
240
                        File.WriteAllText(versionFile.FullName, remoteVersionInfo.RuntimeVersion);
241
                    }
242
                    catch (Exception ex)
243
                    {
244
                        Log.Error(ex, "[DUPDATE] Could not download runtime");
245

246
                        State = DownloadState.Failed;
247
                        return;
248
                    }
249
                }
250
            }
251

252
            try
253
            {
254
                this.SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Assets);
255
                this.ReportOverlayProgress(null, 0, null);
256
                AssetDirectory = await AssetManager.EnsureAssets(this.assetDirectory, this.forceProxy).ConfigureAwait(true);
257
            }
258
            catch (Exception ex)
259
            {
260
                Log.Error(ex, "[DUPDATE] Asset ensurement error, bailing out...");
261
                State = DownloadState.Failed;
262
                return;
263
            }
264

265
            if (!IsIntegrity(currentVersionPath))
266
            {
267
                Log.Error("[DUPDATE] Integrity check failed after ensurement.");
268

269
                State = DownloadState.NoIntegrity;
270
                return;
271
            }
272

273
            WriteVersionJson(currentVersionPath, versionInfoJson);
274

275
            Log.Information("[DUPDATE] All set for " + remoteVersionInfo.SupportedGameVer);
276

277
            Runner = new FileInfo(Path.Combine(currentVersionPath.FullName, "Dalamud.Injector.exe"));
278

279
            State = DownloadState.Done;
280
            SetOverlayProgress(IDalamudLoadingOverlay.DalamudUpdateStep.Starting);
281
        }
282

283
        private static bool CanRead(FileInfo info)
284
        {
285
            try
286
            {
287
                using var stream = info.OpenRead();
288
                stream.ReadByte();
289
            }
290
            catch
291
            {
292
                return false;
293
            }
294

295
            return true;
296
        }
297

298
        public static bool IsIntegrity(DirectoryInfo addonPath)
299
        {
300
            var files = addonPath.GetFiles();
301

302
            try
303
            {
304
                if (!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
                {
308
                    Log.Error("[DUPDATE] Can't open files for read");
309
                    return false;
310
                }
311

312
                var hashesPath = Path.Combine(addonPath.FullName, "hashes.json");
313

314
                if (!File.Exists(hashesPath))
315
                {
316
                    Log.Error("[DUPDATE] No hashes.json");
317
                    return false;
318
                }
319

320
                return CheckIntegrity(addonPath, File.ReadAllText(hashesPath));
321
            }
322
            catch (Exception ex)
323
            {
324
                Log.Error(ex, "[DUPDATE] No dalamud integrity");
325
                return false;
326
            }
327
        }
328

329
        private static bool CheckIntegrity(DirectoryInfo directory, string hashesJson)
330
        {
331
            try
332
            {
333
                Log.Verbose("[DUPDATE] Checking integrity of {Directory}", directory.FullName);
334

335
                var hashes = JsonConvert.DeserializeObject<Dictionary<string, string>>(hashesJson);
336

337
                foreach (var hash in hashes)
338
                {
339
                    var file = Path.Combine(directory.FullName, hash.Key.Replace("\\", "/"));
340
                    using var fileStream = File.OpenRead(file);
341
                    using var md5 = MD5.Create();
342

343
                    var hashed = BitConverter.ToString(md5.ComputeHash(fileStream)).ToUpperInvariant().Replace("-", string.Empty);
344

345
                    if (hashed != hash.Value)
346
                    {
347
                        Log.Error("[DUPDATE] Integrity check failed for {0} ({1} - {2})", file, hash.Value, hashed);
348
                        return false;
349
                    }
350

351
                    Log.Verbose("[DUPDATE] Integrity check OK for {0} ({1})", file, hashed);
352
                }
353
            }
354
            catch (Exception ex)
355
            {
356
                Log.Error(ex, "[DUPDATE] Integrity check failed");
357
                return false;
358
            }
359

360
            return true;
361
        }
362

363
        private static void CleanUpOld(DirectoryInfo addonPath, string currentVer)
364
        {
365
            if (!addonPath.Exists)
366
                return;
367

368
            foreach (var directory in addonPath.GetDirectories())
369
            {
370
                if (directory.Name == "dev" || directory.Name == currentVer) continue;
371

372
                try
373
                {
374
                    directory.Delete(true);
375
                }
376
                catch
377
                {
378
                    // ignored
379
                }
380
            }
381
        }
382

383
        private static void WriteVersionJson(DirectoryInfo addonPath, string info)
384
        {
385
            File.WriteAllText(Path.Combine(addonPath.FullName, "version.json"), info);
386
        }
387

388
        private async Task DownloadDalamud(DirectoryInfo addonPath, DalamudVersionInfo version)
389
        {
390
            // Ensure directory exists
391
            if (!addonPath.Exists)
392
                addonPath.Create();
393
            else
394
            {
395
                addonPath.Delete(true);
396
                addonPath.Create();
397
            }
398

399
            var downloadPath = PlatformHelpers.GetTempFileName();
400

401
            if (File.Exists(downloadPath))
402
                File.Delete(downloadPath);
403

404
            await this.DownloadFile(version.DownloadUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);
405
            ZipFile.ExtractToDirectory(downloadPath, addonPath.FullName);
406

407
            File.Delete(downloadPath);
408

409
            try
410
            {
411
                var devPath = new DirectoryInfo(Path.Combine(addonPath.FullName, "..", "dev"));
412

413
                if (!devPath.Exists)
414
                    devPath.Create();
415
                else
416
                {
417
                    devPath.Delete(true);
418
                    devPath.Create();
419
                }
420

421
                foreach (var fileInfo in addonPath.GetFiles())
422
                {
423
                    fileInfo.CopyTo(Path.Combine(devPath.FullName, fileInfo.Name));
424
                }
425
            }
426
            catch (Exception ex)
427
            {
428
                Log.Error(ex, "[DUPDATE] Could not copy to dev folder.");
429
            }
430
        }
431

432
        private async Task<bool> CheckRuntimeHashes(DirectoryInfo runtimePath, string version)
433
        {
434
#if DEBUG
435
            Log.Warning("Debug build, ignoring runtime hash check");
436
            return true;
437
#endif
438

439
            var hashesFile = new FileInfo(Path.Combine(runtimePath.FullName, $"hashes-{version}.json"));
440
            string? runtimeHashes = null;
441

442
            if (!hashesFile.Exists)
443
            {
444
                Log.Verbose("Hashes file does not exist, redownloading...");
445

446
                using var client = new HttpClient();
447
                runtimeHashes = await client.GetStringAsync($"https://kamori.goats.dev/Dalamud/Release/Runtime/Hashes/{version}").ConfigureAwait(false);
448

449
                File.WriteAllText(hashesFile.FullName, runtimeHashes);
450
            }
451
            else
452
            {
453
                runtimeHashes = File.ReadAllText(hashesFile.FullName);
454
            }
455

456
            return CheckIntegrity(runtimePath, runtimeHashes);
457
        }
458

459
        private async Task DownloadRuntime(DirectoryInfo runtimePath, string version)
460
        {
461
            // Ensure directory exists
462
            if (!runtimePath.Exists)
463
            {
464
                runtimePath.Create();
465
            }
466
            else
467
            {
468
                runtimePath.Delete(true);
469
                runtimePath.Create();
470
            }
471

472
            var dotnetUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/DotNet/{version}";
473
            var desktopUrl = $"https://kamori.goats.dev/Dalamud/Release/Runtime/WindowsDesktop/{version}";
474

475
            var downloadPath = PlatformHelpers.GetTempFileName();
476

477
            if (File.Exists(downloadPath))
478
                File.Delete(downloadPath);
479

480
            await this.DownloadFile(dotnetUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);
481
            ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName);
482

483
            await this.DownloadFile(desktopUrl, downloadPath, this.defaultTimeout).ConfigureAwait(false);
484
            ZipFile.ExtractToDirectory(downloadPath, runtimePath.FullName);
485

486
            File.Delete(downloadPath);
487
        }
488

489
        private async Task DownloadFile(string url, string path, TimeSpan timeout)
490
        {
491
            if (this.forceProxy && url.Contains("/File/Get/"))
492
            {
493
                url = url.Replace("/File/Get/", "/File/GetProxy/");
494
            }
495

496
            using var downloader = new HttpClientDownloadWithProgress(url, path);
497
            downloader.ProgressChanged += this.ReportOverlayProgress;
498

499
            await downloader.Download(timeout).ConfigureAwait(false);
500
        }
501
    }
502
}

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.