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