Browse Source

Added support for self-updating

Updater is now slightly more robust
refactor
Anairkoen Schno 5 years ago
parent
commit
6e11bd09d0
9 changed files with 140 additions and 339 deletions
  1. +2
    -2
      IPA/Properties/AssemblyInfo.cs
  2. +2
    -2
      IllusionInjector/IllusionInjector.csproj
  3. +10
    -0
      IllusionInjector/PluginManager.cs
  4. +69
    -29
      IllusionInjector/Updating/ModsaberML/Updater.cs
  5. +0
    -207
      IllusionInjector/Updating/Old/ModUpdater.cs
  6. +0
    -98
      IllusionInjector/Updating/Old/UpdateScript.cs
  7. +55
    -0
      IllusionInjector/Updating/SelfPlugin.cs
  8. +1
    -1
      IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache
  9. +1
    -0
      IllusionPlugin/IllusionPlugin.csproj

+ 2
- 2
IPA/Properties/AssemblyInfo.cs View File

@ -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")]

+ 2
- 2
IllusionInjector/IllusionInjector.csproj View File

@ -78,10 +78,10 @@
<Compile Include="PluginManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PluginComponent.cs" />
<Compile Include="Updating\Backup\BackupUnit.cs" />
<Compile Include="Updating\ModsaberML\ApiEndpoint.cs" />
<Compile Include="Updating\ModsaberML\Updater.cs" />
<Compile Include="Updating\Old\ModUpdater.cs" />
<Compile Include="Updating\Old\UpdateScript.cs" />
<Compile Include="Updating\SelfPlugin.cs" />
<Compile Include="Utilities\Extensions.cs" />
<Compile Include="Utilities\LoneFunctions.cs" />
<Compile Include="Utilities\SimpleJson.cs" />


+ 10
- 0
IllusionInjector/PluginManager.cs View File

@ -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)


+ 69
- 29
IllusionInjector/Updating/ModsaberML/Updater.cs View File

@ -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<FileInfo>();
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))


+ 0
- 207
IllusionInjector/Updating/Old/ModUpdater.cs View File

@ -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<Uri, UpdateScript> cachedRequests = new Dictionary<Uri, UpdateScript>();
IEnumerator CheckForUpdatesCoroutine()
{
Logger.log.Info("Checking for mod updates...");
var toUpdate = new List<UpdateQueueItem>();
var plugins = new Queue<UpdateCheckQueueItem>(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
}

+ 0
- 98
IllusionInjector/Updating/Old/UpdateScript.cs View File

@ -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
* "<pluginName>": { // an entry for your plugin, using its annotated name
* "version": "<version>", // required, should be in .NET Version class format
* // note: only required if neither newName nor newScript is specified
* "newName": "<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": "<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": "<url>", // 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<string, PluginVersionInfo> info = new Dictionary<string, PluginVersionInfo>();
public IReadOnlyDictionary<string, PluginVersionInfo> 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)
{
}
}
}
}

+ 55
- 0
IllusionInjector/Updating/SelfPlugin.cs View File

@ -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()
{
}
}
}

+ 1
- 1
IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache View File

@ -1 +1 @@
4fc156f3cea4b1f659732d784ceeb2b98e58bc07
f6d2b3a83d839d28f9c6280f831d88c7c7de66f4

+ 1
- 0
IllusionPlugin/IllusionPlugin.csproj View File

@ -40,6 +40,7 @@
<Reference Include="System.Xml" />
<Reference Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>..\Libs\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>


Loading…
Cancel
Save