Click here to Skip to main content
Click here to Skip to main content

IP Watchdog: Simple Windows Service Written in C#

, 14 Aug 2012
Rate this:
Please Sign up or sign in to vote.
Self-installing windows service that monitors computer's WAN address and sends e-mail when it changes

Background

Once upon a time I encountered a need to write a windows service that would monitor my computer's WAN IP and notify me of the changes. I discovered that this was a perfect opportunity to "do it right": this task was complex enough to be practical, and yet simple enough so I could concentrate not only on the problem itself, but also on the surrounding boilerplate issues like logging and installation. This is how the IP Watchdog service was born.

A Sample with Practical Value

The task that this service solves is very real. It runs on my home server and sends me an e-mail every time its external IP changes. Besides this main task, I set the following additional goals:

  • The service must be self-installing, without necessity to run installutil.
  • The service must be able to run in console mode, logging output to a console window.
  • While in windows service mode, the service must write to Windows event log.
  • It must be possible to start and stop the service using the service executable itself.
  • Service code must serve as an example of good coding practices and as a "template" for other services I write.
  • I will be using Git source control when developing it.

The Source Code

The source code (15K ZIP archive) is available here: IpWatchdog.zip (15K).

Git repository with the code: https://github.com/ikriv/IpWatchDog

Why IP Watchdog?

My home network is connected to the Internet via cable, and my cable modem has "almost static" IP, that changes maybe two or three times a year. Naturally, I want this IP to be mapped to a friendly name, like home.ikriv.com. I tried to use dyndns.org service back in the days when it was free, but after 30 days without IP change notifications it would kick me out. Since the IP changes so rarely, I don't see a point in paying additional money for static IP. However, it does change from time to time, and then I cannot access my home until I physically get there, check what new IP is, and change the DNS settings for my domain.

Typical home network

After yet another outage I decided that I had enough. I needed some agent that would monitor my home IP, and send me an e-mail notification when a change occurs. I could receive e-mail on my mobile phone, and then access my domain DNS from wherever I happen to be at the moment.

Of course, I could try to find an existing service, but this task sounded like fun, so I spent a day writing it. I also wanted to refresh my service-writing skills, and do it right, with installer code, event log notification, console mode, etc. So, in a way this service works as a cheat sheet for boilerplate tasks like "how do you install a service without using InstallUtil". If I need to write another service, I will not have to reinvent the wheel.

The Main Loop

The core task of the service is very simple:

  1. Check current external IP by reading it from checkip.dyndns.org.
  2. Compare to previous value of IP.
  3. If different, send notification e-mail.
  4. Wait for some time and repeat.

The code for this loop following the "programming by intention" paradigm is very short:

void CheckIp()
{
    var newIp = _retriever.GetIp();

    if (newIp == null) return;
    if (newIp == _currentIp) return;
 
    if (_currentIp == null)
    {
        _log.Write(LogLevel.Info, "Currrent IP is {0}", newIp);
    }
    else
    {
        _notifier.OnIpChanged(_currentIp, newIp);
    }

    _currentIp = newIp;
    _persistor.SaveIp(_currentIp);
}

This code uses some dependencies: _retriever is a class responsible for reading the IP from the web, _notifier sends e-mail notifications, and _persistor saves IP value between service invocations.

Running the Loop

The code above is run periodically on timer. Starting the timer is trivial:

_timer = new Timer(CheckIp, null, 0, _config.PollingTimeoutSeconds*1000);

Stopping the service means stopping the timer. If IP check is currently underway, it acquires a lock on the _isBusy object. The stopping code tries to obtain this lock using .NET monitor: this is (allegedly) more efficient than full-blown mutex. If the lock cannot be obtained in 5 seconds, we assume IP checking process is stuck and exit without further wait.

void Stop()
{
    _log.Write(LogLevel.Info, "IP Watchdog service is stopping");
    _timer.Dispose();
    _timer = null;
    _stopRequested = true;
    if (!Monitor.TryEnter(_isBusy, 5000))
    {
        _log.Write(LogLevel.Warning, "IP checking process is still running and will be forcefully terminated");
    }
 
    _stopRequested = false;
}

void CheckIp(object unused)
{
    lock (_isBusy)
    {
        if (_stopRequested) return;
        CheckIp();
    }
}

Checking WAN IP Address

Unless your server is directly connected to the Internet, its WAN IP address is not the same as its local IP address. Below is a typical structure of a home network. For the software running on the server, there is no direct way to find out its WAN IP address (99.11.22.33 on the diagram above). All it can know is local IP address (192.168.1.3). The only reliable way to find out the WAN address is to send a request to someone on the outside and ask where it came from.

Fortunately, sites like checkip.dyndns.org provide such a service, and .NET makes sending and receiving HTTP request very easy. The class responsible for retrieving our IP address is called WebIpRetriever. We send a HTTP GET request to checkip.dyndns.org and it replies with a very small HTML page:

<html><head><title>Current IP Check</title></head><body>Current IP Address: 99.11.22.33</body></html>

From there we can extract the IP address by simple string manipulation. Current implementation of Web IP retrieve is synchronous, i.e. it blocks the timer callback until the answer comes. It would be better to implement it asynchronously, but then answer reading and service stopping logic would become somewhat complicated, so I bailed on it.

public string GetIp()
{
    try
    {
        var request = HttpWebRequest.Create("http://checkip.dyndns.org/");
        request.Method = "GET";
                
        var response = request.GetResponse();
 
        using (var reader = new StreamReader(response.GetResponseStream()))
        {
            var answer = reader.ReadToEnd(); // should have better handling here for very long responses
            return ExtractIp(answer);
        }
    }
    catch (Exception ex)
    {
        _log.Write(LogLevel.Warning, "Could not retrieve current IP from web. {0}", ex);
        return null;
    }
}

Sending Notification E-Mail

Sending an e-mail is responsibility of MailIpNotifier class. .NET provides excellent facilities for sending e-mails, so the code is straightforward:

public void OnIpChanged(string oldIp, string newIp)
{
    string msg = GetMessage(oldIp, newIp);
    _log.Write(LogLevel.Warning, msg);
 
    try
    {
        var smtpClient = new SmtpClient(_config.SmtpHost);
        smtpClient.Send(
          _config.MailFrom,
          _config.MailTo,
          "IP change",
          msg);
    }
    catch (Exception ex)
    {
        _log.Write(LogLevel.Error, "Error sending e-mail. {0}", ex);
    }
}
 
private static string GetMessage(string oldIp, string newIp)
{
    return String.Format("IP changed from {0} to {1}", oldIp, newIp);
}

Console Mode Vs. Service Mode

Direclty debugging Windows services is hard, because they are typically invoked by the system and run under a special system account. Thus, we need a way for our service to run as a regular application. We achieved that by separating our code into three parts:

  • The service logic that implements IService interface and is agnostic to the way it is run.
  • The ServiceRunner class that runs the logic as a Windows Service.
  • The ConsoleRunner class that runs the logic as a console application.

Service mode is invoked by default, console mode is invoked via -c command line switch. It would be nicer if console mode were the default, but it requires passing command line arguments to a service. This is possible, but it's a pain if you use stock service installer provided by the framework.

ServiceRunner and ConsoleRunner class diagram

The console runner runs the service and waits for the Ctrl+C combination to be pressed. The service runner inherits from ServiceBase and implements OnStart() and OnStop() methods by calling service's Start() and Stop() respectively.

We also need different logging mechanisms for console and service mode. We abstract logging with ILog interface, and that interface has two implementations. ConsoleLog writes output directly to console, while SystemLog writes to the "Application" event log displayed by "Event viewer" application.

ConsoleLog and SystemLog

Installing the Service

When you create a service project, Visual Studio throws in a "service" component, and then by right clicking on it you can add an installer class. I don't like these generated classes for several reasons:

  • I am not a big fan of a graphic designer for something as code-oriented as a service.
  • I don't like the names like ProjectInstaller1, and renaming them is a pain.
  • You are supposed to use installutil.exe to run the installer. This is ugly and difficult for the users.

In light of all that I wrote my own InstallUtil implementation, based on this example. It mostly deals with calling AssemblyInstaller class and providing error handling around it.

I also created a ProjectInstaller class similar to what the wizard would create for you, that calls ServiceProcessInstaller and ServiceInstaller. Note that my service run under NetworkService account, since network is all it cares about. Also, it looks like you don't need a special installer for eventlog event source, ServiceInstaller will create event source for you.

The service installs itself when invoked with -i command line switch, and uninstalls when invoked with -u switch.

Starting and Stopping the Service

There is a number of ways to start and stop services in Windows, e.g. the "net start" command, but it is nice if you can start and stop the service by using the service executable itself. Fortunately, implementing this in .NET requires just a few lines of code:

_log.Write(LogLevel.Info, "Starting service...");
const int timeout = 10000;
_controller.Start();

var targetStatus = ServiceControllerStatus.Running;
_controller.WaitForStatus(targetStatus, TimeSpan.FromMilliseconds(timeout));

Application Parameters

As application parameters such as polling interval, notification e-mail and SMTP server address don't frequently changed, I put the in an app.config file. You will need to modify that file before you run the service for the first time. The AppConfig class encapsulates access to the file.

Dependency Injection and Configurator Class

As you saw already, our application needs to adapt to different environments. In particular, it may use console runner or service runner, while writing to console log or system log. We achieve this kind of flexibility by following these principles:

  • Single responsibility principle.
  • Coding to interfaces.
  • Dependency injection.

Single responsibility means that each class is responsible for one thing. If you have to use the word "and" when describing class duties, it is a bad sign. Single responsibility results in small lean classes that are easy to reuse and may be combined in a variety of ways. This is the same principle that stands behind very successful UNIX tools paradigm: each tool does one thing, but does it well.

Whenever we have multiple implementations of the same concept, like in case of logs, we must have an interface. Sometimes it is beneficial to have an interface even when there is only one implementation, e.g. to make the contract explicit and get rid of unwanted dependencies. For instance, I could make ConsoleRunner depend directly on IpWatchDogService, but it does not make much sense, since the runner does not really care what kind of service it runs.

Dependency injection is a principle that a class does not create its own dependencies, but receives them from the outside. The program is thus divided into two uneven parts: the code and the "assembly line" that decides how different parts are combined. In a bigger project this assembly line functionality is typically implemented by a special library like Spring.Net or Unity. In this small project assembly line responsibilities are given tot he Configurator class:

private IService CreateWatchDogService()
{
    var config = new AppConfig();

    return new IpWatchDogService(
        _log, 
        config,
        new IpPersistor(_log), 
        new WebIpRetriever(_log), 
        new MailIpNotifier(_log, config));
}

Dependency injection is a powerful mechanism that, among other things, ensures flexibility and reusability of your classes. For example, if we wanted to send IP change notification via Twitter, all we would need to do is to write twitter notifier class and supply it to the IpWatchDogService upon creation. The only class that thus would have to be modified is the Configruator. Here's the diagram of all IpWatchDogService dependencies:

IP Watchdog service dependencies

Summary of Command Line Switches

ipwatchdog -? prints summary of all available switches. Currently supported switches are:

Short FormLong FormMeaning
-c-consoleRun in console mode
-i-installInstall the service
-p-stopStop running service
-s-startStart installed service
-u-uninstallUninstall the service

Conclusion

I hope you enjoyed reading about the IP watchdog as much as I enjoyed writing it. I also hope that it will relieve you from writing boilerplate code and allow you to concentrate on the task at hand instead. Feel free to borrow the code and use it for your needs (see "License" section for details).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Ivan Krivyakov
Technical Lead Thomson Reuters
United States United States
Ivan is a hands-on software architect/technical lead working for Thomson Reuters in the New York City area. At present I am mostly building complex multi-threaded WPF application for the financial sector, but I am also interested in cloud computing, web development, mobile development, etc.
 
Please visit my web site: www.ikriv.com.

Comments and Discussions

 
QuestionGratitude greetings =) PinmemberMember 1086577523-Jun-14 20:44 
AnswerRe: Gratitude greetings =) PinpremiumIvan Krivyakov24-Jun-14 10:54 
GeneralThank you! PinmemberAngieLeigh16-Mar-14 9:11 
QuestionDo i have the permissions to run this!!?? PinmemberMarko Ortiz27-Jan-14 11:06 
GeneralMy vote of 5 PinmemberMember 89853705-Jun-13 4:47 
GeneralMy vote of 5 PinmemberAfzal.gujrat19-Mar-13 22:48 
GeneralMy vote of 5 PinmemberMember 374867928-Jan-13 22:40 
QuestionNice one! My vote for 5. PinmemberZoltán Zörgő27-Dec-12 0:46 
GeneralMy vote of 5 Pinmemberbobfox25-Dec-12 14:05 
GeneralMy vote of 5 [modified] Pinmemberchess99926-Nov-12 0:31 
Questionfails to start the service PinmemberSharki129-Aug-12 19:56 
AnswerRe: fails to start the service PinmemberIvan Krivyakov30-Aug-12 3:13 
In order to provide a meaningful reply, you will have to be more specific. What exactly did you do and what error messages did you see?
 
Please don't forget that you must install the service first (ipwatchdog -i), and that you must have administrator privileges to do so.
GeneralRe: fails to start the service PinmemberSharki131-Aug-12 18:34 
GeneralRe: fails to start the service PinmemberSharki13-Sep-12 11:58 
GeneralRe: fails to start the service PinmemberIvan Krivyakov3-Sep-12 12:10 
GeneralRe: fails to start the service PinmemberSharki13-Sep-12 12:24 
QuestionCool PinmemberSharki121-Aug-12 20:39 
AnswerRe: Cool PinmemberIvan Krivyakov22-Aug-12 4:44 
BugThanks for your contribution PinmemberMember 26015087-Aug-12 0:27 
GeneralRe: Thanks for your contribution PinmemberIvan Krivyakov7-Aug-12 7:42 
GeneralRe: Thanks for your contribution PinmemberIvan Krivyakov14-Aug-12 15:01 
GeneralMy vote of 5 PinmemberCarsten V2.017-Jul-12 8:47 
Generalcan't be downloaded Pinmemberyunhai1317-Jul-12 4:42 
GeneralRe: can't be downloaded PinmemberIvan Krivyakov17-Jul-12 4:56 
GeneralRe: can't be downloaded PinmemberIvan Krivyakov17-Jul-12 6:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 14 Aug 2012
Article Copyright 2012 by Ivan Krivyakov
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid