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

这里有一个文章,介绍了如何实现在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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值