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
pull/4/head
Mark McDowall 9 years ago
parent 446d470f53
commit 638e3ca898

@ -116,14 +116,12 @@ namespace NzbDrone.Api.Test.MappingTests
profileResource.InjectTo<Profile>(); profileResource.InjectTo<Profile>();
} }
[Test] [Test]
public void should_map_tracked_command() public void should_map_tracked_command()
{ {
var profileResource = new ApplicationUpdateCommand(); var commandResource = new CommandModel { Body = new ApplicationUpdateCommand() };
profileResource.InjectTo<CommandResource>(); commandResource.InjectTo<CommandResource>();
} }
} }

@ -4,10 +4,9 @@ using System.Linq;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Mapping; using NzbDrone.Api.Mapping;
using NzbDrone.Api.Validation; using NzbDrone.Api.Validation;
using NzbDrone.Common.Composition; using NzbDrone.Common;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging; using NzbDrone.Core.ProgressMessaging;
using NzbDrone.SignalR; using NzbDrone.SignalR;
@ -15,56 +14,53 @@ using NzbDrone.SignalR;
namespace NzbDrone.Api.Commands 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 IManageCommandQueue _commandQueueManager;
private readonly IContainer _container; private readonly IServiceFactory _serviceFactory;
private readonly ITrackCommands _trackCommands;
public CommandModule(ICommandExecutor commandExecutor, public CommandModule(IManageCommandQueue commandQueueManager,
IBroadcastSignalRMessage signalRBroadcaster, IBroadcastSignalRMessage signalRBroadcaster,
IContainer container, IServiceFactory serviceFactory)
ITrackCommands trackCommands)
: base(signalRBroadcaster) : base(signalRBroadcaster)
{ {
_commandExecutor = commandExecutor; _commandQueueManager = commandQueueManager;
_container = container; _serviceFactory = serviceFactory;
_trackCommands = trackCommands;
GetResourceById = GetCommand; GetResourceById = GetCommand;
CreateResource = StartCommand; CreateResource = StartCommand;
GetResourceAll = GetAllCommands; GetResourceAll = GetStartedCommands;
PostValidator.RuleFor(c => c.Name).NotBlank(); PostValidator.RuleFor(c => c.Name).NotBlank();
} }
private CommandResource GetCommand(int id) private CommandResource GetCommand(int id)
{ {
return _trackCommands.GetById(id).InjectTo<CommandResource>(); return _commandQueueManager.Get(id).InjectTo<CommandResource>();
} }
private int StartCommand(CommandResource commandResource) private int StartCommand(CommandResource commandResource)
{ {
var commandType = var commandType =
_container.GetImplementations(typeof(Command)) _serviceFactory.GetImplementations(typeof (Command))
.Single(c => c.Name.Replace("Command", "") .Single(c => c.Name.Replace("Command", "")
.Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase));
dynamic command = Request.Body.FromJson(commandType); 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; return trackedCommand.Id;
} }
private List<CommandResource> GetAllCommands() private List<CommandResource> GetStartedCommands()
{ {
return ToListResource(_trackCommands.RunningCommands); return ToListResource(_commandQueueManager.GetStarted());
} }
public void Handle(CommandUpdatedEvent message) public void Handle(CommandUpdatedEvent message)
{ {
if (message.Command.SendUpdatesToClient) if (message.Command.Body.SendUpdatesToClient)
{ {
BroadcastResourceChange(ModelAction.Updated, message.Command.Id); BroadcastResourceChange(ModelAction.Updated, message.Command.Id);
} }

@ -1,6 +1,7 @@
using System; using System;
using Newtonsoft.Json;
using NzbDrone.Api.REST; using NzbDrone.Api.REST;
using NzbDrone.Core.Messaging.Commands.Tracking; using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Api.Commands namespace NzbDrone.Api.Commands
{ {
@ -8,11 +9,75 @@ namespace NzbDrone.Api.Commands
{ {
public String Name { get; set; } public String Name { get; set; }
public String Message { get; set; } public String Message { get; set; }
public DateTime StartedOn { get; set; } public Command Body { get; set; }
public DateTime StateChangeTime { get; set; } public CommandPriority Priority { get; set; }
public Boolean SendUpdatesToClient { get; set; } public CommandStatus Status { get; set; }
public CommandStatus State { 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 DateTime? LastExecutionTime { get; set; }
public Boolean Manual { get; set; }
} }
} }

@ -100,6 +100,14 @@ namespace NzbDrone.Common.Cache
_store.Clear(); _store.Clear();
} }
public void ClearExpired()
{
foreach (var cached in _store.Where(c => c.Value.IsExpired()))
{
Remove(cached.Key);
}
}
public ICollection<T> Values public ICollection<T> Values
{ {
get get

@ -6,6 +6,7 @@ namespace NzbDrone.Common.Cache
public interface ICached public interface ICached
{ {
void Clear(); void Clear();
void ClearExpired();
void Remove(string key); void Remove(string key);
int Count { get; } int Count { get; }
} }

@ -61,8 +61,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
Mocker.GetMock<ICommandExecutor>() Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never()); .Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
} }
[Test] [Test]
@ -80,8 +80,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
Mocker.GetMock<ICommandExecutor>() Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.PublishCommand(It.IsAny<CleanMediaFileDb>()), Times.Never()); .Verify(v => v.Clean(It.IsAny<Series>()), Times.Never());
} }
[Test] [Test]

@ -6,7 +6,6 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
@ -16,6 +15,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{ {
private const string DELETED_PATH = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!"; private const string DELETED_PATH = "ANY FILE WITH THIS PATH IS CONSIDERED DELETED!";
private List<Episode> _episodes; private List<Episode> _episodes;
private Series _series;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@ -24,9 +24,8 @@ namespace NzbDrone.Core.Test.MediaFiles
.Build() .Build()
.ToList(); .ToList();
Mocker.GetMock<ISeriesService>() _series = Builder<Series>.CreateNew()
.Setup(s => s.GetSeries(It.IsAny<Int32>())) .Build();
.Returns(Builder<Series>.CreateNew().Build());
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(e => e.FileExists(It.Is<String>(c => !c.Contains(DELETED_PATH)))) .Setup(e => e.FileExists(It.Is<String>(c => !c.Contains(DELETED_PATH))))
@ -61,7 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenEpisodeFiles(episodeFiles); GivenEpisodeFiles(episodeFiles);
Subject.Execute(new CleanMediaFileDb(0)); Subject.Clean(_series);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never()); Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
} }
@ -76,7 +75,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenEpisodeFiles(episodeFiles); 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)); Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.Is<EpisodeFile>(e => e.RelativePath == DELETED_PATH), DeleteMediaFileReason.MissingFromDisk), Times.Exactly(2));
} }
@ -92,7 +91,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenEpisodeFiles(episodeFiles); GivenEpisodeFiles(episodeFiles);
GivenFilesAreNotAttachedToEpisode(); GivenFilesAreNotAttachedToEpisode();
Subject.Execute(new CleanMediaFileDb(0)); Subject.Clean(_series);
Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.IsAny<EpisodeFile>(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10)); Mocker.GetMock<IMediaFileService>().Verify(c => c.Delete(It.IsAny<EpisodeFile>(), DeleteMediaFileReason.NoLinkedEpisodes), Times.Exactly(10));
} }
@ -102,7 +101,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{ {
GivenEpisodeFiles(new List<EpisodeFile>()); 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)); Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.Is<Episode>(e => e.EpisodeFileId == 0)), Times.Exactly(10));
} }
@ -117,7 +116,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenEpisodeFiles(episodeFiles); GivenEpisodeFiles(episodeFiles);
Subject.Execute(new CleanMediaFileDb(0)); Subject.Clean(_series);
Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never()); Mocker.GetMock<IEpisodeService>().Verify(c => c.UpdateEpisode(It.IsAny<Episode>()), Times.Never());
} }

@ -1,121 +1,121 @@
using System; //using System;
using System.Collections.Generic; //using System.Collections.Generic;
using Moq; //using Moq;
using NUnit.Framework; //using NUnit.Framework;
using NzbDrone.Common; //using NzbDrone.Common;
using NzbDrone.Core.Messaging.Commands; //using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking; //using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events; //using NzbDrone.Core.Messaging.Events;
using NzbDrone.Test.Common; //using NzbDrone.Test.Common;
//
namespace NzbDrone.Core.Test.Messaging.Commands //namespace NzbDrone.Core.Test.Messaging.Commands
{ //{
[TestFixture] // [TestFixture]
public class CommandExecutorFixture : TestBase<CommandExecutor> // public class CommandExecutorFixture : TestBase<CommandExecutor>
{ // {
private Mock<IExecute<CommandA>> _executorA; // private Mock<IExecute<CommandA>> _executorA;
private Mock<IExecute<CommandB>> _executorB; // private Mock<IExecute<CommandB>> _executorB;
//
[SetUp] // [SetUp]
public void Setup() // public void Setup()
{ // {
_executorA = new Mock<IExecute<CommandA>>(); // _executorA = new Mock<IExecute<CommandA>>();
_executorB = new Mock<IExecute<CommandB>>(); // _executorB = new Mock<IExecute<CommandB>>();
//
Mocker.GetMock<IServiceFactory>() // Mocker.GetMock<IServiceFactory>()
.Setup(c => c.Build(typeof(IExecute<CommandA>))) // .Setup(c => c.Build(typeof(IExecute<CommandA>)))
.Returns(_executorA.Object); // .Returns(_executorA.Object);
//
Mocker.GetMock<IServiceFactory>() // Mocker.GetMock<IServiceFactory>()
.Setup(c => c.Build(typeof(IExecute<CommandB>))) // .Setup(c => c.Build(typeof(IExecute<CommandB>)))
.Returns(_executorB.Object); // .Returns(_executorB.Object);
//
//
Mocker.GetMock<ITrackCommands>() // Mocker.GetMock<ITrackCommands>()
.Setup(c => c.FindExisting(It.IsAny<Command>())) // .Setup(c => c.FindExisting(It.IsAny<Command>()))
.Returns<Command>(null); // .Returns<Command>(null);
} // }
//
[Test] // [Test]
public void should_publish_command_to_executor() // public void should_publish_command_to_executor()
{ // {
var commandA = new CommandA(); // var commandA = new CommandA();
//
Subject.PublishCommand(commandA); // Subject.Push(commandA);
//
_executorA.Verify(c => c.Execute(commandA), Times.Once()); // _executorA.Verify(c => c.Execute(commandA), Times.Once());
} // }
//
[Test] // [Test]
public void should_publish_command_by_with_optional_arg_using_name() // public void should_publish_command_by_with_optional_arg_using_name()
{ // {
Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command))) // Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command)))
.Returns(new List<Type> { typeof(CommandA), typeof(CommandB) }); // .Returns(new List<Type> { typeof(CommandA), typeof(CommandB) });
//
Subject.PublishCommand(typeof(CommandA).FullName); // Subject.Push(typeof(CommandA).FullName);
_executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once()); // _executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once());
} // }
//
//
[Test] // [Test]
public void should_not_publish_to_incompatible_executor() // public void should_not_publish_to_incompatible_executor()
{ // {
var commandA = new CommandA(); // var commandA = new CommandA();
//
Subject.PublishCommand(commandA); // Subject.Push(commandA);
//
_executorA.Verify(c => c.Execute(commandA), Times.Once()); // _executorA.Verify(c => c.Execute(commandA), Times.Once());
_executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never()); // _executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
} // }
//
[Test] // [Test]
public void broken_executor_should_throw_the_exception() // public void broken_executor_should_throw_the_exception()
{ // {
var commandA = new CommandA(); // var commandA = new CommandA();
//
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>())) // _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
.Throws(new NotImplementedException()); // .Throws(new NotImplementedException());
//
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA)); // Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
} // }
//
//
[Test] // [Test]
public void broken_executor_should_publish_executed_event() // public void broken_executor_should_publish_executed_event()
{ // {
var commandA = new CommandA(); // var commandA = new CommandA();
//
_executorA.Setup(c => c.Execute(It.IsAny<CommandA>())) // _executorA.Setup(c => c.Execute(It.IsAny<CommandA>()))
.Throws(new NotImplementedException()); // .Throws(new NotImplementedException());
//
Assert.Throws<NotImplementedException>(() => Subject.PublishCommand(commandA)); // Assert.Throws<NotImplementedException>(() => Subject.Push(commandA));
//
VerifyEventPublished<CommandExecutedEvent>(); // VerifyEventPublished<CommandExecutedEvent>();
} // }
//
[Test] // [Test]
public void should_publish_executed_event_on_success() // public void should_publish_executed_event_on_success()
{ // {
var commandA = new CommandA(); // var commandA = new CommandA();
Subject.PublishCommand(commandA); // Subject.Push(commandA);
//
VerifyEventPublished<CommandExecutedEvent>(); // VerifyEventPublished<CommandExecutedEvent>();
} // }
} // }
//
public class CommandA : Command // public class CommandA : Command
{ // {
public CommandA(int id = 0) // public CommandA(int id = 0)
{ // {
} // }
} // }
//
public class CommandB : Command // public class CommandB : Command
{ // {
//
public CommandB() // 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="MediaFiles\UpgradeMediaFileServiceFixture.cs" />
<Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" /> <Compile Include="Messaging\Commands\CommandEqualityComparerFixture.cs" />
<Compile Include="Messaging\Commands\CommandExecutorFixture.cs" /> <Compile Include="Messaging\Commands\CommandExecutorFixture.cs" />
<Compile Include="Messaging\Commands\CommandFixture.cs" />
<Compile Include="Messaging\Events\EventAggregatorFixture.cs" /> <Compile Include="Messaging\Events\EventAggregatorFixture.cs" />
<Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" /> <Compile Include="Metadata\Consumers\Roksbox\FindMetadataFileFixture.cs" />
<Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" /> <Compile Include="Metadata\Consumers\Wdtv\FindMetadataFileFixture.cs" />

@ -8,7 +8,6 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests namespace NzbDrone.Core.Test.TvTests.SeriesAddedHandlerTests
{ {

@ -12,7 +12,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Model; using NzbDrone.Common.Model;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
using NzbDrone.Core.Update.Commands; using NzbDrone.Core.Update.Commands;
@ -163,7 +163,7 @@ namespace NzbDrone.Core.Test.UpdateTests
{ {
Mocker.GetMock<IVerifyUpdates>().Setup(c => c.Verify(It.IsAny<UpdatePackage>(), It.IsAny<String>())).Returns(false); 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()); Mocker.GetMock<IArchiveService>().Verify(v => v.Extract(It.IsAny<String>(), It.IsAny<String>()), Times.Never());
} }
@ -189,7 +189,7 @@ namespace NzbDrone.Core.Test.UpdateTests
GivenInstallScript(""); GivenInstallScript("");
Subject.Execute(new ApplicationUpdateCommand()); Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never()); Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -203,7 +203,7 @@ namespace NzbDrone.Core.Test.UpdateTests
GivenInstallScript(null); GivenInstallScript(null);
Subject.Execute(new ApplicationUpdateCommand()); Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never()); Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -221,7 +221,7 @@ namespace NzbDrone.Core.Test.UpdateTests
.Setup(s => s.FileExists(scriptPath, StringComparison.Ordinal)) .Setup(s => s.FileExists(scriptPath, StringComparison.Ordinal))
.Returns(false); .Returns(false);
Subject.Execute(new ApplicationUpdateCommand()); Assert.Throws<CommandFailedException>(() => Subject.Execute(new ApplicationUpdateCommand()));
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never()); Mocker.GetMock<IProcessProvider>().Verify(v => v.Start(scriptPath, It.IsAny<String>(), null, null), Times.Never());
@ -255,7 +255,7 @@ namespace NzbDrone.Core.Test.UpdateTests
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).Returns(@"C:\NzbDrone\AppData".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); ExceptionVerification.ExpectedErrors(1);
} }
@ -265,7 +265,7 @@ namespace NzbDrone.Core.Test.UpdateTests
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic); Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.StartUpFolder).Returns(@"C:\NzbDrone".AsOsAgnostic);
Mocker.GetMock<IAppFolderInfo>().SetupGet(c => c.AppDataFolder).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); ExceptionVerification.ExpectedErrors(1);
} }
@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.UpdateTests
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName); 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()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
@ -293,7 +293,7 @@ namespace NzbDrone.Core.Test.UpdateTests
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName); 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()); Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);

@ -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);
}
}
}

@ -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.ThingiProvider;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
@ -103,6 +104,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles"); Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
Mapper.Entity<User>().RegisterModel("Users"); Mapper.Entity<User>().RegisterModel("Users");
Mapper.Entity<CommandModel>().RegisterModel("Commands")
.Ignore(c => c.Message);
} }
private static void RegisterMappers() private static void RegisterMappers()
@ -125,6 +128,9 @@ namespace NzbDrone.Core.Datastore
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter()); 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() private static void RegisterProviderSettingConverter()

@ -12,14 +12,17 @@ namespace NzbDrone.Core.Download
{ {
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly ICommandExecutor _commandExecutor; private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger; 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; _configService = configService;
_episodeService = episodeService; _episodeService = episodeService;
_commandExecutor = commandExecutor; _commandQueueManager = commandQueueManager;
_logger = logger; _logger = logger;
} }
@ -35,7 +38,7 @@ namespace NzbDrone.Core.Download
{ {
_logger.Debug("Failed download only contains one episode, searching again"); _logger.Debug("Failed download only contains one episode, searching again");
_commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(message.EpisodeIds)); _commandQueueManager.Push(new EpisodeSearchCommand(message.EpisodeIds));
return; return;
} }
@ -47,7 +50,7 @@ namespace NzbDrone.Core.Download
{ {
_logger.Debug("Failed download was entire season, searching again"); _logger.Debug("Failed download was entire season, searching again");
_commandExecutor.PublishCommandAsync(new SeasonSearchCommand _commandQueueManager.Push(new SeasonSearchCommand
{ {
SeriesId = message.SeriesId, SeriesId = message.SeriesId,
SeasonNumber = seasonNumber SeasonNumber = seasonNumber
@ -58,7 +61,7 @@ namespace NzbDrone.Core.Download
_logger.Debug("Failed download contains multiple episodes, probably a double episode, searching again"); _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 @@ namespace NzbDrone.Core.HealthCheck
public void Execute(CheckHealthCommand 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 @@ using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Housekeeping 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 IEnumerable<IHousekeepingTask> _housekeepers;
private readonly Logger _logger; private readonly Logger _logger;
@ -40,7 +41,7 @@ namespace NzbDrone.Core.Housekeeping
} }
} }
// 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"); _logger.Debug("Compressing main database after housekeeping");
_mainDb.Vacuum(); _mainDb.Vacuum();
} }

@ -112,9 +112,9 @@ namespace NzbDrone.Core.IndexerSearch
{ {
List<Episode> episodes; 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 && .Where(e => e.Monitored &&
!e.HasFile && !e.HasFile &&
e.AirDateUtc.HasValue && e.AirDateUtc.HasValue &&

@ -4,7 +4,7 @@ namespace NzbDrone.Core.IndexerSearch
{ {
public class MissingEpisodeSearchCommand : Command public class MissingEpisodeSearchCommand : Command
{ {
public int SeriesId { get; private set; } public int? SeriesId { get; private set; }
public override bool SendUpdatesToClient 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 @@ namespace NzbDrone.Core.Instrumentation
PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec); PagingSpec<Log> Paged(PagingSpec<Log> pagingSpec);
} }
public class LogService : ILogService, IExecute<TrimLogCommand>, IExecute<ClearLogCommand> public class LogService : ILogService, IExecute<ClearLogCommand>
{ {
private readonly ILogRepository _logRepository; private readonly ILogRepository _logRepository;
@ -23,11 +23,6 @@ namespace NzbDrone.Core.Instrumentation
return _logRepository.GetPaged(pagingSpec); return _logRepository.GetPaged(pagingSpec);
} }
public void Execute(TrimLogCommand message)
{
_logRepository.Trim();
}
public void Execute(ClearLogCommand message) public void Execute(ClearLogCommand message)
{ {
_logRepository.Purge(vacuum: true); _logRepository.Purge(vacuum: true);

@ -12,7 +12,6 @@ namespace NzbDrone.Core.Jobs
void SetLastExecutionTime(int id, DateTime executionTime); void SetLastExecutionTime(int id, DateTime executionTime);
} }
public class ScheduledTaskRepository : BasicRepository<ScheduledTask>, IScheduledTaskRepository public class ScheduledTaskRepository : BasicRepository<ScheduledTask>, IScheduledTaskRepository
{ {

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
@ -15,15 +16,15 @@ namespace NzbDrone.Core.Jobs
IHandle<ApplicationShutdownRequested> IHandle<ApplicationShutdownRequested>
{ {
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly ICommandExecutor _commandExecutor; private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger; private readonly Logger _logger;
private static readonly Timer Timer = new Timer(); private static readonly Timer Timer = new Timer();
private static CancellationTokenSource _cancellationTokenSource; private static CancellationTokenSource _cancellationTokenSource;
public Scheduler(ITaskManager taskManager, ICommandExecutor commandExecutor, Logger logger) public Scheduler(ITaskManager taskManager, IManageCommandQueue commandQueueManager, Logger logger)
{ {
_taskManager = taskManager; _taskManager = taskManager;
_commandExecutor = commandExecutor; _commandQueueManager = commandQueueManager;
_logger = logger; _logger = logger;
} }
@ -33,24 +34,16 @@ namespace NzbDrone.Core.Jobs
{ {
Timer.Enabled = false; Timer.Enabled = false;
var tasks = _taskManager.GetPending(); var tasks = _taskManager.GetPending().ToList();
_logger.Trace("Pending Tasks: {0}", tasks.Count); _logger.Trace("Pending Tasks: {0}", tasks.Count);
foreach (var task in tasks) foreach (var task in tasks)
{ {
_cancellationTokenSource.Token.ThrowIfCancellationRequested(); _commandQueueManager.Push(task.TypeName, task.LastExecution, CommandPriority.Low, CommandTrigger.Scheduled);
try
{
_commandExecutor.PublishCommand(task.TypeName, task.LastExecution);
}
catch (Exception e)
{
_logger.ErrorException("Error occurred while executing task " + task.TypeName, e);
}
} }
} }
finally finally
{ {
if (!_cancellationTokenSource.IsCancellationRequested) if (!_cancellationTokenSource.IsCancellationRequested)

@ -10,10 +10,9 @@ using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck; using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Housekeeping; using NzbDrone.Core.Housekeeping;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Instrumentation.Commands;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Tv.Commands;
using NzbDrone.Core.Update.Commands; using NzbDrone.Core.Update.Commands;
@ -62,10 +61,9 @@ namespace NzbDrone.Core.Jobs
{ {
var defaultTasks = new[] var defaultTasks = new[]
{ {
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFinishedDownloadCommand).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 = 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 = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName},
new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
@ -127,7 +125,7 @@ namespace NzbDrone.Core.Jobs
public void Handle(CommandExecutedEvent message) 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) 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 @@ namespace NzbDrone.Core.MediaFiles
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IMakeImportDecision _importDecisionMaker; private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedEpisodes _importApprovedEpisodes; private readonly IImportApprovedEpisodes _importApprovedEpisodes;
private readonly ICommandExecutor _commandExecutor;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
public DiskScanService(IDiskProvider diskProvider, public DiskScanService(IDiskProvider diskProvider,
IMakeImportDecision importDecisionMaker, IMakeImportDecision importDecisionMaker,
IImportApprovedEpisodes importApprovedEpisodes, IImportApprovedEpisodes importApprovedEpisodes,
ICommandExecutor commandExecutor,
IConfigService configService, IConfigService configService,
ISeriesService seriesService, ISeriesService seriesService,
IMediaFileTableCleanupService mediaFileTableCleanupService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
Logger logger) Logger logger)
{ {
_diskProvider = diskProvider; _diskProvider = diskProvider;
_importDecisionMaker = importDecisionMaker; _importDecisionMaker = importDecisionMaker;
_importApprovedEpisodes = importApprovedEpisodes; _importApprovedEpisodes = importApprovedEpisodes;
_commandExecutor = commandExecutor;
_configService = configService; _configService = configService;
_seriesService = seriesService; _seriesService = seriesService;
_mediaFileTableCleanupService = mediaFileTableCleanupService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
} }
@ -79,7 +79,7 @@ namespace NzbDrone.Core.MediaFiles
} }
_logger.ProgressInfo("Scanning disk for {0}", series.Title); _logger.ProgressInfo("Scanning disk for {0}", series.Title);
_commandExecutor.PublishCommand(new CleanMediaFileDb(series.Id)); _mediaFileTableCleanupService.Clean(series);
if (!_diskProvider.FolderExists(series.Path)) if (!_diskProvider.FolderExists(series.Path))
{ {

@ -3,19 +3,20 @@ using System.IO;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Core.MediaFiles 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 IMediaFileService _mediaFileService;
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly ISeriesService _seriesService;
private readonly Logger _logger; private readonly Logger _logger;
public MediaFileTableCleanupService(IMediaFileService mediaFileService, public MediaFileTableCleanupService(IMediaFileService mediaFileService,
@ -27,15 +28,13 @@ namespace NzbDrone.Core.MediaFiles
_mediaFileService = mediaFileService; _mediaFileService = mediaFileService;
_diskProvider = diskProvider; _diskProvider = diskProvider;
_episodeService = episodeService; _episodeService = episodeService;
_seriesService = seriesService;
_logger = logger; _logger = logger;
} }
public void Execute(CleanMediaFileDb message) public void Clean(Series series)
{ {
var seriesFile = _mediaFileService.GetFilesBySeries(message.SeriesId); var seriesFile = _mediaFileService.GetFilesBySeries(series.Id);
var series = _seriesService.GetSeries(message.SeriesId); var episodes = _episodeService.GetEpisodeBySeries(series.Id);
var episodes = _episodeService.GetEpisodeBySeries(message.SeriesId);
foreach (var episodeFile in seriesFile) 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 System;
using FluentMigrator.Runner;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Commands.Tracking;
namespace NzbDrone.Core.Messaging.Commands 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 public virtual Boolean SendUpdatesToClient
{ {
get get
@ -23,63 +12,21 @@ namespace NzbDrone.Core.Messaging.Commands
} }
} }
public TimeSpan Runtime public virtual string CompletionMessage
{ {
get 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 String Name { get; private set; }
public DateTime? LastExecutionTime { get; set; } public DateTime? LastExecutionTime { get; set; }
public CommandTrigger Trigger { get; set; }
protected Command() public Command()
{ {
Name = GetType().Name.Replace("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 @@ namespace NzbDrone.Core.Messaging.Commands
public int GetHashCode(Command obj) public int GetHashCode(Command obj)
{ {
return obj.Id.GetHashCode(); return obj.GetHashCode();
} }
} }
} }

@ -1,117 +1,59 @@
using System; using System;
using System.Linq; using System.Threading;
using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Lifecycle;
using NzbDrone.Common.Serializer;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ProgressMessaging; using NzbDrone.Core.ProgressMessaging;
namespace NzbDrone.Core.Messaging.Commands namespace NzbDrone.Core.Messaging.Commands
{ {
public class CommandExecutor : ICommandExecutor public class CommandExecutor : IHandle<ApplicationStartedEvent>,
IHandle<ApplicationShutdownRequested>
{ {
private readonly Logger _logger; private readonly Logger _logger;
private readonly IServiceFactory _serviceFactory; private readonly IServiceFactory _serviceFactory;
private readonly ITrackCommands _trackCommands; private readonly IManageCommandQueue _commandQueueManager;
private readonly IEventAggregator _eventAggregator; 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;
var scheduler = new LimitedConcurrencyLevelTaskScheduler(3);
public CommandExecutor(IServiceFactory serviceFactory,
IManageCommandQueue commandQueueManager,
IEventAggregator eventAggregator,
Logger logger)
{
_logger = logger; _logger = logger;
_serviceFactory = serviceFactory; _serviceFactory = serviceFactory;
_trackCommands = trackCommands; _commandQueueManager = commandQueueManager;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_taskFactory = new TaskFactory(scheduler);
}
public void PublishCommand<TCommand>(TCommand command) where TCommand : Command
{
Ensure.That(command, () => command).IsNotNull();
_logger.Trace("Publishing {0}", command.GetType().Name);
if (_trackCommands.FindExisting(command) != null)
{
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
return;
}
_trackCommands.Store(command);
ExecuteCommand<TCommand>(command);
}
public void PublishCommand(string commandTypeName)
{
PublishCommand(commandTypeName, null);
} }
public void PublishCommand(string commandTypeName, DateTime? lastExecutionTime) private void ExecuteCommands()
{ {
dynamic command = GetCommand(commandTypeName); try
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)
{
_logger.Trace("Command is already in progress: {0}", command.GetType().Name);
return existingCommand;
}
_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) foreach (var command in _commandQueueManager.Queue(_cancellationTokenSource.Token))
, TaskCreationOptions.PreferFairness | (TaskCreationOptions)0x10) {
.LogExceptions(); try
{
ExecuteCommand((dynamic)command.Body, command);
}
catch (Exception ex)
{
_logger.ErrorException("Error occurred while executing task " + command.Name, ex);
}
}
} }
else catch (ThreadAbortException ex)
{ {
_taskFactory.StartNew(() => ExecuteCommand<TCommand>(command) _logger.ErrorException(ex.Message, ex);
, TaskCreationOptions.PreferFairness) Thread.ResetAbort();
.LogExceptions();
} }
return command;
} }
public Command PublishCommandAsync(string commandTypeName) private void ExecuteCommand<TCommand>(TCommand command, CommandModel commandModel) where TCommand : Command
{
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
{ {
var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType()); var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType());
var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract); var handler = (IExecute<TCommand>)_serviceFactory.Build(handlerContract);
@ -120,47 +62,67 @@ namespace NzbDrone.Core.Messaging.Commands
try try
{ {
_trackCommands.Start(command); _commandQueueManager.Start(commandModel);
BroadcastCommandUpdate(command); 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) _commandQueueManager.Complete(commandModel, command.CompletionMessage);
{ }
_trackCommands.Completed(command); catch (CommandFailedException ex)
} {
_commandQueueManager.Fail(commandModel, ex.Message, ex);
throw;
} }
catch (Exception e) catch (Exception ex)
{ {
_trackCommands.Failed(command, e); _commandQueueManager.SetMessage(commandModel, "Failed");
_commandQueueManager.Fail(commandModel, "Failed", ex);
throw; throw;
} }
finally finally
{ {
BroadcastCommandUpdate(command); BroadcastCommandUpdate(commandModel);
_eventAggregator.PublishEvent(new CommandExecutedEvent(command));
_eventAggregator.PublishEvent(new CommandExecutedEvent(commandModel));
if (MappedDiagnosticsContext.Get("CommandId") == command.Id.ToString()) if (MappedDiagnosticsContext.Get("CommandId") == commandModel.Id.ToString())
{ {
MappedDiagnosticsContext.Remove("CommandId"); 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(CommandModel command)
private void BroadcastCommandUpdate(Command command)
{ {
if (command.SendUpdatesToClient) if (command.Body.SendUpdatesToClient)
{ {
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command)); _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)
{
}
}
}

@ -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)
{
}
}
}

@ -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
}
}

@ -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);
}
}
}
}

@ -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();
}
}
}

@ -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);
}
}
}

@ -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
}
}

@ -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 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\BooleanIntConverter.cs" />
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" /> <Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
<Compile Include="Datastore\Converters\EnumIntConverter.cs" /> <Compile Include="Datastore\Converters\EnumIntConverter.cs" />
<Compile Include="Datastore\Converters\TimeSpanConverter.cs" />
<Compile Include="Datastore\Converters\Int32Converter.cs" /> <Compile Include="Datastore\Converters\Int32Converter.cs" />
<Compile Include="Datastore\Converters\GuidConverter.cs" /> <Compile Include="Datastore\Converters\GuidConverter.cs" />
<Compile Include="Datastore\Converters\OsPathConverter.cs" /> <Compile Include="Datastore\Converters\OsPathConverter.cs" />
<Compile Include="Datastore\Converters\CommandConverter.cs" />
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" /> <Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
<Compile Include="Datastore\Converters\QualityIntConverter.cs" /> <Compile Include="Datastore\Converters\QualityIntConverter.cs" />
<Compile Include="Datastore\Converters\UtcConverter.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\060_remove_enable_from_indexers.cs" />
<Compile Include="Datastore\Migration\062_convert_quality_models.cs" /> <Compile Include="Datastore\Migration\062_convert_quality_models.cs" />
<Compile Include="Datastore\Migration\065_make_scene_numbering_nullable.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\068_add_release_restrictions.cs" />
<Compile Include="Datastore\Migration\066_add_tags.cs" /> <Compile Include="Datastore\Migration\066_add_tags.cs" />
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" /> <Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
@ -424,6 +427,7 @@
<Compile Include="History\HistoryRepository.cs" /> <Compile Include="History\HistoryRepository.cs" />
<Compile Include="History\HistoryService.cs" /> <Compile Include="History\HistoryService.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupAdditionalNamingSpecs.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupCommandQueue.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupDuplicateMetadataFiles.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedBlacklist.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFiles.cs" />
@ -433,6 +437,7 @@
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleases.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedPendingReleases.cs" />
<Compile Include="Housekeeping\Housekeepers\DeleteBadMediaCovers.cs" /> <Compile Include="Housekeeping\Housekeepers\DeleteBadMediaCovers.cs" />
<Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" /> <Compile Include="Housekeeping\Housekeepers\FixFutureRunScheduledTasks.cs" />
<Compile Include="Housekeeping\Housekeepers\TrimLogDatabase.cs" />
<Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" /> <Compile Include="Housekeeping\Housekeepers\UpdateCleanTitleForSeries.cs" />
<Compile Include="Housekeeping\HousekeepingCommand.cs" /> <Compile Include="Housekeeping\HousekeepingCommand.cs" />
<Compile Include="Housekeeping\HousekeepingService.cs" /> <Compile Include="Housekeeping\HousekeepingService.cs" />
@ -525,14 +530,13 @@
<Compile Include="Instrumentation\Commands\ClearLogCommand.cs" /> <Compile Include="Instrumentation\Commands\ClearLogCommand.cs" />
<Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" /> <Compile Include="Instrumentation\Commands\DeleteLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\DeleteUpdateLogFilesCommand.cs" /> <Compile Include="Instrumentation\Commands\DeleteUpdateLogFilesCommand.cs" />
<Compile Include="Instrumentation\Commands\TrimLogCommand.cs" />
<Compile Include="Instrumentation\DatabaseTarget.cs" /> <Compile Include="Instrumentation\DatabaseTarget.cs" />
<Compile Include="Instrumentation\DeleteLogFilesService.cs" /> <Compile Include="Instrumentation\DeleteLogFilesService.cs" />
<Compile Include="Instrumentation\Log.cs" /> <Compile Include="Instrumentation\Log.cs" />
<Compile Include="Instrumentation\LogRepository.cs" /> <Compile Include="Instrumentation\LogRepository.cs" />
<Compile Include="Instrumentation\LogService.cs" /> <Compile Include="Instrumentation\LogService.cs" />
<Compile Include="Instrumentation\ReconfigureLogging.cs" /> <Compile Include="Instrumentation\ReconfigureLogging.cs" />
<Compile Include="Jobs\JobRepository.cs" /> <Compile Include="Jobs\ScheduledTaskRepository.cs" />
<Compile Include="Jobs\ScheduledTask.cs" /> <Compile Include="Jobs\ScheduledTask.cs" />
<Compile Include="Jobs\Scheduler.cs" /> <Compile Include="Jobs\Scheduler.cs" />
<Compile Include="Jobs\TaskManager.cs" /> <Compile Include="Jobs\TaskManager.cs" />
@ -547,7 +551,6 @@
<Compile Include="MediaCover\MediaCoverService.cs" /> <Compile Include="MediaCover\MediaCoverService.cs" />
<Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" /> <Compile Include="MediaCover\MediaCoversUpdatedEvent.cs" />
<Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" /> <Compile Include="MediaFiles\Commands\BackendCommandAttribute.cs" />
<Compile Include="MediaFiles\Commands\CleanMediaFileDb.cs" />
<Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" /> <Compile Include="MediaFiles\Commands\CleanUpRecycleBinCommand.cs" />
<Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" /> <Compile Include="MediaFiles\Commands\DownloadedEpisodesScanCommand.cs" />
<Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" /> <Compile Include="MediaFiles\Commands\RenameFilesCommand.cs" />
@ -603,18 +606,24 @@
<Compile Include="MediaFiles\UpdateEpisodeFileService.cs" /> <Compile Include="MediaFiles\UpdateEpisodeFileService.cs" />
<Compile Include="MediaFiles\UpgradeMediaFileService.cs" /> <Compile Include="MediaFiles\UpgradeMediaFileService.cs" />
<Compile Include="Messaging\Commands\BackendCommandAttribute.cs" /> <Compile Include="Messaging\Commands\BackendCommandAttribute.cs" />
<Compile Include="Messaging\Commands\CleanupCommandMessagingService.cs" />
<Compile Include="Messaging\Commands\Command.cs" /> <Compile Include="Messaging\Commands\Command.cs" />
<Compile Include="Messaging\Commands\CommandEqualityComparer.cs" /> <Compile Include="Messaging\Commands\CommandEqualityComparer.cs" />
<Compile Include="Messaging\Commands\CommandExecutor.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\IExecute.cs" />
<Compile Include="Messaging\Commands\RequeueQueuedCommands.cs" />
<Compile Include="Messaging\Commands\TestCommand.cs" /> <Compile Include="Messaging\Commands\TestCommand.cs" />
<Compile Include="Messaging\Commands\TestCommandExecutor.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\CommandExecutedEvent.cs" />
<Compile Include="Messaging\Events\EventAggregator.cs" /> <Compile Include="Messaging\Events\EventAggregator.cs" />
<Compile Include="Messaging\Events\IEventAggregator.cs" /> <Compile Include="Messaging\Events\IEventAggregator.cs" />
@ -853,6 +862,7 @@
<Compile Include="Tv\RefreshSeriesService.cs" /> <Compile Include="Tv\RefreshSeriesService.cs" />
<Compile Include="Tv\Season.cs" /> <Compile Include="Tv\Season.cs" />
<Compile Include="Tv\Series.cs" /> <Compile Include="Tv\Series.cs" />
<Compile Include="Tv\SeriesAddedHandler.cs" />
<Compile Include="Tv\SeriesScannedHandler.cs" /> <Compile Include="Tv\SeriesScannedHandler.cs" />
<Compile Include="Tv\SeriesEditedService.cs" /> <Compile Include="Tv\SeriesEditedService.cs" />
<Compile Include="Tv\SeriesRepository.cs" /> <Compile Include="Tv\SeriesRepository.cs" />

@ -5,9 +5,9 @@ namespace NzbDrone.Core.ProgressMessaging
{ {
public class CommandUpdatedEvent : IEvent public class CommandUpdatedEvent : IEvent
{ {
public Command Command { get; set; } public CommandModel Command { get; set; }
public CommandUpdatedEvent(Command command) public CommandUpdatedEvent(CommandModel command)
{ {
Command = command; Command = command;
} }

@ -1,9 +1,9 @@
using NLog.Config; using System;
using NLog.Config;
using NLog; using NLog;
using NLog.Targets; using NLog.Targets;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Commands.Tracking;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.ProgressMessaging namespace NzbDrone.Core.ProgressMessaging
@ -11,13 +11,13 @@ namespace NzbDrone.Core.ProgressMessaging
public class ProgressMessageTarget : Target, IHandle<ApplicationStartedEvent> public class ProgressMessageTarget : Target, IHandle<ApplicationStartedEvent>
{ {
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ITrackCommands _trackCommands; private readonly IManageCommandQueue _commandQueueManager;
private static LoggingRule _rule; private static LoggingRule _rule;
public ProgressMessageTarget(IEventAggregator eventAggregator, ITrackCommands trackCommands) public ProgressMessageTarget(IEventAggregator eventAggregator, IManageCommandQueue commandQueueManager)
{ {
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_trackCommands = trackCommands; _commandQueueManager = commandQueueManager;
} }
protected override void Write(LogEventInfo logEvent) protected override void Write(LogEventInfo logEvent)
@ -26,27 +26,26 @@ namespace NzbDrone.Core.ProgressMessaging
if (IsClientMessage(logEvent, command)) if (IsClientMessage(logEvent, command))
{ {
command.SetMessage(logEvent.FormattedMessage); _commandQueueManager.SetMessage(command, logEvent.FormattedMessage);
_eventAggregator.PublishEvent(new CommandUpdatedEvent(command)); _eventAggregator.PublishEvent(new CommandUpdatedEvent(command));
} }
} }
private CommandModel GetCurrentCommand()
private Command GetCurrentCommand()
{ {
var commandId = MappedDiagnosticsContext.Get("CommandId"); var commandId = MappedDiagnosticsContext.Get("CommandId");
if (string.IsNullOrWhiteSpace(commandId)) if (String.IsNullOrWhiteSpace(commandId))
{ {
return null; 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; return false;
} }
@ -63,4 +62,4 @@ namespace NzbDrone.Core.ProgressMessaging
LogManager.ReconfigExistingLoggers(); LogManager.ReconfigExistingLoggers();
} }
} }
} }

@ -11,4 +11,4 @@ namespace NzbDrone.Core.Tv.Events
Series = series; Series = series;
} }
} }
} }

@ -15,7 +15,7 @@ using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Tv namespace NzbDrone.Core.Tv
{ {
public class RefreshSeriesService : IExecute<RefreshSeriesCommand>, IHandleAsync<SeriesAddedEvent> public class RefreshSeriesService : IExecute<RefreshSeriesCommand>
{ {
private readonly IProvideSeriesInfo _seriesInfo; private readonly IProvideSeriesInfo _seriesInfo;
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
@ -138,7 +138,7 @@ namespace NzbDrone.Core.Tv
foreach (var series in allSeries) foreach (var series in allSeries)
{ {
if (message.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series)) if (message.Trigger == CommandTrigger.Manual || _checkIfSeriesShouldBeRefreshed.ShouldRefresh(series))
{ {
try try
{ {
@ -165,10 +165,5 @@ namespace NzbDrone.Core.Tv
} }
} }
} }
public void HandleAsync(SeriesAddedEvent message)
{
RefreshSeriesInfo(message.Series);
}
} }
} }

@ -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> 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) public void Handle(SeriesEditedEvent message)
{ {
if (message.Series.SeriesType != message.OldSeries.SeriesType) 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 @@ namespace NzbDrone.Core.Tv
{ {
private readonly ISeriesService _seriesService; private readonly ISeriesService _seriesService;
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly ICommandExecutor _commandExecutor; private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger; private readonly Logger _logger;
public SeriesScannedHandler(ISeriesService seriesService, public SeriesScannedHandler(ISeriesService seriesService,
IEpisodeService episodeService, IEpisodeService episodeService,
ICommandExecutor commandExecutor, IManageCommandQueue commandQueueManager,
Logger logger) Logger logger)
{ {
_seriesService = seriesService; _seriesService = seriesService;
_episodeService = episodeService; _episodeService = episodeService;
_commandExecutor = commandExecutor; _commandQueueManager = commandQueueManager;
_logger = logger; _logger = logger;
} }
@ -82,7 +82,7 @@ namespace NzbDrone.Core.Tv
if (series.AddOptions.SearchForMissingEpisodes) if (series.AddOptions.SearchForMissingEpisodes)
{ {
_commandExecutor.PublishCommand(new MissingEpisodeSearchCommand(series.Id)); _commandQueueManager.Push(new MissingEpisodeSearchCommand(series.Id));
} }
series.AddOptions = null; series.AddOptions = null;

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using NLog; using NLog;
using NzbDrone.Common.EnsureThat; using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;

@ -11,5 +11,13 @@ namespace NzbDrone.Core.Update.Commands
return true; return true;
} }
} }
public override string CompletionMessage
{
get
{
return "Restarting Sonarr to apply updates";
}
}
} }
} }

@ -191,7 +191,7 @@ namespace NzbDrone.Core.Update
return; 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."); _logger.ProgressDebug("Auto-update not enabled, not installing available update.");
return; return;
@ -200,23 +200,21 @@ namespace NzbDrone.Core.Update
try try
{ {
InstallUpdate(latestAvailable); InstallUpdate(latestAvailable);
message.Completed("Restarting Sonarr to apply updates");
} }
catch (UpdateFolderNotWritableException ex) catch (UpdateFolderNotWritableException ex)
{ {
_logger.ErrorException("Update process failed", 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) catch (UpdateVerificationFailedException ex)
{ {
_logger.ErrorException("Update process failed", 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) catch (UpdateFailedException ex)
{ {
_logger.ErrorException("Update process failed", ex); _logger.ErrorException("Update process failed", ex);
message.Failed(ex); throw new CommandFailedException(ex);
} }
} }
} }

@ -74,7 +74,7 @@ var singleton = function() {
return; return;
} }
model.bind('change:state', function(model) { model.bind('change:status', function(model) {
if (!model.isActive()) { if (!model.isActive()) {
options.element.stopSpin(); options.element.stopSpin();

@ -6,6 +6,14 @@ module.exports = Backbone.Model.extend({
parse : function(response) { parse : function(response) {
response.name = response.name.toLocaleLowerCase(); 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; return response;
}, },
@ -33,10 +41,10 @@ module.exports = Backbone.Model.extend({
}, },
isActive : function() { isActive : function() {
return this.get('state') !== 'completed' && this.get('state') !== 'failed'; return this.get('status') !== 'completed' && this.get('status') !== 'failed';
}, },
isComplete : function() { isComplete : function() {
return this.get('state') === 'completed'; return this.get('status') === 'completed';
} }
}); });
Loading…
Cancel
Save