65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中创建系统监控服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (17投票s)

2012年1月9日

CPOL

10分钟阅读

viewsIcon

139852

downloadIcon

11751

演示了一个可配置的 Windows 服务,具有多个任务监控和日志记录功能。

引言

几年前,我发布了文章系统监视器的实现,它在一个 Windows 窗体中显示计算机资源使用情况,类似于 Windows 任务管理器中的性能页面。有时,您可能不想一直在 UI 中监视您的系统。您可能需要在后台记录系统数据,以监视记录,并在必要时触发一些操作。这是我们可以作为 Windows 服务实现的另一种应用程序。

Windows 服务是一个长时间运行的可执行文件,它在自己的 Windows 会话中运行。该服务可以在启动时自动启动,可以暂停、恢复和重新启动,而无需任何用户界面。Windows 服务非常适合不干扰用户在计算机上工作的长时间周期性运行任务。例如,监视本地系统资源或从远程单元收集和分析数据。

您可以将服务创建为 Microsoft Visual Studio 项目,定义代码以控制发送的命令和对命令采取的操作。然后,您可以使用服务控制管理器来启动、停止、暂停、恢复和配置您的服务。与其他应用程序不同,服务可执行文件必须先安装才能工作。您必须为服务创建一个安装组件,该组件安装并注册服务,并使用服务控制管理器创建其条目。有关详细信息,请参阅MSDN:Windows 服务应用程序

几乎所有应用程序都要求接收一些输入来为任务准备数据和状态。应用程序通常需要显示其输出来指示结果或异常。由于 Windows 服务不是交互式的,因此在服务中弹出的对话框无法看到,并且可能会使其停止响应。通常,服务的运行状态可以记录到 Windows 事件查看器中,但此类日志非常有限,并且不容易检索。因此,问题是如何为输入数据准备 Windows 服务以及如何接收其结果输出。本文将通过演示如何实现以下功能,向您展示一个系统监控服务的示例:

  • 使用 XmlSerializer 通过输入配置服务行为,
  • 在检查周期内模拟本地系统和远程主机上的任务,
  • 将服务轮询结果记录到每周生成的日志文件中,
  • 使用性能计数器和 WMI 检索 CPU、内存和磁盘使用情况,
  • 处理服务的启动、停止、暂停和恢复事件。

如您所见,这是一个真实而完整的 Windows 服务,具有简化但有意义的操作。通过这个服务骨架,您可以在配置中添加您的任务设置,替换您的业务逻辑,并格式化您预期或警报消息的输出。它应该更容易增强和扩展以供您使用。

使用系统监控服务

让我们通过几个步骤来演示示例。首先,您可以下载并运行上面演示安装链接中提供的 SysMonServiceSetup.msi。安装后,您可以在默认文件夹中找到复制的服务可执行文件,即 C:\Program Files (x86)\Dings Code Office\System Monitor Service

SysMonService installed in the file folder

此时,您只能看到 SysMonService.exeSysMonService.InstallState,第二个是在安装时生成的。XML 和日志文件将在您启动 SysMonService 后生成。现在打开“管理工具”中的“服务”小程序,您可以看到创建的名为“系统监视器演示”的服务条目。然后单击“启动”链接以启动 SysMonService

SysMonService installed in the Services applet

现在,当您回到系统监控服务文件夹时,您将找到配置文件 SysMonService.xml 和日志文件 SysMonService1Jan2012.log。让我们阅读 SysMonService.xml,它包含您第一次启动服务时生成的默认输入数据

SysMonService the first SysMonService.xml

第一部分是主机数组。为了模拟网络通信,我只是获取主机名并收集每个主机的 IP 地址。第二部分用于检查本地计算机的 CPU、内存和磁盘空间使用情况。您可以打开或关闭单个设备并设置其百分比阈值。第三部分是毫秒级的时间控制,用于设置检查周期的间隔、服务启动延迟和停止超时。日志文件 SysMonService1Jan2012.log 包含与上述任务对应的输出。以下是一个周期摘录:

====================================================
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 ... ...
---------------------------------------------------

正如预期,我们收到了 Google、Microsoft 和 CNN 的主机名和 IP。我们显示本地资源并根据阈值检查其使用情况。任务每 2 分钟(120000 毫秒)执行一次。

接下来,我们尝试更改 SysMonService.xml 中的设置以查看差异。例如,进行以下更改:

SysMonService the SysMonService.xml changed

这次我创建了两个主机,一个是 ABC,另一个是无效 URI。我禁用了虚拟内存轮询并将磁盘空间阈值重置为 90%。稍等片刻,重新打开 SysMonService1Jan2012.log,日志将如下所示:

---------------------------------------------------
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 ... ...
--------------------------------------------------- 

输出看起来与我们更改的 XML 设置一致。但有一件事并没有立即生效。请注意,我将检查周期设置为 1 分钟(60000 毫秒)。当您检查日志文件时,仍然会看到 2 分钟的间隔。这是为什么呢?因为我还没有在计时器过程中更改间隔(尽管我可以)。您必须通过在服务小程序中使用“暂停”和“恢复”来重置计时器,以重新启动线程过程。我稍后会讨论这个问题。

最后,您可以在“控制面板”的“程序和功能”中找到卸载 SysMonService 的条目,该条目也在安装时创建。

SysMonService for uninstallation created

此时,您可能会想到如何在 C# 中使用各种组件来实现该服务。我将在以下部分重点介绍这些内容。但首先,我假设您了解如何在 Visual Studio 2010 中创建带有安装组件的 Windows 服务项目。如果不是,MSDN 提供了一个简洁且易于理解的演练:在组件设计器中创建 Windows 服务应用程序以供阅读和实践。我在这里不会讨论服务的创建和安装。

可配置的服务设置

可配置的 XML 文件基于 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;
}

ServiceConfig 类相应地表示 SysMonService.xml 中的 XML 项。我调用 _getXmlConfig() 函数来访问 XML 设置。如果 SysMonService.xml 不存在,我将创建一个默认文件,否则只检索其内容。这使得您可以在服务运行时在轮询周期之间配置 XML 项。显然,任何更改都应遵循 ServiceConfig 设计的规范。以下 _getXmlConfig() 由服务线程过程和计时器过程调用。

// 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;
}

现在我想引导您了解 ServiceThread.csServiceThread 类中的线程过程。

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 有三个成员对象:_thread_config_myTimer,并提供 Start()Stop() 方法。启动服务时,我创建一个线程,获取配置,并触发 ThreadProc(),在那里我将所有服务任务委托给 ServiceWorker 对象。为此,我将 ServiceWorkerTimerProc() 传递给 _myTimer,并使用 XML 时间设置初始化 _myTimer。在 Stop() 中,我简单地等待 _config.TimeStopTimeout,以使 _thread 结束,此处不关心同步机制。

独立定义服务任务

软件开发的基本原则之一是将业务逻辑与应用程序逻辑分离。如前所述,我将 ServiceWorker 类定义为一个高内聚的业务单元,它与服务应用程序松散耦合。因此,在服务端,与模块化的 ServiceWorker 中执行的任务没有任何关系。如有必要,当引入更多复杂性(例如数据库和通信)时,ServiceWorker 可以放在 DLL 中。目前,我只放了一些小东西来使这个演示有意义。

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();
}

TimerProc() 中,第一个任务(检查远程主机)很简单,只是通过调用 Dns.GetHostEntry() 收集主机名和 IP。第二个任务(检查本地计算机)涉及性能计数器和 WMI。同样,为了逻辑分离,我在 SysData.cs 中创建了另一个类 SystemData,以封装检索 CPU、内存和磁盘使用情况所需的方法。此类的部分内容借鉴自我的系统监控器实现。为简单起见,我不会再详细讨论它。您可以阅读 SysData.cs 获取详细信息。

请注意,在辅助函数 LogSysValueWithUsage() 中,我检查了使用阈值。如果系统值较高,我会在日志文件中做出指示。在实际应用中,您可能会做得更多,例如写入数据库或发送警报消息。

服务日志记录

我们已经在代码中看到了很多 Log.Instance.WriteLine() 调用。Log 对象显示在 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;
}

如您所见,Log 对象是一个单例。它不是线程安全的,但对于此演示来说足够了。值得一提的是 OpenLog() 方法。在 OpenLog() 中,我获取一年中的周数并构造当前日志名称。如果此文件存在,则追加日志;否则,创建一个新的每周日志文件进行写入。这使得一年大约有 52 个文件,以避免在一个文件中记录大量日志。请注意,OpenLog() 在服务启动时被调用。但是,如果 _streamWriternull,它也会在 LogWriteLine() 中每次被调用。这是一个延迟初始化,发生在 TimerProc() 中一个新的检查周期开始时。为了避免日志文件在周期之间保持打开,我在 TimerProc() 结束时调用 LogClose(),这将 _streamWriter 设置为 null 作为标志。

处理服务事件

最后,让我们回到服务应用程序本身来处理服务事件。在这个服务中,我特意通过设置 CanPauseAndContinuetrue 来启用“暂停”和“恢复”命令。因此,在您启动服务后,打开服务“属性”对话框,您可以看到“暂停”按钮已启用,如果您暂停它,“恢复”按钮将启用。

SysMonService Properties dialog

我们的 SysMonService 类,即主服务引擎,全权负责处理以下四个事件:

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;
}

如上所示,我只是使用 ServiceBaseEventLog 对象,而没有定义自定义的 EventLog 对象(尽管我可以)。我禁用了服务的 AutoLog 并调用了 EventLogWriteEntry()。这样,当您暂停此服务时,可以在事件查看器中看到事件日志信息。

SysMonService paused in the Event Viewer

由于我们之前已经描述了 ServiceThread 类,我只需要创建一个这样的对象成员 _serviceThread,并通过调用 Start()Stop() 将所有工作委托给它。此外,我只需重用此逻辑来处理 OnPause() 中的暂停和 OnContinue() 中的恢复。这里要注意两点:我使用 _paused 标志来避免在用户在服务已暂停时停止服务时重复调用 Stop()。其次,我将一个动作字符串传递给 Start()Stop(),以区分我们自己的日志文件中的调用上下文。请注意,我只在 ThreadProc() 中初始化时间间隔,因此 SysMonService.xmlTimeCheckCycle 的任何更改都不会生效,直到服务重新启动或恢复,这回答了上一节中的问题。

我实现暂停和恢复处理程序的一个原因是用于服务调试。从 MSDN 如何:调试 Windows 服务应用程序中,我们知道调试 OnStart() 方法并不容易。但是,您可以在 OnPause()OnContinue() 中捕获断点而没有问题。

摘要

使用 Microsoft Visual Studio 或 Microsoft .NET Framework SDK,我们可以轻松地创建一个服务及其安装组件。但实际的服务设计、编程和调试则有点挑战性。本文主要侧重于服务 IO 的具体细节,并提供了一个 C# 示例演示。该示例可以作为初学者的教程,因为我介绍了一些 .NET 编程技巧并进行了详细解释。根据我的经验,Windows 服务开发确实是一个有趣的领域,但需要与其他应用程序不同的思维方式。

我没有提及的一个有用工具是 ServiceController 类,它可以在您自己的应用程序中用于控制和访问服务,而不是依赖于服务控制管理器来启动和停止。这可以成为另一篇文章的主题。目前,我只是在源代码中包含了一个控制台程序 GetSystemMonitorService,它简单地使用 ServiceController 显示此演示服务的一些属性。

SysMonService paused in the Event Viewer

历史

  • 2012 年 1 月 9 日 -- 发布原始版本
© . All rights reserved.