Support for Runtime Patches via Harmony

pull/3789/head
Taloth Saldono 5 years ago
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>

@ -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;
}
}
}

@ -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})");
}
}
}

@ -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}");
}
}
}

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

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

@ -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…
Cancel
Save