Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating a System Monitoring Service in .NET

0.00/5 (No votes)
9 Jan 2012 1  
A demonstration of a configurable Windows service with multiple tasks monitoring and logging

Introduction

Years ago, I posted the article An Implementation of System Monitor, which displays computer resource usages in a Windows form, similar to the Performance page in the Windows Task Manager. Sometimes, you might not want to watch your system all the time in UI. You probably need to record system data in the background, to monitor the records, and trigger some actions if necessary. This is another kind of application that we can implement as Windows services.

A Windows service is a long-running executable that runs in its own Windows session. The service can be automatically started at boot time, can be paused, resumed, and restarted, without any user interface. Windows services are ideal for lengthy periodically running tasks that do not interfere with the user working on the computer. The example can be monitoring local system resources or collecting and analyzing data from remote units.

You can create a service as a Microsoft Visual Studio project, defining code to control commands sent and actions taken to the commands. You can then use the Services Control Manager to start, stop, pause, resume, and configure your service. Different from other applications, the service executable must be installed before it can work. You must create an installation component for the service, which installs and registers the service, and creates its entry with the Services Control Manager. For details, refer to MSDN: Windows Service Applications.

Almost all applications require receiving some input to prepare data and states for a task. An application usually need to display their output to indicate results or exceptions. Because the Windows service is not interactive, a dialog raised within the service cannot be seen and may stop it responding. Usually, the service running states can be logged into the Windows Event Viewer, but such a log is quite limited and not easy to retrieve being saved. So the question is how to prepare a Windows service for the input data and how to receive its resulted outputs. This article will show you an example of a system monitoring service by demonstrating how to:

  • Configure the service behavior with input by using XmlSerializer,
  • Simulate tasks on the local system and the remote host in check cycles,
  • Log service polling results in a weekly generated log file,
  • Retrieve CPU, memory, and disk usages with the performance counter and WMI,
  • Handle the service events of Start, Stop, Pause and Resume.

As you can see, this is an actual and complete Windows service with simplified but meaningful operations. With this service skeleton, you can add your tasks with settings in configuration, replace with your business logic, and format your output of expected or alert messages. It should be easier to enhance and extend for your use.

Using System Monitor Service

Let's walk through the sample in a few steps. At first, you can download and run SysMonServiceSetup.msi available at the above Demo Installation link. After it is installed, you can find the service executable copied in the default folder, C:\Program Files (x86)\Dings Code Office\System Monitor Service:

SysMonService installed in the file folder

At this moment, you only see SysMonService.exe and SysMonService.InstallState, the second is generated at installation. The XML and log files will be generated after you start SysMonService. Now open the Services applet in the Administrative Tools and you can see this service entry named System Monitor Demo created. Then you click the Start link to start SysMonService:

SysMonService installed in the Services applet

Now when you go back to the System Monitor Service folder, you will find the configuration file SysMonService.xml and the log file SysMonService1Jan2012.log. Let's read SysMonService.xml that contains the default input data generated the first time when you start the service:

SysMonService the first SysMonService.xml

The first part is an array of hosts. To simulate network communications, I simply get the host name and collect its IP addresses for each host. The second part is for usage check of the local computer for CPU, memory, and disk space. You can turn on or off individual device and set its threshold in percent. The third are time controls in millisecond to set the interval of check cycles, the delay of service start and timeout of stop. The log file SysMonService1Jan2012.log contains the outputs corresponding to the said tasks. The following is an excerpt of one cycle:

====================================================
1/2/2012 3:51:26 PM, SysMonService Thread Started
Info: Service Configuration created
Thread Proc called
1/2/2012 3:51:31 PM, Timer Proc Started ... ...
------ Check Remote Hosts ------
Check www.google.com, Host name:www.l.google.com
IP: (74.125.224.212) (74.125.224.209) (74.125.224.211) (74.125.224.210) (74.125.224.208)
Check www.microsoft.com, Host name:lb1.www.ms.akadns.net
IP: (207.46.19.254)
Check www.cnn.com, Host name:www.cnn.com
IP: (157.166.255.19) (157.166.226.25) (157.166.226.26) (157.166.255.18)
----- Check Local Computer -----
CPU Used 3.00%
Physical Memory 40.43% (2.35 GB/5.80 GB) Over Threshold(30)
Virtual Memory 42.61% (4.94 GB/11.60 GB) Over Threshold(40)
Disk Space:
C: 30.43% (136.75 GB/449.47 GB)
D: 85.58% (13.69 GB/16.00 GB) Over Threshold(50)
1/2/2012 3:51:41 PM, Timer Proc Ended ... ...
---------------------------------------------------

As expected, we have received the host name and IPs for Google, Microsoft, and CNN. We show local resources and check their usages against thresholds. The task is performed every 2 minutes (120000 milliseconds).

Next, let's try to change the settings in SysMonService.xml to watch differences. For example, give some changes below:

SysMonService the SysMonService.xml changed

I made two hosts this time, one is ABC and the other an invalid URI. I disabled the virtual memory polling and reset the disk space threshold 90%. When stay a while and reopen SysMonService1Jan2012.log, the log would be like this:

---------------------------------------------------
1/2/2012 6:06:27 PM, Timer Proc Started ... ...
------ Check Remote Hosts ------
Check www.abc.com, Host name:abc.com
IP: (199.181.132.250)
Check invalidHost, Error: No such host is known
----- Check Local Computer -----
CPU Used 10.00%
Physical Memory 40.97% (2.38 GB/5.80 GB) Over Threshold(30)
Disk Space:
C: 30.42% (136.75 GB/449.47 GB)
D: 85.58% (13.69 GB/16.00 GB)
1/2/2012 6:06:30 PM, Timer Proc Ended ... ...
--------------------------------------------------- 

The output looks all right from our changed XML settings. But one thing does not take effect immediately. Notice that I set the check cycle to 1 minute (60000 milliseconds). When you examine the log file, you still see the 2-minute interval occurred there. Why is that? Because I haven't changed the interval within the timer procedure (although I could). You have to reset the timer by using Pause and Resume in the service applet to restart a thread procedure. I'll talk about this shortly.

Finally, you can find an entry to uninstall SysMonService that also has been created at installation and located in the Control Panel's Programs and Features:

SysMonService for uninstallation created

At this point, you might think of how to use nuts and bolts in C# to implement the service. I'll focus on these in the following sections. But at first, I assume that you have the knowledge of creating a Windows service project with its installation component in Visual Studio 2010. If not, MSDN provides a concise and pretty understandable Walkthrough: Creating a Windows Service Application in the Component Designer to read and practice. I won't talk about the service creation and installation here.

Configurable Service Settings

The configurable XML file is based on the following structure in ServiceConfig.cs:

public class ServiceConfig
{
    [XmlAttribute()]
    public string ServiceName;
    public int TimeCheckCycle { get; set; }
    public int TimeStartDelay { get; set; }
    public int TimeStopTimeout { get; set; }
    public string[] Hosts;
    public Usage[] Usages;
}

public enum DeviceType { CPU, PhysicalMemory, VirtualMemory, DiskSpace };
public class Usage
{
    [XmlAttribute()]
    public DeviceType DeviceID;
    [XmlAttribute()]
    public bool Enable;
    [XmlAttribute()]
    public double Threshold;
}

The ServiceConfig class represents the XML items accordingly in SysMonService.xml. I call the function _getXmlConfig() to access the XML settings. If SysMonService.xml does not exist, I create a default one, otherwise just retrieve its contents. This makes it possible for you to configure the XML items between polling cycles when the service is running. Obviously, any changes should follow the specification of ServiceConfig designed. The following _getXmlConfig() is called by the service thread procedure and the timer procedure as well.

// Get the configurations to prepare timer
internal ServiceConfig _getXmlConfig()
{
    string path = Assembly.GetExecutingAssembly().Location;
    int pos = path.IndexOf(".exe");
    path = path.Substring(0, pos) + ".xml";

    XmlSerializer x = new XmlSerializer(typeof(ServiceConfig));

    if (!File.Exists(path))   // Create XML at the first time
    {
        _config = new ServiceConfig
        {
            ServiceName = Log.Instance.ModuleName,
            TimeStartDelay = 2000, TimeCheckCycle = 120000, TimeStopTimeout = 5000,
            Hosts = new string[] {
                "www.google.com", "www.microsoft.com", "www.cnn.com" }
        };

        // DeviceType { CPU, PhysicalMemory, VirtualMemory, DiskSpace };
        double threshold = 10;
        Array ary = Enum.GetValues(typeof(DeviceType));
        _config.Usages = new Usage[ary.Length];
        foreach (DeviceType value in ary)
        {
            _config.Usages[(int)value]
                = new Usage { DeviceID = value, Enable = true, 
            Threshold = threshold += 10 };
        }

        TextWriter w = new StreamWriter(path);
        x.Serialize(w, _config);
        w.Close();
        Log.Instance.WriteLine("Info: Service Configuration created");
    }
    else    // XML configurations Exist
    {
        try
        {
            TextReader r = new StreamReader(path);
            _config = x.Deserialize(r) as ServiceConfig;
            r.Close();
            Log.Instance.WriteLine("Info: Service Configuration retrieved");
        }
        catch (Exception e)
        {
            Log.Instance.WriteLine("Error in XmlSerializer TextReader: " + e.Message);
        }
    }

    return _config;
}

Now I would like to lead you to the thread procedure that is in the ServiceThread class in ServiceThread.cs:

public class ServiceThread
{
    public void Start(string action)
    {
        Log.Instance.OpenLog();
        Log.Instance.WriteLine("===================================================");
        Log.Instance.WriteLine(Log.Instance.ModuleName + " Thread " +action, true);

        _thread = new Thread(ThreadProc);
        _config = _getXmlConfig();
        _thread.Start(this);
    }

    public void Stop(string action)
    {
        _myTimer.Dispose();
        Log.Instance.WriteLine(Log.Instance.ModuleName + " Thread " + action
            +(_thread.Join(_config.TimeStopTimeout) ? " OK" : " Timeout"), true);
        _config = null;
        Log.Instance.Close();
    }

    // Get the configurations to prepare timer
    internal ServiceConfig _getXmlConfig()
    {
        // ... See above
    }

    static void ThreadProc(object o)
    {
        Log.Instance.WriteLine("Thread Proc called");
        ServiceConfig cfg = (o as ServiceThread)._config;
        ServiceWorker sw = new ServiceWorker();
        _myTimer = new Timer(sw.TimerProc, o, cfg.TimeStartDelay, cfg.TimeCheckCycle);
    }

    Thread _thread;
    ServiceConfig _config;
    static Timer _myTimer;
}

ServiceThread has three member objects: _thread, _config, and _myTimer and provides the Start() and Stop() methods. When start the service, I create a thread, get the configuration, and trigger the ThreadProc(), where I delegate all the service tasks to the ServiceWorker object. To do this, I passed the ServiceWorker's TimerProc() to initialize _myTimer with XML time settings. In Stop(), I briefly wait _config.TimeStopTimeout for _thread end without caring synchronization mechanism here.

Defining Service Tasks Independently

One of the basic principles in software development is to separate the business logic from the application logic. As mentioned before, I define the ServiceWorker class as a highly cohesive business unit that is loosely coupled with the service application. So on the service side, there is nothing related to tasks performed in the modulized ServiceWorker. If necessary, ServiceWorker can be in a DLL when more complications introduced, such as Databases and communications. Currently, I only put a little stuff to make sense to this demo:

internal class ServiceWorker
{
    // This method is called by the timer delegate.
    internal void TimerProc(Object o)
    {
        Log.Instance.WriteLine("Timer Proc Started ... ...", true);
        ServiceConfig cfg = (o as ServiceThread)._getXmlConfig();

        string s = "";
        Log.Instance.WriteLine("------ Check Remote Hosts ------");
        foreach (string host in cfg.Hosts)
        {
            try
            {
                IPHostEntry hostInfo = Dns.GetHostEntry(host);
                Thread.Sleep(1000);
                Log.Instance.WriteLine
        ("Check " +host +", Host name:" + hostInfo.HostName);

                s = "";
                foreach (IPAddress ip in hostInfo.AddressList)
                    s += "(" +ip.ToString() +") ";
                Log.Instance.WriteLine("IP: " + s);
            }
            catch (Exception e)
            {
                Log.Instance.WriteLine("Check " +host + ", Error: " + e.Message);
            }
        }

        Log.Instance.WriteLine("----- Check Local Computer -----");
        foreach (Usage u in cfg.Usages)
        {
            if (!u.Enable)
                continue;

            switch (u.DeviceID)
            {
                case DeviceType.CPU:
                    double d = _SysData.GetProcessorData();
                    Log.Instance.WriteLine("CPU Used " + d.ToString("F") + "%"
                        + (d >= u.Threshold ? " Over Threshold
                (" + u.Threshold + ")" : ""));
                    break;
                case DeviceType.DiskSpace:
                    Log.Instance.WriteLine("Disk Space:");
                    foreach (SysValues v in _SysData.GetDiskSpaces())
                        LogSysValueWithUsage(v, u);
                    break;
                case DeviceType.PhysicalMemory:
                    LogSysValueWithUsage(_SysData.GetPhysicalMemory(), u);
                    break;
                case DeviceType.VirtualMemory:
                        LogSysValueWithUsage(_SysData.GetVirtualMemory(), u);
                    break;
            }
        }

        Log.Instance.WriteLine("Timer Proc Ended ... ...", true);
        Log.Instance.WriteLine("---------------------------------------------------");
        Log.Instance.Close();
    } // TimerProc

    void LogSysValueWithUsage(SysValues val, Usage usage)
    {
        double d = 100 * val.Used / val.Total;
        string s = (d >= usage.Threshold ? 
        " Over Threshold(" + usage.Threshold + ")" : "");
        Log.Instance.WriteLine(val.DeviceID + " " + d.ToString("F") + "% ("
            + FormatBytes(double.Parse(val.Used.ToString())) + "/"
            + FormatBytes(double.Parse(val.Total.ToString())) + ")" + s);
    }

    enum Unit { B, KB, MB, GB, TB, ER }
    string FormatBytes(double bytes)
    {
        int unit = 0;
        while (bytes > 1024)
        {
            bytes /= 1024;
            ++unit;
        }

        return bytes.ToString("F") +" "+ ((Unit)unit).ToString();
    }

    SystemData _SysData = new SystemData();
}

In TimerProc(), the first task (Check Remote Hosts) is trivial, just to collect the host name and IPs by calling Dns.GetHostEntry(). The second task (Check Local Computer) is involving the performance counter and WMI. Again, for logic separation, I create another class SystemData in SysData.cs to encapsulate the required method to retrieve CPU, memory, and disk usages. Part of this class is borrowed from my An Implementation of System Monitor. For simplicity, I won't talk more about it. You can read SysData.cs for details.

Notice that in the helper LogSysValueWithUsage(), I check the usage threshold. If a system value is higher, I make an indication in the log file. In the real application, you probably do more than that, such as writing the Database or sending an alert message.

Service Logging

We have already seen a lot of Log.Instance.WriteLine() calls in the code. The Log object is shown in ServiceLog.cs:

class Log
{
    Log() { }  // Constructor is 'private'

    public static Log Instance
    {
        get
        {   // Lazy initialization, this singleton is not thread safe.
            if (_instance == null)
                _instance = new Log();
            return _instance;
        }
    }

    public void OpenLog()
    {
        // Retrieve the module path
        string path = Assembly.GetExecutingAssembly().Location;
        int pos = path.IndexOf(".exe");
        path = path.Substring(0, pos);
        pos = path.LastIndexOf('\\');
        ModuleName = path.Substring(pos + 1);

        // Get the week of the year
        DateTimeFormatInfo dfi = DateTimeFormatInfo.CurrentInfo;
        DateTime dt = DateTime.Now;
        int week = dfi.Calendar.GetWeekOfYear
        (dt, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);

        // Create one log file per week
        path += week + dt.ToString("MMMyyyy") + ".log";
        FileMode fm = File.Exists(path) ? FileMode.Append : FileMode.CreateNew;
        _fileStream = new FileStream(path, fm, FileAccess.Write, FileShare.Read);
        _streamWriter = new StreamWriter(_fileStream);
    }

    public void WriteLine(string LogMsg, bool timeStamp=false)
    {
        if (_streamWriter == null) // Lazy initialization,
            OpenLog();

        _streamWriter.BaseStream.Seek(0, SeekOrigin.End);

        string time = "";
        if (timeStamp)
            time = DateTime.Now.ToString("G") + ", ";
        _streamWriter.WriteLine(time + LogMsg);
    }

    public void Close()
    {
        // Cleanup for _streamWriter and _fileStream
        if (_streamWriter != null)
        {
            _streamWriter.Close();
            _streamWriter = null;
        }

        if (_fileStream != null)
        {
            _fileStream.Close();
            _fileStream = null;
        }
    }

    public string ModuleName { get; set; }

    static Log _instance;
    FileStream _fileStream;
    StreamWriter _streamWriter;
}

As you can see, the Log object is a singleton. It's not thread safe, but enough for this demo. Worthy of mentioning is the method OpenLog(). In OpenLog(), I get a week number of the year and construct the current log name. If this file exists, append the log; otherwise, create a new weekly log file to write. This makes about 52 files a year to avoid large amount of logging in one file. Notice that OpenLog() is called when the service starts. However, it's also getting called every time in Log's WriteLine() if _streamWriter is null. This is a lazy initialization happened when a new check cycle begins in TimerProc(). In order not to leave the log file opened between cycles, I call Log's Close() at the end of TimerProc() that makes _streamWriter null as a flag.

Handling Service Events

Finally, let's go back to the service application itself to deal with service events. In this service, I purposely enable the Pause and Resume commands by setting CanPauseAndContinue true. So after you start the service, open the service Properties dialog, you can see the Pause button enabled and if you pause it, the Resume button will be enabled:

SysMonService Properties dialog

Our SysMonService class, the main service engine, is solely responsible to handle four events below:

public partial class SysMonService : ServiceBase
{
    public SysMonService()
    {
        InitializeComponent();
        this.AutoLog = false;
        this.CanPauseAndContinue = true;
        _paused = false;
    }

    protected override void OnStart(string[] args)
    {
        // to Application event logs
        EventLog.WriteEntry("Dings System Monitor Started.");

        _paused = false;
        _serviceThread = new ServiceThread();
        _serviceThread.Start("Started");
    }

    protected override void OnStop()
    {
        // to Application event logs
        EventLog.WriteEntry("Dings System Monitor Stopped.");

        if (!_paused)
            _serviceThread.Stop("Stopped");
    }

    protected override void OnPause()
    {
        EventLog.WriteEntry("Dings System Monitor Paused.");

        _serviceThread.Stop("Paused");
        _paused = true;
    }

    protected override void OnContinue()
    {
        EventLog.WriteEntry("Dings System Monitor Resumed.");

        _paused = false;
        _serviceThread = new ServiceThread();
        _serviceThread.Start("Resumed");
    }

    ServiceThread _serviceThread;
    bool _paused;
}

As shown above, I simply play with ServiceBase's EventLog object without defining a custom EventLog object (although I could). I disabled the service's AutoLog and call EventLog's WriteEntry(). This way, you can see the event log info in the Event Viewer when you pause this service:

SysMonService paused in the Event Viewer

Since we already have the ServiceThread class described before, I only need to create such an object member as _serviceThread and delegate all jobs to it by calling Start() and Stop(). Also, I simply reuse this logic to handle Pause in OnPause() and Resume in OnContinue(). Attention to two points here: I use the _paused flag to avoid repeated calling Stop() if the user wants to stop the service when it's already paused. Second, I pass an action string to Start() and Stop() to differentiate the call context in our own log file. Notice that I only initialize the time interval in ThreadProc(), thus any change of TimeCheckCycle in SysMonService.xml will not take effect until the service restarts or resumes, which answers the question in the previous section.

One reason I implement the Pause and Resume handlers is for service debugging. From MSDN How to: Debug Windows Service Applications, we know that it's not easy to debug the OnStart() method. However, you can catch the break point in OnPause() and OnContinue() without problem.

Summary

Using Microsoft Visual Studio or Microsoft .NET Framework SDK, we can easily create a service and its installation component. But the real-world service designing, programming, and debugging are a little bit challenging. This article mainly focused on the service IO specifics with a sample demo in C#. The sample could be a tutorial to the beginners since I presented some .NET programming skills and explained in details. Based on my experience, the Windows service development is really an interesting area but requires some different thought from other applications.

One useful tool I haven't mentioned is the ServiceController class that can be used to control and access services in your own application, instead of relying on the Services Control Manager to start and stop. This can be another topic in an article. At this moment, I just include a console program GetSystemMonitorService in source code that simply display a few properties of this demo service using ServiceController:

SysMonService paused in the Event Viewer

History

  • 9th January, 2012 -- Original version posted

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here