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.

347 lines
13 KiB

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