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:
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
:
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:
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:
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:
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.
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)) {
_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" }
};
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 {
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();
}
internal ServiceConfig _getXmlConfig()
{
}
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
{
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();
}
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() { }
public static Log Instance
{
get
{ if (_instance == null)
_instance = new Log();
return _instance;
}
}
public void OpenLog()
{
string path = Assembly.GetExecutingAssembly().Location;
int pos = path.IndexOf(".exe");
path = path.Substring(0, pos);
pos = path.LastIndexOf('\\');
ModuleName = path.Substring(pos + 1);
DateTimeFormatInfo dfi = DateTimeFormatInfo.CurrentInfo;
DateTime dt = DateTime.Now;
int week = dfi.Calendar.GetWeekOfYear
(dt, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
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) OpenLog();
_streamWriter.BaseStream.Seek(0, SeekOrigin.End);
string time = "";
if (timeStamp)
time = DateTime.Now.ToString("G") + ", ";
_streamWriter.WriteLine(time + LogMsg);
}
public void Close()
{
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:
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)
{
EventLog.WriteEntry("Dings System Monitor Started.");
_paused = false;
_serviceThread = new ServiceThread();
_serviceThread.Start("Started");
}
protected override void OnStop()
{
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:
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
:
History
- 9th January, 2012 -- Original version posted