Skip to content

Instantly share code, notes, and snippets.

@VesselinVassilev
Last active July 22, 2020 14:01
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save VesselinVassilev/89b04f4ce7cef2610590643bafe089ca to your computer and use it in GitHub Desktop.
Save VesselinVassilev/89b04f4ce7cef2610590643bafe089ca to your computer and use it in GitHub Desktop.
Custom Recurring Scheduled Tasks in Sitefinity with Cron Jobs
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;
}
}
}
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();
}
}
}
}
}
}
}
}
@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>
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);
}
}
}
}
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();
}
}
}
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