New: App health displayed in UI

This commit is contained in:
Mark McDowall 2014-02-25 21:40:47 -08:00
parent 90a6bcaa47
commit c8ae9f40fb
54 changed files with 873 additions and 44 deletions

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.HealthCheck;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Api.Health
{
public class HealthModule : NzbDroneRestModuleWithSignalR<HealthResource, HealthCheck>,
IHandle<TriggerHealthCheckEvent>
{
private readonly IHealthCheckService _healthCheckService;
public HealthModule(ICommandExecutor commandExecutor, IHealthCheckService healthCheckService)
: base(commandExecutor)
{
_healthCheckService = healthCheckService;
GetResourceAll = GetHealth;
}
private List<HealthResource> GetHealth()
{
return ToListResource(_healthCheckService.PerformHealthCheck);
}
public void Handle(TriggerHealthCheckEvent message)
{
BroadcastResourceChange(ModelAction.Sync);
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using NzbDrone.Api.REST;
using NzbDrone.Core.HealthCheck;
namespace NzbDrone.Api.Health
{
public class HealthResource : RestResource
{
public HealthCheckResultType Type { get; set; }
public String Message { get; set; }
}
}

View File

@ -132,6 +132,8 @@
<Compile Include="Frontend\Mappers\IMapHttpRequestsToDisk.cs" /> <Compile Include="Frontend\Mappers\IMapHttpRequestsToDisk.cs" />
<Compile Include="Frontend\Mappers\StaticResourceMapperBase.cs" /> <Compile Include="Frontend\Mappers\StaticResourceMapperBase.cs" />
<Compile Include="Frontend\StaticResourceModule.cs" /> <Compile Include="Frontend\StaticResourceModule.cs" />
<Compile Include="Health\HistoryResource.cs" />
<Compile Include="Health\HealthModule.cs" />
<Compile Include="History\HistoryResource.cs" /> <Compile Include="History\HistoryResource.cs" />
<Compile Include="History\HistoryModule.cs" /> <Compile Include="History\HistoryModule.cs" />
<Compile Include="Metadata\MetadataResource.cs" /> <Compile Include="Metadata\MetadataResource.cs" />

View File

@ -92,7 +92,7 @@ namespace NzbDrone.Automation.Test.PageModel
{ {
get get
{ {
return Find(By.LinkText("System")); return Find(By.PartialLinkText("System"));
} }
} }

View File

@ -52,5 +52,10 @@ namespace NzbDrone.Common
{ {
return CollapseSpace.Replace(text, " ").Trim(); return CollapseSpace.Replace(text, " ").Trim();
} }
public static bool IsNullOrWhiteSpace(this string text)
{
return String.IsNullOrWhiteSpace(text);
}
} }
} }

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DownloadClientCheckFixture : CoreTest<DownloadClientCheck>
{
[Test]
public void should_return_warning_when_download_client_has_not_been_configured()
{
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns((IDownloadClient)null);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_when_download_client_throws()
{
var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.GetQueue())
.Throws<Exception>();
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns(downloadClient.Object);
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_null_when_download_client_returns()
{
var downloadClient = Mocker.GetMock<IDownloadClient>();
downloadClient.Setup(s => s.GetQueue())
.Returns(new List<QueueItem>());
Mocker.GetMock<IProvideDownloadClient>()
.Setup(s => s.GetDownloadClient())
.Returns(downloadClient.Object);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DroneFactoryCheckFixture : CoreTest<DroneFactoryCheck>
{
private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted";
private void GivenDroneFactoryFolder(bool exists = false)
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadedEpisodesFolder)
.Returns(DRONE_FACTORY_FOLDER);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER))
.Returns(exists);
}
[Test]
public void should_return_warning_when_drone_factory_folder_is_not_configured()
{
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadedEpisodesFolder)
.Returns("");
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_when_drone_factory_folder_does_not_exist()
{
GivenDroneFactoryFolder();
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_error_when_unable_to_write_to_drone_factory_folder()
{
GivenDroneFactoryFolder(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>()))
.Throws<Exception>();
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_null_when_no_issues_found()
{
GivenDroneFactoryFolder(true);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,18 @@
using FluentAssertions;
using NzbDrone.Core.HealthCheck;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
public static class HealthCheckFixtureExtensions
{
public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result)
{
result.Type.Should().Be(HealthCheckResultType.Warning);
}
public static void ShouldBeError(this Core.HealthCheck.HealthCheck result)
{
result.Type.Should().Be(HealthCheckResultType.Error);
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Download;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class IndexerCheckFixture : CoreTest<IndexerCheck>
{
[Test]
public void should_return_error_when_not_indexers_are_enabled()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer>());
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_warning_when_only_enabled_indexer_is_wombles()
{
var indexer = Mocker.GetMock<IIndexer>();
indexer.SetupGet(s => s.SupportsSearching).Returns(false);
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer>{indexer.Object});
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_null_when_multiple_multiple_indexers_are_enabled()
{
var indexers = new List<IIndexer>{Mocker.GetMock<IIndexer>().Object, Mocker.GetMock<IIndexer>().Object};
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(indexers);
Subject.Check().Should().BeNull();
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class UpdateCheckFixture : CoreTest<UpdateCheck>
{
[Test]
public void should_return_error_when_app_folder_is_write_protected()
{
WindowsOnly();
Mocker.GetMock<IAppFolderInfo>()
.Setup(s => s.StartUpFolder)
.Returns(@"C:\NzbDrone");
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>()))
.Throws<Exception>();
Subject.Check().ShouldBeError();
}
}
}

View File

@ -125,6 +125,11 @@
<Compile Include="Framework\CoreTest.cs" /> <Compile Include="Framework\CoreTest.cs" />
<Compile Include="Framework\DbTest.cs" /> <Compile Include="Framework\DbTest.cs" />
<Compile Include="Framework\NBuilderExtensions.cs" /> <Compile Include="Framework\NBuilderExtensions.cs" />
<Compile Include="HealthCheck\Checks\UpdateCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\IndexerCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\DroneFactoryCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientCheckFixture.cs" />
<Compile Include="HealthCheck\Checks\HealthCheckFixtureExtentions.cs" />
<Compile Include="HistoryTests\HistoryServiceFixture.cs" /> <Compile Include="HistoryTests\HistoryServiceFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItemsFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItemsFixture.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodeFilesFixture.cs" />

View File

@ -58,11 +58,21 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{ {
} }
public void Execute(TestBlackholeCommand message) public override void Test()
{ {
var testPath = Path.Combine(message.Folder, "drone_test.txt"); PerformTest(Settings.Folder);
}
private void PerformTest(string folder)
{
var testPath = Path.Combine(folder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); _diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath); _diskProvider.DeleteFile(testPath);
} }
public void Execute(TestBlackholeCommand message)
{
PerformTest(message.Folder);
}
} }
} }

View File

@ -80,7 +80,12 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
throw new NotImplementedException(); throw new NotImplementedException();
} }
public VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null) public override void Test()
{
_proxy.GetVersion(Settings);
}
private VersionResponse GetVersion(string host = null, int port = 0, string username = null, string password = null)
{ {
return _proxy.GetVersion(Settings); return _proxy.GetVersion(Settings);
} }

View File

@ -80,11 +80,21 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
{ {
} }
public void Execute(TestPneumaticCommand message) public override void Test()
{ {
var testPath = Path.Combine(message.Folder, "drone_test.txt"); PerformTest(Settings.Folder);
}
private void PerformTest(string folder)
{
var testPath = Path.Combine(folder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); _diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath); _diskProvider.DeleteFile(testPath);
} }
public void Execute(TestPneumaticCommand message)
{
PerformTest(message.Folder);
}
} }
} }

View File

@ -122,6 +122,11 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
_sabnzbdProxy.RemoveFrom("history", id, Settings); _sabnzbdProxy.RemoveFrom("history", id, Settings);
} }
public override void Test()
{
_sabnzbdProxy.GetCategories(Settings);
}
public void Execute(TestSabnzbdCommand message) public void Execute(TestSabnzbdCommand message)
{ {
var settings = new SabnzbdSettings(); var settings = new SabnzbdSettings();

View File

@ -44,5 +44,6 @@ namespace NzbDrone.Core.Download
public abstract IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10); public abstract IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 10);
public abstract void RemoveFromQueue(string id); public abstract void RemoveFromQueue(string id);
public abstract void RemoveFromHistory(string id); public abstract void RemoveFromHistory(string id);
public abstract void Test();
} }
} }

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
@ -15,8 +16,8 @@ namespace NzbDrone.Core.Download
{ {
private readonly IDownloadClientRepository _providerRepository; private readonly IDownloadClientRepository _providerRepository;
public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable<IDownloadClient> providers, IContainer container, Logger logger) public DownloadClientFactory(IDownloadClientRepository providerRepository, IEnumerable<IDownloadClient> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
} }

View File

@ -1,9 +1,4 @@
using System.Collections.Generic; using System.Linq;
using System.Linq;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Download.Clients.Nzbget;
using NzbDrone.Core.Download.Clients.Sabnzbd;
namespace NzbDrone.Core.Download namespace NzbDrone.Core.Download
{ {

View File

@ -11,5 +11,6 @@ namespace NzbDrone.Core.Download
IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 0); IEnumerable<HistoryItem> GetHistory(int start = 0, int limit = 0);
void RemoveFromQueue(string id); void RemoveFromQueue(string id);
void RemoveFromHistory(string id); void RemoveFromHistory(string id);
void Test();
} }
} }

View File

@ -0,0 +1,8 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.HealthCheck
{
public class CheckHealthCommand : Command
{
}
}

View File

@ -0,0 +1,36 @@
using System;
using NzbDrone.Core.Download;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class DownloadClientCheck : IProvideHealthCheck
{
private readonly IProvideDownloadClient _downloadClientProvider;
public DownloadClientCheck(IProvideDownloadClient downloadClientProvider)
{
_downloadClientProvider = downloadClientProvider;
}
public HealthCheck Check()
{
var downloadClient = _downloadClientProvider.GetDownloadClient();
if (downloadClient == null)
{
return new HealthCheck(HealthCheckResultType.Warning, "No download client is available");
}
try
{
downloadClient.GetQueue();
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error, "Unable to communicate with download client");
}
return null;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.IO;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class DroneFactoryCheck : IProvideHealthCheck
{
private readonly IConfigService _configService;
private readonly IDiskProvider _diskProvider;
public DroneFactoryCheck(IConfigService configService, IDiskProvider diskProvider)
{
_configService = configService;
_diskProvider = diskProvider;
}
public HealthCheck Check()
{
var droneFactoryFolder = _configService.DownloadedEpisodesFolder;
if (droneFactoryFolder.IsNullOrWhiteSpace())
{
return new HealthCheck(HealthCheckResultType.Warning, "Drone factory folder is not configured");
}
if (!_diskProvider.FolderExists(droneFactoryFolder))
{
return new HealthCheck(HealthCheckResultType.Error, "Drone factory folder does not exist");
}
try
{
var testPath = Path.Combine(droneFactoryFolder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error, "Unable to write to drone factory folder");
}
//Todo: Unable to import one or more files/folders from
return null;
}
}
}

View File

@ -0,0 +1,34 @@
using System.Linq;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class IndexerCheck : IProvideHealthCheck
{
private readonly IIndexerFactory _indexerFactory;
public IndexerCheck(IIndexerFactory indexerFactory)
{
_indexerFactory = indexerFactory;
}
public HealthCheck Check()
{
var enabled = _indexerFactory.GetAvailableProviders();
if (!enabled.Any())
{
return new HealthCheck(HealthCheckResultType.Error, "No indexers are enabled");
}
if (enabled.All(i => i.SupportsSearching == false))
{
return new HealthCheck(HealthCheckResultType.Warning, "Enabled indexers do not support searching");
}
return null;
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.IO;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Update;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class UpdateCheck : IProvideHealthCheck
{
private readonly IDiskProvider _diskProvider;
private readonly IAppFolderInfo _appFolderInfo;
private readonly ICheckUpdateService _checkUpdateService;
public UpdateCheck(IDiskProvider diskProvider, IAppFolderInfo appFolderInfo, ICheckUpdateService checkUpdateService)
{
_diskProvider = diskProvider;
_appFolderInfo = appFolderInfo;
_checkUpdateService = checkUpdateService;
}
public HealthCheck Check()
{
if (OsInfo.IsWindows)
{
try
{
var testPath = Path.Combine(_appFolderInfo.StartUpFolder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception)
{
return new HealthCheck(HealthCheckResultType.Error,
"Unable to update, running from write-protected folder");
}
}
if (BuildInfo.BuildDateTime < DateTime.UtcNow.AddDays(-14))
{
if (_checkUpdateService.AvailableUpdate() != null)
{
return new HealthCheck(HealthCheckResultType.Warning, "New update is available");
}
}
return null;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.HealthCheck
{
public class HealthCheck : ModelBase
{
public HealthCheckResultType Type { get; set; }
public String Message { get; set; }
public HealthCheck(HealthCheckResultType type, string message)
{
Type = type;
Message = message;
}
}
public enum HealthCheckResultType
{
Warning = 1,
Error = 2
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck
{
public interface IHealthCheckService
{
List<HealthCheck> PerformHealthCheck();
}
public class HealthCheckService : IHealthCheckService,
IExecute<CheckHealthCommand>,
IHandleAsync<ConfigSavedEvent>,
IHandleAsync<ProviderUpdatedEvent<IIndexer>>,
IHandleAsync<ProviderUpdatedEvent<IDownloadClient>>
{
private readonly IEnumerable<IProvideHealthCheck> _healthChecks;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public HealthCheckService(IEnumerable<IProvideHealthCheck> healthChecks, IEventAggregator eventAggregator, Logger logger)
{
_healthChecks = healthChecks;
_eventAggregator = eventAggregator;
_logger = logger;
}
public List<HealthCheck> PerformHealthCheck()
{
_logger.Trace("Checking health");
var result = _healthChecks.Select(c => c.Check()).Where(c => c != null).ToList();
return result;
}
public void Execute(CheckHealthCommand message)
{
//Until we have stored health checks we should just trigger the complete event
//and let the clients check in
//Multiple connected clients means we're going to compute the health check multiple times
//Multiple checks feels a bit ugly, but means the most up to date information goes to the client
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ConfigSavedEvent message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ProviderUpdatedEvent<IIndexer> message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
public void HandleAsync(ProviderUpdatedEvent<IDownloadClient> message)
{
_eventAggregator.PublishEvent(new TriggerHealthCheckEvent());
}
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.HealthCheck
{
public interface IProvideHealthCheck
{
HealthCheck Check();
}
}

View File

@ -0,0 +1,8 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.HealthCheck
{
public class TriggerHealthCheckEvent : IEvent
{
}
}

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Indexers
IParseFeed Parser { get; } IParseFeed Parser { get; }
DownloadProtocol Protocol { get; } DownloadProtocol Protocol { get; }
Boolean SupportsPaging { get; } Boolean SupportsPaging { get; }
Boolean SupportsSearching { get; }
IEnumerable<string> RecentFeed { get; } IEnumerable<string> RecentFeed { get; }
IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber); IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);

View File

@ -35,6 +35,7 @@ namespace NzbDrone.Core.Indexers
public abstract DownloadProtocol Protocol { get; } public abstract DownloadProtocol Protocol { get; }
public abstract bool SupportsPaging { get; } public abstract bool SupportsPaging { get; }
public virtual bool SupportsSearching { get { return true; } }
protected TSettings Settings protected TSettings Settings
{ {

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Indexers namespace NzbDrone.Core.Indexers
@ -16,8 +17,8 @@ namespace NzbDrone.Core.Indexers
private readonly IIndexerRepository _providerRepository; private readonly IIndexerRepository _providerRepository;
private readonly INewznabTestService _newznabTestService; private readonly INewznabTestService _newznabTestService;
public IndexerFactory(IIndexerRepository providerRepository, IEnumerable<IIndexer> providers, IContainer container, INewznabTestService newznabTestService, Logger logger) public IndexerFactory(IIndexerRepository providerRepository, IEnumerable<IIndexer> providers, IContainer container, IEventAggregator eventAggregator, INewznabTestService newznabTestService, Logger logger)
: base(providerRepository, providers, container, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_newznabTestService = newznabTestService; _newznabTestService = newznabTestService;

View File

@ -22,6 +22,14 @@ namespace NzbDrone.Core.Indexers.Wombles
} }
} }
public override bool SupportsSearching
{
get
{
return false;
}
}
public override IParseFeed Parser public override IParseFeed Parser
{ {
get get

View File

@ -7,6 +7,7 @@ using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DataAugmentation.Xem; using NzbDrone.Core.DataAugmentation.Xem;
using NzbDrone.Core.Download; using NzbDrone.Core.Download;
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.Instrumentation.Commands;
@ -50,6 +51,7 @@ namespace NzbDrone.Core.Jobs
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName},
new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).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},

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Metadata.Consumers.Fake; using NzbDrone.Core.Metadata.Consumers.Fake;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@ -17,8 +18,8 @@ namespace NzbDrone.Core.Metadata
{ {
private readonly IMetadataRepository _providerRepository; private readonly IMetadataRepository _providerRepository;
public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, Logger logger) public MetadataFactory(IMetadataRepository providerRepository, IEnumerable<IMetadata> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
} }

View File

@ -2,6 +2,7 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Notifications namespace NzbDrone.Core.Notifications
@ -15,8 +16,8 @@ namespace NzbDrone.Core.Notifications
public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory public class NotificationFactory : ProviderFactory<INotification, NotificationDefinition>, INotificationFactory
{ {
public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, Logger logger) public NotificationFactory(INotificationRepository providerRepository, IEnumerable<INotification> providers, IContainer container, IEventAggregator eventAggregator, Logger logger)
: base(providerRepository, providers, container, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
} }

View File

@ -264,6 +264,13 @@
<Compile Include="Exceptions\DownstreamException.cs" /> <Compile Include="Exceptions\DownstreamException.cs" />
<Compile Include="Exceptions\NzbDroneClientException.cs" /> <Compile Include="Exceptions\NzbDroneClientException.cs" />
<Compile Include="Exceptions\StatusCodeToExceptions.cs" /> <Compile Include="Exceptions\StatusCodeToExceptions.cs" />
<Compile Include="HealthCheck\CheckHealthCommand.cs" />
<Compile Include="HealthCheck\Checks\DownloadClientCheck.cs" />
<Compile Include="HealthCheck\Checks\DroneFactoryCheck.cs" />
<Compile Include="HealthCheck\Checks\IndexerCheck.cs" />
<Compile Include="HealthCheck\Checks\UpdateCheck.cs" />
<Compile Include="HealthCheck\HealthCheck.cs" />
<Compile Include="HealthCheck\TriggerHealthCheckEvent.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodes.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedEpisodes.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItems.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedHistoryItems.cs" />
<Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" /> <Compile Include="Housekeeping\Housekeepers\CleanupOrphanedMetadataFiles.cs" />
@ -342,6 +349,8 @@
<Compile Include="Metadata\MetadataRepository.cs" /> <Compile Include="Metadata\MetadataRepository.cs" />
<Compile Include="Metadata\MetadataService.cs" /> <Compile Include="Metadata\MetadataService.cs" />
<Compile Include="Metadata\MetadataType.cs" /> <Compile Include="Metadata\MetadataType.cs" />
<Compile Include="HealthCheck\HealthCheckService.cs" />
<Compile Include="HealthCheck\IProvideHealthCheck.cs" />
<Compile Include="Notifications\NotificationFactory.cs" /> <Compile Include="Notifications\NotificationFactory.cs" />
<Compile Include="Notifications\NotificationService.cs" /> <Compile Include="Notifications\NotificationService.cs" />
<Compile Include="Notifications\DownloadMessage.cs" /> <Compile Include="Notifications\DownloadMessage.cs" />
@ -503,6 +512,7 @@
<Compile Include="Rest\JsonNetSerializer.cs" /> <Compile Include="Rest\JsonNetSerializer.cs" />
<Compile Include="RootFolders\RootFolderRepository.cs" /> <Compile Include="RootFolders\RootFolderRepository.cs" />
<Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" /> <Compile Include="ThingiProvider\ConfigContractNotFoundException.cs" />
<Compile Include="ThingiProvider\Events\ProviderUpdatedEvent.cs" />
<Compile Include="ThingiProvider\IProvider.cs" /> <Compile Include="ThingiProvider\IProvider.cs" />
<Compile Include="Qualities\QualityProfileInUseException.cs" /> <Compile Include="Qualities\QualityProfileInUseException.cs" />
<Compile Include="Qualities\QualityDefinitionRepository.cs" /> <Compile Include="Qualities\QualityDefinitionRepository.cs" />

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.ThingiProvider.Events
{
public class ProviderUpdatedEvent<TProvider> : IEvent
{
}
}

View File

@ -5,6 +5,7 @@ using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.ThingiProvider namespace NzbDrone.Core.ThingiProvider
{ {
@ -14,6 +15,7 @@ namespace NzbDrone.Core.ThingiProvider
{ {
private readonly IProviderRepository<TProviderDefinition> _providerRepository; private readonly IProviderRepository<TProviderDefinition> _providerRepository;
private readonly IContainer _container; private readonly IContainer _container;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
protected readonly List<TProvider> _providers; protected readonly List<TProvider> _providers;
@ -21,10 +23,12 @@ namespace NzbDrone.Core.ThingiProvider
protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository, protected ProviderFactory(IProviderRepository<TProviderDefinition> providerRepository,
IEnumerable<TProvider> providers, IEnumerable<TProvider> providers,
IContainer container, IContainer container,
IEventAggregator eventAggregator,
Logger logger) Logger logger)
{ {
_providerRepository = providerRepository; _providerRepository = providerRepository;
_container = container; _container = container;
_eventAggregator = eventAggregator;
_providers = providers.ToList(); _providers = providers.ToList();
_logger = logger; _logger = logger;
} }
@ -62,6 +66,7 @@ namespace NzbDrone.Core.ThingiProvider
public virtual void Update(TProviderDefinition definition) public virtual void Update(TProviderDefinition definition)
{ {
_providerRepository.Update(definition); _providerRepository.Update(definition);
_eventAggregator.PublishEvent(new ProviderUpdatedEvent<TProvider>());
} }
public void Delete(int id) public void Delete(int id)

View File

@ -48,7 +48,7 @@
.icon-nd-warning:before { .icon-nd-warning:before {
.icon(@warning-sign); .icon(@warning-sign);
color : #f89406; color : @orange;
} }
.icon-nd-edit:before { .icon-nd-edit:before {
@ -79,7 +79,7 @@
.icon-nd-form-warning:before { .icon-nd-form-warning:before {
.icon(@warning-sign); .icon(@warning-sign);
color: #f89406; color: @orange;
} }
.icon-nd-form-danger:before { .icon-nd-form-danger:before {
@ -175,4 +175,14 @@
.icon-nd-restart:before { .icon-nd-restart:before {
.icon(@repeat); .icon(@repeat);
}
.icon-nd-health-warning:before {
.icon(@exclamation-sign);
color : @orange
}
.icon-nd-health-error:before {
.icon(@exclamation-sign);
color : @errorText
} }

View File

@ -22,6 +22,8 @@
li { li {
list-style-type : none; list-style-type : none;
display : inline-block; display : inline-block;
position : relative;
a { a {
&:focus { &:focus {
@ -38,21 +40,20 @@
font-weight : 100; font-weight : 100;
} }
span.label.pull-right { span.label.pull-right {
position : relative; position : absolute;
top : 24px; top : 28px;
right : 14px; right : 18px;
} }
} }
} }
.backdrop #nav-region { .backdrop {
background-color : #000000; #nav-region {
.opacity(0.85); background-color : #000000;
} .opacity(0.85);
}
#nav-region li a:hover, #in-sub-nav li a.active {
background-color : #555555;
text-decoration : none;
} }
#nav-region { #nav-region {
@ -62,6 +63,19 @@
.span12 { .span12 {
margin-left : 0px; margin-left : 0px;
} }
li {
a {
&:hover {
background-color : #555555;
text-decoration : none;
}
.label {
cursor: pointer;
}
}
}
} }
.search { .search {

View File

@ -0,0 +1,16 @@
'use strict';
define(
[
'backbone',
'Health/HealthModel',
'Mixins/backbone.signalr.mixin'
], function (Backbone, HealthModel) {
var Collection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/health',
model: HealthModel
});
var collection = new Collection().bindSignalR();
collection.fetch();
return collection;
});

View File

@ -0,0 +1,9 @@
'use strict';
define(
[
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
});
});

View File

@ -0,0 +1,39 @@
'use strict';
define(
[
'underscore',
'marionette',
'Health/HealthCollection'
], function (_, Marionette, HealthCollection) {
return Marionette.ItemView.extend({
initialize: function () {
this.listenTo(HealthCollection, 'sync', this._healthSync);
HealthCollection.fetch();
},
render: function () {
this.$el.empty();
if (HealthCollection.length === 0) {
return this;
}
var count = HealthCollection.length;
var label = 'label-warning';
var errors = HealthCollection.some(function (model) {
return model.get('type') === 'error';
});
if (errors) {
label = 'label-important';
}
this.$el.html('<span class="label pull-right {0}">{1}</span>'.format(label, count));
return this;
},
_healthSync: function () {
this.render();
}
});
});

View File

@ -47,6 +47,7 @@
<i class="icon-laptop"></i> <i class="icon-laptop"></i>
<br> <br>
System System
<span id="x-health"></span>
</a> </a>
</li> </li>
<li> <li>

View File

@ -3,10 +3,15 @@ define(
[ [
'marionette', 'marionette',
'jquery', 'jquery',
'Health/HealthView',
'Navbar/Search' 'Navbar/Search'
], function (Marionette, $) { ], function (Marionette, $, HealthView) {
return Marionette.ItemView.extend({ return Marionette.Layout.extend({
template: 'Navbar/NavbarTemplate', template: 'Navbar/NavbarLayoutTemplate',
regions: {
health: '#x-health'
},
ui: { ui: {
search: '.x-series-search' search: '.x-series-search'
@ -18,6 +23,7 @@ define(
onRender: function () { onRender: function () {
this.ui.search.bindSearch(); this.ui.search.bindSearch();
this.health.show(new HealthView());
}, },
onClick: function (event) { onClick: function (event) {
@ -30,9 +36,9 @@ define(
var href = event.target.getAttribute('href'); var href = event.target.getAttribute('href');
//if couldn't find it look up' //if couldn't find it look up'
if (!href && target.parent('a') && target.parent('a')[0]) { if (!href && target.closest('a') && target.closest('a')[0]) {
var linkElement = target.parent('a')[0]; var linkElement = target.closest('a')[0];
href = linkElement.getAttribute('href'); href = linkElement.getAttribute('href');
this.setActive(linkElement); this.setActive(linkElement);

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
define([ define([
'vent', 'vent',
'marionette', 'marionette',
'backgrid', 'backgrid',
'System/Info/DiskSpace/DiskSpaceCollection', 'System/Info/DiskSpace/DiskSpaceCollection',
'Shared/LoadingView', 'Shared/LoadingView',
@ -14,6 +14,7 @@ define([
regions: { regions: {
grid: '#x-grid' grid: '#x-grid'
}, },
columns: columns:
[ [
{ {
@ -37,6 +38,7 @@ define([
this.collection = new DiskSpaceCollection(); this.collection = new DiskSpaceCollection();
this.listenTo(this.collection, 'sync', this._showTable); this.listenTo(this.collection, 'sync', this._showTable);
}, },
onRender : function() { onRender : function() {
this.grid.show(new LoadingView()); this.grid.show(new LoadingView());
}, },
@ -44,6 +46,7 @@ define([
onShow: function() { onShow: function() {
this.collection.fetch(); this.collection.fetch();
}, },
_showTable: function() { _showTable: function() {
this.grid.show(new Backgrid.Grid({ this.grid.show(new Backgrid.Grid({
row: Backgrid.Row, row: Backgrid.Row,

View File

@ -0,0 +1,18 @@
'use strict';
define(
[
'Cells/NzbDroneCell'
], function (NzbDroneCell) {
return NzbDroneCell.extend({
className: 'log-level-cell',
render: function () {
var level = this._getValue();
this.$el.html('<i class="icon-nd-health-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level));
return this;
}
});
});

View File

@ -0,0 +1,55 @@
'use strict';
define(
[
'marionette',
'backgrid',
'Health/HealthCollection',
'System/Info/Health/HealthCell',
'System/Info/Health/HealthOkView'
], function (Marionette, Backgrid, HealthCollection, HealthCell, HealthOkView) {
return Marionette.Layout.extend({
template: 'System/Info/Health/HealthLayoutTemplate',
regions: {
grid: '#x-health-grid'
},
columns:
[
{
name: 'type',
label: '',
cell: HealthCell
},
{
name: 'message',
label: 'Message',
cell: 'string'
}
],
initialize: function () {
this.listenTo(HealthCollection, 'sync', this.render);
HealthCollection.fetch();
},
onRender : function() {
if (HealthCollection.length === 0) {
this.grid.show(new HealthOkView());
}
else {
this._showTable();
}
},
_showTable: function() {
this.grid.show(new Backgrid.Grid({
row: Backgrid.Row,
columns: this.columns,
collection: HealthCollection,
className:'table table-hover'
}));
}
});
});

View File

@ -0,0 +1,6 @@
<fieldset class="x-health">
<legend>Health</legend>
<div id="x-health-grid"/>
</fieldset>

View File

@ -0,0 +1,9 @@
'use strict';
define(
[
'marionette'
], function (Marionette) {
return Marionette.ItemView.extend({
template: 'System/Info/Health/HealthOkViewTemplate'
});
});

View File

@ -0,0 +1,2 @@
No issues with your configuration

View File

@ -4,22 +4,26 @@ define(
'backbone', 'backbone',
'marionette', 'marionette',
'System/Info/About/AboutView', 'System/Info/About/AboutView',
'System/Info/DiskSpace/DiskSpaceLayout' 'System/Info/DiskSpace/DiskSpaceLayout',
'System/Info/Health/HealthLayout'
], function (Backbone, ], function (Backbone,
Marionette, Marionette,
AboutView, AboutView,
DiskSpaceLayout) { DiskSpaceLayout,
HealthLayout) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'System/Info/SystemInfoLayoutTemplate', template: 'System/Info/SystemInfoLayoutTemplate',
regions: { regions: {
about : '#about', about : '#about',
diskSpace: '#diskspace' diskSpace: '#diskspace',
health : '#health'
}, },
onRender: function () { onRender: function () {
this.about.show(new AboutView()); this.about.show(new AboutView());
this.diskSpace.show(new DiskSpaceLayout()); this.diskSpace.show(new DiskSpaceLayout());
this.health.show(new HealthLayout());
} }
}); });
}); });

View File

@ -1,4 +1,8 @@
<div class="row"> <div class="row">
<div class="span12" id="health"></div>
</div>
<div class="row">
<div class="span12" id="about"></div> <div class="span12" id="about"></div>
</div> </div>

View File

@ -33,9 +33,9 @@ define(
var href = event.target.getAttribute('href'); var href = event.target.getAttribute('href');
if (!href && $target.parent('a') && $target.parent('a')[0]) { if (!href && $target.closest('a') && $target.closest('a')[0]) {
var linkElement = $target.parent('a')[0]; var linkElement = $target.closest('a')[0];
href = linkElement.getAttribute('href'); href = linkElement.getAttribute('href');
} }