Browse Source

Added libraries; updater works with dependencies

+ Microsoft.CSharp
+ SemVer
pull/46/head
Anairkoen Schno 6 years ago
parent
commit
a2907890a8
14 changed files with 485 additions and 156 deletions
  1. +1
    -0
      .gitignore
  2. +10
    -0
      IPA.Injector/IPA.Injector.csproj
  3. +3
    -1
      IPA.Injector/Properties/AssemblyInfo.cs
  4. +21
    -0
      IPA.Injector/WtfThisDoesntNeedToExist.cs
  5. +2
    -0
      IPA.Loader/IPA.Loader.csproj
  6. +2
    -2
      IPA.Loader/Loader/PluginManager.cs
  7. +3
    -1
      IPA.Loader/Updating/Backup/BackupUnit.cs
  8. +5
    -1
      IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs
  9. +304
    -150
      IPA.Loader/Updating/ModsaberML/Updater.cs
  10. +2
    -0
      IPA.Loader/Updating/SelfPlugin.cs
  11. +52
    -0
      IPA.Loader/Utilities/BeatSaber.cs
  12. +78
    -0
      IPA.Loader/Utilities/Ref.cs
  13. +2
    -1
      IPA.Loader/Utilities/SteamCheck.cs
  14. BIN
      Libs/Microsoft.CSharp.dll

+ 1
- 0
.gitignore View File

@ -250,3 +250,4 @@ paket-files/
# JetBrains Rider # JetBrains Rider
.idea/ .idea/
*.sln.iml *.sln.iml
/MigrationBackup/d2a2abe6/IPA.Injector

+ 10
- 0
IPA.Injector/IPA.Injector.csproj View File

@ -50,6 +50,7 @@
<Compile Include="ConsoleWindow.cs" /> <Compile Include="ConsoleWindow.cs" />
<Compile Include="Injector.cs" /> <Compile Include="Injector.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WtfThisDoesntNeedToExist.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj"> <ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj">
@ -70,11 +71,20 @@
<Link>Libraries\Mono\I18N.West.dll</Link> <Link>Libraries\Mono\I18N.West.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="..\Libs\Microsoft.CSharp.dll">
<Link>Libraries\Mono\Microsoft.CSharp.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.dll"> <Content Include="..\Libs\System.Runtime.Serialization.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link> <Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="SemanticVersioning">
<Version>1.2.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild"> <Target Name="AfterBuild">
<Exec Command="&quot;$(MSBuildBinPath)\MSBuild.exe&quot; &quot;$(MSBuildProjectDirectory)\PostBuild.msbuild&quot; /property:OPath=$(OutputPath) /property:Configuration=$(Configuration) /property:SolDir=$(SolutionDir)" /> <Exec Command="&quot;$(MSBuildBinPath)\MSBuild.exe&quot; &quot;$(MSBuildProjectDirectory)\PostBuild.msbuild&quot; /property:OPath=$(OutputPath) /property:Configuration=$(Configuration) /property:SolDir=$(SolutionDir)" />


+ 3
- 1
IPA.Injector/Properties/AssemblyInfo.cs View File

@ -1,4 +1,5 @@
using System.Reflection;
using IPA.Injector;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -22,6 +23,7 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM // The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] [assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")]
[assembly: InternalsVisibleTo("IPA.Loader")] [assembly: InternalsVisibleTo("IPA.Loader")]
[assembly: ForceAssemblyReference(typeof(SemVer.Version))]
// Version information for an assembly consists of the following four values: // Version information for an assembly consists of the following four values:
// //


+ 21
- 0
IPA.Injector/WtfThisDoesntNeedToExist.cs View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IPA.Injector
{
[AttributeUsage(AttributeTargets.Assembly)]
internal class ForceAssemblyReferenceAttribute : Attribute
{
public ForceAssemblyReferenceAttribute(Type forcedType)
{
//not sure if these two lines are required since
//the type is passed to constructor as parameter,
//thus effectively being used
Action<Type> noop = _ => { };
noop(forcedType);
}
}
}

+ 2
- 0
IPA.Loader/IPA.Loader.csproj View File

@ -72,6 +72,8 @@
<Compile Include="Updating\Converters\ModsaberDependencyConverter.cs" /> <Compile Include="Updating\Converters\ModsaberDependencyConverter.cs" />
<Compile Include="Updating\Converters\SemverRangeConverter.cs" /> <Compile Include="Updating\Converters\SemverRangeConverter.cs" />
<Compile Include="Updating\Converters\SemverVersionConverter.cs" /> <Compile Include="Updating\Converters\SemverVersionConverter.cs" />
<Compile Include="Utilities\BeatSaber.cs" />
<Compile Include="Utilities\Ref.cs" />
<Compile Include="Utilities\ReflectionUtil.cs" /> <Compile Include="Utilities\ReflectionUtil.cs" />
<Compile Include="Loader\Composite\CompositeIPAPlugin.cs" /> <Compile Include="Loader\Composite\CompositeIPAPlugin.cs" />
<Compile Include="Logging\Printers\ColoredConsolePrinter.cs" /> <Compile Include="Logging\Printers\ColoredConsolePrinter.cs" />


+ 2
- 2
IPA.Loader/Loader/PluginManager.cs View File

@ -156,7 +156,7 @@ namespace IPA.Loader
var selfPlugin = new BSPluginMeta var selfPlugin = new BSPluginMeta
{ {
Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"), Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"),
Plugin = new SelfPlugin()
Plugin = SelfPlugin.Instance
}; };
selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo; selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo;
@ -173,7 +173,7 @@ namespace IPA.Loader
Logger.log.Info(exeName); Logger.log.Info(exeName);
Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}"); Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}");
Logger.log.Info($"Game version {UnityEngine.Application.version}");
Logger.log.Info($"Game version {BeatSaber.GameVersion}");
Logger.log.Info("-----------------------------"); Logger.log.Info("-----------------------------");
Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}");
Logger.log.Info("-----------------------------"); Logger.log.Info("-----------------------------");


+ 3
- 1
IPA.Loader/Updating/Backup/BackupUnit.cs View File

@ -73,9 +73,11 @@ namespace IPA.Updating.Backup
/// <param name="file"></param> /// <param name="file"></param>
public void Add(FileInfo file) public void Add(FileInfo file)
{ {
var relativePath = LoneFunctions.GetRelativePath(Environment.CurrentDirectory, file.FullName);
var relativePath = LoneFunctions.GetRelativePath(file.FullName, Environment.CurrentDirectory);
var backupPath = new FileInfo(Path.Combine(_BackupPath.FullName, relativePath)); var backupPath = new FileInfo(Path.Combine(_BackupPath.FullName, relativePath));
Logger.updater.Debug($"rp={relativePath}, bp={backupPath}");
if (_Files.Contains(relativePath)) if (_Files.Contains(relativePath))
{ {
Logger.updater.Debug($"Skipping backup of {relativePath}"); Logger.updater.Debug($"Skipping backup of {relativePath}");


+ 5
- 1
IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs View File

@ -21,7 +21,7 @@ namespace IPA.Updating.ModsaberML
public const string GetApprovedEndpoint = "updater_test.json"; public const string GetApprovedEndpoint = "updater_test.json";
#else #else
public const string ApiBase = "https://www.modsaber.ml/"; public const string ApiBase = "https://www.modsaber.ml/";
public const string GetApprovedEndpoint = "registry/{0}";
public const string GetApprovedEndpoint = "registry/{0}/{1}";
#endif #endif
class HexArrayConverter : JsonConverter class HexArrayConverter : JsonConverter
@ -117,6 +117,7 @@ namespace IPA.Updating.ModsaberML
{ {
[JsonProperty("steam")] [JsonProperty("steam")]
public PlatformFile Steam = null; public PlatformFile Steam = null;
[JsonProperty("oculus")] [JsonProperty("oculus")]
public PlatformFile Oculus = null; public PlatformFile Oculus = null;
} }
@ -133,6 +134,9 @@ namespace IPA.Updating.ModsaberML
[JsonProperty("dependsOn", ItemConverterType = typeof(ModsaberDependencyConverter))] [JsonProperty("dependsOn", ItemConverterType = typeof(ModsaberDependencyConverter))]
public Dependency[] Dependencies = new Dependency[0]; public Dependency[] Dependencies = new Dependency[0];
[JsonProperty("oldVersions", ItemConverterType = typeof(SemverVersionConverter))]
public Version[] OldVersions = new Version[0];
public override string ToString() public override string ToString()
{ {
return $"{{\"{Title} ({Name})\"v{Version} for {GameVersion} by {Author} with \"{Files.Steam}\" and \"{Files.Oculus}\"}}"; return $"{{\"{Title} ({Name})\"v{Version} for {GameVersion} by {Author} with \"{Files.Steam}\" and \"{Files.Oculus}\"}}";


+ 304
- 150
IPA.Loader/Updating/ModsaberML/Updater.cs View File

@ -15,9 +15,13 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using UnityEngine.Networking; using UnityEngine.Networking;
using SemVer;
using Logger = IPA.Logging.Logger; using Logger = IPA.Logging.Logger;
using Version = SemVer.Version; using Version = SemVer.Version;
using IPA.Updating.Backup; using IPA.Updating.Backup;
using System.Runtime.Serialization;
using System.Reflection;
using static IPA.Loader.PluginManager;
namespace IPA.Updating.ModsaberML namespace IPA.Updating.ModsaberML
{ {
@ -43,124 +47,327 @@ namespace IPA.Updating.ModsaberML
} }
} }
public void CheckForUpdates()
private void CheckForUpdates()
{ {
StartCoroutine(CheckForUpdatesCoroutine()); StartCoroutine(CheckForUpdatesCoroutine());
} }
private class ParsedPluginMeta : PluginManager.BSPluginMeta
private class DependencyObject
{ {
private Version _verCache = null;
public Version ModVersion
{
get
{
if (_verCache == null)
_verCache = new Version(ModsaberInfo.CurrentVersion);
return _verCache;
}
}
public string Name { get; set; }
public Version Version { get; set; } = null;
public Version ResolvedVersion { get; set; } = null;
public Range Requirement { get; set; } = null;
public bool Resolved { get; set; } = false;
public bool Has { get; set; } = false;
public HashSet<string> Consumers { get; set; } = new HashSet<string>();
public ParsedPluginMeta(PluginManager.BSPluginMeta meta)
public BSPluginMeta LocalPluginMeta { get; set; } = null;
public override string ToString()
{ {
this.Plugin = meta.Plugin;
this.ModsaberInfo = meta.ModsaberInfo;
this.Filename = meta.Filename;
return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement}) {(Has ? $" Already have" : "")}";
} }
} }
private struct UpdateStruct
private Dictionary<string, ApiEndpoint.Mod> requestCache = new Dictionary<string, ApiEndpoint.Mod>();
private IEnumerator DownloadModInfo(string name, string ver, Ref<ApiEndpoint.Mod> result)
{ {
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);
var uri = ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, name, ver);
foreach (var _plugin in PluginManager.BSMetas)
if (requestCache.TryGetValue(uri, out ApiEndpoint.Mod value))
{ {
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)))
result.Value = value;
yield break;
}
else
{
using (var request = UnityWebRequest.Get(uri))
{ {
yield return request.SendWebRequest(); yield return request.SendWebRequest();
if (request.isNetworkError) if (request.isNetworkError)
{ {
Logger.updater.Error("Network error while trying to update mods");
Logger.updater.Error(request.error);
continue;
result.Error = new NetworkException($"Network error while trying to download: {request.error}");
yield break;
} }
if (request.isHttpError) if (request.isHttpError)
{ {
if (request.responseCode == 404) if (request.responseCode == 404)
{ {
Logger.updater.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}");
continue;
result.Error = new NetworkException("Not found");
yield break;
} }
Logger.updater.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}");
Logger.updater.Error(request.error);
continue;
result.Error = new NetworkException($"Server returned error {request.error} while getting data");
yield break;
} }
var json = request.downloadHandler.text;
ApiEndpoint.Mod modRegistry;
try try
{ {
modRegistry = JsonConvert.DeserializeObject<ApiEndpoint.Mod>(json);
Logger.updater.Debug(modRegistry.ToString());
result.Value = JsonConvert.DeserializeObject<ApiEndpoint.Mod>(request.downloadHandler.text);
requestCache[uri] = result.Value;
} }
catch (Exception e) catch (Exception e)
{ {
Logger.updater.Error($"Parse error while trying to update mods");
result.Error = new Exception("Error decoding response", e);
yield break;
}
}
}
}
private IEnumerator CheckForUpdatesCoroutine()
{
var depList = new Ref<List<DependencyObject>>(new List<DependencyObject>());
foreach (var plugin in BSMetas)
{ // initialize with data to resolve (1.1)
if (plugin.ModsaberInfo != null)
{ // updatable
var msinfo = plugin.ModsaberInfo;
depList.Value.Add(new DependencyObject {
Name = msinfo.InternalName,
Version = new Version(msinfo.CurrentVersion),
Requirement = new Range($">={msinfo.CurrentVersion}"),
LocalPluginMeta = plugin
});
}
}
foreach (var dep in depList.Value)
Logger.updater.Debug($"Phantom Dependency: {dep.ToString()}");
yield return DependencyResolveFirstPass(depList);
foreach (var dep in depList.Value)
Logger.updater.Debug($"Dependency: {dep.ToString()}");
yield return DependencyResolveSecondPass(depList);
foreach (var dep in depList.Value)
Logger.updater.Debug($"Dependency: {dep.ToString()}");
DependendyResolveFinalPass(depList);
}
private IEnumerator DependencyResolveFirstPass(Ref<List<DependencyObject>> list)
{
for (int i = 0; i < list.Value.Count; i++)
{ // Grab dependencies (1.2)
var dep = list.Value[i];
var mod = new Ref<ApiEndpoint.Mod>(null);
#region TEMPORARY get latest // SHOULD BE GREATEST OF VERSION
yield return DownloadModInfo(dep.Name, "", mod);
#endregion
try { mod.Verify(); }
catch (Exception e)
{
Logger.updater.Error($"Error getting info for {dep.Name}");
Logger.updater.Error(e);
continue;
}
list.Value.AddRange(mod.Value.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = d.VersionRange, Consumers = new HashSet<string>() { dep.Name } }));
}
var depNames = new HashSet<string>();
var final = new List<DependencyObject>();
foreach (var dep in list.Value)
{ // agregate ranges and the like (1.3)
if (!depNames.Contains(dep.Name))
{ // should add it
depNames.Add(dep.Name);
final.Add(dep);
}
else
{
var toMod = final.Where(d => d.Name == dep.Name).First();
toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement);
foreach (var consume in dep.Consumers)
toMod.Consumers.Add(consume);
}
}
list.Value = final;
}
private IEnumerator DependencyResolveSecondPass(Ref<List<DependencyObject>> list)
{
IEnumerator GetGameVersionMap(string modname, Ref<Dictionary<Version,Version>> map)
{ // gets map of mod version -> game version (2.0)
map.Value = new Dictionary<Version, Version>();
var mod = new Ref<ApiEndpoint.Mod>(null);
yield return DownloadModInfo(modname, "", mod);
try { mod.Verify(); }
catch (Exception)
{
map.Value = null;
map.Error = new Exception($"Error getting info for {modname}", mod.Error);
yield break;
}
map.Value.Add(mod.Value.Version, mod.Value.GameVersion);
foreach (var ver in mod.Value.OldVersions)
{
yield return DownloadModInfo(modname, ver.ToString(), mod);
try { mod.Verify(); }
catch (Exception e)
{
Logger.updater.Error($"Error getting info for {modname}v{ver}");
Logger.updater.Error(e); Logger.updater.Error(e);
continue; continue;
} }
map.Value.Add(mod.Value.Version, mod.Value.GameVersion);
}
}
foreach(var dep in list.Value)
{
dep.Has = dep.Version != null;// dep.Version is only not null if its already installed
var dict = new Ref<Dictionary<Version, Version>>(null);
yield return GetGameVersionMap(dep.Name, dict);
try { dict.Verify(); }
catch (Exception e)
{
Logger.updater.Error($"Error getting map for {dep.Name}");
Logger.updater.Error(e);
continue;
}
var ver = dep.Requirement.MaxSatisfying(dict.Value.Where(kvp => kvp.Value == BeatSaber.GameVersion).Select(kvp => kvp.Key)); // (2.1)
if (dep.Resolved = ver != null) dep.ResolvedVersion = ver; // (2.2)
dep.Has = dep.Version == dep.ResolvedVersion && dep.Resolved; // dep.Version is only not null if its already installed
}
}
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)
private void DependendyResolveFinalPass(Ref<List<DependencyObject>> list)
{ // also starts download of mods
var toDl = new List<DependencyObject>();
foreach (var dep in list.Value)
{ // figure out which ones need to be downloaded (3.1)
if (dep.Resolved)
{
Logger.updater.Debug($"Resolved: {dep.ToString()}");
if (!dep.Has)
{ {
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.Debug($"To Download: {dep.ToString()}");
toDl.Add(dep);
} }
} }
else if (!dep.Has)
{
Logger.updater.Warn($"Could not resolve dependency {dep}");
}
} }
Logger.updater.Info($"{toUpdate.Count} mods need updating");
if (toUpdate.Count == 0) yield break;
Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}");
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName()); string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory); Directory.CreateDirectory(tempDirectory);
foreach (var item in toUpdate)
{
Logger.updater.Debug($"Temp directory: {tempDirectory}");
foreach (var item in toDl)
StartCoroutine(UpdateModCoroutine(item, tempDirectory)); StartCoroutine(UpdateModCoroutine(item, tempDirectory));
}
private IEnumerator UpdateModCoroutine(DependencyObject item, string tempDirectory)
{ // (3.2)
Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}");
var mod = new Ref<ApiEndpoint.Mod>(null);
yield return DownloadModInfo(item.Name, item.ResolvedVersion.ToString(), mod);
try { mod.Verify(); }
catch (Exception e)
{
Logger.updater.Error($"Error occurred while trying to get information for {item}");
Logger.updater.Error(e);
yield break;
}
ApiEndpoint.Mod.PlatformFile platformFile;
if (BeatSaber.ReleaseType == BeatSaber.Release.Steam || mod.Value.Files.Oculus == null)
platformFile = mod.Value.Files.Steam;
else
platformFile = mod.Value.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.Debug($"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.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");
} }
class StreamDownloadHandler : DownloadHandlerScript
internal class StreamDownloadHandler : DownloadHandlerScript
{ {
public MemoryStream Stream { get; set; } public MemoryStream Stream { get; set; }
@ -205,9 +412,9 @@ namespace IPA.Updating.ModsaberML
} }
} }
private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory)
{
Logger.updater.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}");
private void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory)
{ // (3.3)
Logger.updater.Debug($"Extracting ZIP file for {item.Name}");
var data = stream.GetBuffer(); var data = stream.GetBuffer();
SHA1 sha = new SHA1CryptoServiceProvider(); SHA1 sha = new SHA1CryptoServiceProvider();
@ -216,7 +423,7 @@ namespace IPA.Updating.ModsaberML
throw new Exception("The hash for the file doesn't match what is defined"); throw new Exception("The hash for the file doesn't match what is defined");
var newFiles = new List<FileInfo>(); var newFiles = new List<FileInfo>();
var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}");
var backup = new BackupUnit(tempDirectory, $"backup-{item.Name}");
try try
{ {
@ -248,7 +455,7 @@ namespace IPA.Updating.ModsaberML
FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName)); FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName));
Directory.CreateDirectory(targetFile.DirectoryName); Directory.CreateDirectory(targetFile.DirectoryName);
if (targetFile.FullName == item.plugin.Filename)
if (targetFile.FullName == item.LocalPluginMeta?.Filename)
shouldDeleteOldFile = false; // overwriting old file, no need to delete shouldDeleteOldFile = false; // overwriting old file, no need to delete
if (targetFile.Exists) if (targetFile.Exists)
@ -258,6 +465,7 @@ namespace IPA.Updating.ModsaberML
Logger.updater.Debug($"Extracting file {targetFile.FullName}"); Logger.updater.Debug($"Extracting file {targetFile.FullName}");
targetFile.Delete();
var fstream = targetFile.Create(); var fstream = targetFile.Create();
ostream.CopyTo(fstream); ostream.CopyTo(fstream);
} }
@ -265,17 +473,17 @@ namespace IPA.Updating.ModsaberML
} }
} }
if (item.plugin.Plugin is SelfPlugin)
if (item.LocalPluginMeta?.Plugin is SelfPlugin)
{ // currently updating self { // currently updating self
Process.Start(new ProcessStartInfo Process.Start(new ProcessStartInfo
{ {
FileName = item.plugin.Filename,
Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait",
FileName = item.LocalPluginMeta.Filename,
Arguments = $"-nw={Process.GetCurrentProcess().Id}",
UseShellExecute = false UseShellExecute = false
}); });
} }
else if (shouldDeleteOldFile)
File.Delete(item.plugin.Filename);
else if (shouldDeleteOldFile && item.LocalPluginMeta != null)
File.Delete(item.LocalPluginMeta.Filename);
} }
catch (Exception) catch (Exception)
{ // something failed; restore { // something failed; restore
@ -289,82 +497,28 @@ namespace IPA.Updating.ModsaberML
backup.Delete(); backup.Delete();
Logger.updater.Debug("Downloader exited");
Logger.updater.Debug("Extractor exited");
} }
}
IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory)
[Serializable]
internal class NetworkException : Exception
{
public NetworkException()
{ {
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;
}
public NetworkException(string message) : base(message)
{
}
break;
}
}
public NetworkException(string message, Exception innerException) : base(message, innerException)
{
}
if (maxTries == 0)
Logger.updater.Warn($"Plugin download failed {MaxTries} times, not re-trying");
else
Logger.updater.Debug("Download complete");
protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context)
{
} }
} }
} }

+ 2
- 0
IPA.Loader/Updating/SelfPlugin.cs View File

@ -13,6 +13,8 @@ namespace IPA.Updating
internal const string IPA_Name = "Beat Saber IPA"; internal const string IPA_Name = "Beat Saber IPA";
internal const string IPA_Version = "3.10.0"; internal const string IPA_Version = "3.10.0";
public static SelfPlugin Instance { get; set; } = new SelfPlugin();
public string Name => IPA_Name; public string Name => IPA_Name;
public string Version => IPA_Version; public string Version => IPA_Version;


+ 52
- 0
IPA.Loader/Utilities/BeatSaber.cs View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SemVer;
using Version = SemVer.Version;
namespace IPA.Utilities
{
/// <summary>
/// Provides some basic utility methods and properties of Beat Saber
/// </summary>
public static class BeatSaber
{
private static Version _gameVersion = null;
/// <summary>
/// Provides the current game version
/// </summary>
public static Version GameVersion => _gameVersion ?? (_gameVersion = new Version(UnityEngine.Application.version));
/// <summary>
/// The different types of releases of the game.
/// </summary>
public enum Release
{
/// <summary>
/// Indicates a Steam release.
/// </summary>
Steam,
/// <summary>
/// Indicates an Oculus release.
/// </summary>
Oculus
}
private static Release? _releaseCache = null;
/// <summary>
/// Gets the <see cref="Release"/> type of this installation of Beat Saber
/// </summary>
public static Release ReleaseType => (_releaseCache ?? (_releaseCache = FindSteamVRAsset() ? Release.Steam : Release.Oculus)).Value;
private static bool FindSteamVRAsset()
{
// these require assembly qualified names....
var SteamVRCamera = Type.GetType("SteamVR_Camera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false);
var SteamVRExternalCamera = Type.GetType("SteamVR_ExternalCamera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false);
var SteamVRFade = Type.GetType("SteamVR_Fade, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false);
return SteamVRCamera != null && SteamVRExternalCamera != null && SteamVRFade != null;
}
}
}

+ 78
- 0
IPA.Loader/Utilities/Ref.cs View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace IPA.Utilities
{
/// <summary>
/// A class to store a reference for passing to methods which cannot take ref parameters.
/// </summary>
/// <typeparam name="T">the type of the value</typeparam>
public class Ref<T>
{
private T _value;
/// <summary>
/// The value of the reference
/// </summary>
public T Value
{
get
{
if (Error != null) throw Error;
return _value;
}
set => _value = value;
}
private Exception _error = null;
/// <summary>
/// An exception that was generated while creating the value.
/// </summary>
public Exception Error
{
get
{
return _error;
}
set
{
value.SetStackTrace(new StackTrace(1));
_error = value;
}
}
/// <summary>
/// Constructor.
/// </summary>
/// <param name="reference">the initial value of the reference</param>
public Ref(T reference)
{
_value = reference;
}
/// <summary>
/// Throws error if one was set.
/// </summary>
public void Verify()
{
if (Error != null) throw new Exception("Found error", Error);
}
}
internal static class ExceptionUtilities
{
private static readonly FieldInfo STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly Type TRACE_FORMAT_TI = Type.GetType("System.Diagnostics.StackTrace").GetNestedType("TraceFormat", BindingFlags.NonPublic);
private static readonly MethodInfo TRACE_TO_STRING_MI = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TRACE_FORMAT_TI }, null);
public static Exception SetStackTrace(this Exception target, StackTrace stack)
{
var getStackTraceString = TRACE_TO_STRING_MI.Invoke(stack, new object[] { Enum.GetValues(TRACE_FORMAT_TI).GetValue(0) });
STACK_TRACE_STRING_FI.SetValue(target, getStackTraceString);
return target;
}
}
}

+ 2
- 1
IPA.Loader/Utilities/SteamCheck.cs View File

@ -9,7 +9,8 @@ namespace IPA.Utilities
/// <summary> /// <summary>
/// Provides a utility to test if this is a Steam build of Beat Saber. /// Provides a utility to test if this is a Steam build of Beat Saber.
/// </summary> /// </summary>
public static class SteamCheck
[Obsolete("Use BeatSaber.ReleaseType == BeatSaber.Release.Steam")]
internal static class SteamCheck
{ {
private static Type SteamVRCamera; private static Type SteamVRCamera;
private static Type SteamVRExternalCamera; private static Type SteamVRExternalCamera;


BIN
Libs/Microsoft.CSharp.dll View File


Loading…
Cancel
Save