mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-17 15:02:34 +02:00
Command queue
New: Adding multiple series will queue them instead of running all at once New: Slower scheduled tasks won't be block others from running
This commit is contained in:
parent
446d470f53
commit
638e3ca898
@ -116,14 +116,12 @@ public void should_map_profile()
|
||||
profileResource.InjectTo<Profile>();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Test]
|
||||
public void should_map_tracked_command()
|
||||
{
|
||||
var profileResource = new ApplicationUpdateCommand();
|
||||
profileResource.InjectTo<CommandResource>();
|
||||
var commandResource = new CommandModel { Body = new ApplicationUpdateCommand() };
|
||||
commandResource.InjectTo<CommandResource>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,9 @@
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Api.Mapping;
|
||||
using NzbDrone.Api.Validation;
|
||||
using NzbDrone.Common.Composition;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ProgressMessaging;
|
||||
using NzbDrone.SignalR;
|
||||
@ -15,56 +14,53 @@
|
||||
|
||||
namespace NzbDrone.Api.Commands
|
||||
{
|
||||
public class CommandModule : NzbDroneRestModuleWithSignalR<CommandResource, Command>, IHandle<CommandUpdatedEvent>
|
||||
public class CommandModule : NzbDroneRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent>
|
||||
{
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
private readonly IContainer _container;
|
||||
private readonly ITrackCommands _trackCommands;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
|
||||
public CommandModule(ICommandExecutor commandExecutor,
|
||||
public CommandModule(IManageCommandQueue commandQueueManager,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
IContainer container,
|
||||
ITrackCommands trackCommands)
|
||||
IServiceFactory serviceFactory)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_commandExecutor = commandExecutor;
|
||||
_container = container;
|
||||
_trackCommands = trackCommands;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_serviceFactory = serviceFactory;
|
||||
|
||||
GetResourceById = GetCommand;
|
||||
CreateResource = StartCommand;
|
||||
GetResourceAll = GetAllCommands;
|
||||
GetResourceAll = GetStartedCommands;
|
||||
|
||||
PostValidator.RuleFor(c => c.Name).NotBlank();
|
||||
}
|
||||
|
||||
private CommandResource GetCommand(int id)
|
||||
{
|
||||
return _trackCommands.GetById(id).InjectTo<CommandResource>();
|
||||
return _commandQueueManager.Get(id).InjectTo<CommandResource>();
|
||||
}
|
||||
|
||||
private int StartCommand(CommandResource commandResource)
|
||||
{
|
||||
var commandType =
|
||||
_container.GetImplementations(typeof(Command))
|
||||
.Single(c => c.Name.Replace("Command", "")
|
||||
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
_serviceFactory.GetImplementations(typeof (Command))
|
||||
.Single(c => c.Name.Replace("Command", "")
|
||||
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
dynamic command = Request.Body.FromJson(commandType);
|
||||
command.Manual = true;
|
||||
command.Trigger = CommandTrigger.Manual;
|
||||
|
||||
var trackedCommand = (Command)_commandExecutor.PublishCommandAsync(command);
|
||||
var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual);
|
||||
return trackedCommand.Id;
|
||||
}
|
||||
|
||||
private List<CommandResource> GetAllCommands()
|
||||
private List<CommandResource> GetStartedCommands()
|
||||
{
|
||||
return ToListResource(_trackCommands.RunningCommands);
|
||||
return ToListResource(_commandQueueManager.GetStarted());
|
||||
}
|
||||
|
||||
public void Handle(CommandUpdatedEvent message)
|
||||
{
|
||||
if (message.Command.SendUpdatesToClient)
|
||||
if (message.Command.Body.SendUpdatesToClient)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.Command.Id);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Api.REST;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Api.Commands
|
||||
{
|
||||
@ -8,11 +9,75 @@ public class CommandResource : RestResource
|
||||
{
|
||||
public String Name { get; set; }
|
||||
public String Message { get; set; }
|
||||
public DateTime StartedOn { get; set; }
|
||||
public DateTime StateChangeTime { get; set; }
|
||||
public Boolean SendUpdatesToClient { get; set; }
|
||||
public CommandStatus State { get; set; }
|
||||
public Command Body { get; set; }
|
||||
public CommandPriority Priority { get; set; }
|
||||
public CommandStatus Status { get; set; }
|
||||
public DateTime Queued { get; set; }
|
||||
public DateTime? Started { get; set; }
|
||||
public DateTime? Ended { get; set; }
|
||||
public TimeSpan? Duration { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public CommandTrigger Trigger { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string CompletionMessage { get; set; }
|
||||
|
||||
//Legacy
|
||||
public CommandStatus State
|
||||
{
|
||||
get
|
||||
{
|
||||
return Status;
|
||||
}
|
||||
|
||||
set { }
|
||||
}
|
||||
|
||||
public Boolean Manual
|
||||
{
|
||||
get
|
||||
{
|
||||
return Trigger == CommandTrigger.Manual;
|
||||
}
|
||||
|
||||
set { }
|
||||
}
|
||||
|
||||
public DateTime StartedOn
|
||||
{
|
||||
get
|
||||
{
|
||||
return Queued;
|
||||
}
|
||||
|
||||
set { }
|
||||
}
|
||||
|
||||
public DateTime? StateChangeTime
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
if (Started.HasValue) return Started.Value;
|
||||
|
||||
return Ended;
|
||||
}
|
||||
|
||||
set { }
|
||||
}
|
||||
|
||||
public Boolean SendUpdatesToClient
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Body != null) return Body.SendUpdatesToClient;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
set { }
|
||||
}
|
||||
|
||||
public DateTime? LastExecutionTime { get; set; }
|
||||
public Boolean Manual { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,14 @@ public void Clear()
|
||||
_store.Clear();
|
||||
}
|
||||
|
||||
public void ClearExpired()
|
||||
{
|
||||
foreach (var cached in _store.Where(c => c.Value.IsExpired()))
|
||||
{
|
||||
Remove(cached.Key);
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<T> Values
|
||||
{
|
||||
get
|
||||
|
@ -6,6 +6,7 @@ namespace NzbDrone.Common.Cache
|
||||
public interface ICached
|
||||
{
|
||||
void Clear();
|
||||
void ClearExpired();
|
||||
void Remove(string key);
|
||||
int Count { get; }
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ public void should_not_scan_if_series_root_folder_does_not_exist()
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
Mocker.GetMock<ICommandExecutor>()
|
||||
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never());
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -80,8 +80,8 @@ public void should_not_scan_if_series_root_folder_is_empty()
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
|
||||
Mocker.GetMock<ICommandExecutor>()
|
||||
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never());
|
||||
Mocker.GetMock<IMediaFileTableCleanupService>()
|
||||
.Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -6,7 +6,6 @@
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.Commands;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
@ -16,6 +15,7 @@ public class MediaFileTableCleanupServiceFixture : CoreTest<MediaFileTableCleanu
|
||||
{
|
||||
private const string DELETED_PATH = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!";
|
||||
private List<Episode> _episodes;
|
||||
private Series _series;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
@ -24,9 +24,8 @@ public void SetUp()
|
||||
.Build()
|
||||
.ToList();
|
||||
|
||||
Mocker.GetMock<ISeriesService>()
|
||||
.Setup(s => s.GetSeries(It.IsAny<Int32>()))
|
||||
.Returns(Builder<Series>.CreateNew().Build());
|
||||
_series = Builder<Series>.CreateNew()
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IDiskProvider>()
|
||||
.Setup(e => e.FileExists(It.Is<String>(c => !c.Contains(DELETED_PATH))))
|
||||
@ -61,7 +60,7 @@ public void should_skip_files_that_exist_in_disk()
|
||||
|
||||
GivenEpisodeFiles(episodeFiles);
|
||||
|
||||
Subject.Execute(new CleanMediaFileDb(0));
|
||||
Subject.Clean(_series);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
|
||||
}
|
||||
@ -76,7 +75,7 @@ public void should_delete_non_existent_files()
|
||||
|
||||
GivenEpisodeFiles(episodeFiles);
|
||||
|
||||
Subject.Execute(new CleanMediaFileDb(0));
|
||||
Subject.Clean(_series);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.Is<EpisodeFile>(e => e.RelativePath == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2));
|
||||
}
|
||||
@ -92,7 +91,7 @@ public void should_delete_files_that_dont_belong_to_any_episodes()
|
||||
GivenEpisodeFiles(episodeFiles);
|
||||
GivenFilesAreNotAttachedToEpisode();
|
||||
|
||||
Subject.Execute(new CleanMediaFileDb(0));
|
||||
Subject.Clean(_series);
|
||||
|
||||
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.IsAny<EpisodeFile>(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10));
|
||||
}
|
||||
@ -102,7 +101,7 @@ public void should_unlink_episode_when_episodeFile_does_not_exist()
|
||||
{
|
||||
GivenEpisodeFiles(new List<EpisodeFile>());
|
||||
|
||||
Subject.Execute(new CleanMediaFileDb(0));
|
||||
Subject.Clean(_series);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.Is<Episode>(e => e.EpisodeFileId == 0)), Times.Exactly(10));
|
||||
}
|
||||
@ -117,7 +116,7 @@ public void should_not_update_episode_when_episodeFile_exists()
|
||||
|
||||
GivenEpisodeFiles(episodeFiles);
|
||||
|
||||
Subject.Execute(new CleanMediaFileDb(0));
|
||||
Subject.Clean(_series);
|
||||
|
||||
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
|
||||
}
|
||||
|
@ -1,121 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Messaging.Commands
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandExecutorFixture : TestBase<CommandExecutor>
|
||||
{
|
||||
private Mock<IExecute<CommandA>> _executorA;
|
||||
private Mock<IExecute<CommandB>> _executorB;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_executorA = new Mock<IExecute<CommandA>>();
|
||||
_executorB = new Mock<IExecute<CommandB>>();
|
||||
|
||||
Mocker.GetMock<IServiceFactory>()
|
||||
.Setup(c => c.Build(typeof(IExecute<CommandA>)))
|
||||
.Returns(_executorA.Object);
|
||||
|
||||
Mocker.GetMock<IServiceFactory>()
|
||||
.Setup(c => c.Build(typeof(IExecute<CommandB>)))
|
||||
.Returns(_executorB.Object);
|
||||
|
||||
|
||||
Mocker.GetMock<ITrackCommands>()
|
||||
.Setup(c => c.FindExisting(It.IsAny<Command>()))
|
||||
.Returns<Command>(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_publish_command_to_executor()
|
||||
{
|
||||
var commandA = new CommandA();
|
||||
|
||||
Subject.PublishCommand(commandA);
|
||||
|
||||
_executorA.Verify(c => c.Execute(commandA), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_publish_command_by_with_optional_arg_using_name()
|
||||
{
|
||||
Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command)))
|
||||
.Returns(new List<Type> { typeof(CommandA), typeof(CommandB) });
|
||||
|
||||
Subject.PublishCommand(typeof(CommandA).FullName);
|
||||
_executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once());
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void should_not_publish_to_incompatible_executor()
|
||||
{
|
||||
var commandA = new CommandA();
|
||||
|
||||
Subject.PublishCommand(commandA);
|
||||
|
||||
_executorA.Verify(c => c.Execute(commandA), Times.Once());
|
||||
_executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void broken_executor_should_throw_the_exception()
|
||||
{
|
||||
var commandA = new CommandA();
|
||||
|
||||
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
|
||||
.Throws(new NotImplementedException());
|
||||
|
||||
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA));
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void broken_executor_should_publish_executed_event()
|
||||
{
|
||||
var commandA = new CommandA();
|
||||
|
||||
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
|
||||
.Throws(new NotImplementedException());
|
||||
|
||||
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA));
|
||||
|
||||
VerifyEventPublished<CommandExecutedEvent>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_publish_executed_event_on_success()
|
||||
{
|
||||
var commandA = new CommandA();
|
||||
Subject.PublishCommand(commandA);
|
||||
|
||||
VerifyEventPublished<CommandExecutedEvent>();
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandA : Command
|
||||
{
|
||||
public CommandA(int id = 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class CommandB : Command
|
||||
{
|
||||
|
||||
public CommandB()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using Moq;
|
||||
//using NUnit.Framework;
|
||||
//using NzbDrone.Common;
|
||||
//using NzbDrone.Core.Messaging.Commands;
|
||||
//using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
//using NzbDrone.Core.Messaging.Events;
|
||||
//using NzbDrone.Test.Common;
|
||||
//
|
||||
//namespace NzbDrone.Core.Test.Messaging.Commands
|
||||
//{
|
||||
// [TestFixture]
|
||||
// public class CommandExecutorFixture : TestBase<CommandExecutor>
|
||||
// {
|
||||
// private Mock<IExecute<CommandA>> _executorA;
|
||||
// private Mock<IExecute<CommandB>> _executorB;
|
||||
//
|
||||
// [SetUp]
|
||||
// public void Setup()
|
||||
// {
|
||||
// _executorA = new Mock<IExecute<CommandA>>();
|
||||
// _executorB = new Mock<IExecute<CommandB>>();
|
||||
//
|
||||
// Mocker.GetMock<IServiceFactory>()
|
||||
// .Setup(c => c.Build(typeof(IExecute<CommandA>)))
|
||||
// .Returns(_executorA.Object);
|
||||
//
|
||||
// Mocker.GetMock<IServiceFactory>()
|
||||
// .Setup(c => c.Build(typeof(IExecute<CommandB>)))
|
||||
// .Returns(_executorB.Object);
|
||||
//
|
||||
//
|
||||
// Mocker.GetMock<ITrackCommands>()
|
||||
// .Setup(c => c.FindExisting(It.IsAny<Command>()))
|
||||
// .Returns<Command>(null);
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void should_publish_command_to_executor()
|
||||
// {
|
||||
// var commandA = new CommandA();
|
||||
//
|
||||
// Subject.Push(commandA);
|
||||
//
|
||||
// _executorA.Verify(c => c.Execute(commandA), Times.Once());
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void should_publish_command_by_with_optional_arg_using_name()
|
||||
// {
|
||||
// Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command)))
|
||||
// .Returns(new List<Type> { typeof(CommandA), typeof(CommandB) });
|
||||
//
|
||||
// Subject.Push(typeof(CommandA).FullName);
|
||||
// _executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once());
|
||||
// }
|
||||
//
|
||||
//
|
||||
// [Test]
|
||||
// public void should_not_publish_to_incompatible_executor()
|
||||
// {
|
||||
// var commandA = new CommandA();
|
||||
//
|
||||
// Subject.Push(commandA);
|
||||
//
|
||||
// _executorA.Verify(c => c.Execute(commandA), Times.Once());
|
||||
// _executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void broken_executor_should_throw_the_exception()
|
||||
// {
|
||||
// var commandA = new CommandA();
|
||||
//
|
||||
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
|
||||
// .Throws(new NotImplementedException());
|
||||
//
|
||||
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
|
||||
// }
|
||||
//
|
||||
//
|
||||
// [Test]
|
||||
// public void broken_executor_should_publish_executed_event()
|
||||
// {
|
||||
// var commandA = new CommandA();
|
||||
//
|
||||
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
|
||||
// .Throws(new NotImplementedException());
|
||||
//
|
||||
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
|
||||
//
|
||||
// VerifyEventPublished<CommandExecutedEvent>();
|
||||
// }
|
||||
//
|
||||
// [Test]
|
||||
// public void should_publish_executed_event_on_success()
|
||||
// {
|
||||
// var commandA = new CommandA();
|
||||
// Subject.Push(commandA);
|
||||
//
|
||||
// VerifyEventPublished<CommandExecutedEvent>();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class CommandA : Command
|
||||
// {
|
||||
// public CommandA(int id = 0)
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class CommandB : Command
|
||||
// {
|
||||
//
|
||||
// public CommandB()
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
@ -1,19 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Update.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Test.Messaging.Commands
|
||||
{
|
||||
[TestFixture]
|
||||
public class CommandFixture
|
||||
{
|
||||
[Test]
|
||||
public void default_values()
|
||||
{
|
||||
var command = new ApplicationUpdateCommand();
|
||||
|
||||
command.Id.Should().NotBe(0);
|
||||
command.Name.Should().Be("ApplicationUpdate");
|
||||
}
|
||||
}
|
||||
}
|
@ -257,7 +257,6 @@
|
||||
<Compile Include="MediaFiles\UpgradeMediaFileServiceFixture.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandExecutorFixture.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandFixture.cs" />
|
||||
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
|
||||
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
|
||||
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />
|
||||
|
@ -8,7 +8,6 @@
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests
|
||||
{
|
||||
|
@ -12,7 +12,7 @@
|
||||
using NzbDrone.Common.Model;
|
||||
using NzbDrone.Common.Processes;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Update.Commands;
|
||||
@ -163,7 +163,7 @@ public void should_not_extract_if_verification_fails()
|
||||
{
|
||||
Mocker.GetMock<IVerifyUpdates>().Setup(c => c.Verify(It.IsAny<UpdatePackage>(), It.IsAny<String>())).Returns(false);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
Mocker.GetMock<IArchiveService>().Verify(v => v.Extract(It.IsAny<String>(), It.IsAny<String>()), Times.Never());
|
||||
}
|
||||
@ -189,7 +189,7 @@ public void should_throw_if_script_is_not_set()
|
||||
|
||||
GivenInstallScript("");
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
|
||||
@ -203,7 +203,7 @@ public void should_throw_if_script_is_null()
|
||||
|
||||
GivenInstallScript(null);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
|
||||
@ -221,7 +221,7 @@ public void should_throw_if_script_path_does_not_exist()
|
||||
.Setup(s => s.FileExists(scriptPath, StringComparison.Ordinal))
|
||||
.Returns(false);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
|
||||
@ -255,7 +255,7 @@ public void should_log_error_when_app_data_is_child_of_startup_folder()
|
||||
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
|
||||
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone\AppData".AsOsAgnostic);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ public void should_log_error_when_app_data_is_same_as_startup_folder()
|
||||
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
|
||||
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
|
||||
@ -278,7 +278,7 @@ public void should_log_error_when_startup_folder_is_not_writable()
|
||||
|
||||
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
@ -293,7 +293,7 @@ public void should_log_when_install_cannot_be_started()
|
||||
|
||||
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
|
||||
|
||||
Subject.Execute(new ApplicationUpdateCommand());
|
||||
Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
|
||||
|
||||
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
|
38
src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs
Normal file
38
src/NzbDrone.Core/Datastore/Converters/CommandConverter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Marr.Data.Converters;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Reflection;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class CommandConverter : EmbeddedDocumentConverter
|
||||
{
|
||||
public override object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var stringValue = (string)context.DbValue;
|
||||
|
||||
if (stringValue.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ordinal = context.DataRecord.GetOrdinal("Name");
|
||||
var contract = context.DataRecord.GetString(ordinal);
|
||||
var impType = typeof (Command).Assembly.FindTypeByName(contract + "Command");
|
||||
|
||||
if (impType == null)
|
||||
{
|
||||
throw new CommandNotFoundException(contract);
|
||||
}
|
||||
|
||||
return Json.Deserialize(stringValue, impType);
|
||||
}
|
||||
}
|
||||
}
|
48
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
48
src/NzbDrone.Core/Datastore/Converters/TimeSpanConverter.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Marr.Data.Converters;
|
||||
using Marr.Data.Mapping;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Converters
|
||||
{
|
||||
public class TimeSpanConverter : IConverter
|
||||
{
|
||||
public object FromDB(ConverterContext context)
|
||||
{
|
||||
if (context.DbValue == DBNull.Value)
|
||||
{
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
return TimeSpan.Parse(context.DbValue.ToString());
|
||||
}
|
||||
|
||||
public object FromDB(ColumnMap map, object dbValue)
|
||||
{
|
||||
if (dbValue == DBNull.Value)
|
||||
{
|
||||
return DBNull.Value;
|
||||
}
|
||||
|
||||
if (dbValue is TimeSpan)
|
||||
{
|
||||
return dbValue;
|
||||
}
|
||||
|
||||
return TimeSpan.Parse(dbValue.ToString(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public object ToDB(object clrValue)
|
||||
{
|
||||
if (clrValue.ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ((TimeSpan)clrValue).ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public Type DbType { get; private set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(78)]
|
||||
public class add_commands_table : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("Commands")
|
||||
.WithColumn("Name").AsString().NotNullable()
|
||||
.WithColumn("Body").AsString().NotNullable()
|
||||
.WithColumn("Priority").AsInt32().NotNullable()
|
||||
.WithColumn("Status").AsInt32().NotNullable()
|
||||
.WithColumn("QueuedAt").AsDateTime().NotNullable()
|
||||
.WithColumn("StartedAt").AsDateTime().Nullable()
|
||||
.WithColumn("EndedAt").AsDateTime().Nullable()
|
||||
.WithColumn("Duration").AsString().Nullable()
|
||||
.WithColumn("Exception").AsString().Nullable()
|
||||
.WithColumn("Trigger").AsInt32().NotNullable();
|
||||
}
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
@ -103,6 +104,8 @@ public static void Map()
|
||||
|
||||
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
||||
Mapper.Entity<User>().RegisterModel("Users");
|
||||
Mapper.Entity<CommandModel>().RegisterModel("Commands")
|
||||
.Ignore(c => c.Message);
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
@ -125,6 +128,9 @@ private static void RegisterMappers()
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Command), new CommandConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan), new TimeSpanConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan?), new TimeSpanConverter());
|
||||
}
|
||||
|
||||
private static void RegisterProviderSettingConverter()
|
||||
|
@ -12,14 +12,17 @@ public class RedownloadFailedDownloadService : IHandleAsync<DownloadFailedEvent>
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RedownloadFailedDownloadService(IConfigService configService, IEpisodeService episodeService, ICommandExecutor commandExecutor, Logger logger)
|
||||
public RedownloadFailedDownloadService(IConfigService configService,
|
||||
IEpisodeService episodeService,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_episodeService = episodeService;
|
||||
_commandExecutor = commandExecutor;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -35,7 +38,7 @@ public void HandleAsync(DownloadFailedEvent message)
|
||||
{
|
||||
_logger.Debug("Failed download only contains one episode, searching again");
|
||||
|
||||
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
|
||||
_commandQueueManager.Push(new EpisodeSearchCommand(message.EpisodeIds));
|
||||
|
||||
return;
|
||||
}
|
||||
@ -47,7 +50,7 @@ public void HandleAsync(DownloadFailedEvent message)
|
||||
{
|
||||
_logger.Debug("Failed download was entire season, searching again");
|
||||
|
||||
_commandExecutor.PublishCommandAsync(new SeasonSearchCommand
|
||||
_commandQueueManager.Push(new SeasonSearchCommand
|
||||
{
|
||||
SeriesId = message.SeriesId,
|
||||
SeasonNumber = seasonNumber
|
||||
@ -58,7 +61,7 @@ public void HandleAsync(DownloadFailedEvent message)
|
||||
|
||||
_logger.Debug("Failed download contains multiple episodes, probably a double episode, searching again");
|
||||
|
||||
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds));
|
||||
_commandQueueManager.Push(new EpisodeSearchCommand(message.EpisodeIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public void HandleAsync(ApplicationStartedEvent message)
|
||||
|
||||
public void Execute(CheckHealthCommand message)
|
||||
{
|
||||
PerformHealthCheck(c => message.Manual || c.CheckOnSchedule);
|
||||
PerformHealthCheck(c => message.Trigger == CommandTrigger.Manual || c.CheckOnSchedule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class CleanupCommandQueue : IHousekeepingTask
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public CleanupCommandQueue(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
_commandQueueManager.CleanCommands();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
{
|
||||
public class TrimLogDatabase : IHousekeepingTask
|
||||
{
|
||||
private readonly ILogRepository _logRepo;
|
||||
|
||||
public TrimLogDatabase(ILogRepository logRepo)
|
||||
{
|
||||
_logRepo = logRepo;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
_logRepo.Trim();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,8 @@
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping
|
||||
{
|
||||
public class HousekeepingService : IExecute<HousekeepingCommand>, IHandleAsync<ApplicationStartedEvent>
|
||||
public class HousekeepingService : IExecute<HousekeepingCommand>,
|
||||
IHandleAsync<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IEnumerable<IHousekeepingTask> _housekeepers;
|
||||
private readonly Logger _logger;
|
||||
@ -40,7 +41,7 @@ private void Clean()
|
||||
}
|
||||
}
|
||||
|
||||
// Vacuuming the log db isn't needed since that's done hourly at the TrimLogCommand.
|
||||
// Vacuuming the log db isn't needed since that's done in a separate housekeeping task
|
||||
_logger.Debug("Compressing main database after housekeeping");
|
||||
_mainDb.Vacuum();
|
||||
}
|
||||
|
@ -112,9 +112,9 @@ public void Execute(MissingEpisodeSearchCommand message)
|
||||
{
|
||||
List<Episode> episodes;
|
||||
|
||||
if (message.SeriesId > 0)
|
||||
if (message.SeriesId.HasValue)
|
||||
{
|
||||
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId)
|
||||
episodes = _episodeService.GetEpisodeBySeries(message.SeriesId.Value)
|
||||
.Where(e => e.Monitored &&
|
||||
!e.HasFile &&
|
||||
e.AirDateUtc.HasValue &&
|
||||
|
@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class MissingEpisodeSearchCommand : Command
|
||||
{
|
||||
public int SeriesId { get; private set; }
|
||||
public int? SeriesId { get; private set; }
|
||||
|
||||
public override bool SendUpdatesToClient
|
||||
{
|
||||
|
@ -1,8 +0,0 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Instrumentation.Commands
|
||||
{
|
||||
public class TrimLogCommand : Command
|
||||
{
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ public interface ILogService
|
||||
PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec);
|
||||
}
|
||||
|
||||
public class LogService : ILogService, IExecute<TrimLogCommand>, IExecute<ClearLogCommand>
|
||||
public class LogService : ILogService, IExecute<ClearLogCommand>
|
||||
{
|
||||
private readonly ILogRepository _logRepository;
|
||||
|
||||
@ -23,11 +23,6 @@ public PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec)
|
||||
return _logRepository.GetPaged(pagingSpec);
|
||||
}
|
||||
|
||||
public void Execute(TrimLogCommand message)
|
||||
{
|
||||
_logRepository.Trim();
|
||||
}
|
||||
|
||||
public void Execute(ClearLogCommand message)
|
||||
{
|
||||
_logRepository.Purge(vacuum: true);
|
||||
|
@ -12,7 +12,6 @@ public interface IScheduledTaskRepository : IBasicRepository<ScheduledTask>
|
||||
void SetLastExecutionTime(int id, DateTime executionTime);
|
||||
}
|
||||
|
||||
|
||||
public class ScheduledTaskRepository : BasicRepository<ScheduledTask>, IScheduledTaskRepository
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
@ -15,15 +16,15 @@ public class Scheduler :
|
||||
IHandle<ApplicationShutdownRequested>
|
||||
{
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly Logger _logger;
|
||||
private static readonly Timer Timer = new Timer();
|
||||
private static CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public Scheduler(ITaskManager taskManager, ICommandExecutor commandExecutor, Logger logger)
|
||||
public Scheduler(ITaskManager taskManager, IManageCommandQueue commandQueueManager, Logger logger)
|
||||
{
|
||||
_taskManager = taskManager;
|
||||
_commandExecutor = commandExecutor;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -33,24 +34,16 @@ private void ExecuteCommands()
|
||||
{
|
||||
Timer.Enabled = false;
|
||||
|
||||
var tasks = _taskManager.GetPending();
|
||||
var tasks = _taskManager.GetPending().ToList();
|
||||
|
||||
_logger.Trace("Pending Tasks: {0}", tasks.Count);
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
_cancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
_commandExecutor.PublishCommand(task.TypeName, task.LastExecution);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ErrorException("Error occurred while executing task " + task.TypeName, e);
|
||||
}
|
||||
_commandQueueManager.Push(task.TypeName, task.LastExecution, CommandPriority.Low, CommandTrigger.Scheduled);
|
||||
}
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
if (!_cancellationTokenSource.IsCancellationRequested)
|
||||
|
@ -10,10 +10,9 @@
|
||||
using NzbDrone.Core.HealthCheck;
|
||||
using NzbDrone.Core.Housekeeping;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Instrumentation.Commands;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.MediaFiles.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv.Commands;
|
||||
using NzbDrone.Core.Update.Commands;
|
||||
@ -62,10 +61,9 @@ public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
var defaultTasks = new[]
|
||||
{
|
||||
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
|
||||
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).FullName},
|
||||
new ScheduledTask{ Interval = 5, TypeName = typeof(MessagingCleanupCommand).FullName},
|
||||
new ScheduledTask{ Interval = 6*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
|
||||
new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName},
|
||||
new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},
|
||||
new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName},
|
||||
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
|
||||
@ -127,7 +125,7 @@ private int GetRssSyncInterval()
|
||||
|
||||
public void Handle(CommandExecutedEvent message)
|
||||
{
|
||||
var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.GetType().FullName);
|
||||
var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.Body.GetType().FullName);
|
||||
|
||||
if (scheduledTask != null)
|
||||
{
|
||||
|
@ -1,14 +0,0 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.Commands
|
||||
{
|
||||
public class CleanMediaFileDb : Command
|
||||
{
|
||||
public int SeriesId { get; private set; }
|
||||
|
||||
public CleanMediaFileDb(int seriesId)
|
||||
{
|
||||
SeriesId = seriesId;
|
||||
}
|
||||
}
|
||||
}
|
@ -33,27 +33,27 @@ public class DiskScanService :
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IMakeImportDecision _importDecisionMaker;
|
||||
private readonly IImportApprovedEpisodes _importApprovedEpisodes;
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DiskScanService(IDiskProvider diskProvider,
|
||||
IMakeImportDecision importDecisionMaker,
|
||||
IImportApprovedEpisodes importApprovedEpisodes,
|
||||
ICommandExecutor commandExecutor,
|
||||
IConfigService configService,
|
||||
ISeriesService seriesService,
|
||||
IMediaFileTableCleanupService mediaFileTableCleanupService,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_diskProvider = diskProvider;
|
||||
_importDecisionMaker = importDecisionMaker;
|
||||
_importApprovedEpisodes = importApprovedEpisodes;
|
||||
_commandExecutor = commandExecutor;
|
||||
_configService = configService;
|
||||
_seriesService = seriesService;
|
||||
_mediaFileTableCleanupService = mediaFileTableCleanupService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
@ -79,7 +79,7 @@ public void Scan(Series series)
|
||||
}
|
||||
|
||||
_logger.ProgressInfo("Scanning disk for {0}", series.Title);
|
||||
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id));
|
||||
_mediaFileTableCleanupService.Clean(series);
|
||||
|
||||
if (!_diskProvider.FolderExists(series.Path))
|
||||
{
|
||||
|
@ -3,19 +3,20 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.MediaFiles.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles
|
||||
{
|
||||
public interface IMediaFileTableCleanupService
|
||||
{
|
||||
void Clean(Series series);
|
||||
}
|
||||
|
||||
public class MediaFileTableCleanupService : IExecute<CleanMediaFileDb>
|
||||
public class MediaFileTableCleanupService : IMediaFileTableCleanupService
|
||||
{
|
||||
private readonly IMediaFileService _mediaFileService;
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MediaFileTableCleanupService(IMediaFileService mediaFileService,
|
||||
@ -27,15 +28,13 @@ public MediaFileTableCleanupService(IMediaFileService mediaFileService,
|
||||
_mediaFileService = mediaFileService;
|
||||
_diskProvider = diskProvider;
|
||||
_episodeService = episodeService;
|
||||
_seriesService = seriesService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Execute(CleanMediaFileDb message)
|
||||
public void Clean(Series series)
|
||||
{
|
||||
var seriesFile = _mediaFileService.GetFilesBySeries(message.SeriesId);
|
||||
var series = _seriesService.GetSeries(message.SeriesId);
|
||||
var episodes = _episodeService.GetEpisodeBySeries(message.SeriesId);
|
||||
var seriesFile = _mediaFileService.GetFilesBySeries(series.Id);
|
||||
var episodes = _episodeService.GetEpisodeBySeries(series.Id);
|
||||
|
||||
foreach (var episodeFile in seriesFile)
|
||||
{
|
||||
|
@ -0,0 +1,17 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CleanupCommandMessagingService : IExecute<MessagingCleanupCommand>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public CleanupCommandMessagingService(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Execute(MessagingCleanupCommand message)
|
||||
{
|
||||
_commandQueueManager.CleanCommands();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,9 @@
|
||||
using System;
|
||||
using FluentMigrator.Runner;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public abstract class Command : ModelBase, IMessage
|
||||
public abstract class Command
|
||||
{
|
||||
private static readonly object Mutex = new object();
|
||||
private static int _idCounter;
|
||||
private readonly StopWatch _stopWatch;
|
||||
|
||||
public CommandStatus State { get; private set; }
|
||||
public DateTime StateChangeTime { get; private set; }
|
||||
|
||||
public virtual Boolean SendUpdatesToClient
|
||||
{
|
||||
get
|
||||
@ -23,63 +12,21 @@ public virtual Boolean SendUpdatesToClient
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan Runtime
|
||||
public virtual string CompletionMessage
|
||||
{
|
||||
get
|
||||
{
|
||||
return _stopWatch.ElapsedTime();
|
||||
return "Completed";
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean Manual { get; set; }
|
||||
public Exception Exception { get; private set; }
|
||||
public String Message { get; private set; }
|
||||
|
||||
public String Name { get; private set; }
|
||||
public DateTime? LastExecutionTime { get; set; }
|
||||
public CommandTrigger Trigger { get; set; }
|
||||
|
||||
protected Command()
|
||||
public Command()
|
||||
{
|
||||
Name = GetType().Name.Replace("Command", "");
|
||||
StateChangeTime = DateTime.UtcNow;
|
||||
State = CommandStatus.Pending;
|
||||
_stopWatch = new StopWatch();
|
||||
Manual = false;
|
||||
|
||||
lock (Mutex)
|
||||
{
|
||||
Id = ++_idCounter;
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_stopWatch.Start();
|
||||
StateChangeTime = DateTime.UtcNow;
|
||||
State = CommandStatus.Running;
|
||||
SetMessage("Starting");
|
||||
}
|
||||
|
||||
public void Failed(Exception exception, string message = "Failed")
|
||||
{
|
||||
_stopWatch.Stop();
|
||||
StateChangeTime = DateTime.UtcNow;
|
||||
State = CommandStatus.Failed;
|
||||
Exception = exception;
|
||||
SetMessage(message);
|
||||
}
|
||||
|
||||
public void Completed(string message = "Completed")
|
||||
{
|
||||
_stopWatch.Stop();
|
||||
StateChangeTime = DateTime.UtcNow;
|
||||
State = CommandStatus.Completed;
|
||||
SetMessage(message);
|
||||
}
|
||||
|
||||
public void SetMessage(string message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ public bool Equals(Command x, Command y)
|
||||
|
||||
public int GetHashCode(Command obj)
|
||||
{
|
||||
return obj.Id.GetHashCode();
|
||||
return obj.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,117 +1,59 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.ProgressMessaging;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CommandExecutor : ICommandExecutor
|
||||
public class CommandExecutor : IHandle<ApplicationStartedEvent>,
|
||||
IHandle<ApplicationShutdownRequested>
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
private readonly ITrackCommands _trackCommands;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly TaskFactory _taskFactory;
|
||||
|
||||
public CommandExecutor(Logger logger, IServiceFactory serviceFactory, ITrackCommands trackCommands, IEventAggregator eventAggregator)
|
||||
private static CancellationTokenSource _cancellationTokenSource;
|
||||
private const int THREAD_LIMIT = 3;
|
||||
|
||||
public CommandExecutor(IServiceFactory serviceFactory,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
var scheduler = new LimitedConcurrencyLevelTaskScheduler(3);
|
||||
|
||||
_logger = logger;
|
||||
_serviceFactory = serviceFactory;
|
||||
_trackCommands = trackCommands;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_eventAggregator = eventAggregator;
|
||||
_taskFactory = new TaskFactory(scheduler);
|
||||
}
|
||||
|
||||
public void PublishCommand<TCommand>(TCommand command) where TCommand : Command
|
||||
private void ExecuteCommands()
|
||||
{
|
||||
Ensure.That(command, () => command).IsNotNull();
|
||||
|
||||
_logger.Trace("Publishing {0}", command.GetType().Name);
|
||||
|
||||
if (_trackCommands.FindExisting(command) != null)
|
||||
try
|
||||
{
|
||||
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
|
||||
return;
|
||||
foreach (var command in _commandQueueManager.Queue(_cancellationTokenSource.Token))
|
||||
{
|
||||
try
|
||||
{
|
||||
ExecuteCommand((dynamic)command.Body, command);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error occurred while executing task " + command.Name, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_trackCommands.Store(command);
|
||||
|
||||
ExecuteCommand<TCommand>(command);
|
||||
}
|
||||
|
||||
public void PublishCommand(string commandTypeName)
|
||||
{
|
||||
PublishCommand(commandTypeName, null);
|
||||
}
|
||||
|
||||
public void PublishCommand(string commandTypeName, DateTime? lastExecutionTime)
|
||||
{
|
||||
dynamic command = GetCommand(commandTypeName);
|
||||
command.LastExecutionTime = lastExecutionTime;
|
||||
|
||||
PublishCommand(command);
|
||||
}
|
||||
|
||||
public Command PublishCommandAsync<TCommand>(TCommand command) where TCommand : Command
|
||||
{
|
||||
Ensure.That(command, () => command).IsNotNull();
|
||||
|
||||
_logger.Trace("Publishing {0}", command.GetType().Name);
|
||||
|
||||
var existingCommand = _trackCommands.FindExisting(command);
|
||||
|
||||
if (existingCommand != null)
|
||||
catch (ThreadAbortException ex)
|
||||
{
|
||||
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
|
||||
return existingCommand;
|
||||
_logger.ErrorException(ex.Message, ex);
|
||||
Thread.ResetAbort();
|
||||
}
|
||||
|
||||
_trackCommands.Store(command);
|
||||
|
||||
// TODO: We should use async await (once we get 4.5) or normal Task Continuations on Command processing to prevent blocking the TaskScheduler.
|
||||
// For now we use TaskCreationOptions 0x10, which is actually .net 4.5 HideScheduler.
|
||||
// This will detach the scheduler from the thread, causing new Task creating in the command to be executed on the ThreadPool, avoiding a deadlock.
|
||||
// Please note that the issue only shows itself on mono because since Microsoft .net implementation supports Task inlining on WaitAll.
|
||||
if (Enum.IsDefined(typeof(TaskCreationOptions), (TaskCreationOptions)0x10))
|
||||
{
|
||||
_taskFactory.StartNew(() => ExecuteCommand<TCommand>(command)
|
||||
, TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10)
|
||||
.LogExceptions();
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskFactory.StartNew(() => ExecuteCommand<TCommand>(command)
|
||||
, TaskCreationOptions.PreferFairness)
|
||||
.LogExceptions();
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
public Command PublishCommandAsync(string commandTypeName)
|
||||
{
|
||||
dynamic command = GetCommand(commandTypeName);
|
||||
return PublishCommandAsync(command);
|
||||
}
|
||||
|
||||
private dynamic GetCommand(string commandTypeName)
|
||||
{
|
||||
var commandType = _serviceFactory.GetImplementations(typeof(Command))
|
||||
.Single(c => c.FullName.Equals(commandTypeName, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return Json.Deserialize("{}", commandType);
|
||||
}
|
||||
|
||||
private void ExecuteCommand<TCommand>(Command command) where TCommand : Command
|
||||
private void ExecuteCommand<TCommand>(TCommand command, CommandModel commandModel) where TCommand : Command
|
||||
{
|
||||
var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType());
|
||||
var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract);
|
||||
@ -120,47 +62,67 @@ private void ExecuteCommand<TCommand>(Command command) where TCommand : Command
|
||||
|
||||
try
|
||||
{
|
||||
_trackCommands.Start(command);
|
||||
BroadcastCommandUpdate(command);
|
||||
_commandQueueManager.Start(commandModel);
|
||||
BroadcastCommandUpdate(commandModel);
|
||||
|
||||
if (!MappedDiagnosticsContext.Contains("CommandId") && command.SendUpdatesToClient)
|
||||
if (!MappedDiagnosticsContext.Contains("CommandId"))
|
||||
{
|
||||
MappedDiagnosticsContext.Set("CommandId", command.Id.ToString());
|
||||
MappedDiagnosticsContext.Set("CommandId", commandModel.Id.ToString());
|
||||
}
|
||||
|
||||
handler.Execute((TCommand)command);
|
||||
handler.Execute(command);
|
||||
|
||||
if (command.State == CommandStatus.Running)
|
||||
{
|
||||
_trackCommands.Completed(command);
|
||||
}
|
||||
_commandQueueManager.Complete(commandModel, command.CompletionMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (CommandFailedException ex)
|
||||
{
|
||||
_trackCommands.Failed(command, e);
|
||||
_commandQueueManager.Fail(commandModel, ex.Message, ex);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_commandQueueManager.SetMessage(commandModel, "Failed");
|
||||
_commandQueueManager.Fail(commandModel, "Failed", ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
BroadcastCommandUpdate(command);
|
||||
_eventAggregator.PublishEvent(new CommandExecutedEvent(command));
|
||||
BroadcastCommandUpdate(commandModel);
|
||||
|
||||
if (MappedDiagnosticsContext.Get("CommandId") == command.Id.ToString())
|
||||
_eventAggregator.PublishEvent(new CommandExecutedEvent(commandModel));
|
||||
|
||||
if (MappedDiagnosticsContext.Get("CommandId") == commandModel.Id.ToString())
|
||||
{
|
||||
MappedDiagnosticsContext.Remove("CommandId");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Trace("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, command.Runtime.ToString(""));
|
||||
_logger.Trace("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, commandModel.Duration.ToString());
|
||||
}
|
||||
|
||||
|
||||
private void BroadcastCommandUpdate(Command command)
|
||||
|
||||
private void BroadcastCommandUpdate(CommandModel command)
|
||||
{
|
||||
if (command.SendUpdatesToClient)
|
||||
if (command.Body.SendUpdatesToClient)
|
||||
{
|
||||
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command));
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
for (int i = 0; i < THREAD_LIMIT; i++)
|
||||
{
|
||||
var thread = new Thread(ExecuteCommands);
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ApplicationShutdownRequested message)
|
||||
{
|
||||
_logger.Info("Shutting down task execution");
|
||||
_cancellationTokenSource.Cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CommandFailedException : NzbDroneException
|
||||
{
|
||||
public CommandFailedException(string message, params object[] args) : base(message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandFailedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandFailedException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandFailedException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public CommandFailedException(Exception innerException)
|
||||
: base("Failed", innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
21
src/NzbDrone.Core/Messaging/Commands/CommandModel.cs
Normal file
21
src/NzbDrone.Core/Messaging/Commands/CommandModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CommandModel : ModelBase, IMessage
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Command Body { get; set; }
|
||||
public CommandPriority Priority { get; set; }
|
||||
public CommandStatus Status { get; set; }
|
||||
public DateTime QueuedAt { get; set; }
|
||||
public DateTime? StartedAt { get; set; }
|
||||
public DateTime? EndedAt { get; set; }
|
||||
public TimeSpan? Duration { get; set; }
|
||||
public string Exception { get; set; }
|
||||
public CommandTrigger Trigger { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CommandNotFoundException : NzbDroneException
|
||||
{
|
||||
public CommandNotFoundException(string contract)
|
||||
: base("Couldn't find command " + contract)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
14
src/NzbDrone.Core/Messaging/Commands/CommandPriority.cs
Normal file
14
src/NzbDrone.Core/Messaging/Commands/CommandPriority.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public enum CommandPriority
|
||||
{
|
||||
Low = -1,
|
||||
Normal = 0,
|
||||
High = 1
|
||||
}
|
||||
}
|
128
src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
Normal file
128
src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class CommandQueue : IProducerConsumerCollection<CommandModel>
|
||||
{
|
||||
private object Mutex = new object();
|
||||
|
||||
private List<CommandModel> _items;
|
||||
|
||||
public CommandQueue()
|
||||
{
|
||||
_items = new List<CommandModel>();
|
||||
}
|
||||
|
||||
public IEnumerator<CommandModel> GetEnumerator()
|
||||
{
|
||||
List<CommandModel> copy = null;
|
||||
|
||||
lock (Mutex)
|
||||
{
|
||||
copy = new List<CommandModel>(_items);
|
||||
}
|
||||
|
||||
return copy.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
lock (Mutex)
|
||||
{
|
||||
((ICollection)_items).CopyTo(array, index);
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
return _items.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public object SyncRoot
|
||||
{
|
||||
get
|
||||
{
|
||||
return Mutex;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSynchronized
|
||||
{
|
||||
get
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(CommandModel[] array, int index)
|
||||
{
|
||||
lock (Mutex)
|
||||
{
|
||||
_items.CopyTo(array, index);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryAdd(CommandModel item)
|
||||
{
|
||||
Add(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTake(out CommandModel item)
|
||||
{
|
||||
bool rval = true;
|
||||
lock (Mutex)
|
||||
{
|
||||
if (_items.Count == 0)
|
||||
{
|
||||
item = default(CommandModel);
|
||||
rval = false;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
item = _items.Where(c => c.Status == CommandStatus.Queued)
|
||||
.OrderByDescending(c => c.Priority)
|
||||
.ThenBy(c => c.QueuedAt)
|
||||
.First();
|
||||
|
||||
_items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
public CommandModel[] ToArray()
|
||||
{
|
||||
CommandModel[] rval = null;
|
||||
|
||||
lock (Mutex)
|
||||
{
|
||||
rval = _items.ToArray();
|
||||
}
|
||||
|
||||
return rval;
|
||||
}
|
||||
|
||||
public void Add(CommandModel item)
|
||||
{
|
||||
lock (Mutex)
|
||||
{
|
||||
_items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
204
src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
Normal file
204
src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
Normal file
@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public interface IManageCommandQueue
|
||||
{
|
||||
CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command;
|
||||
CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified);
|
||||
IEnumerable<CommandModel> Queue(CancellationToken cancellationToken);
|
||||
CommandModel Get(int id);
|
||||
List<CommandModel> GetStarted();
|
||||
void SetMessage(CommandModel command, string message);
|
||||
void Start(CommandModel command);
|
||||
void Complete(CommandModel command, string message);
|
||||
void Fail(CommandModel command, string message, Exception e);
|
||||
void Requeue();
|
||||
void CleanCommands();
|
||||
}
|
||||
|
||||
public class CommandQueueManager : IManageCommandQueue, IHandle<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly ICommandRepository _repo;
|
||||
private readonly IServiceFactory _serviceFactory;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly ICached<CommandModel> _commandCache;
|
||||
private readonly BlockingCollection<CommandModel> _commandQueue;
|
||||
|
||||
public CommandQueueManager(ICommandRepository repo,
|
||||
IServiceFactory serviceFactory,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_repo = repo;
|
||||
_serviceFactory = serviceFactory;
|
||||
_logger = logger;
|
||||
|
||||
_commandCache = cacheManager.GetCache<CommandModel>(GetType());
|
||||
_commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
|
||||
}
|
||||
|
||||
public CommandModel Push<TCommand>(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command
|
||||
{
|
||||
Ensure.That(command, () => command).IsNotNull();
|
||||
|
||||
_logger.Trace("Publishing {0}", command.Name);
|
||||
_logger.Trace("Checking if command is queued or started: {0}", command.Name);
|
||||
|
||||
var existingCommands = _repo.FindQueuedOrStarted(command.Name);
|
||||
var existing = existingCommands.SingleOrDefault(c => CommandEqualityComparer.Instance.Equals(c.Body, command));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
_logger.Trace("Command is already in progress: {0}", command.Name);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
var commandModel = new CommandModel
|
||||
{
|
||||
Name = command.Name,
|
||||
Body = command,
|
||||
QueuedAt = DateTime.UtcNow,
|
||||
Trigger = trigger,
|
||||
Priority = priority,
|
||||
Status = CommandStatus.Queued
|
||||
};
|
||||
|
||||
_logger.Trace("Inserting new command: {0}", commandModel.Name);
|
||||
|
||||
_repo.Insert(commandModel);
|
||||
_commandQueue.Add(commandModel);
|
||||
_commandCache.Set(commandModel.Id.ToString(), commandModel);
|
||||
|
||||
return commandModel;
|
||||
}
|
||||
|
||||
public CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified)
|
||||
{
|
||||
dynamic command = GetCommand(commandName);
|
||||
command.LastExecutionTime = lastExecutionTime;
|
||||
command.Trigger = trigger;
|
||||
|
||||
return Push(command, priority, trigger);
|
||||
}
|
||||
|
||||
public IEnumerable<CommandModel> Queue(CancellationToken cancellationToken)
|
||||
{
|
||||
return _commandQueue.GetConsumingEnumerable(cancellationToken);
|
||||
}
|
||||
|
||||
public CommandModel Get(int id)
|
||||
{
|
||||
return _commandCache.Get(id.ToString(), () => FindMessage(_repo.Get(id)));
|
||||
}
|
||||
|
||||
public List<CommandModel> GetStarted()
|
||||
{
|
||||
_logger.Trace("Getting started commands");
|
||||
return _commandCache.Values.Where(c => c.Status == CommandStatus.Started).ToList();
|
||||
}
|
||||
|
||||
public void SetMessage(CommandModel command, string message)
|
||||
{
|
||||
command.Message = message;
|
||||
_commandCache.Set(command.Id.ToString(), command);
|
||||
}
|
||||
|
||||
public void Start(CommandModel command)
|
||||
{
|
||||
command.StartedAt = DateTime.UtcNow;
|
||||
command.Status = CommandStatus.Started;
|
||||
|
||||
_logger.Trace("Marking command as started: {0}", command.Name);
|
||||
_repo.Update(command);
|
||||
_commandCache.Set(command.Id.ToString(), command);
|
||||
}
|
||||
|
||||
public void Complete(CommandModel command, string message)
|
||||
{
|
||||
Update(command, CommandStatus.Completed, message);
|
||||
}
|
||||
|
||||
public void Fail(CommandModel command, string message, Exception e)
|
||||
{
|
||||
command.Exception = e.ToString();
|
||||
|
||||
Update(command, CommandStatus.Failed, message);
|
||||
}
|
||||
|
||||
public void Requeue()
|
||||
{
|
||||
foreach (var command in _repo.Queued())
|
||||
{
|
||||
_commandQueue.Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
public void CleanCommands()
|
||||
{
|
||||
_logger.Trace("Cleaning up old commands");
|
||||
_repo.Trim();
|
||||
|
||||
var old = _commandCache.Values.Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(5));
|
||||
|
||||
foreach (var command in old)
|
||||
{
|
||||
_commandCache.Remove(command.Id.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private dynamic GetCommand(string commandName)
|
||||
{
|
||||
commandName = commandName.Split('.').Last();
|
||||
|
||||
var commandType = _serviceFactory.GetImplementations(typeof(Command))
|
||||
.Single(c => c.Name.Equals(commandName, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
return Json.Deserialize("{}", commandType);
|
||||
}
|
||||
|
||||
private CommandModel FindMessage(CommandModel command)
|
||||
{
|
||||
var cachedCommand = _commandCache.Find(command.Id.ToString());
|
||||
|
||||
if (cachedCommand != null)
|
||||
{
|
||||
command.Message = cachedCommand.Message;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
private void Update(CommandModel command, CommandStatus status, string message)
|
||||
{
|
||||
SetMessage(command, message);
|
||||
|
||||
command.EndedAt = DateTime.UtcNow;
|
||||
command.Duration = command.EndedAt.Value.Subtract(command.StartedAt.Value);
|
||||
command.Status = status;
|
||||
|
||||
_logger.Trace("Updating command status");
|
||||
_repo.Update(command);
|
||||
_commandCache.Set(command.Id.ToString(), command);
|
||||
}
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
_logger.Trace("Orphaning incomplete commands");
|
||||
_repo.OrphanStarted();
|
||||
}
|
||||
}
|
||||
}
|
71
src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs
Normal file
71
src/NzbDrone.Core/Messaging/Commands/CommandRepository.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public interface ICommandRepository : IBasicRepository<CommandModel>
|
||||
{
|
||||
void Trim();
|
||||
void OrphanStarted();
|
||||
List<CommandModel> FindCommands(string name);
|
||||
List<CommandModel> FindQueuedOrStarted(string name);
|
||||
List<CommandModel> Queued();
|
||||
List<CommandModel> Started();
|
||||
}
|
||||
|
||||
public class CommandRepository : BasicRepository<CommandModel>, ICommandRepository
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
|
||||
public CommandRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public void Trim()
|
||||
{
|
||||
var date = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
Delete(c => c.EndedAt < date);
|
||||
}
|
||||
|
||||
public void OrphanStarted()
|
||||
{
|
||||
var mapper = _database.GetDataMapper();
|
||||
|
||||
mapper.Parameters.Add(new SQLiteParameter("@orphaned", (int)CommandStatus.Orphaned));
|
||||
mapper.Parameters.Add(new SQLiteParameter("@started", (int)CommandStatus.Started));
|
||||
mapper.Parameters.Add(new SQLiteParameter("@ended", DateTime.UtcNow));
|
||||
|
||||
mapper.ExecuteNonQuery(@"UPDATE Commands
|
||||
SET Status = @orphaned, EndedAt = @ended
|
||||
WHERE Status = @started");
|
||||
}
|
||||
|
||||
public List<CommandModel> FindCommands(string name)
|
||||
{
|
||||
return Query.Where(c => c.Name == name).ToList();
|
||||
}
|
||||
|
||||
public List<CommandModel> FindQueuedOrStarted(string name)
|
||||
{
|
||||
return Query.Where(c => c.Name == name)
|
||||
.AndWhere("[Status] IN (0,1)")
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<CommandModel> Queued()
|
||||
{
|
||||
return Query.Where(c => c.Status == CommandStatus.Queued);
|
||||
}
|
||||
|
||||
public List<CommandModel> Started()
|
||||
{
|
||||
return Query.Where(c => c.Status == CommandStatus.Started);
|
||||
}
|
||||
}
|
||||
}
|
13
src/NzbDrone.Core/Messaging/Commands/CommandStatus.cs
Normal file
13
src/NzbDrone.Core/Messaging/Commands/CommandStatus.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public enum CommandStatus
|
||||
{
|
||||
Queued = 0,
|
||||
Started = 1,
|
||||
Completed = 2,
|
||||
Failed = 3,
|
||||
Aborted = 4,
|
||||
Cancelled = 5,
|
||||
Orphaned = 6
|
||||
}
|
||||
}
|
9
src/NzbDrone.Core/Messaging/Commands/CommandTrigger.cs
Normal file
9
src/NzbDrone.Core/Messaging/Commands/CommandTrigger.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public enum CommandTrigger
|
||||
{
|
||||
Unspecified = 0,
|
||||
Manual = 1,
|
||||
Scheduled = 2
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public interface ICommandExecutor
|
||||
{
|
||||
void PublishCommand<TCommand>(TCommand command) where TCommand : Command;
|
||||
void PublishCommand(string commandTypeName, DateTime? lastEecutionTime);
|
||||
Command PublishCommandAsync<TCommand>(TCommand command) where TCommand : Command;
|
||||
Command PublishCommandAsync(string commandTypeName);
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class MessagingCleanupCommand : Command
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands
|
||||
{
|
||||
public class RequeueQueuedCommands : IHandle<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public RequeueQueuedCommands(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
_commandQueueManager.Requeue();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands.Tracking
|
||||
{
|
||||
public enum CommandStatus
|
||||
{
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Cache;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands.Tracking
|
||||
{
|
||||
public interface ITrackCommands
|
||||
{
|
||||
Command GetById(int id);
|
||||
Command GetById(string id);
|
||||
void Completed(Command trackedCommand);
|
||||
void Failed(Command trackedCommand, Exception e);
|
||||
IEnumerable<Command> RunningCommands();
|
||||
Command FindExisting(Command command);
|
||||
void Store(Command command);
|
||||
void Start(Command command);
|
||||
}
|
||||
|
||||
public class CommandTrackingService : ITrackCommands, IExecute<TrackedCommandCleanupCommand>
|
||||
{
|
||||
private readonly ICached<Command> _cache;
|
||||
|
||||
public CommandTrackingService(ICacheManager cacheManager)
|
||||
{
|
||||
_cache = cacheManager.GetCache<Command>(GetType());
|
||||
}
|
||||
|
||||
public Command GetById(int id)
|
||||
{
|
||||
return _cache.Find(id.ToString());
|
||||
}
|
||||
|
||||
public Command GetById(string id)
|
||||
{
|
||||
return _cache.Find(id);
|
||||
}
|
||||
|
||||
public void Start(Command command)
|
||||
{
|
||||
command.Start();
|
||||
}
|
||||
|
||||
public void Completed(Command trackedCommand)
|
||||
{
|
||||
trackedCommand.Completed();
|
||||
}
|
||||
|
||||
public void Failed(Command trackedCommand, Exception e)
|
||||
{
|
||||
trackedCommand.Failed(e);
|
||||
}
|
||||
|
||||
public IEnumerable<Command> RunningCommands()
|
||||
{
|
||||
return _cache.Values.Where(c => c.State == CommandStatus.Running);
|
||||
}
|
||||
|
||||
public Command FindExisting(Command command)
|
||||
{
|
||||
return RunningCommands().SingleOrDefault(t => CommandEqualityComparer.Instance.Equals(t, command));
|
||||
}
|
||||
|
||||
public void Store(Command command)
|
||||
{
|
||||
if (command.GetType() == typeof(TrackedCommandCleanupCommand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.Set(command.Id.ToString(), command);
|
||||
}
|
||||
|
||||
public void Execute(TrackedCommandCleanupCommand message)
|
||||
{
|
||||
var old = _cache.Values.Where(c => c.State != CommandStatus.Running && c.StateChangeTime < DateTime.UtcNow.AddMinutes(-5));
|
||||
|
||||
foreach (var trackedCommand in old)
|
||||
{
|
||||
_cache.Remove(trackedCommand.Id.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Commands.Tracking
|
||||
{
|
||||
public class ExistingCommand
|
||||
{
|
||||
public Boolean Existing { get; set; }
|
||||
public Command Command { get; set; }
|
||||
|
||||
public ExistingCommand(Boolean exisitng, Command trackedCommand)
|
||||
{
|
||||
Existing = exisitng;
|
||||
Command = trackedCommand;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace NzbDrone.Core.Messaging.Commands.Tracking
|
||||
{
|
||||
public class TrackedCommandCleanupCommand : Command
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Messaging.Events
|
||||
{
|
||||
public class CommandCreatedEvent : IEvent
|
||||
{
|
||||
public Command Command { get; private set; }
|
||||
|
||||
public CommandCreatedEvent(Command trackedCommand)
|
||||
{
|
||||
Command = trackedCommand;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,11 @@ namespace NzbDrone.Core.Messaging.Events
|
||||
{
|
||||
public class CommandExecutedEvent : IEvent
|
||||
{
|
||||
public Command Command { get; private set; }
|
||||
public CommandModel Command { get; private set; }
|
||||
|
||||
public CommandExecutedEvent(Command trackedCommand)
|
||||
public CommandExecutedEvent(CommandModel command)
|
||||
{
|
||||
Command = trackedCommand;
|
||||
Command = command;
|
||||
}
|
||||
}
|
||||
}
|
@ -156,9 +156,11 @@
|
||||
<Compile Include="Datastore\Converters\BooleanIntConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\EnumIntConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\TimeSpanConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\Int32Converter.cs" />
|
||||
<Compile Include="Datastore\Converters\GuidConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\OsPathConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\CommandConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\QualityIntConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\UtcConverter.cs" />
|
||||
@ -235,6 +237,7 @@
|
||||
<Compile Include="Datastore\Migration\060_remove_enable_from_indexers.cs" />
|
||||
<Compile Include="Datastore\Migration\062_convert_quality_models.cs" />
|
||||
<Compile Include="Datastore\Migration\065_make_scene_numbering_nullable.cs" />
|
||||
<Compile Include="Datastore\Migration\078_add_commands_table.cs" />
|
||||
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
|
||||
<Compile Include="Datastore\Migration\066_add_tags.cs" />
|
||||
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
|
||||
@ -424,6 +427,7 @@
|
||||
<Compile Include="History\HistoryRepository.cs" />
|
||||
<Compile Include="History\HistoryService.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupCommandQueue.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
|
||||
@ -433,6 +437,7 @@
|
||||
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleases.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\DeleteBadMediaCovers.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\TrimLogDatabase.cs" />
|
||||
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
||||
@ -525,14 +530,13 @@
|
||||
<Compile Include="Instrumentation\Commands\ClearLogCommand.cs" />
|
||||
<Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" />
|
||||
<Compile Include="Instrumentation\Commands\DeleteUpdateLogFilesCommand.cs" />
|
||||
<Compile Include="Instrumentation\Commands\TrimLogCommand.cs" />
|
||||
<Compile Include="Instrumentation\DatabaseTarget.cs" />
|
||||
<Compile Include="Instrumentation\DeleteLogFilesService.cs" />
|
||||
<Compile Include="Instrumentation\Log.cs" />
|
||||
<Compile Include="Instrumentation\LogRepository.cs" />
|
||||
<Compile Include="Instrumentation\LogService.cs" />
|
||||
<Compile Include="Instrumentation\ReconfigureLogging.cs" />
|
||||
<Compile Include="Jobs\JobRepository.cs" />
|
||||
<Compile Include="Jobs\ScheduledTaskRepository.cs" />
|
||||
<Compile Include="Jobs\ScheduledTask.cs" />
|
||||
<Compile Include="Jobs\Scheduler.cs" />
|
||||
<Compile Include="Jobs\TaskManager.cs" />
|
||||
@ -547,7 +551,6 @@
|
||||
<Compile Include="MediaCover\MediaCoverService.cs" />
|
||||
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
|
||||
<Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" />
|
||||
<Compile Include="MediaFiles\Commands\CleanMediaFileDb.cs" />
|
||||
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
|
||||
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
|
||||
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
|
||||
@ -603,18 +606,24 @@
|
||||
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
|
||||
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
|
||||
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
|
||||
<Compile Include="Messaging\Commands\CleanupCommandMessagingService.cs" />
|
||||
<Compile Include="Messaging\Commands\Command.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandEqualityComparer.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandExecutor.cs" />
|
||||
<Compile Include="Messaging\Commands\ICommandExecutor.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandFailedException.cs" />
|
||||
<Compile Include="Messaging\Commands\MessagingCleanupCommand.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandModel.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandPriority.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandNotFoundException.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandQueue.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandStatus.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandRepository.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandQueueManager.cs" />
|
||||
<Compile Include="Messaging\Commands\CommandTrigger.cs" />
|
||||
<Compile Include="Messaging\Commands\IExecute.cs" />
|
||||
<Compile Include="Messaging\Commands\RequeueQueuedCommands.cs" />
|
||||
<Compile Include="Messaging\Commands\TestCommand.cs" />
|
||||
<Compile Include="Messaging\Commands\TestCommandExecutor.cs" />
|
||||
<Compile Include="Messaging\Commands\Tracking\CommandStatus.cs" />
|
||||
<Compile Include="Messaging\Commands\Tracking\CommandTrackingService.cs" />
|
||||
<Compile Include="Messaging\Commands\Tracking\ExistingCommand.cs" />
|
||||
<Compile Include="Messaging\Commands\Tracking\TrackedCommandCleanupCommand.cs" />
|
||||
<Compile Include="Messaging\Events\CommandCreatedEvent.cs" />
|
||||
<Compile Include="Messaging\Events\CommandExecutedEvent.cs" />
|
||||
<Compile Include="Messaging\Events\EventAggregator.cs" />
|
||||
<Compile Include="Messaging\Events\IEventAggregator.cs" />
|
||||
@ -853,6 +862,7 @@
|
||||
<Compile Include="Tv\RefreshSeriesService.cs" />
|
||||
<Compile Include="Tv\Season.cs" />
|
||||
<Compile Include="Tv\Series.cs" />
|
||||
<Compile Include="Tv\SeriesAddedHandler.cs" />
|
||||
<Compile Include="Tv\SeriesScannedHandler.cs" />
|
||||
<Compile Include="Tv\SeriesEditedService.cs" />
|
||||
<Compile Include="Tv\SeriesRepository.cs" />
|
||||
|
@ -5,9 +5,9 @@ namespace NzbDrone.Core.ProgressMessaging
|
||||
{
|
||||
public class CommandUpdatedEvent : IEvent
|
||||
{
|
||||
public Command Command { get; set; }
|
||||
public CommandModel Command { get; set; }
|
||||
|
||||
public CommandUpdatedEvent(Command command)
|
||||
public CommandUpdatedEvent(CommandModel command)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using NLog.Config;
|
||||
using System;
|
||||
using NLog.Config;
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands.Tracking;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.ProgressMessaging
|
||||
@ -11,13 +11,13 @@ namespace NzbDrone.Core.ProgressMessaging
|
||||
public class ProgressMessageTarget : Target, IHandle<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ITrackCommands _trackCommands;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private static LoggingRule _rule;
|
||||
|
||||
public ProgressMessageTarget(IEventAggregator eventAggregator, ITrackCommands trackCommands)
|
||||
public ProgressMessageTarget(IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_trackCommands = trackCommands;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
protected override void Write(LogEventInfo logEvent)
|
||||
@ -26,27 +26,26 @@ protected override void Write(LogEventInfo logEvent)
|
||||
|
||||
if (IsClientMessage(logEvent, command))
|
||||
{
|
||||
command.SetMessage(logEvent.FormattedMessage);
|
||||
_commandQueueManager.SetMessage(command, logEvent.FormattedMessage);
|
||||
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Command GetCurrentCommand()
|
||||
private CommandModel GetCurrentCommand()
|
||||
{
|
||||
var commandId = MappedDiagnosticsContext.Get("CommandId");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(commandId))
|
||||
if (String.IsNullOrWhiteSpace(commandId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _trackCommands.GetById(commandId);
|
||||
return _commandQueueManager.Get(Convert.ToInt32(commandId));
|
||||
}
|
||||
|
||||
private bool IsClientMessage(LogEventInfo logEvent, Command command)
|
||||
private bool IsClientMessage(LogEventInfo logEvent, CommandModel command)
|
||||
{
|
||||
if (command == null || !command.SendUpdatesToClient)
|
||||
if (command == null || !command.Body.SendUpdatesToClient)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -63,4 +62,4 @@ public void Handle(ApplicationStartedEvent message)
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,4 +11,4 @@ public SeriesAddedEvent(Series series)
|
||||
Series = series;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class RefreshSeriesService : IExecute<RefreshSeriesCommand>, IHandleAsync<SeriesAddedEvent>
|
||||
public class RefreshSeriesService : IExecute<RefreshSeriesCommand>
|
||||
{
|
||||
private readonly IProvideSeriesInfo _seriesInfo;
|
||||
private readonly ISeriesService _seriesService;
|
||||
@ -138,7 +138,7 @@ public void Execute(RefreshSeriesCommand message)
|
||||
|
||||
foreach (var series in allSeries)
|
||||
{
|
||||
if (message.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
|
||||
if (message.Trigger == CommandTrigger.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -165,10 +165,5 @@ public void Execute(RefreshSeriesCommand message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleAsync(SeriesAddedEvent message)
|
||||
{
|
||||
RefreshSeriesInfo(message.Series);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
src/NzbDrone.Core/Tv/SeriesAddedHandler.cs
Normal file
22
src/NzbDrone.Core/Tv/SeriesAddedHandler.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Tv.Commands;
|
||||
using NzbDrone.Core.Tv.Events;
|
||||
|
||||
namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class SeriesAddedHandler : IHandle<SeriesAddedEvent>
|
||||
{
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public SeriesAddedHandler(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Handle(SeriesAddedEvent message)
|
||||
{
|
||||
_commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id));
|
||||
}
|
||||
}
|
||||
}
|
@ -7,18 +7,18 @@ namespace NzbDrone.Core.Tv
|
||||
{
|
||||
public class SeriesEditedService : IHandle<SeriesEditedEvent>
|
||||
{
|
||||
private readonly CommandExecutor _commandExecutor;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public SeriesEditedService(CommandExecutor commandExecutor)
|
||||
public SeriesEditedService(IManageCommandQueue commandQueueManager)
|
||||
{
|
||||
_commandExecutor = commandExecutor;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
}
|
||||
|
||||
public void Handle(SeriesEditedEvent message)
|
||||
{
|
||||
if (message.Series.SeriesType != message.OldSeries.SeriesType)
|
||||
{
|
||||
_commandExecutor.PublishCommandAsync(new RefreshSeriesCommand(message.Series.Id));
|
||||
_commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,18 @@ public class SeriesScannedHandler : IHandle<SeriesScannedEvent>,
|
||||
{
|
||||
private readonly ISeriesService _seriesService;
|
||||
private readonly IEpisodeService _episodeService;
|
||||
private readonly ICommandExecutor _commandExecutor;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
private readonly Logger _logger;
|
||||
|
||||
public SeriesScannedHandler(ISeriesService seriesService,
|
||||
IEpisodeService episodeService,
|
||||
ICommandExecutor commandExecutor,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
{
|
||||
_seriesService = seriesService;
|
||||
_episodeService = episodeService;
|
||||
_commandExecutor = commandExecutor;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ private void HandleScanEvents(Series series)
|
||||
|
||||
if (series.AddOptions.SearchForMissingEpisodes)
|
||||
{
|
||||
_commandExecutor.PublishCommand(new MissingEpisodeSearchCommand(series.Id));
|
||||
_commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id));
|
||||
}
|
||||
|
||||
series.AddOptions = null;
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
@ -11,5 +11,13 @@ public override bool SendUpdatesToClient
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override string CompletionMessage
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Restarting Sonarr to apply updates";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ public void Execute(ApplicationUpdateCommand message)
|
||||
return;
|
||||
}
|
||||
|
||||
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && !message.Manual)
|
||||
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual)
|
||||
{
|
||||
_logger.ProgressDebug("Auto-update not enabled, not installing available update.");
|
||||
return;
|
||||
@ -200,23 +200,21 @@ public void Execute(ApplicationUpdateCommand message)
|
||||
try
|
||||
{
|
||||
InstallUpdate(latestAvailable);
|
||||
|
||||
message.Completed("Restarting Sonarr to apply updates");
|
||||
}
|
||||
catch (UpdateFolderNotWritableException ex)
|
||||
{
|
||||
_logger.ErrorException("Update process failed", ex);
|
||||
message.Failed(ex, string.Format("Startup folder not writable by user '{0}'", Environment.UserName));
|
||||
throw new CommandFailedException("Startup folder not writable by user '{0}'", ex, Environment.UserName);
|
||||
}
|
||||
catch (UpdateVerificationFailedException ex)
|
||||
{
|
||||
_logger.ErrorException("Update process failed", ex);
|
||||
message.Failed(ex, "Downloaded update package is corrupt");
|
||||
throw new CommandFailedException("Downloaded update package is corrupt", ex);
|
||||
}
|
||||
catch (UpdateFailedException ex)
|
||||
{
|
||||
_logger.ErrorException("Update process failed", ex);
|
||||
message.Failed(ex);
|
||||
throw new CommandFailedException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ var singleton = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
model.bind('change:state', function(model) {
|
||||
model.bind('change:status', function(model) {
|
||||
if (!model.isActive()) {
|
||||
options.element.stopSpin();
|
||||
|
||||
|
@ -6,6 +6,14 @@ module.exports = Backbone.Model.extend({
|
||||
|
||||
parse : function(response) {
|
||||
response.name = response.name.toLocaleLowerCase();
|
||||
response.body.name = response.body.name.toLocaleLowerCase();
|
||||
|
||||
for (var key in response.body) {
|
||||
response[key] = response.body[key];
|
||||
}
|
||||
|
||||
delete response.body;
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
@ -33,10 +41,10 @@ module.exports = Backbone.Model.extend({
|
||||
},
|
||||
|
||||
isActive : function() {
|
||||
return this.get('state') !== 'completed' && this.get('state') !== 'failed';
|
||||
return this.get('status') !== 'completed' && this.get('status') !== 'failed';
|
||||
},
|
||||
|
||||
isComplete : function() {
|
||||
return this.get('state') === 'completed';
|
||||
return this.get('status') === 'completed';
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user