From e100759e71e438b033698ea3770ad03ceb4e97c0 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 21 Apr 2018 00:59:31 -0400 Subject: [PATCH] New: Platform Updates, Socket Closure Workaround --- .../Extensions/Pipelines/GZipPipeline.cs | 4 +- .../Frontend/CacheableSpecification.cs | 4 +- .../Frontend/Mappers/IndexHtmlMapper.cs | 4 +- .../Frontend/Mappers/LoginHtmlMapper.cs | 2 +- .../Mappers/StaticResourceMapperBase.cs | 2 +- src/NzbDrone.Api/NancyBootstrapper.cs | 6 +- src/NzbDrone.Api/System/SystemModule.cs | 55 +++--- .../EnvironmentProviderTest.cs | 2 +- .../Http/HttpClientFixture.cs | 50 +++-- .../Http/UserAgentBuilderFixture.cs | 30 +++ .../NzbDrone.Common.Test.csproj | 1 + .../Composition/ContainerBuilderBase.cs | 9 +- src/NzbDrone.Common/Disk/DiskProviderBase.cs | 15 +- .../EnsureThat/EnsureStringExtensions.cs | 6 +- .../IOperatingSystemVersionInfo.cs | 9 + .../EnvironmentInfo/IOsVersionAdapter.cs | 9 + .../EnvironmentInfo/IPlatformInfo.cs | 50 +++++ .../EnvironmentInfo/IRuntimeInfo.cs | 9 +- src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs | 148 +++++++------- .../EnvironmentInfo/OsVersionModel.cs | 29 +++ .../{RuntimeInfoBase.cs => RuntimeInfo.cs} | 54 +++--- .../Exceptron/ExceptionExtentions.cs | 31 --- .../Extensions/PathExtensions.cs | 9 +- .../Http/Dispatchers/CurlHttpDispatcher.cs | 183 +++++++++--------- .../Dispatchers/FallbackHttpDispatcher.cs | 6 +- .../Http/Dispatchers/ManagedHttpDispatcher.cs | 180 +++++++++++------ src/NzbDrone.Common/Http/HttpClient.cs | 59 ++++-- src/NzbDrone.Common/Http/HttpException.cs | 12 +- src/NzbDrone.Common/Http/HttpRequest.cs | 2 +- .../Http/TlsFailureException.cs | 18 ++ .../Http/UnexpectedHtmlContentException.cs | 14 ++ src/NzbDrone.Common/Http/UserAgentBuilder.cs | 38 +++- .../Instrumentation/ExceptronTarget.cs | 89 --------- .../GlobalExceptionHandlers.cs | 6 +- .../Instrumentation/NzbDroneLogger.cs | 14 +- src/NzbDrone.Common/NzbDrone.Common.csproj | 10 +- .../Processes/ProcessProvider.cs | 4 +- src/NzbDrone.Core.Test/Framework/CoreTest.cs | 14 +- .../Checks/MonoVersionCheckFixture.cs | 16 +- .../UpdatePackageProviderFixture.cs | 7 + .../Analytics/AnalyticsService.cs | 2 +- .../Configuration/ConfigService.cs | 3 +- .../HealthCheck/Checks/MonoVersionCheck.cs | 55 +++--- .../Lifecycle/ApplicationShutdownRequested.cs | 12 +- .../MediaFiles/UpdateMovieFileService.cs | 1 - src/NzbDrone.Core/Rest/RestClientFactory.cs | 10 +- .../Update/UpdateCheckService.cs | 10 +- .../Update/UpdatePackageProvider.cs | 10 +- .../AccessControl/UrlAclAdapter.cs | 28 ++- src/NzbDrone.Host/ApplicationServer.cs | 4 +- src/NzbDrone.Host/MainAppContainerBuilder.cs | 19 +- src/NzbDrone.Host/Router.cs | 9 +- src/NzbDrone.Host/SpinService.cs | 4 +- .../MonoPlatformInfoFixture.cs | 23 +++ .../ReleaseFileVersionAdapterFixture.cs | 29 +++ .../MacOsVersionAdapterFixture.cs | 77 ++++++++ .../ReleaseFileVersionAdapterFixture.cs | 82 ++++++++ .../Files/linux/lsb-release | 4 + src/NzbDrone.Mono.Test/Files/linux/os-release | 9 + .../Files/macOS/SystemVersion.plist | 16 ++ src/NzbDrone.Mono.Test/Files/synology/VERSION | 8 + .../NzbDrone.Mono.Test.csproj | 16 ++ .../EnvironmentInfo/MonoPlatformInfo.cs | 46 +++++ .../EnvironmentInfo/MonoRuntimeProvider.cs | 45 ----- .../IssueFileVersionAdapter.cs | 52 +++++ .../VersionAdapters/MacOsVersionAdapter.cs | 69 +++++++ .../ReleaseFileVersionAdapter.cs | 79 ++++++++ .../VersionAdapters/SynologyVersionAdapter.cs | 78 ++++++++ src/NzbDrone.Mono/NzbDrone.Mono.csproj | 6 +- src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs | 6 +- src/NzbDrone.Test.Common/TestBase.cs | 4 +- src/NzbDrone.Update/UpdateContainerBuilder.cs | 20 +- .../UpdateEngine/DetectApplicationType.cs | 4 +- .../DotNetPlatformInfoFixture.cs | 19 ++ .../WindowsVersionInfoFixture.cs | 23 +++ .../NzbDrone.Windows.Test.csproj | 2 + .../EnvironmentInfo/DotNetPlatformInfo.cs | 86 ++++++++ .../EnvironmentInfo/DotNetRuntimeProvider.cs | 16 -- .../EnvironmentInfo/WindowsVersionInfo.cs | 49 +++++ src/NzbDrone.Windows/NzbDrone.Windows.csproj | 3 +- 80 files changed, 1555 insertions(+), 693 deletions(-) create mode 100644 src/NzbDrone.Common.Test/Http/UserAgentBuilderFixture.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/IOperatingSystemVersionInfo.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/IOsVersionAdapter.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/IPlatformInfo.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/OsVersionModel.cs rename src/NzbDrone.Common/EnvironmentInfo/{RuntimeInfoBase.cs => RuntimeInfo.cs} (70%) delete mode 100644 src/NzbDrone.Common/Exceptron/ExceptionExtentions.cs create mode 100644 src/NzbDrone.Common/Http/TlsFailureException.cs create mode 100644 src/NzbDrone.Common/Http/UnexpectedHtmlContentException.cs delete mode 100644 src/NzbDrone.Common/Instrumentation/ExceptronTarget.cs create mode 100644 src/NzbDrone.Mono.Test/EnvironmentInfo/MonoPlatformInfoFixture.cs create mode 100644 src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs create mode 100644 src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs create mode 100644 src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs create mode 100644 src/NzbDrone.Mono.Test/Files/linux/lsb-release create mode 100644 src/NzbDrone.Mono.Test/Files/linux/os-release create mode 100644 src/NzbDrone.Mono.Test/Files/macOS/SystemVersion.plist create mode 100644 src/NzbDrone.Mono.Test/Files/synology/VERSION create mode 100644 src/NzbDrone.Mono/EnvironmentInfo/MonoPlatformInfo.cs delete mode 100644 src/NzbDrone.Mono/EnvironmentInfo/MonoRuntimeProvider.cs create mode 100644 src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/IssueFileVersionAdapter.cs create mode 100644 src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/MacOsVersionAdapter.cs create mode 100644 src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapter.cs create mode 100644 src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/SynologyVersionAdapter.cs create mode 100644 src/NzbDrone.Windows.Test/EnvironmentInfo/DotNetPlatformInfoFixture.cs create mode 100644 src/NzbDrone.Windows.Test/EnvironmentInfo/WindowsVersionInfoFixture.cs create mode 100644 src/NzbDrone.Windows/EnvironmentInfo/DotNetPlatformInfo.cs delete mode 100644 src/NzbDrone.Windows/EnvironmentInfo/DotNetRuntimeProvider.cs create mode 100644 src/NzbDrone.Windows/EnvironmentInfo/WindowsVersionInfo.cs diff --git a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs index 26278c0543..f419627213 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs +++ b/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Compression; using System.Linq; @@ -23,7 +23,7 @@ public GzipCompressionPipeline(Logger logger) _logger = logger; // On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case. - _writeGZipStream = OsInfo.IsMonoRuntime ? WriteGZipStreamMono : (Action, Stream>)WriteGZipStream; + _writeGZipStream = PlatformInfo.IsMono ? WriteGZipStreamMono : (Action, Stream>)WriteGZipStream; } public void Register(IPipelines pipelines) diff --git a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs b/src/NzbDrone.Api/Frontend/CacheableSpecification.cs index b427658e90..3affedbf0b 100644 --- a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs +++ b/src/NzbDrone.Api/Frontend/CacheableSpecification.cs @@ -14,7 +14,7 @@ public class CacheableSpecification : ICacheableSpecification { public bool IsCacheable(NancyContext context) { - if (!RuntimeInfoBase.IsProduction) + if (!RuntimeInfo.IsProduction) { return false; } @@ -46,4 +46,4 @@ public bool IsCacheable(NancyContext context) return true; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs index c6b77cc9fd..46b5d9c379 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs @@ -79,7 +79,7 @@ protected override Stream GetContentStream(string filePath) private string GetIndexText() { - if (RuntimeInfoBase.IsProduction && _generatedContent != null) + if (RuntimeInfo.IsProduction && _generatedContent != null) { return _generatedContent; } @@ -111,7 +111,7 @@ private string GetIndexText() text = text.Replace("APP_BRANCH", _configFileProvider.Branch.ToLower()); text = text.Replace("APP_ANALYTICS", _analyticsService.IsEnabled.ToString().ToLowerInvariant()); text = text.Replace("URL_BASE", URL_BASE); - text = text.Replace("PRODUCTION", RuntimeInfoBase.IsProduction.ToString().ToLowerInvariant()); + text = text.Replace("PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); _generatedContent = text; diff --git a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs index 523e7cbe21..974e117f9e 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs @@ -67,7 +67,7 @@ protected override Stream GetContentStream(string filePath) private string GetLoginText() { - if (RuntimeInfoBase.IsProduction && _generatedContent != null) + if (RuntimeInfo.IsProduction && _generatedContent != null) { return _generatedContent; } diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs index c3c069ea3e..489d039d0c 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs @@ -21,7 +21,7 @@ protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger) _diskProvider = diskProvider; _logger = logger; - if (!RuntimeInfoBase.IsProduction) + if (!RuntimeInfo.IsProduction) { _caseSensitive = StringComparison.OrdinalIgnoreCase; } diff --git a/src/NzbDrone.Api/NancyBootstrapper.cs b/src/NzbDrone.Api/NancyBootstrapper.cs index 78ac15d6ea..4ac5fa6c16 100644 --- a/src/NzbDrone.Api/NancyBootstrapper.cs +++ b/src/NzbDrone.Api/NancyBootstrapper.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Nancy.Bootstrapper; using Nancy.Diagnostics; using NLog; @@ -24,9 +24,9 @@ public NancyBootstrapper(TinyIoCContainer tinyIoCContainer) protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) { - Logger.Info("Starting NzbDrone API"); + Logger.Info("Starting Web Server"); - if (RuntimeInfoBase.IsProduction) + if (RuntimeInfo.IsProduction) { DiagnosticsHook.Disable(pipelines); } diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/NzbDrone.Api/System/SystemModule.cs index 2ec7871156..fc1f272385 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/NzbDrone.Api/System/SystemModule.cs @@ -1,4 +1,4 @@ -using Nancy; +using Nancy; using Nancy.Routing; using NzbDrone.Api.Extensions; using NzbDrone.Common.EnvironmentInfo; @@ -13,6 +13,8 @@ public class SystemModule : NzbDroneApiModule { private readonly IAppFolderInfo _appFolderInfo; private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; + private readonly IOsInfo _osInfo; private readonly IRouteCacheProvider _routeCacheProvider; private readonly IConfigFileProvider _configFileProvider; private readonly IMainDatabase _database; @@ -20,14 +22,17 @@ public class SystemModule : NzbDroneApiModule public SystemModule(IAppFolderInfo appFolderInfo, IRuntimeInfo runtimeInfo, + IPlatformInfo platformInfo, + IOsInfo osInfo, IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider, IMainDatabase database, - ILifecycleService lifecycleService) - : base("system") + ILifecycleService lifecycleService) : base("system") { _appFolderInfo = appFolderInfo; _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; + _osInfo = osInfo; _routeCacheProvider = routeCacheProvider; _configFileProvider = configFileProvider; _database = database; @@ -41,27 +46,29 @@ public SystemModule(IAppFolderInfo appFolderInfo, private Response GetStatus() { return new - { - Version = BuildInfo.Version.ToString(), - BuildTime = BuildInfo.BuildDateTime, - IsDebug = BuildInfo.IsDebug, - IsProduction = RuntimeInfoBase.IsProduction, - IsAdmin = _runtimeInfo.IsAdmin, - IsUserInteractive = RuntimeInfoBase.IsUserInteractive, - StartupPath = _appFolderInfo.StartUpFolder, - AppData = _appFolderInfo.GetAppDataPath(), - OsVersion = OsInfo.Version.ToString(), - IsMonoRuntime = OsInfo.IsMonoRuntime, - IsMono = OsInfo.IsNotWindows, - IsLinux = OsInfo.IsLinux, - IsOsx = OsInfo.IsOsx, - IsWindows = OsInfo.IsWindows, - Branch = _configFileProvider.Branch, - Authentication = _configFileProvider.AuthenticationMethod, - SqliteVersion = _database.Version, - UrlBase = _configFileProvider.UrlBase, - RuntimeVersion = _runtimeInfo.RuntimeVersion - }.AsResponse(); + { + Version = BuildInfo.Version.ToString(), + BuildTime = BuildInfo.BuildDateTime, + IsDebug = BuildInfo.IsDebug, + IsProduction = RuntimeInfo.IsProduction, + IsAdmin = _runtimeInfo.IsAdmin, + IsUserInteractive = RuntimeInfo.IsUserInteractive, + StartupPath = _appFolderInfo.StartUpFolder, + AppData = _appFolderInfo.GetAppDataPath(), + OsName = _osInfo.Name, + OsVersion = _osInfo.Version, + IsMonoRuntime = PlatformInfo.IsMono, + IsMono = PlatformInfo.IsMono, + IsLinux = OsInfo.IsLinux, + IsOsx = OsInfo.IsOsx, + IsWindows = OsInfo.IsWindows, + Branch = _configFileProvider.Branch, + Authentication = _configFileProvider.AuthenticationMethod, + SqliteVersion = _database.Version, + UrlBase = _configFileProvider.UrlBase, + RuntimeVersion = _platformInfo.Version, + RuntimeName = PlatformInfo.Platform + }.AsResponse(); } private Response GetRoutes() diff --git a/src/NzbDrone.Common.Test/EnvironmentProviderTest.cs b/src/NzbDrone.Common.Test/EnvironmentProviderTest.cs index 362a093762..dd7e8fcf06 100644 --- a/src/NzbDrone.Common.Test/EnvironmentProviderTest.cs +++ b/src/NzbDrone.Common.Test/EnvironmentProviderTest.cs @@ -29,7 +29,7 @@ public void ApplicationPath_should_not_be_empty() [Test] public void IsProduction_should_return_false_when_run_within_nunit() { - RuntimeInfoBase.IsProduction.Should().BeFalse("Process name is " + Process.GetCurrentProcess().ProcessName + " Folder is " + Directory.GetCurrentDirectory()); + RuntimeInfo.IsProduction.Should().BeFalse("Process name is " + Process.GetCurrentProcess().ProcessName + " Folder is " + Directory.GetCurrentDirectory()); } [Test] diff --git a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs index d64a52942f..319736e65a 100644 --- a/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs +++ b/src/NzbDrone.Common.Test/Http/HttpClientFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -9,6 +9,7 @@ using NLog; using NUnit.Framework; using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.Http.Proxy; @@ -30,6 +31,12 @@ public class HttpClientFixture : TestBase where TDispat [SetUp] public void SetUp() { + Mocker.GetMock().Setup(c => c.Version).Returns(new Version("1.0.0")); + Mocker.GetMock().Setup(c => c.Name).Returns("TestOS"); + Mocker.GetMock().Setup(c => c.Version).Returns("9.0.0"); + + Mocker.SetConstant(Mocker.Resolve()); + Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -48,7 +55,7 @@ public void SetUp() [Test] public void should_execute_simple_get() { - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); var response = Subject.Execute(request); @@ -58,7 +65,7 @@ public void should_execute_simple_get() [Test] public void should_execute_https_get() { - var request = new HttpRequest(string.Format("https://{0}/get", _httpBinHost)); + var request = new HttpRequest($"https://{_httpBinHost}/get"); var response = Subject.Execute(request); @@ -68,7 +75,7 @@ public void should_execute_https_get() [Test] public void should_execute_typed_get() { - var request = new HttpRequest(string.Format("http://{0}/get?test=1", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get?test=1"); var response = Subject.Get(request); @@ -81,7 +88,7 @@ public void should_execute_simple_post() { var message = "{ my: 1 }"; - var request = new HttpRequest(string.Format("http://{0}/post", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/post"); request.SetContent(message); var response = Subject.Post(request); @@ -92,7 +99,7 @@ public void should_execute_simple_post() [TestCase("gzip")] public void should_execute_get_using_gzip(string compression) { - var request = new HttpRequest(string.Format("http://{0}/{1}", _httpBinHost, compression)); + var request = new HttpRequest($"http://{_httpBinHost}/{compression}"); var response = Subject.Get(request); @@ -108,7 +115,7 @@ public void should_execute_get_using_gzip(string compression) [TestCase(HttpStatusCode.BadGateway)] public void should_throw_on_unsuccessful_status_codes(int statusCode) { - var request = new HttpRequest(string.Format("http://{0}/status/{1}", _httpBinHost, statusCode)); + var request = new HttpRequest($"http://{_httpBinHost}/status/{statusCode}"); var exception = Assert.Throws(() => Subject.Get(request)); @@ -120,7 +127,7 @@ public void should_throw_on_unsuccessful_status_codes(int statusCode) [Test] public void should_not_follow_redirects_when_not_in_production() { - var request = new HttpRequest(string.Format("http://{0}/redirect/1", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/redirect/1"); Subject.Get(request); @@ -130,7 +137,7 @@ public void should_not_follow_redirects_when_not_in_production() [Test] public void should_follow_redirects() { - var request = new HttpRequest(string.Format("http://{0}/redirect/1", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/redirect/1"); request.AllowAutoRedirect = true; var response = Subject.Get(request); @@ -183,7 +190,7 @@ public void should_throw_on_too_many_redirects() [Test] public void should_send_user_agent() { - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); var response = Subject.Get(request); @@ -197,7 +204,7 @@ public void should_send_user_agent() [TestCase("Accept", "text/xml, text/rss+xml, application/rss+xml")] public void should_send_headers(string header, string value) { - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); request.Headers.Add(header, value); var response = Subject.Get(request); @@ -220,7 +227,7 @@ public void should_not_download_file_with_error() [Test] public void should_send_cookie() { - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); request.Cookies["my"] = "cookie"; var response = Subject.Get(request); @@ -237,7 +244,7 @@ public void GivenOldCookie() var oldRequest = new HttpRequest("http://eu.httpbin.org/get"); oldRequest.Cookies["my"] = "cookie"; - var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve()); + var oldClient = new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.GetMock().Object, Mocker.Resolve()); oldClient.Should().NotBeSameAs(Subject); @@ -330,7 +337,7 @@ public void should_delete_request_cookie() [Test] public void should_not_store_response_cookie() { - var requestSet = new HttpRequest(string.Format("http://{0}/cookies/set?my=cookie", _httpBinHost)); + var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie"); requestSet.AllowAutoRedirect = false; requestSet.StoreRequestCookie = false; requestSet.StoreResponseCookie.Should().BeFalse(); @@ -349,7 +356,7 @@ public void should_not_store_response_cookie() [Test] public void should_store_response_cookie() { - var requestSet = new HttpRequest(string.Format("http://{0}/cookies/set?my=cookie", _httpBinHost)); + var requestSet = new HttpRequest($"http://{_httpBinHost}/cookies/set?my=cookie"); requestSet.AllowAutoRedirect = false; requestSet.StoreRequestCookie = false; requestSet.StoreResponseCookie = true; @@ -528,7 +535,7 @@ public void should_not_send_old_cookie() [Test] public void should_throw_on_http429_too_many_requests() { - var request = new HttpRequest(string.Format("http://{0}/status/429", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/status/429"); Assert.Throws(() => Subject.Get(request)); @@ -548,7 +555,7 @@ public void should_call_interceptor() .Setup(v => v.PostResponse(It.IsAny())) .Returns(r => r); - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); Subject.Get(request); @@ -570,7 +577,7 @@ public void should_parse_malformed_cloudflare_cookie(string culture) { // the date is bad in the below - should be 13-Jul-2026 string malformedCookie = @"__cfduid=d29e686a9d65800021c66faca0a29b4261436890790; expires=Mon, 13-Jul-26 16:19:50 GMT; path=/; HttpOnly"; - var requestSet = new HttpRequestBuilder(string.Format("http://{0}/response-headers", _httpBinHost)) + var requestSet = new HttpRequestBuilder($"http://{_httpBinHost}/response-headers") .AddQueryParam("Set-Cookie", malformedCookie) .Build(); @@ -579,7 +586,7 @@ public void should_parse_malformed_cloudflare_cookie(string culture) var responseSet = Subject.Get(requestSet); - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); var response = Subject.Get(request); @@ -603,7 +610,8 @@ public void should_reject_malformed_domain_cookie(string malformedCookie) { try { - string url = string.Format("http://{0}/response-headers?Set-Cookie={1}", _httpBinHost, Uri.EscapeUriString(malformedCookie)); + string url = + $"http://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}"; var requestSet = new HttpRequest(url); requestSet.AllowAutoRedirect = false; @@ -611,7 +619,7 @@ public void should_reject_malformed_domain_cookie(string malformedCookie) var responseSet = Subject.Get(requestSet); - var request = new HttpRequest(string.Format("http://{0}/get", _httpBinHost)); + var request = new HttpRequest($"http://{_httpBinHost}/get"); var response = Subject.Get(request); diff --git a/src/NzbDrone.Common.Test/Http/UserAgentBuilderFixture.cs b/src/NzbDrone.Common.Test/Http/UserAgentBuilderFixture.cs new file mode 100644 index 0000000000..1ff5db3901 --- /dev/null +++ b/src/NzbDrone.Common.Test/Http/UserAgentBuilderFixture.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; +using NzbDrone.Test.Common; + +namespace NzbDrone.Common.Test.Http +{ + [TestFixture] + public class UserAgentBuilderFixture : TestBase + { + [Test] + public void should_get_user_agent_if_os_version_is_null() + { + Mocker.GetMock().SetupGet(c => c.Version).Returns((string)null); + Mocker.GetMock().SetupGet(c => c.Name).Returns("TestOS"); + + Subject.GetUserAgent(false).Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public void should_get_use_os_family_if_name_is_null() + { + Mocker.GetMock().SetupGet(c => c.Version).Returns((string)null); + Mocker.GetMock().SetupGet(c => c.Name).Returns((string)null); + + Subject.GetUserAgent(false).Should().NotBeNullOrWhiteSpace(); + } + } +} diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index deb2ae33d4..90cd11e29b 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -91,6 +91,7 @@ + diff --git a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs index 5a473fd740..8e0e679674 100644 --- a/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs +++ b/src/NzbDrone.Common/Composition/ContainerBuilderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -13,12 +13,15 @@ public abstract class ContainerBuilderBase { private readonly List _loadedTypes; - public IContainer Container { get; private set; } + protected IContainer Container { get; } - protected ContainerBuilderBase(IStartupContext args, params string[] assemblies) + protected ContainerBuilderBase(IStartupContext args, List assemblies) { _loadedTypes = new List(); + assemblies.Add(OsInfo.IsWindows ? "NzbDrone.Windows" : "NzbDrone.Mono"); + assemblies.Add("NzbDrone.Common"); + foreach (var assembly in assemblies) { _loadedTypes.AddRange(Assembly.Load(assembly).GetTypes()); diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index aaae3f0328..c039f4f151 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -17,6 +17,19 @@ public abstract class DiskProviderBase : IDiskProvider { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DiskProviderBase)); + public static StringComparison PathStringComparison + { + get + { + if (OsInfo.IsWindows) + { + return StringComparison.OrdinalIgnoreCase; + } + + return StringComparison.Ordinal; + } + } + public abstract long? GetAvailableSpace(string path); public abstract void InheritFolderPermissions(string filename); public abstract void SetPermissions(string path, string mask, string user, string group); @@ -87,7 +100,7 @@ public bool FolderExists(string path) public bool FileExists(string path) { Ensure.That(path, () => path).IsValidPath(); - return FileExists(path, OsInfo.PathStringComparison); + return FileExists(path, PathStringComparison); } public bool FileExists(string path, StringComparison stringComparison) diff --git a/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs b/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs index 6532dd593a..972eba39b3 100644 --- a/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs +++ b/src/NzbDrone.Common/EnsureThat/EnsureStringExtensions.cs @@ -101,12 +101,12 @@ public static Param IsValidPath(this Param param) if (param.Value.IsPathValid()) return param; - if (OsInfo.IsNotWindows) + if (OsInfo.IsWindows) { - throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid *nix path. paths must start with /", param.Value)); + throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value)); } - throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid Windows path. paths must be a full path eg. C:\\Windows", param.Value)); + throw ExceptionFactory.CreateForParamValidation(param.Name, string.Format("value [{0}] is not a valid *nix path. paths must start with /", param.Value)); } } } diff --git a/src/NzbDrone.Common/EnvironmentInfo/IOperatingSystemVersionInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IOperatingSystemVersionInfo.cs new file mode 100644 index 0000000000..c0b4b6290b --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/IOperatingSystemVersionInfo.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + public interface IOperatingSystemVersionInfo + { + string Version { get; } + string Name { get; } + string FullName { get; } + } +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/IOsVersionAdapter.cs b/src/NzbDrone.Common/EnvironmentInfo/IOsVersionAdapter.cs new file mode 100644 index 0000000000..25a3cbf1fc --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/IOsVersionAdapter.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + + public interface IOsVersionAdapter + { + bool Enabled { get; } + OsVersionModel Read(); + } +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/IPlatformInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IPlatformInfo.cs new file mode 100644 index 0000000000..5136f90d29 --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/IPlatformInfo.cs @@ -0,0 +1,50 @@ +using System; + +namespace NzbDrone.Common.EnvironmentInfo +{ + + public enum PlatformType + { + DotNet = 0, + Mono = 1 + } + + public interface IPlatformInfo + { + Version Version { get; } + } + + public abstract class PlatformInfo : IPlatformInfo + { + static PlatformInfo() + { + if (Type.GetType("Mono.Runtime") != null) + { + Platform = PlatformType.Mono; + } + else + { + Platform = PlatformType.DotNet; + } + } + + public static PlatformType Platform { get; } + public static bool IsMono => Platform == PlatformType.Mono; + public static bool IsDotNet => Platform == PlatformType.DotNet; + + public static string PlatformName + { + get + { + if (IsDotNet) + { + return ".NET"; + } + + return "Mono"; + } + } + + public abstract Version Version { get; } + } +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs index 2ef08901b2..d387001ef1 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs @@ -1,14 +1,13 @@ -namespace NzbDrone.Common.EnvironmentInfo +namespace NzbDrone.Common.EnvironmentInfo { public interface IRuntimeInfo { bool IsUserInteractive { get; } bool IsAdmin { get; } bool IsWindowsService { get; } - bool IsConsole { get; } - bool IsRunning { get; set; } + bool IsWindowsTray { get; } + bool IsExiting { get; set; } bool RestartPending { get; set; } string ExecutingApplication { get; } - string RuntimeVersion { get; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs index ad1acd4873..9df06528ea 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/OsInfo.cs @@ -1,87 +1,95 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; namespace NzbDrone.Common.EnvironmentInfo { - public static class OsInfo + public class OsInfo : IOsInfo { + public static Os Os { get; } + + public static bool IsNotWindows => !IsWindows; + public static bool IsLinux => Os == Os.Linux; + public static bool IsOsx => Os == Os.Osx; + public static bool IsWindows => Os == Os.Windows; + + public string Version { get; } + public string Name { get; } + public string FullName { get; } static OsInfo() { - var platform = (int)Environment.OSVersion.Platform; + var platform = Environment.OSVersion.Platform; - Version = Environment.OSVersion.Version; - - IsMonoRuntime = Type.GetType("Mono.Runtime") != null; - IsNotWindows = (platform == 4) || (platform == 6) || (platform == 128); - IsOsx = IsRunningOnMac(); - IsLinux = IsNotWindows && !IsOsx; - IsWindows = !IsNotWindows; - - FirstDayOfWeek = CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek; - - if (IsWindows) + switch (platform) { - Os = Os.Windows; - PathStringComparison = StringComparison.OrdinalIgnoreCase; + case PlatformID.Win32NT: + { + Os = Os.Windows; + break; + } + case PlatformID.MacOSX: + case PlatformID.Unix: + { + // Sometimes Mac OS reports itself as Unix + if (Directory.Exists("/System/Library/CoreServices/") && + (File.Exists("/System/Library/CoreServices/SystemVersion.plist") || + File.Exists("/System/Library/CoreServices/ServerVersion.plist")) + ) + { + Os = Os.Osx; + } + else + { + Os = Os.Linux; + } + break; + } + } + } + + public OsInfo(IEnumerable versionAdapters, Logger logger) + { + OsVersionModel osInfo = null; + + foreach (var osVersionAdapter in versionAdapters.Where(c => c.Enabled)) + { + try + { + osInfo = osVersionAdapter.Read(); + } + catch (Exception e) + { + logger.Error(e, "Couldn't get OS Version info"); + } + + if (osInfo != null) + { + break; + } + } + + if (osInfo != null) + { + Name = osInfo.Name; + Version = osInfo.Version; + FullName = osInfo.FullName; } else { - Os = IsOsx ? Os.Osx : Os.Linux; - - PathStringComparison = StringComparison.Ordinal; + Name = Os.ToString(); + FullName = Name; } } + } - public static Version Version { get; private set; } - public static bool IsMonoRuntime { get; private set; } - public static bool IsNotWindows { get; private set; } - public static bool IsLinux { get; private set; } - public static bool IsOsx { get; private set; } - public static bool IsWindows { get; private set; } - public static Os Os { get; private set; } - public static DayOfWeek FirstDayOfWeek { get; private set; } - public static StringComparison PathStringComparison { get; private set; } - - //Borrowed from: https://github.com/jpobst/Pinta/blob/master/Pinta.Core/Managers/SystemManager.cs - //From Managed.Windows.Forms/XplatUI - [DllImport("libc")] - static extern int uname(IntPtr buf); - - [DebuggerStepThrough] - static bool IsRunningOnMac() - { - var buf = IntPtr.Zero; - - try - { - buf = Marshal.AllocHGlobal(8192); - // This is a hacktastic way of getting sysname from uname () - if (uname(buf) == 0) - { - var os = Marshal.PtrToStringAnsi(buf); - - if (os == "Darwin") - { - return true; - } - } - } - catch - { - } - finally - { - if (buf != IntPtr.Zero) - { - Marshal.FreeHGlobal(buf); - } - } - - return false; - } + public interface IOsInfo + { + string Version { get; } + string Name { get; } + string FullName { get; } } public enum Os @@ -90,4 +98,4 @@ public enum Os Linux, Osx } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/OsVersionModel.cs b/src/NzbDrone.Common/EnvironmentInfo/OsVersionModel.cs new file mode 100644 index 0000000000..c20865813d --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/OsVersionModel.cs @@ -0,0 +1,29 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + public class OsVersionModel + { + + + public OsVersionModel(string name, string version, string fullName = null) + { + Name = Trim(name); + Version = Trim(version); + + if (string.IsNullOrWhiteSpace(fullName)) + { + fullName = $"{Name} {Version}"; + } + + FullName = Trim(fullName); + } + + private static string Trim(string source) + { + return source.Trim().Trim('"', '\''); + } + + public string Name { get; } + public string FullName { get; } + public string Version { get; } + } +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs similarity index 70% rename from src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs rename to src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index 2db303551e..d9908cb976 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfoBase.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Reflection; @@ -9,11 +9,11 @@ namespace NzbDrone.Common.EnvironmentInfo { - public abstract class RuntimeInfoBase : IRuntimeInfo + public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; - public RuntimeInfoBase(IServiceProvider serviceProvider, Logger logger) + public RuntimeInfo(IServiceProvider serviceProvider, Logger logger) { _logger = logger; @@ -28,10 +28,11 @@ public RuntimeInfoBase(IServiceProvider serviceProvider, Logger logger) if (entry != null) { ExecutingApplication = entry.Location; + IsWindowsTray = entry.ManifestModule.Name == $"{ProcessProvider.NZB_DRONE_PROCESS_NAME}.exe"; } } - static RuntimeInfoBase() + static RuntimeInfo() { IsProduction = InternalIsProduction(); } @@ -59,31 +60,18 @@ public bool IsAdmin public bool IsWindowsService { get; private set; } - public bool IsConsole - { - get - { - if (OsInfo.IsWindows) - { - return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase); - } - - return true; - } - } - - public bool IsRunning { get; set; } + public bool IsExiting { get; set; } public bool RestartPending { get; set; } - public string ExecutingApplication { get; private set; } + public string ExecutingApplication { get; } - public abstract string RuntimeVersion { get; } - - public static bool IsProduction { get; private set; } + public static bool IsProduction { get; } private static bool InternalIsProduction() { if (BuildInfo.IsDebug || Debugger.IsAttached) return false; - if (BuildInfo.Version.Revision > 10000) return false; //Official builds will never have such a high revision + + //Official builds will never have such a high revision + if (BuildInfo.Version.Revision > 10000) return false; try { @@ -99,21 +87,23 @@ private static bool InternalIsProduction() } - try - { - var currentAssmeblyLocation = typeof(RuntimeInfoBase).Assembly.Location; - if(currentAssmeblyLocation.ToLower().Contains("_output"))return false; - } - catch - { + try + { + var currentAssmeblyLocation = typeof(RuntimeInfo).Assembly.Location; + if (currentAssmeblyLocation.ToLower().Contains("_output")) return false; + } + catch + { - } + } - string lowerCurrentDir = Directory.GetCurrentDirectory().ToLower(); + var lowerCurrentDir = Directory.GetCurrentDirectory().ToLower(); if (lowerCurrentDir.Contains("teamcity")) return false; if (lowerCurrentDir.Contains("_output")) return false; return true; } + + public bool IsWindowsTray { get; private set; } } } diff --git a/src/NzbDrone.Common/Exceptron/ExceptionExtentions.cs b/src/NzbDrone.Common/Exceptron/ExceptionExtentions.cs deleted file mode 100644 index cb3d30b906..0000000000 --- a/src/NzbDrone.Common/Exceptron/ExceptionExtentions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Common.Exceptron -{ - public static class ExceptionExtentions - { - private const string IGNORE_FLAG = "exceptron_ignore"; - - public static Exception ExceptronIgnoreOnMono(this Exception exception) - { - if (OsInfo.IsNotWindows) - { - exception.ExceptronIgnore(); - } - - return exception; - } - - public static Exception ExceptronIgnore(this Exception exception) - { - exception.Data.Add(IGNORE_FLAG, true); - return exception; - } - - public static bool ExceptronShouldIgnore(this Exception exception) - { - return exception.Data.Contains(IGNORE_FLAG); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 63dc578844..34fc833f74 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnvironmentInfo; @@ -47,7 +48,7 @@ public static bool PathEquals(this string firstPath, string secondPath, StringCo { if (!comparison.HasValue) { - comparison = OsInfo.PathStringComparison; + comparison = DiskProviderBase.PathStringComparison; } if (firstPath.Equals(secondPath, comparison.Value)) return true; @@ -93,7 +94,7 @@ public static bool IsParentPath(this string parentPath, string childPath) while (child.Parent != null) { - if (child.Parent.FullName.Equals(parent.FullName, OsInfo.PathStringComparison)) + if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison)) { return true; } @@ -275,4 +276,4 @@ public static string GetNlogConfigPath(this IAppFolderInfo appFolderInfo) return Path.Combine(appFolderInfo.StartUpFolder, NLOG_CONFIG_FILE); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs index 233e9a81f5..b508a6e9df 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/CurlHttpDispatcher.cs @@ -21,6 +21,7 @@ public class CurlHttpDispatcher : IHttpDispatcher private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private readonly IHttpProxySettingsProvider _proxySettingsProvider; + private readonly IUserAgentBuilder _userAgentBuilder; private readonly Logger _logger; private const string _caBundleFileName = "curl-ca-bundle.crt"; @@ -37,10 +38,11 @@ static CurlHttpDispatcher() _caBundleFilePath = _caBundleFileName; } } - - public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, Logger logger) + + public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger) { _proxySettingsProvider = proxySettingsProvider; + _userAgentBuilder = userAgentBuilder; _logger = logger; } @@ -68,94 +70,93 @@ public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) { using (Stream responseStream = new MemoryStream()) using (Stream headerStream = new MemoryStream()) + + using (var curlEasy = new CurlEasy()) { - using (var curlEasy = new CurlEasy()) + curlEasy.AutoReferer = false; + curlEasy.WriteFunction = (b, s, n, o) => { - curlEasy.AutoReferer = false; - curlEasy.WriteFunction = (b, s, n, o) => + responseStream.Write(b, 0, s * n); + return s * n; + }; + curlEasy.HeaderFunction = (b, s, n, o) => + { + headerStream.Write(b, 0, s * n); + return s * n; + }; + + AddProxy(curlEasy, request); + + curlEasy.Url = request.Url.FullUri; + + switch (request.Method) + { + case HttpMethod.GET: + curlEasy.HttpGet = true; + break; + + case HttpMethod.POST: + curlEasy.Post = true; + break; + + case HttpMethod.PUT: + curlEasy.Put = true; + break; + + default: + throw new NotSupportedException($"HttpCurl method {request.Method} not supported"); + } + curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); + curlEasy.FollowLocation = false; + + if (request.RequestTimeout != TimeSpan.Zero) + { + curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds); + } + + if (OsInfo.IsWindows) + { + curlEasy.CaInfo = _caBundleFilePath; + } + + if (cookies != null) + { + curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url); + } + + if (request.ContentData != null) + { + curlEasy.PostFieldSize = request.ContentData.Length; + curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v))); + } + + // Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state + using (var httpRequestHeaders = SerializeHeaders(request)) + { + curlEasy.HttpHeader = httpRequestHeaders; + + var result = curlEasy.Perform(); + + if (result != CurlCode.Ok) { - responseStream.Write(b, 0, s * n); - return s * n; - }; - curlEasy.HeaderFunction = (b, s, n, o) => - { - headerStream.Write(b, 0, s * n); - return s * n; - }; - - AddProxy(curlEasy, request); - - curlEasy.Url = request.Url.FullUri; - - switch (request.Method) - { - case HttpMethod.GET: - curlEasy.HttpGet = true; - break; - - case HttpMethod.POST: - curlEasy.Post = true; - break; - - case HttpMethod.PUT: - curlEasy.Put = true; - break; - - default: - throw new NotSupportedException(string.Format("HttpCurl method {0} not supported", request.Method)); - } - curlEasy.FollowLocation = false; - curlEasy.UserAgent = request.UseSimplifiedUserAgent ? UserAgentBuilder.UserAgentSimplified : UserAgentBuilder.UserAgent; ; - - if (request.RequestTimeout != TimeSpan.Zero) - { - curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds); - } - - if (OsInfo.IsWindows) - { - curlEasy.CaInfo = _caBundleFilePath; - } - - if (cookies != null) - { - curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url); - } - - if (request.ContentData != null) - { - curlEasy.PostFieldSize = request.ContentData.Length; - curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v))); - } - - // Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state - using (var httpRequestHeaders = SerializeHeaders(request)) - { - curlEasy.HttpHeader = httpRequestHeaders; - - var result = curlEasy.Perform(); - - if (result != CurlCode.Ok) + switch (result) { - switch (result) - { - case CurlCode.SslCaCert: - case (CurlCode)77: - throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url)); - default: - throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url)); - - } + case CurlCode.SslCaCert: + case (CurlCode)77: + throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url)); + default: + throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url)); + } } - - var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream); - var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection); - - var httpHeader = new HttpHeader(webHeaderCollection); - - return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode); } + + var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream); + var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection); + + var httpHeader = new HttpHeader(webHeaderCollection); + + return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode); } } } @@ -243,7 +244,7 @@ private WebHeaderCollection ProcessHeaderStream(HttpRequest request, CookieConta private string FixSetCookieHeader(string setCookie) { // fix up the date if it was malformed - var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match) + var setCookieClean = ExpiryDate.Replace(setCookie, delegate (Match match) { string shortFormat = "ddd, dd-MMM-yy HH:mm:ss"; string longFormat = "ddd, dd-MMM-yyyy HH:mm:ss"; @@ -260,7 +261,6 @@ private string FixSetCookieHeader(string setCookie) private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection) { - byte[] bytes = null; responseStream.Position = 0; if (responseStream.Length != 0) @@ -270,27 +270,20 @@ private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, { if (encoding.IndexOf("gzip") != -1) { - using (var zipStream = new GZipStream(responseStream, CompressionMode.Decompress)) - { - bytes = zipStream.ToBytes(); - } + responseStream = new GZipStream(responseStream, CompressionMode.Decompress); webHeaderCollection.Remove("Content-Encoding"); } else if (encoding.IndexOf("deflate") != -1) { - using (var deflateStream = new DeflateStream(responseStream, CompressionMode.Decompress)) - { - bytes = deflateStream.ToBytes(); - } + responseStream = new DeflateStream(responseStream, CompressionMode.Decompress); webHeaderCollection.Remove("Content-Encoding"); } } } - if (bytes == null) bytes = responseStream.ToBytes(); - return bytes; + return responseStream.ToBytes(); } } diff --git a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs index 109d4aec22..707004c9da 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/FallbackHttpDispatcher.cs @@ -10,21 +10,23 @@ public class FallbackHttpDispatcher : IHttpDispatcher { private readonly ManagedHttpDispatcher _managedDispatcher; private readonly CurlHttpDispatcher _curlDispatcher; + private readonly IPlatformInfo _platformInfo; private readonly Logger _logger; private readonly ICached _curlTLSFallbackCache; - public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, Logger logger) + public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, IPlatformInfo platformInfo, Logger logger) { _managedDispatcher = managedDispatcher; _curlDispatcher = curlDispatcher; + _platformInfo = platformInfo; _curlTLSFallbackCache = cacheManager.GetCache(GetType(), "curlTLSFallback"); _logger = logger; } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) { - if (OsInfo.IsMonoRuntime && request.Url.Scheme == "https") + if (PlatformInfo.IsMono && request.Url.Scheme == "https") { if (!_curlTLSFallbackCache.Find(request.Url.Host)) { diff --git a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs index 660daad5a2..228cce35c2 100644 --- a/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs +++ b/src/NzbDrone.Common/Http/Dispatchers/ManagedHttpDispatcher.cs @@ -2,9 +2,13 @@ using System.IO; using System.IO.Compression; using System.Net; +using System.Reflection; +using NLog; +using NLog.Fluent; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http.Proxy; +using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Security; namespace NzbDrone.Common.Http.Dispatchers @@ -13,55 +17,59 @@ public class ManagedHttpDispatcher : IHttpDispatcher { private readonly IHttpProxySettingsProvider _proxySettingsProvider; private readonly ICreateManagedWebProxy _createManagedWebProxy; + private readonly IUserAgentBuilder _userAgentBuilder; + private readonly IPlatformInfo _platformInfo; + private readonly Logger _logger; - public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy) + public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger) { _proxySettingsProvider = proxySettingsProvider; _createManagedWebProxy = createManagedWebProxy; + _userAgentBuilder = userAgentBuilder; + _platformInfo = platformInfo; + _logger = logger; } public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) { - HttpWebResponse httpWebResponse = null; - HttpWebRequest webRequest = null; + var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url); + + if (PlatformInfo.IsMono) + { + // On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case. + webRequest.AutomaticDecompression = DecompressionMethods.None; + webRequest.Headers.Add("Accept-Encoding", "gzip"); + } + else + { + // Deflate is not a standard and could break depending on implementation. + // we should just stick with the more compatible Gzip + //http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net + webRequest.AutomaticDecompression = DecompressionMethods.GZip; + } + + webRequest.Method = request.Method.ToString(); + webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent); + webRequest.KeepAlive = request.ConnectionKeepAlive; + webRequest.AllowAutoRedirect = false; + webRequest.CookieContainer = cookies; + + if (request.RequestTimeout != TimeSpan.Zero) + { + webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds); + } + + AddProxy(webRequest, request); + + if (request.Headers != null) + { + AddRequestHeaders(webRequest, request.Headers); + } + + HttpWebResponse httpWebResponse; + try { - webRequest = (HttpWebRequest) WebRequest.Create((Uri) request.Url); - - if (OsInfo.IsMonoRuntime) - { - // On Mono GZipStream/DeflateStream leaks memory if an exception is thrown, use an intermediate buffer in that case. - webRequest.AutomaticDecompression = DecompressionMethods.None; - webRequest.Headers.Add("Accept-Encoding", "gzip"); - } - else - { - // Deflate is not a standard and could break depending on implementation. - // we should just stick with the more compatible Gzip - //http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net - webRequest.AutomaticDecompression = DecompressionMethods.GZip; - } - - webRequest.Method = request.Method.ToString(); - webRequest.UserAgent = request.UseSimplifiedUserAgent - ? UserAgentBuilder.UserAgentSimplified - : UserAgentBuilder.UserAgent; - webRequest.KeepAlive = request.ConnectionKeepAlive; - webRequest.AllowAutoRedirect = false; - webRequest.CookieContainer = cookies; - - if (request.RequestTimeout != TimeSpan.Zero) - { - webRequest.Timeout = (int) Math.Ceiling(request.RequestTimeout.TotalMilliseconds); - } - - AddProxy(webRequest, request); - - if (request.Headers != null) - { - AddRequestHeaders(webRequest, request.Headers); - } - if (request.ContentData != null) { webRequest.ContentLength = request.ContentData.Length; @@ -71,34 +79,57 @@ public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) } } - try + httpWebResponse = (HttpWebResponse)webRequest.GetResponse(); + } + catch (WebException e) + { + if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows) { - httpWebResponse = (HttpWebResponse) webRequest.GetResponse(); + SecurityProtocolPolicy.DisableTls12(); } - catch (WebException e) + + httpWebResponse = (HttpWebResponse)e.Response; + + if (httpWebResponse == null) { - if (e.Status == WebExceptionStatus.SecureChannelFailure && OsInfo.IsWindows) + // Workaround for mono not closing connections properly in certain situations. + AbortWebRequest(webRequest); + + // The default messages for WebException on mono are pretty horrible. + if (e.Status == WebExceptionStatus.NameResolutionFailure) { - SecurityProtocolPolicy.DisableTls12(); + throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status); } - - httpWebResponse = (HttpWebResponse) e.Response; - - if (httpWebResponse == null) + else if (e.ToString().Contains("TLS Support not")) + { + throw new TlsFailureException(webRequest, e); + } + else if (e.ToString().Contains("The authentication or decryption has failed.")) + { + throw new TlsFailureException(webRequest, e); + } + else if (OsInfo.IsNotWindows) + { + throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response); + } + else { throw; } } + } - byte[] data = null; + byte[] data = null; - using (var responseStream = httpWebResponse.GetResponseStream()) + using (var responseStream = httpWebResponse.GetResponseStream()) + { + if (responseStream != null && responseStream != Stream.Null) { - if (responseStream != null) + try { data = responseStream.ToBytes(); - if (OsInfo.IsMonoRuntime && httpWebResponse.ContentEncoding == "gzip") + if (PlatformInfo.IsMono && httpWebResponse.ContentEncoding == "gzip") { using (var compressedStream = new MemoryStream(data)) using (var gzip = new GZipStream(compressedStream, CompressionMode.Decompress)) @@ -111,17 +142,14 @@ public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies) httpWebResponse.Headers.Remove("Content-Encoding"); } } + catch (Exception ex) + { + throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse); + } } + } - return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, - httpWebResponse.StatusCode); - } - finally - { - webRequest = null; - (httpWebResponse as IDisposable)?.Dispose(); - httpWebResponse = null; - } + return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode); } protected virtual void AddProxy(HttpWebRequest webRequest, HttpRequest request) @@ -181,5 +209,35 @@ protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader h } } } + + // Workaround for mono not closing connections properly on timeouts + private void AbortWebRequest(HttpWebRequest webRequest) + { + // First affected version was mono 5.16 + if (OsInfo.IsNotWindows && _platformInfo.Version >= new Version(5, 16)) + { + try + { + var currentOperationInfo = webRequest.GetType().GetField("currentOperation", BindingFlags.NonPublic | BindingFlags.Instance); + var currentOperation = currentOperationInfo.GetValue(webRequest); + + if (currentOperation != null) + { + var responseStreamInfo = currentOperation.GetType().GetField("responseStream", BindingFlags.NonPublic | BindingFlags.Instance); + var responseStream = responseStreamInfo.GetValue(currentOperation) as Stream; + // Note that responseStream will likely be null once mono fixes it. + responseStream?.Dispose(); + } + } + catch (Exception ex) + { + // This can fail randomly on future mono versions that have been changed/fixed. Log to sentry and ignore. + _logger.Trace() + .Exception(ex) + .Message("Unable to dispose responseStream on mono {0}", _platformInfo.Version) + .Write(); + } + } + } } } diff --git a/src/NzbDrone.Common/Http/HttpClient.cs b/src/NzbDrone.Common/Http/HttpClient.cs index 1c234c4275..61d4a9a09a 100644 --- a/src/NzbDrone.Common/Http/HttpClient.cs +++ b/src/NzbDrone.Common/Http/HttpClient.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -31,12 +31,19 @@ public class HttpClient : IHttpClient private readonly ICached _cookieContainerCache; private readonly List _requestInterceptors; private readonly IHttpDispatcher _httpDispatcher; + private readonly IUserAgentBuilder _userAgentBuilder; - public HttpClient(IEnumerable requestInterceptors, ICacheManager cacheManager, IRateLimitService rateLimitService, IHttpDispatcher httpDispatcher, Logger logger) + public HttpClient(IEnumerable requestInterceptors, + ICacheManager cacheManager, + IRateLimitService rateLimitService, + IHttpDispatcher httpDispatcher, + IUserAgentBuilder userAgentBuilder, + Logger logger) { _requestInterceptors = requestInterceptors.ToList(); _rateLimitService = rateLimitService; _httpDispatcher = httpDispatcher; + _userAgentBuilder = userAgentBuilder; _logger = logger; ServicePointManager.DefaultConnectionLimit = 12; @@ -71,7 +78,7 @@ public HttpResponse Execute(HttpRequest request) while (response.HasHttpRedirect); } - if (response.HasHttpRedirect && !RuntimeInfoBase.IsProduction) + if (response.HasHttpRedirect && !RuntimeInfo.IsProduction) { _logger.Error("Server requested a redirect to [{0}] while in developer mode. Update the request URL to avoid this redirect.", response.Headers["Location"]); } @@ -143,19 +150,30 @@ private CookieContainer InitializeRequestCookies(HttpRequest request) if (!request.IgnorePersistentCookies) { var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); - sourceContainer.Add(persistentCookies); + sourceContainer.Add(persistentCookies); } if (request.Cookies.Count != 0) { foreach (var pair in request.Cookies) { - var cookie = new Cookie(pair.Key, pair.Value, "/") + Cookie cookie; + if (pair.Value == null) { - // Use Now rather than UtcNow to work around Mono cookie expiry bug. - // See https://gist.github.com/ta264/7822b1424f72e5b4c961 - Expires = DateTime.Now.AddHours(1) - }; + cookie = new Cookie(pair.Key, "", "/") + { + Expires = DateTime.Now.AddDays(-1) + }; + } + else + { + cookie = new Cookie(pair.Key, pair.Value, "/") + { + // Use Now rather than UtcNow to work around Mono cookie expiry bug. + // See https://gist.github.com/ta264/7822b1424f72e5b4c961 + Expires = DateTime.Now.AddHours(1) + }; + } sourceContainer.Add((Uri)request.Url, cookie); @@ -178,7 +196,6 @@ private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieCo var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer()); var persistentCookies = presistentContainer.GetCookies((Uri)request.Url); var existingCookies = cookieContainer.GetCookies((Uri)request.Url); - cookieContainer.Add(persistentCookies); cookieContainer.Add(existingCookies); }*/ @@ -226,13 +243,11 @@ public void DownloadFile(string url, string fileName) _logger.Debug("Downloading [{0}] to [{1}]", url, fileName); var stopWatch = Stopwatch.StartNew(); - using (var webClient = new GZipWebClient()) - { - webClient.Headers.Add(HttpRequestHeader.UserAgent, UserAgentBuilder.UserAgent); - webClient.DownloadFile(url, fileName); - stopWatch.Stop(); - _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds); - } + var webClient = new GZipWebClient(); + webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent()); + webClient.DownloadFile(url, fileName); + stopWatch.Stop(); + _logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds); } catch (WebException e) { @@ -255,6 +270,7 @@ public HttpResponse Get(HttpRequest request) public HttpResponse Get(HttpRequest request) where T : new() { var response = Get(request); + CheckResponseContentType(response); return new HttpResponse(response); } @@ -273,7 +289,16 @@ public HttpResponse Post(HttpRequest request) public HttpResponse Post(HttpRequest request) where T : new() { var response = Post(request); + CheckResponseContentType(response); return new HttpResponse(response); } + + private void CheckResponseContentType(HttpResponse response) + { + if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html")) + { + throw new UnexpectedHtmlContentException(response); + } + } } } diff --git a/src/NzbDrone.Common/Http/HttpException.cs b/src/NzbDrone.Common/Http/HttpException.cs index 759a104c19..005fab57ae 100644 --- a/src/NzbDrone.Common/Http/HttpException.cs +++ b/src/NzbDrone.Common/Http/HttpException.cs @@ -7,13 +7,19 @@ public class HttpException : Exception public HttpRequest Request { get; private set; } public HttpResponse Response { get; private set; } - public HttpException(HttpRequest request, HttpResponse response) - : base(string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url)) + public HttpException(HttpRequest request, HttpResponse response, string message) + : base(message) { Request = request; Response = response; } + public HttpException(HttpRequest request, HttpResponse response) + : this(request, response, string.Format("HTTP request failed: [{0}:{1}] [{2}] at [{3}]", (int)response.StatusCode, response.StatusCode, request.Method, request.Url)) + { + + } + public HttpException(HttpResponse response) : this(response.Request, response) { @@ -30,4 +36,4 @@ public override string ToString() return base.ToString(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Http/HttpRequest.cs b/src/NzbDrone.Common/Http/HttpRequest.cs index f0f2d053c9..61b46e53fc 100644 --- a/src/NzbDrone.Common/Http/HttpRequest.cs +++ b/src/NzbDrone.Common/Http/HttpRequest.cs @@ -17,7 +17,7 @@ public HttpRequest(string url, HttpAccept httpAccept = null) IgnorePersistentCookies = false; Cookies = new Dictionary(); - if (!RuntimeInfoBase.IsProduction) + if (!RuntimeInfo.IsProduction) { AllowAutoRedirect = false; } diff --git a/src/NzbDrone.Common/Http/TlsFailureException.cs b/src/NzbDrone.Common/Http/TlsFailureException.cs new file mode 100644 index 0000000000..300e907c20 --- /dev/null +++ b/src/NzbDrone.Common/Http/TlsFailureException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace NzbDrone.Common.Http +{ + public class TlsFailureException : WebException + { + public TlsFailureException(WebRequest request, WebException innerException) + : base("Failed to establish secure https connection to '" + request.RequestUri + "', libcurl fallback might be unavailable.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response) + { + + } + + } +} diff --git a/src/NzbDrone.Common/Http/UnexpectedHtmlContentException.cs b/src/NzbDrone.Common/Http/UnexpectedHtmlContentException.cs new file mode 100644 index 0000000000..b2356f7d5b --- /dev/null +++ b/src/NzbDrone.Common/Http/UnexpectedHtmlContentException.cs @@ -0,0 +1,14 @@ + +using System; + +namespace NzbDrone.Common.Http +{ + public class UnexpectedHtmlContentException : HttpException + { + public UnexpectedHtmlContentException(HttpResponse response) + : base(response.Request, response, $"Site responded with browser content instead of api data. This disruption may be temporary, please try again later. [{response.Request.Url}]") + { + + } + } +} diff --git a/src/NzbDrone.Common/Http/UserAgentBuilder.cs b/src/NzbDrone.Common/Http/UserAgentBuilder.cs index f0cff30e97..9626cd9f7c 100644 --- a/src/NzbDrone.Common/Http/UserAgentBuilder.cs +++ b/src/NzbDrone.Common/Http/UserAgentBuilder.cs @@ -2,19 +2,39 @@ namespace NzbDrone.Common.Http { - public static class UserAgentBuilder + public interface IUserAgentBuilder { - public static string UserAgent { get; private set; } - public static string UserAgentSimplified { get; private set; } + string GetUserAgent(bool simplified = false); + } - static UserAgentBuilder() + public class UserAgentBuilder : IUserAgentBuilder + { + private readonly string _userAgentSimplified; + private readonly string _userAgent; + + public string GetUserAgent(bool simplified) { - UserAgent = string.Format("Radarr/{0} ({1} {2})", - BuildInfo.Version, - OsInfo.Os, OsInfo.Version.ToString(2)); + if (simplified) + { + return _userAgentSimplified; + } - UserAgentSimplified = string.Format("Radarr/{0}", - BuildInfo.Version.ToString(2)); + return _userAgent; + } + + public UserAgentBuilder(IOsInfo osInfo) + { + var osName = OsInfo.Os.ToString(); + + if (!string.IsNullOrWhiteSpace(osInfo.Name)) + { + osName = osInfo.Name.ToLower(); + } + + var osVersion = osInfo.Version?.ToLower(); + + _userAgent = $"Radarr/{BuildInfo.Version} ({osName} {osVersion})"; + _userAgentSimplified = $"Radarr/{BuildInfo.Version.ToString(2)}"; } } } diff --git a/src/NzbDrone.Common/Instrumentation/ExceptronTarget.cs b/src/NzbDrone.Common/Instrumentation/ExceptronTarget.cs deleted file mode 100644 index 437b480245..0000000000 --- a/src/NzbDrone.Common/Instrumentation/ExceptronTarget.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using NLog; -using NLog.Common; -using NLog.Layouts; -using NLog.Targets; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Exceptron; -using NzbDrone.Common.Exceptron.Configuration; - -namespace NzbDrone.Common.Instrumentation -{ - /// - /// target for exceptron. Allows you to automatically report all - /// exceptions logged to Nlog/> - /// - [Target("Exceptron")] - public class ExceptronTarget : Target - { - /// - /// instance that Nlog Target uses to report the exceptions. - /// - public IExceptronClient ExceptronClient { get; internal set; } - - protected override void InitializeTarget() - { - var config = new ExceptronConfiguration - { - ApiKey = "d64e0a72845d495abc625af3a27cf5f5", - IncludeMachineName = true, - }; - - if (RuntimeInfoBase.IsProduction) - { - config.ApiKey = "82c0f66dd2d64d1480cc88b551c9bdd8"; - } - - ExceptronClient = new ExceptronClient(config, BuildInfo.Version); - } - - - /// - /// String that identifies the active user - /// - public Layout UserId { get; set; } - - protected override void Write(LogEventInfo logEvent) - { - if (logEvent == null || logEvent.Exception == null || logEvent.Exception.ExceptronShouldIgnore()) return; - - try - { - var exceptionData = new ExceptionData - { - Exception = logEvent.Exception, - Component = logEvent.LoggerName, - Message = logEvent.FormattedMessage, - }; - - if (UserId != null) - { - exceptionData.UserId = UserId.Render(logEvent); - } - - if (logEvent.Level <= LogLevel.Info) - { - exceptionData.Severity = ExceptionSeverity.None; - } - else if (logEvent.Level <= LogLevel.Warn) - { - exceptionData.Severity = ExceptionSeverity.Warning; - } - else if (logEvent.Level <= LogLevel.Error) - { - exceptionData.Severity = ExceptionSeverity.Error; - } - else if (logEvent.Level <= LogLevel.Fatal) - { - exceptionData.Severity = ExceptionSeverity.Fatal; - } - - ExceptronClient.SubmitException(exceptionData); - } - catch (Exception e) - { - InternalLogger.Warn("Unable to report exception. {0}", e); - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs index 2fee91f562..99032f0107 100644 --- a/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs +++ b/src/NzbDrone.Common/Instrumentation/GlobalExceptionHandlers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using NLog; using NzbDrone.Common.EnvironmentInfo; @@ -35,7 +35,7 @@ private static void HandleAppDomainException(object sender, UnhandledExceptionEv return; } - if (OsInfo.IsMonoRuntime) + if (PlatformInfo.IsMono) { if (exception is TypeInitializationException && exception.InnerException is DllNotFoundException || exception is DllNotFoundException) @@ -51,4 +51,4 @@ private static void HandleAppDomainException(object sender, UnhandledExceptionEv Logger.Fatal(exception, "EPIC FAIL: " + exception.Message); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs index 8d319a77a7..35d8d0e877 100644 --- a/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs +++ b/src/NzbDrone.Common/Instrumentation/NzbDroneLogger.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.IO; using LogentriesNLog; @@ -48,7 +48,7 @@ public static void Register(IStartupContext startupContext, bool updateApp, bool } else { - if (inConsole && (OsInfo.IsNotWindows || RuntimeInfoBase.IsUserInteractive)) + if (inConsole && (OsInfo.IsNotWindows || RuntimeInfo.IsUserInteractive)) { RegisterConsole(); } @@ -152,16 +152,6 @@ private static void RegisterUpdateFile(IAppFolderInfo appFolderInfo) LogManager.Configuration.LoggingRules.Add(loggingRule); } - private static void RegisterExceptron() - { - var exceptronTarget = new ExceptronTarget(); - var rule = new LoggingRule("*", LogLevel.Warn, exceptronTarget); - - LogManager.Configuration.AddTarget("ExceptronTarget", exceptronTarget); - LogManager.Configuration.LoggingRules.Add(rule); - } - - public static Logger GetLogger(Type obj) { return LogManager.GetLogger(obj.Name.Replace("NzbDrone.", "")); diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 3e72040075..072d719b21 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -94,6 +94,10 @@ + + + + @@ -125,13 +129,12 @@ - + - @@ -190,11 +193,12 @@ + + - diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index a6a837f8c2..83a42ac5dd 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -108,7 +108,7 @@ public void OpenDefaultBrowser(string url) public Process Start(string path, string args = null, StringDictionary environmentVariables = null, Action onOutputDataReceived = null, Action onErrorDataReceived = null) { - if (OsInfo.IsMonoRuntime && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) { args = GetMonoArgs(path, args); path = "mono"; @@ -192,7 +192,7 @@ public Process Start(string path, string args = null, StringDictionary environme public Process SpawnNewProcess(string path, string args = null, StringDictionary environmentVariables = null) { - if (OsInfo.IsMonoRuntime && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) + if (PlatformInfo.IsMono && path.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase)) { args = GetMonoArgs(path, args); path = "mono"; diff --git a/src/NzbDrone.Core.Test/Framework/CoreTest.cs b/src/NzbDrone.Core.Test/Framework/CoreTest.cs index dd9221b023..36efd648e2 100644 --- a/src/NzbDrone.Core.Test/Framework/CoreTest.cs +++ b/src/NzbDrone.Core.Test/Framework/CoreTest.cs @@ -1,9 +1,9 @@ -using System.Collections.Specialized; -using System.Security.AccessControl; using Moq; +using System; using NUnit.Framework; using NzbDrone.Common.Cache; using NzbDrone.Common.Cloud; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Common.Http.Dispatchers; using NzbDrone.Common.TPL; @@ -24,12 +24,16 @@ public abstract class CoreTest : TestBase { protected void UseRealHttp() { + Mocker.GetMock().SetupGet(c => c.Version).Returns(new Version("3.0.0")); + Mocker.GetMock().SetupGet(c => c.Version).Returns("1.0.0"); + Mocker.GetMock().SetupGet(c => c.Name).Returns("TestOS"); + Mocker.SetConstant(new HttpProxySettingsProvider(Mocker.Resolve())); Mocker.SetConstant(new ManagedWebProxyFactory(Mocker.Resolve())); - Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve())); - Mocker.SetConstant(new CurlHttpDispatcher(Mocker.Resolve(), Mocker.Resolve())); + Mocker.SetConstant(new ManagedHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); + Mocker.SetConstant(new CurlHttpDispatcher(Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new HttpProvider(TestLogger)); - Mocker.SetConstant(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); + Mocker.SetConstant(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), Mocker.Resolve(), TestLogger)); Mocker.SetConstant(new RadarrCloudRequestBuilder()); } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs index 420e0268a7..052d4f6094 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.HealthCheck.Checks; using NzbDrone.Core.Test.Framework; @@ -8,17 +9,12 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks [TestFixture] public class MonoVersionCheckFixture : CoreTest { - [SetUp] - public void Setup() - { - MonoOnly(); - } - private void GivenOutput(string version) { - Mocker.GetMock() - .SetupGet(s => s.RuntimeVersion) - .Returns(string.Format("{0} (tarball Wed Sep 25 16:35:44 CDT 2013)", version)); + MonoOnly(); + Mocker.GetMock() + .SetupGet(s => s.Version) + .Returns(new Version(version)); } [TestCase("3.10")] diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs index 0f1c4a5d99..cfe4bf9df6 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs @@ -2,6 +2,7 @@ using System.Linq; using FluentAssertions; using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Update; @@ -10,6 +11,12 @@ namespace NzbDrone.Core.Test.UpdateTests { public class UpdatePackageProviderFixture : CoreTest { + [SetUp] + public void Setup() + { + Mocker.GetMock().SetupGet(c => c.Version).Returns(new Version("9.9.9")); + } + [Test] public void no_update_when_version_higher() { diff --git a/src/NzbDrone.Core/Analytics/AnalyticsService.cs b/src/NzbDrone.Core/Analytics/AnalyticsService.cs index 0ddb9813d0..6e2d433828 100644 --- a/src/NzbDrone.Core/Analytics/AnalyticsService.cs +++ b/src/NzbDrone.Core/Analytics/AnalyticsService.cs @@ -17,6 +17,6 @@ public AnalyticsService(IConfigFileProvider configFileProvider) _configFileProvider = configFileProvider; } - public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfoBase.IsProduction; + public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction; } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index e107a107a1..b79750ff34 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using NLog; using NzbDrone.Common.EnsureThat; @@ -373,7 +374,7 @@ public string ChownGroup public int FirstDayOfWeek { - get { return GetValueInt("FirstDayOfWeek", (int)OsInfo.FirstDayOfWeek); } + get { return GetValueInt("FirstDayOfWeek", (int)CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek); } set { SetValue("FirstDayOfWeek", value); } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index f74156cb32..f2688e2f95 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.EnvironmentInfo; @@ -9,50 +8,43 @@ namespace NzbDrone.Core.HealthCheck.Checks { public class MonoVersionCheck : HealthCheckBase { - private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; private readonly Logger _logger; - private static readonly Regex VersionRegex = new Regex(@"(?<=\W|^)(?\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - public MonoVersionCheck(IRuntimeInfo runtimeInfo, Logger logger) + public MonoVersionCheck(IPlatformInfo platformInfo, Logger logger) { - _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; _logger = logger; } public override HealthCheck Check() { - if (OsInfo.IsWindows) + if (!PlatformInfo.IsMono) { return new HealthCheck(GetType()); } - var versionString = _runtimeInfo.RuntimeVersion; - var versionMatch = VersionRegex.Match(versionString); + var monoVersion = _platformInfo.Version; - if (versionMatch.Success) + if (monoVersion == new Version("3.4.0") && HasMonoBug18599()) { - var version = new Version(versionMatch.Groups["version"].Value); - - if (version == new Version(3, 4, 0) && HasMonoBug18599()) - { - _logger.Debug("mono version 3.4.0, checking for mono bug #18599 returned positive."); - return new HealthCheck(GetType(), HealthCheckResult.Error, "your mono version 3.4.0 has a critical bug, you should upgrade to a higher version"); - } - - if (version == new Version(4, 4, 0) || version == new Version(4, 4, 1)) - { - _logger.Debug("mono version {0}", version); - return new HealthCheck(GetType(), HealthCheckResult.Error, $"your mono version {version} has a bug that causes issues connecting to indexers/download clients"); - } - - if (version >= new Version(3, 10)) - { - _logger.Debug("mono version is 3.10 or better: {0}", version.ToString()); - return new HealthCheck(GetType()); - } + _logger.Debug("Mono version 3.4.0, checking for Mono bug #18599 returned positive."); + return new HealthCheck(GetType(), HealthCheckResult.Error, "You are running an old and unsupported version of Mono with a known bug. You should upgrade to a higher version"); } - return new HealthCheck(GetType(), HealthCheckResult.Warning, "mono version is less than 3.10, upgrade for improved stability"); + if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1")) + { + _logger.Debug("Mono version {0}", monoVersion); + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version"); + } + + if (monoVersion >= new Version("3.10")) + { + _logger.Debug("Mono version is 3.10 or better: {0}", monoVersion); + return new HealthCheck(GetType()); + } + + return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability."); } public override bool CheckOnConfigChange => false; @@ -70,7 +62,8 @@ private bool HasMonoBug18599() return false; } - var fieldInfo = numberFormatterType.GetField("userFormatProvider", BindingFlags.Static | BindingFlags.NonPublic); + var fieldInfo = numberFormatterType.GetField("userFormatProvider", + BindingFlags.Static | BindingFlags.NonPublic); if (fieldInfo == null) { diff --git a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs index 50446ed1d6..535df3d9f6 100644 --- a/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs +++ b/src/NzbDrone.Core/Lifecycle/ApplicationShutdownRequested.cs @@ -1,18 +1,14 @@ -using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Lifecycle { public class ApplicationShutdownRequested : IEvent { - public bool Restarting { get; set; } + public bool Restarting { get; } - public ApplicationShutdownRequested() - { - } - - public ApplicationShutdownRequested(bool restarting) + public ApplicationShutdownRequested(bool restarting = false) { Restarting = restarting; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs index b7b9c7d00d..851ad8076f 100644 --- a/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpdateMovieFileService.cs @@ -189,7 +189,6 @@ private bool ChangeFileDateToUtcAirDate(string filePath, DateTime airDateUtc) catch (Exception ex) { - ex.ExceptronIgnoreOnMono(); _logger.Warn(ex, "Unable to set date of file [" + filePath + "]"); } } diff --git a/src/NzbDrone.Core/Rest/RestClientFactory.cs b/src/NzbDrone.Core/Rest/RestClientFactory.cs index 545c002588..ce28b10c93 100644 --- a/src/NzbDrone.Core/Rest/RestClientFactory.cs +++ b/src/NzbDrone.Core/Rest/RestClientFactory.cs @@ -7,12 +7,10 @@ public static class RestClientFactory { public static RestClient BuildClient(string baseUrl) { - var restClient = new RestClient(baseUrl); - - restClient.UserAgent = string.Format("Radarr/{0} (RestSharp/{1}; {2}/{3})", - BuildInfo.Version, - restClient.GetType().Assembly.GetName().Version, - OsInfo.Os, OsInfo.Version.ToString(2)); + var restClient = new RestClient(baseUrl) + { + UserAgent = $"Radarr/{BuildInfo.Version} ({OsInfo.Os})" + }; return restClient; } diff --git a/src/NzbDrone.Core/Update/UpdateCheckService.cs b/src/NzbDrone.Core/Update/UpdateCheckService.cs index 8072507800..46e4b6b631 100644 --- a/src/NzbDrone.Core/Update/UpdateCheckService.cs +++ b/src/NzbDrone.Core/Update/UpdateCheckService.cs @@ -1,4 +1,3 @@ -using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; @@ -14,16 +13,11 @@ public class CheckUpdateService : ICheckUpdateService private readonly IUpdatePackageProvider _updatePackageProvider; private readonly IConfigFileProvider _configFileProvider; - private readonly Logger _logger; - - public CheckUpdateService(IUpdatePackageProvider updatePackageProvider, - IConfigFileProvider configFileProvider, - Logger logger) + IConfigFileProvider configFileProvider) { _updatePackageProvider = updatePackageProvider; _configFileProvider = configFileProvider; - _logger = logger; } public UpdatePackage AvailableUpdate() @@ -31,4 +25,4 @@ public UpdatePackage AvailableUpdate() return _updatePackageProvider.GetLatestUpdate(_configFileProvider.Branch, BuildInfo.Version); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index 96959e5afc..84d0c50577 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Common.Cloud; using NzbDrone.Common.EnvironmentInfo; @@ -15,11 +15,13 @@ public interface IUpdatePackageProvider public class UpdatePackageProvider : IUpdatePackageProvider { private readonly IHttpClient _httpClient; + private readonly IPlatformInfo _platformInfo; private readonly IHttpRequestBuilderFactory _requestBuilder; - public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder) + public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder, IPlatformInfo platformInfo) { _httpClient = httpClient; + _platformInfo = platformInfo; _requestBuilder = requestBuilder.Services; } @@ -29,6 +31,7 @@ public UpdatePackage GetLatestUpdate(string branch, Version currentVersion) .Resource("/update/{branch}") .AddQueryParam("version", currentVersion) .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) + .AddQueryParam("runtimeVer", _platformInfo.Version) .SetSegment("branch", branch) .Build(); @@ -45,6 +48,7 @@ public List GetRecentUpdates(string branch, Version currentVersio .Resource("/update/{branch}/changes") .AddQueryParam("version", currentVersion) .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) + .AddQueryParam("runtimeVer", _platformInfo.Version) .SetSegment("branch", branch) .Build(); @@ -53,4 +57,4 @@ public List GetRecentUpdates(string branch, Version currentVersio return updates.Resource; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs index 7c61f43207..ff365c0a42 100644 --- a/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs +++ b/src/NzbDrone.Host/AccessControl/UrlAclAdapter.cs @@ -20,6 +20,7 @@ public class UrlAclAdapter : IUrlAclAdapter private readonly INetshProvider _netshProvider; private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; + private readonly IOsInfo _osInfo; private readonly Logger _logger; public List Urls @@ -30,7 +31,7 @@ public List Urls } } - private List InternalUrls { get; set; } + private List InternalUrls { get; } private List RegisteredUrls { get; set; } private static readonly Regex UrlAclRegex = new Regex(@"(?https?)\:\/\/(?
.+?)\:(?\d+)/(?.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -38,19 +39,26 @@ public List Urls public UrlAclAdapter(INetshProvider netshProvider, IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, + IOsInfo osInfo, Logger logger) { _netshProvider = netshProvider; _configFileProvider = configFileProvider; _runtimeInfo = runtimeInfo; + _osInfo = osInfo; _logger = logger; InternalUrls = new List(); - RegisteredUrls = GetRegisteredUrls(); + RegisteredUrls = new List(); } public void ConfigureUrls() { + if (RegisteredUrls.Empty()) + { + GetRegisteredUrls(); + } + var localHostHttpUrls = BuildUrlAcls("http", "localhost", _configFileProvider.Port); var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, _configFileProvider.Port); @@ -105,7 +113,8 @@ public void ConfigureUrls() private void RefreshRegistration() { - if (OsInfo.Version.Major < 6) return; + var osVersion = new Version(_osInfo.Version); + if (osVersion.Major < 6) return; foreach (var urlAcl in InternalUrls) { @@ -124,19 +133,24 @@ private bool IsRegistered(UrlAcl urlAcl) c.UrlBase == urlAcl.UrlBase); } - private List GetRegisteredUrls() + private void GetRegisteredUrls() { if (OsInfo.IsNotWindows) { - return new List(); + return; + } + + if (RegisteredUrls.Any()) + { + return; } var arguments = string.Format("http show urlacl"); var output = _netshProvider.Run(arguments); - if (output == null || !output.Standard.Any()) return new List(); + if (output == null || !output.Standard.Any()) return; - return output.Standard.Select(line => + RegisteredUrls = output.Standard.Select(line => { var match = UrlAclRegex.Match(line.Content); diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index 5027b6be56..55717e7987 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -58,7 +58,7 @@ public void Start() //_cancelHandler = new CancelHandler(); } - _runtimeInfo.IsRunning = true; + _runtimeInfo.IsExiting = false; DbFactory.RegisterDatabase(_container); _hostController.StartServer(); @@ -87,7 +87,7 @@ private void Shutdown() _logger.Info("Attempting to stop application."); _hostController.StopServer(); _logger.Info("Application has finished stop routine."); - _runtimeInfo.IsRunning = false; + _runtimeInfo.IsExiting = true; } public void Handle(ApplicationShutdownRequested message) diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index a82d3d836e..e14560c38e 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Nancy.Bootstrapper; using NzbDrone.Api; using NzbDrone.Common.Composition; @@ -15,26 +15,15 @@ public static IContainer BuildContainer(StartupContext args) var assemblies = new List { "Radarr.Host", - "NzbDrone.Common", "NzbDrone.Core", "NzbDrone.Api", "NzbDrone.SignalR" }; - if (OsInfo.IsWindows) - { - assemblies.Add("NzbDrone.Windows"); - } - - else - { - assemblies.Add("NzbDrone.Mono"); - } - - return new MainAppContainerBuilder(args, assemblies.ToArray()).Container; + return new MainAppContainerBuilder(args, assemblies).Container; } - private MainAppContainerBuilder(StartupContext args, string[] assemblies) + private MainAppContainerBuilder(StartupContext args, List assemblies) : base(args, assemblies) { AutoRegisterImplementations(); @@ -43,4 +32,4 @@ private MainAppContainerBuilder(StartupContext args, string[] assemblies) Container.Register(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/Router.cs b/src/NzbDrone.Host/Router.cs index 8009ccb70d..cfc9259296 100644 --- a/src/NzbDrone.Host/Router.cs +++ b/src/NzbDrone.Host/Router.cs @@ -1,5 +1,6 @@ -using NLog; +using NLog; using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; namespace Radarr.Host { @@ -8,14 +9,16 @@ public class Router private readonly INzbDroneServiceFactory _nzbDroneServiceFactory; private readonly IServiceProvider _serviceProvider; private readonly IConsoleService _consoleService; + private readonly IRuntimeInfo _runtimeInfo; private readonly Logger _logger; public Router(INzbDroneServiceFactory nzbDroneServiceFactory, IServiceProvider serviceProvider, - IConsoleService consoleService, Logger logger) + IConsoleService consoleService, IRuntimeInfo runtimeInfo, Logger logger) { _nzbDroneServiceFactory = nzbDroneServiceFactory; _serviceProvider = serviceProvider; _consoleService = consoleService; + _runtimeInfo = runtimeInfo; _logger = logger; } @@ -34,7 +37,7 @@ public void Route(ApplicationModes applicationModes) case ApplicationModes.Interactive: { - _logger.Debug("Console selected"); + _logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected"); _nzbDroneServiceFactory.Start(); break; } diff --git a/src/NzbDrone.Host/SpinService.cs b/src/NzbDrone.Host/SpinService.cs index ae35590fdd..0604134b21 100644 --- a/src/NzbDrone.Host/SpinService.cs +++ b/src/NzbDrone.Host/SpinService.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using NLog; using NLog.Common; using NzbDrone.Common.EnvironmentInfo; @@ -28,7 +28,7 @@ public SpinService(IRuntimeInfo runtimeInfo, IProcessProvider processProvider, I public void Spin() { - while (_runtimeInfo.IsRunning) + while (!_runtimeInfo.IsExiting) { Thread.Sleep(1000); } diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/MonoPlatformInfoFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/MonoPlatformInfoFixture.cs new file mode 100644 index 0000000000..a1bcfd1cd1 --- /dev/null +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/MonoPlatformInfoFixture.cs @@ -0,0 +1,23 @@ +using System; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Mono.EnvironmentInfo; +using NzbDrone.Test.Common; + +namespace NzbDrone.Mono.Test.EnvironmentInfo +{ + [TestFixture] + [Platform("Mono")] + public class MonoPlatformInfoFixture : TestBase + { + [Test] + public void should_get_framework_version() + { + Subject.Version.Major.Should().BeOneOf(4, 5); + if (Subject.Version.Major == 4) + { + Subject.Version.Minor.Should().BeOneOf(0, 5, 6); + } + } + } +} diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs new file mode 100644 index 0000000000..383db510bd --- /dev/null +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/ReleaseFileVersionAdapterFixture.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Mono.Disk; +using NzbDrone.Mono.EnvironmentInfo.VersionAdapters; +using NzbDrone.Test.Common; + +namespace NzbDrone.Mono.Test.EnvironmentInfo +{ + [TestFixture] + [Platform("Mono")] + public class ReleaseFileVersionAdapterFixture : TestBase + { + [SetUp] + public void Setup() + { + Mocker.SetConstant(Mocker.Resolve()); + } + + [Test] + public void should_get_version_info() + { + var info = Subject.Read(); + info.FullName.Should().NotBeNullOrWhiteSpace(); + info.Name.Should().NotBeNullOrWhiteSpace(); + info.Version.Should().NotBeNullOrWhiteSpace(); + } + } +} diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs new file mode 100644 index 0000000000..56fe88bc0d --- /dev/null +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/MacOsVersionAdapterFixture.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Mono.EnvironmentInfo.VersionAdapters; +using NzbDrone.Test.Common; + +namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters +{ + [TestFixture] + public class MacOsVersionAdapterFixture : TestBase + { + [TestCase("10.8.0")] + [TestCase("10.8")] + [TestCase("10.8.1")] + [TestCase("10.11.20")] + public void should_get_version_info(string versionString) + { + var fileContent = File.ReadAllText(GetTestPath("Files/macOS/SystemVersion.plist")).Replace("10.0.0", versionString); + + const string plistPath = "/System/Library/CoreServices/SystemVersion.plist"; + + Mocker.GetMock() + .Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(true); + + Mocker.GetMock() + .Setup(c => c.GetFiles("/System/Library/CoreServices/", SearchOption.TopDirectoryOnly)) + .Returns(new[] { plistPath }); + + Mocker.GetMock() + .Setup(c => c.ReadAllText(plistPath)) + .Returns(fileContent); + + var versionName = Subject.Read(); + versionName.Version.Should().Be(versionString); + versionName.Name.Should().Be("macOS"); + versionName.FullName.Should().Be("macOS " + versionString); + } + + + [TestCase] + public void should_detect_server() + { + var fileContent = File.ReadAllText(GetTestPath("Files/macOS/SystemVersion.plist")); + + const string plistPath = "/System/Library/CoreServices/ServerVersion.plist"; + + Mocker.GetMock() + .Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(true); + + Mocker.GetMock() + .Setup(c => c.GetFiles("/System/Library/CoreServices/", SearchOption.TopDirectoryOnly)) + .Returns(new[] { plistPath }); + + Mocker.GetMock() + .Setup(c => c.ReadAllText(plistPath)) + .Returns(fileContent); + + var versionName = Subject.Read(); + versionName.Name.Should().Be("macOS Server"); + } + + [TestCase] + public void should_return_null_if_folder_doesnt_exist() + { + Mocker.GetMock() + .Setup(c => c.FolderExists("/System/Library/CoreServices/")).Returns(false); + + Subject.Read().Should().BeNull(); + + Mocker.GetMock() + .Verify(c => c.GetFiles(It.IsAny(), SearchOption.TopDirectoryOnly), Times.Never()); + } + } +} diff --git a/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs new file mode 100644 index 0000000000..798b99d8f7 --- /dev/null +++ b/src/NzbDrone.Mono.Test/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapterFixture.cs @@ -0,0 +1,82 @@ +using System.IO; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Mono.Disk; +using NzbDrone.Mono.EnvironmentInfo.VersionAdapters; +using NzbDrone.Test.Common; +using NzbDrone.Test.Common.Categories; + +namespace NzbDrone.Mono.Test.EnvironmentInfo.VersionAdapters +{ + [TestFixture] + public class ReleaseFileVersionAdapterFixture : TestBase + { + [Test] + [IntegrationTest] + [Platform("Mono")] + public void should_get_version_info_from_actual_linux() + { + Mocker.SetConstant(Mocker.Resolve()); + var info = Subject.Read(); + info.FullName.Should().NotBeNullOrWhiteSpace(); + info.Name.Should().NotBeNullOrWhiteSpace(); + info.Version.Should().NotBeNullOrWhiteSpace(); + } + + [Test] + public void should_return_null_if_etc_doestn_exist() + { + Mocker.GetMock().Setup(c => c.FolderExists("/etc/")).Returns(false); + Subject.Read().Should().BeNull(); + + Mocker.GetMock() + .Verify(c => c.GetFiles(It.IsAny(), SearchOption.TopDirectoryOnly), Times.Never()); + + Subject.Read().Should().BeNull(); + } + + + [Test] + public void should_return_null_if_release_file_doestn_exist() + { + Mocker.GetMock().Setup(c => c.FolderExists("/etc/")).Returns(true); + Subject.Read().Should().BeNull(); + + Mocker.GetMock() + .Setup(c => c.GetFiles(It.IsAny(), SearchOption.TopDirectoryOnly)).Returns(new string[0]); + + Subject.Read().Should().BeNull(); + } + + [Test] + public void should_detect_version() + { + Mocker.GetMock().Setup(c => c.FolderExists("/etc/")).Returns(true); + Subject.Read().Should().BeNull(); + + Mocker.GetMock() + .Setup(c => c.GetFiles(It.IsAny(), SearchOption.TopDirectoryOnly)).Returns(new[] + { + "/etc/lsb-release", + "/etc/os-release" + }); + + Mocker.GetMock() + .Setup(c => c.ReadAllText("/etc/lsb-release")) + .Returns(File.ReadAllText(GetTestPath("Files/linux/lsb-release"))); + + Mocker.GetMock() + .Setup(c => c.ReadAllText("/etc/os-release")) + .Returns(File.ReadAllText(GetTestPath("Files/linux/os-release"))); + + var version = Subject.Read(); + version.Should().NotBeNull(); + version.Name.Should().Be("ubuntu"); + version.Version.Should().Be("14.04"); + version.FullName.Should().Be("Ubuntu 14.04.5 LTS"); + + } + } +} diff --git a/src/NzbDrone.Mono.Test/Files/linux/lsb-release b/src/NzbDrone.Mono.Test/Files/linux/lsb-release new file mode 100644 index 0000000000..55eb268d55 --- /dev/null +++ b/src/NzbDrone.Mono.Test/Files/linux/lsb-release @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=14.04 +DISTRIB_CODENAME=trusty +DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS" \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/Files/linux/os-release b/src/NzbDrone.Mono.Test/Files/linux/os-release new file mode 100644 index 0000000000..724f6a79fc --- /dev/null +++ b/src/NzbDrone.Mono.Test/Files/linux/os-release @@ -0,0 +1,9 @@ +NAME="Ubuntu" +VERSION="14.04.5 LTS, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 14.04.5 LTS" +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/Files/macOS/SystemVersion.plist b/src/NzbDrone.Mono.Test/Files/macOS/SystemVersion.plist new file mode 100644 index 0000000000..253dd50755 --- /dev/null +++ b/src/NzbDrone.Mono.Test/Files/macOS/SystemVersion.plist @@ -0,0 +1,16 @@ + + + + + ProductBuildVersion + 16C68 + ProductCopyright + 1983-2016 Apple Inc. + ProductName + Mac OS X + ProductUserVisibleVersion + 10.0.0 + ProductVersion + 10.0.0 + + diff --git a/src/NzbDrone.Mono.Test/Files/synology/VERSION b/src/NzbDrone.Mono.Test/Files/synology/VERSION new file mode 100644 index 0000000000..029234d3c3 --- /dev/null +++ b/src/NzbDrone.Mono.Test/Files/synology/VERSION @@ -0,0 +1,8 @@ +majorversion="6" +minorversion="0" +productversion="6.0.2" +buildphase="hotfix" +buildnumber="8451" +smallfixnumber="7" +builddate="2016/12/20" +buildtime="05:11:44" \ No newline at end of file diff --git a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj index ad038409c4..b505154f1c 100644 --- a/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj +++ b/src/NzbDrone.Mono.Test/NzbDrone.Mono.Test.csproj @@ -81,10 +81,26 @@ + + + + + + Always + + + Always + + + Always + + + Always + diff --git a/src/NzbDrone.Mono/EnvironmentInfo/MonoPlatformInfo.cs b/src/NzbDrone.Mono/EnvironmentInfo/MonoPlatformInfo.cs new file mode 100644 index 0000000000..d1a228b654 --- /dev/null +++ b/src/NzbDrone.Mono/EnvironmentInfo/MonoPlatformInfo.cs @@ -0,0 +1,46 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Mono.EnvironmentInfo +{ + public class MonoPlatformInfo : PlatformInfo + { + private static readonly Regex VersionRegex = new Regex(@"(?<=\W|^)(?\d+\.\d+(\.\d+)?(\.\d+)?)(?=\W)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public override Version Version { get; } + + public MonoPlatformInfo(Logger logger) + { + var runTimeVersion = new Version(); + + 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) + { + runTimeVersion = new Version(versionMatch.Groups["version"].Value); + } + } + } + } + catch (Exception ex) + { + logger.Error(ex, "Unable to get mono version"); + } + + + Version = runTimeVersion; + } + } +} diff --git a/src/NzbDrone.Mono/EnvironmentInfo/MonoRuntimeProvider.cs b/src/NzbDrone.Mono/EnvironmentInfo/MonoRuntimeProvider.cs deleted file mode 100644 index b9884df778..0000000000 --- a/src/NzbDrone.Mono/EnvironmentInfo/MonoRuntimeProvider.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Reflection; -using NLog; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Mono.EnvironmentInfo -{ - public class MonoRuntimeProvider : RuntimeInfoBase - { - private readonly Logger _logger; - - public MonoRuntimeProvider(Common.IServiceProvider serviceProvider, Logger logger) - :base(serviceProvider, logger) - { - _logger = logger; - } - - public override string RuntimeVersion - { - get - { - try - { - var type = Type.GetType("Mono.Runtime"); - - if (type != null) - { - var displayName = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); - - if (displayName != null) - { - return displayName.Invoke(null, null).ToString(); - } - } - } - catch (Exception ex) - { - _logger.Error(ex, "Unable to get mono version: " + ex.Message); - } - - return string.Empty; - } - } - } -} diff --git a/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/IssueFileVersionAdapter.cs b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/IssueFileVersionAdapter.cs new file mode 100644 index 0000000000..7410a76d06 --- /dev/null +++ b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/IssueFileVersionAdapter.cs @@ -0,0 +1,52 @@ +using System.IO; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Mono.EnvironmentInfo.VersionAdapters +{ + public class IssueFileVersionAdapter : IOsVersionAdapter + { + private readonly IDiskProvider _diskProvider; + + public IssueFileVersionAdapter(IDiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public OsVersionModel Read() + { + if (!_diskProvider.FolderExists("/etc/")) + { + return null; + } + + var issueFile = _diskProvider.GetFiles("/etc/", SearchOption.TopDirectoryOnly).SingleOrDefault(c => c.EndsWith("/issue")); + + if (issueFile == null) + { + return null; + } + + var fileContent = _diskProvider.ReadAllText(issueFile); + + + // Ubuntu 14.04.5 LTS \n \l + // Ubuntu 16.04.1 LTS \n \l + + // Fedora/Centos + // Kernel \r on an \m (\l) + + // Arch Linux \r (\l) + // Debian GNU/Linux 8 \n \l + if (fileContent.Contains("Arch Linux")) + { + return new OsVersionModel("Arch", "1.0", "Arch Linux"); + } + + return null; + } + + public bool Enabled => OsInfo.IsLinux; + } +} diff --git a/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/MacOsVersionAdapter.cs b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/MacOsVersionAdapter.cs new file mode 100644 index 0000000000..c9521588e4 --- /dev/null +++ b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/MacOsVersionAdapter.cs @@ -0,0 +1,69 @@ +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Mono.EnvironmentInfo.VersionAdapters +{ + public class MacOsVersionAdapter : IOsVersionAdapter + { + private static readonly Regex DarwinVersionRegex = new Regex("(?10\\.\\d{1,2}\\.?\\d{0,2}?)<\\/string>", + RegexOptions.Compiled | + RegexOptions.IgnoreCase + ); + + private const string PLIST_DIR = "/System/Library/CoreServices/"; + + + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + public MacOsVersionAdapter(IDiskProvider diskProvider, Logger logger) + { + _diskProvider = diskProvider; + _logger = logger; + } + + public OsVersionModel Read() + { + var version = "10.0"; + + if (!_diskProvider.FolderExists(PLIST_DIR)) + { + _logger.Debug("Directory {0} doesn't exist", PLIST_DIR); + return null; + } + + var allFiles = _diskProvider.GetFiles(PLIST_DIR, SearchOption.TopDirectoryOnly); + + var versionFile = allFiles.SingleOrDefault(c => + c.EndsWith("SystemVersion.plist") || + c.EndsWith("ServerVersion.plist") + ); + + if (string.IsNullOrWhiteSpace(versionFile)) + { + _logger.Debug("Couldn't find version plist file in {0}", PLIST_DIR); + return null; + } + + var text = _diskProvider.ReadAllText(versionFile); + var match = DarwinVersionRegex.Match(text); + + + + if (match.Success) + { + version = match.Groups["version"].Value; + } + + var name = versionFile.Contains("Server") ? "macOS Server" : "macOS"; + + return new OsVersionModel(name, version); + } + + public bool Enabled => OsInfo.IsOsx; + } +} diff --git a/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapter.cs b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapter.cs new file mode 100644 index 0000000000..d0b5cb3bed --- /dev/null +++ b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/ReleaseFileVersionAdapter.cs @@ -0,0 +1,79 @@ +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Mono.EnvironmentInfo.VersionAdapters +{ + public class ReleaseFileVersionAdapter : IOsVersionAdapter + { + private readonly IDiskProvider _diskProvider; + + public ReleaseFileVersionAdapter(IDiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public OsVersionModel Read() + { + if (!_diskProvider.FolderExists("/etc/")) + { + return null; + } + + var releaseFiles = _diskProvider.GetFiles("/etc/", SearchOption.TopDirectoryOnly).Where(c => c.EndsWith("release")).ToList(); + + var name = "Linux"; + var fullName = ""; + var version = ""; + + bool success = false; + + foreach (var releaseFile in releaseFiles) + { + var fileContent = _diskProvider.ReadAllText(releaseFile); + var lines = Regex.Split(fileContent, "\r\n|\r|\n"); ; + + foreach (var line in lines) + { + var parts = line.Split('='); + if (parts.Length >= 2) + { + var key = parts[0]; + var value = parts[1]; + + if (!string.IsNullOrWhiteSpace(value)) + { + switch (key) + { + case "ID": + success = true; + name = value; + break; + case "PRETTY_NAME": + success = true; + fullName = value; + break; + case "VERSION_ID": + success = true; + version = value; + break; + } + } + } + } + } + + if (!success) + { + return null; + } + + return new OsVersionModel(name, version, fullName); + + } + + public bool Enabled => OsInfo.IsLinux; + } +} diff --git a/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/SynologyVersionAdapter.cs b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/SynologyVersionAdapter.cs new file mode 100644 index 0000000000..02bba34b11 --- /dev/null +++ b/src/NzbDrone.Mono/EnvironmentInfo/VersionAdapters/SynologyVersionAdapter.cs @@ -0,0 +1,78 @@ +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Mono.EnvironmentInfo.VersionAdapters +{ + public class SynologyVersionAdapter : IOsVersionAdapter + { + private readonly IDiskProvider _diskProvider; + private const string NAME = "DSM"; + private const string FULL_NAME = "Synology DSM"; + + + public SynologyVersionAdapter(IDiskProvider diskProvider) + { + _diskProvider = diskProvider; + } + + public OsVersionModel Read() + { + if (!_diskProvider.FolderExists("/etc.defaults/")) + { + return null; + } + + var versionFile = _diskProvider.GetFiles("/etc.defaults/", SearchOption.TopDirectoryOnly).SingleOrDefault(c => c.EndsWith("VERSION")); + + if (versionFile == null) + { + return null; + } + + var version = ""; + var major = ""; + var minor = "0"; + + var fileContent = _diskProvider.ReadAllText(versionFile); + var lines = Regex.Split(fileContent, "\r\n|\r|\n"); ; + + foreach (var line in lines) + { + var parts = line.Split('='); + if (parts.Length >= 2) + { + var key = parts[0]; + var value = parts[1].Trim('"'); + + if (!string.IsNullOrWhiteSpace(value)) + { + switch (key) + { + case "productversion": + version = value; + break; + case "majorversion": + major = value; + break; + case "minorversion": + minor = value; + break; + } + } + } + } + + if (string.IsNullOrWhiteSpace(version) && !string.IsNullOrWhiteSpace(major)) + { + version = $"{major}.{minor}"; + } + + return new OsVersionModel(NAME, version, $"{FULL_NAME} {version}"); + } + + public bool Enabled => OsInfo.IsLinux; + } +} diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index e50a3ee178..e41cf94762 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -74,9 +74,13 @@ - + + + + + diff --git a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs index 98faebac28..c78a27e406 100644 --- a/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs +++ b/src/NzbDrone.Test.Common/AutoMoq/AutoMoqer.cs @@ -1,5 +1,3 @@ - - using System; using System.Collections.Generic; using System.Diagnostics; @@ -151,7 +149,7 @@ private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityCont private Mock TheRegisteredMockForThisType(Type type) where T : class { - return (Mock)_registeredMocks.Where(x => x.Key == type).First().Value; + return (Mock)_registeredMocks.First(x => x.Key == type).Value; } private void CreateANewMockAndRegisterIt(Type type, MockBehavior behavior) where T : class @@ -190,4 +188,4 @@ private void RegisterPlatformLibrary(IUnityContainer container) #endregion } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Test.Common/TestBase.cs b/src/NzbDrone.Test.Common/TestBase.cs index 7efe432e95..14310f7f3f 100644 --- a/src/NzbDrone.Test.Common/TestBase.cs +++ b/src/NzbDrone.Test.Common/TestBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Threading; using FluentAssertions; @@ -133,7 +133,7 @@ protected void WindowsOnly() protected void MonoOnly() { - if (OsInfo.IsWindows) + if (!PlatformInfo.IsMono) { throw new IgnoreException("mono specific test"); } diff --git a/src/NzbDrone.Update/UpdateContainerBuilder.cs b/src/NzbDrone.Update/UpdateContainerBuilder.cs index aeaa130ad4..47fcbe5981 100644 --- a/src/NzbDrone.Update/UpdateContainerBuilder.cs +++ b/src/NzbDrone.Update/UpdateContainerBuilder.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http.Dispatchers; @@ -7,7 +7,7 @@ namespace NzbDrone.Update { public class UpdateContainerBuilder : ContainerBuilderBase { - private UpdateContainerBuilder(IStartupContext startupContext, string[] assemblies) + private UpdateContainerBuilder(IStartupContext startupContext, List assemblies) : base(startupContext, assemblies) { Container.Register(); @@ -17,22 +17,10 @@ public static IContainer Build(IStartupContext startupContext) { var assemblies = new List { - "Radarr.Update", - "NzbDrone.Common" + "Radarr.Update" }; - if (OsInfo.IsWindows) - { - assemblies.Add("NzbDrone.Windows"); - } - - else - { - assemblies.Add("NzbDrone.Mono"); - } - - return new UpdateContainerBuilder(startupContext, assemblies.ToArray()).Container; + return new UpdateContainerBuilder(startupContext, assemblies).Container; } } } - \ No newline at end of file diff --git a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs index e7507b25de..9d2df5af24 100644 --- a/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs +++ b/src/NzbDrone.Update/UpdateEngine/DetectApplicationType.cs @@ -24,7 +24,7 @@ public AppType GetAppType() { if (OsInfo.IsNotWindows) { - //Tehcnically its the console, but its been renamed for mono (Linux/OS X) + //Tehcnically it is the console, but it has been renamed for mono (Linux/OS X) return AppType.Normal; } @@ -42,4 +42,4 @@ public AppType GetAppType() return AppType.Normal; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Windows.Test/EnvironmentInfo/DotNetPlatformInfoFixture.cs b/src/NzbDrone.Windows.Test/EnvironmentInfo/DotNetPlatformInfoFixture.cs new file mode 100644 index 0000000000..7bfeb70e19 --- /dev/null +++ b/src/NzbDrone.Windows.Test/EnvironmentInfo/DotNetPlatformInfoFixture.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Windows.EnvironmentInfo; + +namespace NzbDrone.Windows.Test.EnvironmentInfo +{ + [TestFixture] + [Platform("Win")] + public class DotNetPlatformInfoFixture : TestBase + { + [Test] + public void should_get_framework_version() + { + Subject.Version.Major.Should().Be(4); + Subject.Version.Minor.Should().BeOneOf(0, 5, 6, 7, 8); + } + } +} diff --git a/src/NzbDrone.Windows.Test/EnvironmentInfo/WindowsVersionInfoFixture.cs b/src/NzbDrone.Windows.Test/EnvironmentInfo/WindowsVersionInfoFixture.cs new file mode 100644 index 0000000000..28ad1728db --- /dev/null +++ b/src/NzbDrone.Windows.Test/EnvironmentInfo/WindowsVersionInfoFixture.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Windows.EnvironmentInfo; + +namespace NzbDrone.Windows.Test.EnvironmentInfo +{ + [TestFixture] + [Platform("Win")] + public class WindowsVersionInfoFixture : TestBase + { + [Test] + public void should_get_windows_version() + { + var info = Subject.Read(); + info.Version.Should().NotBeNullOrWhiteSpace(); + info.Name.Should().Contain("Windows"); + info.FullName.Should().Contain("Windows"); + info.FullName.Should().Contain("NT"); + info.FullName.Should().Contain(info.Version); + } + } +} diff --git a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj index 942eb4dd05..fc9dba12d0 100644 --- a/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj +++ b/src/NzbDrone.Windows.Test/NzbDrone.Windows.Test.csproj @@ -73,6 +73,8 @@ + + diff --git a/src/NzbDrone.Windows/EnvironmentInfo/DotNetPlatformInfo.cs b/src/NzbDrone.Windows/EnvironmentInfo/DotNetPlatformInfo.cs new file mode 100644 index 0000000000..4e6e5ed3d8 --- /dev/null +++ b/src/NzbDrone.Windows/EnvironmentInfo/DotNetPlatformInfo.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Win32; +using NLog; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Windows.EnvironmentInfo +{ + public class DotNetPlatformInfo : PlatformInfo + { + private readonly Logger _logger; + + public DotNetPlatformInfo(Logger logger) + { + _logger = logger; + var version = GetFrameworkVersion(); + Environment.SetEnvironmentVariable("RUNTIME_VERSION", version.ToString()); + Version = version; + } + + public override Version Version { get; } + + private Version GetFrameworkVersion() + { + try + { + const string subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; + using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey)) + { + if (ndpKey == null) + { + return new Version(4, 0); + } + + var releaseKey = (int)ndpKey.GetValue("Release"); + + if (releaseKey >= 528040) + { + return new Version(4, 8, 0); + } + if (releaseKey >= 461808) + { + return new Version(4, 7, 2); + } + if (releaseKey >= 461308) + { + return new Version(4, 7, 1); + } + if (releaseKey >= 460798) + { + return new Version(4, 7); + } + if (releaseKey >= 394802) + { + return new Version(4, 6, 2); + } + if (releaseKey >= 394254) + { + return new Version(4, 6, 1); + } + if (releaseKey >= 393295) + { + return new Version(4, 6); + } + if (releaseKey >= 379893) + { + return new Version(4, 5, 2); + } + if (releaseKey >= 378675) + { + return new Version(4, 5, 1); + } + if (releaseKey >= 378389) + { + return new Version(4, 5); + } + } + } + catch (Exception e) + { + _logger.Error(e, "Couldnt get .NET framework version"); + } + + return new Version(4, 0); + } + } +} diff --git a/src/NzbDrone.Windows/EnvironmentInfo/DotNetRuntimeProvider.cs b/src/NzbDrone.Windows/EnvironmentInfo/DotNetRuntimeProvider.cs deleted file mode 100644 index 8e0330f1fe..0000000000 --- a/src/NzbDrone.Windows/EnvironmentInfo/DotNetRuntimeProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using NLog; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Windows.EnvironmentInfo -{ - public class DotNetRuntimeProvider : RuntimeInfoBase - { - public DotNetRuntimeProvider(Common.IServiceProvider serviceProvider, Logger logger) - : base(serviceProvider, logger) - { - } - - public override string RuntimeVersion => Environment.Version.ToString(); - } -} diff --git a/src/NzbDrone.Windows/EnvironmentInfo/WindowsVersionInfo.cs b/src/NzbDrone.Windows/EnvironmentInfo/WindowsVersionInfo.cs new file mode 100644 index 0000000000..17b37c091d --- /dev/null +++ b/src/NzbDrone.Windows/EnvironmentInfo/WindowsVersionInfo.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.Win32; +using NLog; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Windows.EnvironmentInfo +{ + public class WindowsVersionInfo : IOsVersionAdapter + { + private readonly Logger _logger; + public bool Enabled => OsInfo.IsWindows; + + public WindowsVersionInfo(Logger logger) + { + _logger = logger; + } + + public OsVersionModel Read() + { + var windowsServer = IsServer(); + var osName = windowsServer ? "Windows Server" : "Windows"; + return new OsVersionModel(osName, Environment.OSVersion.Version.ToString(), Environment.OSVersion.VersionString); + } + + private bool IsServer() + { + try + { + const string subkey = @"Software\Microsoft\Windows NT\CurrentVersion"; + var openSubKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(subkey); + if (openSubKey != null) + { + var productName = openSubKey.GetValue("ProductName").ToString(); + + if (productName.ToLower().Contains("server")) + { + return true; + } + } + } + catch (Exception e) + { + _logger.Error(e, "Couldn't detect if running Windows Server"); + } + + return false; + } + } +} diff --git a/src/NzbDrone.Windows/NzbDrone.Windows.csproj b/src/NzbDrone.Windows/NzbDrone.Windows.csproj index 66841fa317..ee3ec4f409 100644 --- a/src/NzbDrone.Windows/NzbDrone.Windows.csproj +++ b/src/NzbDrone.Windows/NzbDrone.Windows.csproj @@ -67,7 +67,8 @@ - + +