Browse Source

The updater works, dont worry. Really this time.

refactor
Anairkoen Schno 5 years ago
parent
commit
c019c52f38
9 changed files with 181 additions and 216 deletions
  1. +4
    -24
      IPA.Tests/updater_test.json
  2. +1
    -0
      IPA/PatchContext.cs
  3. +2
    -2
      IllusionInjector/IllusionInjector.csproj
  4. +23
    -5
      IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs
  5. +108
    -56
      IllusionInjector/Updating/ModsaberML/Updater.cs
  6. +0
    -88
      IllusionInjector/Utilities/BlockingStream.cs
  7. +0
    -40
      IllusionInjector/Utilities/EchoStream.cs
  8. +42
    -0
      IllusionInjector/Utilities/LoneFunctions.cs
  9. +1
    -1
      IllusionInjector/obj/Debug/IllusionInjector.csproj.CoreCompileInputs.cache

+ 4
- 24
IPA.Tests/updater_test.json View File

@ -15,32 +15,12 @@
"conflictsWith": [],
"files": {
"steam": {
"hash": "b38f5f58b0131e10d4676cc94dad5315e2999aa8",
"hash": "a94e7eea2f656b2830a86000ee222b6cb06f8da5",
"files": {
"CustomSongs/One More Time/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"CustomSongs/One More Time/cover.jpg": "95dbaecba1ac8165f4cd0a83950a31859931db27",
"CustomSongs/One More Time/Credits.txt": "d067a9f5e0e7c6301dd9f6e8e7a609e82815345c",
"CustomSongs/One More Time/Easy.json": "e93e234a6fd38afc3d216e6d0e1585f19cb49f69",
"CustomSongs/One More Time/Expert.json": "8bd7f7e0e0782dce7b77a3bc8bdbc091f2fd202f",
"CustomSongs/One More Time/Hard.json": "99e693c77d280c37a6cfd9f479f158cd6c84a354",
"CustomSongs/One More Time/info.json": "d214d1990af6debf38ccfdb7c57462926fb0995c",
"CustomSongs/One More Time/lyrics.srt": "b3ed8a5f4fc3595fb097943379fc8ea22548bd51",
"CustomSongs/One More Time/Normal.json": "0f7c68b29dfb8c5cfde34b0dc3238353e2f9b24b",
"CustomSongs/One More Time/One More Time.ogg": "4d74e9a58341439299512dce2155c97a00d3fb3b",
"IPA.exe": "57521373f240845798bb99723ceb19ee09774131",
"IPA/Backups/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"IPA/Data/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"IPA/Data/Managed/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"IPA/Data/Managed/IllusionInjector.dll": "b7f05f3c7abc052fab29787e97dfa5533ce8ff7d",
"IPA/Data/Managed/IllusionInjector.pdb": "a4ca3e2aada545eae600a6b8971ca892682b6693",
"IPA/Data/Managed/IllusionPlugin.dll": "c715be1506e16367c6cdb1c8e7b8bba845233f0f",
"IPA/Data/Managed/IllusionPlugin.pdb": "516f73cc420bfcdcd38a991c7664d62d7deb745a",
"IPA/Data/Managed/IllusionPlugin.xml": "e79c7cd5bc5369056d262fd963451e4fb22f8021",
"IPA/Launcher.exe": "76a68378dd0ef1fe660b87b96b18c5e77709e3ff",
"Mono.Cecil.dll": "762fb07e4d81722f0a766460289c6114cd4b7dae",
"Plugins/SongLoaderPlugin.dll": "f4ab080fbcc5abc6005a868cd965920bac46ddaf"
"Plugins/": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"Plugins/RandomSong.dll": "64ee1ecfeda73c550004bdb5123c7ac47a45e428"
},
"url": "file://Z:/Users/aaron/Source/Repos/IPA-Reloaded-BeatSaber/IPA/bin/Debug/Debug.zip"
"url": "https://www.modsaber.ml/cdn/randomsong/1.0.0-default.zip"
}
},
"weight": 7,


+ 1
- 0
IPA/PatchContext.cs View File

@ -20,6 +20,7 @@ namespace IPA
public string DataPathDst { get; private set; }
public string ManagedPath { get; private set; }
public string EngineFile { get; private set; }
public string EngineWebRequestFile { get; private set; }
public string AssemblyFile { get; private set; }
public string[] Args { get; private set; }
public string ProjectRoot { get; private set; }


+ 2
- 2
IllusionInjector/IllusionInjector.csproj View File

@ -25,6 +25,7 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
@ -80,8 +81,7 @@
<Compile Include="Updating\ModsaberML\Updater.cs" />
<Compile Include="Updating\Old\ModUpdater.cs" />
<Compile Include="Updating\Old\UpdateScript.cs" />
<Compile Include="Utilities\BlockingStream.cs" />
<Compile Include="Utilities\EchoStream.cs" />
<Compile Include="Utilities\LoneFunctions.cs" />
<Compile Include="Utilities\SimpleJson.cs" />
<Compile Include="Utilities\SteamCheck.cs" />
</ItemGroup>


+ 23
- 5
IllusionInjector/Updating/ModsaberML/ApiEndpoint.cs View File

@ -1,4 +1,5 @@
using SimpleJSON;
using IllusionInjector.Utilities;
using SimpleJSON;
using System;
using System.Collections.Generic;
using System.Linq;
@ -25,8 +26,16 @@ namespace IllusionInjector.Updating.ModsaberML
public string Title;
public Version GameVersion;
public string Author;
public string SteamFile = null;
public string OculusFile = null;
public class PlatformFile
{
public byte[] Hash = new byte[20]; // 20 byte because sha1 is fucky
public Dictionary<string, byte[]> FileHashes = new Dictionary<string, byte[]>();
public string DownloadPath = null;
}
public PlatformFile SteamFile = null;
public PlatformFile OculusFile = null;
public static Mod DecodeJSON(JSONObject obj)
{
@ -43,10 +52,19 @@ namespace IllusionInjector.Updating.ModsaberML
foreach (var item in obj["files"])
{
var key = item.Key;
var pfile = new PlatformFile()
{
DownloadPath = item.Value["url"],
Hash = LoneFunctions.StringToByteArray(item.Value["hash"])
};
foreach (var file in item.Value["files"])
pfile.FileHashes.Add(file.Key, LoneFunctions.StringToByteArray(file.Value));
if (key == "steam")
outp.SteamFile = item.Value["url"];
outp.SteamFile = pfile;
if (key == "oculus")
outp.OculusFile = item.Value["url"];
outp.OculusFile = pfile;
}
return outp;


+ 108
- 56
IllusionInjector/Updating/ModsaberML/Updater.cs View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
@ -150,29 +151,29 @@ namespace IllusionInjector.Updating.ModsaberML
StartCoroutine(UpdateModCoroutine(tempDirectory, item));
}
}
public class StreamDownloadHandler : DownloadHandlerScript
class StreamDownloadHandler : DownloadHandlerScript
{
public BlockingStream Stream { get; set; }
public StreamDownloadHandler(BlockingStream stream)
public MemoryStream Stream { get; set; }
public StreamDownloadHandler(MemoryStream stream) : base()
{
Stream = stream;
}
protected void ReceiveContentLength(long contentLength)
protected override void ReceiveContentLength(int contentLength)
{
//(Stream.BaseStream as MemoryStream).Capacity = (int)contentLength;
Stream.Capacity = contentLength;
Logger.log.Debug($"Got content length: {contentLength}");
}
protected void OnContentComplete()
protected override void CompleteContent()
{
Stream.Open = false;
Logger.log.Debug("Download complete");
}
protected bool ReceiveData(byte[] data, long dataLength)
protected override bool ReceiveData(byte[] data, int dataLength)
{
Logger.log.Debug("ReceiveData");
if (data == null || data.Length < 1)
@ -181,7 +182,7 @@ namespace IllusionInjector.Updating.ModsaberML
return false;
}
Stream.Write(data, 0, (int)dataLength);
Stream.Write(data, 0, dataLength);
return true;
}
@ -199,19 +200,54 @@ namespace IllusionInjector.Updating.ModsaberML
}
private void DownloadPluginAsync(BlockingStream stream, UpdateStruct item, string tempdir)
private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo)
{
Logger.log.Debug($"Getting ZIP file for {item.plugin.Plugin.Name}");
//var stream = await httpClient.GetStreamAsync(url);
using (var zipFile = new ZipInputStream(stream))
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");
using (var zipFile = ZipFile.Read(stream))
{
Logger.log.Debug("Streams opened");
ZipEntry entry;
while ((entry = zipFile.GetNextEntry()) != null)
foreach (var entry in zipFile)
{
Logger.log.Debug(entry?.FileName ?? "NULL");
if (entry.IsDirectory)
{
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");
ostream.Seek(0, SeekOrigin.Begin);
FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName));
if (targetFile.Exists)
{
Logger.log.Debug($"Target file {targetFile.FullName} exists");
}
var fstream = targetFile.Create();
ostream.CopyTo(fstream);
Logger.log.Debug($"Wrote file {targetFile.FullName}");
}
}
}
}
@ -220,59 +256,75 @@ namespace IllusionInjector.Updating.ModsaberML
IEnumerator UpdateModCoroutine(string tempdir, UpdateStruct item)
{
string url;
ApiEndpoint.Mod.PlatformFile platformFile;
if (SteamCheck.IsAvailable || item.externInfo.OculusFile == null)
url = item.externInfo.SteamFile;
platformFile = item.externInfo.SteamFile;
else
url = item.externInfo.OculusFile;
platformFile = item.externInfo.OculusFile;
string url = platformFile.DownloadPath;
Logger.log.Debug($"URL = {url}");
using (var memStream = new EchoStream())
using (var stream = new BlockingStream(memStream))
using (var request = UnityWebRequest.Get(url))
using (var taskTokenSource = new CancellationTokenSource())
const int MaxTries = 3;
int maxTries = MaxTries;
while (maxTries > 0)
{
var dlh = new StreamDownloadHandler(stream);
request.downloadHandler = dlh;
if (maxTries-- != MaxTries)
Logger.log.Info($"Re-trying download...");
var downloadTask = Task.Run(() =>
{ // use slightly more multithreaded approach than coroutines
DownloadPluginAsync(stream, item, tempdir);
}, taskTokenSource.Token);
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.log.Debug("Sending request");
Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
yield return request.SendWebRequest();
Logger.log.Debug("Download finished");
Logger.log.Debug("Sending request");
//Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL");
yield return request.SendWebRequest();
Logger.log.Debug("Download finished");
if (stream.Open)
{ // anti-hang
Logger.log.Warn("Downloader failed to call DownloadHandler");
stream.Open = false; // no more writing
stream.BaseStream.Write(new byte[] { 0 }, 0, 1);
}
if (request.isNetworkError)
{
Logger.log.Error("Network error while trying to update mod");
Logger.log.Error(request.error);
taskTokenSource.Cancel();
continue;
}
if (request.isHttpError)
{
Logger.log.Error($"Server returned an error code while trying to update mod");
Logger.log.Error(request.error);
taskTokenSource.Cancel();
continue;
}
if (request.isNetworkError)
{
Logger.log.Error("Network error while trying to update mod");
Logger.log.Error(request.error);
taskTokenSource.Cancel();
yield break;
}
if (request.isHttpError)
{
Logger.log.Error($"Server returned an error code while trying to update mod");
Logger.log.Error(request.error);
taskTokenSource.Cancel();
yield break;
}
stream.Seek(0, SeekOrigin.Begin); // reset to beginning
downloadTask.Wait(); // wait for the damn thing to finish
var downloadTask = Task.Run(() =>
{ // use slightly more multithreaded approach than coroutines
ExtractPluginAsync(stream, item, platformFile);
}, taskTokenSource.Token);
while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted))
yield return null; // pause coroutine until task is done
if (downloadTask.IsFaulted)
{
Logger.log.Error($"Error downloading mod {item.plugin.Plugin.Name}");
Logger.log.Error(downloadTask.Exception);
continue;
}
break;
}
}
yield return null;
if (maxTries == 0)
Logger.log.Warn($"Plugin download failed {MaxTries} times, not re-trying");
else
Logger.log.Debug("Download complete");
}
}
}

+ 0
- 88
IllusionInjector/Utilities/BlockingStream.cs View File

@ -1,88 +0,0 @@
using IllusionInjector.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IllusionInjector.Utilities
{
class BlockingStream : Stream
{
public BlockingStream(Stream bstr)
{
BaseStream = bstr;
}
public Stream BaseStream { get; set; }
private bool _open = true;
public bool Open {
get
{
return CanWrite;
}
set
{
if (!_open)
throw new InvalidOperationException("Blocking stream has already been closed!");
else
_open = value;
}
}
private bool canReadOverride = true;
public override bool CanRead => BaseStream.CanRead && canReadOverride;
public override bool CanSeek => BaseStream.CanSeek;
public override bool CanWrite => BaseStream.CanWrite && _open;
public override long Length => BaseStream.Length;
public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; }
public override void Flush()
{
BaseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
var read = 0;
while (read < count && Open)
{
read += BaseStream.Read(buffer, read, count-read);
}
if (read == 0)
{
canReadOverride = false;
}
return read;
}
public override long Seek(long offset, SeekOrigin origin)
{
return BaseStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
BaseStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
BaseStream.Write(buffer, offset, count);
}
public override string ToString()
{
return $"{base.ToString()} ({BaseStream?.ToString()})";
}
}
}

+ 0
- 40
IllusionInjector/Utilities/EchoStream.cs View File

@ -1,40 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace IllusionInjector.Utilities
{
public class EchoStream : MemoryStream
{
private ManualResetEvent m_dataReady = new ManualResetEvent(false);
private byte[] m_buffer;
private int m_offset;
private int m_count;
public override void Write(byte[] buffer, int offset, int count)
{
m_buffer = buffer;
m_offset = offset;
m_count = count;
m_dataReady.Set();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (m_buffer == null)
{
// Block until the stream has some more data.
m_dataReady.Reset();
m_dataReady.WaitOne();
}
Buffer.BlockCopy(m_buffer, m_offset, buffer, offset, (count < m_count) ? count : m_count);
m_buffer = null;
return (count < m_count) ? count : m_count;
}
}
}

+ 42
- 0
IllusionInjector/Utilities/LoneFunctions.cs View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IllusionInjector.Utilities
{
public static class LoneFunctions
{
public static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// From: https://stackoverflow.com/a/8808245/3117125
public static unsafe bool UnsafeCompare(byte[] a1, byte[] a2)
{
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
int l = a1.Length;
for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
if (*((long*)x1) != *((long*)x2)) return false;
if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; }
if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; }
if ((l & 1) != 0) if (*((byte*)x1) != *((byte*)x2)) return false;
return true;
}
}
}
}

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

@ -1 +1 @@
c0ee92dfbaba38e571471f2b468aeb2a80b0f76b
53986b9c1e3d7933198e1551a3ec82914a34cbb2

Loading…
Cancel
Save