From bdb8f34da3e400a0dd69fa81e4227e5e0459dbfd Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 30 Oct 2020 13:56:13 +0100 Subject: [PATCH] Netcore: Health check notifier hosted service (#9295) * Implemented health check notifier as a hosted service. Added validation to health check settings. * Registered health check notifier as a hosted service. Modified health check nested settings to use concrete classes to align with other configuration models. * Resolved issues with email sending using development server. * PR review comments and fixed failing unit test. * Changed period and delay millisecond and hourly values to TimeSpans. Changed configuration of first run time for health check notifications to use H:mm format. * Set up SecureSocketOptions as a locally defined enum. * Tightened up time format validation to verify input is an actual time (with hours and minutes only) and not a timespan. * Aligned naming and namespace of health check configuration related classes with other configuration classes. * Created constants for hex colors used in formatting health check results as HTML. * Revert "Tightened up time format validation to verify input is an actual time (with hours and minutes only) and not a timespan." This reverts commit f9bb8a7a825bcb58146879f18b47922e09453e2d. * Renamed method to be clear validation is of a TimeSpan and not a time. Co-authored-by: Bjarke Berg --- .../Models/DisabledHealthCheckSettings.cs} | 6 +- .../HealthCheckSettingsExtensions.cs | 25 +++ .../HealthChecksNotificationMethodSettings.cs | 16 ++ .../HealthChecksNotificationSettings.cs | 19 ++ .../Models/HealthChecksSettings.cs | 19 +- .../Configuration/Models/SmtpSettings.cs | 18 +- .../Validation/ConfigurationValidatorBase.cs | 13 ++ .../Validation/ContentSettingsValidator.cs | 1 - .../HealthChecksSettingsValidator.cs | 22 +++ src/Umbraco.Core/DateTimeExtensions.cs | 2 +- src/Umbraco.Core/HealthCheck/HealthCheck.cs | 4 +- .../HealthCheck/INotificationMethod.cs | 13 -- .../INotificationMethodSettings.cs | 8 - src/Umbraco.Core/StringExtensions.cs | 15 ++ .../HealthCheck/HealthCheckResults.cs | 12 +- .../EmailNotificationMethod.cs | 4 +- .../IHealthCheckNotificationMethod.cs | 3 +- .../NotificationMethodBase.cs | 10 +- .../HealthCheckNotifier.cs | 96 +++++----- .../RecurringHostedServiceBase.cs | 47 +++++ .../Runtime/CoreInitialComposer.cs | 1 - .../Scheduling/SchedulerComponent.cs | 45 +---- .../Users/EmailSender.cs | 27 +-- .../HealthCheckSettingsExtensionsTests.cs | 41 ++++ .../ContentSettingsValidatorTests.cs | 1 - .../HealthChecksSettingsValidatorTests.cs | 42 ++++ .../StringExtensionsTests.cs | 13 +- .../HealthCheckNotifierTests.cs | 174 +++++++++++++++++ .../Extensions/UmbracoBuilderExtensions.cs | 3 +- .../Builder/UmbracoBuilderExtensions.cs | 3 + .../UmbracoCoreServiceCollectionExtensions.cs | 181 ++++++++++-------- 31 files changed, 636 insertions(+), 248 deletions(-) rename src/Umbraco.Core/{HealthCheck/Checks/DisabledHealthCheck.cs => Configuration/Models/DisabledHealthCheckSettings.cs} (64%) create mode 100644 src/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensions.cs create mode 100644 src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs create mode 100644 src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs create mode 100644 src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs delete mode 100644 src/Umbraco.Core/HealthCheck/INotificationMethod.cs delete mode 100644 src/Umbraco.Core/HealthCheck/INotificationMethodSettings.cs rename src/Umbraco.Infrastructure/{Scheduling => HostedServices}/HealthCheckNotifier.cs (67%) create mode 100644 src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensionsTests.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidatorTests.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs diff --git a/src/Umbraco.Core/HealthCheck/Checks/DisabledHealthCheck.cs b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs similarity index 64% rename from src/Umbraco.Core/HealthCheck/Checks/DisabledHealthCheck.cs rename to src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs index 99ff05ed55..1d96a9027f 100644 --- a/src/Umbraco.Core/HealthCheck/Checks/DisabledHealthCheck.cs +++ b/src/Umbraco.Core/Configuration/Models/DisabledHealthCheckSettings.cs @@ -1,11 +1,13 @@ using System; -namespace Umbraco.Core.HealthCheck.Checks +namespace Umbraco.Core.Configuration.Models { - public class DisabledHealthCheck + public class DisabledHealthCheckSettings { public Guid Id { get; set; } + public DateTime DisabledOn { get; set; } + public int DisabledBy { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensions.cs new file mode 100644 index 0000000000..3fa37a0b19 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensions.cs @@ -0,0 +1,25 @@ +using System; + +namespace Umbraco.Core.Configuration.Models.Extensions +{ + public static class HealthCheckSettingsExtensions + { + public static TimeSpan GetNotificationDelay(this HealthChecksSettings settings, DateTime now, TimeSpan defaultDelay) + { + // If first run time not set, start with just small delay after application start. + var firstRunTime = settings.Notification.FirstRunTime; + if (string.IsNullOrEmpty(firstRunTime)) + { + return defaultDelay; + } + else + { + // Otherwise start at scheduled time. + var delay = TimeSpan.FromMinutes(now.PeriodicMinutesFrom(firstRunTime)); + return (delay < defaultDelay) + ? defaultDelay + : delay; + } + } + } +} diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs new file mode 100644 index 0000000000..1cfc4f9168 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Umbraco.Core.HealthCheck; + +namespace Umbraco.Core.Configuration.Models +{ + public class HealthChecksNotificationMethodSettings + { + public bool Enabled { get; set; } = false; + + public HealthCheckNotificationVerbosity Verbosity { get; set; } = HealthCheckNotificationVerbosity.Summary; + + public bool FailureOnly { get; set; } = false; + + public IDictionary Settings { get; set; } = new Dictionary(); + } +} diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs new file mode 100644 index 0000000000..052b5a4997 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationSettings.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Configuration.Models +{ + public class HealthChecksNotificationSettings + { + public bool Enabled { get; set; } = false; + + public string FirstRunTime { get; set; } = string.Empty; + + public TimeSpan Period { get; set; } = TimeSpan.FromHours(24); + + public IDictionary NotificationMethods { get; set; } = new Dictionary(); + + public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); + } +} diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs index 7600b946fd..705611b0c1 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksSettings.cs @@ -1,27 +1,12 @@ using System.Collections.Generic; using System.Linq; -using Umbraco.Core.HealthCheck; -using Umbraco.Core.HealthCheck.Checks; namespace Umbraco.Core.Configuration.Models { public class HealthChecksSettings { - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); + public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); - public HealthCheckNotificationSettings NotificationSettings { get; set; } = new HealthCheckNotificationSettings(); - - public class HealthCheckNotificationSettings - { - public bool Enabled { get; set; } = false; - - public string FirstRunTime { get; set; } - - public int PeriodInHours { get; set; } = 24; - - public IReadOnlyDictionary NotificationMethods { get; set; } = new Dictionary(); - - public IEnumerable DisabledChecks { get; set; } = Enumerable.Empty(); - } + public HealthChecksNotificationSettings Notification { get; set; } = new HealthChecksNotificationSettings(); } } diff --git a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs index 7c19f28d87..c1ba5edea2 100644 --- a/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SmtpSettings.cs @@ -1,10 +1,22 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Net.Mail; using Umbraco.Core.Configuration.Models.Validation; namespace Umbraco.Core.Configuration.Models { + /// + /// Matches MailKit.Security.SecureSocketOptions and defined locally to avoid having to take + /// thi + /// + public enum SecureSocketOptions + { + None = 0, + Auto = 1, + SslOnConnect = 2, + StartTls = 3, + StartTlsWhenAvailable = 4 + } + public class SmtpSettings : ValidatableEntryBase { [Required] @@ -15,6 +27,8 @@ namespace Umbraco.Core.Configuration.Models public int Port { get; set; } + public SecureSocketOptions SecureSocketOptions { get; set; } = SecureSocketOptions.Auto; + public string PickupDirectoryLocation { get; set; } public SmtpDeliveryMethod DeliveryMethod { get; set; } = SmtpDeliveryMethod.Network; diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs index fe8d077166..9b7f02a60c 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ConfigurationValidatorBase.cs @@ -41,5 +41,18 @@ namespace Umbraco.Core.Configuration.Models.Validation message = string.Empty; return true; } + + public bool ValidateOptionalTime(string configPath, string value, out string message) + { + if (!string.IsNullOrEmpty(value) && !value.IsValidTimeSpan()) + { + message = $"Configuration entry {configPath} contains an invalid time value."; + return false; + } + + message = string.Empty; + return true; + } + } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs index 9ed22f922e..f6e839aa39 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidator.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Microsoft.Extensions.Options; -using Umbraco.Core.Macros; namespace Umbraco.Core.Configuration.Models.Validation { diff --git a/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs new file mode 100644 index 0000000000..fe6a4d056b --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidator.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Options; + +namespace Umbraco.Core.Configuration.Models.Validation +{ + public class HealthChecksSettingsValidator : ConfigurationValidatorBase, IValidateOptions + { + public ValidateOptionsResult Validate(string name, HealthChecksSettings options) + { + if (!ValidateNotificationFirstRunTime(options.Notification.FirstRunTime, out var message)) + { + return ValidateOptionsResult.Fail(message); + } + + return ValidateOptionsResult.Success; + } + + private bool ValidateNotificationFirstRunTime(string value, out string message) + { + return ValidateOptionalTime($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message); + } + } +} diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index f665aaa8ed..1920205b69 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core /// /// Calculates the number of minutes from a date time, on a rolling daily basis (so if - /// date time is before the time, calculate onto next day) + /// date time is before the time, calculate onto next day). /// /// Date to start from /// Time to compare against (in Hmm form, e.g. 330, 2200) diff --git a/src/Umbraco.Core/HealthCheck/HealthCheck.cs b/src/Umbraco.Core/HealthCheck/HealthCheck.cs index 89a1f41f4d..9f4e364be6 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheck.cs +++ b/src/Umbraco.Core/HealthCheck/HealthCheck.cs @@ -13,8 +13,8 @@ namespace Umbraco.Core.HealthCheck { protected HealthCheck() { - Type thisType = GetType(); - HealthCheckAttribute meta = thisType.GetCustomAttribute(false); + var thisType = GetType(); + var meta = thisType.GetCustomAttribute(false); if (meta == null) { throw new InvalidOperationException($"The health check {thisType} requires a {typeof(HealthCheckAttribute)}"); diff --git a/src/Umbraco.Core/HealthCheck/INotificationMethod.cs b/src/Umbraco.Core/HealthCheck/INotificationMethod.cs deleted file mode 100644 index 9c4ec70cfe..0000000000 --- a/src/Umbraco.Core/HealthCheck/INotificationMethod.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core.HealthCheck -{ - public interface INotificationMethod - { - string Alias { get; } - bool Enabled { get; } - HealthCheckNotificationVerbosity Verbosity { get; } - bool FailureOnly { get; } - IReadOnlyDictionary Settings { get; } - } -} diff --git a/src/Umbraco.Core/HealthCheck/INotificationMethodSettings.cs b/src/Umbraco.Core/HealthCheck/INotificationMethodSettings.cs deleted file mode 100644 index 01ad667d94..0000000000 --- a/src/Umbraco.Core/HealthCheck/INotificationMethodSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Core.HealthCheck -{ - public interface INotificationMethodSettings - { - string Key { get; } - string Value { get; } - } -} diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 67b0b49a45..93d664a2bd 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -1478,5 +1478,20 @@ namespace Umbraco.Core { return shortStringHelper.CleanStringForSafeFileName(text, culture); } + + /// + /// Validates a string matches a time stamp. + /// + /// String with timespan representation (in standard timespan format: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings) + /// + public static bool IsValidTimeSpan(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return false; + } + + return TimeSpan.TryParse(input, out var _); + } } } diff --git a/src/Umbraco.Infrastructure/HealthCheck/HealthCheckResults.cs b/src/Umbraco.Infrastructure/HealthCheck/HealthCheckResults.cs index 1561645be9..281b0fc445 100644 --- a/src/Umbraco.Infrastructure/HealthCheck/HealthCheckResults.cs +++ b/src/Umbraco.Infrastructure/HealthCheck/HealthCheckResults.cs @@ -128,11 +128,17 @@ namespace Umbraco.Infrastructure.HealthCheck return html; } + internal Dictionary> ResultsAsDictionary => _results; + private string ApplyHtmlHighlighting(string html) { - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, "5cb85c"); - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, "f0ad4e"); - return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, "d9534f"); + const string SuccessHexColor = "5cb85c"; + const string WarningHexColor = "f0ad4e"; + const string ErrorHexColor = "d9534f"; + + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor); + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor); + return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor); } private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) diff --git a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index 37a3b5b072..29135ea1c4 100644 --- a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods IOptions contentSettings) : base(healthChecksSettings) { - var recipientEmail = Settings?["recipientEmail"]?.Value; + var recipientEmail = Settings?["RecipientEmail"]; if (string.IsNullOrWhiteSpace(recipientEmail)) { Enabled = false; @@ -45,7 +45,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public string RecipientEmail { get; } - public override async Task SendAsync(HealthCheckResults results, CancellationToken token) + public override async Task SendAsync(HealthCheckResults results) { if (ShouldSend(results) == false) { diff --git a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs index fdf72251be..77614c41e3 100644 --- a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs +++ b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs @@ -8,6 +8,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public interface IHealthCheckNotificationMethod : IDiscoverable { bool Enabled { get; } - Task SendAsync(HealthCheckResults results, CancellationToken token); + + Task SendAsync(HealthCheckResults results); } } diff --git a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/NotificationMethodBase.cs b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/NotificationMethodBase.cs index 39025df2ab..eeb8452492 100644 --- a/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/NotificationMethodBase.cs +++ b/src/Umbraco.Infrastructure/HealthCheck/NotificationMethods/NotificationMethodBase.cs @@ -1,11 +1,9 @@ using System.Collections.Generic; using System.Reflection; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Core.HealthCheck; -using Umbraco.Core.HealthCheck.Checks; using Umbraco.Infrastructure.HealthCheck; namespace Umbraco.Web.HealthCheck.NotificationMethods @@ -22,8 +20,8 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods return; } - var notificationMethods = healthCheckSettings.Value.NotificationSettings.NotificationMethods; - if(!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod)) + var notificationMethods = healthCheckSettings.Value.Notification.NotificationMethods; + if (!notificationMethods.TryGetValue(attribute.Alias, out var notificationMethod)) { Enabled = false; return; @@ -41,13 +39,13 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public HealthCheckNotificationVerbosity Verbosity { get; protected set; } - public IReadOnlyDictionary Settings { get; } + public IDictionary Settings { get; } protected bool ShouldSend(HealthCheckResults results) { return Enabled && (!FailureOnly || !results.AllChecksSuccessful); } - public abstract Task SendAsync(HealthCheckResults results, CancellationToken token); + public abstract Task SendAsync(HealthCheckResults results); } } diff --git a/src/Umbraco.Infrastructure/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs similarity index 67% rename from src/Umbraco.Infrastructure/Scheduling/HealthCheckNotifier.cs rename to src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index 33eff2c949..af0a0f335c 100644 --- a/src/Umbraco.Infrastructure/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -1,77 +1,86 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Configuration.Models.Extensions; using Umbraco.Core.HealthCheck; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Sync; -using Umbraco.Web.HealthCheck; -using Microsoft.Extensions.Logging; using Umbraco.Infrastructure.HealthCheck; +using Umbraco.Web.HealthCheck; -namespace Umbraco.Web.Scheduling +namespace Umbraco.Infrastructure.HostedServices { - public class HealthCheckNotifier : RecurringTaskBase + /// + /// Hosted service implementation for recurring health check notifications. + /// + public class HealthCheckNotifier : RecurringHostedServiceBase { - private readonly IMainDom _mainDom; + private readonly HealthChecksSettings _healthChecksSettings; private readonly HealthCheckCollection _healthChecks; private readonly HealthCheckNotificationMethodCollection _notifications; - private readonly IScopeProvider _scopeProvider; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly HealthChecksSettings _healthChecksSettings; - private readonly IServerRegistrar _serverRegistrar; private readonly IRuntimeState _runtimeState; + private readonly IServerRegistrar _serverRegistrar; + private readonly IMainDom _mainDom; + private readonly IScopeProvider _scopeProvider; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; public HealthCheckNotifier( - IBackgroundTaskRunner runner, - int delayMilliseconds, - int periodMilliseconds, + IOptions healthChecksSettings, HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IMainDom mainDom, - IProfilingLogger profilingLogger , - ILogger logger, - HealthChecksSettings healthChecksSettings, - IServerRegistrar serverRegistrar, IRuntimeState runtimeState, - IScopeProvider scopeProvider) - : base(runner, delayMilliseconds, periodMilliseconds) + IServerRegistrar serverRegistrar, + IMainDom mainDom, + IScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger) + : base(healthChecksSettings.Value.Notification.Period, + healthChecksSettings.Value.GetNotificationDelay(DateTime.Now, DefaultDelay)) { + _healthChecksSettings = healthChecksSettings.Value; _healthChecks = healthChecks; _notifications = notifications; + _runtimeState = runtimeState; + _serverRegistrar = serverRegistrar; _mainDom = mainDom; _scopeProvider = scopeProvider; - _runtimeState = runtimeState; - _profilingLogger = profilingLogger ; _logger = logger; - _healthChecksSettings = healthChecksSettings; - _serverRegistrar = serverRegistrar; - _runtimeState = runtimeState; + _profilingLogger = profilingLogger; } - public override async Task PerformRunAsync(CancellationToken token) + + public override async void ExecuteAsync(object state) { + if (_healthChecksSettings.Notification.Enabled == false) + { + return; + } + if (_runtimeState.Level != RuntimeLevel.Run) - return true; // repeat... + { + return; + } switch (_serverRegistrar.GetCurrentServerRole()) { case ServerRole.Replica: _logger.LogDebug("Does not run on replica servers."); - return true; // DO repeat, server role can change + return; case ServerRole.Unknown: _logger.LogDebug("Does not run on servers with unknown role."); - return true; // DO repeat, server role can change + return; } - // ensure we do not run if not main domain, but do NOT lock it + // Ensure we do not run if not main domain, but do NOT lock it if (_mainDom.IsMainDom == false) { _logger.LogDebug("Does not run if not MainDom."); - return false; // do NOT repeat, going down + return; } // Ensure we use an explicit scope since we are running on a background thread and plugin health @@ -80,13 +89,10 @@ namespace Umbraco.Web.Scheduling using (var scope = _scopeProvider.CreateScope()) using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) { - var healthCheckConfig = _healthChecksSettings; - - // Don't notify for any checks that are disabled, nor for any disabled - // just for notifications - var disabledCheckIds = healthCheckConfig.NotificationSettings.DisabledChecks + // Don't notify for any checks that are disabled, nor for any disabled just for notifications. + var disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks .Select(x => x.Id) - .Union(healthCheckConfig.DisabledChecks + .Union(_healthChecksSettings.DisabledChecks .Select(x => x.Id)) .Distinct() .ToArray(); @@ -97,14 +103,12 @@ namespace Umbraco.Web.Scheduling var results = new HealthCheckResults(checks); results.LogResults(); - // Send using registered notification methods that are enabled + // Send using registered notification methods that are enabled. foreach (var notificationMethod in _notifications.Where(x => x.Enabled)) - await notificationMethod.SendAsync(results, token); + { + await notificationMethod.SendAsync(results); + } } - - return true; // repeat } - - public override bool IsAsync => true; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs new file mode 100644 index 0000000000..071ada0b62 --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Infrastructure.HostedServices +{ + /// + /// Provides a base class for recurring background tasks implemented as hosted services. + /// + /// + /// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks + /// + public abstract class RecurringHostedServiceBase : IHostedService, IDisposable + { + protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + + private readonly TimeSpan _period; + private readonly TimeSpan _delay; + private Timer _timer; + + protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) + { + _period = period; + _delay = delay; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); + return Task.CompletedTask; + } + + public abstract void ExecuteAsync(object state); + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 8ce59b49e7..01bfb79e26 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -303,7 +303,6 @@ namespace Umbraco.Core.Runtime composition.HealthChecks() .Add(() => composition.TypeLoader.GetTypes()); - composition.WithCollectionBuilder() .Add(() => composition.TypeLoader.GetTypes()); diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs index 6e4b4bb629..49999c9c56 100644 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs @@ -36,10 +36,7 @@ namespace Umbraco.Web.Scheduling private readonly ILoggerFactory _loggerFactory; private readonly IApplicationShutdownRegistry _applicationShutdownRegistry; private readonly IScopeProvider _scopeProvider; - private readonly HealthCheckCollection _healthChecks; - private readonly HealthCheckNotificationMethodCollection _notifications; private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly HealthChecksSettings _healthChecksSettings; private readonly IServerMessenger _serverMessenger; private readonly IRequestAccessor _requestAccessor; private readonly IBackofficeSecurityFactory _backofficeSecurityFactory; @@ -59,9 +56,8 @@ namespace Umbraco.Web.Scheduling public SchedulerComponent(IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, IAuditService auditService, - HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger profilingLogger , ILoggerFactory loggerFactory, - IApplicationShutdownRegistry applicationShutdownRegistry, IOptions healthChecksSettings, + IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger profilingLogger, ILoggerFactory loggerFactory, + IApplicationShutdownRegistry applicationShutdownRegistry, IServerMessenger serverMessenger, IRequestAccessor requestAccessor, IOptions loggingSettings, IOptions keepAliveSettings, IHostingEnvironment hostingEnvironment, @@ -73,15 +69,11 @@ namespace Umbraco.Web.Scheduling _contentService = contentService; _auditService = auditService; _scopeProvider = scopeProvider; - _profilingLogger = profilingLogger ; + _profilingLogger = profilingLogger; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); _applicationShutdownRegistry = applicationShutdownRegistry; _umbracoContextFactory = umbracoContextFactory; - - _healthChecks = healthChecks; - _notifications = notifications; - _healthChecksSettings = healthChecksSettings.Value ?? throw new ArgumentNullException(nameof(healthChecksSettings)); _serverMessenger = serverMessenger; _requestAccessor = requestAccessor; _backofficeSecurityFactory = backofficeSecurityFactory; @@ -98,7 +90,6 @@ namespace Umbraco.Web.Scheduling _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", logger, _applicationShutdownRegistry); _scrubberRunner = new BackgroundTaskRunner("LogScrubber", logger, _applicationShutdownRegistry); _fileCleanupRunner = new BackgroundTaskRunner("TempFileCleanup", logger, _applicationShutdownRegistry); - _healthCheckRunner = new BackgroundTaskRunner("HealthCheckNotifier", logger, _applicationShutdownRegistry); // we will start the whole process when a successful request is made _requestAccessor.RouteAttempt += RegisterBackgroundTasksOnce; @@ -138,10 +129,6 @@ namespace Umbraco.Web.Scheduling tasks.Add(RegisterLogScrubber(_loggingSettings)); tasks.Add(RegisterTempFileCleanup()); - var healthCheckConfig = _healthChecksSettings; - if (healthCheckConfig.NotificationSettings.Enabled) - tasks.Add(RegisterHealthCheckNotifier(healthCheckConfig, _healthChecks, _notifications, _profilingLogger)); - return tasks.ToArray(); }); } @@ -164,32 +151,6 @@ namespace Umbraco.Web.Scheduling return task; } - private IBackgroundTask RegisterHealthCheckNotifier(HealthChecksSettings healthCheckSettingsConfig, - HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IProfilingLogger logger) - { - // If first run time not set, start with just small delay after application start - int delayInMilliseconds; - if (string.IsNullOrEmpty(healthCheckSettingsConfig.NotificationSettings.FirstRunTime)) - { - delayInMilliseconds = DefaultDelayMilliseconds; - } - else - { - // Otherwise start at scheduled time - delayInMilliseconds = DateTime.Now.PeriodicMinutesFrom(healthCheckSettingsConfig.NotificationSettings.FirstRunTime) * 60 * 1000; - if (delayInMilliseconds < DefaultDelayMilliseconds) - { - delayInMilliseconds = DefaultDelayMilliseconds; - } - } - - var periodInMilliseconds = healthCheckSettingsConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000; - var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _mainDom, logger, _loggerFactory.CreateLogger(), _healthChecksSettings, _serverRegistrar, _runtime, _scopeProvider); - _healthCheckRunner.TryAdd(task); - return task; - } - private IBackgroundTask RegisterLogScrubber(LoggingSettings settings) { // log scrubbing diff --git a/src/Umbraco.Infrastructure/Users/EmailSender.cs b/src/Umbraco.Infrastructure/Users/EmailSender.cs index a58426d82f..6218ae66c7 100644 --- a/src/Umbraco.Infrastructure/Users/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Users/EmailSender.cs @@ -1,6 +1,7 @@ using System; using System.Net.Mail; using System.Threading.Tasks; +using MailKit.Security; using Microsoft.Extensions.Options; using MimeKit; using MimeKit.Text; @@ -45,15 +46,15 @@ namespace Umbraco.Core { OnSendEmail(new SendEmailEventArgs(message)); } - else + else if (_smtpConfigured.Value == true) { using (var client = new SmtpClient()) { + client.Connect(_globalSettings.Smtp.Host, + _globalSettings.Smtp.Port, + (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - client.Connect(_globalSettings.Smtp.Host, _globalSettings.Smtp.Port); - - if (!(_globalSettings.Smtp.Username is null && - _globalSettings.Smtp.Password is null)) + if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) { client.Authenticate(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } @@ -75,14 +76,15 @@ namespace Umbraco.Core { OnSendEmail(new SendEmailEventArgs(message)); } - else + else if (_smtpConfigured.Value == true) { using (var client = new SmtpClient()) { - await client.ConnectAsync(_globalSettings.Smtp.Host, _globalSettings.Smtp.Port); + await client.ConnectAsync(_globalSettings.Smtp.Host, + _globalSettings.Smtp.Port, + (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - if (!(_globalSettings.Smtp.Username is null && - _globalSettings.Smtp.Password is null)) + if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) { await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } @@ -125,8 +127,7 @@ namespace Umbraco.Core private static void OnSendEmail(SendEmailEventArgs e) { - var handler = SendEmail; - if (handler != null) handler(null, e); + SendEmail?.Invoke(null, e); } private MimeMessage ConstructEmailMessage(EmailMessage mailMessage) @@ -138,10 +139,10 @@ namespace Umbraco.Core var messageToSend = new MimeMessage { Subject = mailMessage.Subject, - From = { new MailboxAddress(fromEmail)}, + From = { MailboxAddress.Parse(fromEmail) }, Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body } }; - messageToSend.To.Add(new MailboxAddress(mailMessage.To)); + messageToSend.To.Add(MailboxAddress.Parse(mailMessage.To)); return messageToSend; } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensionsTests.cs new file mode 100644 index 0000000000..3ec9af0b5a --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Extensions/HealthCheckSettingsExtensionsTests.cs @@ -0,0 +1,41 @@ +using System; +using NUnit.Framework; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Configuration.Models.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Extensions +{ + [TestFixture] + public class HealthCheckSettingsExtensionsTests + { + [Test] + public void Returns_Notification_Delay_From_Provided_Time() + { + var settings = new HealthChecksSettings + { + Notification = new HealthChecksNotificationSettings + { + FirstRunTime = "1230", + } + }; + var now = DateTime.Now.Date.AddHours(12); + var result = settings.GetNotificationDelay(now, TimeSpan.Zero); + Assert.AreEqual(30, result.Minutes); + } + + [Test] + public void Returns_Notification_Delay_From_Default_When_Provided_Time_Too_Close_To_Current_Time() + { + var settings = new HealthChecksSettings + { + Notification = new HealthChecksNotificationSettings + { + FirstRunTime = "1230", + } + }; + var now = DateTime.Now.Date.AddHours(12).AddMinutes(25); + var result = settings.GetNotificationDelay(now, TimeSpan.FromMinutes(10)); + Assert.AreEqual(10, result.Minutes); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidatorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidatorTests.cs index c90e4e4c7d..31f6f5e31c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidatorTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/ContentSettingsValidatorTests.cs @@ -1,7 +1,6 @@ using NUnit.Framework; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Configuration.Models.Validation; -using Umbraco.Tests.Common.Builders; namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidatorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidatorTests.cs new file mode 100644 index 0000000000..e6ca06a34b --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/HealthChecksSettingsValidatorTests.cs @@ -0,0 +1,42 @@ +using System; +using NUnit.Framework; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Configuration.Models.Validation; + +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation +{ + [TestFixture] + public class HealthChecksSettingsValidationTests + { + [Test] + public void Returns_Success_ForValid_Configuration() + { + var validator = new HealthChecksSettingsValidator(); + var options = BuildHealthChecksSettings(); + var result = validator.Validate("settings", options); + Assert.True(result.Succeeded); + } + + [Test] + public void Returns_Fail_For_Configuration_With_Invalid_Notification_FirstRunTime() + { + var validator = new HealthChecksSettingsValidator(); + var options = BuildHealthChecksSettings(firstRunTime: "25:00"); + var result = validator.Validate("settings", options); + Assert.False(result.Succeeded); + } + + private static HealthChecksSettings BuildHealthChecksSettings(string firstRunTime = "12:00") + { + return new HealthChecksSettings + { + Notification = new HealthChecksNotificationSettings + { + Enabled = true, + FirstRunTime = firstRunTime, + Period = TimeSpan.FromHours(1), + } + }; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs index b3244f9884..1644b9049e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/StringExtensionsTests.cs @@ -293,14 +293,23 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper Assert.AreEqual(expected, output); } - #region Cases [TestCase("val$id!ate|this|str'ing", "$!'", '-', "val-id-ate|this|str-ing")] [TestCase("val$id!ate|this|str'ing", "$!'", '*', "val*id*ate|this|str*ing")] - #endregion public void ReplaceManyByOneChar(string input, string toReplace, char replacement, string expected) { var output = input.ReplaceMany(toReplace.ToArray(), replacement); Assert.AreEqual(expected, output); } + + [TestCase("", false)] + [TestCase("12:34", true)] + [TestCase("1:14:23", true)] + [TestCase("25:03", false)] + [TestCase("18:61", false)] + public void IsValidTimeSpan(string input, bool expected) + { + var result = input.IsValidTimeSpan(); + Assert.AreEqual(expected, result); + } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs new file mode 100644 index 0000000000..6506c227fc --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.HealthCheck; +using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; +using Umbraco.Core.Sync; +using Umbraco.Infrastructure.HealthCheck; +using Umbraco.Infrastructure.HostedServices; +using Umbraco.Web.HealthCheck; +using Umbraco.Web.HealthCheck.NotificationMethods; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices +{ + [TestFixture] + public class HealthCheckNotifierTests + { + private Mock _mockNotificationMethod; + + private const string Check1Id = "00000000-0000-0000-0000-000000000001"; + private const string Check2Id = "00000000-0000-0000-0000-000000000002"; + private const string Check3Id = "00000000-0000-0000-0000-000000000003"; + + [Test] + public void Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateHealthCheckNotifier(enabled: false); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Does_Not_Execute_When_Runtime_State_Is_Not_Run() + { + var sut = CreateHealthCheckNotifier(runtimeLevel: RuntimeLevel.Boot); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Does_Not_Execute_When_Server_Role_Is_Replica() + { + var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Replica); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Does_Not_Execute_When_Server_Role_Is_Unknown() + { + var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Unknown); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Does_Not_Execute_When_Not_Main_Dom() + { + var sut = CreateHealthCheckNotifier(isMainDom: false); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Does_Not_Execute_With_No_Enabled_Notification_Methods() + { + var sut = CreateHealthCheckNotifier(notificationEnabled: false); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + } + + [Test] + public void Executes_With_Enabled_Notification_Methods() + { + var sut = CreateHealthCheckNotifier(); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Once); + } + + [Test] + public void Executes_Only_Enabled_Checks() + { + var sut = CreateHealthCheckNotifier(); + sut.ExecuteAsync(null); + _mockNotificationMethod.Verify(x => x.SendAsync(It.Is( + y => y.ResultsAsDictionary.Count == 1 && y.ResultsAsDictionary.ContainsKey("Check1"))), Times.Once); + } + + private HealthCheckNotifier CreateHealthCheckNotifier( + bool enabled = true, + RuntimeLevel runtimeLevel = RuntimeLevel.Run, + ServerRole serverRole = ServerRole.Single, + bool isMainDom = true, + bool notificationEnabled = true) + { + var settings = new HealthChecksSettings + { + Notification = new HealthChecksNotificationSettings + { + Enabled = enabled, + DisabledChecks = new List + { + new DisabledHealthCheckSettings { Id = Guid.Parse(Check3Id) } + } + }, + DisabledChecks = new List + { + new DisabledHealthCheckSettings { Id = Guid.Parse(Check2Id) } + } + }; + var checks = new HealthCheckCollection(new List + { + new TestHealthCheck1(), + new TestHealthCheck2(), + new TestHealthCheck3(), + }); + + _mockNotificationMethod = new Mock(); + _mockNotificationMethod.SetupGet(x => x.Enabled).Returns(notificationEnabled); + var notifications = new HealthCheckNotificationMethodCollection(new List { _mockNotificationMethod.Object }); + + var mockRunTimeState = new Mock(); + mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel); + + var mockServerRegistrar = new Mock(); + mockServerRegistrar.Setup(x => x.GetCurrentServerRole()).Returns(serverRole); + + var mockMainDom = new Mock(); + mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom); + + var mockScopeProvider = new Mock(); + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + return new HealthCheckNotifier(Options.Create(settings), checks, notifications, + mockRunTimeState.Object, mockServerRegistrar.Object, mockMainDom.Object, mockScopeProvider.Object, + mockLogger.Object, mockProfilingLogger.Object); + } + + [HealthCheck(Check1Id, "Check1")] + private class TestHealthCheck1 : TestHealthCheck + { + } + + [HealthCheck(Check2Id, "Check2")] + private class TestHealthCheck2 : TestHealthCheck + { + } + + [HealthCheck(Check3Id, "Check3")] + private class TestHealthCheck3 : TestHealthCheck + { + } + + private class TestHealthCheck : HealthCheck + { + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + return new HealthCheckStatus("Check message"); + } + + public override IEnumerable GetStatus() + { + return Enumerable.Empty(); + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 6a6a896bae..649b8b76b7 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -16,7 +16,8 @@ namespace Umbraco.Extensions .WithMiniProfiler() .WithMvcAndRazor() .WithWebServer() - .WithPreview(); + .WithPreview() + .WithHostedServices(); } public static IUmbracoBuilder WithBackOffice(this IUmbracoBuilder builder) diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 1d13c4c712..2ac4130fe4 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -33,6 +33,9 @@ namespace Umbraco.Web.Common.Builder public static IUmbracoBuilder WithCore(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithCore), () => builder.Services.AddUmbracoCore(builder.WebHostEnvironment, builder.Config)); + public static IUmbracoBuilder WithHostedServices(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithHostedServices), () => builder.Services.AddUmbracoHostedServices()); + public static IUmbracoBuilder WithMiniProfiler(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithMiniProfiler), () => builder.Services.AddMiniProfiler(options => diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 1a157a74be..704a7ce066 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -12,8 +12,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; using Serilog.Extensions.Hosting; -using Serilog.Extensions.Logging; -using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; @@ -26,97 +24,17 @@ using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Runtime; +using Umbraco.Infrastructure.HostedServices; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Profiler; using ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings; using CoreDebugSettings = Umbraco.Core.Configuration.Models.CoreDebugSettings; using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; -using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Umbraco.Extensions { public static class UmbracoCoreServiceCollectionExtensions { - /// - /// Adds SqlCe support for Umbraco - /// - /// - /// - public static IServiceCollection AddUmbracoSqlCeSupport(this IServiceCollection services) - { - try - { - var binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (binFolder != null) - { - var dllPath = Path.Combine(binFolder, "Umbraco.Persistance.SqlCe.dll"); - var umbSqlCeAssembly = Assembly.LoadFrom(dllPath); - - var sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeSyntaxProvider"); - var sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeBulkSqlInsertProvider"); - var sqlCeEmbeddedDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeEmbeddedDatabaseCreator"); - - if (!(sqlCeSyntaxProviderType is null || sqlCeBulkSqlInsertProviderType is null || sqlCeEmbeddedDatabaseCreatorType is null)) - { - services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); - services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); - services.AddSingleton(typeof(IEmbeddedDatabaseCreator), sqlCeEmbeddedDatabaseCreatorType); - } - - var sqlCeAssembly = Assembly.LoadFrom(Path.Combine(binFolder, "System.Data.SqlServerCe.dll")); - - var sqlCe = sqlCeAssembly.GetType("System.Data.SqlServerCe.SqlCeProviderFactory"); - if (!(sqlCe is null)) - { - DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlCe, sqlCe); - } - } - } - catch - { - // Ignore if SqlCE is not available - } - - return services; - } - - /// - /// Adds Sql Server support for Umbraco - /// - /// - /// - public static IServiceCollection AddUmbracoSqlServerSupport(this IServiceCollection services) - { - DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - return services; - } - - /// - /// Adds the Umbraco Back Core requirements - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddUmbracoCore(this IServiceCollection services, - IWebHostEnvironment webHostEnvironment, - Assembly entryAssembly, - AppCaches appCaches, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - => services.AddUmbracoCore(webHostEnvironment, entryAssembly, appCaches, loggingConfiguration, configuration, GetCoreRuntime); - /// /// Adds the Umbraco Configuration requirements /// @@ -127,10 +45,13 @@ namespace Umbraco.Extensions { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + // Register configuration validators. services.AddSingleton, ContentSettingsValidator>(); services.AddSingleton, GlobalSettingsValidator>(); + services.AddSingleton, HealthChecksSettingsValidator >(); services.AddSingleton, RequestHandlerSettingsValidator>(); + // Register configuration sections. services.Configure(configuration.GetSection(Constants.Configuration.ConfigActiveDirectory)); services.Configure(configuration.GetSection("ConnectionStrings"), o => o.BindNonPublicProperties = true); services.Configure(configuration.GetSection(Constants.Configuration.ConfigContent)); @@ -157,6 +78,27 @@ namespace Umbraco.Extensions return services; } + /// + /// Adds the Umbraco Back Core requirements + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddUmbracoCore(this IServiceCollection services, + IWebHostEnvironment webHostEnvironment, + Assembly entryAssembly, + AppCaches appCaches, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) + => services.AddUmbracoCore(webHostEnvironment, entryAssembly, appCaches, loggingConfiguration, configuration, GetCoreRuntime); + /// /// Adds the Umbraco Back Core requirements /// @@ -284,6 +226,77 @@ namespace Umbraco.Extensions return services; } + /// + /// Adds SqlCe support for Umbraco + /// + /// + /// + private static IServiceCollection AddUmbracoSqlCeSupport(this IServiceCollection services) + { + try + { + var binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (binFolder != null) + { + var dllPath = Path.Combine(binFolder, "Umbraco.Persistance.SqlCe.dll"); + var umbSqlCeAssembly = Assembly.LoadFrom(dllPath); + + var sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeSyntaxProvider"); + var sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeBulkSqlInsertProvider"); + var sqlCeEmbeddedDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Persistance.SqlCe.SqlCeEmbeddedDatabaseCreator"); + + if (!(sqlCeSyntaxProviderType is null || sqlCeBulkSqlInsertProviderType is null || sqlCeEmbeddedDatabaseCreatorType is null)) + { + services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); + services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); + services.AddSingleton(typeof(IEmbeddedDatabaseCreator), sqlCeEmbeddedDatabaseCreatorType); + } + + var sqlCeAssembly = Assembly.LoadFrom(Path.Combine(binFolder, "System.Data.SqlServerCe.dll")); + + var sqlCe = sqlCeAssembly.GetType("System.Data.SqlServerCe.SqlCeProviderFactory"); + if (!(sqlCe is null)) + { + DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlCe, sqlCe); + } + } + } + catch + { + // Ignore if SqlCE is not available + } + + return services; + } + + /// + /// Adds Sql Server support for Umbraco + /// + /// + /// + public static IServiceCollection AddUmbracoSqlServerSupport(this IServiceCollection services) + { + DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + /// + /// Adds hosted services for Umbraco. + /// + /// + /// + public static IServiceCollection AddUmbracoHostedServices(this IServiceCollection services) + { + services.AddHostedService(); + + return services; + } + private static ITypeFinder CreateTypeFinder(ILoggerFactory loggerFactory, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly, IOptionsMonitor typeFinderSettings) { var runtimeHashPaths = new RuntimeHashPaths(); @@ -355,7 +368,7 @@ namespace Umbraco.Extensions /// private static void AddLogger( IServiceCollection services, - Core.Hosting.IHostingEnvironment hostingEnvironment, + IHostingEnvironment hostingEnvironment, ILoggingConfiguration loggingConfiguration, IConfiguration configuration) {