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.

370 lines
14 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
  1. using IPA.Utilities;
  2. using IPA.Loader;
  3. using Ionic.Zip;
  4. using Newtonsoft.Json;
  5. using System;
  6. using System.Collections;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Security.Cryptography;
  12. using System.Text;
  13. using System.Text.RegularExpressions;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. using UnityEngine;
  17. using UnityEngine.Networking;
  18. using Logger = IPA.Logging.Logger;
  19. using Version = SemVer.Version;
  20. using IPA.Updating.Backup;
  21. namespace IPA.Updating.ModsaberML
  22. {
  23. class Updater : MonoBehaviour
  24. {
  25. public static Updater instance;
  26. public void Awake()
  27. {
  28. try
  29. {
  30. if (instance != null)
  31. Destroy(this);
  32. else
  33. {
  34. instance = this;
  35. CheckForUpdates();
  36. }
  37. }
  38. catch (Exception e)
  39. {
  40. Logger.updater.Error(e);
  41. }
  42. }
  43. public void CheckForUpdates()
  44. {
  45. StartCoroutine(CheckForUpdatesCoroutine());
  46. }
  47. private class ParsedPluginMeta : PluginManager.BSPluginMeta
  48. {
  49. private Version _verCache = null;
  50. public Version ModVersion
  51. {
  52. get
  53. {
  54. if (_verCache == null)
  55. _verCache = new Version(ModsaberInfo.CurrentVersion);
  56. return _verCache;
  57. }
  58. }
  59. public ParsedPluginMeta(PluginManager.BSPluginMeta meta)
  60. {
  61. this.Plugin = meta.Plugin;
  62. this.ModsaberInfo = meta.ModsaberInfo;
  63. this.Filename = meta.Filename;
  64. }
  65. }
  66. private struct UpdateStruct
  67. {
  68. public ParsedPluginMeta plugin;
  69. public ApiEndpoint.Mod externInfo;
  70. }
  71. IEnumerator CheckForUpdatesCoroutine()
  72. {
  73. Logger.updater.Info("Checking for mod updates...");
  74. var toUpdate = new List<UpdateStruct>();
  75. var GameVersion = new Version(Application.version);
  76. foreach (var _plugin in PluginManager.BSMetas)
  77. {
  78. var plugin = new ParsedPluginMeta(_plugin);
  79. var info = plugin.ModsaberInfo;
  80. if (info == null) continue;
  81. using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, info.InternalName)))
  82. {
  83. yield return request.SendWebRequest();
  84. if (request.isNetworkError)
  85. {
  86. Logger.updater.Error("Network error while trying to update mods");
  87. Logger.updater.Error(request.error);
  88. continue;
  89. }
  90. if (request.isHttpError)
  91. {
  92. if (request.responseCode == 404)
  93. {
  94. Logger.updater.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}");
  95. continue;
  96. }
  97. Logger.updater.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}");
  98. Logger.updater.Error(request.error);
  99. continue;
  100. }
  101. var json = request.downloadHandler.text;
  102. ApiEndpoint.Mod modRegistry;
  103. try
  104. {
  105. modRegistry = JsonConvert.DeserializeObject<ApiEndpoint.Mod>(json);
  106. Logger.updater.Debug(modRegistry.ToString());
  107. }
  108. catch (Exception e)
  109. {
  110. Logger.updater.Error($"Parse error while trying to update mods");
  111. Logger.updater.Error(e);
  112. continue;
  113. }
  114. Logger.updater.Debug($"Found Modsaber.ML registration for {plugin.Plugin.Name} ({info.InternalName})");
  115. Logger.updater.Debug($"Installed version: {plugin.ModVersion}; Latest version: {modRegistry.Version}");
  116. if (modRegistry.Version > plugin.ModVersion)
  117. {
  118. Logger.updater.Debug($"{plugin.Plugin.Name} needs an update!");
  119. if (modRegistry.GameVersion == GameVersion)
  120. {
  121. Logger.updater.Debug($"Queueing update...");
  122. toUpdate.Add(new UpdateStruct
  123. {
  124. plugin = plugin,
  125. externInfo = modRegistry
  126. });
  127. }
  128. else
  129. {
  130. Logger.updater.Warn($"Update avaliable for {plugin.Plugin.Name}, but for a different Beat Saber version!");
  131. }
  132. }
  133. }
  134. }
  135. Logger.updater.Info($"{toUpdate.Count} mods need updating");
  136. if (toUpdate.Count == 0) yield break;
  137. string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName());
  138. Directory.CreateDirectory(tempDirectory);
  139. foreach (var item in toUpdate)
  140. {
  141. StartCoroutine(UpdateModCoroutine(item, tempDirectory));
  142. }
  143. }
  144. class StreamDownloadHandler : DownloadHandlerScript
  145. {
  146. public MemoryStream Stream { get; set; }
  147. public StreamDownloadHandler(MemoryStream stream) : base()
  148. {
  149. Stream = stream;
  150. }
  151. protected override void ReceiveContentLength(int contentLength)
  152. {
  153. Stream.Capacity = contentLength;
  154. Logger.updater.Debug($"Got content length: {contentLength}");
  155. }
  156. protected override void CompleteContent()
  157. {
  158. Logger.updater.Debug("Download complete");
  159. }
  160. protected override bool ReceiveData(byte[] data, int dataLength)
  161. {
  162. if (data == null || data.Length < 1)
  163. {
  164. Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer");
  165. return false;
  166. }
  167. Stream.Write(data, 0, dataLength);
  168. return true;
  169. }
  170. protected override byte[] GetData() { return null; }
  171. protected override float GetProgress()
  172. {
  173. return 0f;
  174. }
  175. public override string ToString()
  176. {
  177. return $"{base.ToString()} ({Stream?.ToString()})";
  178. }
  179. }
  180. private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory)
  181. {
  182. Logger.updater.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}");
  183. var data = stream.GetBuffer();
  184. SHA1 sha = new SHA1CryptoServiceProvider();
  185. var hash = sha.ComputeHash(data);
  186. if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash))
  187. throw new Exception("The hash for the file doesn't match what is defined");
  188. var newFiles = new List<FileInfo>();
  189. var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}");
  190. try
  191. {
  192. bool shouldDeleteOldFile = true;
  193. using (var zipFile = ZipFile.Read(stream))
  194. {
  195. Logger.updater.Debug("Streams opened");
  196. foreach (var entry in zipFile)
  197. {
  198. if (entry.IsDirectory)
  199. {
  200. Logger.updater.Debug($"Creating directory {entry.FileName}");
  201. Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName));
  202. }
  203. else
  204. {
  205. using (var ostream = new MemoryStream((int)entry.UncompressedSize))
  206. {
  207. entry.Extract(ostream);
  208. ostream.Seek(0, SeekOrigin.Begin);
  209. sha = new SHA1CryptoServiceProvider();
  210. var fileHash = sha.ComputeHash(ostream);
  211. if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName]))
  212. throw new Exception("The hash for the file doesn't match what is defined");
  213. ostream.Seek(0, SeekOrigin.Begin);
  214. FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName));
  215. Directory.CreateDirectory(targetFile.DirectoryName);
  216. if (targetFile.FullName == item.plugin.Filename)
  217. shouldDeleteOldFile = false; // overwriting old file, no need to delete
  218. if (targetFile.Exists)
  219. backup.Add(targetFile);
  220. else
  221. newFiles.Add(targetFile);
  222. Logger.updater.Debug($"Extracting file {targetFile.FullName}");
  223. var fstream = targetFile.Create();
  224. ostream.CopyTo(fstream);
  225. }
  226. }
  227. }
  228. }
  229. if (item.plugin.Plugin is SelfPlugin)
  230. { // currently updating self
  231. Process.Start(new ProcessStartInfo
  232. {
  233. FileName = item.plugin.Filename,
  234. Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait",
  235. UseShellExecute = false
  236. });
  237. }
  238. else if (shouldDeleteOldFile)
  239. File.Delete(item.plugin.Filename);
  240. }
  241. catch (Exception)
  242. { // something failed; restore
  243. foreach (var file in newFiles)
  244. file.Delete();
  245. backup.Restore();
  246. backup.Delete();
  247. throw;
  248. }
  249. backup.Delete();
  250. Logger.updater.Debug("Downloader exited");
  251. }
  252. IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory)
  253. {
  254. Logger.updater.Debug($"Steam avaliable: {SteamCheck.IsAvailable}");
  255. ApiEndpoint.Mod.PlatformFile platformFile;
  256. if (SteamCheck.IsAvailable || item.externInfo.Files.Oculus == null)
  257. platformFile = item.externInfo.Files.Steam;
  258. else
  259. platformFile = item.externInfo.Files.Oculus;
  260. string url = platformFile.DownloadPath;
  261. Logger.updater.Debug($"URL = {url}");
  262. const int MaxTries = 3;
  263. int maxTries = MaxTries;
  264. while (maxTries > 0)
  265. {
  266. if (maxTries-- != MaxTries)
  267. Logger.updater.Info($"Re-trying download...");
  268. using (var stream = new MemoryStream())
  269. using (var request = UnityWebRequest.Get(url))
  270. using (var taskTokenSource = new CancellationTokenSource())
  271. {
  272. var dlh = new StreamDownloadHandler(stream);
  273. request.downloadHandler = dlh;
  274. Logger.updater.Debug("Sending request");
  275. //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
  276. yield return request.SendWebRequest();
  277. Logger.updater.Debug("Download finished");
  278. if (request.isNetworkError)
  279. {
  280. Logger.updater.Error("Network error while trying to update mod");
  281. Logger.updater.Error(request.error);
  282. taskTokenSource.Cancel();
  283. continue;
  284. }
  285. if (request.isHttpError)
  286. {
  287. Logger.updater.Error($"Server returned an error code while trying to update mod");
  288. Logger.updater.Error(request.error);
  289. taskTokenSource.Cancel();
  290. continue;
  291. }
  292. stream.Seek(0, SeekOrigin.Begin); // reset to beginning
  293. var downloadTask = Task.Run(() =>
  294. { // use slightly more multithreaded approach than coroutines
  295. ExtractPluginAsync(stream, item, platformFile, tempDirectory);
  296. }, taskTokenSource.Token);
  297. while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
  298. yield return null; // pause coroutine until task is done
  299. if (downloadTask.IsFaulted)
  300. {
  301. Logger.updater.Error($"Error downloading mod {item.plugin.Plugin.Name}");
  302. Logger.updater.Error(downloadTask.Exception);
  303. continue;
  304. }
  305. break;
  306. }
  307. }
  308. if (maxTries == 0)
  309. Logger.updater.Warn($"Plugin download failed {MaxTries} times, not re-trying");
  310. else
  311. Logger.updater.Debug("Download complete");
  312. }
  313. }
  314. }