refactor: New command structure

The goal is to separate initialization logic from command business
logic. Some initialization requires modifying the environment before we
instantiate many objects needed for implementing command behavior. If
those objects get instantiated, they will most likely already start
using files/directories/environment on the system and we can't modify
those while they're in use.
pull/76/head
Robert Dailey 2 years ago
parent 42e41da177
commit 243d076087

@ -23,8 +23,9 @@ public class CliTypeActivatorTest
{
public bool Preview => false;
public bool Debug => false;
public ICollection<string>? Config => null;
public ICollection<string> Config => new List<string>();
public string CacheStoragePath => "";
public string Name => "";
}
[Test]

@ -1,41 +1,45 @@
using AutoFixture.NUnit3;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using Recyclarr.Command;
using Recyclarr.Command.Services;
using TestLibrary.AutoFixture;
using TrashLib.Sonarr;
namespace Recyclarr.Tests.Command;
namespace Recyclarr.Tests.Command.Services;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class SonarrCommandTest
public class SonarrServiceTest
{
[Test, AutoMockData]
public async Task List_terms_without_value_fails(
IConsole console,
SonarrCommand sut)
[Frozen] ISonarrCommand cmd,
SonarrService sut)
{
cmd.ListReleaseProfiles.Returns(false);
// When `--list-terms` is specified on the command line without a value, it gets a `null` value assigned.
sut.ListTerms = null;
cmd.ListTerms.Returns((string?) null);
var act = () => sut.ExecuteAsync(console).AsTask();
var act = () => sut.Execute(cmd);
await act.Should().ThrowAsync<CommandException>();
}
[Test, AutoMockData]
public async Task List_terms_with_empty_value_fails(
IConsole console,
SonarrCommand sut)
[Frozen] ISonarrCommand cmd,
SonarrService sut)
{
cmd.ListReleaseProfiles.Returns(false);
// If the user specifies a blank string as the value, it should still fail.
sut.ListTerms = "";
cmd.ListTerms.Returns("");
var act = () => sut.ExecuteAsync(console).AsTask();
var act = () => sut.Execute(cmd);
await act.Should().ThrowAsync<CommandException>();
}
@ -43,12 +47,14 @@ public class SonarrCommandTest
[Test, AutoMockData]
public async Task List_terms_uses_specified_trash_id(
[Frozen] IReleaseProfileLister lister,
IConsole console,
SonarrCommand sut)
[Frozen] ISonarrCommand cmd,
SonarrService sut)
{
sut.ListTerms = "some_id";
cmd.ListReleaseProfiles.Returns(false);
cmd.ListTerms.Returns("some_id");
await sut.ExecuteAsync(console);
await sut.Execute(cmd);
lister.Received().ListTerms("some_id");
}
@ -56,12 +62,13 @@ public class SonarrCommandTest
[Test, AutoMockData]
public async Task List_release_profiles_is_invoked(
[Frozen] IReleaseProfileLister lister,
IConsole console,
SonarrCommand sut)
[Frozen] ISonarrCommand cmd,
SonarrService sut)
{
sut.ListReleaseProfiles = true;
cmd.ListReleaseProfiles.Returns(true);
cmd.ListTerms.Returns((string?) null);
await sut.ExecuteAsync(console);
await sut.Execute(cmd);
lister.Received().ListReleaseProfiles();
}

@ -11,10 +11,10 @@ using FluentValidation.Results;
using JetBrains.Annotations;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Config;
using TestLibrary;
using TestLibrary.AutoFixture;
using TestLibrary.NSubstitute;
using Recyclarr.Config;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Sonarr.Config;

@ -0,0 +1,6 @@
namespace Recyclarr.Command.Helpers;
public interface IServiceInitialization
{
void Initialize(IServiceCommand cmd);
}

@ -0,0 +1,75 @@
using Common.Networking;
using Flurl.Http;
using Flurl.Http.Configuration;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Repo;
namespace Recyclarr.Command.Helpers;
internal class ServiceInitialization : IServiceInitialization
{
private readonly ILogger _log;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
private readonly ISettingsPersister _settingsPersister;
private readonly ISettingsProvider _settingsProvider;
private readonly IRepoUpdater _repoUpdater;
public ServiceInitialization(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ISettingsPersister settingsPersister,
ISettingsProvider settingsProvider,
IRepoUpdater repoUpdater)
{
_log = log;
_loggingLevelSwitch = loggingLevelSwitch;
_settingsPersister = settingsPersister;
_settingsProvider = settingsProvider;
_repoUpdater = repoUpdater;
}
public void Initialize(IServiceCommand cmd)
{
// Must happen first because everything can use the logger.
_loggingLevelSwitch.MinimumLevel = cmd.Debug ? LogEventLevel.Debug : LogEventLevel.Information;
// Has to happen right after logging because stuff below may use settings.
_settingsPersister.Load();
SetupHttp();
_repoUpdater.UpdateRepo();
}
private void SetupHttp()
{
FlurlHttp.Configure(settings =>
{
var jsonSettings = new JsonSerializerSettings
{
// This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future
// version, this needs to fail to indicate that a software change is required. Otherwise, we lose
// state between when we request settings, and re-apply them again with a few properties modified.
MissingMemberHandling = MissingMemberHandling.Error,
// This makes sure that null properties, such as maxSize and preferredSize in Radarr
// Quality Definitions, do not get written out to JSON request bodies.
NullValueHandling = NullValueHandling.Ignore
};
settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
FlurlLogging.SetupLogging(settings, _log);
if (!_settingsProvider.Settings.EnableSslCertificateValidation)
{
_log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting `enable_ssl_certificate_validation` is set to `false`");
settings.HttpClientFactory = new UntrustedCertClientFactory();
}
});
}
}

@ -0,0 +1,5 @@
namespace Recyclarr.Command;
public interface IRadarrCommand : IServiceCommand
{
}

@ -4,6 +4,7 @@ public interface IServiceCommand
{
bool Preview { get; }
bool Debug { get; }
ICollection<string>? Config { get; }
ICollection<string> Config { get; }
string CacheStoragePath { get; }
string Name { get; }
}

@ -0,0 +1,7 @@
namespace Recyclarr.Command;
public interface ISonarrCommand : IServiceCommand
{
bool ListReleaseProfiles { get; }
string? ListTerms { get; }
}

@ -1,71 +1,29 @@
using CliFx.Attributes;
using Flurl.Http;
using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using Serilog.Core;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.QualityDefinition;
using TrashLib.Repo;
using Recyclarr.Command.Services;
using Recyclarr.Migration;
namespace Recyclarr.Command;
[Command("radarr", Description = "Perform operations on a Radarr instance")]
[UsedImplicitly]
public class RadarrCommand : ServiceCommand
internal class RadarrCommand : ServiceCommand, IRadarrCommand
{
private readonly IConfigurationLoader<RadarrConfiguration> _configLoader;
private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
private readonly ILogger _log;
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
public RadarrCommand(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
ISettingsPersister settingsPersister,
ISettingsProvider settingsProvider,
IRepoUpdater repoUpdater,
IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(log, loggingLevelSwitch, logJanitor, settingsPersister, settingsProvider, repoUpdater)
{
_log = log;
_configLoader = configLoader;
_qualityUpdaterFactory = qualityUpdaterFactory;
_customFormatUpdaterFactory = customFormatUpdaterFactory;
}
private readonly Lazy<RadarrService> _service;
public override string CacheStoragePath { get; } =
Path.Combine(AppPaths.AppDataPath, "cache", "radarr");
protected override async Task Process()
{
try
{
foreach (var config in _configLoader.LoadMany(Config, "radarr"))
{
_log.Information("Processing server {Url}", config.BaseUrl);
public override string Name => "Radarr";
if (config.QualityDefinition != null)
{
await _qualityUpdaterFactory().Process(Preview, config);
}
public RadarrCommand(IMigrationExecutor migration, Lazy<RadarrService> service)
: base(migration)
{
_service = service;
}
if (config.CustomFormats.Count > 0)
{
await _customFormatUpdaterFactory().Process(Preview, config);
}
}
}
catch (FlurlHttpException e)
{
_log.Error("HTTP error while communicating with Radarr: {Msg}", e.SanitizedExceptionMessage());
ExitDueToFailure();
}
protected override async Task Process()
{
await _service.Value.Execute(this);
}
}

@ -1,30 +1,16 @@
using System.Text;
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using Common.Networking;
using Flurl.Http;
using Flurl.Http.Configuration;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Repo;
using YamlDotNet.Core;
using Recyclarr.Migration;
namespace Recyclarr.Command;
public abstract class ServiceCommand : ICommand, IServiceCommand
{
private readonly ILogger _log;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
private readonly ILogJanitor _logJanitor;
private readonly ISettingsPersister _settingsPersister;
private readonly ISettingsProvider _settingsProvider;
private readonly IRepoUpdater _repoUpdater;
private readonly IMigrationExecutor _migration;
[CommandOption("preview", 'p', Description =
"Only display the processed markdown results without making any API calls.")]
@ -41,93 +27,52 @@ public abstract class ServiceCommand : ICommand, IServiceCommand
new List<string> {AppPaths.DefaultConfigPath};
public abstract string CacheStoragePath { get; }
public abstract string Name { get; }
protected ServiceCommand(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
ISettingsPersister settingsPersister,
ISettingsProvider settingsProvider,
IRepoUpdater repoUpdater)
protected ServiceCommand(IMigrationExecutor migration)
{
_loggingLevelSwitch = loggingLevelSwitch;
_logJanitor = logJanitor;
_settingsPersister = settingsPersister;
_settingsProvider = settingsProvider;
_repoUpdater = repoUpdater;
_log = log;
_migration = migration;
}
public async ValueTask ExecuteAsync(IConsole console)
{
// Must happen first because everything can use the logger.
_loggingLevelSwitch.MinimumLevel = Debug ? LogEventLevel.Debug : LogEventLevel.Information;
// Stuff that needs to happen pre-service-initialization goes here
// Has to happen right after logging because stuff below may use settings.
_settingsPersister.Load();
// Migrations are performed before we process command line arguments because we cannot instantiate any service
// objects via the DI container before migration logic is performed. This is due to the fact that migration
// steps may alter important files and directories which those services may depend on.
PerformMigrations();
SetupHttp();
_repoUpdater.UpdateRepo();
// Initialize command services and execute business logic (system environment changes should be done by this
// point)
await Process();
}
private void PerformMigrations()
{
try
{
await Process();
_migration.PerformAllMigrationSteps();
}
catch (YamlException e)
catch (MigrationException e)
{
var inner = e.InnerException;
if (inner == null)
var msg = new StringBuilder();
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
if (e.Remediation.Any())
{
throw;
msg.AppendLine("\nPossible remediation steps:");
foreach (var remedy in e.Remediation)
{
msg.AppendLine($" - {remedy}");
}
}
_log.Error("Found Unrecognized YAML Property: {ErrorMsg}", inner.Message);
_log.Error("Please remove the property quoted in the above message from your YAML file");
throw new CommandException("Exiting due to invalid configuration");
}
catch (Exception e) when (e is not CommandException)
{
_log.Error(e, "Unrecoverable Exception");
ExitDueToFailure();
throw new CommandException(msg.ToString());
}
finally
{
_logJanitor.DeleteOldestLogFiles(20);
}
}
private void SetupHttp()
{
FlurlHttp.Configure(settings =>
{
var jsonSettings = new JsonSerializerSettings
{
// This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future
// version, this needs to fail to indicate that a software change is required. Otherwise, we lose
// state between when we request settings, and re-apply them again with a few properties modified.
MissingMemberHandling = MissingMemberHandling.Error,
// This makes sure that null properties, such as maxSize and preferredSize in Radarr
// Quality Definitions, do not get written out to JSON request bodies.
NullValueHandling = NullValueHandling.Ignore
};
settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
FlurlLogging.SetupLogging(settings, _log);
if (!_settingsProvider.Settings.EnableSslCertificateValidation)
{
_log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting `enable_ssl_certificate_validation` is set to `false`");
settings.HttpClientFactory = new UntrustedCertClientFactory();
}
});
}
protected abstract Task Process();
protected static void ExitDueToFailure()
{
throw new CommandException("Exiting due to previous exception");
}
}

@ -0,0 +1,48 @@
using Recyclarr.Command.Helpers;
using Recyclarr.Config;
using Serilog;
using TrashLib.Radarr.Config;
using TrashLib.Radarr.CustomFormat;
using TrashLib.Radarr.QualityDefinition;
namespace Recyclarr.Command.Services;
public class RadarrService : ServiceBase<IRadarrCommand>
{
private readonly IConfigurationLoader<RadarrConfiguration> _configLoader;
private readonly Func<ICustomFormatUpdater> _customFormatUpdaterFactory;
private readonly ILogger _log;
private readonly Func<IRadarrQualityDefinitionUpdater> _qualityUpdaterFactory;
public RadarrService(
ILogger log,
IServiceInitialization serviceInitialization,
IConfigurationLoader<RadarrConfiguration> configLoader,
Func<IRadarrQualityDefinitionUpdater> qualityUpdaterFactory,
Func<ICustomFormatUpdater> customFormatUpdaterFactory)
: base(log, serviceInitialization)
{
_log = log;
_configLoader = configLoader;
_qualityUpdaterFactory = qualityUpdaterFactory;
_customFormatUpdaterFactory = customFormatUpdaterFactory;
}
protected override async Task Process(IRadarrCommand cmd)
{
foreach (var config in _configLoader.LoadMany(cmd.Config, "radarr"))
{
_log.Information("Processing server {Url}", config.BaseUrl);
if (config.QualityDefinition != null)
{
await _qualityUpdaterFactory().Process(cmd.Preview, config);
}
if (config.CustomFormats.Count > 0)
{
await _customFormatUpdaterFactory().Process(cmd.Preview, config);
}
}
}
}

@ -0,0 +1,55 @@
using CliFx.Exceptions;
using Flurl.Http;
using Recyclarr.Command.Helpers;
using Serilog;
using TrashLib.Extensions;
using YamlDotNet.Core;
namespace Recyclarr.Command.Services;
/// <summary>
/// Mainly intended to handle common exception recovery logic between service command classes.
/// </summary>
public abstract class ServiceBase<T> where T : IServiceCommand
{
private readonly ILogger _log;
private readonly IServiceInitialization _serviceInitialization;
protected ServiceBase(ILogger log, IServiceInitialization serviceInitialization)
{
_log = log;
_serviceInitialization = serviceInitialization;
}
public async Task Execute(T cmd)
{
try
{
_serviceInitialization.Initialize(cmd);
await Process(cmd);
}
catch (YamlException e)
{
var message = e.InnerException is not null ? e.InnerException.Message : e.Message;
_log.Error("Found Unrecognized YAML Property: {ErrorMsg}", message);
_log.Error("Please remove the property quoted in the above message from your YAML file");
throw new CommandException("Exiting due to invalid configuration");
}
catch (FlurlHttpException e)
{
_log.Error("HTTP error while communicating with {ServiceName}: {Msg}", cmd.Name,
e.SanitizedExceptionMessage());
ExitDueToFailure();
}
catch (Exception e) when (e is not CommandException)
{
_log.Error(e, "Unrecoverable Exception");
ExitDueToFailure();
}
}
protected abstract Task Process(T cmd);
private static void ExitDueToFailure()
=> throw new CommandException("Exiting due to previous exception");
}

@ -0,0 +1,74 @@
using CliFx.Exceptions;
using Recyclarr.Command.Helpers;
using Recyclarr.Config;
using Serilog;
using TrashLib.Sonarr;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile;
namespace Recyclarr.Command.Services;
public class SonarrService : ServiceBase<ISonarrCommand>
{
private readonly ILogger _log;
private readonly IConfigurationLoader<SonarrConfiguration> _configLoader;
private readonly Func<IReleaseProfileUpdater> _profileUpdaterFactory;
private readonly Func<ISonarrQualityDefinitionUpdater> _qualityUpdaterFactory;
private readonly IReleaseProfileLister _lister;
public SonarrService(
ILogger log,
IServiceInitialization serviceInitialization,
IConfigurationLoader<SonarrConfiguration> configLoader,
Func<IReleaseProfileUpdater> profileUpdaterFactory,
Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory,
IReleaseProfileLister lister)
: base(log, serviceInitialization)
{
_log = log;
_configLoader = configLoader;
_profileUpdaterFactory = profileUpdaterFactory;
_qualityUpdaterFactory = qualityUpdaterFactory;
_lister = lister;
}
protected override async Task Process(ISonarrCommand cmd)
{
if (cmd.ListReleaseProfiles)
{
_lister.ListReleaseProfiles();
return;
}
if (cmd.ListTerms != "empty")
{
if (!string.IsNullOrEmpty(cmd.ListTerms))
{
_lister.ListTerms(cmd.ListTerms);
}
else
{
throw new CommandException(
"The --list-terms option was specified without a Release Profile Trash ID specified");
}
return;
}
foreach (var config in _configLoader.LoadMany(cmd.Config, "sonarr"))
{
_log.Information("Processing server {Url}", config.BaseUrl);
if (config.ReleaseProfiles.Count > 0)
{
await _profileUpdaterFactory().Process(cmd.Preview, config);
}
if (config.QualityDefinition.HasValue)
{
await _qualityUpdaterFactory().Process(cmd.Preview, config);
}
}
}
}

@ -1,32 +1,19 @@
using CliFx.Attributes;
using CliFx.Exceptions;
using Flurl.Http;
using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using Serilog.Core;
using TrashLib.Config.Settings;
using TrashLib.Repo;
using TrashLib.Sonarr;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.QualityDefinition;
using TrashLib.Sonarr.ReleaseProfile;
using Recyclarr.Command.Services;
using Recyclarr.Migration;
namespace Recyclarr.Command;
[Command("sonarr", Description = "Perform operations on a Sonarr instance")]
[UsedImplicitly]
public class SonarrCommand : ServiceCommand
internal class SonarrCommand : ServiceCommand, ISonarrCommand
{
private readonly IConfigurationLoader<SonarrConfiguration> _configLoader;
private readonly ILogger _log;
private readonly Func<IReleaseProfileUpdater> _profileUpdaterFactory;
private readonly Func<ISonarrQualityDefinitionUpdater> _qualityUpdaterFactory;
private readonly IReleaseProfileLister _lister;
private readonly Lazy<SonarrService> _service;
[CommandOption("list-release-profiles", Description =
"List available release profiles from the guide in YAML format.")]
public bool ListReleaseProfiles { get; [UsedImplicitly] set; } = false;
public bool ListReleaseProfiles { get; [UsedImplicitly] set; }
// The default value is "empty" because I need to know when the user specifies the option but no value with it.
// Discussed here: https://github.com/Tyrrrz/CliFx/discussions/128#discussioncomment-2647015
@ -35,73 +22,19 @@ public class SonarrCommand : ServiceCommand
"Note that not every release profile has terms that may be filtered.")]
public string? ListTerms { get; [UsedImplicitly] set; } = "empty";
public SonarrCommand(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
ILogJanitor logJanitor,
ISettingsPersister settingsPersister,
ISettingsProvider settingsProvider,
IRepoUpdater repoUpdater,
IConfigurationLoader<SonarrConfiguration> configLoader,
Func<IReleaseProfileUpdater> profileUpdaterFactory,
Func<ISonarrQualityDefinitionUpdater> qualityUpdaterFactory,
IReleaseProfileLister lister)
: base(log, loggingLevelSwitch, logJanitor, settingsPersister, settingsProvider, repoUpdater)
{
_log = log;
_configLoader = configLoader;
_profileUpdaterFactory = profileUpdaterFactory;
_qualityUpdaterFactory = qualityUpdaterFactory;
_lister = lister;
}
public override string CacheStoragePath { get; } =
Path.Combine(AppPaths.AppDataPath, "cache", "sonarr");
protected override async Task Process()
{
try
{
if (ListReleaseProfiles)
{
_lister.ListReleaseProfiles();
return;
}
if (ListTerms != "empty")
{
if (!string.IsNullOrEmpty(ListTerms))
{
_lister.ListTerms(ListTerms);
}
else
{
throw new CommandException(
"The --list-terms option was specified without a Release Profile Trash ID specified");
}
public override string Name => "Sonarr";
return;
}
foreach (var config in _configLoader.LoadMany(Config, "sonarr"))
{
_log.Information("Processing server {Url}", config.BaseUrl);
if (config.ReleaseProfiles.Count > 0)
{
await _profileUpdaterFactory().Process(Preview, config);
}
public SonarrCommand(IMigrationExecutor migration, Lazy<SonarrService> service)
: base(migration)
{
_service = service;
}
if (config.QualityDefinition.HasValue)
{
await _qualityUpdaterFactory().Process(Preview, config);
}
}
}
catch (FlurlHttpException e)
{
_log.Error(e, "HTTP error while communicating with Sonarr");
ExitDueToFailure();
}
protected override async Task Process()
{
await _service.Value.Execute(this);
}
}

@ -7,6 +7,7 @@ using CliFx;
using CliFx.Infrastructure;
using Common;
using Recyclarr.Command.Helpers;
using Recyclarr.Command.Services;
using Recyclarr.Config;
using Recyclarr.Migration;
using Serilog;
@ -60,9 +61,13 @@ public static class CompositionRoot
private static void CommandRegistrations(ContainerBuilder builder)
{
builder.RegisterType<SonarrService>();
builder.RegisterType<RadarrService>();
builder.RegisterType<ServiceInitialization>().As<IServiceInitialization>();
// Register all types deriving from CliFx's ICommand. These are all of our supported subcommands.
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t => t.IsAssignableTo(typeof(ICommand)));
.AssignableTo<ICommand>();
// Used to access the chosen command class. This is assigned from CliTypeActivator
//

@ -1,10 +1,11 @@
using System.Diagnostics;
using System.Text;
using Autofac;
using Autofac.Core;
using Autofac.Core.Activators.Reflection;
using CliFx;
using CliFx.Infrastructure;
using Recyclarr.Command.Helpers;
using Recyclarr.Migration;
namespace Recyclarr;
@ -20,39 +21,35 @@ internal static class Program
var console = _container.Resolve<IConsole>();
try
{
var migration = _container.Resolve<IMigrationExecutor>();
migration.PerformAllMigrationSteps();
}
catch (MigrationException e)
{
var msg = new StringBuilder();
msg.AppendLine("Fatal exception during migration step. Details are below.\n");
msg.AppendLine($"Step That Failed: {e.OperationDescription}");
msg.AppendLine($"Failure Reason: {e.OriginalException.Message}");
if (e.Remediation.Any())
{
msg.AppendLine("\nPossible remediation steps:");
foreach (var remedy in e.Remediation)
{
msg.AppendLine($" - {remedy}");
}
}
await console.Error.WriteAsync(msg);
return 1;
}
return await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
var status = await new CliApplicationBuilder()
.AddCommands(GetRegisteredCommandTypes())
.SetExecutableName(ExecutableName)
.SetVersion(BuildVersion())
.UseTypeActivator(type => CliTypeActivator.ResolveType(_container, type))
.UseConsole(console)
.Build()
.RunAsync();
// Log cleanup happens here instead of ServiceBase or other objects because we want it to run only once before
// application exit, not per-service.
var logJanitor = _container.Resolve<ILogJanitor>();
logJanitor.DeleteOldestLogFiles(20);
return status;
}
private static IEnumerable<Type> GetRegisteredCommandTypes()
{
if (_container is null)
{
throw new NullReferenceException("DI Container was null during migration process");
}
return _container.ComponentRegistry.Registrations
.SelectMany(x => x.Services)
.OfType<TypedService>()
.Select(x => x.ServiceType)
.Where(x => x.IsAssignableTo<ICommand>());
}
private static string BuildVersion()

@ -82,7 +82,6 @@ public class ReleaseProfileListerTest
console.ReadOutputString().Should().ContainAll(expectedOutput.SelectMany(x => x));
}
[Test, AutoMockData]
public void Release_profile_trash_id_is_used_to_look_up_data(
[Frozen] ISonarrGuideService guide,

Loading…
Cancel
Save