This an article is on launching scheduled tasks. Not quite as exciting as launching a spaceship into outer space, but…hey, even astronauts have to automate some of their day to day activities.
Well we've entered a new era folks! The government isn't the only one who can make a spacecraft and launch it into space. In honor of the historical space mission by Mike Melvill, first civilian astronaut, I've decided to write an article on launching, well... scheduled tasks. Not quite as exciting as launching a spaceship into outer space, but...hey, even astronauts have to automate some of their day to day activities.Although Microsoft already includes a task scheduler in the operating system, I thought it would be an interesting exercise to create one that runs processes read out of an xml file. This task scheduler did not have to be all things to all people. I simply needed it to start a process at a certain time of day everyday. Specifically, I need it to start and stop windows services using the two batch files listed below: Start.bat Stop.bat net start MyService net stop MyService
Table 1 - Using Batch files to stop and start Windows Services
The UML design for the Schedule Launcher (reverse engineered with WithClass) is shown below in figure 2. The design consists of the Service1 class that is automatically generated by the framework. The Service class contains a Threading.Timer which is used to poll the system clock for the current time in order to check for a possible launch. Also in the design is a singleton ProcessReader class that reads the processes and times out of an xml file.Figure 2 - UML Diagram of the Windows Service for Launching Scheduled TasksThe timer class allows us to intercept an event every 30 seconds that the service is active. The timer is constructed with the callback delegate for the event handler along with the time we want the timer to trigger an event. Initially we set the time to infinite in order to keep the timer stopped.Listing 1 - Constructor of Service containing timer construction public Service1() { // This call is required by the Windows.Forms Component Designer. InitializeComponent(); // Read process and launch times from the xml file ReadProcesses(); // set up the timer in the stopped state _timer = new Timer(new TimerCallback(OnNextMinute), null,Timeout.Infinite, Timeout.Infinite); } When we are ready to start the timer, we simply change the time period from infinite to a finite period in milliseconds:Listing 2 - Starting the Timerconst long TIMER_INTERVAL = 30000L; private void StartTimer(){// set the timer to trigger an event every 30 seconds _timer.Change(0, TIMER_INTERVAL); } Once we've started the timer we need to check it against the system clock to see if we are ready to launch our process. The OnNextMinute event handler is triggered every 30 seconds by the timer and compares the system clock against the file time in the xml file corresponding to the process. If the time is within 1 minute, it launches the process.Listing 3 - Event Handler triggered by the timer every 30 seconds public void OnNextMinute(object state){// get the current system clock timeDateTime currentTime = DateTime.Now; // loop through each process data read from the XML fileforeach (ProcessInfo p in _processes){ if ((currentTime.Hour == p.StartTime.Hour) && (currentTime.Minute == p.StartTime.Minute) && p.Started == false) {// minute reached, start the processstring path = p.Path + "\\" + p.File; System.Diagnostics.Process.Start(path); p.Started = true; } // reset process flag two minutes later, to be safe if ((currentTime.Hour == p.StartTime.Hour) && (currentTime.Minute > p.StartTime.Minute + 2) && p.Started == true) {p.Started = false;} } // end for each process info } Parsing Xml with XPath Using an XmlDocument with XPath is a very convenient way for us to get the process information out of our file. Our Xml file consists of a set of processes containing the file to execute, the path, and the time to execute in each process node.Listing 4 - Xml File containing Processes to Launch<?xml version="1.0" encoding="utf-8" ?> <Processes> <Process><Path>C:\workspace\QuotingService\bin\bin</Path><File>start.bat</File><Time>9:20 AM</Time></Process> <Process><Path>C:\workspace\QuotingService\bin\bin</Path><File>stop.bat</File><Time> 4:15 PM </Time></Process> <Process><Path>C:\CommodityService\bin</Path><File>start.bat</File><Time>9:20 AM</Time></Process> <Process><Path>C:\CommodityService\bin</Path><File>stop.bat</File><Time> 4:15 PM </Time></Process> </Processes> Below is the routine in the ProcessReader that reads the process nodes into an array of ProcessInfo classes. The constructor loads in the Xml file by calling Load on the XmlDocument. The GetProcesses method selects all the process nodes via XPath. The XPath query //Processes/* asks for all of the nodes underneath the Processes node. The double slash tells SelectNodes to skip past all the ancestor nodes and go right to the Processes node. The star (*) tells xpath to choose all of the nodes underneath Processes.Listing 5 - Reading the Process Nodes using XPath and XmlDocument XmlDocument _xDoc = null; public ProcessReader(){string path = GetConfigPath();_xDoc = new XmlDocument();_xDoc.Load(path); // Load the xml file } public ProcessInfo[] GetProcesses(){ // XPath statement for selecting nodes string xpath = "//Processes/*";// Select the nodes with the XPath queryXmlNodeList nodes = _xDoc.SelectNodes(xpath);// Create an array to hold process infoProcessInfo[] processes =(ProcessInfo[])Array.CreateInstance(Type.GetType("ApplicationLauncherService.ProcessInfo"),nodes.Count); // Go through each process node and populate process// info objectint i = 0; foreach (XmlNode node in nodes){ ProcessInfo p = new ProcessInfo(node["Path"].InnerText, node["File"].InnerText, node["Time"].InnerText,""); processes[i] = p;i++;} return processes; } Below is the ProcessInfo class used to contain the process launch information that we read with our GetProcesses method. It is a simple class containing only fields to hold the process info and times to launch along with a constructor. This class also converts our time string to a DateTime. We are only interested in the time here, so we concatenate a date onto the string in order to produce a legitimate DateTime with the Convert class.Listing 6 - Reading Process info class public class ProcessInfo{ public string Path; // path of the process filepublic string File; // name of the applicationpublic string Arguements; // arguments of the apppublic bool Started; // whether or not it was already// started public DateTime StartTime; public ProcessInfo(string path, string file, stringstarttime, string arguments) {Path = path;File = file;StartTime = Convert.ToDateTime(" 11/17/1965 " +starttime);Arguements = arguments;Started = false;} }Conclusion The time for civilian space travel may be here sooner than we know it. In the meantime, while I'm waiting for my lunar flight, I'll continue to hang out in my namespace and experiment with C# and .NET. Happy Launching!
Pro WPF: Windows Presentation Foundation in .NET 3.0