简介:这是一套面向Windows运维场景的实时监测源码项目,基于C#与.NET Framework 4开发,使用Visual Studio 2022即可打开并编译运行。核心功能是持续监测Windows服务、IIS网站及应用程序池的运行状态,一旦发现异常停止便会自动执行重启操作,适合在业务故障根因尚未明确时作为临时救场工具,能显著降低服务中断影响,并为后续排查留出时间。资源压缩包约373KB,共70个文件:其中23个cs格式的C#源码构成主体,4个exe为可直接运行的监测程序,3个dll为运行时库,另有config、xml、resx等配置与资源文件,以及sln、csproj、README等工程说明文件;目录包含Model、Kernal、AllForms、Global等模块,项目结构完整清晰。程序内既提供开箱即用的监测工具,也封装了Windows服务与IIS操作帮助类、文件操作类、日志操作类,方便在项目基础上进行二次开发。目前已有114人学习下载,适合运维人员应急使用,也适合C# WinForm开发者参考学习。
1. 服务停止后的临时救场:Windows服务与IIS站点监测的选型思考
业务网站凌晨挂着,Windows 服务不知什么时候变成了 Stopped,等发现时业务已经停了一两个小时。这类故障往往不是根因导向的——重启就能恢复,但没人能 7x24 小时盯着服务列表。这个基于 C# .NET Framework 4 的 Winform 源码项目,用一套轮询机制实时监测 Windows 服务和 IIS 网站及应用程序池的运行状态,一旦检测到停止,就自动执行重启操作。它解决的是运维中"暂时查不出根因、但业务不能停"的中间阶段,适合现场运维快速部署,也适合想学习服务操作、IIS 管理、日志与文件帮助类写法的 .NET 开发者。下文直接从源码工程结构拆起,把它的监测逻辑、配置方式和二次开发路径逐一讲透。
2. ServiceCheck源码工程的目录拆解与启动调用链
解压 servicecheck.zip 后,解决方案里包含ServiceCheck.sln、.vs目录和ServiceCheck项目文件夹,还有 README.md。真正要关注的源码都在ServiceCheck项目下,其中Kernal(注意拼写是 Kernal,不是 Kernel)存放核心监测与操作逻辑,AllForms存放 Winform 窗体,Model存放实体类,Image是 UI 用到的图标资源。这个目录划分对小项目来说有一个明确的好处:界面、实体、操作逻辑三层分开,二次开发时改监测逻辑不用碰窗体代码。
2.1 解决方案结构:从 sln 到 Kernal,哪些目录需要改
从项目正文可以看到完整的目录清单,下面按职责分组:
ServiceCheck.sln:Visual Studio 2022 解决方案文件,双击打开即可还原整个工程ServiceCheck.csproj/ServiceCheck.csproj.user:项目文件与用户级设置,打包发布时主要操作前者Program.cs:Winform 入口,负责启动主窗体app.config:配置监测项与轮询参数,编译后复制为ServiceCheck.exe.configKernal:核心业务逻辑,包含对 Windows 服务和 IIS 操作的帮助类Model:数据实体,例如监测目标的状态模型AllForms:主窗体和配置窗体Image:窗体中的状态图标、按钮图片Properties:程序集信息与资源文件obj/bin:编译中间目录和输出目录,发布时只取 bin 下的 exe 与 config
项目采用 .NET Framework 4,意味着在没有安装更高运行时版本的 Windows Server 2008 R2 / 2012 上也能直接运行,这点对存量服务器环境比较友好。如果你用 Visual Studio 2022 打开,需要确认已安装 ".NET 桌面开发" 工作负载,否则加载 csproj 时会提示目标框架不受支持。
2.2 Program.cs 入口与 Winform 主循环
Program.cs 是标准 Winform 入口,核心代码通常是这样:
using System; using System.Windows.Forms; namespace ServiceCheck { internal static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } } }这段代码有两个关键点。[STAThread]指定线程模型为单线程单元,Winform 拖拽控件、剪贴板操作以及通过 Com 组件访问 IIS 元数据库时都依赖这个模型;如果缺失,部分环境会抛出ThreadStateException。Application.Run启动了消息循环,程序的 UI 事件和System.Windows.Forms.Timer都会在这个循环里执行。
2.3 核心轮询调度如何串起服务、站点与应用池
监测调度的入口在Kernal目录中。常见做法是定义一个监测管理类,持有监测目标列表和一个定时器,每次定时器触发时遍历列表,先查 Windows 服务状态,再查 IIS 站点状态和应用程序池状态,发现异常后调用对应的重启方法。这种把所有检查放在一个类里的方式,对小型项目很实用,切换监测项时只需要改 app.config,不需要改编译后的逻辑。
需要注意的是:System.Windows.Forms.Timer的回调发生在 UI 线程,如果监测项多或者 ServerManager 实例化较慢,界面会卡顿;工程里更多是用System.Threading.Timer或后台线程做轮询,然后通过Invoke更新界面状态。后续第 4 章会给出主循环的具体写法。
3. Windows服务与IIS操作帮助类的实现细节
这一章回答项目中最核心的问题:如何用代码判断服务停了、如何拉起服务、如何检测 IIS 站点和应用池的状态。两类操作走的是完全不同的 API,Windows 服务用System.ServiceProcess命名空间下的ServiceController即可,而 IIS 站点和应用池在 IIS 7+ 环境推荐用Microsoft.Web.Administration.ServerManager,它既支持站点启停,也支持应用池回收。
3.1 ServiceController 封装:状态读取、启动与超时控制
ServiceController是 .NET Framework 自带的类,不需要额外安装 SDK。封装时至少要考虑三个方法:查询状态、启动服务、停止服务。下面是在工程里最常见的封装方式:
using System; using System.ServiceProcess; namespace ServiceCheck.Kernal { public class ServiceHelper { public static bool IsRunning(string serviceName) { using (var controller = new ServiceController(serviceName)) { return controller.Status == ServiceControllerStatus.Running; } } public static bool Restart(string serviceName, int timeoutSeconds) { try { using (var controller = new ServiceController(serviceName)) { if (controller.Status == ServiceControllerStatus.Running) { controller.Stop(); controller.WaitForStatus( ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(timeoutSeconds)); } controller.Start(); controller.WaitForStatus( ServiceControllerStatus.Running, TimeSpan.FromSeconds(timeoutSeconds)); return controller.Status == ServiceControllerStatus.Running; } } catch (Exception ex) { // 记录日志后返回失败,由上层决定是否重试 return false; } } } }这里有一个容易被忽略的细节:new ServiceController(serviceName)在服务名不存在时不会立即抛异常,而是延迟到第一次访问Status时才抛Win32Exception。所以Restart中把整段逻辑包在 try 里是必要的,不能只保护Start那一行。TimeoutException是另一个高频异常,当服务在指定时间内没有进入目标状态时发生,timeoutSeconds 设为 30 比较常见,但如果被监测的服务启动本身就需要 40 秒,那就应该调大这个值,否则会出现启停超时被误判为失败的case。
Stop时如果服务当前处于Stopped状态,直接调用会抛异常,所以上面的代码先判断Running再 Stop。Start时同理,如果服务已经在启动过程中,Start会抛InvalidOperationException,此时等待状态确认比直接抛错更合适。更健壮的写法是把Start后等待状态变更放在一个循环里,这里不再展开。
3.2 ServerManager 封装:IIS站点与应用池的重启接口
IIS 操作是这个项目里技术含量最高的部分。Microsoft.Web.Administration.ServerManager需要引用Microsoft.Web.Administration.dll,该 DLL 在安装 IIS 管理工具后位于%windir%\system32\inetsrv\下,也可以从 NuGet 获取。每次new ServerManager()会读取 IIS 配置并建立对象模型,实例化开销比普通类大,所以不建议在循环体里频繁创建。下面是站点和应用池的封装:
using System; using System.Linq; using Microsoft.Web.Administration; namespace ServiceCheck.Kernal { public class IisHelper { public static bool SiteExists(string siteName) { using (var sm = new ServerManager()) { return sm.Sites.Any(s => s.Name == siteName); } } public static bool RestartSite(string siteName) { using (var sm = new ServerManager()) { var site = sm.Sites.FirstOrDefault(s => s.Name == siteName); if (site == null) { return false; } site.Stop(); site.Start(); return site.State == ObjectState.Started; } } public static bool RestartAppPool(string poolName) { using (var sm = new ServerManager()) { var pool = sm.ApplicationPools.FirstOrDefault(p => p.Name == poolName); if (pool == null) { return false; } pool.Stop(); pool.Start(); return pool.State == ObjectState.Started; } } } }站点启动和应用程序池启动是两回事:站点停掉不一定导致应用池停止,应用池停止则站点必然无法响应。实际部署中,有些业务是站点本身 unstable,有些是应用池崩溃,所以配置里要区分两个监测项。监控应用池时,不需要把站点对应关系硬编码在代码里,可以直接通过site.Applications[0].ApplicationPoolName拿到站点关联的池名称,做一次自动映射。
ObjectState.Started是ServerManager对已启动状态的枚举值,这里对比的是site.State。需要注意,site.Stop()后紧接着site.Start()是同步方法,正常情况下立即返回,但某些老 IIS 版本上会遇到状态未及时刷新的情况,返回的State可能还是 Stopped。遇到这种问题,可以在 Stop/Start 之间加一个短暂延时,或者用轮询方式等待状态稳定。源码项目里如果直接复用了这个方法,在 IIS 8/10 上通常没问题,但部署到 IIS 7 时要关注这个差异。
3.3 连续失败阈值:避免服务启动过程中的误重启
如果每次轮询发现服务不是 Running 状态就立刻重启,会踩到一个典型的坑:服务本身正在启动过程中,Status返回的是StartPending,这时候去执行Start(),会抛InvalidOperationException。更稳妥的办法是引入"连续失败次数"机制,第一次监测到异常只计数、不处理,连续 N 次仍异常才触发重启,同时成功一次就清零计数。
private readonly Dictionary<string, int> _failureCount = new Dictionary<string, int>(); private bool ShouldRestart(string key, int threshold) { if (!_failureCount.ContainsKey(key)) { _failureCount[key] = 1; return false; } _failureCount[key]++; return _failureCount[key] >= threshold; }这个方法的原理很简单:每次检查不通过时调用ShouldRestart,返回 false 就只写入日志并等待下一轮,返回 true 才执行重启并把计数清零。阈值设置为 2 到 3 比较常见,因为一次轮询失败可能只是网络瞬间抖动或 ServerManager 实例化失败。另一个相关问题是监测间隔和阈值要配合:间隔 30 秒、阈值 3,意味着服务停止后最多 90 秒内会被拉起,这个时间窗口要在部署时告知业务方。某些数据库类型的 Windows 服务启动耗时较长,重启后第一次轮询大概率还是非 Running 状态,阈值 1 会导致反复二次重启,这是这个项目二次开发时最值得调整的参数。
4. app.config 配置驱动的监测系统:参数表、主循环与部署验证
把监测目标写在代码里是运维的噩梦,所以这个工程使用 app.config 作为配置中心。编译部署后,配置文件被复制为ServiceCheck.exe.config,运维人员可以直接编辑 XML 来调整监测项,无需重新编译。下面先给出完整的配置项设计,再看主循环怎么写。
4.1 配置项结构与参数说明
一个常见的app.config结构如下:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="CheckInterval" value="30" /> <add key="TimeoutSeconds" value="30" /> <add key="RestartThreshold" value="3" /> <add key="WatchServices" value="W3SVC,MSSQLSERVER" /> <add key="WatchSites" value="Default Web Site" /> <add key="WatchAppPools" value="DefaultAppPool" /> </appSettings> </configuration>各配置项说明如下:
| 配置键 | 类型 | 示例值 | 说明 |
|---|---|---|---|
| CheckInterval | int | 30 | 轮询间隔,单位秒,决定服务停止后多久被发现 |
| TimeoutSeconds | int | 30 | 单次停止/启动操作的超时时间,单位秒 |
| RestartThreshold | int | 3 | 连续失败达到该次数才执行重启,防止误判 |
| WatchServices | string | W3SVC,MSSQLSERVER | 需要监测的 Windows 服务名,逗号分隔 |
| WatchSites | string | Default Web Site | 需要监测的 IIS 站点名,逗号分隔 |
| WatchAppPools | string | DefaultAppPool | 需要监测的应用程序池,逗号分隔 |
这里的WatchServices使用的是服务名(Service Name),不是显示名(Display Name)。两者通常一致,但有些服务如"Windows Update"服务名是wuauserv,显示名却是"Windows Update",填错会导致找不到服务。拿到一台新服务器时,建议先在 PowerShell 里执行Get-Service确认服务名到底是什么。
4.2 Timer 轮询主循环与线程安全
读取配置后,主循环使用System.Threading.Timer而不是System.Windows.Forms.Timer,目的是把监测工作放在后台线程,避免 UI 卡顿。同时要加一个防重入标记,因为CheckInterval如果设置得很短,上一次检查还没结束,下一次回调就可能启动:
private System.Threading.Timer _timer; private int _isChecking; public void Start() { var interval = int.Parse(ConfigurationManager.AppSettings["CheckInterval"] ?? "30"); _timer = new System.Threading.Timer(MonitorTick, null, TimeSpan.Zero, TimeSpan.FromSeconds(interval)); } private void MonitorTick(object state) { if (Interlocked.Exchange(ref _isChecking, 1) == 1) { return; } try { CheckServices(); CheckSites(); CheckAppPools(); } finally { Interlocked.Exchange(ref _isChecking, 0); } }Interlocked.Exchange是原子操作,前提是字段类型为 int。这段代码的作用是:当线程 A 还在执行检查、线程 B 又触发回调时,B 发现_isChecking已经被置为 1,直接返回,避免并发检查带来的重复重启。CheckServices内部遍历WatchServices,对每个服务调用ServiceHelper.IsRunning,失败时累加计数并决定是否重启;CheckSites和CheckAppPools同理。
如果被监测的服务器是单核低配机器,且直接复用了 ServerManager,每次new ServerManager()读取配置文件的耗时可能达到几百毫秒,监测项一多压力会明显上升。这时可以把ServerManager实例复用到单次检查周期中,即一个MonitorTick里只创建一次实例,然后同时查站点和应用池。要注意的是,ServerManager在长时间持有后可能读到缓存配置,所以不建议跨多个轮询周期复用同一个实例。
4.3 部署验证与异常排查
部署时不可直接双击 exe。IIS 操作和ServiceController都有权限要求,必须以管理员身份运行。工程输出目录下的ServiceCheck.exe右键"以管理员身份运行",或通过批处理配合runas启动。下面是验证顺序:
- 打开窗体,确认界面上的监测项列表与 app.config 中的配置一致
- 手动停止一个测试服务,等待
CheckInterval * RestartThreshold秒,观察服务是否自动拉起 - 在 IIS 管理器中停止一个站点,确认站点状态恢复为 Started
- 停止一个应用程序池,观察应用池是否被自动启动
如果在验证过程中没有生效,优先查看日志文件。日志输出到Logs目录下(具体以工程中 LogHelper 的实现为准),里面会记录每次状态检查的结果、异常信息以及重启动作。常见的两种异常是"拒绝访问"和"找不到服务名"。拒绝访问说明当前进程没有管理员权限,或者用户不属于 Administrators 组;找不到服务名则说明配置里填的是显示名而非服务名。
提示:从 Visual Studio 按 F5 调试时,默认不启用管理员权限,需要在项目中添加 app.manifest 并将
requestedExecutionLevel设为requireAdministrator,或直接以管理员身份打开 VS 再调试。否则访问部分服务时会抛出Win32Exception。
5. 二次开发切入点:日志按天轮转、异常通知与监测目标扩展
这个项目能直接用于生产,但真正提升运维效率的是把日志、通知和配置管理补完整。下面分享几个常见的改造方向,每个都是独立的小功能,不需要大改核心结构。
5.1 日志帮助类的按天轮转改造
原工程的 LogHelper 如果只往单个 Log.txt 里写,运行几个月后文件会膨胀,排查问题时按时间定位也很麻烦。常见做法是改造为按日期生成文件,同时加线程锁:
private static readonly object LockObj = new object(); public static void WriteInfo(string message) { lock (LockObj) { var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); var logFile = Path.Combine(logDir, DateTime.Now.ToString("yyyy-MM-dd") + ".log"); Directory.CreateDirectory(logDir); File.AppendAllText(logFile, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [INFO] {message}{Environment.NewLine}"); } }按天切分后,运维可以按日期归档旧日志,也方便和 IIS 日志分析工具配合使用——当应用池重启记录与站点访问日志的停止时间对得上,判断根因就更有依据。文件锁是必须的吗?如果只有 Timer 的一个线程在写,不加锁也能跑,但监测逻辑一旦扩展出邮件通知、手动刷新按钮等入口,多线程写日志的风险就会出现,提前加锁成本最低。
5.2 告警通知
自动重启是兜底,但不通知人就等于没有监控。改造方式是在Restart方法执行完成后追加一个通知方法,按优先级选择邮件、企业微信或钉钉机器人 Webhook。Webhook 通知只需要一个 HTTP POST,不需要短信网关,接入成本最低。注意把 Webhook 地址放在 app.config 里,与现有配置风格保持一致。
5.3 监测目标从配置到服务端下发
管理 5 台机器时手动改配置文件还能接受,数量上来后就需要把监测清单从配置文件中解放出来。常见的做法是让程序启动时先请求运维平台获取监测目标列表,请求失败时回落到本地配置。改造时把WatchServices字符串拆成List<string>,通过 JSON 序列化与外部系统交互。这与源码中 Model 目录的定位一致,新增一个MonitorTarget实体类保存服务名、类型、重启阈值等字段,监测调度逻辑不需要跟着改。
5.4 验证改造后逻辑的正确性
改造完不是编译通过就结束。建议在测试服务器上注册两个测试 Windows 服务,推荐用 NSSM 把普通 exe 包成服务,一个设置为"自动启动但运行 5 秒后退出",另一个保持正常运行。同时建一个测试站点,通过应用程序池的"特定时间回收"功能制造一次状态变更,观察监测程序是否正确识别、重启,并核对日志中记录的重启时间和服务实际恢复时间是否吻合。观察 30 分钟后,根据日志中的失败记录调整RestartThreshold和CheckInterval的配合值。
本文还有配套的精品资源,点击获取