Using a Windows service in your .NET project part 7: Windows Service body part 1
October 27, 2014 3 Comments
Introduction
In the previous post we saw how to install and uninstall the Windows service of our demo project. Currently the service doesn’t do anything as there’s no method implementation anywhere but it’s good that we can safely install and uninstall it.
Let’s review what this service should be able to do and what dependencies it will need:
- It should periodically check the MongoDb database for new Http jobs. We have a service method for this in IHttpJobService
- It should then execute those jobs using an IHttpJobExecutionService
- Log the actions using an interface and implementation we haven’t seen yet
I need to stress the importance of logging in a Windows service. First of all it’s vital to log at least some activity in any real business application so that you can trace what your users are doing and find the source of exceptions. It’s even more important to have logging in place for an application with no user interface such as a Windows service. A Windows service cannot even send messages to a command prompt for you to see during testing. If you have no logging in place then you’ll be wondering what is going on if the service doesn’t perform its job.
We won’t build a full-blown logging system here, just a simplified file-based one. You can read more about logging in two blog posts: an introduction and a concrete example with log4net.
Logging
Let’s get this done first. We’ll reuse the logging interface from the logging intro referred to above. Open the Demo.Infrastructure project and add a new folder called Logging. Insert the following interface:
public interface ILoggingService { void LogInfo(object logSource, string message, Exception exception = null); void LogWarning(object logSource, string message, Exception exception = null); void LogError(object logSource, string message, Exception exception = null); void LogFatal(object logSource, string message, Exception exception = null); }
We’ll implement a home-made file-based logging service. Insert the following class to the Logging folder:
public class FileBasedLoggingService : ILoggingService { private readonly string _logFileFullPath; public FileBasedLoggingService(string logFileFullPath) { if (string.IsNullOrEmpty(logFileFullPath)) throw new ArgumentException("Log file full path"); FileInfo logFileInfo = new FileInfo(logFileFullPath); if (!logFileInfo.Exists) throw new ArgumentNullException("Log file does not exist"); _logFileFullPath = logFileFullPath; } public void LogInfo(object logSource, string message, Exception exception = null) { AppendMessage(logSource, message, "INFO", exception); } public void LogWarning(object logSource, string message, Exception exception = null) { AppendMessage(logSource, message, "WARNING", exception); } public void LogError(object logSource, string message, Exception exception = null) { AppendMessage(logSource, message, "ERROR", exception); } public void LogFatal(object logSource, string message, Exception exception = null) { AppendMessage(logSource, message, "FATAL", exception); } private void AppendMessage(object source, string message, string level, Exception exception) { try { File.AppendAllText(_logFileFullPath, FormatMessage(source, message, level, exception)); } catch { } } private string FormatMessage(object source, string message, string level, Exception exception) { return string.Concat(Environment.NewLine, DateTime.UtcNow.ToString(), ": source: ", source.ToString(), ", level: ", level, ", message: ", message, ", any exception: ", (exception == null ? "None" : exception.Message) ); } }
In a real application you’d use a professional tool to log to a file but that would unnecessarily sidetrack us now. Also, we could take the level of abstraction even further. Note that FileBasedLoggingService directly uses objects like File and FileInfo which make it difficult to test the methods in this class. A more complete solution would require us to separate out the concrete file-based operations to an abstraction. We’ll leave the implementation as it is for now. You can clean up this code if you’d like to as a homework. You can take a look at this post where I go through how to factor out file-related operations. It ties in well with what I’ve written in this paragraph.
Create a text file like “log.txt” somewhere on your hard drive and take note of its full path. Make sure that the file is editable by the service. For demo purposes you can give full access to Everyone so that we don’t need to trace any logging-related exceptions:
Note how we simply ignore any exceptions thrown within AppendMessage. This is called fire-and-forget. This strategy can be acceptable in logging as we don’t want the application to stop just because e.g. the log file is inaccessible. In a testing phase you can catch the exception and log it somewhere else, such as the event log, in case you don’t see the logging messages in the target file, here’s an example:
private void AppendMessage(object source, string message, string level, Exception exception) { try { File.AppendAllText(_logFileFullPath, FormatMessage(source, message, level, exception)); } catch (Exception ex) { string s = "HttpJobRunner"; string log = "Application"; if (!EventLog.SourceExists(s)) { EventLog.CreateEventSource(s, log); } EventLog.WriteEntry(s, "Exception in HttpJobRunner logging: " + ex.Message, EventLogEntryType.Error, 1234); } }
Let’s add this as a dependency to HttpJobRunner. At present HttpJobRunner- the partial class that inherits from ServiceBase – is quite empty with a constructor that calls upon InitializeComponent and two empty overridden methods.
Change the implementation as follows:
public partial class HttpJobRunner : ServiceBase { private readonly ILoggingService _loggingService; public HttpJobRunner(ILoggingService loggingService) { InitializeComponent(); if (loggingService == null) throw new ArgumentNullException("LoggingService"); _loggingService = loggingService; } protected override void OnStart(string[] args) { _loggingService.LogInfo(this, "HttpJobRunner starting up"); } protected override void OnStop() { _loggingService.LogInfo(this, "HttpJobRunner stopping"); } }
You’ll need to add a project reference to the Infrastructure layer for this to compile. We let an ILoggingService be injected to HttpJobRunner through its constructor. Then we simply take note of the Windows service starting up and shutting down in the log file. Build the project and you should get an exception from Program.cs in the Windows service layer: HttpJobRunner doesn’t have an empty constructor. Change the implementation as follows:
static void Main() { ServiceBase[] ServicesToRun; ServicesToRun = new ServiceBase[] { new HttpJobRunner(new FileBasedLoggingService(@"c:\logging\log.txt")) }; ServiceBase.Run(ServicesToRun); }
Make sure you insert the full path of the log file that you created before in the FileBasedLoggingService constructor.
Let’s see if this works. Rebuild the project and re-install the Windows service following the steps outlined in the previous part. Check the contents of the log file, there should be one entry similar to the following:
20/09/2014 20:07:25: source: Demo.WindowsService.HttpJobRunner, level: INFO, message: HttpJobRunner starting up, any exception: None
You can stop and/or restart the service in the Services window:
The log messages should be added to the log file accordingly, e.g.:
20/09/2014 21:00:42: source: Demo.WindowsService.HttpJobRunner, level: INFO, message: HttpJobRunner starting up, any exception: None
20/09/2014 21:01:02: source: Demo.WindowsService.HttpJobRunner, level: INFO, message: HttpJobRunner shutting down, any exception: None
20/09/2014 21:01:03: source: Demo.WindowsService.HttpJobRunner, level: INFO, message: HttpJobRunner starting up, any exception: None
Great, we have some logging in place.
Restarting a Windows service can have negative consequences in your application. If the service is carrying out a job without saving its current state somewhere then restarting the service will in effect kill the job. So at a minimum we want to know any time the service status has changed. Add the following overrides to HttpJobRunner:
protected override void OnShutdown() { _loggingService.LogInfo(this, "HttpJobRunner shutting down"); } protected override void OnContinue() { _loggingService.LogInfo(this, "HttpJobRunner continuing"); } protected override void OnPause() { _loggingService.LogInfo(this, "HttpJobRunner pausing"); }
This doesn’t of course solve the problem of interrupted jobs but at least we know that something noteworthy has happened which can be the cause of a failed job. It is your responsibility to make your service “clever” enough to pick up an interrupted job when it’s restarted. In our example the service could check all jobs that have been started but not yet finished and start from there – or simply restart the job from the beginning. However, that’s beyond the scope of this series, but it’s still good to know about this issue.
Before we finish this post let’s add one more method to HttpJobRunner. The goal is that the service is never restarted due to an unhandled exception. If there’s an uncaught exception somewhere during the code execution the the service will fail and act according to the failure policy, such as “Restart the service” in our case. However, as noted above service restarts can be detrimental. It’s not always possible to catch all exceptions in a large code base even if you put try-catch clauses everywhere.
There’s a solution to catch all uncaught exceptions in a Windows service though. I mentioned it already in this short post on threading. We’ll reuse much of it here.
Within the HttpJobRunner constructore start typing…
TaskScheduler.UnobservedTaskException +=
…and press Tab twice. This will add a default handler that’s called whenever an uncaught exception bubbles up:
void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { throw new NotImplementedException(); }
Insert the following implementation:
void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); AggregateException aggregateException = (e.Exception as AggregateException).Flatten(); aggregateException.Handle(ex => { return true; }); List<Exception> inners = aggregateException.InnerExceptions.ToList(); StringBuilder sb = new StringBuilder(); sb.Append("Unhandled exception caught in HttpJobRunner by the generic catch-all handler.") .Append(NL); if (inners != null && inners.Count > 0) { foreach (Exception inner in inners) { sb.Append("Exception: ").Append(inner.Message).Append(NL).Append("Stacktrace: ").Append(NL) .Append(inner.StackTrace).Append(NL); if (inner.InnerException != null) { Exception innerInner = inner.InnerException; sb.Append("Inner exception has also an inner exception. Message: ") .Append(innerInner.Message).Append(". ").Append(NL) .Append("Stacktrace: ").Append(innerInner.StackTrace); } sb.Append(NL).Append(NL); } } else { sb.Append("No inner exceptions.").Append(NL); sb.Append("Plain message: ").Append(aggregateException.Message).Append(NL); sb.Append("Stacktrace: ").Append(aggregateException.StackTrace).Append(NL); } _loggingService.LogFatal(this, sb.ToString()); }
…where “NL” refers to a private variable to shorten Environment.NewLine:
private readonly string NL = Environment.NewLine;
This is enough for now. We’ll continue with the service implementation in the next post.
View the list of posts on Architecture and Patterns here.
Pingback: Architecture and patterns | Michael's Excerpts
Hi Andras,
I want to say thank you. I really enjoy reading your blog.
This blog makes me a better developer!
Btw: This post is missing a link to the last part of the series.
Best regards from Germany
Ben
Hi Ben,
Thanks for your comment and kind words.
I’ve updated the post with the correct link.
//Andras