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 Tasks
The 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 Timer
const 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 time
DateTime currentTime = DateTime.Now;
// loop through each process data read from the XML file
foreach (ProcessInfo p in _processes)
{
if ((currentTime.Hour == p.StartTime.Hour) &&
(currentTime.Minute == p.StartTime.Minute)
&& p.Started == false)
{
// minute reached, start the process
string 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 query
XmlNodeList nodes = _xDoc.SelectNodes(xpath);
// Create an array to hold process info
ProcessInfo[] processes =
(ProcessInfo[])Array.CreateInstance(Type.GetType(
"ApplicationLauncherService.ProcessInfo"),
nodes.Count);
// Go through each process node and populate process
// info object
int 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 file
public string File; // name of the application
public string Arguements; // arguments of the app
public bool Started; // whether or not it was already
// started
public DateTime StartTime;
public ProcessInfo(string path, string file, string
starttime, 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!