Robert Dailey 5 months ago
parent 537f225542
commit b6a82b5bd1

@ -21,9 +21,9 @@
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Spectre.Console" Version="0.48.0" /> <PackageVersion Include="Spectre.Console" Version="0.48.1-preview.0.7" />
<PackageVersion Include="Spectre.Console.Analyzer" Version="0.48.0" /> <PackageVersion Include="Spectre.Console.Analyzer" Version="0.48.0" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.48.0" /> <PackageVersion Include="Spectre.Console.Cli" Version="0.48.1-preview.0.7" />
<PackageVersion Include="SuperLinq" Version="5.4.0" /> <PackageVersion Include="SuperLinq" Version="5.4.0" />
<PackageVersion Include="System.Data.HashFunction.FNV" Version="2.0.0" /> <PackageVersion Include="System.Data.HashFunction.FNV" Version="2.0.0" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" /> <PackageVersion Include="System.Private.Uri" Version="4.3.2" />
@ -54,7 +54,7 @@
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Serilog.Sinks.Observable" Version="2.0.2" /> <PackageVersion Include="Serilog.Sinks.Observable" Version="2.0.2" />
<PackageVersion Include="Serilog.Sinks.NUnit" Version="1.0.3" /> <PackageVersion Include="Serilog.Sinks.NUnit" Version="1.0.3" />
<PackageVersion Include="Spectre.Console.Testing" Version="0.48.0" /> <PackageVersion Include="Spectre.Console.Testing" Version="0.48.1-preview.0.7" />
<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="20.0.4" /> <PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
</ItemGroup> </ItemGroup>
<!-- Following found during vulerabilities Code Scan --> <!-- Following found during vulerabilities Code Scan -->

@ -4,6 +4,7 @@ using Autofac;
using Autofac.Extras.Ordering; using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection; using AutoMapper.Contrib.Autofac.DependencyInjection;
using Recyclarr.Cli.Cache; using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Interceptors;
using Recyclarr.Cli.Console.Setup; using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging; using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration; using Recyclarr.Cli.Migration;
@ -63,7 +64,7 @@ public static class CompositionRoot
builder.RegisterAutoMapper(thisAssembly); builder.RegisterAutoMapper(thisAssembly);
CommandRegistrations(builder); CliRegistrations(builder);
PipelineRegistrations(builder); PipelineRegistrations(builder);
} }
@ -93,28 +94,24 @@ public static class CompositionRoot
private static void RegisterLogger(ContainerBuilder builder) private static void RegisterLogger(ContainerBuilder builder)
{ {
builder.RegisterType<LogJanitor>().As<ILogJanitor>(); builder.RegisterType<LogJanitor>().As<ILogJanitor>();
builder.RegisterType<LoggingLevelSwitch>().SingleInstance();
builder.RegisterType<LoggerFactory>(); builder.RegisterType<LoggerFactory>();
builder.Register(c => c.Resolve<LoggerFactory>().Create()).As<ILogger>().SingleInstance(); builder.Register(c => c.Resolve<LoggerFactory>().Create()).As<ILogger>().SingleInstance();
} }
private static void CommandRegistrations(ContainerBuilder builder) private static void CliRegistrations(ContainerBuilder builder)
{ {
builder.RegisterType<BaseCommandSetupInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<VersionLogInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<GlobalTaskInterceptor>().As<ICommandInterceptor>();
builder.RegisterTypes( builder.RegisterTypes(
typeof(AppPathSetupTask), typeof(AppPathSetupTask),
typeof(JanitorCleanupTask)) typeof(JanitorCleanupTask))
.As<IBaseCommandSetupTask>() .As<IGlobalSetupTask>()
.OrderByRegistration(); .OrderByRegistration();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AssignableTo<CommandSettings>(); .AssignableTo<CommandSettings>();
} }
public static void RegisterExternal(
ContainerBuilder builder,
LoggingLevelSwitch logLevelSwitch,
AppDataPathProvider appDataPathProvider)
{
builder.RegisterInstance(logLevelSwitch);
builder.RegisterInstance(appDataPathProvider);
}
} }

@ -3,7 +3,7 @@ using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Helpers; namespace Recyclarr.Cli.Console.Helpers;
internal class AutofacTypeRegistrar(ContainerBuilder builder, Action<ILifetimeScope> assignScope) internal class AutofacTypeRegistrar(ContainerBuilder builder)
: ITypeRegistrar : ITypeRegistrar
{ {
public void Register(Type service, Type implementation) public void Register(Type service, Type implementation)
@ -23,8 +23,6 @@ internal class AutofacTypeRegistrar(ContainerBuilder builder, Action<ILifetimeSc
public ITypeResolver Build() public ITypeResolver Build()
{ {
var container = builder.Build(); return new AutofacTypeResolver(builder.Build());
assignScope(container);
return new AutofacTypeResolver(container);
} }
} }

@ -1,23 +1,17 @@
using System.Reactive; using Recyclarr.Cli.Console.Commands;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Console.Helpers; using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Platform; using Recyclarr.Platform;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using Spectre.Console.Cli; using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Setup; namespace Recyclarr.Cli.Console.Interceptors;
public class CliInterceptor(LoggingLevelSwitch loggingLevelSwitch, AppDataPathProvider appDataPathProvider) public class BaseCommandSetupInterceptor(LoggingLevelSwitch loggingLevelSwitch, IAppDataSetup appDataSetup)
: ICommandInterceptor : ICommandInterceptor
{ {
private readonly Subject<Unit> _interceptedSubject = new();
private readonly ConsoleAppCancellationTokenSource _ct = new(); private readonly ConsoleAppCancellationTokenSource _ct = new();
public IObservable<Unit> OnIntercepted => _interceptedSubject.AsObservable();
public void Intercept(CommandContext context, CommandSettings settings) public void Intercept(CommandContext context, CommandSettings settings)
{ {
switch (settings) switch (settings)
@ -30,22 +24,17 @@ public class CliInterceptor(LoggingLevelSwitch loggingLevelSwitch, AppDataPathPr
HandleBaseCommand(cmd); HandleBaseCommand(cmd);
break; break;
} }
_interceptedSubject.OnNext(Unit.Default);
_interceptedSubject.OnCompleted();
} }
private void HandleServiceCommand(ServiceCommandSettings cmd) private void HandleServiceCommand(ServiceCommandSettings cmd)
{ {
HandleBaseCommand(cmd); HandleBaseCommand(cmd);
appDataSetup.AppDataDirectoryOverride = cmd.AppData;
appDataPathProvider.AppDataPath = cmd.AppData;
} }
private void HandleBaseCommand(BaseCommandSettings cmd) private void HandleBaseCommand(BaseCommandSettings cmd)
{ {
cmd.CancellationToken = _ct.Token; cmd.CancellationToken = _ct.Token;
loggingLevelSwitch.MinimumLevel = cmd.Debug switch loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{ {
true => LogEventLevel.Debug, true => LogEventLevel.Debug,

@ -0,0 +1,17 @@
using Recyclarr.Cli.Console.Setup;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Interceptors;
public class GlobalTaskInterceptor(IOrderedEnumerable<IGlobalSetupTask> tasks) : ICommandInterceptor
{
public void Intercept(CommandContext context, CommandSettings settings)
{
tasks.ForEach(x => x.OnStart());
}
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
tasks.Reverse().ForEach(x => x.OnFinish());
}
}

@ -0,0 +1,11 @@
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Interceptors;
public class VersionLogInterceptor(ILogger log) : ICommandInterceptor
{
public void Intercept(CommandContext context, CommandSettings settings)
{
log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
}
}

@ -2,7 +2,7 @@ using Recyclarr.Platform;
namespace Recyclarr.Cli.Console.Setup; namespace Recyclarr.Cli.Console.Setup;
public class AppPathSetupTask(ILogger log, IAppPaths paths) : IBaseCommandSetupTask public class AppPathSetupTask(ILogger log, IAppPaths paths) : IGlobalSetupTask
{ {
public void OnStart() public void OnStart()
{ {

@ -1,6 +1,6 @@
namespace Recyclarr.Cli.Console.Setup; namespace Recyclarr.Cli.Console.Setup;
public interface IBaseCommandSetupTask public interface IGlobalSetupTask
{ {
void OnStart(); void OnStart();
void OnFinish(); void OnFinish();

@ -4,7 +4,7 @@ using Recyclarr.Settings;
namespace Recyclarr.Cli.Console.Setup; namespace Recyclarr.Cli.Console.Setup;
public class JanitorCleanupTask(ILogJanitor janitor, ILogger log, ISettingsProvider settingsProvider) public class JanitorCleanupTask(ILogJanitor janitor, ILogger log, ISettingsProvider settingsProvider)
: IBaseCommandSetupTask : IGlobalSetupTask
{ {
public void OnStart() public void OnStart()
{ {

@ -2,9 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using Autofac; using Autofac;
using Recyclarr.Cli.Console; using Recyclarr.Cli.Console;
using Recyclarr.Cli.Console.Helpers; using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Platform;
using Serilog.Core;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Cli; using Spectre.Console.Cli;
@ -12,21 +9,13 @@ namespace Recyclarr.Cli;
internal static class Program internal static class Program
{ {
private static ILifetimeScope? _scope;
private static IBaseCommandSetupTask[] _tasks = Array.Empty<IBaseCommandSetupTask>();
private static ILogger? _log;
[SuppressMessage("Design", "CA1031:Do not catch general exception types")] [SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public static int Main(string[] args) public static int Main(string[] args)
{ {
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
CompositionRoot.Setup(builder); CompositionRoot.Setup(builder);
var logLevelSwitch = new LoggingLevelSwitch(); var app = new CommandApp(new AutofacTypeRegistrar(builder));
var appDataPathProvider = new AppDataPathProvider();
CompositionRoot.RegisterExternal(builder, logLevelSwitch, appDataPathProvider);
var app = new CommandApp(new AutofacTypeRegistrar(builder, s => _scope = s));
app.Configure(config => app.Configure(config =>
{ {
#if DEBUG #if DEBUG
@ -34,60 +23,29 @@ internal static class Program
config.ValidateExamples(); config.ValidateExamples();
#endif #endif
config.Settings.PropagateExceptions = true; // config.Settings.PropagateExceptions = true;
config.Settings.StrictParsing = true; config.Settings.StrictParsing = true;
config.SetApplicationName("recyclarr"); config.SetApplicationName("recyclarr");
config.SetApplicationVersion( config.SetApplicationVersion(
$"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})"); $"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})");
var interceptor = new CliInterceptor(logLevelSwitch, appDataPathProvider); config.SetExceptionHandler((ex, resolver) =>
interceptor.OnIntercepted.Subscribe(_ => OnAppInitialized());
config.SetInterceptor(interceptor);
CliSetup.Commands(config);
});
var result = 1;
try
{ {
result = app.Run(args); var log = (ILogger?) resolver?.Resolve(typeof(ILogger));
} if (log is null)
catch (Exception ex)
{
if (_log is null)
{ {
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything); AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
} }
else else
{ {
_log.Error(ex, "Non-recoverable Exception"); log.Error(ex, "Non-recoverable Exception");
}
}
finally
{
OnAppCleanup();
} }
});
return result; CliSetup.Commands(config);
} });
private static void OnAppInitialized()
{
if (_scope is null)
{
throw new InvalidProgramException("Composition root is not initialized");
}
_log = _scope.Resolve<ILogger>();
_log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
_tasks = _scope.Resolve<IOrderedEnumerable<IBaseCommandSetupTask>>().ToArray();
_tasks.ForEach(x => x.OnStart());
}
private static void OnAppCleanup() return app.Run(args);
{
_tasks.Reverse().ForEach(x => x.OnFinish());
} }
} }

@ -1,6 +0,0 @@
namespace Recyclarr.Platform;
public class AppDataPathProvider
{
public string? AppDataPath { get; set; }
}

@ -5,11 +5,13 @@ namespace Recyclarr.Platform;
public class DefaultAppDataSetup( public class DefaultAppDataSetup(
IEnvironment env, IEnvironment env,
IFileSystem fs, IFileSystem fs,
IRuntimeInformation runtimeInfo) IRuntimeInformation runtimeInfo) : IAppDataSetup
{ {
public IAppPaths CreateAppPaths(string? appDataDirectoryOverride = null) public string? AppDataDirectoryOverride { get; set; }
public IAppPaths CreateAppPaths()
{ {
var appDir = GetAppDataDirectory(appDataDirectoryOverride); var appDir = GetAppDataDirectory(AppDataDirectoryOverride);
return new AppPaths(fs.DirectoryInfo.New(appDir)); return new AppPaths(fs.DirectoryInfo.New(appDir));
} }

@ -0,0 +1,6 @@
namespace Recyclarr.Platform;
public interface IAppDataSetup
{
public string? AppDataDirectoryOverride { get; set; }
}

@ -12,16 +12,11 @@ public class PlatformAutofacModule : Module
private static void RegisterAppPaths(ContainerBuilder builder) private static void RegisterAppPaths(ContainerBuilder builder)
{ {
builder.RegisterType<DefaultAppDataSetup>(); builder.RegisterType<DefaultAppDataSetup>().As<IAppDataSetup>().AsSelf().SingleInstance();
builder.RegisterType<DefaultEnvironment>().As<IEnvironment>(); builder.RegisterType<DefaultEnvironment>().As<IEnvironment>();
builder.RegisterType<DefaultRuntimeInformation>().As<IRuntimeInformation>(); builder.RegisterType<DefaultRuntimeInformation>().As<IRuntimeInformation>();
builder.Register(c => builder.Register(c => c.Resolve<DefaultAppDataSetup>().CreateAppPaths())
{
var appData = c.Resolve<AppDataPathProvider>();
var dataSetup = c.Resolve<DefaultAppDataSetup>();
return dataSetup.CreateAppPaths(appData.AppDataPath);
})
.As<IAppPaths>() .As<IAppPaths>()
.SingleInstance(); .SingleInstance();
} }

@ -11,7 +11,7 @@ internal class BaseCommandSetupIntegrationTest : CliIntegrationFixture
[Test] [Test]
public void Base_command_startup_tasks_are_registered() public void Base_command_startup_tasks_are_registered()
{ {
var registrations = Resolve<IEnumerable<IBaseCommandSetupTask>>(); var registrations = Resolve<IEnumerable<IGlobalSetupTask>>();
registrations.Select(x => x.GetType()).Should().BeEquivalentTo(new[] registrations.Select(x => x.GetType()).Should().BeEquivalentTo(new[]
{ {
typeof(JanitorCleanupTask), typeof(JanitorCleanupTask),

@ -3,9 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Autofac; using Autofac;
using Autofac.Core; using Autofac.Core;
using NUnit.Framework.Internal; using NUnit.Framework.Internal;
using Recyclarr.Platform;
using Recyclarr.TestLibrary.Autofac; using Recyclarr.TestLibrary.Autofac;
using Serilog.Core;
using Spectre.Console; using Spectre.Console;
namespace Recyclarr.Cli.IntegrationTests; namespace Recyclarr.Cli.IntegrationTests;
@ -22,7 +20,6 @@ public class CompositionRootTest
{ {
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
CompositionRoot.Setup(builder); CompositionRoot.Setup(builder);
CompositionRoot.RegisterExternal(builder, new LoggingLevelSwitch(), new AppDataPathProvider());
// These are things that Spectre.Console normally registers for us, so they won't explicitly be // These are things that Spectre.Console normally registers for us, so they won't explicitly be
// in the CompositionRoot. Register mocks/stubs here. // in the CompositionRoot. Register mocks/stubs here.

@ -34,7 +34,8 @@ public class DefaultAppDataSetupTest
.SubDirectory("override") .SubDirectory("override")
.SubDirectory("path"); .SubDirectory("path");
var paths = sut.CreateAppPaths(overridePath.FullName); sut.AppDataDirectoryOverride = overridePath.FullName;
var paths = sut.CreateAppPaths();
paths.AppDataDirectory.FullName.Should().Be(overridePath.FullName); paths.AppDataDirectory.FullName.Should().Be(overridePath.FullName);
} }
@ -88,7 +89,8 @@ public class DefaultAppDataSetupTest
.SubDirectory("var") .SubDirectory("var")
.SubDirectory("path").FullName; .SubDirectory("path").FullName;
sut.CreateAppPaths(expectedPath); sut.AppDataDirectoryOverride = expectedPath;
sut.CreateAppPaths();
env.DidNotReceiveWithAnyArgs().GetEnvironmentVariable(default!); env.DidNotReceiveWithAnyArgs().GetEnvironmentVariable(default!);
fs.AllDirectories.Should().Contain(expectedPath); fs.AllDirectories.Should().Contain(expectedPath);

Loading…
Cancel
Save