Creating a Simple Windows Service in Managed C

Creating a Simple Windows Service in Managed C++

The sample service in this chapter will check Web sites for you to be sure they are serving pages. It will make extensive use of a configuration file, because it has no user interface. The configuration file will control

  • The URLs to check

  • An email address to notify if a site is down

  • How often to check

The service will check the URLs when it starts, and then sleep for the specified period of time. If any URL doesn't respond, an email will be sent and an entry will be added to the event log. Email and event logs are excellent ways for applications without a user interface to notify an administrator of problems that need intervention.

Creating the Skeleton of the Service

Start by creating the project. Create a Windows Service (.NET) project called URLChecker . Because a service doesn't have a user interface, the view that opens (of your user interface) is not very useful. Click the link to switch to code view.

A class has been generated, called URLCheckerWinService , which inherits from ServiceBase in the System::ServiceProcess namespace. This class has two useful methods : OnStart() and OnStop() . You add code to these methods to actually implement the service.

Services that react to events thrown by others don't need a loop construct; the OnStart adds the service to the list of listeners for a particular event and then you write the corresponding event handler. This chapter's service, however, will use a loop. The OnStart() method creates a new thread which loops until a control variable makes it stop. It has to loop in a separate thread so that OnStart() can return control to the code that called it.

Add these private variables to the class, just before the definition of OnStart() :

 
 private:    bool stopping;    int loopsleep; //milliseconds    Thread* servicethread; 

Add a function to the class:

 
 void CheckURLs() {     loopsleep = 1000; //milliseconds     stopping = false;     while (!stopping)     {         Threading::Thread::Sleep(loopsleep);     } } 

This is the function the thread will call. It loops until it is told to stop, through the control variable stopping . This function is just a skeleton to begin with and will expand later in this chapter. Add code to OnStart() and remove the TODO comment, so that it reads like this:

 
 void OnStart(String* args[]) {     Threading::ThreadStart* threadStart =                   new Threading::ThreadStart(this,CheckURLs);     servicethread = new Threading::Thread(threadStart);     servicethread->Start(); } 

This code uses a helper class called ThreadStart to hold the delegate representing CheckURLs . The ThreadStart object is passed to the constructor of a Thread() object, and finally OnStart() starts the thread. This kicks off the loop. To be sure that the loop will stop, implement OnStop() as follows :

 
 void OnStop() {    stopping = true; } 

Although this service doesn't do anything yet, it can be installed, started, and stopped .

Setting Properties and Adding an Installer

Switch back to the empty design view of your service, right-click the background, and choose Properties. Change the name for URLCheckerWinService to URLChecker . Then right-click the background again and choose Add Installer. A new file is created and opened in design view, showing a service process installer and a service installer.

The service process installer has only one property of interest: the account under which the service will run. Click serviceProcessInstaller1 to select it, and then open the Properties window. By default, the Account property is User, which means that the installer will prompt for an ID and password when you install the service, and the service will run with that user's privileges. It's more useful to run the service under a system account. Changing the Account property to LocalSystem will run the service as a privileged account. Use this only if you need it; LocalService is a less-privileged choice for services that don't need to pass credentials to another machine, and Network Service is another nonprivileged account that can authenticate to another machine. Although this service contacts other machines, it doesn't need to authenticate, so LocalService is a good choice if you are installing the service on a Windows 2003 machine. To support a variety of operating systems, use the older LocalSystem account.

The service installer is used to control the way the service runs, and you have three choices: Manual, Automatic, and Disabled. An Automatic service starts whenever the machine is restarted. A Manual service can be started on request. A Disabled service cannot be started. A user can change the startup type with Computer Management or Server Explorer, but other code cannot start the service. Leave this property set to Manual.

Installing and Testing the Service

In order to test the service, you must install it. Services written in Visual Basic or C# can be installed using a utility called installutil.exe, but this utility has trouble loading C++ assemblies. The wizard generates a main() function for this service that you can use to install the service. Open a command prompt and change directories to the Debug folder beneath the project folder. Make sure you have built the project, and then execute its main function like this:

 
 urlchecker.exe Install 

Expand the Services node in Server Explorer and scroll down to the bottom, where you should see URLChecker preceded by a symbol made from a gear wheel and a small red square. Right-click it and choose Start. After a small delay, the red square becomes a green triangle. Right-click it again and choose Stop. You have demonstrated that your service can be installed successfully, and can process both Start and Stop commands without errors. Now it's time to add code so that the service does something useful.

Once the service is installed, you don't need to reinstall it or update or refresh anything when you make changes to your code. Just stop the service in Server Explorer, change your code, build the project, and start the service again. Your new code will execute. (If you forget to stop the service before building, you'll get a fatal link error because the linker will be unable to open your .EXE file.)

Checking a URL

To change the service so that it checks URLs, first add a configuration file to the project. Configuration files are discussed in Chapter 11, "Writing a Data Layer in Managed C++," where a configuration file holds a connection string for database access. Right-click the project in Solution Explorer, and choose Add, Add New Item. Select a Configuration File and click Open. Enter XML so that the configuration file reads like this:

 
 <configuration>   <appSettings>     <add key="urls" value="http://www.gregcons.com http://www.microsoft.com" />     <add key="email" value="you@yourdomain.com" />     <add key="minutesinterval" value="2" />   </appSettings> </configuration> 

Make sure you change the email address to one that will reach you. If you want, add some more URLs. Leave a space between each URL, and don't forget to include the http:// specifier . Add a postbuild step, as first discussed in Chapter 11, to copy the configuration file to the output directory. The command line should read like this:

 
 copy app.config $(ConfigurationName)$(TargetFileName).config 

Add this code at the beginning of CheckURLs() , just before the loop:

 
 //get config info from file String* URLString =       Configuration::ConfigurationSettings::AppSettings->get_Item("urls"); String* delims = S" "; Char delimiter[] = delims->ToCharArray(); URLs = URLString->Split(delimiter); email = Configuration::ConfigurationSettings::AppSettings->get_Item("email"); interval = Configuration::ConfigurationSettings::AppSettings->         get_Item("minutesinterval")->ToInt16(NULL); //set lastrun to force an immediate check lastrun = DateTime::Now - TimeSpan(0,interval+1,0); //hours, minutes, seconds 

This code could go in the OnStart() method, but you can't debug a service until it is started, so you want as little code as possible in OnStart() . Because this code is before the loop, it will only execute once anyway.

This code retrieves the list of URLs from the configuration file and uses Split() to separate it at spaces. It also retrieves the email address and the interval at which the URLs should be checked.

You might be tempted to change the Sleep() call at the bottom of the loop to sleep for however many minutes the configuration file requested , but that will leave your service unable to respond to stop requests until it wakes up. An unresponsive service can interfere with shutdown and other system processes. It's better to leave the loopsleep value at one second, and use a saved time to track when URLs were last checked. This variable, called lastrun in this sample, starts at a value small enough to ensure the URL checking will happen immediately when the service starts running. The calculation uses the TimeSpan helper class, which simplifies date and time arithmetic significantly.

Add these member variables to the class:

 
 DateTime lastrun; String* URLs[]; String* email; int interval; //minutes 

Change CheckURLs() to use the information from the configuration file and attempt to retrieve information from each URL in turn . Edit the body of the loop so that it reads like this:

 
 if (DateTime::Now > lastrun + TimeSpan(0,interval,0)) {     lastrun = DateTime::Now;     for (int i = 0; i < URLs->Length; i++)     {         try         {             Net::WebRequest* req = Net::WebRequest::Create(URLs[i]);             req->Method = "HEAD";             Net::WebResponse* resp = req->GetResponse();         }         catch (...)         {             //couldn't reach server - notify someone         }     } } Threading::Thread::Sleep(loopsleep); 

This code determines whether it's time to check URLs, and if it is, it sets lastrun and then goes through all the URLs in the array. Notice that the array, declared with square brackets just like an old-style C++ array, has a property called Length that can be used to set up this loop. The WebRequest class is used to get the headers only by setting the Method to HEAD . This saves time, because there's no need to read the entire page returned from the server; you just want to confirm there's a page to return.

If this service was a link-checker, it might be interested in whether the Web server returned a page for that URL, or a 404 error, or some other kind of response. But this service is simply confirming that the server exists and responds. If the server doesn't exist, the GetResponse() method throws an exception, which this code catches and uses as the trigger to notify the administrator that one of the monitored Web sites is down.

Sending Email

The SmtpMail class in the System::Web::Mail namespace represents a mail message. The simplest way to use it is with the static Send() method, which takes four string parameters:

  • The email address from which the message will appear to come

  • The email address to which the message will be sent

  • The subject line for the message

  • The body of the message

Add a reference to System.Web.dll and then edit the catch block in CheckURLs to read as follows:

 
 catch (Exception* e) {     Text::StringBuilder* body = new Text::StringBuilder(S"");     body->Append(S"URLChecker could not reach ");     body->Append(URLs[i]);     body->Append(Environment::NewLine);     body->Append(e->ToString());     Web::Mail::SmtpMail::Send(S"urlchecker@yourdomain.com",                               email,                               S"URL Checker failure report",                               body->ToString()); } 

If there is no SMTP server running on your machine, set the shared Web::Mail::SmtpMail::SmtpServer property to the IP address or fully qualified name of your mail server, such as mail.yourdomain.com , before calling Send() . Also, make sure you change the From address to your own domain when you edit this code.

You can test this service now. Simply edit the configuration file so that it contains at least one URL that will not return a page. For example, if there's a computer on your network that does not have a Web server installed, use that computer's IP address. If you plan to try making up a domain name, check in a browser first to see whether there is a server at that domain or not. Stop the service, rebuild the solution (so as to trigger the post build step that copies the configuration file), and start the service. Wait for at least as long as your interval time, and then check your mail. You should receive a message that reads like this (with a different URL):

 
[View full width]
 
[View full width]
URLChecker could not reach http://205.210.50.4 System.Net.WebException: The underlying connection was closed: Unable to connect to the graphics/ccc.gif remote server. at System.Net.HttpWebRequest.CheckFinalStatus() at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult) at System.Net.HttpWebRequest.GetResponse() at URLChecker.URLCheckerWinService.CheckURLs() in e:\urlchecker\urlcheckerwinservice.h graphics/ccc.gif :line 94

Stop the service, or you'll continue to get email every few minutes.

Adding Event Log Entries

Sending email is one way a service can notify the administrator of a problem. The Event Log is another way. Because it's easy to use, and works even when your Internet connection cuts you off from your email, why not add event logging to URLChecker ?

Before the loop in CheckURLs() , add these lines:

 
 Diagnostics::EventLog* log; if (! Diagnostics::EventLog::SourceExists("URLCheckerService") )     Diagnostics::EventLog::CreateEventSource("URLCheckerService",                                              "URLCheckerLog"); log = new Diagnostics::EventLog("URLCheckerLog"); log->Source = "URLCheckerService"; 

This sets up an event source called URLCheckerService and a custom event log called URLCheckerLog . It then creates a EventLog object that can write to URLCheckerLog and sets the source to URLCheckerService .

Add this line at the end of the catch block, after the lines that send the email:

 
 log->WriteEntry(body->ToString(),Diagnostics::EventLogEntryType::Error); 

That's all it takes to add event logging to your service! Stop the service, build it, and start it again. Wait for the email to reach you, and then scroll up in Server Explorer to the EventLogs node. Expand it, and then expand URLCheckerLog (the log name) beneath it, and finally URLCheckerService (the source name) beneath that. You see at least one error entry, identified with an X on a red background as in Figure 12.3.

Figure 12.3. The service has added entries to its own event log.

graphics/12fig03.gif

The Server Explorer only shows the first few characters of the log entry. To see the whole entry, use the Event Log section under Computer Management. You might have to close and re-open Computer Management to refresh the list of Event Logs. Expand Event Viewer, and then select URLCheckerLog . You will see the log entries: Double-click one for the details, as in Figure 12.4.

Figure 12.4. The details of the event log entries are available from the Event Viewer.

graphics/12fig04.jpg



Microsoft Visual C++. NET 2003 Kick Start
Microsoft Visual C++ .NET 2003 Kick Start
ISBN: 0672326000
EAN: 2147483647
Year: 2002
Pages: 141
Authors: Kate Gregory

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net