using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.HealthCheck; using Umbraco.Web.Routing; namespace Umbraco.Web.Scheduling { public sealed class SchedulerComponent : IComponent { private const int DefaultDelayMilliseconds = 180000; // 3 mins private const int OneMinuteMilliseconds = 60000; private const int FiveMinuteMilliseconds = 300000; private const int OneHourMilliseconds = 3600000; private readonly IRuntimeState _runtime; private readonly IMainDom _mainDom; private readonly IServerRegistrar _serverRegistrar; private readonly IContentService _contentService; private readonly IAuditService _auditService; private readonly IProfilingLogger _pLogger; private readonly ILogger _logger; 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; private readonly LoggingSettings _loggingSettings; private readonly KeepAliveSettings _keepAliveSettings; private readonly IHostingEnvironment _hostingEnvironment; private BackgroundTaskRunner _keepAliveRunner; private BackgroundTaskRunner _publishingRunner; private BackgroundTaskRunner _scrubberRunner; private BackgroundTaskRunner _fileCleanupRunner; private BackgroundTaskRunner _healthCheckRunner; private bool _started; private object _locker = new object(); private IBackgroundTask[] _tasks; public SchedulerComponent(IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, IAuditService auditService, HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger pLogger, ILoggerFactory loggerFactory, IApplicationShutdownRegistry applicationShutdownRegistry, IOptions healthChecksSettings, IServerMessenger serverMessenger, IRequestAccessor requestAccessor, IOptions loggingSettings, IOptions keepAliveSettings, IHostingEnvironment hostingEnvironment, IBackofficeSecurityFactory backofficeSecurityFactory) { _runtime = runtime; _mainDom = mainDom; _serverRegistrar = serverRegistrar; _contentService = contentService; _auditService = auditService; _scopeProvider = scopeProvider; _pLogger = pLogger; _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; _loggingSettings = loggingSettings.Value; _keepAliveSettings = keepAliveSettings.Value; _hostingEnvironment = hostingEnvironment; } public void Initialize() { var logger = _loggerFactory.CreateLogger>(); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly _keepAliveRunner = new BackgroundTaskRunner("KeepAlive", logger, _applicationShutdownRegistry); _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; } public void Terminate() { // the AppDomain / maindom / whatever takes care of stopping background task runners } private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e) { switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: _requestAccessor.RouteAttempt -= RegisterBackgroundTasksOnce; RegisterBackgroundTasks(); break; } } private void RegisterBackgroundTasks() { LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => { _logger.LogDebug("Initializing the scheduler"); var tasks = new List(); if (_keepAliveSettings.DisableKeepAliveTask == false) { tasks.Add(RegisterKeepAlive(_keepAliveSettings)); } tasks.Add(RegisterScheduledPublishing()); tasks.Add(RegisterLogScrubber(_loggingSettings)); tasks.Add(RegisterTempFileCleanup()); var healthCheckConfig = _healthChecksSettings; if (healthCheckConfig.NotificationSettings.Enabled) tasks.Add(RegisterHealthCheckNotifier(healthCheckConfig, _healthChecks, _notifications, _pLogger)); return tasks.ToArray(); }); } private IBackgroundTask RegisterKeepAlive(KeepAliveSettings keepAliveSettings) { // ping/keepalive // on all servers var task = new KeepAlive(_keepAliveRunner, DefaultDelayMilliseconds, FiveMinuteMilliseconds, _requestAccessor, _mainDom, Options.Create(keepAliveSettings), _loggerFactory.CreateLogger(), _pLogger, _serverRegistrar); _keepAliveRunner.TryAdd(task); return task; } private IBackgroundTask RegisterScheduledPublishing() { // scheduled publishing/unpublishing // install on all, will only run on non-replica servers var task = new ScheduledPublishing(_publishingRunner, DefaultDelayMilliseconds, OneMinuteMilliseconds, _runtime, _mainDom, _serverRegistrar, _contentService, _umbracoContextFactory, _loggerFactory.CreateLogger(), _serverMessenger, _backofficeSecurityFactory); _publishingRunner.TryAdd(task); 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 // install on all, will only run on non-replica servers var task = new LogScrubber(_scrubberRunner, DefaultDelayMilliseconds, LogScrubber.GetLogScrubbingInterval(), _mainDom, _serverRegistrar, _auditService, Options.Create(settings), _scopeProvider, _pLogger, _loggerFactory.CreateLogger()); _scrubberRunner.TryAdd(task); return task; } private IBackgroundTask RegisterTempFileCleanup() { var tempFolderPaths = new[] { _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) }; foreach (var tempFolderPath in tempFolderPaths) { //ensure it exists Directory.CreateDirectory(tempFolderPath); } // temp file cleanup, will run on all servers - even though file upload should only be handled on the master, this will // ensure that in the case it happes on replicas that they are cleaned up. var task = new TempFileCleanup(_fileCleanupRunner, DefaultDelayMilliseconds, OneHourMilliseconds, tempFolderPaths.Select(x=>new DirectoryInfo(x)), TimeSpan.FromDays(1), //files that are over a day old _mainDom, _pLogger, _loggerFactory.CreateLogger()); _scrubberRunner.TryAdd(task); return task; } } }