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.

278 lines
9.6 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.Text;
  10. using System.Text.RegularExpressions;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. using UnityEngine;
  14. using UnityEngine.Networking;
  15. using Logger = IllusionInjector.Logging.Logger;
  16. namespace IllusionInjector.Updating.ModsaberML
  17. {
  18. class Updater : MonoBehaviour
  19. {
  20. public static Updater instance;
  21. public void Awake()
  22. {
  23. try
  24. {
  25. if (instance != null)
  26. Destroy(this);
  27. else
  28. {
  29. instance = this;
  30. CheckForUpdates();
  31. }
  32. }
  33. catch (Exception e)
  34. {
  35. Logger.log.Error(e);
  36. }
  37. }
  38. public void CheckForUpdates()
  39. {
  40. StartCoroutine(CheckForUpdatesCoroutine());
  41. }
  42. private struct UpdateStruct
  43. {
  44. public PluginManager.BSPluginMeta plugin;
  45. public ApiEndpoint.Mod externInfo;
  46. }
  47. IEnumerator CheckForUpdatesCoroutine()
  48. {
  49. Logger.log.Info("Checking for mod updates...");
  50. var toUpdate = new List<UpdateStruct>();
  51. var modList = new List<ApiEndpoint.Mod>();
  52. using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase+ApiEndpoint.GetApprovedEndpoint))
  53. {
  54. yield return request.SendWebRequest();
  55. if (request.isNetworkError)
  56. {
  57. Logger.log.Error("Network error while trying to update mods");
  58. Logger.log.Error(request.error);
  59. yield break;
  60. }
  61. if (request.isHttpError)
  62. {
  63. Logger.log.Error($"Server returned an error code while trying to update mods");
  64. Logger.log.Error(request.error);
  65. }
  66. var json = request.downloadHandler.text;
  67. JSONObject obj = null;
  68. try
  69. {
  70. obj = JSON.Parse(json).AsObject;
  71. }
  72. catch (InvalidCastException)
  73. {
  74. Logger.log.Error($"Parse error while trying to update mods");
  75. Logger.log.Error($"Response doesn't seem to be a JSON object");
  76. yield break;
  77. }
  78. catch (Exception e)
  79. {
  80. Logger.log.Error($"Parse error while trying to update mods");
  81. Logger.log.Error(e);
  82. yield break;
  83. }
  84. foreach (var modObj in obj["mods"].AsArray.Children)
  85. {
  86. try
  87. {
  88. modList.Add(ApiEndpoint.Mod.DecodeJSON(modObj.AsObject));
  89. }
  90. catch (Exception e)
  91. {
  92. Logger.log.Error($"Parse error while trying to update mods");
  93. Logger.log.Error($"Response doesn't seem to be correctly formatted");
  94. Logger.log.Error(e);
  95. break;
  96. }
  97. }
  98. }
  99. var GameVersion = new Version(Application.version);
  100. foreach (var plugin in PluginManager.BSMetas)
  101. {
  102. var info = plugin.ModsaberInfo;
  103. var modRegistry = modList.FirstOrDefault(o => o.Name == info.InternalName);
  104. if (modRegistry != null)
  105. { // a.k.a we found it
  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. string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName());
  130. Directory.CreateDirectory(tempDirectory);
  131. Logger.log.Debug($"Created temp download dirtectory {tempDirectory}");
  132. foreach (var item in toUpdate)
  133. {
  134. StartCoroutine(UpdateModCoroutine(tempDirectory, item));
  135. }
  136. }
  137. public class StreamDownloadHandler : DownloadHandlerScript
  138. {
  139. public BlockingStream Stream { get; set; }
  140. public StreamDownloadHandler(BlockingStream stream)
  141. {
  142. Stream = stream;
  143. }
  144. protected void ReceiveContentLength(long contentLength)
  145. {
  146. //(Stream.BaseStream as MemoryStream).Capacity = (int)contentLength;
  147. Logger.log.Debug($"Got content length: {contentLength}");
  148. }
  149. protected void OnContentComplete()
  150. {
  151. Stream.Open = false;
  152. Logger.log.Debug("Download complete");
  153. }
  154. protected bool ReceiveData(byte[] data, long dataLength)
  155. {
  156. Logger.log.Debug("ReceiveData");
  157. if (data == null || data.Length < 1)
  158. {
  159. Logger.log.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer");
  160. return false;
  161. }
  162. Stream.Write(data, 0, (int)dataLength);
  163. return true;
  164. }
  165. protected override byte[] GetData() { return null; }
  166. protected override float GetProgress()
  167. {
  168. return 0f;
  169. }
  170. public override string ToString()
  171. {
  172. return $"{base.ToString()} ({Stream?.ToString()})";
  173. }
  174. }
  175. private void DownloadPluginAsync(BlockingStream stream, UpdateStruct item, string tempdir)
  176. {
  177. Logger.log.Debug($"Getting ZIP file for {item.plugin.Plugin.Name}");
  178. //var stream = await httpClient.GetStreamAsync(url);
  179. using (var zipFile = new ZipInputStream(stream))
  180. {
  181. Logger.log.Debug("Streams opened");
  182. ZipEntry entry;
  183. while ((entry = zipFile.GetNextEntry()) != null)
  184. {
  185. Logger.log.Debug(entry?.FileName ?? "NULL");
  186. }
  187. }
  188. Logger.log.Debug("Downloader exited");
  189. }
  190. IEnumerator UpdateModCoroutine(string tempdir, UpdateStruct item)
  191. {
  192. string url;
  193. if (SteamCheck.IsAvailable || item.externInfo.OculusFile == null)
  194. url = item.externInfo.SteamFile;
  195. else
  196. url = item.externInfo.OculusFile;
  197. Logger.log.Debug($"URL = {url}");
  198. using (var memStream = new EchoStream())
  199. using (var stream = new BlockingStream(memStream))
  200. using (var request = UnityWebRequest.Get(url))
  201. using (var taskTokenSource = new CancellationTokenSource())
  202. {
  203. var dlh = new StreamDownloadHandler(stream);
  204. request.downloadHandler = dlh;
  205. var downloadTask = Task.Run(() =>
  206. { // use slightly more multithreaded approach than coroutines
  207. DownloadPluginAsync(stream, item, tempdir);
  208. }, taskTokenSource.Token);
  209. Logger.log.Debug("Sending request");
  210. Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
  211. yield return request.SendWebRequest();
  212. Logger.log.Debug("Download finished");
  213. if (stream.Open)
  214. { // anti-hang
  215. Logger.log.Warn("Downloader failed to call DownloadHandler");
  216. stream.Open = false; // no more writing
  217. stream.BaseStream.Write(new byte[] { 0 }, 0, 1);
  218. }
  219. if (request.isNetworkError)
  220. {
  221. Logger.log.Error("Network error while trying to update mod");
  222. Logger.log.Error(request.error);
  223. taskTokenSource.Cancel();
  224. yield break;
  225. }
  226. if (request.isHttpError)
  227. {
  228. Logger.log.Error($"Server returned an error code while trying to update mod");
  229. Logger.log.Error(request.error);
  230. taskTokenSource.Cancel();
  231. yield break;
  232. }
  233. downloadTask.Wait(); // wait for the damn thing to finish
  234. }
  235. yield return null;
  236. }
  237. }
  238. }