资讯详情

C# Winform实时监测Windows服务与IIS站点并自动重启的源码解析

📅 2026/10/1 1:45:04 | 华诺云谱 👁 阅读
C# Winform实时监测Windows服务与IIS站点并自动重启的源码解析
简介面向Windows运维与开发人员的实时监测源码项目用于解决业务网站或Windows服务因不确定因素停止后需手动重启的痛点可在服务或IIS网站/应用程序池停止时自动拉起。项目基于C#语言、.NET Framework4开发使用Visual Studio 2022构建同时提供Winform窗体程序、IIS与Windows服务操作帮助类、文件操作类、日志操作类等内容既可直接用于项目运维也适合二次开发和Winform入门学习。压缩包共70个文件约373KB以C#源码为主包含23个cs、4个exe可执行程序、4个pdb调试文件、3个dll类库以及配置文件、资源文件等目录涵盖ServiceCheck.sln工程、Kernal核心模块、Model与AllForms界面等结构清晰便于扩展。已有114人学习下载适合需要快速搭建自动重启机制或研究C# Winform项目结构的开发者参考。1. 服务一停就自动拉起这套 C# Winform 实时监测源码项目能干什么做运维最难受的时刻不是业务彻底崩了而是业务网站或 Windows 服务半夜悄悄停了第二天被人叫醒上去右键重启一下它又活了。这种“又好了”的状态最悬不修它早晚再犯修你又不知道下一个稳定运行周期能撑多久。Windows 服务、IIS 网站和应用程序池实时监测源码项目就是在这个场景下出现的——它是一个基于 C#、.NET Framework 4 开发的 Winform 程序盯住 Windows 服务和 IIS 站点/应用程序池一旦发现停止就自动拉起先保住业务再给你留出排查根因的时间。项目本身是完整源码工程也附带可直接运行的程序适合正在做运维值班的人也适合想学 WinformIIS 交互的二次开发者。下面我从项目结构开始拆尽量让新手能照着跑让熟手能直接复制项目里的帮助类逻辑。2. 拆开 ServiceCheck.sln项目骨架、核心类与三类帮助类的落位2.1 Program.cs 与解决方案结构Winform 工程的实际启动路径刚拿到压缩包时我习惯先按 SERVICE 目录里有没有被塞进没用的发布物。展开 servicecheck 能看到.vs、ServiceCheck.sln、ServiceCheck.csproj、app.config、Model、Kernal、Properties、Program.cs、Image、AllForms、Global等目录和文件。从命名就能猜出作者把代码按职责分成了窗体放 AllForms业务逻辑放 Kernal数据模型放 Model全局配置放 Global。项目根目录下的ServiceCheck.sln是唯一建议用 Visual Studio 2022 直接打开的文件不要只开 csproj否则容易出现依赖上下文丢失的问题。通常 Winform 项目的入口在 Program.cs打开后逻辑相近核心是启动主窗体并启用视觉样式using System; using System.Windows.Forms; namespace ServiceCheck { internal static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // MainForm 是 AllForms 命名空间下的主窗体 Application.Run(new AllForms.MainForm()); } } }这段代码里值得留意的是[STAThread]特性它声明当前线程的 COM 线程模型为单线程单元。Winform 的剪贴板、拖拽、系统对话框大量依赖 STAThread去掉后轻则功能异常重则直接崩溃。Application.Run会把主窗体放到消息循环中后续监测逻辑如果直接放在 UI 线程里会卡界面所以项目里真正做实时监测的模块通常在 Load 事件或后台线程中启动。主窗体启动后一般会读取app.config里的服务列表、站点列表、轮询间隔等参数。App.config是 .NET Framework 项目的标准配置文件编译后会变成ServiceCheck.exe.config。如果你在二次开发时新增了配置项记得同时改 csproj 里该文件的“复制到输出目录”属性否则 debug 目录下不更新改了半天不生效。2.2 Model 与 Kernal监测对象的数据模型和核心逻辑层Model 目录里一般放着服务监测对象、站点监测对象、应用池监测对象。这些模型的核心作用不是“存对象”而是把每个被监控项的状态、上次重启时间、冷却截止时间、重启次数记下来方便核心逻辑判断“这次要不要拉起”。常见做法是定义一个基类或公共属性然后各自继承扩展。namespace ServiceCheck.Model { public class MonitorItemBase { public string Name { get; set; } // 服务名或站点名 public bool EnableAutoRestart { get; set; } // 是否允许自动重启 public DateTime LastRestartTime { get; set; } public int RestartCount { get; set; } public string Remark { get; set; } } public class ServiceMonitorItem : MonitorItemBase { public string ServiceName { get; set; } // 用于 ServiceController public string DisplayName { get; set; } // 界面展示用 } }这里的LastRestartTime和RestartCount不能省。很多第一次做自动拉起的人只判断是不是 Stopped忽略了上一次重启时间结果服务刚启动还在进入 Running 的过程中又被监测逻辑发现状态非 Running形成无限重启。把这两个字段放进模型核心逻辑每次触发前先做冷却判断才能避免“拉起→崩溃→再拉起”的恶性循环。Kernal 目录在主线程调用层和系统 API 之间做桥接。比如服务监测逻辑里会调用System.ServiceProcess.ServiceControllerIIS 监测会调用Microsoft.Web.Administration.ServerManager还有一些基础文件操作和日志写入。这个项目的帮助类大致可以按下面这张表理解帮助类/模块职责关键能力Windows 服务操作类查询、启动、停止服务获取 ServiceControllerStatus、WaitForStatus、StartIIS 操作类查询站点和应用程序池读取 Site.State、ApplicationPool.State、启动/回收文件操作类读写配置文件、持久化状态追加日志、保存上次重启状态日志操作类记录监测和重启行为按天生成文件记录时间、动作、对象名2.3 文件日志类多线程安全写入和按天归档日志类虽然不起眼但在自动拉起场景里就是“后悔药”。没日志时服务夜里被拉起来多少次、因为什么没人知道有了日志第二天至少能确定“它确实停了也确实被拉起来了”。文件日志常见的实现是每次写入前先拿到当前日期按天生成文件名同时用锁保证多线程下不会互相覆盖。using System; using System.IO; namespace ServiceCheck.Kernal { public static class LogHelper { private static readonly object _lock new object(); public static void Write(string message) { lock (_lock) { string logDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logs); Directory.CreateDirectory(logDir); string today DateTime.Now.ToString(yyyyMMdd); string logFile Path.Combine(logDir, today .log); string line $[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}; File.AppendAllText(logFile, line Environment.NewLine); } } } }lock很重要。如果监测逻辑用后台线程轮询多个服务多个线程同时写同一个日志文件会出现互相覆盖或 IOException。这里用全局锁把写入串行化是最朴素但最有效的做法。File.AppendAllText默认使用 UTF-8 无 BOM在中英文混写时比 StreamWriter 更稳。日志目录放在程序运行目录下的 logs 文件夹部署时不需要额外建目录代码里会Directory.CreateDirectory自动创建。在这个项目中日志不只是给人看的它还承担“自检”职能。如果监测程序本身的权限连 logs 目录都写不进去日志会静默失败这时候你以为程序在跑其实已经死了。这一点留给后续二次开发时考虑后面避坑章节会展开。3. Windows 服务监测ServiceController 轮询、重启与防抖参数设计3.1 轮询还是事件驱动为什么实时监测通常用轮询很多同学第一个问题是Windows 服务停止时能不能像 RabbitMQ 那样收到事件通知现实是服务控制管理器SCM没有为普通进程提供订阅“服务状态变化”的 .NET 事件接口可用的方案基本只有两种写一个 Windows 服务自己调用WaitForChanged之类的机制或者定时轮询。对于这个源码项目的定位轮询是最容易落地、最可控的方案。Winform 里做轮询有几种常见做法System.Windows.Forms.Timer、System.Threading.Timer、后台Threadwhile循环。前一种只能跑在 UI 线程回调里做跨线程访问控件方便但如果监测对象多或单次查询耗时界面会掉帧甚至卡死。更稳的做法是启动一个后台线程每轮遍历服务列表查完状态后更新 UI更新时用BeginInvoke回到 UI 线程。轮询间隔通常在 5 到 15 秒之间具体值取决于服务启动时间和业务容忍度。3.2 核心监测逻辑判断 Stopped 状态并触发自动重启项目最核心的监测代码大致是这样的思路遍历要监测的服务列表用ServiceController打开目标服务检查当前状态如果是Stopped或StopPending就进入自动重启分支。注意StopPending一定要和Stopped一起处理否则你会在服务停止过程中反复执行 Start导致系统提示“服务正在停止或启动中”。using System.ServiceProcess; using ServiceCheck.Model; using ServiceCheck.Kernal; private void CheckWindowsService(ServiceMonitorItem item) { try { using (ServiceController sc new ServiceController(item.ServiceName)) { bool needRestart sc.Status ServiceControllerStatus.Stopped || sc.Status ServiceControllerStatus.StopPending; if (!needRestart) return; // 冷却期内跳过避免反复拉起 if ((DateTime.Now - item.LastRestartTime).TotalSeconds 60) return; sc.Start(); item.LastRestartTime DateTime.Now; item.RestartCount; LogHelper.Write($服务 [{item.ServiceName}] 状态为 {sc.Status}触发自动重启); } } catch (InvalidOperationException ex) { LogHelper.Write($服务 [{item.ServiceName}] 不存在/不可访问{ex.Message}); } catch (Exception ex) { LogHelper.Write($服务 [{item.ServiceName}] 自动重启异常{ex.Message}); } }这段代码里ServiceController的构造函数参数item.ServiceName是服务的“系统名称”不是显示名。例如“Print Spooler”服务的系统名是Spooler写显示名会抛InvalidOperationException。ServiceControllerStatus.StopPending表示服务正在停止过程中此时再 Start 大概率失败所以要先等它彻底停。代码里的冷却时间 60 秒是我偏保守的取值如果服务本身启动只需要 3 秒可以把冷却时间下调到 20 秒但不要低于两倍启动时间否则还是容易在 StartPending 阶段重复触发。using块保证 ServiceController 访问完后释放资源。这个很重要因为 ServiceController 底层持有 SCM 句柄频繁创建不释放会造成句柄泄漏监控程序跑几天后会越来越慢。另外要注意的是在以管理员身份运行时Start()才不会被拒绝如果监控程序是普通进程启动服务Exception分支就会把“拒绝访问”打进日志而不是真正拉起服务。3.3 自动重启策略防抖、次数上限和依赖顺序裸调 Start 很天真。实际生产场景里一个服务可能是连数据库、连消息队列的启动后需要几秒到几十秒进入可用状态如果没到稳定期又崩了应该把自动重启次数限制住而不是无限拉起。我一般会在核心逻辑外再加一层“限流”判断同一个服务在 1 小时内最多重启 N 次超过后只记录日志并提示人工介入。private bool AllowRestart(ServiceMonitorItem item, int maxTimesPerHour) { if (item.RestartCount maxTimesPerHour) { LogHelper.Write($服务 [{item.ServiceName}] 在 1 小时内重启次数达到 {maxTimesPerHour}停止自动拉起等待人工处理); return false; } // 等到服务真正进入 Running 再放开后续巡检 using (ServiceController sc new ServiceController(item.ServiceName)) { try { sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(30)); } catch (System.ServiceProcess.TimeoutException) { LogHelper.Write($服务 [{item.ServiceName}] 在 30 秒内未进入 Running仍保持自动监控); return true; } } return true; }WaitForStatus的第一个参数是目标状态第二个参数是超时时间这里设置 30 秒。要注意WaitForStatus会阻塞当前线程不要放在 UI 线程里调用。如果超时会抛出System.ServiceProcess.TimeoutException捕获后不要误判为成功但可以按“允许下一次拉起重试”处理。很多服务的重启本身就是要等它先进入 Running 才能保证后续依赖它的人不报错这个细节是这个项目里能看出来的运维经验不能只发指令还要确认结果。依赖顺序也是常见问题。例如 A 服务依赖 B 服务B 停了A 也会跟着停。如果监测逻辑把 A 和 B 同时拉起来A 会先启动失败然后被冷却期限制造成假死。我一般会在配置里增加Dependencies数组轮询时先拉依赖项再拉被依赖项或者干脆在程序逻辑里只监听根服务子服务跟着根服务一起启。当前项目提供了服务监测帮助类但依赖关系处理需要二次开发人员按实际业务补充。4. IIS 网站与应用程序池监测ServerManager 与 appcmd 双通道自愈4.1 应用程序池监测ServerManager 遍历与 ObjectState 判断IIS 的监测和 Windows 服务不太一样。IIS 7 及以上版本提供了Microsoft.Web.Administration.dll用ServerManager可以直接读取站点和应用池状态不需要调用 WMI 或解析 html。程序集默认在C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll在 Visual Studio 里添加引用时路径要选system32\inetsrv下的版本不要从 GAC 里误选。用这个 API 遍历应用池并自动启动的核心逻辑如下using Microsoft.Web.Administration; private void CheckApplicationPools() { using (ServerManager sm new ServerManager()) { foreach (ApplicationPool pool in sm.ApplicationPools) { if (pool.State ! ObjectState.Stopped) continue; // 如果池被配置为不自启动跳过不要强行拉 if (!pool.AutoStart) { LogHelper.Write($应用池 [{pool.Name}] 被配置为 AutoStartfalse未执行自动启动); continue; } LogHelper.Write($应用池 [{pool.Name}] 当前状态{pool.State}尝试启动); pool.Start(); sm.CommitChanges(); } } }关键点是ApplicationPool.State的类型是ObjectState不是ServiceControllerStatus包括Started、Stopping、Stopped、Starting和Unknown。只判断Stopped是合理的起点Stopping和Starting都不要碰。pool.Start()之后必须调用sm.CommitChanges()否则改动只停留在内存里不会真正写入 IIS 配置。很多初学 IIS 管理 API 的人就是漏了这一步代码跑起来没有报错但 IIS 管理器里状态纹丝不动。AutoStart参数值得注意。有些管理员会主动停掉某些池防止临时维护的站点被自动拉起抢资源。如果监控程序无脑拉所有池会和人工维护策略打架。所以建议在遍历时加上AutoStart判断只有允许自启动的池才自动拉起。这段代码把状态、启动权限、日志全部落进去是项目里可以直接复用的一段干货。4.2 网站监测先拉应用池再拉站点网站的停止往往不是站点本身被手动停止而是它绑定的应用程序池崩了导致站点进入Stopped。所以监控网站时不能只调site.Start()要先检查该站点关联的池把池拉起来再拉站点。用ServerManager找到站点和池对应关系的逻辑如下using Microsoft.Web.Administration; private void CheckSites() { using (ServerManager sm new ServerManager()) { foreach (Site site in sm.Sites) { if (site.State ! ObjectState.Stopped) continue; // 找到该站点根应用对应的应用池名 string poolName site.Applications[/].ApplicationPoolName; ApplicationPool pool sm.ApplicationPools[poolName]; if (pool ! null pool.State ObjectState.Stopped) { LogHelper.Write($站点 [{site.Name}] 关联应用池 [{poolName}] 已停止先启动应用池); pool.Start(); sm.CommitChanges(); } site.Start(); sm.CommitChanges(); LogHelper.Write($站点 [{site.Name}] 自动启动完成); } } }这里site.Applications[/]取的是站点根应用默认路径是/。如果站点下面挂了多个应用且每个应用用不同池就需要遍历site.Applications取出所有池名而不是只取根应用。另外site.Start()也可能因为绑定端口被占用、证书失效等原因失败所以外面最好包一层try-catch把失败原因写进日志不要只写“启动完成”。端口冲突是 IIS 排查里的高频问题一个站点用到 80 或 443 时和另一个站点的绑定冲突会导致 Start 失败这在监控软件中只能记录不能自动解决。4.3 用 appcmd 做人工核实与故障确认用ServerManager做程序化检查之外我也会用 appcmd 做人工验证。appcmd 是 IIS 自带的命令行工具路径在C:\Windows\System32\inetsrv\appcmd.exe必须在管理员权限的 CMD 或 PowerShell 里执行。下面是我常用的一组命令C:\Windows\System32\inetsrv\appcmd list sites C:\Windows\System32\inetsrv\appcmd list apppool C:\Windows\System32\inetsrv\appcmd start site Default Web Site C:\Windows\System32\inetsrv\appcmd start apppool /apppool.name:DefaultAppPoollist命令会显示站点/池名和当前状态常用于对照程序日志判断自动拉起是否生效。start site的第二个参数是站点名不是 URLstart apppool要用/apppool.name:语法。appcmd 对中文站点名的支持在部分 Windows 版本上存在乱码问题如果站点名是中文程序日志里最好记录站点 ID 而不是只记名称否则核对时会找不到对应项。需要注意的是 appcmd 只能操作本机 IIS不能跨服务器管理。如果监控程序部署在 IIS 所在的同一台机器上用 appcmd 没问题如果要远程监测多台服务器建议改用 Microsoft.Web.Administration 的远程连接模式或者用 WMI 配合IIsWebService枚举。这个项目的定位是“本机实时监测”所以 appcmd 足够。5. 自动拉起服务的常见坑权限、轮询间隔与 IIS 假死的排查5.1 坑一ServiceController.Start() 抛“无法打开服务”现象监控程序启动后日志里出现InvalidOperationException: Cannot open Service ...服务没有自动拉起。原因ServiceController.Start()要求当前进程对目标服务有启动权限。如果你的监控程序是普通双击运行的当前用户即便属于 Users 组也没有对服务的 SCM 启动权限。另外还有一个隐蔽原因Visual Studio 调试时如果以 x86 编译运行在 64 位系统上访问 64 位服务时句柄类型不匹配也会抛出类似错误。解决在 Visual Studio 里以 AnyCPU 编译项目部署到生产环境后右键 exe选择“以管理员身份运行”如果需要完全无感运行可以把监控程序注册成 Windows 计划任务并使用“最高权限”完成或者通过sc.exe sdset给指定服务 ACL 增加启动权限但生产环境不建议轻易改服务安全描述符。5.2 坑二服务显示“已停止”但进程还活着重启后提示“服务实例已存在”现象服务状态为Stopped但任务管理器里对应 exe 还在。自动重启执行Start()后报错服务管理器里状态变成StartPending后卡住。原因服务在收到停止命令后主线程没有在指定时间内退出SCM 已经把它标记为停止了但进程还没释放资源此时再启动同一个进程操作系统会阻止重复实例。解决在启动前等待进程完全退出或者先调用Stop()再等待Stopped。类似的代码可以做成帮助类方法private void ForceRestart(ServiceController sc) { try { sc.Stop(); sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)); } catch (Exception) { // 已停止或正在停止时 Stop 会抛异常忽略即可 } sc.Start(); sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(30)); }这里的两次等待都不能少。第一次等Stopped确保进程退出第二次等Running确保服务真正起来后才结束本次监测。如果第一次等待超时说明服务的 Stop 流程卡住需要人工看 exe 是否挂死不能直接暴力杀掉。5.3 坑三轮询间隔太短导致服务反复重启现象监控程序把服务拉起来后服务正常运行十几秒又停了日志显示间隔很短又触发一次自动拉起。原因服务每次重启都需要一个预热过程在StartPending或Stopped状态之间会短暂出现非Running状态。如果轮询间隔小于服务启动时间监测线程会误判为“又停了”从而触发下一次启动。解决把轮询间隔设为该服务正常启动耗时的 2 到 3 倍同时必须保留“最近一次重启时间”的冷却判断。我常用的配置是轮询间隔 15 秒冷却时间 60 秒最大重启次数每小时 5 次。如果服务启动本身要 2 分钟就用 15 秒轮询但冷却设为 120 秒。关键是冷却时间必须大于服务启动时间否则上面代码里的if ((DateTime.Now - item.LastRestartTime).TotalSeconds 60)会直接把第二次重启让过去。5.4 坑四IIS 应用程序池被 Rapid-Fail Protection 锁死自动启动无效现象应用池状态显示Stopped但pool.Start()调用后不报错CommitChanges()也成功状态仍是Stopped或在很短时间内再次停止。原因IIS 的快速失败保护机制会在短时间内多次崩溃后禁用应用池默认 30 秒内失败 5 次就自动停止池。普通Start()会被该保护机制继续拦截。解决先用ServerManager读取池配置确认pool.Failure.RapidFailProtection如果开启了快速失败保护要么在监控程序里忽略该池要么在自动启动前把保护次数调大。更稳妥的做法是让监控程序在启动池前先检查pool.Failure下的FailureCount和FailureInterval属性超过阈值时只记录日志不强行启动避免掩盖程序集 Bug 导致的频繁崩溃。if (pool.Failure.RapidFailProtection) { LogHelper.Write($应用池 [{pool.Name}] 启用了快速失败保护失败计数{pool.Failure.FailureCount}本次不自动启动); continue; }这段代码的作用是把“池崩溃”和“池被停用”区分开。如果池是因为真崩溃被保护机制停掉强行拉起只会让它在几秒内再挂掉还会攒错误日志。此时优先查事件查看器的 “应用程序池” 分类看崩溃前最后一条错误确定是代码问题还是资源问题。5.5 坑五日志目录没有写权限监控程序“静默失效”现象程序看起来在运行但一直没看到日志手动停掉一个服务也不拉起来重启系统后监控也没有恢复。原因程序运行目录在C:\Program Files\xxx下普通权限写不进去File.AppendAllText抛出的异常在主线程中被吞掉或者 Winform 程序未捕获异常导致线程崩溃。解决日志目录不要用程序运行目录改用C:\ProgramData\ServiceCheck\logs第一次运行用代码创建并给 Everyone 或 Users 写权限在程序启动时先写一条 “servicecheck monitor started” 日志用来自检。如果没有这条日志说明程序本身没跑起来或日志路径有问题第一时间就能察觉。6. 二次开发技巧把监测列表外置到 App.config并做一次自愈演练这个项目既然预留了二次开发空间那最重要的事就是把可变动项从代码中抽出来。服务名、站点名、轮询间隔、冷却时间、最大重启次数这些都应该是配置项而不是散落在硬编码里。我一般会在app.config里加如下配置appSettings add keyPollingIntervalSeconds value15 / add keyCoolDownSeconds value60 / add keyMaxRestartPerHour value5 / add keyMonitorServices valueSpooler;W3SVC / add keyMonitorSites valueDefault Web Site;AdminSite / add keyMonitorAppPools valueDefaultAppPool;AdminPool / /appSettings然后在代码里通过ConfigurationManager.AppSettings读取。注意MonitorServices用分号分隔解析时用Split(;)并过滤空项。这样换一台服务器时只需要改配置文件不需要重新编译 exe。更进阶一点的做法是把配置放在一个独立的 XML 文件里由程序热加载但我个人经验是 Winform 监控工具追求稳定性和清晰度App.config 足够。验证方法也很关键。我曾经犯过一个错误把轮询间隔改成 2 秒以为监测频率越高越好结果一台生产服务的重启计数一晚上涨了 300 多次。从那以后我每次改完轮询间隔或自动重启策略都强制走一遍完整演练——手动停止服务 → 观察日志确认自动拉起 → 再停止 → 确认冷却生效 → 查看重启次数和日志时间戳。这个流程不需要复杂测试框架只需要 Windows 服务管理器和日志文件就能完成。同样的方法也适用于 IIS在 IIS 管理器里点“停止”站点或应用池看监控程序是否在预期时间内把它拉起来并在日志里找到对应记录。这份源码项目的价值就在于帮类、日志和态机都已经被拆好你不需要从零开始造轮子。希望能够帮到你把那个“半夜把人叫醒”的救场问题彻底变成下班后不用管的事。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑