|
|
- using IPA.Utilities;
- using IPA.Loader;
- using Ionic.Zip;
- using Newtonsoft.Json;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.IO;
- using System.Linq;
- using System.Security.Cryptography;
- using System.Text;
- using System.Text.RegularExpressions;
- using System.Threading;
- using System.Threading.Tasks;
- using UnityEngine;
- using UnityEngine.Networking;
- using Logger = IPA.Logging.Logger;
- using Version = SemVer.Version;
- using IPA.Updating.Backup;
-
- namespace IPA.Updating.ModsaberML
- {
- class Updater : MonoBehaviour
- {
- public static Updater instance;
-
- public void Awake()
- {
- try
- {
- if (instance != null)
- Destroy(this);
- else
- {
- instance = this;
- CheckForUpdates();
- }
- }
- catch (Exception e)
- {
- Logger.updater.Error(e);
- }
- }
-
- public void CheckForUpdates()
- {
- StartCoroutine(CheckForUpdatesCoroutine());
- }
-
- private class ParsedPluginMeta : PluginManager.BSPluginMeta
- {
- private Version _verCache = null;
- public Version ModVersion
- {
- get
- {
- if (_verCache == null)
- _verCache = new Version(ModsaberInfo.CurrentVersion);
- return _verCache;
- }
- }
-
- public ParsedPluginMeta(PluginManager.BSPluginMeta meta)
- {
- this.Plugin = meta.Plugin;
- this.ModsaberInfo = meta.ModsaberInfo;
- this.Filename = meta.Filename;
- }
- }
-
- private struct UpdateStruct
- {
- public ParsedPluginMeta plugin;
- public ApiEndpoint.Mod externInfo;
- }
-
- IEnumerator CheckForUpdatesCoroutine()
- {
- Logger.updater.Info("Checking for mod updates...");
-
- var toUpdate = new List<UpdateStruct>();
- var GameVersion = new Version(Application.version);
-
- foreach (var _plugin in PluginManager.BSMetas)
- {
- var plugin = new ParsedPluginMeta(_plugin);
- var info = plugin.ModsaberInfo;
- if (info == null) continue;
-
- using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, info.InternalName)))
- {
- yield return request.SendWebRequest();
-
- if (request.isNetworkError)
- {
- Logger.updater.Error("Network error while trying to update mods");
- Logger.updater.Error(request.error);
- continue;
- }
- if (request.isHttpError)
- {
- if (request.responseCode == 404)
- {
- Logger.updater.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}");
- continue;
- }
-
- Logger.updater.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}");
- Logger.updater.Error(request.error);
- continue;
- }
-
- var json = request.downloadHandler.text;
-
- ApiEndpoint.Mod modRegistry;
- try
- {
- modRegistry = JsonConvert.DeserializeObject<ApiEndpoint.Mod>(json);
- Logger.updater.Debug(modRegistry.ToString());
- }
- catch (Exception e)
- {
- Logger.updater.Error($"Parse error while trying to update mods");
- Logger.updater.Error(e);
- continue;
- }
-
- Logger.updater.Debug($"Found Modsaber.ML registration for {plugin.Plugin.Name} ({info.InternalName})");
- Logger.updater.Debug($"Installed version: {plugin.ModVersion}; Latest version: {modRegistry.Version}");
- if (modRegistry.Version > plugin.ModVersion)
- {
- Logger.updater.Debug($"{plugin.Plugin.Name} needs an update!");
- if (modRegistry.GameVersion == GameVersion)
- {
- Logger.updater.Debug($"Queueing update...");
- toUpdate.Add(new UpdateStruct
- {
- plugin = plugin,
- externInfo = modRegistry
- });
- }
- else
- {
- Logger.updater.Warn($"Update avaliable for {plugin.Plugin.Name}, but for a different Beat Saber version!");
- }
- }
- }
- }
-
- Logger.updater.Info($"{toUpdate.Count} mods need updating");
-
- if (toUpdate.Count == 0) yield break;
-
- string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName());
- Directory.CreateDirectory(tempDirectory);
- foreach (var item in toUpdate)
- {
- StartCoroutine(UpdateModCoroutine(item, tempDirectory));
- }
- }
-
- class StreamDownloadHandler : DownloadHandlerScript
- {
- public MemoryStream Stream { get; set; }
-
- public StreamDownloadHandler(MemoryStream stream) : base()
- {
- Stream = stream;
- }
-
- protected override void ReceiveContentLength(int contentLength)
- {
- Stream.Capacity = contentLength;
- Logger.updater.Debug($"Got content length: {contentLength}");
- }
-
- protected override void CompleteContent()
- {
- Logger.updater.Debug("Download complete");
- }
-
- protected override bool ReceiveData(byte[] data, int dataLength)
- {
- if (data == null || data.Length < 1)
- {
- Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer");
- return false;
- }
-
- Stream.Write(data, 0, dataLength);
- return true;
- }
-
- protected override byte[] GetData() { return null; }
-
- protected override float GetProgress()
- {
- return 0f;
- }
-
- public override string ToString()
- {
- return $"{base.ToString()} ({Stream?.ToString()})";
- }
- }
-
- private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory)
- {
- Logger.updater.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}");
-
- var data = stream.GetBuffer();
- SHA1 sha = new SHA1CryptoServiceProvider();
- var hash = sha.ComputeHash(data);
- if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash))
- throw new Exception("The hash for the file doesn't match what is defined");
-
- var newFiles = new List<FileInfo>();
- var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}");
-
- try
- {
- bool shouldDeleteOldFile = true;
-
- using (var zipFile = ZipFile.Read(stream))
- {
- Logger.updater.Debug("Streams opened");
- foreach (var entry in zipFile)
- {
- if (entry.IsDirectory)
- {
- Logger.updater.Debug($"Creating directory {entry.FileName}");
- Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName));
- }
- else
- {
- using (var ostream = new MemoryStream((int)entry.UncompressedSize))
- {
- entry.Extract(ostream);
- ostream.Seek(0, SeekOrigin.Begin);
-
- sha = new SHA1CryptoServiceProvider();
- var fileHash = sha.ComputeHash(ostream);
- if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName]))
- throw new Exception("The hash for the file doesn't match what is defined");
-
- ostream.Seek(0, SeekOrigin.Begin);
- FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName));
- Directory.CreateDirectory(targetFile.DirectoryName);
-
- if (targetFile.FullName == item.plugin.Filename)
- shouldDeleteOldFile = false; // overwriting old file, no need to delete
-
- if (targetFile.Exists)
- backup.Add(targetFile);
- else
- newFiles.Add(targetFile);
-
- Logger.updater.Debug($"Extracting file {targetFile.FullName}");
-
- var fstream = targetFile.Create();
- ostream.CopyTo(fstream);
- }
- }
- }
- }
-
- if (item.plugin.Plugin is SelfPlugin)
- { // currently updating self
- Process.Start(new ProcessStartInfo
- {
- FileName = item.plugin.Filename,
- Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait",
- UseShellExecute = false
- });
- }
- else if (shouldDeleteOldFile)
- File.Delete(item.plugin.Filename);
- }
- catch (Exception)
- { // something failed; restore
- foreach (var file in newFiles)
- file.Delete();
- backup.Restore();
- backup.Delete();
-
- throw;
- }
-
- backup.Delete();
-
- Logger.updater.Debug("Downloader exited");
- }
-
- IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory)
- {
- Logger.updater.Debug($"Steam avaliable: {SteamCheck.IsAvailable}");
-
- ApiEndpoint.Mod.PlatformFile platformFile;
- if (SteamCheck.IsAvailable || item.externInfo.Files.Oculus == null)
- platformFile = item.externInfo.Files.Steam;
- else
- platformFile = item.externInfo.Files.Oculus;
-
- string url = platformFile.DownloadPath;
-
- Logger.updater.Debug($"URL = {url}");
-
- const int MaxTries = 3;
- int maxTries = MaxTries;
- while (maxTries > 0)
- {
- if (maxTries-- != MaxTries)
- Logger.updater.Info($"Re-trying download...");
-
- using (var stream = new MemoryStream())
- using (var request = UnityWebRequest.Get(url))
- using (var taskTokenSource = new CancellationTokenSource())
- {
- var dlh = new StreamDownloadHandler(stream);
- request.downloadHandler = dlh;
-
- Logger.updater.Debug("Sending request");
- //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
- yield return request.SendWebRequest();
- Logger.updater.Debug("Download finished");
-
- if (request.isNetworkError)
- {
- Logger.updater.Error("Network error while trying to update mod");
- Logger.updater.Error(request.error);
- taskTokenSource.Cancel();
- continue;
- }
- if (request.isHttpError)
- {
- Logger.updater.Error($"Server returned an error code while trying to update mod");
- Logger.updater.Error(request.error);
- taskTokenSource.Cancel();
- continue;
- }
-
- stream.Seek(0, SeekOrigin.Begin); // reset to beginning
-
- var downloadTask = Task.Run(() =>
- { // use slightly more multithreaded approach than coroutines
- ExtractPluginAsync(stream, item, platformFile, tempDirectory);
- }, taskTokenSource.Token);
-
- while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
- yield return null; // pause coroutine until task is done
-
- if (downloadTask.IsFaulted)
- {
- Logger.updater.Error($"Error downloading mod {item.plugin.Plugin.Name}");
- Logger.updater.Error(downloadTask.Exception);
- continue;
- }
-
- break;
- }
- }
-
- if (maxTries == 0)
- Logger.updater.Warn($"Plugin download failed {MaxTries} times, not re-trying");
- else
- Logger.updater.Debug("Download complete");
- }
- }
- }
|