mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-14 02:37:08 -07:00
New: Switch to ASPNetCore Kestrel and SignalR
This commit is contained in:
parent
f136449573
commit
8a938f6856
47 changed files with 847 additions and 1288 deletions
BIN
src/Libraries/Mono/System.Diagnostics.Tracing.dll
Normal file
BIN
src/Libraries/Mono/System.Diagnostics.Tracing.dll
Normal file
Binary file not shown.
|
@ -2,7 +2,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
@ -44,7 +43,7 @@ namespace Lidarr.Api.V1.Config
|
|||
|
||||
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslPort).NotEqual(c => c.Port).When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
|
||||
SharedValidator.RuleFor(c => c.SslCertPath).NotEmpty().When(c => c.EnableSsl);
|
||||
|
||||
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
|
||||
SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script);
|
||||
|
|
|
@ -22,7 +22,8 @@ namespace Lidarr.Api.V1.Config
|
|||
public string ConsoleLogLevel { get; set; }
|
||||
public string Branch { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public string SslCertHash { get; set; }
|
||||
public string SslCertPath { get; set; }
|
||||
public string SslCertPassword { get; set; }
|
||||
public string UrlBase { get; set; }
|
||||
public bool UpdateAutomatically { get; set; }
|
||||
public UpdateMechanism UpdateMechanism { get; set; }
|
||||
|
@ -61,7 +62,8 @@ namespace Lidarr.Api.V1.Config
|
|||
ConsoleLogLevel = model.ConsoleLogLevel,
|
||||
Branch = model.Branch,
|
||||
ApiKey = model.ApiKey,
|
||||
SslCertHash = model.SslCertHash,
|
||||
SslCertPath = model.SslCertPath,
|
||||
SslCertPassword = model.SslCertPassword,
|
||||
UrlBase = model.UrlBase,
|
||||
UpdateAutomatically = model.UpdateAutomatically,
|
||||
UpdateMechanism = model.UpdateMechanism,
|
||||
|
|
|
@ -2,14 +2,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
|
@ -37,7 +35,8 @@ namespace NzbDrone.Core.Configuration
|
|||
bool FilterSentryEvents { get; }
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
string SslCertHash { get; }
|
||||
string SslCertPath { get; }
|
||||
string SslCertPassword { get; }
|
||||
string UrlBase { get; }
|
||||
string UiFolder { get; }
|
||||
bool UpdateAutomatically { get; }
|
||||
|
@ -54,7 +53,6 @@ namespace NzbDrone.Core.Configuration
|
|||
private readonly ICached<string> _cache;
|
||||
|
||||
private readonly string _configFile;
|
||||
private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly object Mutex = new object();
|
||||
|
||||
|
@ -99,12 +97,6 @@ namespace NzbDrone.Core.Configuration
|
|||
continue;
|
||||
}
|
||||
|
||||
if (configValue.Key.Equals("SslCertHash", StringComparison.InvariantCultureIgnoreCase) && configValue.Value.ToString().IsNotNullOrWhiteSpace())
|
||||
{
|
||||
SetValue(configValue.Key.FirstCharToUpper(), HiddenCharacterRegex.Replace(configValue.Value.ToString(), string.Empty));
|
||||
continue;
|
||||
}
|
||||
|
||||
object currentValue;
|
||||
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
||||
if (currentValue == null) continue;
|
||||
|
@ -183,8 +175,8 @@ namespace NzbDrone.Core.Configuration
|
|||
public string LogLevel => GetValue("LogLevel", "info");
|
||||
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
|
||||
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
|
||||
|
||||
public string SslCertHash => GetValue("SslCertHash", "");
|
||||
public string SslCertPath => GetValue("SslCertPath", "");
|
||||
public string SslCertPassword => GetValue("SslCertPassword", "");
|
||||
|
||||
public string UrlBase
|
||||
{
|
||||
|
|
|
@ -15,6 +15,8 @@ using System.Linq;
|
|||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.SignalR;
|
||||
using Moq;
|
||||
|
||||
namespace NzbDrone.App.Test
|
||||
{
|
||||
|
@ -31,6 +33,10 @@ namespace NzbDrone.App.Test
|
|||
_container = MainAppContainerBuilder.BuildContainer(args);
|
||||
|
||||
_container.Register<IMainDatabase>(new MainDatabase(null));
|
||||
|
||||
// set up a dummy broadcaster to allow tests to resolve
|
||||
var mockBroadcaster = new Mock<IBroadcastSignalRMessage>();
|
||||
_container.Register<IBroadcastSignalRMessage>(mockBroadcaster.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace NzbDrone.App.Test
|
|||
|
||||
Subject.Route(ApplicationModes.Interactive);
|
||||
|
||||
Mocker.GetMock<INzbDroneServiceFactory>().Verify(c => c.Start(), Times.Once());
|
||||
Mocker.GetMock<INzbDroneConsoleFactory>().Verify(c => c.Start(), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Processes;
|
||||
|
||||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface INetshProvider
|
||||
{
|
||||
ProcessOutput Run(string arguments);
|
||||
}
|
||||
|
||||
public class NetshProvider : INetshProvider
|
||||
{
|
||||
private readonly IProcessProvider _processProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NetshProvider(IProcessProvider processProvider, Logger logger)
|
||||
{
|
||||
_processProvider = processProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ProcessOutput Run(string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var output = _processProvider.StartAndCapture("netsh.exe", arguments);
|
||||
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Error executing netsh with arguments: " + arguments);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface ISslAdapter
|
||||
{
|
||||
void Register();
|
||||
}
|
||||
|
||||
public class SslAdapter : ISslAdapter
|
||||
{
|
||||
private const string APP_ID = "87CAF14C-6750-42DB-B6A0-3BB826315E91";
|
||||
private static readonly Regex CertificateHashRegex = new Regex(@"^\s+(?:Certificate Hash\s+:\s+)(?<hash>\w+)", RegexOptions.Compiled);
|
||||
|
||||
private readonly INetshProvider _netshProvider;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SslAdapter(INetshProvider netshProvider, IConfigFileProvider configFileProvider, Logger logger)
|
||||
{
|
||||
_netshProvider = netshProvider;
|
||||
_configFileProvider = configFileProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Register()
|
||||
{
|
||||
if (!_configFileProvider.EnableSsl) return;
|
||||
if (IsRegistered()) return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_configFileProvider.SslCertHash))
|
||||
{
|
||||
_logger.Warn("Unable to enable SSL, SSL Cert Hash is required");
|
||||
return;
|
||||
}
|
||||
|
||||
var arguments = string.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}",
|
||||
_configFileProvider.SslPort,
|
||||
_configFileProvider.SslCertHash,
|
||||
APP_ID);
|
||||
|
||||
//TODO: Validate that the cert was added properly, invisible spaces FTL
|
||||
_netshProvider.Run(arguments);
|
||||
}
|
||||
|
||||
private bool IsRegistered()
|
||||
{
|
||||
var ipPort = "0.0.0.0:" + _configFileProvider.SslPort;
|
||||
var arguments = string.Format("http show sslcert ipport={0}", ipPort);
|
||||
|
||||
var output = _netshProvider.Run(arguments);
|
||||
|
||||
if (output == null || !output.Standard.Any()) return false;
|
||||
|
||||
var hashLine = output.Standard.SingleOrDefault(line => CertificateHashRegex.IsMatch(line.Content));
|
||||
|
||||
if (hashLine != null)
|
||||
{
|
||||
var match = CertificateHashRegex.Match(hashLine.Content);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (match.Groups["hash"].Value != _configFileProvider.SslCertHash)
|
||||
{
|
||||
Unregister();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output.Standard.Any(line => line.Content.Contains(ipPort));
|
||||
}
|
||||
|
||||
private void Unregister()
|
||||
{
|
||||
var ipPort = "0.0.0.0:" + _configFileProvider.SslPort;
|
||||
var arguments = string.Format("http delete sslcert ipport={0}", ipPort);
|
||||
|
||||
_netshProvider.Run(arguments);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public class UrlAcl
|
||||
{
|
||||
public string Scheme { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string UrlBase { get; set; }
|
||||
|
||||
public string Url => string.Format("{0}://{1}:{2}/{3}", Scheme, Address, Port, UrlBase);
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface IUrlAclAdapter
|
||||
{
|
||||
void ConfigureUrls();
|
||||
List<string> Urls { get; }
|
||||
}
|
||||
|
||||
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<string> Urls
|
||||
{
|
||||
get
|
||||
{
|
||||
return InternalUrls.Select(c => c.Url).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<UrlAcl> InternalUrls { get; }
|
||||
private List<UrlAcl> RegisteredUrls { get; set; }
|
||||
|
||||
private static readonly Regex UrlAclRegex = new Regex(@"(?<scheme>https?)\:\/\/(?<address>.+?)\:(?<port>\d+)/(?<urlbase>.+)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
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<UrlAcl>();
|
||||
RegisteredUrls = new List<UrlAcl>();
|
||||
}
|
||||
|
||||
public void ConfigureUrls()
|
||||
{
|
||||
var enableSsl = _configFileProvider.EnableSsl;
|
||||
var port = _configFileProvider.Port;
|
||||
var sslPort = _configFileProvider.SslPort;
|
||||
|
||||
if (enableSsl && sslPort == port)
|
||||
{
|
||||
throw new LidarrStartupException("Cannot use the same port for HTTP and HTTPS. Port {0}", port);
|
||||
}
|
||||
|
||||
if (RegisteredUrls.Empty())
|
||||
{
|
||||
GetRegisteredUrls();
|
||||
}
|
||||
|
||||
var localHostHttpUrls = BuildUrlAcls("http", "localhost", port);
|
||||
var interfaceHttpUrls = BuildUrlAcls("http", _configFileProvider.BindAddress, port);
|
||||
|
||||
var localHostHttpsUrls = BuildUrlAcls("https", "localhost", sslPort);
|
||||
var interfaceHttpsUrls = BuildUrlAcls("https", _configFileProvider.BindAddress, sslPort);
|
||||
|
||||
if (!enableSsl)
|
||||
{
|
||||
localHostHttpsUrls.Clear();
|
||||
interfaceHttpsUrls.Clear();
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows && !_runtimeInfo.IsAdmin)
|
||||
{
|
||||
var httpUrls = interfaceHttpUrls.All(IsRegistered) ? interfaceHttpUrls : localHostHttpUrls;
|
||||
var httpsUrls = interfaceHttpsUrls.All(IsRegistered) ? interfaceHttpsUrls : localHostHttpsUrls;
|
||||
|
||||
InternalUrls.AddRange(httpUrls);
|
||||
InternalUrls.AddRange(httpsUrls);
|
||||
|
||||
if (_configFileProvider.BindAddress != "*")
|
||||
{
|
||||
if (httpUrls.None(c => c.Address.Equals("localhost")))
|
||||
{
|
||||
InternalUrls.AddRange(localHostHttpUrls);
|
||||
}
|
||||
|
||||
if (httpsUrls.None(c => c.Address.Equals("localhost")))
|
||||
{
|
||||
InternalUrls.AddRange(localHostHttpsUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalUrls.AddRange(interfaceHttpUrls);
|
||||
InternalUrls.AddRange(interfaceHttpsUrls);
|
||||
|
||||
//Register localhost URLs so the IP Address doesn't need to be used from the local system
|
||||
if (_configFileProvider.BindAddress != "*")
|
||||
{
|
||||
InternalUrls.AddRange(localHostHttpUrls);
|
||||
InternalUrls.AddRange(localHostHttpsUrls);
|
||||
}
|
||||
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
RefreshRegistration();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshRegistration()
|
||||
{
|
||||
var osVersion = new Version(_osInfo.Version);
|
||||
if (osVersion.Major < 6) return;
|
||||
|
||||
foreach (var urlAcl in InternalUrls)
|
||||
{
|
||||
if (IsRegistered(urlAcl) || urlAcl.Address.Equals("localhost")) continue;
|
||||
|
||||
RemoveSimilar(urlAcl);
|
||||
RegisterUrl(urlAcl);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsRegistered(UrlAcl urlAcl)
|
||||
{
|
||||
return RegisteredUrls.Any(c => c.Scheme == urlAcl.Scheme &&
|
||||
c.Address == urlAcl.Address &&
|
||||
c.Port == urlAcl.Port &&
|
||||
c.UrlBase == urlAcl.UrlBase);
|
||||
}
|
||||
|
||||
private void GetRegisteredUrls()
|
||||
{
|
||||
if (OsInfo.IsNotWindows)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (RegisteredUrls.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var arguments = string.Format("http show urlacl");
|
||||
var output = _netshProvider.Run(arguments);
|
||||
|
||||
if (output == null || !output.Standard.Any()) return;
|
||||
|
||||
RegisteredUrls = output.Standard.Select(line =>
|
||||
{
|
||||
var match = UrlAclRegex.Match(line.Content);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return new UrlAcl
|
||||
{
|
||||
Scheme = match.Groups["scheme"].Value,
|
||||
Address = match.Groups["address"].Value,
|
||||
Port = Convert.ToInt32(match.Groups["port"].Value),
|
||||
UrlBase = match.Groups["urlbase"].Value.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}).Where(r => r != null).ToList();
|
||||
}
|
||||
|
||||
private void RegisterUrl(UrlAcl urlAcl)
|
||||
{
|
||||
var arguments = string.Format("http add urlacl {0} sddl=D:(A;;GX;;;S-1-1-0)", urlAcl.Url);
|
||||
_netshProvider.Run(arguments);
|
||||
}
|
||||
|
||||
private void RemoveSimilar(UrlAcl urlAcl)
|
||||
{
|
||||
var similar = RegisteredUrls.Where(c => c.Scheme == urlAcl.Scheme &&
|
||||
InternalUrls.None(x => x.Address == c.Address) &&
|
||||
c.Port == urlAcl.Port &&
|
||||
c.UrlBase == urlAcl.UrlBase);
|
||||
|
||||
foreach (var s in similar)
|
||||
{
|
||||
UnregisterUrl(s);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterUrl(UrlAcl urlAcl)
|
||||
{
|
||||
_logger.Trace("Removing URL ACL {0}", urlAcl.Url);
|
||||
|
||||
var arguments = string.Format("http delete urlacl {0}", urlAcl.Url);
|
||||
_netshProvider.Run(arguments);
|
||||
}
|
||||
|
||||
private List<UrlAcl> BuildUrlAcls(string scheme, string address, int port)
|
||||
{
|
||||
var urlAcls = new List<UrlAcl>();
|
||||
var urlBase = _configFileProvider.UrlBase;
|
||||
|
||||
if (urlBase.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
urlAcls.Add(new UrlAcl
|
||||
{
|
||||
Scheme = scheme,
|
||||
Address = address,
|
||||
Port = port,
|
||||
UrlBase = urlBase.Trim('/') + "/"
|
||||
});
|
||||
}
|
||||
|
||||
urlAcls.Add(new UrlAcl
|
||||
{
|
||||
Scheme = scheme,
|
||||
Address = address,
|
||||
Port = port,
|
||||
UrlBase = string.Empty
|
||||
});
|
||||
|
||||
return urlAcls;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,17 +7,54 @@ using NzbDrone.Core.Configuration;
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Host.Owin;
|
||||
|
||||
namespace NzbDrone.Host
|
||||
{
|
||||
public interface INzbDroneServiceFactory
|
||||
{
|
||||
ServiceBase Build();
|
||||
void Start();
|
||||
}
|
||||
|
||||
public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory, IHandle<ApplicationShutdownRequested>
|
||||
public interface INzbDroneConsoleFactory
|
||||
{
|
||||
void Start();
|
||||
void Shutdown();
|
||||
}
|
||||
|
||||
public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory
|
||||
{
|
||||
private readonly INzbDroneConsoleFactory _consoleFactory;
|
||||
|
||||
public NzbDroneServiceFactory(INzbDroneConsoleFactory consoleFactory)
|
||||
{
|
||||
_consoleFactory = consoleFactory;
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
_consoleFactory.Start();
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
_consoleFactory.Shutdown();
|
||||
}
|
||||
|
||||
public ServiceBase Build()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class DummyNzbDroneServiceFactory : INzbDroneServiceFactory
|
||||
{
|
||||
public ServiceBase Build()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class NzbDroneConsoleFactory : INzbDroneConsoleFactory, IHandle<ApplicationShutdownRequested>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
|
@ -27,7 +64,7 @@ namespace NzbDrone.Host
|
|||
private readonly IContainer _container;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NzbDroneServiceFactory(IConfigFileProvider configFileProvider,
|
||||
public NzbDroneConsoleFactory(IConfigFileProvider configFileProvider,
|
||||
IHostController hostController,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
IStartupContext startupContext,
|
||||
|
@ -44,11 +81,6 @@ namespace NzbDrone.Host
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override void OnStart(string[] args)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (OsInfo.IsNotWindows)
|
||||
|
@ -69,17 +101,7 @@ namespace NzbDrone.Host
|
|||
_container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
|
||||
}
|
||||
|
||||
protected override void OnStop()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
public ServiceBase Build()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
private void Shutdown()
|
||||
public void Shutdown()
|
||||
{
|
||||
_logger.Info("Attempting to stop application.");
|
||||
_hostController.StopServer();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
namespace NzbDrone.Host.Owin
|
||||
namespace NzbDrone.Host
|
||||
{
|
||||
public interface IHostController
|
||||
{
|
||||
void StartServer();
|
||||
void StopServer();
|
||||
}
|
||||
}
|
||||
}
|
7
src/NzbDrone.Host/IRemoteAccessAdapter.cs
Normal file
7
src/NzbDrone.Host/IRemoteAccessAdapter.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface IRemoteAccessAdapter
|
||||
{
|
||||
void MakeAccessible(bool passive);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,12 @@
|
|||
<TargetFrameworks>net462</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.SignalR.SelfHost" Version="2.4.1" />
|
||||
<PackageReference Include="Nancy.Owin" Version="2.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="2.2.0" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.5.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" />
|
||||
|
@ -14,10 +18,12 @@
|
|||
<ProjectReference Include="..\Lidarr.Http\Lidarr.Http.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="Interop.NetFwTypeLib">
|
||||
<HintPath>..\Libraries\Interop.NetFwTypeLib.dll</HintPath>
|
||||
<EmbedInteropTypes>True</EmbedInteropTypes>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -27,9 +27,18 @@ namespace NzbDrone.Host
|
|||
private MainAppContainerBuilder(StartupContext args, List<string> assemblies)
|
||||
: base(args, assemblies)
|
||||
{
|
||||
AutoRegisterImplementations<NzbDronePersistentConnection>();
|
||||
AutoRegisterImplementations<MessageHub>();
|
||||
|
||||
Container.Register<INancyBootstrapper, LidarrBootstrapper>();
|
||||
|
||||
if (OsInfo.IsWindows)
|
||||
{
|
||||
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Container.Register<INzbDroneServiceFactory, DummyNzbDroneServiceFactory>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
using Owin;
|
||||
|
||||
namespace NzbDrone.Host.Owin.MiddleWare
|
||||
{
|
||||
public interface IOwinMiddleWare
|
||||
{
|
||||
int Order { get; }
|
||||
void Attach(IAppBuilder appBuilder);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Owin;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using Owin;
|
||||
|
||||
namespace NzbDrone.Host.Owin.MiddleWare
|
||||
{
|
||||
public class NzbDroneVersionMiddleWare : IOwinMiddleWare
|
||||
{
|
||||
public int Order => 0;
|
||||
|
||||
public void Attach(IAppBuilder appBuilder)
|
||||
{
|
||||
appBuilder.Use(typeof(AddApplicationVersionHeader));
|
||||
}
|
||||
}
|
||||
|
||||
public class AddApplicationVersionHeader : OwinMiddleware
|
||||
{
|
||||
private readonly KeyValuePair<string, string[]> _versionHeader;
|
||||
|
||||
public AddApplicationVersionHeader(OwinMiddleware next)
|
||||
: base(next)
|
||||
{
|
||||
_versionHeader = new KeyValuePair<string, string[]>("X-ApplicationVersion",
|
||||
new[] { BuildInfo.Version.ToString() });
|
||||
}
|
||||
public override async Task Invoke(IOwinContext context)
|
||||
{
|
||||
context.Response.Headers.Add(_versionHeader);
|
||||
await Next.Invoke(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.SignalR;
|
||||
using Owin;
|
||||
|
||||
namespace NzbDrone.Host.Owin.MiddleWare
|
||||
{
|
||||
public class SignalRMiddleWare : IOwinMiddleWare
|
||||
{
|
||||
public int Order => 1;
|
||||
|
||||
public SignalRMiddleWare(IContainer container)
|
||||
{
|
||||
SignalRDependencyResolver.Register(container);
|
||||
SignalRJsonSerializer.Register();
|
||||
|
||||
// Note there are some important timeouts involved here:
|
||||
// nginx has a default 60 sec proxy_read_timeout, this means the connection will be terminated if the server doesn't send anything within that time.
|
||||
// Previously we lowered the ConnectionTimeout from 110s to 55s to remedy that, however all we should've done is set an appropriate KeepAlive.
|
||||
// By default KeepAlive is 1/3rd of the DisconnectTimeout, which we set incredibly high 5 years ago, resulting in KeepAlive being 1 minute.
|
||||
// So when adjusting these values in the future, please keep that all in mind.
|
||||
GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(110);
|
||||
GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromSeconds(180);
|
||||
GlobalHost.Configuration.KeepAlive = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public void Attach(IAppBuilder appBuilder)
|
||||
{
|
||||
appBuilder.MapSignalR("/signalr", typeof(NzbDronePersistentConnection), new ConnectionConfiguration ());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Host.Owin
|
||||
{
|
||||
public class NlogTextWriter : TextWriter
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NlogTextWriter(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => Encoding.Default;
|
||||
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
Write(buffer);
|
||||
}
|
||||
public override void Write(char[] buffer)
|
||||
{
|
||||
Write(new string(buffer));
|
||||
}
|
||||
|
||||
public override void Write(string value)
|
||||
{
|
||||
_logger.Log(GetLogLevel(value), value);
|
||||
}
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
_logger.Trace(value);
|
||||
}
|
||||
|
||||
private LogLevel GetLogLevel(string value)
|
||||
{
|
||||
var lower = value.ToLowerInvariant();
|
||||
|
||||
if (!lower.Contains("error"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
|
||||
if (lower.Contains("sqlite"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
|
||||
if (lower.Contains("\"errors\":null"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
|
||||
if (lower.Contains("signalr"))
|
||||
{
|
||||
if (lower.Contains("an operation was attempted on a nonexistent network connection"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
|
||||
if (lower.Contains("the network connection was aborted by the local system"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
|
||||
if (lower.Contains("the socket has been shut down"))
|
||||
{
|
||||
return LogLevel.Trace;
|
||||
}
|
||||
}
|
||||
|
||||
return LogLevel.Error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Host.AccessControl;
|
||||
|
||||
namespace NzbDrone.Host.Owin
|
||||
{
|
||||
public class OwinHostController : IHostController
|
||||
{
|
||||
private readonly IOwinAppFactory _owinAppFactory;
|
||||
private readonly IRemoteAccessAdapter _remoteAccessAdapter;
|
||||
private readonly IUrlAclAdapter _urlAclAdapter;
|
||||
private readonly Logger _logger;
|
||||
private IDisposable _owinApp;
|
||||
|
||||
public OwinHostController(
|
||||
IOwinAppFactory owinAppFactory,
|
||||
IRemoteAccessAdapter remoteAccessAdapter,
|
||||
IUrlAclAdapter urlAclAdapter,
|
||||
Logger logger)
|
||||
{
|
||||
_owinAppFactory = owinAppFactory;
|
||||
_remoteAccessAdapter = remoteAccessAdapter;
|
||||
_urlAclAdapter = urlAclAdapter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void StartServer()
|
||||
{
|
||||
_remoteAccessAdapter.MakeAccessible(true);
|
||||
|
||||
_logger.Info("Listening on the following URLs:");
|
||||
foreach (var url in _urlAclAdapter.Urls)
|
||||
{
|
||||
_logger.Info(" {0}", url);
|
||||
}
|
||||
|
||||
_owinApp = _owinAppFactory.CreateApp(_urlAclAdapter.Urls);
|
||||
}
|
||||
|
||||
public void StopServer()
|
||||
{
|
||||
if (_owinApp == null) return;
|
||||
|
||||
_logger.Info("Attempting to stop OWIN host");
|
||||
_owinApp.Dispose();
|
||||
_owinApp = null;
|
||||
_logger.Info("Host has stopped");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Microsoft.Owin.Hosting;
|
||||
using Microsoft.Owin.Hosting.Engine;
|
||||
using Microsoft.Owin.Hosting.Services;
|
||||
using Microsoft.Owin.Hosting.Tracing;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Host.Owin.MiddleWare;
|
||||
using Owin;
|
||||
|
||||
namespace NzbDrone.Host.Owin
|
||||
{
|
||||
public interface IOwinAppFactory
|
||||
{
|
||||
IDisposable CreateApp(List<string> urls);
|
||||
}
|
||||
|
||||
public class OwinAppFactory : IOwinAppFactory
|
||||
{
|
||||
private readonly IEnumerable<IOwinMiddleWare> _owinMiddleWares;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public OwinAppFactory(IEnumerable<IOwinMiddleWare> owinMiddleWares, IConfigFileProvider configFileProvider, Logger logger)
|
||||
{
|
||||
_owinMiddleWares = owinMiddleWares;
|
||||
_configFileProvider = configFileProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IDisposable CreateApp(List<string> urls)
|
||||
{
|
||||
var services = CreateServiceFactory();
|
||||
var engine = services.GetService<IHostingEngine>();
|
||||
|
||||
var options = new StartOptions()
|
||||
{
|
||||
ServerFactory = "Microsoft.Owin.Host.HttpListener"
|
||||
};
|
||||
|
||||
urls.ForEach(options.Urls.Add);
|
||||
|
||||
var context = new StartContext(options) { Startup = BuildApp };
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
return engine.Start(context);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
if (ex.InnerException == null)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
if (ex.InnerException is HttpListenerException)
|
||||
{
|
||||
throw new PortInUseException("Port {0} is already in use, please ensure {1} is not already running.", ex, _configFileProvider.Port, BuildInfo.AppName);
|
||||
}
|
||||
|
||||
throw ex.InnerException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void BuildApp(IAppBuilder appBuilder)
|
||||
{
|
||||
appBuilder.Properties["host.AppName"] = BuildInfo.AppName;
|
||||
|
||||
foreach (var middleWare in _owinMiddleWares.OrderBy(c => c.Order))
|
||||
{
|
||||
_logger.Debug("Attaching {0} to host", middleWare.GetType().Name);
|
||||
middleWare.Attach(appBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private IServiceProvider CreateServiceFactory()
|
||||
{
|
||||
var provider = (ServiceProvider)ServicesFactory.Create();
|
||||
provider.Add(typeof(ITraceOutputFactory), typeof(OwinTraceOutputFactory));
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
using System.IO;
|
||||
using Microsoft.Owin.Hosting.Tracing;
|
||||
using NLog;
|
||||
|
||||
namespace NzbDrone.Host.Owin
|
||||
{
|
||||
public class OwinTraceOutputFactory : ITraceOutputFactory
|
||||
{
|
||||
private readonly Logger _logger = LogManager.GetLogger("Owin");
|
||||
|
||||
public TextWriter Create(string outputFile)
|
||||
{
|
||||
return new NlogTextWriter(_logger);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Host.Owin
|
||||
{
|
||||
public class PortInUseException : NzbDroneException
|
||||
{
|
||||
public PortInUseException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ namespace NzbDrone.Host
|
|||
{
|
||||
public class Router
|
||||
{
|
||||
private readonly INzbDroneConsoleFactory _nzbDroneConsoleFactory;
|
||||
private readonly INzbDroneServiceFactory _nzbDroneServiceFactory;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IConsoleService _consoleService;
|
||||
|
@ -19,7 +20,8 @@ namespace NzbDrone.Host
|
|||
private readonly IRemoteAccessAdapter _remoteAccessAdapter;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public Router(INzbDroneServiceFactory nzbDroneServiceFactory,
|
||||
public Router(INzbDroneConsoleFactory nzbDroneConsoleFactory,
|
||||
INzbDroneServiceFactory nzbDroneServiceFactory,
|
||||
IServiceProvider serviceProvider,
|
||||
IConsoleService consoleService,
|
||||
IRuntimeInfo runtimeInfo,
|
||||
|
@ -27,6 +29,7 @@ namespace NzbDrone.Host
|
|||
IRemoteAccessAdapter remoteAccessAdapter,
|
||||
Logger logger)
|
||||
{
|
||||
_nzbDroneConsoleFactory = nzbDroneConsoleFactory;
|
||||
_nzbDroneServiceFactory = nzbDroneServiceFactory;
|
||||
_serviceProvider = serviceProvider;
|
||||
_consoleService = consoleService;
|
||||
|
@ -52,7 +55,7 @@ namespace NzbDrone.Host
|
|||
case ApplicationModes.Interactive:
|
||||
{
|
||||
_logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected");
|
||||
_nzbDroneServiceFactory.Start();
|
||||
_nzbDroneConsoleFactory.Start();
|
||||
break;
|
||||
}
|
||||
case ApplicationModes.InstallService:
|
||||
|
|
|
@ -2,27 +2,16 @@ using NzbDrone.Common.EnvironmentInfo;
|
|||
|
||||
namespace NzbDrone.Host.AccessControl
|
||||
{
|
||||
public interface IRemoteAccessAdapter
|
||||
{
|
||||
void MakeAccessible(bool passive);
|
||||
}
|
||||
|
||||
public class RemoteAccessAdapter : IRemoteAccessAdapter
|
||||
{
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IUrlAclAdapter _urlAclAdapter;
|
||||
private readonly IFirewallAdapter _firewallAdapter;
|
||||
private readonly ISslAdapter _sslAdapter;
|
||||
|
||||
public RemoteAccessAdapter(IRuntimeInfo runtimeInfo,
|
||||
IUrlAclAdapter urlAclAdapter,
|
||||
IFirewallAdapter firewallAdapter,
|
||||
ISslAdapter sslAdapter)
|
||||
IFirewallAdapter firewallAdapter)
|
||||
{
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_urlAclAdapter = urlAclAdapter;
|
||||
_firewallAdapter = firewallAdapter;
|
||||
_sslAdapter = sslAdapter;
|
||||
}
|
||||
|
||||
public void MakeAccessible(bool passive)
|
||||
|
@ -32,15 +21,12 @@ namespace NzbDrone.Host.AccessControl
|
|||
if (_runtimeInfo.IsAdmin)
|
||||
{
|
||||
_firewallAdapter.MakeAccessible();
|
||||
_sslAdapter.Register();
|
||||
}
|
||||
else if (!passive)
|
||||
{
|
||||
throw new RemoteAccessException("Failed to register URLs for Lidarr. Lidarr will not be accessible remotely");
|
||||
}
|
||||
}
|
||||
|
||||
_urlAclAdapter.ConfigureUrls();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace NzbDrone.Host.Middleware
|
||||
{
|
||||
public interface IAspNetCoreMiddleware
|
||||
{
|
||||
int Order { get; }
|
||||
void Attach(IApplicationBuilder appBuilder);
|
||||
}
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
using Nancy.Bootstrapper;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Owin;
|
||||
using Owin;
|
||||
|
||||
namespace NzbDrone.Host.Owin.MiddleWare
|
||||
namespace NzbDrone.Host.Middleware
|
||||
{
|
||||
public class NancyMiddleWare : IOwinMiddleWare
|
||||
public class NancyMiddleware : IAspNetCoreMiddleware
|
||||
{
|
||||
private readonly INancyBootstrapper _nancyBootstrapper;
|
||||
|
||||
public NancyMiddleWare(INancyBootstrapper nancyBootstrapper)
|
||||
public int Order => 2;
|
||||
|
||||
public NancyMiddleware(INancyBootstrapper nancyBootstrapper)
|
||||
{
|
||||
_nancyBootstrapper = nancyBootstrapper;
|
||||
}
|
||||
|
||||
public int Order => 2;
|
||||
|
||||
public void Attach(IAppBuilder appBuilder)
|
||||
public void Attach(IApplicationBuilder appBuilder)
|
||||
{
|
||||
var options = new NancyOptions
|
||||
{
|
||||
|
@ -23,7 +23,7 @@ namespace NzbDrone.Host.Owin.MiddleWare
|
|||
PerformPassThrough = context => context.Request.Path.StartsWith("/signalr")
|
||||
};
|
||||
|
||||
appBuilder.UseNancy(options);
|
||||
appBuilder.UseOwin(x => x.UseNancy(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs
Normal file
69
src/NzbDrone.Host/WebHost/Middleware/SignalRMiddleware.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.SignalR;
|
||||
|
||||
namespace NzbDrone.Host.Middleware
|
||||
{
|
||||
public class SignalRMiddleware : IAspNetCoreMiddleware
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
private readonly Logger _logger;
|
||||
private static string API_KEY;
|
||||
public int Order => 1;
|
||||
|
||||
public SignalRMiddleware(IContainer container,
|
||||
IConfigFileProvider configFileProvider,
|
||||
Logger logger)
|
||||
{
|
||||
_container = container;
|
||||
_logger = logger;
|
||||
API_KEY = configFileProvider.ApiKey;
|
||||
}
|
||||
|
||||
public void Attach(IApplicationBuilder appBuilder)
|
||||
{
|
||||
appBuilder.UseWebSockets();
|
||||
|
||||
appBuilder.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/signalr") &&
|
||||
!context.Request.Path.Value.EndsWith("/negotiate") &&
|
||||
(!context.Request.Query.ContainsKey("access_token") ||
|
||||
context.Request.Query["access_token"] != API_KEY))
|
||||
{
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Unauthorized");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next();
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
// Demote the exception to trace logging so users don't worry (as much).
|
||||
_logger.Trace(e);
|
||||
}
|
||||
});
|
||||
|
||||
appBuilder.UseSignalR(routes =>
|
||||
{
|
||||
routes.MapHub<MessageHub>("/signalr/messages");
|
||||
});
|
||||
|
||||
// This is a side effect of haing multiple IoC containers, TinyIoC and whatever
|
||||
// Kestrel/SignalR is using. Ideally we'd have one IoC container, but that's non-trivial with TinyIoC
|
||||
// TODO: Use a single IoC container if supported for TinyIoC or if we switch to another system (ie Autofac).
|
||||
|
||||
var hubContext = appBuilder.ApplicationServices.GetService<IHubContext<MessageHub>>();
|
||||
_container.Register(hubContext);
|
||||
}
|
||||
}
|
||||
}
|
134
src/NzbDrone.Host/WebHost/WebHostController.cs
Normal file
134
src/NzbDrone.Host/WebHost/WebHostController.cs
Normal file
|
@ -0,0 +1,134 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NLog;
|
||||
using NLog.Extensions.Logging;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Host.AccessControl;
|
||||
using NzbDrone.Host.Middleware;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace NzbDrone.Host
|
||||
{
|
||||
public class WebHostController : IHostController
|
||||
{
|
||||
private readonly IRuntimeInfo _runtimeInfo;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IFirewallAdapter _firewallAdapter;
|
||||
private readonly IEnumerable<IAspNetCoreMiddleware> _middlewares;
|
||||
private readonly Logger _logger;
|
||||
private IWebHost _host;
|
||||
|
||||
public WebHostController(IRuntimeInfo runtimeInfo,
|
||||
IConfigFileProvider configFileProvider,
|
||||
IFirewallAdapter firewallAdapter,
|
||||
IEnumerable<IAspNetCoreMiddleware> middlewares,
|
||||
Logger logger)
|
||||
{
|
||||
_runtimeInfo = runtimeInfo;
|
||||
_configFileProvider = configFileProvider;
|
||||
_firewallAdapter = firewallAdapter;
|
||||
_middlewares = middlewares;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void StartServer()
|
||||
{
|
||||
if (OsInfo.IsWindows && _runtimeInfo.IsAdmin)
|
||||
{
|
||||
_firewallAdapter.MakeAccessible();
|
||||
}
|
||||
|
||||
var bindAddress = _configFileProvider.BindAddress;
|
||||
var enableSsl = _configFileProvider.EnableSsl;
|
||||
var sslCertPath = _configFileProvider.SslCertPath;
|
||||
|
||||
var urls = new List<string>();
|
||||
|
||||
urls.Add(BuildUrl("http", bindAddress, _configFileProvider.Port));
|
||||
|
||||
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
urls.Add(BuildUrl("https", bindAddress, _configFileProvider.SslPort));
|
||||
}
|
||||
|
||||
_host = new WebHostBuilder()
|
||||
.UseUrls(urls.ToArray())
|
||||
.UseKestrel(options =>
|
||||
{
|
||||
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
options.ConfigureHttpsDefaults(configureOptions =>
|
||||
{
|
||||
var certificate = new X509Certificate2();
|
||||
certificate.Import(_configFileProvider.SslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
|
||||
|
||||
configureOptions.ServerCertificate = certificate;
|
||||
});
|
||||
}
|
||||
})
|
||||
.ConfigureLogging(logging =>
|
||||
{
|
||||
logging.AddProvider(new NLogLoggerProvider());
|
||||
logging.SetMinimumLevel(LogLevel.Warning);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services
|
||||
.AddSignalR()
|
||||
.AddJsonProtocol(options =>
|
||||
{
|
||||
options.PayloadSerializerSettings = Json.GetSerializerSettings();
|
||||
});
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UsePathBase(_configFileProvider.UrlBase);
|
||||
app.Properties["host.AppName"] = BuildInfo.AppName;
|
||||
|
||||
foreach (var middleWare in _middlewares.OrderBy(c => c.Order))
|
||||
{
|
||||
_logger.Debug("Attaching {0} to host", middleWare.GetType().Name);
|
||||
middleWare.Attach(app);
|
||||
}
|
||||
})
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.Build();
|
||||
|
||||
_logger.Info("Listening on the following URLs:");
|
||||
|
||||
foreach (var url in urls)
|
||||
{
|
||||
_logger.Info(" {0}", url);
|
||||
}
|
||||
|
||||
_host.Start();
|
||||
}
|
||||
|
||||
public async void StopServer()
|
||||
{
|
||||
_logger.Info("Attempting to stop OWIN host");
|
||||
|
||||
await _host.StopAsync(TimeSpan.FromSeconds(5));
|
||||
_host.Dispose();
|
||||
_host = null;
|
||||
|
||||
_logger.Info("Host has stopped");
|
||||
}
|
||||
|
||||
private string BuildUrl(string scheme, string bindAddress, int port)
|
||||
{
|
||||
return $"{scheme}://{bindAddress}:{port}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,10 +15,9 @@ namespace NzbDrone.Integration.Test.ApiTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("SignalR on CI seems unstable")]
|
||||
public void should_add_and_delete_root_folders()
|
||||
{
|
||||
ConnectSignalR();
|
||||
ConnectSignalR().Wait();
|
||||
|
||||
var rootFolder = new RootFolderResource
|
||||
{
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using NLog;
|
||||
using Lidarr.Api.V1;
|
||||
using Lidarr.Http;
|
||||
using Lidarr.Http.REST;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using RestSharp;
|
||||
using System.Linq;
|
||||
using Lidarr.Http;
|
||||
|
||||
namespace NzbDrone.Integration.Test.Client
|
||||
{
|
||||
|
@ -70,7 +69,9 @@ namespace NzbDrone.Integration.Test.Client
|
|||
|
||||
private static void AssertDisableCache(IList<Parameter> headers)
|
||||
{
|
||||
headers.Single(c => c.Name == "Cache-Control").Value.Should().Be("no-cache, no-store, must-revalidate, max-age=0");
|
||||
// cache control header gets reordered on net core
|
||||
((string)headers.Single(c => c.Name == "Cache-Control").Value).Split(',').Select(x => x.Trim())
|
||||
.Should().BeEquivalentTo("no-store, must-revalidate, no-cache, max-age=0".Split(',').Select(x => x.Trim()));
|
||||
headers.Single(c => c.Name == "Pragma").Value.Should().Be("no-cache");
|
||||
headers.Single(c => c.Name == "Expires").Value.Should().Be("0");
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNet.SignalR.Client;
|
||||
using Microsoft.AspNet.SignalR.Client.Transports;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
|
@ -20,7 +18,6 @@ using Lidarr.Api.V1.Artist;
|
|||
using Lidarr.Api.V1.Albums;
|
||||
using Lidarr.Api.V1.Tags;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Integration.Test.Client;
|
||||
using NzbDrone.SignalR;
|
||||
|
@ -28,6 +25,8 @@ using NzbDrone.Test.Common.Categories;
|
|||
using RestSharp;
|
||||
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
|
||||
using NzbDrone.Test.Common;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace NzbDrone.Integration.Test
|
||||
{
|
||||
|
@ -57,7 +56,8 @@ namespace NzbDrone.Integration.Test
|
|||
public ClientBase<AlbumResource> WantedCutoffUnmet;
|
||||
|
||||
private List<SignalRMessage> _signalRReceived;
|
||||
private Connection _signalrConnection;
|
||||
|
||||
private HubConnection _signalrConnection;
|
||||
|
||||
protected IEnumerable<SignalRMessage> SignalRMessages => _signalRReceived;
|
||||
|
||||
|
@ -136,19 +136,12 @@ namespace NzbDrone.Integration.Test
|
|||
}
|
||||
|
||||
[TearDown]
|
||||
public void IntegrationTearDown()
|
||||
public async Task IntegrationTearDown()
|
||||
{
|
||||
if (_signalrConnection != null)
|
||||
{
|
||||
switch (_signalrConnection.State)
|
||||
{
|
||||
case ConnectionState.Connected:
|
||||
case ConnectionState.Connecting:
|
||||
{
|
||||
_signalrConnection.Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await _signalrConnection.StopAsync();
|
||||
|
||||
_signalrConnection = null;
|
||||
_signalRReceived = new List<SignalRMessage>();
|
||||
|
@ -191,33 +184,51 @@ namespace NzbDrone.Integration.Test
|
|||
return path;
|
||||
}
|
||||
|
||||
protected void ConnectSignalR()
|
||||
protected async Task ConnectSignalR()
|
||||
{
|
||||
_signalRReceived = new List<SignalRMessage>();
|
||||
_signalrConnection = new Connection("http://localhost:8686/signalr");
|
||||
_signalrConnection.Start(new LongPollingTransport()).ContinueWith(task =>
|
||||
_signalrConnection = new HubConnectionBuilder()
|
||||
.WithUrl("http://localhost:8686/signalr/messages", options =>
|
||||
{
|
||||
options.AccessTokenProvider = () => Task.FromResult(ApiKey);
|
||||
})
|
||||
.Build();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
_signalrConnection.Closed += e =>
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
Assert.Fail("SignalrConnection failed. {0}", task.Exception.GetBaseException());
|
||||
}
|
||||
cts.Cancel();
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_signalrConnection.On<SignalRMessage>("receiveMessage", (message) =>
|
||||
{
|
||||
_signalRReceived.Add(message);
|
||||
});
|
||||
|
||||
var connected = false;
|
||||
var retryCount = 0;
|
||||
|
||||
while (_signalrConnection.State != ConnectionState.Connected)
|
||||
while (!connected)
|
||||
{
|
||||
if (retryCount > 25)
|
||||
try
|
||||
{
|
||||
Assert.Fail("Couldn't establish signalr connection. State: {0}", _signalrConnection.State);
|
||||
await _signalrConnection.StartAsync();
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (retryCount > 25)
|
||||
{
|
||||
Assert.Fail("Couldn't establish signalR connection");
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
Console.WriteLine("Connecting to signalR" + _signalrConnection.State);
|
||||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
_signalrConnection.Received += json => _signalRReceived.Add(Json.Deserialize<SignalRMessage>(json)); ;
|
||||
}
|
||||
|
||||
public static void WaitForCompletion(Func<bool> predicate, int timeout = 10000, int interval = 500)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<TargetFrameworks>net462</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.SignalR.Client" Version="2.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" />
|
||||
|
|
10
src/NzbDrone.SignalR/IBroadcastSignalRMessage.cs
Normal file
10
src/NzbDrone.SignalR/IBroadcastSignalRMessage.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public interface IBroadcastSignalRMessage
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
Task BroadcastMessage(SignalRMessage message);
|
||||
}
|
||||
}
|
|
@ -3,11 +3,7 @@
|
|||
<TargetFrameworks>net462</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.SignalR.SelfHost" Version="2.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.SignalR.SystemWeb" Version="2.4.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Host.SystemWeb" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Owin.Security" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Owin.SelfHost" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
using System.Threading;
|
||||
using Microsoft.AspNet.SignalR.Infrastructure;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public class LidarrPerformanceCounterManager : IPerformanceCounterManager
|
||||
{
|
||||
private readonly IPerformanceCounter _counter = new NoOpPerformanceCounter();
|
||||
|
||||
public void Initialize(string instanceName, CancellationToken hostShutdownToken)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public IPerformanceCounter LoadCounter(string categoryName, string counterName, string instanceName, bool isReadOnly)
|
||||
{
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public IPerformanceCounter ConnectionsConnected => _counter;
|
||||
public IPerformanceCounter ConnectionsReconnected => _counter;
|
||||
public IPerformanceCounter ConnectionsDisconnected => _counter;
|
||||
public IPerformanceCounter ConnectionsCurrent => _counter;
|
||||
public IPerformanceCounter ConnectionMessagesReceivedTotal => _counter;
|
||||
public IPerformanceCounter ConnectionMessagesSentTotal => _counter;
|
||||
public IPerformanceCounter ConnectionMessagesReceivedPerSec => _counter;
|
||||
public IPerformanceCounter ConnectionMessagesSentPerSec => _counter;
|
||||
public IPerformanceCounter MessageBusMessagesReceivedTotal => _counter;
|
||||
public IPerformanceCounter MessageBusMessagesReceivedPerSec => _counter;
|
||||
public IPerformanceCounter ScaleoutMessageBusMessagesReceivedPerSec => _counter;
|
||||
public IPerformanceCounter MessageBusMessagesPublishedTotal => _counter;
|
||||
public IPerformanceCounter MessageBusMessagesPublishedPerSec => _counter;
|
||||
public IPerformanceCounter MessageBusSubscribersCurrent => _counter;
|
||||
public IPerformanceCounter MessageBusSubscribersTotal => _counter;
|
||||
public IPerformanceCounter MessageBusSubscribersPerSec => _counter;
|
||||
public IPerformanceCounter MessageBusAllocatedWorkers => _counter;
|
||||
public IPerformanceCounter MessageBusBusyWorkers => _counter;
|
||||
public IPerformanceCounter MessageBusTopicsCurrent => _counter;
|
||||
public IPerformanceCounter ErrorsAllTotal => _counter;
|
||||
public IPerformanceCounter ErrorsAllPerSec => _counter;
|
||||
public IPerformanceCounter ErrorsHubResolutionTotal => _counter;
|
||||
public IPerformanceCounter ErrorsHubResolutionPerSec => _counter;
|
||||
public IPerformanceCounter ErrorsHubInvocationTotal => _counter;
|
||||
public IPerformanceCounter ErrorsHubInvocationPerSec => _counter;
|
||||
public IPerformanceCounter ErrorsTransportTotal => _counter;
|
||||
public IPerformanceCounter ErrorsTransportPerSec => _counter;
|
||||
public IPerformanceCounter ScaleoutStreamCountTotal => _counter;
|
||||
public IPerformanceCounter ScaleoutStreamCountOpen => _counter;
|
||||
public IPerformanceCounter ScaleoutStreamCountBuffering => _counter;
|
||||
public IPerformanceCounter ScaleoutErrorsTotal => _counter;
|
||||
public IPerformanceCounter ScaleoutErrorsPerSec => _counter;
|
||||
public IPerformanceCounter ScaleoutSendQueueLength => _counter;
|
||||
public IPerformanceCounter ConnectionsCurrentForeverFrame => _counter;
|
||||
public IPerformanceCounter ConnectionsCurrentLongPolling => _counter;
|
||||
public IPerformanceCounter ConnectionsCurrentServerSentEvents => _counter;
|
||||
public IPerformanceCounter ConnectionsCurrentWebSockets => _counter;
|
||||
}
|
||||
}
|
71
src/NzbDrone.SignalR/MessageHub.cs
Normal file
71
src/NzbDrone.SignalR/MessageHub.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public class SignalRMessageBroadcaster : IBroadcastSignalRMessage
|
||||
{
|
||||
private readonly IHubContext<MessageHub> _hubContext;
|
||||
|
||||
public SignalRMessageBroadcaster(IHubContext<MessageHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
public async Task BroadcastMessage(SignalRMessage message)
|
||||
{
|
||||
await _hubContext.Clients.All.SendAsync("receiveMessage", message);
|
||||
}
|
||||
|
||||
public bool IsConnected => MessageHub.IsConnected;
|
||||
}
|
||||
|
||||
public class MessageHub : Hub
|
||||
{
|
||||
private static HashSet<string> _connections = new HashSet<string>();
|
||||
|
||||
public static bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
return _connections.Count != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
_connections.Add(Context.ConnectionId);
|
||||
}
|
||||
|
||||
var message = new SignalRMessage
|
||||
{
|
||||
Name = "version",
|
||||
Body = new
|
||||
{
|
||||
Version = BuildInfo.Version.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
await Clients.All.SendAsync("receiveMessage", message);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
_connections.Remove(Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.AspNet.SignalR.Infrastructure;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public class NoOpPerformanceCounter : IPerformanceCounter
|
||||
{
|
||||
public string CounterName
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
public long Decrement()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long Increment()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long IncrementBy(long value)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public long RawValue
|
||||
{
|
||||
get { return 0; }
|
||||
set { }
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void RemoveInstance()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CounterSample NextSample()
|
||||
{
|
||||
return CounterSample.Empty;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Infrastructure;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public interface IBroadcastSignalRMessage
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
void BroadcastMessage(SignalRMessage message);
|
||||
}
|
||||
|
||||
public sealed class NzbDronePersistentConnection : PersistentConnection, IBroadcastSignalRMessage
|
||||
{
|
||||
private IPersistentConnectionContext Context => ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType());
|
||||
|
||||
private static string API_KEY;
|
||||
private readonly Dictionary<string, string> _messageHistory;
|
||||
private HashSet<string> _connections = new HashSet<string>();
|
||||
|
||||
public NzbDronePersistentConnection(IConfigFileProvider configFileProvider)
|
||||
{
|
||||
API_KEY = configFileProvider.ApiKey;
|
||||
_messageHistory = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
return _connections.Count != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastMessage(SignalRMessage message)
|
||||
{
|
||||
string lastMessage;
|
||||
if (_messageHistory.TryGetValue(message.Name, out lastMessage))
|
||||
{
|
||||
if (message.Action == ModelAction.Updated && message.Body.ToJson() == lastMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_messageHistory[message.Name] = message.Body.ToJson();
|
||||
|
||||
Context.Connection.Broadcast(message);
|
||||
}
|
||||
|
||||
protected override bool AuthorizeRequest(IRequest request)
|
||||
{
|
||||
var apiKey = request.QueryString["apiKey"];
|
||||
|
||||
if (apiKey.IsNotNullOrWhiteSpace() && apiKey.Equals(API_KEY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override Task OnConnected(IRequest request, string connectionId)
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
_connections.Add(connectionId);
|
||||
}
|
||||
|
||||
return SendVersion(connectionId);
|
||||
}
|
||||
|
||||
protected override Task OnReconnected(IRequest request, string connectionId)
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
_connections.Add(connectionId);
|
||||
}
|
||||
|
||||
return SendVersion(connectionId);
|
||||
}
|
||||
|
||||
protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
|
||||
{
|
||||
lock (_connections)
|
||||
{
|
||||
_connections.Remove(connectionId);
|
||||
}
|
||||
|
||||
return base.OnDisconnected(request, connectionId, stopCalled);
|
||||
}
|
||||
|
||||
private Task SendVersion(string connectionId)
|
||||
{
|
||||
return Context.Connection.Send(connectionId, new SignalRMessage
|
||||
{
|
||||
Name = "version",
|
||||
Body = new
|
||||
{
|
||||
Version = BuildInfo.Version.ToString()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
using System;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public class SignalRContractResolver : IContractResolver
|
||||
{
|
||||
private readonly IContractResolver _camelCaseContractResolver;
|
||||
private readonly IContractResolver _defaultContractSerializer;
|
||||
|
||||
public SignalRContractResolver()
|
||||
{
|
||||
_defaultContractSerializer = new DefaultContractResolver();
|
||||
_camelCaseContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||
}
|
||||
|
||||
public JsonContract ResolveContract(Type type)
|
||||
{
|
||||
var fullName = type.FullName;
|
||||
if (fullName.StartsWith("NzbDrone") || fullName.StartsWith("Lidarr"))
|
||||
{
|
||||
return _camelCaseContractResolver.ResolveContract(type);
|
||||
}
|
||||
|
||||
return _defaultContractSerializer.ResolveContract(type);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.AspNet.SignalR;
|
||||
using Microsoft.AspNet.SignalR.Infrastructure;
|
||||
using NzbDrone.Common.Composition;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public class SignalRDependencyResolver : DefaultDependencyResolver
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
|
||||
public static void Register(IContainer container)
|
||||
{
|
||||
GlobalHost.DependencyResolver = new SignalRDependencyResolver(container);
|
||||
}
|
||||
|
||||
private SignalRDependencyResolver(IContainer container)
|
||||
{
|
||||
_container = container;
|
||||
var performanceCounterManager = new LidarrPerformanceCounterManager();
|
||||
Register(typeof(IPerformanceCounterManager), () => performanceCounterManager);
|
||||
}
|
||||
|
||||
public override object GetService(Type serviceType)
|
||||
{
|
||||
// Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber is not registered in our internal contaiiner,
|
||||
// but it still gets treated like it is (possibly due to being a concrete type).
|
||||
|
||||
var fullName = serviceType.FullName;
|
||||
|
||||
if (fullName == "Microsoft.AspNet.SignalR.Infrastructure.AckSubscriber" ||
|
||||
fullName == "Newtonsoft.Json.JsonSerializer")
|
||||
{
|
||||
return base.GetService(serviceType);
|
||||
}
|
||||
|
||||
if (_container.IsTypeRegistered(serviceType))
|
||||
{
|
||||
return _container.Resolve(serviceType);
|
||||
}
|
||||
|
||||
return base.GetService(serviceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
using Microsoft.AspNet.SignalR;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.SignalR
|
||||
{
|
||||
public static class SignalRJsonSerializer
|
||||
{
|
||||
private static JsonSerializer _serializer;
|
||||
private static JsonSerializerSettings _serializerSettings;
|
||||
|
||||
public static void Register()
|
||||
{
|
||||
_serializerSettings = Json.GetSerializerSettings();
|
||||
_serializerSettings.ContractResolver = new SignalRContractResolver();
|
||||
_serializerSettings.Formatting = Formatting.None; // ServerSentEvents doesn't like newlines
|
||||
|
||||
_serializer = JsonSerializer.Create(_serializerSettings);
|
||||
|
||||
GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => _serializer);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue