You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

754 lines
30 KiB

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Diagnostics.CodeAnalysis;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Runtime.Serialization;
  9. using System.Security.Cryptography;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using Ionic.Zip;
  13. using IPA.Config;
  14. using IPA.Loader;
  15. using IPA.Loader.Features;
  16. using IPA.Utilities;
  17. using IPA.Utilities.Async;
  18. using Newtonsoft.Json;
  19. using SemVer;
  20. using UnityEngine;
  21. using UnityEngine.Networking;
  22. using static IPA.Loader.PluginManager;
  23. using Logger = IPA.Logging.Logger;
  24. using Version = SemVer.Version;
  25. namespace IPA.Updating.BeatMods
  26. {
  27. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
  28. internal partial class Updater : MonoBehaviour
  29. {
  30. internal const string SpecialDeletionsFile = "$$delete";
  31. }
  32. #if BeatSaber
  33. [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
  34. internal partial class Updater : MonoBehaviour
  35. {
  36. public static Updater Instance;
  37. internal static bool ModListPresent = false;
  38. public void Awake()
  39. {
  40. try
  41. {
  42. if (Instance != null)
  43. Destroy(this);
  44. else
  45. {
  46. Instance = this;
  47. DontDestroyOnLoad(this);
  48. if (!ModListPresent && SelfConfig.Updates_.AutoCheckUpdates_)
  49. CheckForUpdates();
  50. }
  51. }
  52. catch (Exception e)
  53. {
  54. Logger.updater.Error(e);
  55. }
  56. }
  57. internal delegate void CheckUpdatesComplete(List<DependencyObject> toUpdate);
  58. public void CheckForUpdates(CheckUpdatesComplete onComplete = null) => StartCoroutine(CheckForUpdatesCoroutine(onComplete));
  59. internal class DependencyObject
  60. {
  61. public string Name { get; set; }
  62. public Version Version { get; set; }
  63. public Version ResolvedVersion { get; set; }
  64. public Range Requirement { get; set; }
  65. public Range Conflicts { get; set; } // a range of versions that are not allowed to be downloaded
  66. public bool Resolved { get; set; }
  67. public bool Has { get; set; }
  68. public HashSet<string> Consumers { get; set; } = new HashSet<string>();
  69. public bool MetaRequestFailed { get; set; }
  70. public PluginMetadata LocalPluginMeta { get; set; }
  71. public bool IsLegacy { get; set; } = false;
  72. public override string ToString()
  73. {
  74. return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement} ! {Conflicts}) {(Has ? " Already have" : "")}";
  75. }
  76. }
  77. public static void ResetRequestCache()
  78. {
  79. requestCache.Clear();
  80. modCache.Clear();
  81. modVersionsCache.Clear();
  82. }
  83. private static readonly Dictionary<string, string> requestCache = new Dictionary<string, string>();
  84. private static IEnumerator GetBeatModsEndpoint(string url, Ref<string> result)
  85. {
  86. if (requestCache.TryGetValue(url, out string value))
  87. {
  88. result.Value = value;
  89. }
  90. else
  91. {
  92. using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + url))
  93. {
  94. yield return request.SendWebRequest();
  95. if (request.isNetworkError)
  96. {
  97. result.Error = new NetworkException($"Network error while trying to download: {request.error}");
  98. yield break;
  99. }
  100. if (request.isHttpError)
  101. {
  102. if (request.responseCode == 404)
  103. {
  104. result.Error = new NetworkException("Not found");
  105. yield break;
  106. }
  107. result.Error = new NetworkException($"Server returned error {request.error} while getting data");
  108. yield break;
  109. }
  110. result.Value = request.downloadHandler.text;
  111. requestCache[url] = result.Value;
  112. }
  113. }
  114. }
  115. private static readonly Dictionary<string, ApiEndpoint.Mod> modCache = new Dictionary<string, ApiEndpoint.Mod>();
  116. internal static IEnumerator GetModInfo(string modName, string ver, Ref<ApiEndpoint.Mod> result)
  117. {
  118. var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeDataString(modName), Uri.EscapeDataString(ver));
  119. if (modCache.TryGetValue(uri, out ApiEndpoint.Mod value))
  120. {
  121. result.Value = value;
  122. }
  123. else
  124. {
  125. Ref<string> reqResult = new Ref<string>("");
  126. yield return GetBeatModsEndpoint(uri, reqResult);
  127. try
  128. {
  129. result.Value = JsonConvert.DeserializeObject<List<ApiEndpoint.Mod>>(reqResult.Value).First();
  130. modCache[uri] = result.Value;
  131. }
  132. catch (Exception e)
  133. {
  134. result.Error = new Exception("Error decoding response", e);
  135. }
  136. }
  137. }
  138. private static readonly Dictionary<string, List<ApiEndpoint.Mod>> modVersionsCache = new Dictionary<string, List<ApiEndpoint.Mod>>();
  139. internal static IEnumerator GetModVersionsMatching(string modName, Range range, Ref<List<ApiEndpoint.Mod>> result)
  140. {
  141. var uri = string.Format(ApiEndpoint.GetModsByName, Uri.EscapeDataString(modName));
  142. if (modVersionsCache.TryGetValue(uri, out List<ApiEndpoint.Mod> value))
  143. {
  144. result.Value = value;
  145. }
  146. else
  147. {
  148. Ref<string> reqResult = new Ref<string>("");
  149. yield return GetBeatModsEndpoint(uri, reqResult);
  150. try
  151. {
  152. result.Value = JsonConvert.DeserializeObject<List<ApiEndpoint.Mod>>(reqResult.Value)
  153. .Where(m => range.IsSatisfied(m.Version)).ToList();
  154. modVersionsCache[uri] = result.Value;
  155. }
  156. catch (Exception e)
  157. {
  158. result.Error = new Exception("Error decoding response", e);
  159. }
  160. }
  161. }
  162. internal IEnumerator CheckForUpdatesCoroutine(CheckUpdatesComplete onComplete)
  163. {
  164. var depList = new Ref<List<DependencyObject>>(new List<DependencyObject>());
  165. foreach (var plugin in BSMetas)
  166. { // initialize with data to resolve (1.1)
  167. if (plugin.Metadata.Id != null)
  168. { // updatable
  169. var msinfo = plugin.Metadata;
  170. var dep = new DependencyObject
  171. {
  172. Name = msinfo.Id,
  173. Version = msinfo.Version,
  174. Requirement = new Range($">={msinfo.Version}"),
  175. LocalPluginMeta = msinfo
  176. };
  177. depList.Value.Add(dep);
  178. }
  179. }
  180. foreach (var meta in PluginLoader.ignoredPlugins.Keys)
  181. { // update ignored
  182. if (meta.Id != null)
  183. { // updatable
  184. var dep = new DependencyObject
  185. {
  186. Name = meta.Id,
  187. Version = meta.Version,
  188. Requirement = new Range($">={meta.Version}"),
  189. LocalPluginMeta = meta
  190. };
  191. depList.Value.Add(dep);
  192. }
  193. }
  194. foreach (var meta in DisabledPlugins)
  195. { // update ignored
  196. if (meta.Id != null)
  197. { // updatable
  198. var dep = new DependencyObject
  199. {
  200. Name = meta.Id,
  201. Version = meta.Version,
  202. Requirement = new Range($">={meta.Version}"),
  203. LocalPluginMeta = meta
  204. };
  205. depList.Value.Add(dep);
  206. }
  207. }
  208. #pragma warning disable CS0618 // Type or member is obsolete
  209. foreach (var plug in Plugins)
  210. { // throw these in the updater on the off chance that they are set up properly
  211. try
  212. {
  213. var dep = new DependencyObject
  214. {
  215. Name = plug.Name,
  216. Version = new Version(plug.Version),
  217. Requirement = new Range($">={plug.Version}"),
  218. IsLegacy = true,
  219. LocalPluginMeta = null
  220. };
  221. depList.Value.Add(dep);
  222. }
  223. catch (Exception e)
  224. {
  225. Logger.updater.Warn($"Error trying to add legacy plugin {plug.Name} to updater");
  226. Logger.updater.Warn(e);
  227. }
  228. }
  229. #pragma warning restore CS0618 // Type or member is obsolete
  230. foreach (var dep in depList.Value)
  231. Logger.updater.Debug($"Phantom Dependency: {dep}");
  232. yield return ResolveDependencyRanges(depList);
  233. foreach (var dep in depList.Value)
  234. Logger.updater.Debug($"Dependency: {dep}");
  235. yield return ResolveDependencyPresence(depList);
  236. foreach (var dep in depList.Value)
  237. Logger.updater.Debug($"Dependency: {dep}");
  238. CheckDependencies(depList);
  239. onComplete?.Invoke(depList);
  240. if (!ModListPresent && SelfConfig.Updates_.AutoUpdate_)
  241. StartDownload(depList.Value);
  242. }
  243. internal IEnumerator ResolveDependencyRanges(Ref<List<DependencyObject>> list)
  244. {
  245. for (int i = 0; i < list.Value.Count; i++)
  246. { // Grab dependencies (1.2)
  247. var dep = list.Value[i];
  248. var mod = new Ref<ApiEndpoint.Mod>(null);
  249. yield return GetModInfo(dep.Name, "", mod);
  250. try { mod.Verify(); }
  251. catch (Exception e)
  252. {
  253. Logger.updater.Error($"Error getting info for {dep.Name}");
  254. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  255. Logger.updater.Error(e);
  256. dep.MetaRequestFailed = true;
  257. continue;
  258. }
  259. list.Value.AddRange(mod.Value.Dependencies.Select(m => new DependencyObject
  260. {
  261. Name = m.Name,
  262. Requirement = new Range($"^{m.Version}"),
  263. Consumers = new HashSet<string> { dep.Name }
  264. }));
  265. // currently no conflicts exist in BeatMods
  266. //list.Value.AddRange(mod.Value.Links.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = d.VersionRange, Consumers = new HashSet<string> { dep.Name } }));
  267. //list.Value.AddRange(mod.Value.Links.Conflicts.Select(d => new DependencyObject { Name = d.Name, Conflicts = d.VersionRange, Consumers = new HashSet<string> { dep.Name } }));
  268. }
  269. var depNames = new HashSet<string>();
  270. var final = new List<DependencyObject>();
  271. foreach (var dep in list.Value)
  272. { // agregate ranges and the like (1.3)
  273. if (!depNames.Contains(dep.Name))
  274. { // should add it
  275. depNames.Add(dep.Name);
  276. final.Add(dep);
  277. }
  278. else
  279. {
  280. var toMod = final.First(d => d.Name == dep.Name);
  281. if (dep.Requirement != null)
  282. {
  283. toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement);
  284. foreach (var consume in dep.Consumers)
  285. toMod.Consumers.Add(consume);
  286. }
  287. if (dep.Conflicts != null)
  288. {
  289. toMod.Conflicts = toMod.Conflicts == null
  290. ? dep.Conflicts
  291. : new Range($"{toMod.Conflicts} || {dep.Conflicts}");
  292. }
  293. }
  294. }
  295. list.Value = final;
  296. }
  297. internal IEnumerator ResolveDependencyPresence(Ref<List<DependencyObject>> list)
  298. {
  299. foreach(var dep in list.Value)
  300. {
  301. dep.Has = dep.Version != null; // dep.Version is only not null if its already installed
  302. if (dep.MetaRequestFailed)
  303. {
  304. Logger.updater.Warn($"{dep.Name} info request failed, not trying again");
  305. continue;
  306. }
  307. var modsMatching = new Ref<List<ApiEndpoint.Mod>>(null);
  308. yield return GetModVersionsMatching(dep.Name, dep.Requirement, modsMatching);
  309. try { modsMatching.Verify(); }
  310. catch (Exception e)
  311. {
  312. Logger.updater.Error($"Error getting mod list for {dep.Name}");
  313. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  314. Logger.updater.Error(e);
  315. dep.MetaRequestFailed = true;
  316. continue;
  317. }
  318. var ver = modsMatching.Value
  319. .NonNull() // entry is not null
  320. .Where(versionCheck => versionCheck.GameVersion == UnityGame.GameVersion) // game version matches
  321. .Where(approvalCheck => approvalCheck.Status == ApiEndpoint.Mod.ApprovedStatus) // version approved
  322. // TODO: fix; it seems wrong somehow
  323. .Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version)) // not a conflicting version
  324. .Select(mod => mod.Version).Max(); // (2.1) get the max version
  325. dep.Resolved = ver != null;
  326. if (dep.Resolved) dep.ResolvedVersion = ver; // (2.2)
  327. dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion;
  328. }
  329. }
  330. internal void CheckDependencies(Ref<List<DependencyObject>> list)
  331. {
  332. var toDl = new List<DependencyObject>();
  333. foreach (var dep in list.Value)
  334. { // figure out which ones need to be downloaded (3.1)
  335. if (dep.Resolved)
  336. {
  337. Logger.updater.Debug($"Resolved: {dep}");
  338. if (!dep.Has)
  339. {
  340. Logger.updater.Debug($"To Download: {dep}");
  341. toDl.Add(dep);
  342. }
  343. }
  344. else if (!dep.Has)
  345. {
  346. if (dep.Version != null && dep.Requirement.IsSatisfied(dep.Version))
  347. Logger.updater.Notice($"Mod {dep.Name} running a newer version than is on BeatMods ({dep.Version})");
  348. else
  349. Logger.updater.Warn($"Could not resolve dependency {dep}");
  350. }
  351. }
  352. Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}");
  353. list.Value = toDl;
  354. }
  355. internal delegate void DownloadStart(DependencyObject obj);
  356. internal delegate void DownloadProgress(DependencyObject obj, long totalBytes, long currentBytes, double progress);
  357. internal delegate void DownloadFailed(DependencyObject obj, string error);
  358. internal delegate void DownloadFinish(DependencyObject obj);
  359. /// <summary>
  360. /// This will still be called even if there was an error. Called after all three download/install attempts, or after a successful installation.
  361. /// ALWAYS called.
  362. /// </summary>
  363. /// <param name="obj"></param>
  364. /// <param name="didError"></param>
  365. internal delegate void InstallFinish(DependencyObject obj, bool didError);
  366. /// <summary>
  367. /// This can be called multiple times
  368. /// </summary>
  369. /// <param name="obj"></param>
  370. /// <param name="error"></param>
  371. internal delegate void InstallFailed(DependencyObject obj, Exception error);
  372. internal void StartDownload(IEnumerable<DependencyObject> download, DownloadStart downloadStart = null,
  373. DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null,
  374. InstallFailed installFail = null, InstallFinish installFinish = null)
  375. {
  376. foreach (var item in download)
  377. StartCoroutine(UpdateModCoroutine(item, downloadStart, downloadProgress, downloadFail, downloadFinish, installFail, installFinish));
  378. }
  379. private static IEnumerator UpdateModCoroutine(DependencyObject item, DownloadStart downloadStart,
  380. DownloadProgress progress, DownloadFailed dlFail, DownloadFinish finish,
  381. InstallFailed installFail, InstallFinish installFinish)
  382. { // (3.2)
  383. Logger.updater.Debug($"Release: {UnityGame.ReleaseType}");
  384. var mod = new Ref<ApiEndpoint.Mod>(null);
  385. yield return GetModInfo(item.Name, item.ResolvedVersion.ToString(), mod);
  386. try { mod.Verify(); }
  387. catch (Exception e)
  388. {
  389. Logger.updater.Error($"Error occurred while trying to get information for {item}");
  390. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  391. Logger.updater.Error(e);
  392. yield break;
  393. }
  394. var releaseName = UnityGame.ReleaseType == UnityGame.Release.Steam
  395. ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus;
  396. var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName);
  397. string url = ApiEndpoint.BeatModBase + platformFile.Path;
  398. Logger.updater.Debug($"URL = {url}");
  399. const int maxTries = 3;
  400. int tries = maxTries;
  401. while (tries > 0)
  402. {
  403. if (tries-- != maxTries)
  404. Logger.updater.Debug("Re-trying download...");
  405. using (var stream = new MemoryStream())
  406. using (var request = UnityWebRequest.Get(url))
  407. using (var taskTokenSource = new CancellationTokenSource())
  408. {
  409. var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress?.Invoke(item, i1, i2, d));
  410. request.downloadHandler = dlh;
  411. downloadStart?.Invoke(item);
  412. Logger.updater.Debug("Sending request");
  413. //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
  414. yield return request.SendWebRequest();
  415. Logger.updater.Debug("Download finished");
  416. if (request.isNetworkError)
  417. {
  418. Logger.updater.Error("Network error while trying to update mod");
  419. Logger.updater.Error(request.error);
  420. dlFail?.Invoke(item, request.error);
  421. taskTokenSource.Cancel();
  422. continue;
  423. }
  424. if (request.isHttpError)
  425. {
  426. Logger.updater.Error("Server returned an error code while trying to update mod");
  427. Logger.updater.Error(request.error);
  428. dlFail?.Invoke(item, request.error);
  429. taskTokenSource.Cancel();
  430. continue;
  431. }
  432. finish?.Invoke(item);
  433. stream.Seek(0, SeekOrigin.Begin); // reset to beginning
  434. var downloadTask = Task.Run(() =>
  435. { // use slightly more multi threaded approach than co-routines
  436. // ReSharper disable once AccessToDisposedClosure
  437. ExtractPluginAsync(stream, item, platformFile);
  438. }, taskTokenSource.Token);
  439. yield return Coroutines.WaitForTask(downloadTask);
  440. if (downloadTask.IsFaulted)
  441. {
  442. if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException))
  443. { // any exception is an intercept exception
  444. Logger.updater.Error($"BeatMods did not return expected data for {item.Name}");
  445. }
  446. else
  447. Logger.updater.Error($"Error downloading mod {item.Name}");
  448. if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
  449. Logger.updater.Error(downloadTask.Exception);
  450. installFail?.Invoke(item, downloadTask.Exception);
  451. continue;
  452. }
  453. break;
  454. }
  455. }
  456. if (tries == 0)
  457. {
  458. Logger.updater.Warn($"Plugin download failed {maxTries} times, not re-trying");
  459. installFinish?.Invoke(item, true);
  460. }
  461. else
  462. {
  463. Logger.updater.Debug("Download complete");
  464. installFinish?.Invoke(item, false);
  465. }
  466. }
  467. internal class StreamDownloadHandler : DownloadHandlerScript
  468. {
  469. internal int length;
  470. internal int cLen;
  471. internal Action<int, int, double> progress;
  472. public MemoryStream Stream { get; set; }
  473. public StreamDownloadHandler(MemoryStream stream, Action<int, int, double> progress = null)
  474. {
  475. Stream = stream;
  476. this.progress = progress;
  477. }
  478. protected override void ReceiveContentLength(int contentLength)
  479. {
  480. Stream.Capacity = length = contentLength;
  481. cLen = 0;
  482. Logger.updater.Debug($"Got content length: {contentLength}");
  483. }
  484. protected override void CompleteContent()
  485. {
  486. Logger.updater.Debug("Download complete");
  487. }
  488. protected override bool ReceiveData(byte[] rData, int dataLength)
  489. {
  490. if (rData == null || rData.Length < 1)
  491. {
  492. Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer");
  493. return false;
  494. }
  495. cLen += dataLength;
  496. Stream.Write(rData, 0, dataLength);
  497. progress?.Invoke(length, cLen, ((double)cLen) / length);
  498. return true;
  499. }
  500. protected override byte[] GetData() { return null; }
  501. protected override float GetProgress()
  502. {
  503. return 0f;
  504. }
  505. public override string ToString()
  506. {
  507. return $"{base.ToString()} ({Stream})";
  508. }
  509. }
  510. private static void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo)
  511. { // (3.3)
  512. Logger.updater.Debug($"Extracting ZIP file for {item.Name}");
  513. /*var data = stream.GetBuffer();
  514. SHA1 sha = new SHA1CryptoServiceProvider();
  515. var hash = sha.ComputeHash(data);
  516. if (!Utils.UnsafeCompare(hash, fileInfo.Hash))
  517. throw new Exception("The hash for the file doesn't match what is defined");*/
  518. var targetDir = Path.Combine(UnityGame.InstallPath, "IPA", Path.GetRandomFileName() + "_Pending");
  519. Directory.CreateDirectory(targetDir);
  520. var eventualOutput = Path.Combine(UnityGame.InstallPath, "IPA", "Pending");
  521. if (!Directory.Exists(eventualOutput))
  522. Directory.CreateDirectory(eventualOutput);
  523. try
  524. {
  525. bool shouldDeleteOldFile = !(item.LocalPluginMeta?.IsSelf).Unwrap();
  526. using (var zipFile = ZipFile.Read(stream))
  527. {
  528. Logger.updater.Debug("Streams opened");
  529. foreach (var entry in zipFile)
  530. {
  531. if (entry.IsDirectory)
  532. {
  533. Logger.updater.Debug($"Creating directory {entry.FileName}");
  534. Directory.CreateDirectory(Path.Combine(targetDir, entry.FileName));
  535. }
  536. else
  537. {
  538. using (var ostream = new MemoryStream((int)entry.UncompressedSize))
  539. {
  540. entry.Extract(ostream);
  541. ostream.Seek(0, SeekOrigin.Begin);
  542. var md5 = new MD5CryptoServiceProvider();
  543. var fileHash = md5.ComputeHash(ostream);
  544. try
  545. {
  546. if (!Utils.UnsafeCompare(fileHash, fileInfo.Hashes.Where(h => h.File == entry.FileName).Select(h => h.Hash).First()))
  547. throw new Exception("The hash for the file doesn't match what is defined");
  548. }
  549. catch (KeyNotFoundException)
  550. {
  551. throw new BeatmodsInterceptException("BeatMods did not send the hashes for the zip's content!");
  552. }
  553. ostream.Seek(0, SeekOrigin.Begin);
  554. FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName));
  555. Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException());
  556. if (item.LocalPluginMeta != null &&
  557. Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, UnityGame.InstallPath))
  558. shouldDeleteOldFile = false; // overwriting old file, no need to delete
  559. /*if (targetFile.Exists)
  560. backup.Add(targetFile);
  561. else
  562. newFiles.Add(targetFile);*/
  563. Logger.updater.Debug($"Extracting file {targetFile.FullName}");
  564. targetFile.Delete();
  565. using (var fstream = targetFile.Create())
  566. ostream.CopyTo(fstream);
  567. }
  568. }
  569. }
  570. }
  571. if (shouldDeleteOldFile && item.LocalPluginMeta != null)
  572. File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, UnityGame.InstallPath) });
  573. }
  574. catch (Exception)
  575. { // something failed; restore
  576. Directory.Delete(targetDir, true); // delete extraction site
  577. throw;
  578. }
  579. if ((item.LocalPluginMeta?.IsSelf).Unwrap())
  580. { // currently updating self, so copy to working dir and update
  581. NeedsManualRestart = true; // flag so that ModList keeps the restart button hidden
  582. Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(UnityGame.InstallPath));
  583. var deleteFile = Path.Combine(UnityGame.InstallPath, SpecialDeletionsFile);
  584. if (File.Exists(deleteFile)) File.Delete(deleteFile);
  585. Process.Start(new ProcessStartInfo
  586. {
  587. // will never actually be null
  588. FileName = item.LocalPluginMeta?.File.FullName ?? throw new InvalidOperationException(),
  589. Arguments = $"-nw={Process.GetCurrentProcess().Id}",
  590. UseShellExecute = false
  591. });
  592. }
  593. else
  594. Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile);
  595. Directory.Delete(targetDir, true); // delete extraction site
  596. Logger.updater.Debug("Extractor exited");
  597. }
  598. internal static bool NeedsManualRestart = false;
  599. }
  600. [Serializable]
  601. internal class NetworkException : Exception
  602. {
  603. public NetworkException()
  604. {
  605. }
  606. public NetworkException(string message) : base(message)
  607. {
  608. }
  609. public NetworkException(string message, Exception innerException) : base(message, innerException)
  610. {
  611. }
  612. protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context)
  613. {
  614. }
  615. }
  616. [Serializable]
  617. internal class BeatmodsInterceptException : Exception
  618. {
  619. public BeatmodsInterceptException()
  620. {
  621. }
  622. public BeatmodsInterceptException(string message) : base(message)
  623. {
  624. }
  625. public BeatmodsInterceptException(string message, Exception innerException) : base(message, innerException)
  626. {
  627. }
  628. protected BeatmodsInterceptException(SerializationInfo info, StreamingContext context) : base(info, context)
  629. {
  630. }
  631. }
  632. #endif
  633. }