用户友好的异常处理






3.20/5 (5投票s)
2003 年 8 月 6 日
6分钟阅读

52896

590
如何在发布版本中以用户友好的方式处理异常
引言
作为用户,您多久会遇到这种情况?您正在处理一个程序,然后毫无征兆地,发生了一个异常,您丢失了所有工作。很少有程序在发布版本中包含异常处理器。从开发者的角度来看,这可以理解 - 如果您添加了异常处理器,它就会隐藏问题。如果可能,您希望收到带有异常地址的错误报告。我们都希望,当我们的程序发布到正式版时,就已经没有剩余的异常了。但尽管付出了最大的努力,有时可能仍然会剩下一些异常。那么,如何以用户友好的方式处理这些异常,同时又方便用户将带有异常地址的错误报告发送给开发者呢?
答案是编写一个异常处理器,它可以报告异常地址,但允许程序继续执行。这样,用户就有机会保存他的工作,如果异常不严重,也许还可以继续工作。为了方便用户发送错误报告,异常处理器应该能够主动提出为用户创建一封电子邮件发送给开发者。电子邮件可以包含关于异常的技术细节作为附件,并在正文中鼓励用户添加更多关于它是如何发生的细节。
所以,这就是我现在所有发布版本中的做法。这在多年来逐渐演变而来,所以在这里我将展示我最新的异常处理器。我在纯C编写的程序中使用它,但也许它也可以用于C++。演示程序是一个简单的“Hello World”程序,其中包含一个C++异常处理器。zip文件中的cpp文件也是有效的c代码,所以如果您碰巧像我一样编写纯C,只需将其保存为带有.c扩展名的文件,并将`#include
背景
嗯,您需要了解异常处理。这段代码使用了`__try{ ... } __except(..){...}`块。
然后,您需要一种方法来获取异常地址。查阅文档,您会发现您需要做的就是在此类语句中调用一个过程:
__except( Eval_Exception(GetExceptionCode( ), GetExceptionInformation() )
{
...
}
然后,您可以使用它来设置几个全局变量,代码可以稍后检查这些变量。这就是我在这里所做的。
异常信息只在该点可用。您无法在`__except(...)`语句之后的块中获取它。
Using the Code
嗯,您可能会自己演变出一个版本 - 这更多的是一个概念演示,而不是可以直接添加到您程序中的代码。但如果您想直接使用这段源代码,方法如下。请注意,示例代码是为未定义UNICODE的构建编写的 - 在UNICODE构建中,您需要处理`sprintf`等的使用。
将`ExceptionHandler.h`、`ExceptionHandler.cpp`和`ExceptionHandlerUtils.cpp`包含到您的构建中。
您还需要像演示中那样适当地设置这些变量:
#define SZ_EMAIL "support@tunesmithy.co.uk"
#define SZ_NAME "Robert Walker"
// Set these in your WinMain
extern char szWorkingDirectory[MAX_PATH];
extern char szAppName[MAX_PATH];
在您的代码中,您需要将可能发生异常的所有地方用以下宏括起来:
DO_ENTER_TRY_BLOCK
DO_EXIT_TRY_BLOCK_DLGPROC("ProcName",message,wParam,lParam);
用于消息类型回调,以及
DO_ENTER_TRY_BLOCK
DO_EXIT_TRY_BLOCK_PROC("ProcName");
用于其他过程。
这里的`ProcName`字符串会显示在处理器生成的异常报告中。
关键在于,有时异常可能发生在被代码中许多地方调用的一个小型子程序中 - 即使您知道异常发生在哪个行,您可能还需要了解该子程序是从哪里被调用的。当然,您可以将它们嵌套起来,所以您应该将它们放在所有大型过程周围,任何您希望能够识别为异常作用域的代码部分。目前,报告仅显示处理该异常的块的标签,但可以轻松地对其进行扩展,以显示所有嵌套在它之上的异常块标签的调用堆栈式报告。
这是这些宏的定义:
#ifdef cl_exception_handler
#define DO_ENTER_TRY_BLOCK\
__try\
{
void ExceptionHandlerForDlgProc(char *sztype,UINT message,WPARAM wParam,LPARAM lParam);
void ExceptionHandler(char *sztype);
int Eval_Exception ( int n_except,LPEXCEPTION_POINTERS ExceptionInfo);
#define DO_EXIT_TRY_BLOCK_DLGPROC(szName,message,wParam,lParam)\
}\
__except(Eval_Exception(GetExceptionCode(),GetExceptionInformation()))\
{\
ExceptionHandlerForDlgProc(szName,message,wParam,lParam);\
}
#define DO_EXIT_TRY_BLOCK_PROC(szName)\
}\
__except(Eval_Exception(GetExceptionCode( ),GetExceptionInformation()))\
{\
ExceptionHandler(szName);\
}
#else
#define DO_ENTER_TRY_BLOCK
#define DO_EXIT_TRY_BLOCK_DLGPROC(szName,message,wParam,lParam)
#define DO_EXIT_TRY_BLOCK_PROC(szName)
#endif
我通常只在发布版本中定义`cl_exception_handler`,因为在调试版本中,我们更倾向于在异常处中断。在调试器中运行时,如果您想在异常后继续执行,您可以简单地将下一条语句设置为异常之后的语句,然后继续。
然后,为了自动添加构建日期到错误报告中,您需要以某种方式添加版本日期。我将其定义在一个每次我运行调试版本时都会重新生成的头文件中。总之,使用一种方法来确保每个发布版本都有一个唯一的标识日期 - 然后备份您上传的每个版本的源代码(当然也包括日期头文件)。
这是我的做法:
#ifdef _DEBUG
char *szVersionDate="<DATE builds release in here shown gets>";
#else
#include "VersionDate.h" // Save this every time we run the debug build.
// so that the exception message can include the date of the release
// which we can check to make sure we have the same version of the source as
// the user. See the beginning of WinMain(...) for example to show how one can make it.
#endif
当您收到用户发送的异常报告时,您会想要找出异常在您的代码中发生的位置,这可以通过程序生成一个Map文件来从异常地址推断出来。有关详细信息,请参阅使用MAP文件查找崩溃信息。
如果您自动为所有代码进行版本控制,并在每次上传构建时备份代码,那么您就不一定需要提前创建一个Map文件,而是可以查看您的备份,找到具有相应版本日期的那个,在收到报告时重新构建应用程序,然后再创建Map文件。
请注意,我使用`MessageBox`而不是自定义对话框来显示异常消息,因为应用程序可能在资源非常有限的情况下运行,以至于无法创建新的对话框来显示。`MessageBox`对话框保证始终可用。
历史
- 2003年8月2日:首次发布
链接
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。