如何在程序出现错误后关闭并重新启动

这里有一个文章,介绍了如何实现在MFC程序出现致命错误的时候用户有机会保存数据:

QWhat is the best way to implement a scheme to catch any uncaught exception in an MFC app so the user can get a last opportunity to save data before the program does an emergency exit?

Allan Bommer

AGood question! There are a couple of different ways to do this, and C++ itself even has a mechanism. A little-known feature of C++ is the "terminate" function. C++ calls this special function whenever a program throws an exception for which there is no catch handler. The terminate function always has a void signature-no arguments and no return value. For example, you could write your own terminate function like this:

 void my_terminate()
{
   printf("Help me, I'm dying./n");
   // Save docs now
}

Then, you can make your function the global terminate function.

 set_terminate(my_terminate);

set_terminate is part of the standard C++ runtime library that's required in every implementation of C++. It returns a pointer to the old terminate function, which you should probably call from your new one.

 typedef void (*TERMINATE_FN)();
TERMINATE_FN old_terminate = NULL;

void main()
{
   old_terminate = set_terminate(my_terminate);
.
.
.
}
void my_terminate()
{
   printf("Oh no, I'm dying./n");
   // Save docs now
   if (old_terminate)
      old_terminate();  // call original terminate fn
}

While it's not relevant for your question, I should mention another special C++ function related to terminate. This function is called "unexpected," and you use set_unexpected to set it. C++ calls unexpected when a function in your program throws an exception it's not supposed to throw. Though few programmers use this feature, in C++ you can declare which exceptions a function throws.

 extern void DoSomething() throw (CFooError, CBarError);

In this example, if DoSomething throws an exception other than CFooError or CBarError, C++ calls unexpected. If a function has no exceptions specified, then it's allowed to throw any exception.

Now, back to terminate. The Official C++ Rule Book (aka The Annotated C++ Reference Manual), section 15.6.1, stipulates that the default terminate function should call abort. That's not very friendly, even if you save the user's docs before dying. It would be nice if there were some way to keep running. The problem is that C++ exceptions are really just a long jump with some stack cleanup thrown in. There's no way to resume from the point the exception was thrown; you have to reinitialize your app and start all over again.

That is, unless your app is a Windows app. If you recall from your Basic Windows Training (which many of you MFC whippersnappers probably skipped), at the heart of every Windows program lies a main loop that looks something like this:

 while (GetMessage(msg, ...)) {  // get a message
   DispatchMessage(msg, ...);   // process it (call
                                // window proc)
}

When stuff happens, Windows puts message in your app's message queue-things like WM_COMMAND, WM_LBUTTONDOWN, or WM_SAYHEYJOEWHATSUP. Windows doesn't actually call your window procedure; you must explicitly retrieve the messages from the queue and call DispatchMessage, which calls your window procedure. This little dance is how Windows 3.1 and earlier versions provide pseudo multitasking.

When your app starts up, it does a little initialization, then immediately enters this get/dispatch loop (or message pump as it's frequently called). If you've only programmed in MFC, you might not even realize there's a message pump because you never have to write it-MFC does it for you. In any case, the message pump is the ideal place to put an exception handler of last resort.

 while (GetMessage(msg,...)) {
   TRY {
      DispatchMessage(msg,...);
   } CATCH_ALL(e) {
      // handler of last resort
   } END_CATCH_ALL
}

By catching exceptions inside the main message loop, you let your app keep on loopin' when an exception occurs. Since every Windows program is in its main loop virtually all the time (though not in the case of startup, termination, modal dialogs, and some weird windows "hook" functions), this handler will effectively catch any exception your code might throw.

I've shown the code for a normal Windows app written in C. In MFC, all you have to do is override the right virtual function to insert your TRY/CATCH handler. Of course, knowing which function to override is rarely trivial-that's the whole trick to programming with MFC! As it turns out, the message pump is implemented in CWinThread::Run and CWinThread::PumpMessage. In Win32¨, message queues exist on a per-thread basis, not per-app. Run calls PeekMessage to see if there's a message waiting, and it only calls PumpMessage to dispatch it if there is. Knowing this, you can implement a global exception handler like this:

 //////////////////
// Override message pump to catch exceptions.
//
BOOL CMyApp::PumpMessage()
{
   BOOL ret;
   TRY {
        // Let MFC pump the message as normal
        ret = CWinApp::PumpMessage(); 
   } CATCH_ALL(e) {
        // my handler of last resort...
        ret = TRUE;   // keep looping
   } END_CATCH_ALL
   return ret;
}

There's a slight flaw with this, but let's ignore it for now. With PumpMessage written as shown, if your code throws an exception while processing some WM_xxx windows message, the exception will jump back to PumpMessage if no one else catches it. You can save docs or whatever, and return TRUE to keep running. Pretty cool, right?

But if putting an exception handler in the message loop is such a great idea, why doesn't MFC do it? In fact, MFC does have an exception handler of last resort-but not in the message pump! It's in the window proc. Under MFC, every window has the same window proc, which looks like this:

 // simplified
LRESULT AfxWndProc(HWND hWnd,...)
{
    pWnd = // get CWnd for this hWnd
   return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, 
                         lParam);
}

AfxWndProc just passes the buck to AfxCallWndProc, which does the dirty work.

 // also simplified
LRESULT AfxCallWndProc(CWnd* pWnd, HWND hWnd, ...)
{
   LONG lResult;
   TRY {
      lResult = pWnd->WindowProc(nMsg, wParam, lParam);
   } CATCH_ALL(e) {
      lResult = AfxGetThread()->ProcessWndProcException(e);
   } END_CATCH_ALL
   return lResult;
}

I've stripped a lot of details to highlight the important point: AfxWndProc-the window proc used for every MFC window-contains an exception handler that calls the virtual function CWinThread::ProcessWndProcException. This means, if your app throws an exception while processing a message, the exception goes to CWinThread::ProcessWndProcException if nobody else handles it. You can call set_terminate all day and your terminate function will never get called, even if your app throws an "unhandled" exception, because all exceptions are handled in AfxCallWndProc. (Again, with the caveat that your app is processing some message, which it just about always is.)

Now, why do you suppose the Redmondtonians put the TRY/CATCH handler in AfxCallWndProc, instead of putting it in CWinThread::MessagePump the way I showed you earlier? This is where that little flaw I mentioned comes in. I'll quote from the online documentation (Programming with MFC: Overview/Using the General Purpose/Handling Exceptions Classes):

When programming for Windows, it is important to remember that exceptions cannot cross the boundary of a "callback." In other words, if an exception occurs within the scope of a message or command handler, it must be caught there, before the next message is processed.

Why can't you throw an exception past a callback? Because exceptions aren't part of the deal. When Windows calls your app (usually through your window proc) or you call Windows, neither party expects the other to throw an exception. If you do, there's no telling what might happen. It might work, or it might not. When windows calls your window proc, it expects control to return to the immediately following instruction, not go gallivanting somewhere into outer space.

This is relevant because in Windows one message typically sends others. Your handler for WM_FOO might respond by sending WM_BAR. Even if you don't call SendMessage directly, many function calls are really a SendMessage in disguise. For example, CWnd::GetFont sends WM_GETFONT and CWnd::SetIcon sends WM_SETICON. Practically all the MFC functions for edit controls, list boxes, combo boxes, and so on actually call SendMessage to send EM_THIS, LB_THAT, or CB_THEOTHERTHING. Every time you call SendMessage, control passes into Windows, then out again to some window proc. Even if that window proc is yours, it's unsafe to throw an exception all the way up to the top-level DispatchMessage that started the chain of messages, bypassing all the secondary messages-you might leave Windows in an unhappy state.

In effect, there's a wall between Windows and your application. Neither side is allowed to throw an exception over the wall. The same wall exists in OLE, where you can't throw an exception from one interface to another, but must instead convert your internal exception to an HRESULT error code and return it. The wall also applies whenever you're using callbacks to or from third-party systems; unless the interface explicitly states that exceptions are allowed, you shouldn't throw past the boundary of a callback (see Figure 4).

Figure 4 Handling exceptions in AfxWndProc avoids throwing across callback boundary

By putting the TRY/CATCH block inside the window procedure, MFC guarantees that exceptions never jump across the wall. When your program throws an unhandled exception, control jumps to the closest AfxCallWndProc frame on the stack, the one for the most recently issued WM_xxx message. The window proc is Windows' portal to your app, and the TRY/CATCH block in AfxCallWndProc is the gatekeeper guarding the door against exceptions that would escape.

One unfortunate consequence is that ProcessWndProcException doesn't catch exceptions thrown while processing messages in a non-MFC subclassed window. Normally, every window in an MFC app uses AfxWndProc as its window procedure, but there are times when you need to subclass a window by installing your own window proc over AfxWndProc. If so, you lose the exception handling; if you need it, you'll have to implement it yourself, which is easy-just mimic the code in AfxCallWndProc.

 LRESULT MySpecialWndProc(HWND hwnd, ...)
{
   LONG lResult;
   TRY {
      lResult = // process the message
   } CATCH_ALL(e) {
      lResult = AfxGetThread()->
                 ProcessWndProcException(e);
   } END_CATCH_ALL
   return lResult;
}

In case you find all this stuff exceptionally mind-boggling, the sort of thing that makes you wish you were a cab driver instead of a programmer, don't worry. The short answer to your question is: override CWinThread/CWinApp::ProcessWndProcException.

I modified the SCRIBBLE program from the MFC tutorial to show how it works. CScribbleApp::ProcessWndProcException handles uncaught exceptions by displaying the dialog box in Figure 5. After that, it calls CWinApp::SaveAllModified, which is an MFC CWinApp function that prompts the user to save all modified documents. Then Scribble dies or goes on, depending on the user's response to the dialog.

Figure 5 An MFC exception dialog

If the user chooses Abort, CScribbleApp::ProcessWnd-ProcException rethrows the exception. Since it's already in the handler-of-last-resort, this has the affect of really throwing an uncaught exception, and control passes to the terminate handler. To prove it, my InitInstance function installs a new terminate function that displays the message, "Help me, I'm dying.", then calls the old terminate handler, if any. (Empirical evidence suggests the old terminate handler is NULL under Windows-go figure.)

If the user chooses Retry, CScribbleApp calls the base class CWinApp::ProcessWndProcException. The default MFC handler does its thing: if the exception occurred during a WM_PAINT message, MFC validates the paint region; if the exception happened during a WM_COMMAND, MFC acts as if the command succeeded. (For details, read the source code.) MFC then displays a message depending on what kind of exception it is-memory, resource, or whatever-and continues the message loop. I added a Throw command to Scribble (see Figure 6) that lets you throw different kinds of exceptions so you can see what MFC does in each case.

Figure 6 Choose your exception

Finally, if the user chooses Ignore, my ProcessWndProcException handler returns zero. Go directly to AfxCallWndProc, do not passCWinApp::ProcessWndProcException. From there, control returns to Windows (DispatchMessage) as if whatever message was being processed had been handled, and from there back to Scribble's main message loop.

My modified Scribble is fine for an educational tool designed to show you how exceptions work in MFC, but my implementation of ProcessWndProcException has a few shortcomings you should correct for a commercial app. For one thing, the user interface is bad; you should replace my Abort/Retry/Ignore dialog with one that asks the user right away whether to save documents. Then, of course, you have to be very careful what you do. If your save operation requires allocating memory, you might crash again if the original exception was CMemoryException. There's not much you can do here except preallocate a chunk of memory during initialization so you can free it in the exception handler and hope it's enough to save the docs.

You also have to be careful how you load the dialog. Ideally you should build it from static data (the way I did with hard-coded strings), not loaded from the resource file, because the resource file may be having problems if the exception is CResourceException. Yet another thing to consider is whether a document was corrupted, which could easily happen if the exception was thrown in the middle of some document operation. You don't want to save a bad doc! The best thing to do here is save a hidden file with a different name like ##docname.doc. Then, the next time the user opens that doc, you can display a message like "there is a crash version of this document newer than the one on disk-do you want to open it?" If the user wants to open it but the crash doc won't open (because it was corrupted), then at least you still have the most recently saved version. Another possibility is to make sure the doc is valid before you save it by doing the equivalent of AssertValid.

Yet another thing to worry about in your exception handler is what message you are currently processing. MFC's CWinThread::ProcessWndProcException calls ValidateRect if the exception happened inside a WM_PAINT message because it can't just return zero. If it did, Windows would send WM_PAINT messages ad infinitum, trying to repaint your window. Also, while the return value is unimportant for most messages, some-like WM_COMPAREITEM-actually look at the return value. The problem is further complicated by the fact that you could be several levels deep in processing a message. Your WM_PAINT handler might send some other WM_xxx message, and that might send another, which is the one that raises the exception. Looking at the current message doesn't tell you what the top-level message is.

It's impossible to write a perfect last-ditch exception handler. My best advice is to be very careful what you do inside ProcessWndProcException, assume the worst, and do your best to protect the user from the slings and arrows of outrageous software misfortune. Automatic backups and crash files go a long way in this department.

One final word about terminate before I, er, terminate: some of you more experienced MFC users may be familiar with AfxTerminate and AfxSetTerminate, which are like terminate and set_terminate in C++. Those functions still exist, but only for backward compatibility. They're officially considered passŽ; you're supposed to use either set_terminate (if you're writing a straight C++ app) or ProcessWndProcException (if you're writing a Windows application.

原文连接:

http://www.microsoft.com/msj/archive/S204D.aspx

<think>我们面临的任务是:在C#中开发一个程序,监控其他应用程序(目标程序)。当目标程序出现异常时,我们需要关闭它的异常提示框,然后重新启动程序。 解决方案思路: 1. 监控目标程序:我们可以通过进程名来监控目标程序是否在运行。 2. 检测异常提示框:当目标程序崩溃时,通常会弹出一个错误对话框(例如Windows错误报告对话框)。我们需要检测到这个对话框关闭它。 3. 关闭异常提示框:找到异常提示框的窗口,然后发送关闭消息(例如发送WM_CLOSE消息)。 4. 重启目标程序:在关闭异常提示框后,重新启动目标程序。 具体步骤: 步骤1:监控目标程序 我们可以使用System.Diagnostics.Process类来获取目标程序的进程,监控它是否退出。但是,这里需要注意的是,当目标程序崩溃时,它可能没有正常退出(而是弹出了错误对话框),所以我们需要同时监控进程和错误对话框。 步骤2:检测异常提示框 我们可以通过枚举窗口来查找属于目标进程的窗口,然后根据窗口标题或类名来识别异常提示框。常见的Windows错误报告对话框的类名可能是“#32770”(对话框类)或特定的标题(如“应用程序错误”)。 步骤3:关闭异常提示框 一旦检测到异常提示框,我们可以使用Win32 API发送消息(如WM_CLOSE)来关闭它。 步骤4:重启目标程序关闭异常提示框后,我们可以重新启动目标程序。 由于需要操作其他窗口,我们需要使用一些Win32 API。以下是一些关键的API: - FindWindow / FindWindowEx:用于查找窗口。 - EnumWindows:枚举所有顶层窗口。 - GetWindowThreadProcessId:通过窗口获取进程ID。 - SendMessage 或 PostMessage:发送消息给窗口(例如WM_CLOSE)。 另外,我们还需要一个循环来定期检查目标程序是否在运行,以及是否出现了异常提示框。 实现方案: 1. 创建一个定时器(Timer),定期检查目标进程是否存在。 2. 如果目标进程不存在(可能是崩溃后已经被关闭),则重新启动它。 3. 如果目标进程存在,但是出现了异常提示框,则关闭该提示框,然后终止目标进程(如果它还在运行),最后重新启动它。 注意:有时候异常提示框可能是目标进程的子窗口,所以我们需要遍历所有窗口,检查每个窗口的进程ID是否与目标进程的ID相同,然后根据窗口标题或类名判断是否为异常提示框。 代码结构: 我们创建一个Windows服务或一个后台应用程序(如控制台应用程序或WinForm应用程序)来执行监控。 这里以WinForm为例,我们可以设置一个System.Windows.Forms.Timer来定期执行检查。 步骤代码: 1. 导入必要的Win32 API函数。 2. 定义监控的目标进程名称(不带扩展名)或路径。 3. 在定时器的Tick事件中: a. 检查目标进程是否在运行。 b. 如果没有运行,则启动它。 c. 如果正在运行,则检查是否有异常提示框出现(通过枚举窗口,检查窗口的进程ID是否与目标进程相同,且窗口标题或类名匹配异常对话框的特征)。如果找到,则关闭它,终止目标进程(确保完全关闭),然后重新启动。 注意:关闭异常提示框后,目标进程可能已经崩溃,实际上可能已经退出,所以我们需要先尝试终止目标进程(如果它还在运行),然后再启动。 代码示例: 首先,我们定义一些Win32 API的P/Invoke声明。</think>在 C# 中开发监控自动重启其他应用程序程序,可以通过以下步骤实现: ### 解决方案步骤 #### 1. **监控目标程序状态** ```csharp using System.Diagnostics; // 监控目标进程 Process targetProcess = Process.Start("目标程序路径.exe"); // 创建定时器检查状态 System.Timers.Timer monitorTimer = new System.Timers.Timer(5000); // 每5秒检查一次 monitorTimer.Elapsed += (sender, e) => CheckProcessStatus(targetProcess); monitorTimer.Start(); void CheckProcessStatus(Process proc) { if (proc.HasExited) { // 进程已退出(可能是崩溃) RestartApplication(proc); } else { // 检查是否存在异常对话框 CheckErrorDialogs(proc.Id); } } ``` #### 2. **检测关闭异常提示框** ```csharp using System.Runtime.InteropServices; [DllImport("user32.dll")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] private static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam); const uint WM_CLOSE = 0x0010; void CheckErrorDialogs(int processId) { // 常见错误对话框标题(根据实际目标程序调整) string[] errorTitles = { "应用程序错误", "程序已停止工作", "Runtime Error" }; foreach (var title in errorTitles) { IntPtr hWnd = FindWindow(null, title); if (hWnd != IntPtr.Zero) { // 获取对话框所属进程ID GetWindowThreadProcessId(hWnd, out int dialogProcId); if (dialogProcId == processId) { PostMessage(hWnd, WM_CLOSE, 0, 0); // 发送关闭消息 RestartApplication(Process.GetProcessById(processId)); } } } } [DllImport("user32.dll", SetLastError = true)] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId); ``` #### 3. **重启目标应用程序** ```csharp void RestartApplication(Process oldProcess) { try { // 确保原进程完全关闭 if (!oldProcess.HasExited) oldProcess.Kill(); oldProcess.WaitForExit(5000); // 等待5秒 // 重启程序(带原始参数) ProcessStartInfo startInfo = new ProcessStartInfo() { FileName = oldProcess.StartInfo.FileName, Arguments = oldProcess.StartInfo.Arguments, UseShellExecute = true }; targetProcess = Process.Start(startInfo); // 启动新实例 } catch (Exception ex) { // 记录错误日志(参考引用[2]) File.AppendAllText("monitor.log", $"[{DateTime.Now}] 重启失败: {ex.Message}\n"); } } ``` #### 4. **完整监控服务(控制台/WinForms服务)** ```csharp class Program { static Process targetProcess; static void Main() { StartMonitoring("C:\\Path\\To\\TargetApp.exe"); Console.ReadLine(); // 保持主线程运行 } static void StartMonitoring(string appPath) { targetProcess = Process.Start(appPath); System.Timers.Timer timer = new System.Timers.Timer(3000); timer.Elapsed += (s, e) => MonitorTask(); timer.Start(); } static void MonitorTask() { if (targetProcess.HasExited || !IsProcessResponsive(targetProcess)) { RestartApplication(targetProcess); } else { CheckErrorDialogs(targetProcess.Id); } } } ``` ### 关键注意事项 1. **权限要求**: - 以管理员权限运行监控程序(需在 manifest 中设置`<requestedExecutionLevel level="requireAdministrator"/>`) - 否则可能无法关闭其他进程的对话框 2. **异常对话框识别**: - 需根据目标程序的实际错误窗口标题调整`errorTitles`数组 - 可使用 Spy++ 工具获取准确的窗口标题和类名 3. **进程响应检测**(可选增强): ```csharp bool IsProcessResponsive(Process proc) { try { return !proc.Responding; // 检查GUI程序是否无响应 } catch { return false; // 访问异常视为进程异常 } } ``` 4. **日志记录**: - 实现完善的日志系统(参考引用[2]) - 记录重启事件、错误详情和时间戳 5. **远程监控支持**: - 可通过 WMI 实现远程机器监控 - 使用`ManagementScope`连接远程计算机 ### 潜在挑战与解决方案 | 挑战 | 解决方案 | |------|----------| | 权限不足 | 以管理员身份运行监控程序 | | 跨会话问题(如服务) | 使用`CreateProcessAsUser` API | | 误关合法窗口 | 精确匹配窗口标题/类名 + 进程ID验证 | | 高频重启保护 | 添加重启间隔限制(如5分钟内最多重启3次) | ### 扩展优化建议 1. **添加心跳检测**:通过 TCP/UDP 或命名管道实现进程间通信 2. **邮件/SMS 通知**:当重启发生时发送警报(参考引用[4]的安全通知机制) 3. **创建 Windows 服务**:使用`Topshelf`库实现后台服务化 > 注意:频繁重启可能掩盖程序根本问题,建议同时收集目标程序的崩溃报告(通过 Windows 事件日志或 minidump)用于后续分析[^1][^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值