mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-01-19 10:54:05 +02:00
Support for Runtime Patches via Harmony
This commit is contained in:
parent
9f54ff8169
commit
0c05236bee
@ -3,7 +3,7 @@
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
|
@ -102,12 +102,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||
o.Debug = false;
|
||||
o.DiagnosticsLevel = SentryLevel.Debug;
|
||||
o.Release = BuildInfo.Release;
|
||||
if (PlatformInfo.IsMono)
|
||||
{
|
||||
// Mono 6.0 broke GzipStream.WriteAsync
|
||||
// TODO: Check specific version
|
||||
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
|
||||
}
|
||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||
o.Environment = BuildInfo.Branch;
|
||||
|
@ -6,6 +6,7 @@ using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Host;
|
||||
using NzbDrone.Host.AccessControl;
|
||||
using NzbDrone.RuntimePatches;
|
||||
|
||||
namespace NzbDrone.Console
|
||||
{
|
||||
@ -23,6 +24,8 @@ namespace NzbDrone.Console
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
RuntimePatcher.Initialize();
|
||||
|
||||
try
|
||||
{
|
||||
var startupArgs = new StartupContext(args);
|
||||
|
@ -9,5 +9,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\NzbDrone.Host\Sonarr.Host.csproj" />
|
||||
<ProjectReference Include="..\Sonarr.RuntimePatches\Sonarr.RuntimePatches.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
96
src/Sonarr.RuntimePatches/Mono/DeflateStreamAsyncPatch.cs
Normal file
96
src/Sonarr.RuntimePatches/Mono/DeflateStreamAsyncPatch.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NzbDrone.RuntimePatches.Mono
|
||||
{
|
||||
// Mono 6.0 - 6.x bug 16122
|
||||
// Unimplemented method used in GzipStream initiated via the http stack, the method existed as far back as 5.10
|
||||
public class DeflateStreamAsyncPatch : MonoRuntimePatchBase
|
||||
{
|
||||
private static DeflateStreamAsyncPatch Instance;
|
||||
|
||||
public override Version MonoMinVersion => new Version(6, 0);
|
||||
public override Version MonoMaxVersion => new Version(6, 0, 0, 334);
|
||||
|
||||
protected override void Patch()
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
TryPatchMethod(typeof(DeflateStream), "ReadAsyncMemory", "Memory<Byte>", "CancellationToken");
|
||||
TryPatchMethod(typeof(DeflateStream), "WriteAsyncMemory", "ReadOnlyMemory<Byte>", "CancellationToken");
|
||||
}
|
||||
|
||||
// We need a Transpiler coz these methods are for net4.7.2 so we cannot access the types directly
|
||||
|
||||
// internal ValueTask<int> ReadAsyncMemory(Memory<byte> destination, CancellationToken cancellationToken)
|
||||
// {
|
||||
// - throw new NotImplementedException();
|
||||
// + return base.ReadAsync(destination, cancellationToken);
|
||||
// }
|
||||
static IEnumerable<CodeInstruction> Transpiler_ReadAsyncMemory(IEnumerable<CodeInstruction> instructions, MethodBase method)
|
||||
{
|
||||
var codes = instructions.ToList();
|
||||
|
||||
var patchable = codes.Matches(OpCodes.Newobj, OpCodes.Throw);
|
||||
|
||||
var readAsync = method.DeclaringType.BaseType.GetMethod("ReadAsync", method.GetParameterTypes());
|
||||
|
||||
if (patchable && readAsync != null)
|
||||
{
|
||||
codes.Clear();
|
||||
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_0));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_1));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_2));
|
||||
codes.Add(new CodeInstruction(OpCodes.Call, readAsync));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ret));
|
||||
|
||||
Instance.Debug($"Patch applied to method {method.GetSimplifiedName()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance.Error($"Skipped patching method {method.GetSimplifiedName()}: Method construct different than expected");
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
// internal ValueTask WriteAsyncMemory(ReadOnlyMemory<byte> source, CancellationToken cancellationToken)
|
||||
// {
|
||||
// - throw new NotImplementedException();
|
||||
// + return base.WriteAsync(source, cancellationToken);
|
||||
// }
|
||||
static IEnumerable<CodeInstruction> Transpiler_WriteAsyncMemory(IEnumerable<CodeInstruction> instructions, MethodBase method)
|
||||
{
|
||||
var codes = instructions.ToList();
|
||||
|
||||
var patchable = codes.Matches(OpCodes.Newobj, OpCodes.Throw);
|
||||
|
||||
var writeAsync = method.DeclaringType.BaseType.GetMethod("WriteAsync", method.GetParameterTypes());
|
||||
|
||||
if (patchable && writeAsync != null)
|
||||
{
|
||||
codes.Clear();
|
||||
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_0));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_1));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ldarg_2));
|
||||
codes.Add(new CodeInstruction(OpCodes.Call, writeAsync));
|
||||
codes.Add(new CodeInstruction(OpCodes.Ret));
|
||||
|
||||
Instance.Debug($"Patch applied to method {method.GetSimplifiedName()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance.Error($"Skipped patching method {method.GetSimplifiedName()}: Method construct different than expected");
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
}
|
||||
}
|
57
src/Sonarr.RuntimePatches/MonoRuntimePatchBase.cs
Normal file
57
src/Sonarr.RuntimePatches/MonoRuntimePatchBase.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NzbDrone.RuntimePatches
|
||||
{
|
||||
public abstract class MonoRuntimePatchBase : RuntimePatchBase
|
||||
{
|
||||
private static readonly Regex VersionRegex = new Regex(@"(?<=\W|^)(?<version>\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public static readonly Version MonoVersion;
|
||||
public virtual Version MonoMinVersion => new Version(0, 0);
|
||||
public virtual Version MonoMaxVersion => new Version(100, 0);
|
||||
|
||||
static MonoRuntimePatchBase()
|
||||
{
|
||||
// Copied from MonoPlatformInfo, coz we want to load as little as possible at this stage.
|
||||
try
|
||||
{
|
||||
var type = Type.GetType("Mono.Runtime");
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
var displayNameMethod = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
if (displayNameMethod != null)
|
||||
{
|
||||
var displayName = displayNameMethod.Invoke(null, null).ToString();
|
||||
var versionMatch = VersionRegex.Match(displayName);
|
||||
|
||||
if (versionMatch.Success)
|
||||
{
|
||||
MonoVersion = new Version(versionMatch.Groups["version"].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ShouldPatch()
|
||||
{
|
||||
if (MonoVersion == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return MonoVersion >= MonoMinVersion && MonoVersion < MonoMaxVersion;
|
||||
}
|
||||
|
||||
protected override void Log(string log)
|
||||
{
|
||||
base.Log($"{log} (Mono {MonoVersion})");
|
||||
}
|
||||
}
|
||||
}
|
121
src/Sonarr.RuntimePatches/RuntimePatchBase.cs
Normal file
121
src/Sonarr.RuntimePatches/RuntimePatchBase.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NzbDrone.RuntimePatches
|
||||
{
|
||||
public abstract class RuntimePatchBase
|
||||
{
|
||||
private Harmony _harmony;
|
||||
|
||||
public virtual bool ShouldPatch() => true;
|
||||
protected abstract void Patch();
|
||||
|
||||
public void Patch(Harmony harmony)
|
||||
{
|
||||
_harmony = harmony;
|
||||
|
||||
if (ShouldPatch())
|
||||
{
|
||||
Patch();
|
||||
}
|
||||
}
|
||||
|
||||
protected const BindingFlags DefaultBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
|
||||
protected static MethodInfo FindMethod(Type type, string methodName, params string[] paramTypes)
|
||||
{
|
||||
foreach (var methodInfo in type.GetMethods(DefaultBindingFlags))
|
||||
{
|
||||
if (methodInfo.Name != methodName) continue;
|
||||
|
||||
var parameters = methodInfo.GetParameters();
|
||||
|
||||
if (parameters.Length != paramTypes.Length) continue;
|
||||
|
||||
var parametersMatch = true;
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
if (parameters[i].ParameterType.Name != paramTypes[i] &&
|
||||
parameters[i].ParameterType.FullName != paramTypes[i] &&
|
||||
parameters[i].ParameterType.GetSimplifiedName() != paramTypes[i] &&
|
||||
parameters[i].ParameterType.GetSimplifiedName(true) != paramTypes[i])
|
||||
{
|
||||
parametersMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parametersMatch) continue;
|
||||
|
||||
return methodInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void PatchMethod(MethodInfo methodInfo)
|
||||
{
|
||||
var prefix = GetPatchMethod("Prefix_" + methodInfo.Name);
|
||||
var postfix = GetPatchMethod("Postfix_" + methodInfo.Name);
|
||||
var transpiler = GetPatchMethod("Transpiler_" + methodInfo.Name);
|
||||
|
||||
_harmony.Patch(methodInfo, prefix, postfix, transpiler);
|
||||
}
|
||||
|
||||
protected void TryPatchMethod(string typeName, string methodName, params string[] paramTypes)
|
||||
{
|
||||
var type = Type.GetType(typeName);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
TryPatchMethod(type, "GetSslServer");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug($"Skipped patching method {typeName}.{methodName}: Type not found");
|
||||
}
|
||||
}
|
||||
|
||||
protected void TryPatchMethod(Type type, string methodName, params string[] paramTypes)
|
||||
{
|
||||
var methodInfo = FindMethod(type, methodName, paramTypes);
|
||||
if (methodInfo != null)
|
||||
{
|
||||
PatchMethod(methodInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug($"Skipped patching method {type.GetSimplifiedName()}.{methodName}: Method not found");
|
||||
}
|
||||
}
|
||||
|
||||
private HarmonyMethod GetPatchMethod(string name)
|
||||
{
|
||||
var patch = GetType().GetMethod(name, DefaultBindingFlags);
|
||||
if (patch != null)
|
||||
{
|
||||
return new HarmonyMethod(patch);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void Debug(string log)
|
||||
{
|
||||
#if DEBUG
|
||||
Log(log);
|
||||
#endif
|
||||
}
|
||||
|
||||
protected void Error(string log)
|
||||
{
|
||||
Log(log);
|
||||
}
|
||||
|
||||
protected virtual void Log(string log)
|
||||
{
|
||||
Console.WriteLine($"RuntimePatch {GetType().Name}: {log}");
|
||||
}
|
||||
}
|
||||
}
|
69
src/Sonarr.RuntimePatches/RuntimePatchExtensions.cs
Normal file
69
src/Sonarr.RuntimePatches/RuntimePatchExtensions.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NzbDrone.RuntimePatches
|
||||
{
|
||||
public static class RuntimePatchExtensions
|
||||
{
|
||||
public static bool Matches(this List<CodeInstruction> instructions, params OpCode[] opcodes)
|
||||
{
|
||||
var codes = instructions.Select(v => v.opcode).Where(v => v != OpCodes.Nop).ToList();
|
||||
|
||||
if (codes.Count != opcodes.Length) return false;
|
||||
|
||||
for (var i = 0; i < codes.Count; i++)
|
||||
{
|
||||
if (codes[i] != opcodes[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Type[] GetParameterTypes(this MethodBase method)
|
||||
{
|
||||
return Array.ConvertAll(method.GetParameters(), v => v.ParameterType);
|
||||
}
|
||||
|
||||
public static string GetSimplifiedName(this MethodBase method, bool includeNamespace = false)
|
||||
{
|
||||
return $"{method.DeclaringType.GetSimplifiedName()}.{method.Name}";
|
||||
}
|
||||
|
||||
public static string GetSimplifiedName(this Type t, bool includeNamespace = false)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (includeNamespace && string.IsNullOrEmpty(t.Namespace))
|
||||
{
|
||||
sb.Append(t.Namespace);
|
||||
sb.Append('.');
|
||||
}
|
||||
|
||||
if (t.IsGenericType)
|
||||
{
|
||||
sb.Append(t.Name, 0, t.Name.LastIndexOf('`'));
|
||||
sb.Append('<');
|
||||
var args = t.GetGenericArguments();
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
sb.Append(", ");
|
||||
|
||||
sb.Append(GetSimplifiedName(args[i], includeNamespace));
|
||||
}
|
||||
sb.Append('>');
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(t.Name);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
47
src/Sonarr.RuntimePatches/RuntimePatcher.cs
Normal file
47
src/Sonarr.RuntimePatches/RuntimePatcher.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NzbDrone.RuntimePatches
|
||||
{
|
||||
public static class RuntimePatcher
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
var env = Environment.GetEnvironmentVariable("DISABLE_RUNTIMEPATCHES");
|
||||
if (env != "1")
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyPatches();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Failed to apply runtime patches, attempting to continue normally.\r\n" + ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyPatches()
|
||||
{
|
||||
var patches = Assembly.GetExecutingAssembly()
|
||||
.GetExportedTypes()
|
||||
.Where(type => !type.IsAbstract && typeof(RuntimePatchBase).IsAssignableFrom(type))
|
||||
.Select(Activator.CreateInstance)
|
||||
.Cast<RuntimePatchBase>()
|
||||
.Where(patch => patch.ShouldPatch())
|
||||
.ToList();
|
||||
|
||||
if (patches.Any())
|
||||
{
|
||||
var harmony = new Harmony("tv.sonarr");
|
||||
|
||||
foreach (var patch in patches)
|
||||
{
|
||||
patch.Patch(harmony);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj
Normal file
9
src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj
Normal file
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net462</TargetFramework>
|
||||
<Platforms>x86</Platforms>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lib.Harmony" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27130.2010
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29806.167
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonarr.Console", "NzbDrone.Console\Sonarr.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
@ -97,6 +97,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonarr.Http", "Sonarr.Http\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonarr.Host.Test", "NzbDrone.Host.Test\Sonarr.Host.Test.csproj", "{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonarr.RuntimePatches", "Sonarr.RuntimePatches\Sonarr.RuntimePatches.csproj", "{F3F63718-63C6-432F-BDDC-C960AD95EC82}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x86 = Debug|x86
|
||||
@ -284,6 +286,12 @@ Global
|
||||
{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.Build.0 = Release|x86
|
||||
{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86
|
||||
{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Debug|x86.Build.0 = Debug|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Mono|x86.ActiveCfg = Release|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Mono|x86.Build.0 = Release|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Release|x86.ActiveCfg = Release|x86
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82}.Release|x86.Build.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -315,6 +323,7 @@ Global
|
||||
{90D6E9FC-7B88-4E1B-B018-8FA742274558} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}
|
||||
{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}
|
||||
{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5} = {57A04B72-8088-4F75-A582-1158CF8291F7}
|
||||
{F3F63718-63C6-432F-BDDC-C960AD95EC82} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35
|
||||
|
Loading…
x
Reference in New Issue
Block a user