using Hangfire; using Hangfire.MemoryStorage; using Hangfire.Storage; using Microsoft.Extensions.DependencyInjection; using Sitecore; using Sitecore.Abstractions; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.DependencyInjection; using Sitecore.Diagnostics; using Sitecore.Jobs; using Sitecore.Owin.Pipelines.Initialize; using Sitecore.Tasks; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Scms.Foundation.Scheduling.Pipelines { public class PrecisionScheduler : InitializeProcessor { private const string SCHEDULE_DATABASE = "master"; public string RefreshSchedule { get; set; } = "*/2 * * * *"; public int StartupDelaySeconds { get; set; } = 15; private static void LogMessage(string message) { Log.Info($"[PrecisionScheduler] {message}", nameof(PrecisionScheduler)); } public override void Process(InitializeArgs args) { var app = args.App; app.UseHangfireAspNet(() => { GlobalConfiguration.Configuration.UseMemoryStorage(); return new[] { new BackgroundJobServer() }; }); LogMessage("Starting up precision scheduler."); BackgroundJob.Schedule(() => Initialize(RefreshSchedule), TimeSpan.FromSeconds(StartupDelaySeconds)); } private static string GenerateMultiDayCronExpression(TimeSpan runTime, List<DayOfWeek> daysToRun) { var castedDaysToRun = daysToRun.Cast<int>().ToList(); return $"{ParseCronTimeSpan(runTime)} * * {ParseMultiDaysList(castedDaysToRun)}"; } private static string ParseCronTimeSpan(TimeSpan timeSpan) { if (timeSpan.Days > 0) { //At HH:mm every day. return $"{timeSpan.Minutes} {timeSpan.Hours}"; } else if (timeSpan.Hours > 0) { //At m minutes past the hour, every h hours. return $"{timeSpan.Minutes} */{timeSpan.Hours}"; } else if (timeSpan.Minutes > 0) { //Every m minutes. return $"*/{timeSpan.Minutes} *"; } return $"*/30 *"; } private static string ParseMultiDaysList(List<int> daysToRun) { if (daysToRun.Any() && daysToRun.Count == 7) return "*"; return string.Join(",", daysToRun); } private static List<DayOfWeek> ParseDays(int days) { var daysOfWeek = new List<DayOfWeek>(); if (days <= 0) return daysOfWeek; if (MainUtil.IsBitSet((int)DaysOfWeek.Sunday, days)) { daysOfWeek.Add(DayOfWeek.Sunday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Monday, days)) { daysOfWeek.Add(DayOfWeek.Monday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Tuesday, days)) { daysOfWeek.Add(DayOfWeek.Tuesday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Wednesday, days)) { daysOfWeek.Add(DayOfWeek.Wednesday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Thursday, days)) { daysOfWeek.Add(DayOfWeek.Thursday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Friday, days)) { daysOfWeek.Add(DayOfWeek.Friday); } if (MainUtil.IsBitSet((int)DaysOfWeek.Saturday, days)) { daysOfWeek.Add(DayOfWeek.Saturday); } return daysOfWeek; } public static void RunSchedule(ID itemId) { var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase("master", true); var item = database.GetItem(itemId); if (item == null) { LogMessage($"Removing background job for {itemId}."); RecurringJob.RemoveIfExists(itemId.ToString()); return; } var jobName = $"{nameof(PrecisionScheduler)}-{itemId}"; var runningJob = JobManager.GetJob(jobName); if(runningJob != null && runningJob.Status.State == JobState.Running) { LogMessage($"Background job for {itemId} is already running."); return; } LogMessage($"Running background job for {itemId}."); var scheduleItem = new ScheduleItem(item); var jobOptions = new DefaultJobOptions(jobName, "scheduling", "scheduler", Activator.CreateInstance(typeof(JobRunner)), "Run", new object[] { ID.Parse(itemId) }); JobManager.Start(jobOptions); } public static void Initialize(string refreshSchedule) { ManageJobs(true); RecurringJob.AddOrUpdate(nameof(ManageJobs), () => ManageJobs(false), refreshSchedule); } public static void ManageJobs(bool isStartup) { var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase(SCHEDULE_DATABASE, true); var descendants = database.SelectItems($"/sitecore/system/tasks/schedules//*[@@templateid='{TemplateIDs.Schedule}']"); var schedules = new Dictionary<string, string>(); foreach (var item in descendants) { if (item.TemplateID != TemplateIDs.Schedule) continue; var itemId = item.ID.ToString(); var schedule = GetSchedule(item); if (string.IsNullOrEmpty(schedule)) continue; schedules.Add(itemId, schedule); } var jobs = JobStorage.Current.GetConnection().GetRecurringJobs(); var existingJobs = new List<string>(); foreach (var job in jobs) { if (!ID.IsID(job.Id)) continue; var itemId = job.Id; if (!schedules.ContainsKey(itemId)) { LogMessage($"Removing {itemId} from recurring schedule."); RecurringJob.RemoveIfExists(itemId); continue; } var item = database.GetItem(itemId); var schedule = GetSchedule(item); if (string.IsNullOrEmpty(schedule)) { LogMessage($"Removing {itemId} from recurring schedule with invalid expression."); RecurringJob.RemoveIfExists(itemId); continue; } if (!string.Equals(job.Cron, schedule, StringComparison.InvariantCultureIgnoreCase)) { LogMessage($"Updating {itemId} with a new schedule '{schedule}'."); RecurringJob.AddOrUpdate($"{itemId}", () => RunSchedule(ID.Parse(itemId)), schedule, TimeZoneInfo.Local); } existingJobs.Add(itemId); } var missingJobs = schedules.Keys.Except(existingJobs); foreach (var missingJob in missingJobs) { var itemId = missingJob; var item = database.GetItem(itemId); var schedule = GetSchedule(item); LogMessage($"Registering recurring job for {itemId} with schedule '{schedule}'."); RecurringJob.AddOrUpdate($"{itemId}", () => RunSchedule(ID.Parse(itemId)), schedule, TimeZoneInfo.Local); } if (isStartup) { var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs(); if (recurringJobs == null) return; foreach (var recurringJob in recurringJobs) { if (!ID.IsID(recurringJob.Id)) continue; var itemId = recurringJob.Id; if (!schedules.ContainsKey(itemId)) continue; var item = database.GetItem(itemId); var scheduleItem = new ScheduleItem(item); var missedLastRun = (recurringJob.NextExecution - scheduleItem.LastRun) > TimeSpan.FromHours(24); if (missedLastRun) { LogMessage($"Running missed job {itemId}."); var jobName = $"{nameof(PrecisionScheduler)}-{itemId}"; var jobOptions = new DefaultJobOptions(jobName, "scheduling", "scheduler", Activator.CreateInstance(typeof(JobRunner)), "Run", new object[] { ID.Parse(itemId) }); JobManager.Start(jobOptions); } } } } private static string GetSchedule(Item item) { var schedule = item.Fields[ScheduleFieldIDs.Schedule].Value; if (string.IsNullOrEmpty(schedule)) return string.Empty; if (Regex.IsMatch(schedule, @"^(((\d+,)+\d+|(\d+|\*(\/|-)\d+)|\d+|\*)\s?){5,7}$", RegexOptions.Compiled)) { return schedule; } var recurrence = new Recurrence(schedule); if (recurrence.Days == DaysOfWeek.None || recurrence.Interval == TimeSpan.Zero || recurrence.InRange(DateTime.UtcNow) != true) return string.Empty; return GenerateMultiDayCronExpression(recurrence.Interval, ParseDays((int)recurrence.Days).ToList()); } } public class JobRunner { public void Run(ID itemId) { var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase("master", true); var item = database.GetItem(itemId); if (item == null) return; var scheduleItem = new ScheduleItem(item); scheduleItem.Execute(); } } }