From 6e11bd09d0298ff7340d2ba7d2606e3754dd909a Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Sun, 5 Aug 2018 11:02:52 -0500 Subject: [PATCH] Added support for self-updating Updater is now slightly more robust --- IPA/Properties/AssemblyInfo.cs | 4 +- IllusionInjector/IllusionInjector.csproj | 4 +- IllusionInjector/PluginManager.cs | 10 + .../Updating/ModsaberML/Updater.cs | 98 ++++++--- IllusionInjector/Updating/Old/ModUpdater.cs | 207 ------------------ IllusionInjector/Updating/Old/UpdateScript.cs | 98 --------- IllusionInjector/Updating/SelfPlugin.cs | 55 +++++ ...ionInjector.csproj.CoreCompileInputs.cache | 2 +- IllusionPlugin/IllusionPlugin.csproj | 1 + 9 files changed, 140 insertions(+), 339 deletions(-) delete mode 100644 IllusionInjector/Updating/Old/ModUpdater.cs delete mode 100644 IllusionInjector/Updating/Old/UpdateScript.cs create mode 100644 IllusionInjector/Updating/SelfPlugin.cs diff --git a/IPA/Properties/AssemblyInfo.cs b/IPA/Properties/AssemblyInfo.cs index 8f4ac1df..85e4516e 100644 --- a/IPA/Properties/AssemblyInfo.cs +++ b/IPA/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.8.1")] -[assembly: AssemblyFileVersion("3.8.1")] +[assembly: AssemblyVersion("3.8.2")] +[assembly: AssemblyFileVersion("3.8.2")] diff --git a/IllusionInjector/IllusionInjector.csproj b/IllusionInjector/IllusionInjector.csproj index 7aa1a08f..9063611c 100644 --- a/IllusionInjector/IllusionInjector.csproj +++ b/IllusionInjector/IllusionInjector.csproj @@ -78,10 +78,10 @@ + - - + diff --git a/IllusionInjector/PluginManager.cs b/IllusionInjector/PluginManager.cs index c907cb1f..88dfccec 100644 --- a/IllusionInjector/PluginManager.cs +++ b/IllusionInjector/PluginManager.cs @@ -1,4 +1,5 @@ using IllusionInjector.Logging; +using IllusionInjector.Updating; using IllusionInjector.Utilities; using IllusionPlugin; using IllusionPlugin.BeatSaber; @@ -100,6 +101,15 @@ namespace IllusionInjector File.Copy(Path.Combine(pluginDirectory, s), pluginCopy); } + var selfPlugin = new BSPluginMeta + { + Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"), + Plugin = new SelfPlugin() + }; + selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo; + + _bsPlugins.Add(selfPlugin); + //Load copied plugins string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll"); foreach (string s in copiedPlugins) diff --git a/IllusionInjector/Updating/ModsaberML/Updater.cs b/IllusionInjector/Updating/ModsaberML/Updater.cs index 46fb0a09..6982322e 100644 --- a/IllusionInjector/Updating/ModsaberML/Updater.cs +++ b/IllusionInjector/Updating/ModsaberML/Updater.cs @@ -1,9 +1,11 @@ -using IllusionInjector.Utilities; +using IllusionInjector.Updating.Backup; +using IllusionInjector.Utilities; using Ionic.Zip; using SimpleJSON; using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -143,9 +145,11 @@ namespace IllusionInjector.Updating.ModsaberML 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)); + StartCoroutine(UpdateModCoroutine(item, tempDirectory)); } } @@ -195,7 +199,7 @@ namespace IllusionInjector.Updating.ModsaberML } } - private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo) + private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory) { Logger.log.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}"); //var stream = await httpClient.GetStreamAsync(url); @@ -206,47 +210,83 @@ namespace IllusionInjector.Updating.ModsaberML if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash)) throw new Exception("The hash for the file doesn't match what is defined"); - using (var zipFile = ZipFile.Read(stream)) + var newFiles = new List(); + var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}"); + + try { - Logger.log.Debug("Streams opened"); - foreach (var entry in zipFile) + bool shouldDeleteOldFile = true; + + using (var zipFile = ZipFile.Read(stream)) { - if (entry.IsDirectory) - { - Logger.log.Debug($"Creating directory {entry.FileName}"); - Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName)); - } - else + Logger.log.Debug("Streams opened"); + foreach (var entry in zipFile) { - using (var ostream = new MemoryStream((int)entry.UncompressedSize)) + if (entry.IsDirectory) { - entry.Extract(ostream); - ostream.Seek(0, SeekOrigin.Begin); + Logger.log.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"); + 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)); - if (targetFile.Exists) - { - } + ostream.Seek(0, SeekOrigin.Begin); + FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName)); + + 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.log.Debug($"Extracting file {targetFile.FullName}"); + Logger.log.Debug($"Extracting file {targetFile.FullName}"); - var fstream = targetFile.Create(); - ostream.CopyTo(fstream); + 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.log.Debug("Downloader exited"); } - IEnumerator UpdateModCoroutine(UpdateStruct item) + IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory) { ApiEndpoint.Mod.PlatformFile platformFile; if (SteamCheck.IsAvailable || item.externInfo.OculusFile == null) @@ -296,7 +336,7 @@ namespace IllusionInjector.Updating.ModsaberML var downloadTask = Task.Run(() => { // use slightly more multithreaded approach than coroutines - ExtractPluginAsync(stream, item, platformFile); + ExtractPluginAsync(stream, item, platformFile, tempDirectory); }, taskTokenSource.Token); while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted)) diff --git a/IllusionInjector/Updating/Old/ModUpdater.cs b/IllusionInjector/Updating/Old/ModUpdater.cs deleted file mode 100644 index 99ab7409..00000000 --- a/IllusionInjector/Updating/Old/ModUpdater.cs +++ /dev/null @@ -1,207 +0,0 @@ -using IllusionInjector.Logging; -using SimpleJSON; -using System; -using System.Collections.Generic; -using System.Collections; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using UnityEngine.Networking; -using UnityEngine; -using IllusionPlugin; -using System.Text.RegularExpressions; -using Logger = IllusionInjector.Logging.Logger; - -namespace IllusionInjector.Updating -{ -#if OLD_UPDATER - class ModUpdater : MonoBehaviour - { - public ModUpdater instance; - - public void Awake() - { - instance = this; - CheckForUpdates(); - } - - public void CheckForUpdates() - { - StartCoroutine(CheckForUpdatesCoroutine()); - } - - struct UpdateCheckQueueItem - { - public PluginManager.BSPluginMeta Plugin; - public Uri UpdateUri; - public string Name; - } - - struct UpdateQueueItem - { - public PluginManager.BSPluginMeta Plugin; - public Uri DownloadUri; - public string Name; - public Version NewVersion; - } - - private Regex commentRegex = new Regex(@"(?: \/\/.+)?$", RegexOptions.Compiled | RegexOptions.Multiline); - private Dictionary cachedRequests = new Dictionary(); - IEnumerator CheckForUpdatesCoroutine() - { - Logger.log.Info("Checking for mod updates..."); - - var toUpdate = new List(); - var plugins = new Queue(PluginManager.BSMetas.Select(p => new UpdateCheckQueueItem { Plugin = p, UpdateUri = p.Plugin.UpdateUri, Name = p.Plugin.Name })); - - for (; plugins.Count > 0;) - { - var plugin = plugins.Dequeue(); - - Logger.log.Debug($"Checking for updates for {plugin.Name}"); - - if (plugin.UpdateUri != null) - { - if (!cachedRequests.ContainsKey(plugin.UpdateUri)) - using (var request = UnityWebRequest.Get(plugin.UpdateUri)) - { - yield return request.SendWebRequest(); - - if (request.isNetworkError) - { - Logger.log.Error("Network error while trying to update mods"); - Logger.log.Error(request.error); - break; - } - if (request.isHttpError) - { - Logger.log.Error($"Server returned an error code while trying to update mod {plugin.Name}"); - Logger.log.Error(request.error); - } - - var json = request.downloadHandler.text; - - json = commentRegex.Replace(json, ""); - - JSONObject obj = null; - try - { - obj = JSON.Parse(json).AsObject; - } - catch (InvalidCastException) - { - Logger.log.Error($"Parse error while trying to update mod {plugin.Name}"); - Logger.log.Error($"Response doesn't seem to be a JSON object"); - continue; - } - catch (Exception e) - { - Logger.log.Error($"Parse error while trying to update mod {plugin.Name}"); - Logger.log.Error(e); - continue; - } - - UpdateScript ss; - try - { - ss = UpdateScript.Parse(obj); - } - catch (Exception e) - { - Logger.log.Error($"Parse error while trying to update mod {plugin.Name}"); - Logger.log.Error($"Script at {plugin.UpdateUri} doesn't seem to be a valid update script"); - Logger.log.Debug(e); - continue; - } - - cachedRequests.Add(plugin.UpdateUri, ss); - } - - var script = cachedRequests[plugin.UpdateUri]; - if (script.Info.TryGetValue(plugin.Name, out UpdateScript.PluginVersionInfo info)) - { - Logger.log.Debug($"Checking version info for {plugin.Name} ({plugin.Plugin.Plugin.Name})"); - if (info.NewName != null || info.NewScript != null) - plugins.Enqueue(new UpdateCheckQueueItem - { - Plugin = plugin.Plugin, - Name = info.NewName ?? plugin.Name, - UpdateUri = info.NewScript ?? plugin.UpdateUri - }); - else - { - Logger.log.Debug($"New version: {info.Version}, Current version: {plugin.Plugin.Plugin.Version}"); - if (info.Version > plugin.Plugin.Plugin.Version) - { // we should update plugin - Logger.log.Debug($"Queueing update for {plugin.Name} ({plugin.Plugin.Plugin.Name})"); - - toUpdate.Add(new UpdateQueueItem - { - Plugin = plugin.Plugin, - DownloadUri = info.Download, - Name = plugin.Name, - NewVersion = info.Version - }); - } - } - } - else - { - Logger.log.Error($"Script defined for plugin {plugin.Name} doesn't define information for {plugin.Name}"); - continue; - } - } - } - - Logger.log.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); - Logger.log.Debug($"Created temp download dirtectory {tempDirectory}"); - foreach (var item in toUpdate) - { - StartCoroutine(DownloadPluginCoroutine(tempDirectory, item)); - } - } - - IEnumerator DownloadPluginCoroutine(string tempdir, UpdateQueueItem item) - { - var file = Path.Combine(tempdir, item.Name + ".dll"); - - using (var req = UnityWebRequest.Get(item.DownloadUri)) - { - req.downloadHandler = new DownloadHandlerFile(file); - yield return req.SendWebRequest(); - - if (req.isNetworkError) - { - Logger.log.Error($"Network error while trying to download update for {item.Plugin.Plugin.Name}"); - Logger.log.Error(req.error); - yield break; - } - if (req.isHttpError) - { - Logger.log.Error($"Server returned an error code while trying to download update for {item.Plugin.Plugin.Name}"); - Logger.log.Error(req.error); - yield break; - } - } - - var pluginDir = Path.GetDirectoryName(item.Plugin.Filename); - var newFile = Path.Combine(pluginDir, item.Name + ".dll"); - - File.Delete(item.Plugin.Filename); - if (File.Exists(newFile)) - File.Delete(newFile); - File.Move(file, newFile); - - Logger.log.Info($"{item.Plugin.Plugin.Name} updated to {item.NewVersion}"); - } - } -#endif -} diff --git a/IllusionInjector/Updating/Old/UpdateScript.cs b/IllusionInjector/Updating/Old/UpdateScript.cs deleted file mode 100644 index e133b676..00000000 --- a/IllusionInjector/Updating/Old/UpdateScript.cs +++ /dev/null @@ -1,98 +0,0 @@ -using SimpleJSON; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace IllusionInjector.Updating -{ - /** // JSON format - * { - * "_updateScript": "0.1", // version - * "": { // an entry for your plugin, using its annotated name - * "version": "", // required, should be in .NET Version class format - * // note: only required if neither newName nor newScript is specified - * "newName": "", // optional, defines a new name for the plugin (gets saved under this name) - * // (updater will also check this file for this name to get latest) - * "newScript": "", // optional, defines a new location for the update script - * // updater will look here for latest version too - * // note: if both newName and newScript are defined, the updater will only look in newScript - * // for newName, and not any other combination - * "download": "", // required, defines URL to use for downloading new version - * // note: only required if neither newName nor newScript is specified - * }, - * ... - * } - */ - - class UpdateScript - { - static readonly Version ScriptVersion = new Version(0, 1); - - public Version Version { get; private set; } - - private Dictionary info = new Dictionary(); - public IReadOnlyDictionary Info { get => info; } - - public class PluginVersionInfo - { - public Version Version { get; protected internal set; } - public string NewName { get; protected internal set; } - public Uri NewScript { get; protected internal set; } - public Uri Download { get; protected internal set; } - } - - public static UpdateScript Parse(JSONObject jscript) - { - var script = new UpdateScript - { - Version = Version.Parse(jscript["_updateScript"].Value) - }; - if (script.Version != ScriptVersion) - throw new UpdateScriptParseException("Script version mismatch"); - - jscript.Remove("_updateScript"); - - foreach (var kvp in jscript) - { - var obj = kvp.Value.AsObject; - var pvi = new PluginVersionInfo - { - Version = obj.Linq.Any(p => p.Key == "version") ? Version.Parse(obj["version"].Value) : null, - Download = obj.Linq.Any(p => p.Key == "download") ? new Uri(obj["download"].Value) : null, - - NewName = obj.Linq.Any(p => p.Key == "newName") ? obj["newName"] : null, - NewScript = obj.Linq.Any(p => p.Key == "newScript") ? new Uri(obj["newScript"]) : null - }; - if (pvi.NewName == null && pvi.NewScript == null && (pvi.Version == null || pvi.Download == null)) - throw new UpdateScriptParseException($"Required fields missing from object {kvp.Key}"); - - script.info.Add(kvp.Key, pvi); - } - - return script; - } - - [Serializable] - private class UpdateScriptParseException : Exception - { - public UpdateScriptParseException() - { - } - - public UpdateScriptParseException(string message) : base(message) - { - } - - public UpdateScriptParseException(string message, Exception innerException) : base(message, innerException) - { - } - - protected UpdateScriptParseException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } - } -} diff --git a/IllusionInjector/Updating/SelfPlugin.cs b/IllusionInjector/Updating/SelfPlugin.cs new file mode 100644 index 00000000..6b6b1b28 --- /dev/null +++ b/IllusionInjector/Updating/SelfPlugin.cs @@ -0,0 +1,55 @@ +using IllusionPlugin; +using IllusionPlugin.BeatSaber; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine.SceneManagement; + +namespace IllusionInjector.Updating +{ + internal class SelfPlugin : IBeatSaberPlugin + { + internal const string IPA_Name = "Beat Saber IPA"; + internal const string IPA_Version = "3.8.2"; + + public string Name => IPA_Name; + + public string Version => IPA_Version; + + public ModsaberModInfo ModInfo => new ModsaberModInfo + { + CurrentVersion = new Version(IPA_Version), + InternalName = "beatsaber-ipa-reloaded" + }; + + public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) + { + } + + public void OnApplicationQuit() + { + } + + public void OnApplicationStart() + { + } + + public void OnFixedUpdate() + { + } + + public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) + { + } + + public void OnSceneUnloaded(Scene scene) + { + } + + public void OnUpdate() + { + } + } +} diff --git a/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache b/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache index 264d2538..3de3543e 100644 --- a/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache +++ b/IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -4fc156f3cea4b1f659732d784ceeb2b98e58bc07 +f6d2b3a83d839d28f9c6280f831d88c7c7de66f4 diff --git a/IllusionPlugin/IllusionPlugin.csproj b/IllusionPlugin/IllusionPlugin.csproj index 9aa220d7..8375b907 100644 --- a/IllusionPlugin/IllusionPlugin.csproj +++ b/IllusionPlugin/IllusionPlugin.csproj @@ -40,6 +40,7 @@ ..\Libs\UnityEngine.CoreModule.dll + False