Last active
July 22, 2020 14:01
-
-
Save VesselinVassilev/89b04f4ce7cef2610590643bafe089ca to your computer and use it in GitHub Desktop.
Custom Recurring Scheduled Tasks in Sitefinity with Cron Jobs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NCrontab; | |
using System; | |
using Telerik.Sitefinity.Configuration; | |
using Telerik.Sitefinity.Services; | |
namespace SitefinityWebApp.Custom.Helpers | |
{ | |
/// <summary> | |
/// Copied from the internal sitefinity classes | |
/// </summary> | |
public static class CrontabHelper | |
{ | |
public static DateTime? GetNextOccurence(string scheduleSpec, string scheduleSpecType = null, DateTime? baseTime = null) | |
{ | |
string str; | |
string str1; | |
ParseScheduleSpec(scheduleSpec, out str, out str1, scheduleSpecType); | |
DateTime? nullable = baseTime; | |
return GetNextOccurrence(str1, (nullable.HasValue ? nullable.GetValueOrDefault() : DateTime.UtcNow)); | |
} | |
internal static void ParseScheduleSpec(string spec, out string type, out string expr, string defaultType = null) | |
{ | |
defaultType = defaultType ?? "crontab"; | |
int num = spec.IndexOf(':'); | |
if (num == -1) | |
{ | |
type = defaultType; | |
expr = spec; | |
return; | |
} | |
type = spec.Substring(0, num); | |
expr = spec.Substring(num + 1); | |
} | |
public static DateTime? GetNextOccurrence(string scheduleSpec, DateTime baseTime) | |
{ | |
int? nullable; | |
ParseYearField(ref scheduleSpec, out nullable); | |
ValueOrError<CrontabSchedule> valueOrError = CrontabSchedule.TryParse(scheduleSpec); | |
if (valueOrError.IsError) | |
{ | |
throw valueOrError.Error; | |
} | |
DateTime maxValue = DateTime.MaxValue; | |
if (nullable.HasValue) | |
{ | |
if (nullable.Value != baseTime.Year) | |
{ | |
baseTime = new DateTime(nullable.Value, 1, 1, 0, 0, 0, baseTime.Kind); | |
} | |
maxValue = new DateTime(nullable.Value, 12, 31, 23, 59, 0, baseTime.Kind); | |
} | |
DateTime nextOccurrence = valueOrError.Value.GetNextOccurrence(baseTime, maxValue); | |
if (nextOccurrence != maxValue) | |
{ | |
return new DateTime?(nextOccurrence); | |
} | |
return null; | |
} | |
private static void ParseYearField(ref string spec, out int? year) | |
{ | |
int num; | |
year = null; | |
string[] strArrays = spec.Split(new char[0]); | |
if ((int)strArrays.Length > 6) | |
{ | |
throw new FormatException(string.Concat("Too many fields in crontab expression: ", spec)); | |
} | |
if ((int)strArrays.Length > 5) | |
{ | |
string str = strArrays[5]; | |
if (str != "*") | |
{ | |
if (!int.TryParse(str, out num)) | |
{ | |
throw new FormatException(string.Concat("Invalid year field in crontab expression: ", str)); | |
} | |
year = new int?(num); | |
} | |
spec = string.Join(" ", strArrays, 0, 5); | |
} | |
} | |
public static DateTime GetExecuteTime(string crontabExpr, string scheduleSpecType) | |
{ | |
// copied from the internal Sitefinity classes | |
TimeZoneInfo currentTimeZone = Config.Get<SystemConfig>().UITimeZoneSettings.CurrentTimeZoneInfo ?? TimeZoneInfo.Local; | |
var dateTimeInCurrentZone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, currentTimeZone); | |
DateTime? nextOccurence = CrontabHelper.GetNextOccurence(crontabExpr, scheduleSpecType, new DateTime?(dateTimeInCurrentZone)); | |
if (nextOccurence.HasValue) | |
{ | |
nextOccurence = new DateTime?(TimeZoneInfo.ConvertTimeToUtc(nextOccurence.Value, currentTimeZone)); | |
return nextOccurence.Value; | |
} | |
return DateTime.UtcNow; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using SitefinityWebApp.Custom.Helpers; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Telerik.Sitefinity.Abstractions; | |
using Telerik.Sitefinity.Scheduling; | |
using Telerik.Sitefinity.Scheduling.Model; | |
namespace SitefinityWebApp.Custom.ScheduledTasks | |
{ | |
public abstract class CustomScheduledTaskBase : ScheduledTask | |
{ | |
/// <summary> | |
/// Each implementation should define its own Crontab expression for when to execute. | |
/// Usually this is a custom config section value for easy maintenance | |
/// </summary> | |
public abstract string CrontabExpression { get; } | |
private static readonly string scheduleSpecType = "crontab"; | |
/// <summary> | |
/// The actual code that will execute when the task is running | |
/// </summary> | |
protected abstract void ExecuteTheTask(); | |
public override void ExecuteTask() | |
{ | |
try | |
{ | |
ExecuteTheTask(); | |
} | |
catch (Exception ex) | |
{ | |
Log.Write(string.Format("Error during execution of {0}: {1}", this.TaskName, ex.ToString())); | |
// send an email to admin, etc. | |
// ... | |
throw ex; | |
} | |
finally | |
{ | |
// in case the crontab expression was changed in the config | |
this.ScheduleSpec = CrontabExpression; | |
SchedulingManager.GetManager().SaveChanges(); | |
} | |
} | |
public void ScheduleCrontabTask() | |
{ | |
using (var manager = SchedulingManager.GetManager()) | |
{ | |
var task = (CustomScheduledTaskBase)Activator.CreateInstance(this.GetType()); | |
task.Id = Guid.NewGuid(); | |
task.ScheduleSpecType = scheduleSpecType; | |
task.Title = task.TaskName; | |
task.ScheduleSpec = CrontabExpression; | |
task.ExecuteTime = CrontabHelper.GetExecuteTime(CrontabExpression, scheduleSpecType); | |
manager.AddTask(task); | |
manager.SaveChanges(); | |
} | |
} | |
/// <summary> | |
/// Registers all custom scheduled tasks | |
/// </summary> | |
public static void RegisterCustomScheduledTasks() | |
{ | |
using (SchedulingManager manager = SchedulingManager.GetManager()) | |
{ | |
List<ScheduledTaskData> allTasks = manager.GetTaskData().ToList(); | |
// get all defined custom Scheduled tasks in this assembly | |
var scootTasks = typeof(CustomScheduledTaskBase).Assembly | |
.GetTypes() | |
.Where(t => t.IsSubclassOf(typeof(CustomScheduledTaskBase)) && !t.IsAbstract) | |
.Select(t => (CustomScheduledTaskBase)Activator.CreateInstance(t)); | |
foreach (var task in scootTasks) | |
{ | |
var taskData = allTasks.Where(t => t.TaskName == task.TaskName).ToList(); | |
if (taskData.Count == 0) | |
{ | |
task.ScheduleCrontabTask(); | |
} | |
else | |
{ | |
foreach (var td in taskData) | |
{ | |
// delete failed or stopped tasks and those that have been running for | |
// more than 3 hours | |
if ( | |
td.Status == TaskStatus.Failed || td.Status == TaskStatus.Stopped || | |
(td.IsRunning && td.LastModified.AddHours(3) < DateTime.UtcNow) | |
) | |
{ | |
manager.DeleteTaskData(td); | |
manager.SaveChanges(); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@using Telerik.Sitefinity.Frontend.Mvc.Helpers; | |
@using Telerik.Sitefinity.Modules.Pages; | |
@Html.Script(ScriptRef.JQuery, "top") | |
@Html.Script(ScriptRef.KendoWeb, "top") | |
@Html.StyleSheet("/ResourcePackages/Bootstrap/assets/dist/css/main.min.css") | |
@Html.StyleSheet("https://kendo.cdn.telerik.com/2017.3.913/styles/kendo.common.min.css") | |
@Html.StyleSheet("https://kendo.cdn.telerik.com/2017.3.913/styles/kendo.default.min.css") | |
<style> | |
.sfProgressBarWrp .sfProgressBarm, .k-progressbar .k-progress-status-wrap { | |
height: 35px; | |
} | |
.k-progressbar-horizontal > .k-state-selected { | |
border: none; | |
} | |
.sfMain { | |
padding: 45px; | |
} | |
</style> | |
<div class="sfMain sfClearfix"> | |
<h1 class="sfBreadCrumb">Custom Demo Scheduled Task</h1> | |
<div class="row"> | |
<label> | |
You can manually start the task by clicking the button below | |
</label> | |
<div> | |
<button class="sfLinkBtn sfSave" onclick="return start();">Manual Start Task</button> | |
</div> | |
</div> | |
<div class="row" style="margin-top:50px;"> | |
<div id="progressbar"></div> | |
<div id="status" style="margin-top:30px;"></div> | |
</div> | |
</div> | |
<script> | |
var $progress = null; | |
var $status = $("#status"); | |
var _intervalHandle = null; | |
$(function () { | |
// .container is the wrapper class of the parent Layout widget | |
var $editDiv = $(".container"); | |
if ($editDiv.parent().hasClass("sfHeader")) { | |
// edit div is in the wrong place for some reason in SF 10.1 | |
// we need to move it out of sfHeader, to become a sibling instead of child | |
$editDiv.insertAfter(".sfHeader"); | |
} | |
// init kendo | |
$progress = $("#progressbar").kendoProgressBar({ | |
type: "percent" | |
}).data("kendoProgressBar"); | |
// in case user refreshes the page | |
beginPolling(); | |
}) | |
function start() { | |
$.ajax({ | |
url: window.location.pathname + "/startScheduledTask?taskName=SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask", | |
type: "GET" | |
}) | |
.done(function (data) { | |
if (data == "Success") { | |
alert("The task has started successfully, you can check its status below"); | |
beginPolling(); | |
} | |
}) | |
.fail(function (jqXHR, textStatus) { | |
console.log(jqXHR); | |
alert(jqXHR.responseText); | |
}); | |
return false; | |
} | |
function beginPolling() { | |
refreshProgressBar(); | |
_intervalHandle = window.setInterval(function () { | |
refreshProgressBar(); | |
}, 1500); | |
} | |
function _removeHandlers() { | |
if (_intervalHandle) { | |
window.clearInterval(_intervalHandle); | |
_intervalHandle = null; | |
} | |
} | |
function refreshProgressBar() { | |
// get task progress from Sitefinity Scheduling Service | |
$.ajax({ | |
url: "/Sitefinity/Services/SchedulingService.svc/taskName/SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask/progress?providerName=", | |
type: "GET" | |
}) | |
.done(function (data) { | |
$progress.value(data.ProgressStatus); | |
if (data.StatusMessage) | |
$status.text(data.StatusMessage); | |
else | |
$status.text(""); | |
if (data.ProgressStatus == 100 || data.Status != 1) { | |
_removeHandlers(); | |
} | |
}) | |
} | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Threading; | |
namespace SitefinityWebApp.Custom.ScheduledTasks | |
{ | |
public class DemoScheduledTask : CustomScheduledTaskBase | |
{ | |
public override string CrontabExpression | |
{ | |
get | |
{ | |
// you can read the value from a custom config section | |
// execute the task every day at 11:50AM | |
return "50 11 * * *"; | |
} | |
} | |
protected override void ExecuteTheTask() | |
{ | |
for (int i = 1; i <= 100; i++) | |
{ | |
// this is useful if you are going to provide a UI that shows the current task progress | |
this.UpdateProgress(i, "Doing stuff {0}".Arrange(i)); | |
// do your stuff... | |
Thread.Sleep(2000); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using SitefinityWebApp.Custom.ScheduledTasks; | |
using System; | |
using Telerik.Sitefinity.Services; | |
namespace SitefinityWebApp | |
{ | |
public class Global : System.Web.HttpApplication | |
{ | |
protected void Application_Start(object sender, EventArgs e) | |
{ | |
SystemManager.ApplicationStart += SystemManager_ApplicationStart; | |
} | |
private void SystemManager_ApplicationStart(object sender, EventArgs e) | |
{ | |
CustomScheduledTaskBase.RegisterCustomScheduledTasks(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Web.Mvc; | |
using Telerik.Sitefinity.Mvc; | |
using Telerik.Sitefinity.Mvc.ActionFilters; | |
using Telerik.Sitefinity.Scheduling; | |
using Telerik.Sitefinity.Security; | |
namespace SitefinityWebApp.Mvc.Controllers | |
{ | |
[ControllerToolboxItem(Name = "ScheduledTaskAdmin", Title = "Scheduled Task Admin", SectionName = "Admin Widgets", CssClass = "sfMvcIcn")] | |
public class ScheduledTaskAdminController : Controller | |
{ | |
/// <summary> | |
/// This widget will let admin user to start/see progress of a custom scheduled task | |
/// </summary> | |
/// <returns></returns> | |
public ActionResult Index() | |
{ | |
return View("Default"); | |
} | |
/// <summary> | |
/// Used to manually start a scheduled task on demand (at any time) | |
/// </summary> | |
/// <param name="taskName"></param> | |
/// <returns></returns> | |
[HttpGet] | |
[JsonResultFilter] | |
public JsonResult startScheduledTask(string taskName) | |
{ | |
try | |
{ | |
// will throw exception if the user is not admin | |
SecurityManager.EnsureCurrentUserIsUnrestricted(); | |
var manager = SchedulingManager.GetManager(); | |
var existingTask = manager.GetTaskData().Where(t => t.TaskName == taskName).FirstOrDefault(); | |
if (existingTask == null) | |
{ | |
return new JsonResult() { Data = "No such task exist" }; | |
} | |
if (existingTask.IsRunning) | |
{ | |
return new JsonResult() { Data = "Task is already running" }; | |
} | |
existingTask.ExecuteTime = DateTime.UtcNow.AddDays(-1); | |
manager.SaveChanges(); | |
} | |
catch (Exception ex) | |
{ | |
return new JsonResult() { Data = ex.ToString() }; | |
} | |
return new JsonResult() { Data = "Success", JsonRequestBehavior = JsonRequestBehavior.AllowGet }; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment