产线上位机的崩溃和办公软件不是一个性质:没人在屏幕前点"确定",崩了之后数据断采、画面冻死,可能几小时后才被发现;更糟的是 Windows 弹出错误报告对话框,进程挂在那儿不退出,连重启都不会触发。一套合格的崩溃防护要解决四件事:异常有兜底、死前留证据、系统能自恢复、恢复后状态安全。这篇按这四层给完整方案和代码。
一、先认清:.NET 有三个未处理异常入口,少注册一个就漏一片
| 入口 | 捕获范围 | 进程能否继续 |
|---|---|---|
| Application.ThreadException | UI 线程(消息循环)上未被 catch 的异常 | 能,吞掉后消息循环继续 |
| AppDomain.UnhandledException | 所有线程的未处理异常(含后台线程、线程池) | 不能,事件结束后进程默认终止 |
| TaskScheduler.UnobservedTaskException | async/await 与 Task 中未观察的异常(GC 回收 Task 时触发) | 能,设置 Observed 可防进程崩溃 |
▲ 只注册 ThreadException 是新手最常见错误:采集线程、定时器回调里的异常根本不走它,直接闪退。
Program.cs 里的标准注册三件套(必须在任何窗体/业务代码之前):
// ① UI线程异常:可恢复,记录后让界面继续跑 Application.ThreadException += (s, e) => { CrashGuard.Report("UI", e.Exception, recoverable: true); }; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); // ② 非UI线程未处理异常:默认致命,这里是"留遗言"的最后机会 AppDomain.CurrentDomain.UnhandledException += (s, e) => { CrashGuard.Report("AppDomain", e.ExceptionObject as Exception, recoverable: false); Thread.Sleep(800); // 等日志刷盘 }; // ③ Task未观察异常:标记为已观察,避免后台任务偶发异常拖垮进程 TaskScheduler.UnobservedTaskException += (s, e) => { CrashGuard.Report("Task", e.Exception, recoverable: true); e.SetObserved(); };
二、死前留证:崩溃报告要让远程的人能定位
UnhandledException 里能执行的时间只有几百毫秒到几秒,别做网络请求,只做最稳的本地写盘。报告内容清单:
- 异常全文:ToString() 含完整堆栈和内部异常链,一行都不能截;
- 环境快照:程序版本(从 assembly 读)、操作系统、.NET 版本、机器名、崩溃时间;
- 最近日志尾巴:把内存环形日志的最后 200 行附进报告(所以日志器要有内存缓冲,光写文件遇到磁盘问题就瞎了);
- 业务现场:当前工单号、各设备连接状态、采集水位——这几样是判断"崩溃时在干什么"的关键;
- 可选 dump:疑难问题用 procdump 注册为实时调试器(
procdump -ma -i),崩溃时自动落完整 dump,平时不开,dump 文件大且涉及现场数据。
报告文件命名 crash_20260913_031255_机器名.txt,单独目录、最多保留 20 个;程序每次启动时检查上次是否异常退出(见后文标记文件),把积压报告在启动后网络空闲时打包上报服务器。
三、关掉 Windows 错误报告:无人值守的必做项
.NET 程序遇到致命异常,系统默认可能弹出"程序已停止工作"对话框并挂起进程——操作工不会点,看门狗发现进程还"活着"也不会重启,系统就此冻死。必须在部署时关掉 WER 弹窗(注册表,安装包里执行):
// 注册表路径 HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting // DontShowUI = 1 (DWORD),同时可设 Disabled = 1 // 效果:崩溃直接退出、不弹窗,看门狗才能接管
四、看门狗:独立进程,心跳判定,崩溃即拉起
看门狗的关键原则是它必须独立于主程序进程——主程序自己卡死时,内部任何定时器都不会再触发。我们用一个几十 KB 的控制台小程序 Watchdog.exe,开机自启,职责只有三件事:启动主程序、监控心跳、异常时重启。
// Watchdog 核心循环(独立进程) while (true) { var procs = Process.GetProcessesByName("ScadaMain"); var stale = procs.Length == 0 || (DateTime.Now - File.GetLastWriteTime(heartPath)).TotalSeconds > 15; if (stale) { procs.ToList().ForEach(p => { p.Kill(); p.WaitForExit(5000); }); if (RecentRestarts() < 3) Process.Start(exePath); else Alarm("主程序1分钟内连续崩溃3次,已停止自动重启"); } Thread.Sleep(5000); }
shutdown.flag,看门狗看到标记不重启并自己待命;主程序被看门狗拉起时携带命令行参数 --watchdog-restart,用于区分"人工启动"和"崩溃恢复",恢复策略不同。五、重启之后:状态恢复的安全原则
能自动重启不等于能自动接着干。工业现场的恢复必须保守,三条原则:
- 采集可以自动恢复,控制指令不能自动补发。重启后自动重连设备、恢复数据采集没问题;但崩溃前未完成的下发指令(配方、启停)一律作废,不猜不补,由 PLC 侧或人工确认后重新下发——程序无法知道指令执行到了哪一步;
- 工单状态要对账。启动时发现"进行中"的工单,读 PLC/设备侧实际状态对账,界面弹醒目提示"程序异常退出后恢复,请确认工单 X 当前状态",确认前只读不写;
- 首屏必须显示崩溃事实。带 --watchdog-restart 启动时主界面顶部挂红色横幅"系统于 03:12 异常退出后自动恢复,报告已生成",直到操作员确认。自动恢复要让人知道它发生过——静默恢复会掩盖系统性故障。
最后把整套体系的设计哲学说清楚:兜底不是为了让程序永不崩,而是让崩溃的代价可控。异常分层处理,能恢复的就地恢复、不能恢复的体面退出;退出前把证据留在本地;独立看门狗保证服务尽快回来;回来后用保守策略恢复现场。配合日志系统(现场证据链)和掉电检查点(数据不丢),上位机才能真正达到产线要求的"无人值守、长期运行"。
