Skip to main content

Creating a Windows Task Scheduler Service

Recently, one of my friends asked me for help in creating a windows service for scheduling some tasks. He said that he could not use the task scheduler that comes with Windows as the Task Scheduler expects each task to be a stand alone executable file. All his tasks were in a single class library and it would be a lot of work to separate each task into its own executable. Also he said that that the schedule for the tasks could change from time to time and it would be nice if he could configure the tasks in an XML file.

To Recap, the requirements for the windows service are

1) The tasks should be loaded from a class library

2) The schedule information for the tasks should be configurable in an XML file.

We googled (binged :)) for the solution and found an excellent article by Ajit Kumar Application Scheduler Service Using C#.Net And XML to base our solution upon. We took some good points like configuring the task information from the above mentioned article.

This is how we took a stab at our solution. We used a bit of reflection to get instances of tasks that need to run.

  • Configure the scheduled tasks in an XML file (Tasks.xml)
  • On Service Start, load the the tasks configuration from the Tasks.xml file into a DataSet.
  • Get a reference to the assembly that contains the tasks.
  • Use a Systems.Timer to run a method (RunTaks) periodically that checks the tasks that need to run and run the tasks.


This is how the Tasks.xml file looks like

name is the class name of the task to run, time is date and time (MM/dd/yyyy HH:mm format) when the task should run, and repeat is how often the task should run (H- hourly, W-Weekly, M-Monthly, D-Daily)

<appSchedule>
<task name="Task1" time="06/07/2009 12:00" repeat="H" />
<task name="Task2" time="06/15/2009 12:00" repeat="W" />
<task name="Task3" time="06/29/2009 12:00" repeat="D" />
<task name="Task5" time="06/10/2009 19:00" repeat="M" />
</appSchedule>




The path to the tasks.xml should be configured in the service's app.config file

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="tasksConfigPath" value="E:\Training\Demo\SchedulerService\SchedulerService\Tasks.xml"/>
</appSettings>
</configuration>


Next, to ease the process of running a task, we made sure that all the tasks implement an interface ITask which has one method called RunTask that returns nothing (void).

MailTasks



namespace MailTasks
{
public interface ITask
{
void RunTask();
}
}



We used ThreadPooling to ease the burden of managing the threads. Since each task has to run in its own thread and we dont know how many threads to create, we zeroed in on ThreadPooling to manage the threads.


We have System.Timer object that periodically calls the RunTasks method. We use the global boolean variable workInProgress to track if the RunTasks method is running or idling. If the workInProgress is true we just return to wait for the completion of the earlier call to RunTasks method. If the workInProgress is false, we proceed further to run the scheduled tasks.


We get the list of tasks to run by calling a method GetTasksToRun(). Inside the GetTasksToRun method, we go through the DataSet with the tasks schedule information, for each task scheduled, if the current time is greater than the scheduled time, using reflection we create the Task Object that needs to run and then add it to the list of tasks to run.


Once we get the list of tasks to run, we update a global variable numBusy with the count of tasks to run. This numBusy variable will be used to track the number of busy threads at any given time. we loop through the scheduled tasks list, and queue each task in the ThreadPool by passing reference to a method (DoTask) and the task object itself to the ThreadPool's QueueUserWorkItem method.


Inside the DoTask method, we call the RunTask() method on the task object passed in as an argument. We update the next run time for the task in the DataSet by calling the method UpdateNextRunTime and decrement the count of busy threads (numBusy) in the finally.

Back in the RunTasks method we wait for all the threads to complete by calling WaitOne() method on the ManualResetEvent object doneEvent.


After all the queued tasks are complete, we persist the tasks data in the DataSet back to the disk and set workInProgress to false to mark the completion of all the tasks queued.


using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;
using System.Timers;
using System.Xml;
using System.Configuration;
using MailTasks;
namespace SchedulerService
{
public partial class Scheduler : ServiceBase
{
private static ManualResetEvent doneEvent;
private static string configPath = string.Empty;
private static int numBusy;
private static DataSet dsTasks;
private const string TASKS_NAME_SPACE = "MailTasks."; //Period is needed
private const string DATE_FORMAT_STRING = "MM/dd/yyyy HH:mm";
private static Assembly tasksAssembly;
private static EventLog eventLog1;
readonly System.Timers.Timer _timer = new System.Timers.Timer();
private static bool workInProgress;

public Scheduler()
{
InitializeComponent();
if (!EventLog.SourceExists("MailScheduler"))
EventLog.CreateEventSource("MailScheduler", "Application" );

eventLog1 = new EventLog("Application", Environment.MachineName, "MailScheduler");
}

protected override void OnStart(string[] args)
{
try
{
eventLog1.WriteEntry("Mail Scheduler Service Started");
LoadTasksIntoDataSet();
LoadTasksAssembly();
_timer.Interval = 60000;
_timer.Elapsed += RunTasks;
_timer.Start();
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred OnStart "+ex.Message);
}
}

protected override void OnStop()
{
eventLog1.WriteEntry("MailScheduler service stopped");
try
{
UpdateTasksConfigonDisk();
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred onStop "+ex.Message);
}
}

private static void RunTasks(object sender, ElapsedEventArgs args)
{
//If the processing of RunTasks lasts longer than the Timer's interval, RunTasks could be called
//again before the previous call finished. To overcome this, using a bool variable workInProgress to track if this method is in progress
//If not, go ahead else return
if (workInProgress) return;

numBusy = 0;
// LoadTasksIntoDataSet();
doneEvent = new ManualResetEvent(false);

List tasksList = GetTasksToRun();
numBusy = tasksList.Count; //Number of threads to create is not constant, depends on the tasks ready to run at a given time
if (numBusy > 0)
{
workInProgress = true;
foreach (ITask task in tasksList)
{
ThreadPool.QueueUserWorkItem(DoTask, task);
}
doneEvent.WaitOne();
}
//All scheduled tasks completed, persist the tasks data to disk,iteration over
if (numBusy == 0 && tasksList.Count > 0)
{
workInProgress = false;
UpdateTasksConfigonDisk();
}
}

private static void DoTask(object o)
{
ITask task = o as ITask;
if (task == null) return;
string scheduleName = task.GetType().ToString();
try
{
//Event Log, starting task at this time.
task.RunTask();
//Task completed successfuly at this time

int lastIndexOfPeriod = scheduleName.LastIndexOf(".");
UpdateNextRunTime(scheduleName.Substring(lastIndexOfPeriod + 1));
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while executing task: " + scheduleName);
eventLog1.WriteEntry("Stack Trace is: " + ex.Message);
}
finally
{
if (Interlocked.Decrement(ref numBusy) == 0)
{
doneEvent.Set();
}
}
}

private static void LoadTasksIntoDataSet()
{
try
{
eventLog1.WriteEntry("Trying to Load Tasks into DataSet");
configPath = ConfigurationManager.AppSettings["tasksConfigPath"];
XmlTextReader xmlTextReader = new XmlTextReader(configPath);
XmlDataDocument xdoc1 = new XmlDataDocument();
xdoc1.DataSet.ReadXml(xmlTextReader, XmlReadMode.InferSchema);
dsTasks = xdoc1.DataSet;
xmlTextReader.Close();
eventLog1.WriteEntry("Finished Loading Tasks into DataSet");
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred while loading tasks into DataSet " + ex.Message);
throw;
}
}

private static void UpdateTasksConfigonDisk()
{
try
{
eventLog1.WriteEntry("Attempting to save tasks information to disk ");
StreamWriter sWrite = new StreamWriter(configPath);
XmlTextWriter xWrite = new XmlTextWriter(sWrite);
dsTasks.WriteXml(xWrite, XmlWriteMode.WriteSchema);
xWrite.Close();
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while savings tasks information to disk "+ex.Message);
throw;
}
}

//updating the dataset is not thread safe
private static void UpdateNextRunTime(string taskName)
{
if (dsTasks == null) return;
foreach (DataRow row in dsTasks.Tables[0].Rows)
{
if (taskName.ToLower() != row[0].ToString().ToLower()) continue;
DateTime scheduledTime = DateTime.Parse(row[1].ToString());
string repeat = row["repeat"].ToString().ToUpper();
switch (repeat)
{
case "H":
scheduledTime = scheduledTime.AddHours(1);
if (scheduledTime < DateTime.Now)
scheduledTime = DateTime.Now.AddHours(1);
break;
case "D":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddDays(1);
}
break;
case "W":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddDays(7);
}
break;
case "M":
while (scheduledTime < DateTime.Now)
{
scheduledTime = scheduledTime.AddMonths(1);
}
break;
}
row[1] = scheduledTime.ToString(DATE_FORMAT_STRING);
dsTasks.AcceptChanges();
}
}

private static List GetTasksToRun()
{
if (dsTasks == null) return null;
List tasks = new List();
foreach (DataRow row in dsTasks.Tables[0].Rows)
{
DateTime scheduledTime = DateTime.Parse(row[1].ToString());
if (DateTime.Now < scheduledTime) continue;
ITask task = CreateTaskInstance(row[0].ToString());
if (task != null)
tasks.Add(task);
}
return tasks;
}

private static ITask CreateTaskInstance(string taskName)
{
string taskFullName = TASKS_NAME_SPACE + taskName;
try
{
if(tasksAssembly==null)
throw new Exception("Tasks Assembly is null, cannot proceed further..");
//Create an instance of the task
ITask task = (ITask)tasksAssembly.CreateInstance(taskFullName, true);
return task;
}
catch (Exception ex)
{
eventLog1.WriteEntry("Error occurred while creating Task Instance " + ex.Message);
}
return null;
}

private static void LoadTasksAssembly()
{
try
{
if (tasksAssembly == null)
tasksAssembly = Assembly.GetAssembly(typeof(MailTasks.ITask));
}
catch(Exception ex)
{
eventLog1.WriteEntry("Error occurred while loading tasks Assembly " + ex.Message);
throw;
}
}
}
}

Comments

Unknown said…
How to define ITASK, would appreciate if you could show me some example code.

Popular posts from this blog

Clear Validation Errors and Validation Summary messages

ASP.net built in validation does not provide us a straight forward to clear all the validation errors. This would be really helpful while resetting a form. The reset html button would simply reset the form values but will not clear the validation errors. The following javascript code snippet can be used to clear the validation error messages. Have a reset button on your form and call the following js function onclick. <input type="reset" onclick="HideValidationErrors();" /> function HideValidationErrors() { //Hide all validation errors if (window.Page_Validators) for (var vI = 0; vI < Page_Validators.length; vI++) { var vValidator = Page_Validators[vI]; vValidator.isvalid = true; ValidatorUpdateDisplay(vValidator); } //Hide all validaiton summaries if (typeof (Page_ValidationSummaries) != "undefined") { //hide the validation summaries

Kill a remote user session remotely

When trying to connect to your Windows 2000/2003 server remotely, you may receive the following error. "The terminal server has exceeded the maximum number of allowed connections." You could kill one or more of those connections by using PsExec tool that can be downloaded from the following link. This tool and a bunch of others were developed by SysInternals which was bought by Microsoft. http://www.microsoft.com/technet/sysinternals/utilities/pstools.mspx Open your command prompt and from the directory that contains the psexec utility, do the following 1) psexec \\x.x.x.x -u user -p password cmd (this will give you access to the cmd prompt on the server) Example: psexec \\127.0.0.1 -u admin -p password cmd 2) once you get the command prompt run the command qwinsta to get a list of all Terminal Services connections. Each connection has an Id Number. 3) Run the command logoff [id# of session to quit] /v (this will kill the connection with that id #) Example: logoff 2 /v Once