refactor: Remove DI hacks and workarounds for Spectre.Console

pull/254/head
Robert Dailey 9 months ago
parent 1d1eb62ae1
commit 1963ef09b1

@ -4,6 +4,7 @@ using Autofac;
using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Interceptors;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration;
@ -63,7 +64,7 @@ public static class CompositionRoot
builder.RegisterAutoMapper(thisAssembly);
CommandRegistrations(builder);
CliRegistrations(builder);
PipelineRegistrations(builder);
}
@ -89,28 +90,24 @@ public static class CompositionRoot
private static void RegisterLogger(ContainerBuilder builder)
{
builder.RegisterType<LogJanitor>().As<ILogJanitor>();
builder.RegisterType<LoggingLevelSwitch>().SingleInstance();
builder.RegisterType<LoggerFactory>();
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(
typeof(AppPathSetupTask),
typeof(JanitorCleanupTask))
.As<IBaseCommandSetupTask>()
.As<IGlobalSetupTask>()
.OrderByRegistration();
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.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;
internal class AutofacTypeRegistrar(ContainerBuilder builder, Action<ILifetimeScope> assignScope)
internal class AutofacTypeRegistrar(ContainerBuilder builder)
: ITypeRegistrar
{
public void Register(Type service, Type implementation)
@ -23,8 +23,6 @@ internal class AutofacTypeRegistrar(ContainerBuilder builder, Action<ILifetimeSc
public ITypeResolver Build()
{
var container = builder.Build();
assignScope(container);
return new AutofacTypeResolver(container);
return new AutofacTypeResolver(builder.Build());
}
}

@ -1,23 +1,17 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Platform;
using Serilog.Core;
using Serilog.Events;
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
{
private readonly Subject<Unit> _interceptedSubject = new();
private readonly ConsoleAppCancellationTokenSource _ct = new();
public IObservable<Unit> OnIntercepted => _interceptedSubject.AsObservable();
public void Intercept(CommandContext context, CommandSettings settings)
{
switch (settings)
@ -30,22 +24,17 @@ public class CliInterceptor(LoggingLevelSwitch loggingLevelSwitch, AppDataPathPr
HandleBaseCommand(cmd);
break;
}
_interceptedSubject.OnNext(Unit.Default);
_interceptedSubject.OnCompleted();
}
private void HandleServiceCommand(ServiceCommandSettings cmd)
{
HandleBaseCommand(cmd);
appDataPathProvider.AppDataPath = cmd.AppData;
appDataSetup.AppDataDirectoryOverride = cmd.AppData;
}
private void HandleBaseCommand(BaseCommandSettings cmd)
{
cmd.CancellationToken = _ct.Token;
loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{
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;
public class AppPathSetupTask(ILogger log, IAppPaths paths) : IBaseCommandSetupTask
public class AppPathSetupTask(ILogger log, IAppPaths paths) : IGlobalSetupTask
{
public void OnStart()
{

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

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

@ -1,10 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
using Recyclarr.Cli.Console;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Platform;
using Serilog.Core;
using Spectre.Console;
using Spectre.Console.Cli;
@ -12,21 +8,12 @@ namespace Recyclarr.Cli;
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")]
public static int Main(string[] args)
{
var builder = new ContainerBuilder();
CompositionRoot.Setup(builder);
var logLevelSwitch = new LoggingLevelSwitch();
var appDataPathProvider = new AppDataPathProvider();
CompositionRoot.RegisterExternal(builder, logLevelSwitch, appDataPathProvider);
var app = new CommandApp(new AutofacTypeRegistrar(builder, s => _scope = s));
var app = new CommandApp(new AutofacTypeRegistrar(builder));
app.Configure(config =>
{
#if DEBUG
@ -34,60 +21,29 @@ internal static class Program
config.ValidateExamples();
#endif
config.Settings.PropagateExceptions = true;
// config.Settings.PropagateExceptions = true;
config.Settings.StrictParsing = true;
config.SetApplicationName("recyclarr");
config.SetApplicationVersion(
$"v{GitVersionInformation.SemVer} ({GitVersionInformation.FullBuildMetaData})");
var interceptor = new CliInterceptor(logLevelSwitch, appDataPathProvider);
interceptor.OnIntercepted.Subscribe(_ => OnAppInitialized());
config.SetInterceptor(interceptor);
CliSetup.Commands(config);
});
var result = 1;
try
config.SetExceptionHandler((ex, resolver) =>
{
result = app.Run(args);
}
catch (Exception ex)
{
if (_log is null)
var log = (ILogger?) resolver?.Resolve(typeof(ILogger));
if (log is null)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything);
}
else
{
_log.Error(ex, "Non-recoverable Exception");
}
}
finally
{
OnAppCleanup();
log.Error(ex, "Non-recoverable Exception");
}
});
return result;
}
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());
}
CliSetup.Commands(config);
});
private static void OnAppCleanup()
{
_tasks.Reverse().ForEach(x => x.OnFinish());
return app.Run(args);
}
}

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

@ -2,11 +2,13 @@ using System.IO.Abstractions;
namespace Recyclarr.Platform;
public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs)
public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs) : 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));
}

@ -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)
{
builder.RegisterType<DefaultAppDataSetup>();
builder.RegisterType<DefaultAppDataSetup>().As<IAppDataSetup>().AsSelf().SingleInstance();
builder.RegisterType<DefaultEnvironment>().As<IEnvironment>();
builder.RegisterType<DefaultRuntimeInformation>().As<IRuntimeInformation>();
builder.Register(c =>
{
var appData = c.Resolve<AppDataPathProvider>();
var dataSetup = c.Resolve<DefaultAppDataSetup>();
return dataSetup.CreateAppPaths(appData.AppDataPath);
})
builder.Register(c => c.Resolve<DefaultAppDataSetup>().CreateAppPaths())
.As<IAppPaths>()
.SingleInstance();
}

@ -11,7 +11,7 @@ internal class BaseCommandSetupIntegrationTest : CliIntegrationFixture
[Test]
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[]
{
typeof(JanitorCleanupTask),

@ -3,9 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Autofac;
using Autofac.Core;
using NUnit.Framework.Internal;
using Recyclarr.Platform;
using Recyclarr.TestLibrary.Autofac;
using Serilog.Core;
using Spectre.Console;
namespace Recyclarr.Cli.IntegrationTests;
@ -22,7 +20,6 @@ public class CompositionRootTest
{
var builder = new ContainerBuilder();
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
// in the CompositionRoot. Register mocks/stubs here.

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

Loading…
Cancel
Save