New: Use native dotnet host and DryIoc

(cherry picked from commit d6170dbfedf27a6218afe242a0fae2eb8b368aec)
pull/2397/head
ta264 3 years ago committed by Qstick
parent c247d07e84
commit 7fe36a7e92

@ -23,10 +23,7 @@ const requiresRestartKeys = [
'enableSsl',
'sslPort',
'sslCertPath',
'sslCertPassword',
'authenticationMethod',
'username',
'password'
'sslCertPassword'
];
class GeneralSettings extends Component {

@ -85,7 +85,6 @@ class SecuritySettings extends Component {
name="authenticationMethod"
values={authenticationMethodOptions}
helpText="Require Username and Password to access Lidarr"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...authenticationMethod}
/>
@ -99,7 +98,6 @@ class SecuritySettings extends Component {
<FormInputGroup
type={inputTypes.TEXT}
name="username"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...username}
/>
@ -114,7 +112,6 @@ class SecuritySettings extends Component {
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...password}
/>

@ -8,6 +8,7 @@ using Lidarr.Http.REST.Attributes;
using Lidarr.Http.Validation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Datastore.Events;
@ -22,17 +23,17 @@ namespace Lidarr.Api.V1.Commands
public class CommandController : RestControllerWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
{
private readonly IManageCommandQueue _commandQueueManager;
private readonly IServiceFactory _serviceFactory;
private readonly KnownTypes _knownTypes;
private readonly Debouncer _debouncer;
private readonly Dictionary<int, CommandResource> _pendingUpdates;
public CommandController(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster,
IServiceFactory serviceFactory)
KnownTypes knownTypes)
: base(signalRBroadcaster)
{
_commandQueueManager = commandQueueManager;
_serviceFactory = serviceFactory;
_knownTypes = knownTypes;
PostValidator.RuleFor(c => c.Name).NotBlank();
@ -49,7 +50,7 @@ namespace Lidarr.Api.V1.Commands
public ActionResult<CommandResource> StartCommand(CommandResource commandResource)
{
var commandType =
_serviceFactory.GetImplementations(typeof(Command))
_knownTypes.GetImplementations(typeof(Command))
.Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));

@ -10,9 +10,9 @@ namespace Lidarr.Api.V1.ImportLists
{
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
public ImportListController(ImportListFactory importListFactory,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
public ImportListController(IImportListFactory importListFactory,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper)
{
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId));

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
@ -17,17 +18,20 @@ namespace Lidarr.Http.Authentication
public string HeaderName { get; set; }
public string QueryName { get; set; }
public string ApiKey { get; set; }
}
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private readonly string _apiKey;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
ISystemClock clock,
IConfigFileProvider config)
: base(options, logger, encoder, clock)
{
_apiKey = config.ApiKey;
}
private string ParseApiKey()
@ -56,7 +60,7 @@ namespace Lidarr.Http.Authentication
return Task.FromResult(AuthenticateResult.NoResult());
}
if (Options.ApiKey == providedApiKey)
if (_apiKey == providedApiKey)
{
var claims = new List<Claim>
{

@ -2,7 +2,6 @@ using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
{
@ -13,53 +12,37 @@ namespace Lidarr.Http.Authentication
return authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(name, options);
}
public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder authenticationBuilder)
public static AuthenticationBuilder AddBasic(this AuthenticationBuilder authenticationBuilder, string name)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(AuthenticationType.Basic.ToString(), options => { });
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(name, options => { });
}
public static AuthenticationBuilder AddNoAuthentication(this AuthenticationBuilder authenticationBuilder)
public static AuthenticationBuilder AddNone(this AuthenticationBuilder authenticationBuilder, string name)
{
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(AuthenticationType.None.ToString(), options => { });
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
}
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services, IConfigFileProvider config)
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
{
var authBuilder = services.AddAuthentication(config.AuthenticationMethod.ToString());
if (config.AuthenticationMethod == AuthenticationType.Basic)
{
authBuilder.AddBasicAuthentication();
}
else if (config.AuthenticationMethod == AuthenticationType.Forms)
{
authBuilder.AddCookie(AuthenticationType.Forms.ToString(), options =>
return services.AddAuthentication()
.AddNone(AuthenticationType.None.ToString())
.AddBasic(AuthenticationType.Basic.ToString())
.AddCookie(AuthenticationType.Forms.ToString(), options =>
{
options.AccessDeniedPath = "/login?loginFailed=true";
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddApiKey("API", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "apikey";
})
.AddApiKey("SignalR", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "access_token";
});
}
else
{
authBuilder.AddNoAuthentication();
}
authBuilder.AddApiKey("API", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "apikey";
options.ApiKey = config.ApiKey;
});
authBuilder.AddApiKey("SignalR", options =>
{
options.HeaderName = "X-Api-Key";
options.QueryName = "access_token";
options.ApiKey = config.ApiKey;
});
return authBuilder;
}
}
}

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Authentication;
using NzbDrone.Core.Configuration;
namespace Lidarr.Http.Authentication
@ -35,14 +36,14 @@ namespace Lidarr.Http.Authentication
{
new Claim("user", user.Username),
new Claim("identifier", user.Identifier.ToString()),
new Claim("UiAuth", "true")
new Claim("AuthType", AuthenticationType.Forms.ToString())
};
var authProperties = new AuthenticationProperties
{
IsPersistent = resource.RememberMe == "on"
};
await HttpContext.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties);
await HttpContext.SignInAsync(AuthenticationType.Forms.ToString(), new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies", "user", "identifier")), authProperties);
return Redirect("/");
}
@ -51,7 +52,7 @@ namespace Lidarr.Http.Authentication
public async Task<IActionResult> Logout()
{
_authService.Logout(HttpContext);
await HttpContext.SignOutAsync();
await HttpContext.SignOutAsync(AuthenticationType.Forms.ToString());
return Redirect("/");
}
}

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Authentication;
namespace Lidarr.Http.Authentication
{
@ -58,7 +59,7 @@ namespace Lidarr.Http.Authentication
{
new Claim("user", user.Username),
new Claim("identifier", user.Identifier.ToString()),
new Claim("UiAuth", "true")
new Claim("AuthType", AuthenticationType.Basic.ToString())
};
var identity = new ClaimsIdentity(claims, "Basic", "user", "identifier");

@ -5,13 +5,13 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NzbDrone.Core.Authentication;
namespace Lidarr.Http.Authentication
{
public class NoAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public NoAuthenticationHandler(IAuthenticationService authService,
IOptionsMonitor<AuthenticationSchemeOptions> options,
public NoAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
@ -24,7 +24,7 @@ namespace Lidarr.Http.Authentication
var claims = new List<Claim>
{
new Claim("user", "Anonymous"),
new Claim("UiAuth", "true")
new Claim("AuthType", AuthenticationType.None.ToString())
};
var identity = new ClaimsIdentity(claims, "NoAuth", "user", "identifier");

@ -10,13 +10,13 @@ namespace Lidarr.Http.Frontend.Mappers
public abstract class HtmlMapperBase : StaticResourceMapperBase
{
private readonly IDiskProvider _diskProvider;
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
private readonly Lazy<ICacheBreakerProvider> _cacheBreakProviderFactory;
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private string _generatedContent;
protected HtmlMapperBase(IDiskProvider diskProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger)
: base(diskProvider, logger)
{
@ -56,7 +56,7 @@ namespace Lidarr.Http.Frontend.Mappers
}
var text = _diskProvider.ReadAllText(HtmlPath);
var cacheBreakProvider = _cacheBreakProviderFactory();
var cacheBreakProvider = _cacheBreakProviderFactory.Value;
text = ReplaceRegex.Replace(text, match =>
{

@ -14,7 +14,7 @@ namespace Lidarr.Http.Frontend.Mappers
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
IConfigFileProvider configFileProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger)
{

@ -11,7 +11,7 @@ namespace Lidarr.Http.Frontend.Mappers
{
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider,
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
IConfigFileProvider configFileProvider,
Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger)

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Lidarr.Http.Middleware
{
public class BufferingMiddleware
{
private readonly RequestDelegate _next;
private readonly List<string> _urls;
public BufferingMiddleware(RequestDelegate next, List<string> urls)
{
_next = next;
_urls = urls;
}
public async Task InvokeAsync(HttpContext context)
{
if (_urls.Any(p => context.Request.Path.StartsWithSegments(p, StringComparison.OrdinalIgnoreCase)))
{
context.Request.EnableBuffering();
}
await _next(context);
}
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Globalization;
using System.Linq;
using FluentAssertions;
@ -21,7 +21,6 @@ namespace NzbDrone.Common.Test.InstrumentationTests
private static Exception[] FilteredExceptions = new Exception[]
{
new UnauthorizedAccessException(),
new TinyIoC.TinyIoCResolutionException(typeof(string)),
new OutOfMemoryException()
};

@ -1,8 +1,13 @@
using System.Linq;
using System.Linq;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Datastore;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host;
@ -16,11 +21,16 @@ namespace NzbDrone.Common.Test
[Test]
public void event_handlers_should_be_unique()
{
var container = MainAppContainerBuilder.BuildContainer(new StartupContext());
container.Register<IMainDatabase>(new MainDatabase(null));
container.Resolve<IAppFolderFactory>().Register();
var container = new Container(rules => rules.WithNzbDroneRules())
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"))
.GetServiceProvider();
Mocker.SetConstant(container);
container.GetRequiredService<IAppFolderFactory>().Register();
Mocker.SetConstant<System.IServiceProvider>(container);
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
.Select(c => c.GetType().FullName);

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition
{
public class AssemblyLoader
{
static AssemblyLoader()
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
RegisterSQLiteResolver();
}
public static IEnumerable<Assembly> Load(IEnumerable<string> assemblies)
{
var toLoad = assemblies.ToList();
toLoad.Add("Lidarr.Common");
toLoad.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono");
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
return toLoad.Select(x =>
AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{x}.dll")));
}
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
{
var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location);
var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name));
if (assemblyPath == null)
{
return null;
}
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
public static void RegisterSQLiteResolver()
{
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
// is less likely to exist.
var sqliteAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "System.Data.SQLite.dll"));
try
{
NativeLibrary.SetDllImportResolver(sqliteAssembly, LoadSqliteNativeLib);
}
catch (InvalidOperationException)
{
// This can only be set once per assembly
// Catch required for NzbDrone.Host tests
}
}
private static IntPtr LoadSqliteNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
{
var mappedName = OsInfo.IsLinux && libraryName == "sqlite3" ? "libsqlite3.so.0" : libraryName;
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
}
}
}

@ -1,113 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TinyIoC;
namespace NzbDrone.Common.Composition
{
public class Container : IContainer
{
private readonly TinyIoCContainer _container;
private readonly List<Type> _loadedTypes;
public Container(TinyIoCContainer container, List<Type> loadedTypes)
{
_container = container;
_loadedTypes = loadedTypes;
_container.Register<IContainer>(this);
}
public void Register<TService, TImplementation>()
where TImplementation : class, TService
where TService : class
{
_container.Register<TService, TImplementation>();
}
public void Register<T>(T instance)
where T : class
{
_container.Register<T>(instance);
}
public T Resolve<T>()
where T : class
{
return _container.Resolve<T>();
}
public object Resolve(Type type)
{
return _container.Resolve(type);
}
public void Register(Type serviceType, Type implementationType)
{
_container.Register(serviceType, implementationType);
}
public void Register<TService>(Func<IContainer, TService> factory)
where TService : class
{
_container.Register((c, n) => factory(this));
}
public void RegisterSingleton(Type service, Type implementation)
{
var factory = CreateSingletonImplementationFactory(implementation);
// For Resolve and ResolveAll
_container.Register(service, factory);
// For ctor(IEnumerable<T>)
var enumerableType = typeof(IEnumerable<>).MakeGenericType(service);
_container.Register(enumerableType, (c, p) =>
{
var instance = factory(c, p);
var result = Array.CreateInstance(service, 1);
result.SetValue(instance, 0);
return result;
});
}
public IEnumerable<T> ResolveAll<T>()
where T : class
{
return _container.ResolveAll<T>();
}
public void RegisterAllAsSingleton(Type service, IEnumerable<Type> implementationList)
{
foreach (var implementation in implementationList)
{
var factory = CreateSingletonImplementationFactory(implementation);
// For ResolveAll and ctor(IEnumerable<T>)
_container.Register(service, factory, implementation.FullName);
}
}
private Func<TinyIoCContainer, NamedParameterOverloads, object> CreateSingletonImplementationFactory(Type implementation)
{
const string singleImplPrefix = "singleImpl_";
_container.Register(implementation, implementation, singleImplPrefix + implementation.FullName).AsSingleton();
return (c, p) => _container.Resolve(implementation, singleImplPrefix + implementation.FullName);
}
public bool IsTypeRegistered(Type type)
{
return _container.CanResolve(type);
}
public IEnumerable<Type> GetImplementations(Type contractType)
{
return _loadedTypes
.Where(implementation =>
contractType.IsAssignableFrom(implementation) &&
!implementation.IsInterface &&
!implementation.IsAbstract);
}
}
}

@ -1,119 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Messaging;
using TinyIoC;
namespace NzbDrone.Common.Composition
{
public abstract class ContainerBuilderBase
{
private readonly List<Type> _loadedTypes;
protected IContainer Container { get; }
protected ContainerBuilderBase(IStartupContext args, List<string> assemblies)
{
_loadedTypes = new List<Type>();
assemblies.Add(OsInfo.IsWindows ? "Lidarr.Windows" : "Lidarr.Mono");
assemblies.Add("Lidarr.Common");
var startupPath = AppDomain.CurrentDomain.BaseDirectory;
foreach (var assemblyName in assemblies)
{
_loadedTypes.AddRange(AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(startupPath, $"{assemblyName}.dll")).GetTypes());
}
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(ContainerResolveEventHandler);
RegisterSQLiteResolver();
Container = new Container(new TinyIoCContainer(), _loadedTypes);
AutoRegisterInterfaces();
Container.Register(args);
}
private static Assembly ContainerResolveEventHandler(object sender, ResolveEventArgs args)
{
var resolver = new AssemblyDependencyResolver(args.RequestingAssembly.Location);
var assemblyPath = resolver.ResolveAssemblyToPath(new AssemblyName(args.Name));
if (assemblyPath == null)
{
return null;
}
return AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
}
public static void RegisterSQLiteResolver()
{
// This ensures we look for sqlite3 using libsqlite3.so.0 on Linux and not libsqlite3.so which
// is less likely to exist.
var sqliteAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "System.Data.SQLite.dll"));
try
{
NativeLibrary.SetDllImportResolver(sqliteAssembly, LoadSqliteNativeLib);
}
catch (InvalidOperationException)
{
// This can only be set once per assembly
// Catch required for NzbDrone.Host tests
}
}
private static IntPtr LoadSqliteNativeLib(string libraryName, Assembly assembly, DllImportSearchPath? dllImportSearchPath)
{
var mappedName = OsInfo.IsLinux && libraryName == "sqlite3" ? "libsqlite3.so.0" : libraryName;
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);
}
private void AutoRegisterInterfaces()
{
var loadedInterfaces = _loadedTypes.Where(t => t.IsInterface).ToList();
var implementedInterfaces = _loadedTypes.SelectMany(t => t.GetInterfaces());
var contracts = loadedInterfaces.Union(implementedInterfaces).Where(c => !c.IsGenericTypeDefinition && !string.IsNullOrWhiteSpace(c.FullName))
.Where(c => !c.FullName.StartsWith("System"))
.Except(new List<Type> { typeof(IMessage), typeof(IEvent), typeof(IContainer) }).Distinct().OrderBy(c => c.FullName);
foreach (var contract in contracts)
{
AutoRegisterImplementations(contract);
}
}
protected void AutoRegisterImplementations<TContract>()
{
AutoRegisterImplementations(typeof(TContract));
}
private void AutoRegisterImplementations(Type contractType)
{
var implementations = Container.GetImplementations(contractType).Where(c => !c.IsGenericTypeDefinition).ToList();
if (implementations.Count == 0)
{
return;
}
if (implementations.Count == 1)
{
var impl = implementations.Single();
Container.RegisterSingleton(contractType, impl);
}
else
{
Container.RegisterAllAsSingleton(contractType, implementations);
}
}
}
}

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using DryIoc;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Composition.Extensions
{
public static class ServiceCollectionExtensions
{
public static Rules WithNzbDroneRules(this Rules rules)
{
return rules.WithMicrosoftDependencyInjectionRules()
.WithAutoConcreteTypeResolution()
.WithDefaultReuse(Reuse.Singleton);
}
public static IContainer AddStartupContext(this IContainer container, StartupContext context)
{
container.RegisterInstance<IStartupContext>(context, ifAlreadyRegistered: IfAlreadyRegistered.Replace);
return container;
}
public static IContainer AutoAddServices(this IContainer container, List<string> assemblyNames)
{
var assemblies = AssemblyLoader.Load(assemblyNames);
container.RegisterMany(assemblies,
serviceTypeCondition: type => type.IsInterface && !string.IsNullOrWhiteSpace(type.FullName) && !type.FullName.StartsWith("System"),
reuse: Reuse.Singleton);
var knownTypes = new KnownTypes(assemblies.SelectMany(x => x.GetTypes()).ToList());
container.RegisterInstance(knownTypes);
return container;
}
}
}

@ -1,28 +0,0 @@
using System;
using System.Collections.Generic;
namespace NzbDrone.Common.Composition
{
public interface IContainer
{
void Register<T>(T instance)
where T : class;
void Register<TService, TImplementation>()
where TImplementation : class, TService
where TService : class;
T Resolve<T>()
where T : class;
object Resolve(Type type);
void Register(Type serviceType, Type implementationType);
void Register<TService>(Func<IContainer, TService> factory)
where TService : class;
void RegisterSingleton(Type service, Type implementation);
IEnumerable<T> ResolveAll<T>()
where T : class;
void RegisterAllAsSingleton(Type registrationType, IEnumerable<Type> implementationList);
bool IsTypeRegistered(Type type);
IEnumerable<Type> GetImplementations(Type contractType);
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Common.Composition
{
public class KnownTypes
{
private List<Type> _knownTypes;
// So unity can resolve for tests
public KnownTypes()
: this(new List<Type>())
{
}
public KnownTypes(List<Type> loadedTypes)
{
_knownTypes = loadedTypes;
}
public IEnumerable<Type> GetImplementations(Type contractType)
{
return _knownTypes
.Where(implementation =>
contractType.IsAssignableFrom(implementation) &&
!implementation.IsInterface &&
!implementation.IsAbstract);
}
}
}

@ -0,0 +1,14 @@
using DryIoc;
using NLog;
namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class CompositionExtensions
{
public static IContainer AddNzbDroneLogger(this IContainer container)
{
container.Register(Made.Of<Logger>(() => LogManager.GetLogger(Arg.Index<string>(0)), r => r.Parent.ImplementationType.Name.ToString()), reuse: Reuse.Transient);
return container;
}
}
}

@ -5,6 +5,8 @@
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.9" />

@ -1,7 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Composition;
using Microsoft.Extensions.DependencyInjection;
namespace NzbDrone.Common
{
@ -17,9 +17,9 @@ namespace NzbDrone.Common
public class ServiceFactory : IServiceFactory
{
private readonly IContainer _container;
private readonly System.IServiceProvider _container;
public ServiceFactory(IContainer container)
public ServiceFactory(System.IServiceProvider container)
{
_container = container;
}
@ -27,23 +27,23 @@ namespace NzbDrone.Common
public T Build<T>()
where T : class
{
return _container.Resolve<T>();
return _container.GetRequiredService<T>();
}
public IEnumerable<T> BuildAll<T>()
where T : class
{
return _container.ResolveAll<T>().GroupBy(c => c.GetType().FullName).Select(g => g.First());
return _container.GetServices<T>().GroupBy(c => c.GetType().FullName).Select(g => g.First());
}
public object Build(Type contract)
{
return _container.Resolve(contract);
return _container.GetRequiredService(contract);
}
public IEnumerable<Type> GetImplementations(Type contract)
{
return _container.GetImplementations(contract);
return _container.GetServices(contract).Select(x => x.GetType());
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,15 +0,0 @@
using NzbDrone.Host;
namespace NzbDrone.Console
{
public class ConsoleAlerts : IUserAlert
{
public void Alert(string message)
{
System.Console.WriteLine();
System.Console.WriteLine(message);
System.Console.WriteLine("Press enter to continue");
System.Console.ReadLine();
}
}
}

@ -2,6 +2,8 @@ using System;
using System.IO;
using System.Net.Sockets;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
@ -15,7 +17,7 @@ namespace NzbDrone.Console
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(ConsoleApp));
private enum ExitCodes : int
private enum ExitCodes
{
Normal = 0,
UnknownFailure = 1,
@ -40,7 +42,7 @@ namespace NzbDrone.Console
throw;
}
Bootstrap.Start(startupArgs, new ConsoleAlerts());
Bootstrap.Start(args);
}
catch (LidarrStartupException ex)
{

@ -1,6 +1,7 @@
using System;
using FluentMigrator;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using NUnit.Framework;
using NzbDrone.Core.Datastore.Migration.Framework;
@ -39,7 +40,7 @@ namespace NzbDrone.Core.Test.Framework
protected override void SetupLogging()
{
Mocker.SetConstant<ILoggerProvider>(Mocker.Resolve<MigrationLoggerProvider>());
Mocker.SetConstant<ILoggerProvider>(Mocker.Resolve<NLogLoggerProvider>());
}
[SetUp]

@ -1,7 +1,6 @@
using System;
using System.Data.SQLite;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
@ -39,17 +38,6 @@ namespace NzbDrone.Core.Datastore
Environment.SetEnvironmentVariable("No_PreLoadSQLite", "true");
}
public static void RegisterDatabase(IContainer container)
{
var mainDb = new MainDatabase(container.Resolve<IDbFactory>().Create());
container.Register<IMainDatabase>(mainDb);
var logDb = new LogDatabase(container.Resolve<IDbFactory>().Create(MigrationType.Log));
container.Register<ILogDatabase>(logDb);
}
public DbFactory(IMigrationController migrationController,
IConnectionStringFactory connectionStringFactory,
IDiskProvider diskProvider,

@ -0,0 +1,24 @@
using DryIoc;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Extensions
{
public static class CompositionExtensions
{
public static IContainer AddDatabase(this IContainer container)
{
container.RegisterDelegate<IDbFactory, IMainDatabase>(f => new MainDatabase(f.Create()), Reuse.Singleton);
container.RegisterDelegate<IDbFactory, ILogDatabase>(f => new LogDatabase(f.Create(MigrationType.Log)), Reuse.Singleton);
return container;
}
public static IContainer AddDummyDatabase(this IContainer container)
{
container.RegisterInstance<IMainDatabase>(new MainDatabase(null));
container.RegisterInstance<ILogDatabase>(new LogDatabase(null));
return container;
}
}
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Reflection;
using FluentMigrator.Runner;
@ -7,6 +7,7 @@ using FluentMigrator.Runner.Processors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
@ -34,7 +35,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_logger.Info("*** Migrating {0} ***", connectionString);
var serviceProvider = new ServiceCollection()
.AddLogging(lb => lb.AddProvider(_migrationLoggerProvider))
.AddLogging(b => b.AddNLog())
.AddFluentMigratorCore()
.ConfigureRunner(
builder => builder

@ -1,59 +0,0 @@
using System;
using FluentMigrator.Runner;
using FluentMigrator.Runner.Logging;
using NLog;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class MigrationLogger : FluentMigratorLogger
{
private readonly Logger _logger;
public MigrationLogger(Logger logger,
FluentMigratorLoggerOptions options)
: base(options)
{
_logger = logger;
}
protected override void WriteHeading(string message)
{
_logger.Info("*** {0} ***", message);
}
protected override void WriteSay(string message)
{
_logger.Debug(message);
}
protected override void WriteEmphasize(string message)
{
_logger.Warn(message);
}
protected override void WriteSql(string sql)
{
_logger.Debug(sql);
}
protected override void WriteEmptySql()
{
_logger.Debug(@"No SQL statement executed.");
}
protected override void WriteElapsedTime(TimeSpan timeSpan)
{
_logger.Debug("Took: {0}", timeSpan);
}
protected override void WriteError(string message)
{
_logger.Error(message);
}
protected override void WriteError(Exception exception)
{
_logger.Error(exception);
}
}
}

@ -1,34 +0,0 @@
using System;
using FluentMigrator.Runner;
using Microsoft.Extensions.Logging;
using NLog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class MigrationLoggerProvider : ILoggerProvider
{
private readonly Logger _logger;
public MigrationLoggerProvider(Logger logger)
{
_logger = logger;
}
public ILogger CreateLogger(string categoryName)
{
return new MigrationLogger(_logger, new FluentMigratorLoggerOptions() { ShowElapsedTime = true, ShowSql = true });
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// Nothing to clean up
}
}
}

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider;
@ -21,7 +21,7 @@ namespace NzbDrone.Core.Download
public DownloadClientFactory(IDownloadClientStatusService downloadClientStatusService,
IDownloadClientRepository providerRepository,
IEnumerable<IDownloadClient> providers,
IContainer container,
IServiceProvider container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Extras
public ExistingExtraFileService(IDiskProvider diskProvider,
IDiskScanService diskScanService,
List<IImportExistingExtraFiles> existingExtraFileImporters,
IEnumerable<IImportExistingExtraFiles> existingExtraFileImporters,
Logger logger)
{
_diskProvider = diskProvider;

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Extras
ITrackService trackService,
IDiskProvider diskProvider,
IConfigService configService,
List<IManageExtraFiles> extraFileManagers,
IEnumerable<IManageExtraFiles> extraFileManagers,
Logger logger)
{
_mediaFileService = mediaFileService;

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
@ -17,7 +17,7 @@ namespace NzbDrone.Core.Extras.Metadata
{
private readonly IMetadataRepository _providerRepository;
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
_providerRepository = providerRepository;

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
@ -21,7 +22,7 @@ namespace NzbDrone.Core.ImportLists
public ImportListFactory(IImportListStatusService importListStatusService,
IImportListRepository providerRepository,
IEnumerable<IImportList> providers,
IContainer container,
IServiceProvider container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
@ -23,7 +24,7 @@ namespace NzbDrone.Core.Indexers
public IndexerFactory(IIndexerStatusService indexerStatusService,
IIndexerRepository providerRepository,
IEnumerable<IIndexer> providers,
IContainer container,
IServiceProvider container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)

@ -6,12 +6,16 @@
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="FluentMigrator.Runner" Version="4.0.0-alpha.268" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.268" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="MailKit" Version="2.11.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.9" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
<PackageReference Include="RestSharp" Version="106.11.7" />
<PackageReference Include="System.IO.Abstractions" Version="13.2.29" />
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.25" />

@ -5,6 +5,7 @@ using System.Net;
using System.Threading;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions;
@ -36,17 +37,18 @@ namespace NzbDrone.Core.Messaging.Commands
public class CommandQueueManager : IManageCommandQueue, IHandle<ApplicationStartedEvent>
{
private readonly ICommandRepository _repo;
private readonly IServiceFactory _serviceFactory;
private readonly KnownTypes _knownTypes;
private readonly Logger _logger;
private readonly CommandQueue _commandQueue;
public CommandQueueManager(ICommandRepository repo,
IServiceFactory serviceFactory,
KnownTypes knownTypes,
Logger logger)
{
_repo = repo;
_serviceFactory = serviceFactory;
_knownTypes = knownTypes;
_logger = logger;
_commandQueue = new CommandQueue();
@ -233,9 +235,8 @@ namespace NzbDrone.Core.Messaging.Commands
private dynamic GetCommand(string commandName)
{
commandName = commandName.Split('.').Last();
var commandType = _serviceFactory.GetImplementations(typeof(Command))
.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
var commands = _knownTypes.GetImplementations(typeof(Command));
var commandType = commands.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
return Json.Deserialize("{}", commandType);
}

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
@ -21,7 +22,7 @@ namespace NzbDrone.Core.Notifications
public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory
{
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IServiceProvider container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger)
{
}

@ -37,7 +37,7 @@ namespace NzbDrone.Core.Tags
public TagService(ITagRepository repo,
IEventAggregator eventAggregator,
IDelayProfileService delayProfileService,
ImportListFactory importListFactory,
IImportListFactory importListFactory,
INotificationFactory notificationFactory,
IReleaseProfileService releaseProfileService,
IArtistService artistService,

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Core.Lifecycle;
@ -15,7 +16,7 @@ namespace NzbDrone.Core.ThingiProvider
where TProvider : IProvider
{
private readonly IProviderRepository<TProviderDefinition> _providerRepository;
private readonly IContainer _container;
private readonly IServiceProvider _container;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@ -23,7 +24,7 @@ namespace NzbDrone.Core.ThingiProvider
protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository,
IEnumerable<TProvider> providers,
IContainer container,
IServiceProvider container,
IEventAggregator eventAggregator,
Logger logger)
{
@ -123,7 +124,7 @@ namespace NzbDrone.Core.ThingiProvider
public TProvider GetInstance(TProviderDefinition definition)
{
var type = GetImplementation(definition);
var instance = (TProvider)_container.Resolve(type);
var instance = (TProvider)_container.GetRequiredService(type);
instance.Definition = definition;
SetProviderCharacteristics(instance, definition);
return instance;

@ -1,12 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Datastore;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Datastore.Extensions;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Indexers;
@ -15,44 +19,50 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Host;
using NzbDrone.SignalR;
using NzbDrone.Test.Common;
using IServiceProvider = System.IServiceProvider;
namespace NzbDrone.App.Test
{
[TestFixture]
public class ContainerFixture : TestBase
{
private IContainer _container;
private IServiceProvider _container;
[SetUp]
public void SetUp()
{
var args = new StartupContext("first", "second");
_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);
var container = new Container(rules => rules.WithNzbDroneRules())
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddNzbDroneLogger()
.AddDummyDatabase()
.AddStartupContext(args);
container.RegisterInstance<IBroadcastSignalRMessage>(mockBroadcaster.Object);
_container = container.GetServiceProvider();
}
[Test]
public void should_be_able_to_resolve_indexers()
{
_container.Resolve<IEnumerable<IIndexer>>().Should().NotBeEmpty();
_container.GetRequiredService<IEnumerable<IIndexer>>().Should().NotBeEmpty();
}
[Test]
public void should_be_able_to_resolve_downloadclients()
{
_container.Resolve<IEnumerable<IDownloadClient>>().Should().NotBeEmpty();
_container.GetRequiredService<IEnumerable<IDownloadClient>>().Should().NotBeEmpty();
}
[Test]
public void container_should_inject_itself()
{
var factory = _container.Resolve<IServiceFactory>();
var factory = _container.GetRequiredService<IServiceFactory>();
factory.Build<IIndexerFactory>().Should().NotBeNull();
}
@ -62,7 +72,7 @@ namespace NzbDrone.App.Test
{
var genericExecutor = typeof(IExecute<>).MakeGenericType(typeof(RssSyncCommand));
var executor = _container.Resolve(genericExecutor);
var executor = _container.GetRequiredService(genericExecutor);
executor.Should().NotBeNull();
executor.Should().BeAssignableTo<IExecute<RssSyncCommand>>();
@ -71,8 +81,8 @@ namespace NzbDrone.App.Test
[Test]
public void should_return_same_instance_via_resolve_and_resolveall()
{
var first = (DownloadMonitoringService)_container.Resolve<IHandle<TrackedDownloadsRemovedEvent>>();
var second = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
var first = (DownloadMonitoringService)_container.GetRequiredService<IHandle<TrackedDownloadsRemovedEvent>>();
var second = _container.GetServices<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
first.Should().BeSameAs(second);
}
@ -80,8 +90,8 @@ namespace NzbDrone.App.Test
[Test]
public void should_return_same_instance_of_singletons_by_same_interface()
{
var first = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = _container.ResolveAll<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
var first = _container.GetServices<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = _container.GetServices<IHandle<TrackedDownloadsRemovedEvent>>().OfType<DownloadMonitoringService>().Single();
first.Should().BeSameAs(second);
}
@ -89,8 +99,8 @@ namespace NzbDrone.App.Test
[Test]
public void should_return_same_instance_of_singletons_by_different_interfaces()
{
var first = _container.ResolveAll<IHandle<AlbumGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = (DownloadMonitoringService)_container.Resolve<IExecute<RefreshMonitoredDownloadsCommand>>();
var first = _container.GetServices<IHandle<AlbumGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
var second = (DownloadMonitoringService)_container.GetRequiredService<IExecute<RefreshMonitoredDownloadsCommand>>();
first.Should().BeSameAs(second);
}

@ -1,4 +1,3 @@
using System.ServiceProcess;
using Moq;
using NUnit.Framework;
using NzbDrone.Common;
@ -10,7 +9,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.App.Test
{
[TestFixture]
public class RouterTest : TestBase<Router>
public class RouterTest : TestBase<UtilityModeRouter>
{
[SetUp]
public void Setup()
@ -48,33 +47,6 @@ namespace NzbDrone.App.Test
serviceProviderMock.Verify(c => c.Uninstall(ServiceProvider.SERVICE_NAME), Times.Once());
}
[Test]
public void Route_should_call_console_service_when_application_mode_is_console()
{
Mocker.GetMock<IRuntimeInfo>().SetupGet(c => c.IsUserInteractive).Returns(true);
Subject.Route(ApplicationModes.Interactive);
Mocker.GetMock<INzbDroneConsoleFactory>().Verify(c => c.Start(), Times.Once());
}
[Test]
public void Route_should_call_service_start_when_run_in_service_mode()
{
var envMock = Mocker.GetMock<IRuntimeInfo>();
var serviceProvider = Mocker.GetMock<IServiceProvider>();
envMock.SetupGet(c => c.IsUserInteractive).Returns(false);
serviceProvider.Setup(c => c.Run(It.IsAny<ServiceBase>()));
serviceProvider.Setup(c => c.ServiceExist(It.IsAny<string>())).Returns(true);
serviceProvider.Setup(c => c.GetStatus(It.IsAny<string>())).Returns(ServiceControllerStatus.StartPending);
Subject.Route(ApplicationModes.Service);
serviceProvider.Verify(c => c.Run(It.IsAny<ServiceBase>()), Times.Once());
}
[Test]
public void show_error_on_install_if_service_already_exist()
{

@ -2,6 +2,11 @@ using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Host.AccessControl
{
public interface IRemoteAccessAdapter
{
void MakeAccessible(bool passive);
}
public class RemoteAccessAdapter : IRemoteAccessAdapter
{
private readonly IRuntimeInfo _runtimeInfo;

@ -0,0 +1,119 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Host
{
public class AppLifetime : IHostedService, IHandle<ApplicationShutdownRequested>
{
private readonly IHostApplicationLifetime _appLifetime;
private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService;
private readonly IProcessProvider _processProvider;
private readonly IEventAggregator _eventAggregator;
private readonly IUtilityModeRouter _utilityModeRouter;
private readonly Logger _logger;
public AppLifetime(IHostApplicationLifetime appLifetime,
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IStartupContext startupContext,
IBrowserService browserService,
IProcessProvider processProvider,
IEventAggregator eventAggregator,
IUtilityModeRouter utilityModeRouter,
Logger logger)
{
_appLifetime = appLifetime;
_configFileProvider = configFileProvider;
_runtimeInfo = runtimeInfo;
_startupContext = startupContext;
_browserService = browserService;
_processProvider = processProvider;
_eventAggregator = eventAggregator;
_utilityModeRouter = utilityModeRouter;
_logger = logger;
appLifetime.ApplicationStarted.Register(OnAppStarted);
appLifetime.ApplicationStopped.Register(OnAppStopped);
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnAppStarted()
{
_runtimeInfo.IsExiting = false;
if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER)
&& _configFileProvider.LaunchBrowser)
{
_browserService.LaunchWebUI();
}
_eventAggregator.PublishEvent(new ApplicationStartedEvent());
}
private void OnAppStopped()
{
if (_runtimeInfo.RestartPending)
{
var restartArgs = GetRestartArgs();
_logger.Info("Attempting restart with arguments: {0}", restartArgs);
_processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs);
}
}
private void Shutdown()
{
_logger.Info("Attempting to stop application.");
_logger.Info("Application has finished stop routine.");
_runtimeInfo.IsExiting = true;
_appLifetime.StopApplication();
}
private string GetRestartArgs()
{
var args = _startupContext.PreservedArguments;
args += " /restart";
if (!args.Contains("/nobrowser"))
{
args += " /nobrowser";
}
return args;
}
public void Handle(ApplicationShutdownRequested message)
{
if (!_runtimeInfo.IsWindowsService)
{
if (message.Restarting)
{
_runtimeInfo.RestartPending = true;
}
LogManager.Configuration = null;
Shutdown();
}
}
}
}

@ -1,134 +0,0 @@
using System;
using System.ServiceProcess;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Host
{
public interface INzbDroneServiceFactory
{
ServiceBase Build();
}
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;
private readonly IHostController _hostController;
private readonly IStartupContext _startupContext;
private readonly IBrowserService _browserService;
private readonly IContainer _container;
private readonly Logger _logger;
public NzbDroneConsoleFactory(IConfigFileProvider configFileProvider,
IHostController hostController,
IRuntimeInfo runtimeInfo,
IStartupContext startupContext,
IBrowserService browserService,
IContainer container,
Logger logger)
{
_configFileProvider = configFileProvider;
_hostController = hostController;
_runtimeInfo = runtimeInfo;
_startupContext = startupContext;
_browserService = browserService;
_container = container;
_logger = logger;
}
public void Start()
{
if (OsInfo.IsNotWindows)
{
Console.CancelKeyPress += (sender, eventArgs) => LogManager.Configuration = null;
}
_runtimeInfo.IsExiting = false;
DbFactory.RegisterDatabase(_container);
_container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartingEvent());
if (_runtimeInfo.IsExiting)
{
return;
}
_hostController.StartServer();
if (!_startupContext.Flags.Contains(StartupContext.NO_BROWSER)
&& _configFileProvider.LaunchBrowser)
{
_browserService.LaunchWebUI();
}
_container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent());
}
public void Shutdown()
{
_logger.Info("Attempting to stop application.");
_hostController.StopServer();
_logger.Info("Application has finished stop routine.");
_runtimeInfo.IsExiting = true;
}
public void Handle(ApplicationShutdownRequested message)
{
if (!_runtimeInfo.IsWindowsService)
{
if (message.Restarting)
{
_runtimeInfo.RestartPending = true;
}
LogManager.Configuration = null;
Shutdown();
}
}
}
}

@ -1,54 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Core.Datastore.Extensions;
namespace NzbDrone.Host
{
public static class Bootstrap
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Bootstrap));
private static IContainer _container;
public static void Start(StartupContext startupContext, IUserAlert userAlert, Action<IContainer> startCallback = null)
public static readonly List<string> ASSEMBLIES = new List<string>
{
"Lidarr.Host",
"Lidarr.Core",
"Lidarr.SignalR",
"Lidarr.Api.V1",
"Lidarr.Http"
};
public static void Start(string[] args, Action<IHostBuilder> trayCallback = null)
{
try
{
Logger.Info("Starting Lidarr - {0} - Version {1}", Assembly.GetCallingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version);
if (!PlatformValidation.IsValidate(userAlert))
{
throw new TerminateApplicationException("Missing system requirements");
}
var startupContext = new StartupContext(args);
LongPathSupport.Enable();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
_container = MainAppContainerBuilder.BuildContainer(startupContext);
_container.Resolve<InitializeLogger>().Initialize();
_container.Resolve<IAppFolderFactory>().Register();
_container.Resolve<IProvidePidFile>().Write();
var appMode = GetApplicationMode(startupContext);
Start(appMode, startupContext);
if (startCallback != null)
{
startCallback(_container);
}
else
switch (appMode)
{
SpinToExit(appMode);
case ApplicationModes.Service:
{
Logger.Debug("Service selected");
CreateConsoleHostBuilder(args, startupContext).UseWindowsService().Build().Run();
break;
}
case ApplicationModes.Interactive:
{
Logger.Debug(trayCallback != null ? "Tray selected" : "Console selected");
var builder = CreateConsoleHostBuilder(args, startupContext);
if (trayCallback != null)
{
trayCallback(builder);
}
builder.Build().Run();
break;
}
// Utility mode
default:
{
new Container(rules => rules.WithNzbDroneRules())
.AutoAddServices(ASSEMBLIES)
.AddNzbDroneLogger()
.AddStartupContext(startupContext)
.Resolve<UtilityModeRouter>()
.Route(appMode);
break;
}
}
}
catch (InvalidConfigFileException ex)
@ -66,61 +102,57 @@ namespace NzbDrone.Host
}
}
private static void Start(ApplicationModes applicationModes, StartupContext startupContext)
public static IHostBuilder CreateConsoleHostBuilder(string[] args, StartupContext context)
{
_container.Resolve<ReconfigureLogging>().Reconfigure();
if (!IsInUtilityMode(applicationModes))
{
if (startupContext.Flags.Contains(StartupContext.RESTART))
{
Thread.Sleep(2000);
}
var config = GetConfiguration(context);
EnsureSingleInstance(applicationModes == ApplicationModes.Service, startupContext);
}
var bindAddress = config.GetValue(nameof(ConfigFileProvider.BindAddress), "*");
var port = config.GetValue(nameof(ConfigFileProvider.Port), 8686);
var sslPort = config.GetValue(nameof(ConfigFileProvider.SslPort), 6868);
var enableSsl = config.GetValue(nameof(ConfigFileProvider.EnableSsl), false);
var sslCertPath = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPath));
var sslCertPassword = config.GetValue<string>(nameof(ConfigFileProvider.SslCertPassword));
_container.Resolve<Router>().Route(applicationModes);
}
var urls = new List<string> { BuildUrl("http", bindAddress, port) };
private static void SpinToExit(ApplicationModes applicationModes)
{
if (IsInUtilityMode(applicationModes))
{
return;
}
_container.Resolve<IWaitForExit>().Spin();
}
private static void EnsureSingleInstance(bool isService, IStartupContext startupContext)
{
if (startupContext.Flags.Contains(StartupContext.NO_SINGLE_INSTANCE_CHECK))
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
return;
urls.Add(BuildUrl("https", bindAddress, sslPort));
}
var instancePolicy = _container.Resolve<ISingleInstancePolicy>();
if (startupContext.Flags.Contains(StartupContext.TERMINATE))
{
instancePolicy.KillAllOtherInstance();
}
else if (startupContext.Args.ContainsKey(StartupContext.APPDATA))
{
instancePolicy.WarnIfAlreadyRunning();
}
else if (isService)
{
instancePolicy.KillAllOtherInstance();
}
else
{
instancePolicy.PreventStartIfAlreadyRunning();
}
return new HostBuilder()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules())))
.ConfigureContainer<IContainer>(c =>
{
c.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddNzbDroneLogger()
.AddDatabase()
.AddStartupContext(context);
})
.ConfigureWebHost(builder =>
{
builder.UseUrls(urls.ToArray());
builder.UseKestrel(options =>
{
if (enableSsl && sslCertPath.IsNotNullOrWhiteSpace())
{
options.ConfigureHttpsDefaults(configureOptions =>
{
configureOptions.ServerCertificate = ValidateSslCertificate(sslCertPath, sslCertPassword);
});
}
});
builder.ConfigureKestrel(serverOptions =>
{
serverOptions.AllowSynchronousIO = true;
serverOptions.Limits.MaxRequestBodySize = null;
});
builder.UseStartup<Startup>();
});
}
private static ApplicationModes GetApplicationMode(IStartupContext startupContext)
public static ApplicationModes GetApplicationMode(IStartupContext startupContext)
{
if (startupContext.Help)
{
@ -142,7 +174,7 @@ namespace NzbDrone.Host
return ApplicationModes.UninstallService;
}
if (_container.Resolve<IRuntimeInfo>().IsWindowsService)
if (OsInfo.IsWindows && WindowsServiceHelpers.IsWindowsService())
{
return ApplicationModes.Service;
}
@ -150,23 +182,39 @@ namespace NzbDrone.Host
return ApplicationModes.Interactive;
}
private static bool IsInUtilityMode(ApplicationModes applicationMode)
private static IConfiguration GetConfiguration(StartupContext context)
{
var appFolder = new AppFolderInfo(context);
return new ConfigurationBuilder()
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
.Build();
}
private static string BuildUrl(string scheme, string bindAddress, int port)
{
switch (applicationMode)
return $"{scheme}://{bindAddress}:{port}";
}
private static X509Certificate2 ValidateSslCertificate(string cert, string password)
{
X509Certificate2 certificate;
try
{
case ApplicationModes.InstallService:
case ApplicationModes.UninstallService:
case ApplicationModes.RegisterUrl:
case ApplicationModes.Help:
{
return true;
}
certificate = new X509Certificate2(cert, password, X509KeyStorageFlags.DefaultKeySet);
}
catch (CryptographicException ex)
{
if (ex.HResult == 0x2 || ex.HResult == 0x2006D080)
{
throw new LidarrStartupException(ex,
$"The SSL certificate file {cert} does not exist");
}
default:
{
return false;
}
throw new LidarrStartupException(ex);
}
return certificate;
}
}
}

@ -1,8 +0,0 @@
namespace NzbDrone.Host
{
public interface IHostController
{
void StartServer();
void StopServer();
}
}

@ -1,7 +0,0 @@
namespace NzbDrone.Host.AccessControl
{
public interface IRemoteAccessAdapter
{
void MakeAccessible(bool passive);
}
}

@ -1,7 +0,0 @@
namespace NzbDrone.Host
{
public interface IUserAlert
{
void Alert(string message);
}
}

@ -4,9 +4,10 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="5.0.5" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="5.0.1" />
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" />

@ -1,39 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.SignalR;
namespace NzbDrone.Host
{
public class MainAppContainerBuilder : ContainerBuilderBase
{
public static IContainer BuildContainer(StartupContext args)
{
var assemblies = new List<string>
{
"Lidarr.Host",
"Lidarr.Core",
"Lidarr.SignalR",
"Lidarr.Api.V1",
"Lidarr.Http"
};
return new MainAppContainerBuilder(args, assemblies).Container;
}
private MainAppContainerBuilder(StartupContext args, List<string> assemblies)
: base(args, assemblies)
{
AutoRegisterImplementations<MessageHub>();
if (OsInfo.IsWindows)
{
Container.Register<INzbDroneServiceFactory, NzbDroneServiceFactory>();
}
else
{
Container.Register<INzbDroneServiceFactory, DummyNzbDroneServiceFactory>();
}
}
}
}

@ -1,56 +0,0 @@
using System;
using System.Diagnostics;
using System.Reflection;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Host
{
public static class PlatformValidation
{
private const string DOWNLOAD_LINK = "http://www.microsoft.com/en-us/download/details.aspx?id=42643";
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PlatformValidation));
public static bool IsValidate(IUserAlert userAlert)
{
if (OsInfo.IsNotWindows)
{
return true;
}
if (!IsAssemblyAvailable("System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"))
{
userAlert.Alert("It looks like you don't have the correct version of .NET Framework installed. You will now be directed the download page.");
try
{
Process.Start(DOWNLOAD_LINK);
}
catch (Exception)
{
userAlert.Alert("Oops. Couldn't start your browser. Please visit http://www.microsoft.com/net to download the latest version of .NET Framework");
}
return false;
}
return true;
}
private static bool IsAssemblyAvailable(string assemblyString)
{
try
{
Assembly.Load(assemblyString);
return true;
}
catch (Exception e)
{
Logger.Warn(e, "Couldn't load {0}", assemblyString);
return false;
}
}
}
}

@ -1,60 +0,0 @@
using System.Threading;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
namespace NzbDrone.Host
{
public interface IWaitForExit
{
void Spin();
}
public class SpinService : IWaitForExit
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly IProcessProvider _processProvider;
private readonly IStartupContext _startupContext;
private readonly Logger _logger;
public SpinService(IRuntimeInfo runtimeInfo, IProcessProvider processProvider, IStartupContext startupContext, Logger logger)
{
_runtimeInfo = runtimeInfo;
_processProvider = processProvider;
_startupContext = startupContext;
_logger = logger;
}
public void Spin()
{
while (!_runtimeInfo.IsExiting)
{
Thread.Sleep(1000);
}
_logger.Debug("Wait loop was terminated.");
if (_runtimeInfo.RestartPending)
{
var restartArgs = GetRestartArgs();
_logger.Info("Attempting restart with arguments: {0}", restartArgs);
_processProvider.SpawnNewProcess(_runtimeInfo.ExecutingApplication, restartArgs);
}
}
private string GetRestartArgs()
{
var args = _startupContext.PreservedArguments;
args += " /restart";
if (!args.Contains("/nobrowser"))
{
args += " /nobrowser";
}
return args;
}
}
}

@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using Lidarr.Api.V1.System;
using Lidarr.Http;
using Lidarr.Http.Authentication;
using Lidarr.Http.ErrorManagement;
using Lidarr.Http.Frontend;
using Lidarr.Http.Middleware;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Processes;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Instrumentation;
using NzbDrone.Host.AccessControl;
using NzbDrone.Http.Authentication;
using NzbDrone.SignalR;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(b =>
{
b.ClearProviders();
b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning);
b.AddFilter("Lidarr.Http.Authentication", LogLevel.Information);
b.AddNLog();
});
services.AddRouting(options => options.LowercaseUrls = true);
services.AddResponseCompression();
services.AddCors(options =>
{
options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY,
builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
options.AddPolicy("AllowGet",
builder =>
builder.AllowAnyOrigin()
.WithMethods("GET", "OPTIONS")
.AllowAnyHeader());
});
services
.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddApplicationPart(typeof(SystemController).Assembly)
.AddApplicationPart(typeof(StaticResourceController).Assembly)
.AddJsonOptions(options =>
{
STJson.ApplySerializerSettings(options.JsonSerializerOptions);
})
.AddControllersAsServices();
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddAuthorization(options =>
{
options.AddPolicy("SignalR", policy =>
{
policy.AuthenticationSchemes.Add("SignalR");
policy.RequireAuthenticatedUser();
});
// Require auth on everything except those marked [AllowAnonymous]
options.FallbackPolicy = new AuthorizationPolicyBuilder("API")
.RequireAuthenticatedUser()
.Build();
});
services.AddAppAuthentication();
}
public void Configure(IApplicationBuilder app,
IStartupContext startupContext,
Lazy<IMainDatabase> mainDatabaseFactory,
Lazy<ILogDatabase> logDatabaseFactory,
DatabaseTarget dbTarget,
ISingleInstancePolicy singleInstancePolicy,
InitializeLogger initializeLogger,
ReconfigureLogging reconfigureLogging,
IAppFolderFactory appFolderFactory,
IProvidePidFile pidFileProvider,
IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo,
IFirewallAdapter firewallAdapter,
LidarrErrorPipeline errorHandler)
{
initializeLogger.Initialize();
appFolderFactory.Register();
pidFileProvider.Write();
reconfigureLogging.Reconfigure();
EnsureSingleInstance(false, startupContext, singleInstancePolicy);
// instantiate the databases to initialize/migrate them
_ = mainDatabaseFactory.Value;
_ = logDatabaseFactory.Value;
dbTarget.Register();
if (OsInfo.IsNotWindows)
{
Console.CancelKeyPress += (sender, eventArgs) => NLog.LogManager.Configuration = null;
}
if (OsInfo.IsWindows && runtimeInfo.IsAdmin)
{
firewallAdapter.MakeAccessible();
}
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = errorHandler.HandleException
});
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UseMiddleware<VersionMiddleware>();
app.UseMiddleware<UrlBaseMiddleware>(configFileProvider.UrlBase);
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.UseMiddleware<BufferingMiddleware>(new List<string> { "/api/v1/command" });
app.UseWebSockets();
app.UseEndpoints(x =>
{
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
x.MapControllers();
});
}
private void EnsureSingleInstance(bool isService, IStartupContext startupContext, ISingleInstancePolicy instancePolicy)
{
if (startupContext.Flags.Contains(StartupContext.NO_SINGLE_INSTANCE_CHECK))
{
return;
}
if (startupContext.Flags.Contains(StartupContext.TERMINATE))
{
instancePolicy.KillAllOtherInstance();
}
else if (startupContext.Args.ContainsKey(StartupContext.APPDATA))
{
instancePolicy.WarnIfAlreadyRunning();
}
else if (isService)
{
instancePolicy.KillAllOtherInstance();
}
else
{
instancePolicy.PreventStartIfAlreadyRunning();
}
}
}
}

@ -7,10 +7,13 @@ using IServiceProvider = NzbDrone.Common.IServiceProvider;
namespace NzbDrone.Host
{
public class Router
public interface IUtilityModeRouter
{
void Route(ApplicationModes applicationModes);
}
public class UtilityModeRouter : IUtilityModeRouter
{
private readonly INzbDroneConsoleFactory _nzbDroneConsoleFactory;
private readonly INzbDroneServiceFactory _nzbDroneServiceFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IConsoleService _consoleService;
private readonly IRuntimeInfo _runtimeInfo;
@ -18,17 +21,13 @@ namespace NzbDrone.Host
private readonly IRemoteAccessAdapter _remoteAccessAdapter;
private readonly Logger _logger;
public Router(INzbDroneConsoleFactory nzbDroneConsoleFactory,
INzbDroneServiceFactory nzbDroneServiceFactory,
IServiceProvider serviceProvider,
public UtilityModeRouter(IServiceProvider serviceProvider,
IConsoleService consoleService,
IRuntimeInfo runtimeInfo,
IProcessProvider processProvider,
IRemoteAccessAdapter remoteAccessAdapter,
Logger logger)
{
_nzbDroneConsoleFactory = nzbDroneConsoleFactory;
_nzbDroneServiceFactory = nzbDroneServiceFactory;
_serviceProvider = serviceProvider;
_consoleService = consoleService;
_runtimeInfo = runtimeInfo;
@ -43,20 +42,6 @@ namespace NzbDrone.Host
switch (applicationModes)
{
case ApplicationModes.Service:
{
_logger.Debug("Service selected");
_serviceProvider.Run(_nzbDroneServiceFactory.Build());
break;
}
case ApplicationModes.Interactive:
{
_logger.Debug(_runtimeInfo.IsWindowsTray ? "Tray selected" : "Console selected");
_nzbDroneConsoleFactory.Start();
break;
}
case ApplicationModes.InstallService:
{
_logger.Debug("Install Service selected");

@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using NzbDrone.Common.Composition;
namespace NzbDrone.Host
{
public class ControllerActivator : IControllerActivator
{
private readonly IContainer _container;
public ControllerActivator(IContainer container)
{
_container = container;
}
public object Create(ControllerContext context)
{
return _container.Resolve(context.ActionDescriptor.ControllerTypeInfo.AsType());
}
public void Release(ControllerContext context, object controller)
{
// Nothing to do
}
}
}

@ -1,272 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Lidarr.Api.V1.System;
using Lidarr.Http;
using Lidarr.Http.Authentication;
using Lidarr.Http.ErrorManagement;
using Lidarr.Http.Frontend;
using Lidarr.Http.Middleware;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Host.AccessControl;
using NzbDrone.SignalR;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace NzbDrone.Host
{
public class WebHostController : IHostController
{
private readonly IContainer _container;
private readonly IRuntimeInfo _runtimeInfo;
private readonly IConfigFileProvider _configFileProvider;
private readonly IFirewallAdapter _firewallAdapter;
private readonly LidarrErrorPipeline _errorHandler;
private readonly Logger _logger;
private IWebHost _host;
public WebHostController(IContainer container,
IRuntimeInfo runtimeInfo,
IConfigFileProvider configFileProvider,
IFirewallAdapter firewallAdapter,
LidarrErrorPipeline errorHandler,
Logger logger)
{
_container = container;
_runtimeInfo = runtimeInfo;
_configFileProvider = configFileProvider;
_firewallAdapter = firewallAdapter;
_errorHandler = errorHandler;
_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 =>
{
X509Certificate2 certificate;
try
{
certificate = new X509Certificate2(sslCertPath, _configFileProvider.SslCertPassword, X509KeyStorageFlags.DefaultKeySet);
}
catch (CryptographicException ex)
{
if (ex.HResult == 0x2 || ex.HResult == 0x2006D080)
{
throw new LidarrStartupException(ex, $"The SSL certificate file {sslCertPath} does not exist");
}
throw new LidarrStartupException(ex);
}
configureOptions.ServerCertificate = certificate;
});
}
})
.ConfigureKestrel(serverOptions =>
{
serverOptions.AllowSynchronousIO = true;
serverOptions.Limits.MaxRequestBodySize = null;
})
.ConfigureLogging(logging =>
{
logging.AddProvider(new NLogLoggerProvider());
logging.SetMinimumLevel(LogLevel.Warning);
})
.ConfigureServices(services =>
{
// So that we can resolve containers with our TinyIoC services
services.AddSingleton(_container);
services.AddSingleton<IControllerActivator, ControllerActivator>();
// Bits used in our custom middleware
services.AddSingleton(_container.Resolve<LidarrErrorPipeline>());
services.AddSingleton(_container.Resolve<ICacheableSpecification>());
// Used in authentication
services.AddSingleton(_container.Resolve<IAuthenticationService>());
services.AddRouting(options => options.LowercaseUrls = true);
services.AddResponseCompression();
services.AddCors(options =>
{
options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY,
builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
options.AddPolicy("AllowGet",
builder =>
builder.AllowAnyOrigin()
.WithMethods("GET", "OPTIONS")
.AllowAnyHeader());
});
services
.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddApplicationPart(typeof(SystemController).Assembly)
.AddApplicationPart(typeof(StaticResourceController).Assembly)
.AddJsonOptions(options =>
{
STJson.ApplySerializerSettings(options.JsonSerializerOptions);
});
services
.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddAuthorization(options =>
{
options.AddPolicy("UI", policy =>
{
policy.AuthenticationSchemes.Add(_configFileProvider.AuthenticationMethod.ToString());
policy.RequireAuthenticatedUser();
});
options.AddPolicy("SignalR", policy =>
{
policy.AuthenticationSchemes.Add("SignalR");
policy.RequireAuthenticatedUser();
});
// Require auth on everything except those marked [AllowAnonymous]
options.DefaultPolicy = new AuthorizationPolicyBuilder("API")
.RequireAuthenticatedUser()
.Build();
});
services.AddAppAuthentication(_configFileProvider);
})
.Configure(app =>
{
app.UseMiddleware<LoggingMiddleware>();
app.UsePathBase(new PathString(_configFileProvider.UrlBase));
app.UseExceptionHandler(new ExceptionHandlerOptions
{
AllowStatusCode404Response = true,
ExceptionHandler = _errorHandler.HandleException
});
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.Properties["host.AppName"] = BuildInfo.AppName;
app.UseMiddleware<VersionMiddleware>();
app.UseMiddleware<UrlBaseMiddleware>(_configFileProvider.UrlBase);
app.UseMiddleware<CacheHeaderMiddleware>();
app.UseMiddleware<IfModifiedMiddleware>();
app.Use((context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/v1/command", StringComparison.CurrentCultureIgnoreCase))
{
context.Request.EnableBuffering();
}
return next();
});
app.UseWebSockets();
app.UseEndpoints(x =>
{
x.MapHub<MessageHub>("/signalr/messages").RequireAuthorization("SignalR");
x.MapControllers();
});
// 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).
_container.Register(app.ApplicationServices);
_container.Register(app.ApplicationServices.GetService<IHubContext<MessageHub>>());
_container.Register(app.ApplicationServices.GetService<IActionDescriptorCollectionProvider>());
_container.Register(app.ApplicationServices.GetService<EndpointDataSource>());
_container.Register(app.ApplicationServices.GetService<DfaGraphWriter>());
})
.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}";
}
}
}

@ -164,7 +164,7 @@ namespace NzbDrone.Test.Common.AutoMoq
RegisterPlatformLibrary(container);
AddTheAutoMockingContainerExtensionToTheContainer(container);
ContainerBuilderBase.RegisterSQLiteResolver();
AssemblyLoader.RegisterSQLiteResolver();
}
private static void AddTheAutoMockingContainerExtensionToTheContainer(IUnityContainer container)

@ -4,6 +4,8 @@
<TargetFrameworks>net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
<PackageReference Include="NLog" Version="4.7.9" />
</ItemGroup>
<ItemGroup>

@ -1,11 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DryIoc;
using NLog;
using NzbDrone.Common.Composition;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Processes;
using NzbDrone.Update.UpdateEngine;
@ -18,8 +21,6 @@ namespace NzbDrone.Update
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(UpdateApp));
private static IContainer _container;
public UpdateApp(IInstallUpdateService installUpdateService, IProcessProvider processProvider)
{
_installUpdateService = installUpdateService;
@ -35,9 +36,13 @@ namespace NzbDrone.Update
Logger.Info("Starting Lidarr Update Client");
_container = UpdateContainerBuilder.Build(startupContext);
_container.Resolve<InitializeLogger>().Initialize();
_container.Resolve<UpdateApp>().Start(args);
var container = new Container(rules => rules.WithNzbDroneRules())
.AutoAddServices(new List<string> { "Lidarr.Update" })
.AddNzbDroneLogger()
.AddStartupContext(startupContext);
container.Resolve<InitializeLogger>().Initialize();
container.Resolve<UpdateApp>().Start(args);
Logger.Info("Update completed successfully");
}

@ -1,24 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Composition;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Update
{
public class UpdateContainerBuilder : ContainerBuilderBase
{
private UpdateContainerBuilder(IStartupContext startupContext, List<string> assemblies)
: base(startupContext, assemblies)
{
}
public static IContainer Build(IStartupContext startupContext)
{
var assemblies = new List<string>
{
"Lidarr.Update"
};
return new UpdateContainerBuilder(startupContext, assemblies).Container;
}
}
}

@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Host\Lidarr.Host.csproj" />
<ProjectReference Include="..\NzbDrone.Windows\Lidarr.Windows.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">

@ -1,13 +0,0 @@
using System.Windows.Forms;
using NzbDrone.Host;
namespace NzbDrone
{
public class MessageBoxUserAlert : IUserAlert
{
public void Alert(string message)
{
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Warning, caption: "NzbDrone");
}
}
}

@ -1,6 +1,9 @@
using System;
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Extensions.Hosting;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
@ -8,12 +11,7 @@ using NzbDrone.Host;
namespace NzbDrone.SysTray
{
public interface ISystemTrayApp
{
void Start();
}
public class SystemTrayApp : Form, ISystemTrayApp
public class SystemTrayApp : Form, IHostedService
{
private readonly IBrowserService _browserService;
private readonly IRuntimeInfo _runtimeInfo;
@ -34,8 +32,12 @@ namespace NzbDrone.SysTray
Application.ThreadException += OnThreadException;
Application.ApplicationExit += OnApplicationExit;
Application.SetHighDpiMode(HighDpiMode.PerMonitor);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
_trayMenu.Items.Add(new ToolStripMenuItem("Launch Browser", null, LaunchBrowser));
_trayMenu.Items.Add(new ToolStripMenuItem("-"));
_trayMenu.Items.Add(new ToolStripSeparator());
_trayMenu.Items.Add(new ToolStripMenuItem("Exit", null, OnExit));
_trayIcon.Text = string.Format("Lidarr - {0}", BuildInfo.Version);
@ -48,6 +50,20 @@ namespace NzbDrone.SysTray
Application.Run(this);
}
public Task StartAsync(CancellationToken cancellationToken)
{
var thread = new Thread(Start);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
protected override void OnClosing(CancelEventArgs e)
{
DisposeTrayIcon();

@ -1,5 +1,7 @@
using System;
using System;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation;
@ -20,11 +22,9 @@ namespace NzbDrone
NzbDroneLogger.Register(startupArgs, false, true);
Bootstrap.Start(startupArgs, new MessageBoxUserAlert(), container =>
Bootstrap.Start(args, e =>
{
container.Register<ISystemTrayApp, SystemTrayApp>();
var trayApp = container.Resolve<ISystemTrayApp>();
trayApp.Start();
e.ConfigureServices((_, s) => s.AddSingleton<IHostedService, SystemTrayApp>());
});
}
catch (Exception e)

Loading…
Cancel
Save