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.

322 lines
12 KiB

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