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.
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:
- Check current external IP by reading it from checkip.dyndns.org.
- Compare to previous value of IP.
- If different, send notification e-mail.
- 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(); 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.
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.
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:
Summary of Command Line Switches
ipwatchdog -?
prints summary of all available switches. Currently supported switches are:
Short Form | Long Form | Meaning |
-c | -console | Run in console mode |
-i | -install | Install the service |
-p | -stop | Stop running service |
-s | -start | Start installed service |
-u | -uninstall | Uninstall 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).