diff --git a/distribution/debian/rules b/distribution/debian/rules index babfa7e50..e775b54f7 100644 --- a/distribution/debian/rules +++ b/distribution/debian/rules @@ -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 diff --git a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs index fdde6f24b..3c7f8e199 100644 --- a/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs +++ b/src/NzbDrone.Common/Instrumentation/Sentry/SentryTarget.cs @@ -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; diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index d50bbe6c9..e8e3ddaa5 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -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); diff --git a/src/NzbDrone.Console/Sonarr.Console.csproj b/src/NzbDrone.Console/Sonarr.Console.csproj index 3ab1fbef4..f0c615b21 100644 --- a/src/NzbDrone.Console/Sonarr.Console.csproj +++ b/src/NzbDrone.Console/Sonarr.Console.csproj @@ -9,5 +9,6 @@ + \ No newline at end of file diff --git a/src/Sonarr.RuntimePatches/Mono/DeflateStreamAsyncPatch.cs b/src/Sonarr.RuntimePatches/Mono/DeflateStreamAsyncPatch.cs new file mode 100644 index 000000000..0e2ed5fd8 --- /dev/null +++ b/src/Sonarr.RuntimePatches/Mono/DeflateStreamAsyncPatch.cs @@ -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", "CancellationToken"); + TryPatchMethod(typeof(DeflateStream), "WriteAsyncMemory", "ReadOnlyMemory", "CancellationToken"); + } + + // We need a Transpiler coz these methods are for net4.7.2 so we cannot access the types directly + + // internal ValueTask ReadAsyncMemory(Memory destination, CancellationToken cancellationToken) + // { + // - throw new NotImplementedException(); + // + return base.ReadAsync(destination, cancellationToken); + // } + static IEnumerable Transpiler_ReadAsyncMemory(IEnumerable 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 source, CancellationToken cancellationToken) + // { + // - throw new NotImplementedException(); + // + return base.WriteAsync(source, cancellationToken); + // } + static IEnumerable Transpiler_WriteAsyncMemory(IEnumerable 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; + } + } +} diff --git a/src/Sonarr.RuntimePatches/MonoRuntimePatchBase.cs b/src/Sonarr.RuntimePatches/MonoRuntimePatchBase.cs new file mode 100644 index 000000000..f8760994c --- /dev/null +++ b/src/Sonarr.RuntimePatches/MonoRuntimePatchBase.cs @@ -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|^)(?\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})"); + } + } +} diff --git a/src/Sonarr.RuntimePatches/RuntimePatchBase.cs b/src/Sonarr.RuntimePatches/RuntimePatchBase.cs new file mode 100644 index 000000000..3f8cc13e3 --- /dev/null +++ b/src/Sonarr.RuntimePatches/RuntimePatchBase.cs @@ -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}"); + } + } +} diff --git a/src/Sonarr.RuntimePatches/RuntimePatchExtensions.cs b/src/Sonarr.RuntimePatches/RuntimePatchExtensions.cs new file mode 100644 index 000000000..6ee158c19 --- /dev/null +++ b/src/Sonarr.RuntimePatches/RuntimePatchExtensions.cs @@ -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 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(); + } + } +} diff --git a/src/Sonarr.RuntimePatches/RuntimePatcher.cs b/src/Sonarr.RuntimePatches/RuntimePatcher.cs new file mode 100644 index 000000000..06d7cde64 --- /dev/null +++ b/src/Sonarr.RuntimePatches/RuntimePatcher.cs @@ -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() + .Where(patch => patch.ShouldPatch()) + .ToList(); + + if (patches.Any()) + { + var harmony = new Harmony("tv.sonarr"); + + foreach (var patch in patches) + { + patch.Patch(harmony); + } + } + } + } +} diff --git a/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj b/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj new file mode 100644 index 000000000..9f9e401af --- /dev/null +++ b/src/Sonarr.RuntimePatches/Sonarr.RuntimePatches.csproj @@ -0,0 +1,9 @@ + + + net462 + x86 + + + + + diff --git a/src/Sonarr.sln b/src/Sonarr.sln index c81e1eba0..2a54bffe8 100644 --- a/src/Sonarr.sln +++ b/src/Sonarr.sln @@ -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