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.

711 lines
29 KiB

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