fix: --app-data option works again

Fixes #284
pull/286/head
Robert Dailey 4 months ago
parent 51553b2eaf
commit c9c7c05261

@ -10,15 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- In rare circumstances outside of Recyclarr, quality profiles become invalid due to missing
- Sync: In rare circumstances outside of Recyclarr, quality profiles become invalid due to missing
required qualities. When this happens, users are not even able to save the profile using the
Sonarr or Radarr UI. Recyclarr now detects this situation and automatically repairs the quality
profile by re-adding these missing qualities for users. See [this issue][9738].
### Fixed
- Signal interrupt support for all API calls. Now when you press CTRL+C to gracefully exit/cancel
Recyclarr, it will bail out of any ongoing API calls.
- CLI: Signal interrupt support for all API calls. Now when you press CTRL+C to gracefully
exit/cancel Recyclarr, it will bail out of any ongoing API calls.
- CLI: The `--app-data` option works again (#284).
[9738]: https://github.com/Radarr/Radarr/issues/9738

@ -4,7 +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.Helpers;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration;
@ -97,11 +97,11 @@ public static class CompositionRoot
private static void CliRegistrations(ContainerBuilder builder)
{
builder.RegisterType<BaseCommandSetupInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<ProgramInformationLogInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<GlobalTaskInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<CommandSetupInterceptor>().As<ICommandInterceptor>();
builder.RegisterType<GlobalSetupTaskExecutor>();
builder.RegisterTypes(
typeof(ProgramInformationDisplayTask),
typeof(JanitorCleanupTask))
.As<IGlobalSetupTask>()
.OrderByRegistration();

@ -0,0 +1,71 @@
using Recyclarr.Cli.Console.Commands;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Platform;
using Serilog.Core;
using Serilog.Events;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Helpers;
internal sealed class CommandSetupInterceptor : ICommandInterceptor, IDisposable
{
private readonly ConsoleAppCancellationTokenSource _ct = new();
private readonly LoggingLevelSwitch _loggingLevelSwitch;
private readonly IAppDataSetup _appDataSetup;
private readonly Lazy<GlobalSetupTaskExecutor> _taskExecutor;
public CommandSetupInterceptor(
Lazy<ILogger> log,
LoggingLevelSwitch loggingLevelSwitch,
IAppDataSetup appDataSetup,
Lazy<GlobalSetupTaskExecutor> taskExecutor)
{
_loggingLevelSwitch = loggingLevelSwitch;
_appDataSetup = appDataSetup;
_taskExecutor = taskExecutor;
_ct.CancelPressed.Subscribe(_ => log.Value.Information("Exiting due to signal interrupt"));
}
public void Intercept(CommandContext context, CommandSettings settings)
{
switch (settings)
{
case ServiceCommandSettings cmd:
HandleServiceCommand(cmd);
break;
case BaseCommandSettings cmd:
HandleBaseCommand(cmd);
break;
}
_taskExecutor.Value.OnStart();
}
public void InterceptResult(CommandContext context, CommandSettings settings, ref int result)
{
_taskExecutor.Value.OnFinish();
}
private void HandleServiceCommand(ServiceCommandSettings cmd)
{
HandleBaseCommand(cmd);
_appDataSetup.SetAppDataDirectoryOverride(cmd.AppData ?? "");
}
private void HandleBaseCommand(BaseCommandSettings cmd)
{
cmd.CancellationToken = _ct.Token;
_loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{
true => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
}
public void Dispose()
{
_ct.Dispose();
}
}

@ -1,17 +1,20 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Recyclarr.Cli.Console.Helpers;
// Taken from: https://github.com/spectreconsole/spectre.console/issues/701#issuecomment-1081834778
internal sealed class ConsoleAppCancellationTokenSource : IDisposable
{
private readonly ILogger _log;
private readonly CancellationTokenSource _cts = new();
private readonly Subject<Unit> _cancellationSubject = new();
public CancellationToken Token => _cts.Token;
public IObservable<Unit> CancelPressed => _cancellationSubject.AsObservable();
public ConsoleAppCancellationTokenSource(ILogger log)
public ConsoleAppCancellationTokenSource()
{
_log = log;
System.Console.CancelKeyPress += OnCancelKeyPress;
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
@ -24,7 +27,7 @@ internal sealed class ConsoleAppCancellationTokenSource : IDisposable
private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
_log.Information("Exiting due to signal interrupt");
_cancellationSubject.OnNext(Unit.Default);
// NOTE: cancel event, don't terminate the process
e.Cancel = true;
@ -46,6 +49,7 @@ internal sealed class ConsoleAppCancellationTokenSource : IDisposable
public void Dispose()
{
_cancellationSubject.Dispose();
_cts.Dispose();
}
}

@ -1,52 +0,0 @@
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.Interceptors;
internal sealed class BaseCommandSetupInterceptor(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
IAppDataSetup appDataSetup)
: ICommandInterceptor, IDisposable
{
private readonly ConsoleAppCancellationTokenSource _ct = new(log);
public void Intercept(CommandContext context, CommandSettings settings)
{
switch (settings)
{
case ServiceCommandSettings cmd:
HandleServiceCommand(cmd);
break;
case BaseCommandSettings cmd:
HandleBaseCommand(cmd);
break;
}
}
private void HandleServiceCommand(ServiceCommandSettings cmd)
{
HandleBaseCommand(cmd);
appDataSetup.AppDataDirectoryOverride = cmd.AppData;
}
private void HandleBaseCommand(BaseCommandSettings cmd)
{
cmd.CancellationToken = _ct.Token;
loggingLevelSwitch.MinimumLevel = cmd.Debug switch
{
true => LogEventLevel.Debug,
_ => LogEventLevel.Information
};
}
public void Dispose()
{
_ct.Dispose();
}
}

@ -1,17 +0,0 @@
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());
}
}

@ -1,13 +0,0 @@
using Recyclarr.Platform;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Interceptors;
public class ProgramInformationLogInterceptor(ILogger log, IAppPaths paths) : ICommandInterceptor
{
public void Intercept(CommandContext context, CommandSettings settings)
{
log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
log.Debug("App Data Dir: {AppData}", paths.AppDataDirectory);
}
}

@ -0,0 +1,14 @@
namespace Recyclarr.Cli.Console.Setup;
public class GlobalSetupTaskExecutor(IOrderedEnumerable<IGlobalSetupTask> tasks)
{
public void OnStart()
{
tasks.ForEach(x => x.OnStart());
}
public void OnFinish()
{
tasks.Reverse().ForEach(x => x.OnFinish());
}
}

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

@ -6,11 +6,6 @@ namespace Recyclarr.Cli.Console.Setup;
public class JanitorCleanupTask(LogJanitor janitor, ILogger log, ISettingsProvider settingsProvider)
: IGlobalSetupTask
{
public void OnStart()
{
// No work to do for this event
}
public void OnFinish()
{
var maxFiles = settingsProvider.Settings.LogJanitor.MaxFiles;

@ -0,0 +1,12 @@
using Recyclarr.Platform;
namespace Recyclarr.Cli.Console.Setup;
public class ProgramInformationDisplayTask(ILogger log, IAppPaths paths) : IGlobalSetupTask
{
public void OnStart()
{
log.Debug("Recyclarr Version: {Version}", GitVersionInformation.InformationalVersion);
log.Debug("App Data Dir: {AppData}", paths.AppDataDirectory);
}
}

@ -4,11 +4,22 @@ namespace Recyclarr.Platform;
public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs) : IAppDataSetup
{
public string? AppDataDirectoryOverride { get; set; }
private string? _appDataDirectoryOverride;
public void SetAppDataDirectoryOverride(string path)
{
_appDataDirectoryOverride = path;
}
public IAppPaths CreateAppPaths()
{
var appDir = GetAppDataDirectory(AppDataDirectoryOverride);
// If anything (like the Spectre.Console interceptors) tries to grab IAppPaths directly or indirectly (in the
// case of ILogger, which uses LoggerFactory, which uses IAppPaths to get the path where log files should be
// saved), the app data dir override won't be initialized yet, so we error out. This will assist in finding
// logic/programming errors.
ArgumentNullException.ThrowIfNull(_appDataDirectoryOverride);
var appDir = GetAppDataDirectory(_appDataDirectoryOverride);
var paths = new AppPaths(fs.DirectoryInfo.New(appDir));
// Initialize other directories used throughout the application
@ -21,10 +32,13 @@ public class DefaultAppDataSetup(IEnvironment env, IFileSystem fs) : IAppDataSet
return paths;
}
private string GetAppDataDirectory(string? appDataDirectoryOverride)
private string GetAppDataDirectory(string appDataDirectoryOverride)
{
// If a specific app data directory is not provided, use the following environment variable to find the path.
appDataDirectoryOverride ??= env.GetEnvironmentVariable("RECYCLARR_APP_DATA");
if (string.IsNullOrEmpty(appDataDirectoryOverride))
{
// If a specific app data directory is not provided, use the following environment variable to find the path.
appDataDirectoryOverride = env.GetEnvironmentVariable("RECYCLARR_APP_DATA") ?? "";
}
// Ensure user-specified app data directory is created and use it.
if (!string.IsNullOrEmpty(appDataDirectoryOverride))

@ -2,5 +2,5 @@ namespace Recyclarr.Platform;
public interface IAppDataSetup
{
public string? AppDataDirectoryOverride { get; set; }
public void SetAppDataDirectoryOverride(string path);
}

@ -13,7 +13,8 @@ internal class BaseCommandSetupIntegrationTest : CliIntegrationFixture
var registrations = Resolve<IEnumerable<IGlobalSetupTask>>();
registrations.Select(x => x.GetType()).Should().BeEquivalentTo(new[]
{
typeof(JanitorCleanupTask)
typeof(JanitorCleanupTask),
typeof(ProgramInformationDisplayTask)
});
}

@ -3,6 +3,7 @@ using Autofac;
using Autofac.Core;
using NUnit.Framework.Internal;
using Recyclarr.Config.Models;
using Recyclarr.Platform;
using Recyclarr.TestLibrary.Autofac;
using Spectre.Console;
@ -42,6 +43,9 @@ public class CompositionRootTest
[TestCaseSource(typeof(ConcreteTypeEnumerator))]
public void Service_should_be_instantiable(ILifetimeScope scope, Type service)
{
// Required to bypass exception due to the directory override being null
scope.Resolve<IAppDataSetup>().SetAppDataDirectoryOverride("");
scope.Resolve(service).Should().NotBeNull();
}
}

@ -19,6 +19,7 @@ public class DefaultAppDataSetupTest
.SubDirectory("path");
env.GetFolderPath(default, default).ReturnsForAnyArgs(basePath.FullName);
sut.SetAppDataDirectoryOverride("");
var paths = sut.CreateAppPaths();
@ -34,7 +35,7 @@ public class DefaultAppDataSetupTest
.SubDirectory("override")
.SubDirectory("path");
sut.AppDataDirectoryOverride = overridePath.FullName;
sut.SetAppDataDirectoryOverride(overridePath.FullName);
var paths = sut.CreateAppPaths();
paths.AppDataDirectory.FullName.Should().Be(overridePath.FullName);
@ -46,17 +47,18 @@ public class DefaultAppDataSetupTest
[Frozen] IEnvironment env,
DefaultAppDataSetup sut)
{
var overridePath = fs.CurrentDirectory()
var appDataPath = fs.CurrentDirectory()
.SubDirectory("override")
.SubDirectory("path");
env.GetEnvironmentVariable(default!).ReturnsForAnyArgs((string?) null);
env.GetFolderPath(default).ReturnsForAnyArgs(overridePath.FullName);
env.GetFolderPath(default).ReturnsForAnyArgs(appDataPath.FullName);
sut.SetAppDataDirectoryOverride("");
sut.CreateAppPaths();
env.Received().GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create);
fs.AllDirectories.Should().NotContain(overridePath.FullName);
fs.AllDirectories.Should().NotContain(appDataPath.FullName);
}
[Test, AutoMockData]
@ -71,6 +73,7 @@ public class DefaultAppDataSetupTest
.SubDirectory("path").FullName;
env.GetEnvironmentVariable(default!).ReturnsForAnyArgs(expectedPath);
sut.SetAppDataDirectoryOverride("");
sut.CreateAppPaths();
@ -89,7 +92,8 @@ public class DefaultAppDataSetupTest
.SubDirectory("var")
.SubDirectory("path").FullName;
sut.AppDataDirectoryOverride = expectedPath;
sut.SetAppDataDirectoryOverride(expectedPath);
sut.CreateAppPaths();
env.DidNotReceiveWithAnyArgs().GetEnvironmentVariable(default!);

Loading…
Cancel
Save