65.9K
CodeProject 正在变化。 阅读更多。
Home

CrashRptEx - CrashRpt 崩溃报告系统的扩展

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2012年5月7日

LGPL3

8分钟阅读

viewsIcon

36608

downloadIcon

4

如何使用 CrashRptEx,以避免 MFC 应用程序在崩溃报告中的一些陷阱,或者如果您希望在应用程序崩溃后能够继续运行。

引言

本文介绍的项目只是对 Mike Carruth 和 zexspectrum 的优秀 CrashRpt 崩溃报告系统 的一个扩展。他们都曾撰写过 CodeProject 文章(此处此处)。它处理了一些 MFC 和 SysWOW64 特有的陷阱,并增加了在崩溃后继续执行的功能。

背景

在遇到 crashrpt 之前,我部分基于 Hans' Dietrich 的 XCrashRpt 编写了自己的代码。当需要添加附加功能时,我环顾四周寻找支持良好但轻量级的替代方案,并考虑了 crashrpt Wiki 中列出的一些候选方案。长话短说——我最喜欢 crashrpt,原因在于它的相对简单性,这部分归功于它只针对 Windows 应用程序,可以直接使用许多微软调试工具进行崩溃分析。

事实证明,将其集成到我的应用程序中非常直接,并且利用了现有的文档,几乎不需要处理特殊情况。然而,我的应用程序是用 MFC 创建的,并且同时在 32 位机器、64 位机器的 SysWOW64 中以及原生 64 位模式下运行。事实证明,使用 MFC 和在 SysWOW64 中运行都会导致一些问题,需要处理这些问题才能捕获所有崩溃并正确报告其来源。

我还有一个额外的要求,就是希望允许用户在崩溃后继续执行。虽然这通常被认为是一个坏主意,因为程序的内存很可能已损坏,但在某些场景下这是完全合理的。我的用户可能花费数天准备实验,并运行数小时,如果应用程序因为零除法在一个次要的在线分析中崩溃,或者因为我忘了捕获某个异常而崩溃,那么即使我允许像 WORD 通过微软的“Dr.Watson”那样保存已获取的数据,这些工作也将付之东流。

当然,这是仅次于编写更好软件的第二好的解决方案,但崩溃报告系统的存在本身就表明崩溃确实会发生。此外,在某些环境中,用户可能需要在日常工作中才能使用 Beta 或 Alpha 状态的软件,因为冒着失败的风险使用它仍然比根本不使用要好。

尽管如此,crashrpt 比我编写的任何东西都领先一步,并且由 zex spectrum 进行了彻底的测试和维护,他对我提出的建议和在原始版本中发现的几个小问题也极其响应迅速。因此,我决定向 CrashRpt 添加所需的功能,并在我的程序中使用该衍生版本。经过数月的无问题使用后,我认为是时候与社区分享这个相对较小的贡献以及我所了解的 MFC 问题了,希望它能对某些人有所帮助。

Using the Code

代码的使用方式与 crashrpt 相同,除了上述文章之外,它还拥有出色的文档、FAQ 和 Wiki 条目。附加功能是可选的,并在 CrashRptEx.h 头文件中进行了说明。下面还将简要介绍这些功能以及一些背景信息。

简而言之

简而言之,添加了以下新选项和函数。

CRASHRPTAPI(int) crAllowContinue(DWORD dwFlags);
CRASHRPTAPI(int) crDiscardError(CR_EXCEPTION_INFO &ei);
CRASHRPTAPI(int) crHandleError(CR_EXCEPTION_INFO &ei);  

第一个函数选择(针对当前线程)崩溃处理程序是否允许程序继续执行。具体行为由 dwFlags 控制,该参数是以下组合:

(1) 以下之一:

CR_INST_APP_CONTINUE (用户选择,默认终止)
CR_INST_APP_CONTINUE_DEFAULT (用户选择,默认继续)
CR_INST_APP_CONTINUE_ONLY (始终继续)
CR_INST_APP_TERMINATE (终止应用程序。)

如果选择了前三个选项之一,则会抛出一个 CR_EXCEPTION_INFO ,而不是终止应用程序,该异常可以被应用程序调用堆栈中的相应 catch 子句捕获。

(2) ... 可选地

CR_INST_APP_CONTINUE_NOSENDER (不调用崩溃发送程序,抛出异常信息。)

这将不会终止应用程序,也不会启动崩溃发送程序,即使选择了 CR_INST_APP_TERMINATE 。相反,应用程序可以在 catch 子句中调用 crHandleError,其行为将根据 (1) 中描述的标志,或 crDiscardError(例如,在记录错误后)静默地继续执行,而与这些标志无关。

int crEnableProcessCallbackFilter(BOOL bEnable);
int crProcessCallbackFilterStatus();  

禁用/启用异常过滤器(这是一个已知的微软 bug,见下文),该过滤器会静默捕获在 SysWOW64 下运行的应用程序中的 Windows 回调例程中引发的异常。

WNDPROC crInstallWndProcWrapper(pfnWndProc);
int crEnableWndProcWrapper(BOOL bEnable);
int crWndProcWrapperStatus();

安装(第一个实际上是一个宏,用于处理 MFC 的特殊性)、启用或禁用一个包装器,该包装器实现了上述 catch 子句的 Windows 过程。这是必需的,因为当异常发生在调用 MFC (?) Windows 过程的后面时,微软的 Dr. Watson 会启动,而 CrashRpt 没有机会。

CRASHRPTAPI(int) crModifyFlags(DWORD dwFlags, DWORD dwMask);

可在程序执行期间的任何时间调用。此函数会修改已安装的崩溃处理程序的标志,而无需重新安装(这需要重新添加所有文件等)。它主要用于上述功能,但也可以独立使用。

在 WTL 和 MFC 的测试应用程序中都演示了允许程序继续执行的功能,而所有其他功能仅在 MFC 版本中演示。让我们来看看如何在您的应用程序中集成新功能,重点关注后者测试应用程序。

CrashRpt 的设计初衷是捕获崩溃并启动 CrashSender 应用程序,该应用程序会保持崩溃进程的运行,直到它收集到所有配置为发送的信息,然后允许它结束,并通过配置的方法发送有关进程和崩溃的信息。CrashSender 应用程序由库安装的崩溃处理程序启动。为了允许应用程序从定义的位置继续执行(例如,通过将堆栈展开到消息循环),对 CrashRpt 进行了一些必要的更改。最重要的是,崩溃处理程序不应再终止进程。让我们看一下 CrashRpt(Ex) 安装的典型崩溃处理程序。

// Structured exception handler
LONG WINAPI CCrashHandler::SehHandler(PEXCEPTION_POINTERS pExceptionPtrs)
{ 
    CCrashHandler* pCrashHandler = CCrashHandler::GetCurrentProcessCrashHandler();
    ATLASSERT(pCrashHandler!=NULL);  

    if(pCrashHandler!=NULL)
    {
        // Acquire lock to avoid other threads (if exist) to crash while we are 
        // inside. 
        pCrashHandler->CrashLock(TRUE);

        CR_EXCEPTION_INFO ei;
        memset(&ei, 0, sizeof(CR_EXCEPTION_INFO));
        ei.cb = sizeof(CR_EXCEPTION_INFO);
        ei.exctype = CR_SEH_EXCEPTION;
        ei.pexcptrs = pExceptionPtrs;

#ifdef CRASHRPT_EX
        // AS: Error report generation now in _s_HandleError
        _s_HandleError(ei, pCrashHandler);
#else
        pCrashHandler->GenerateErrorReport(&ei);

        // Terminate process
        TerminateProcess(GetCurrentProcess(), 1);    
#endif
    }   

    // Unreacheable code  
    return EXCEPTION_EXECUTE_HANDLER;
} 

原始版本的代码仍然在 #else 子句中可见。原始的错误报告创建和终止进程的调用已被移至一个函数,因为在每个崩溃处理程序中都会重复一些代码。新函数是:

void _s_HandleError(CR_EXCEPTION_INFO &ei, CCrashHandler * pCrashHandler)
{
  // Maybe we should later add an option for which types of crashes we should
  // allow to continue execution.
  DWORD dwFlags = pCrashHandler ->IsContinueAllowed();
  
  if (dwFlags)
  {
      pCrashHandler ->ModifyFlags(dwFlags, CR_INST_APP_CONTINUE_MASK);
      if (dwFlags & CR_INST_APP_CONTINUE_NOSENDER)
          throw ei;
  }
  
  // This will return as soon as the screenshot was set
  pCrashHandler->GenerateErrorReport(&ei);
  // If continue is allowed, we wait for the launcher to finish and
  // read out the exit code
  if (dwFlags)
  {
      WaitForSingleObject(ei.hSenderProcess, INFINITE);
      DWORD dwExitCode = 1;
      GetExitCodeProcess(ei.hSenderProcess, &dwExitCode);
      if (dwExitCode & CR_INST_APP_CONTINUE)
      {
          pCrashHandler->CrashLock(FALSE);
          pCrashHandler ->ChangeGUID();
          throw ei;
      }
  }
          
  switch(ei.exctype)
  {
  case CR_CPP_NEW_OPERATOR_ERROR:
  case CR_CPP_INVALID_PARAMETER:
      pCrashHandler->CrashLock(FALSE);
  default:
      ;
  }
  // Terminate process
  TerminateProcess(GetCurrentProcess(), 1);    
}

首先,处理程序检查是否允许继续执行(稍后我们将看到,这可能因线程而异,并且可以随时在运行时更改),并修改崩溃处理程序标志,以便在启动崩溃发送程序时将此信息传递给它。下一块……

if (dwFlags & CR_INST_APP_CONTINUE_NOSENDER)
          throw ei;

……是由于该系统的另一个新选项,只有当您打算允许应用程序继续运行时才有意义:如果选择了 CR_INST_APP_CONTINUE_NOSENDER,则不会在崩溃处理程序中启动发送程序,而是将异常信息抛出,并可以在调用堆栈的更上方捕获。该功能及其用途将在下文更详细地介绍。如果未选择该选项,我们将启动崩溃发送程序。它将向用户显示此新选项,如果继续执行不是唯一的选项,用户可以修改默认选择。

如果用户选择继续执行应用程序,我们将解锁崩溃处理程序并更改其 GUID(CCrashHandler::ChangeGUID() 是为此目的引入的新函数。这是必需的,因为应用程序稍后可能发生第二次崩溃,我们希望它不要覆盖队列中的第一次崩溃,并被崩溃分析识别为不同的崩溃)。然后抛出异常信息。应用程序可以在应停止展开堆栈的任何位置捕获 CR_EXCEPTION_INFO 引用,然后根据该引用决定是否真正继续执行。

如果应用程序不应继续执行,则在必要时解锁崩溃处理程序后调用 TerminateProcess()

捕获崩溃(不仅限于 MFC 应用程序)时的陷阱

SysWOW64 中的异常

在 64 位系统上运行 32 位应用程序时,进程通常会在窗口回调后面吞噬异常。这是一个 Windows bug,可能会一直存在,因为它已经存在很长时间了,而且一些应用程序可能已经开始依赖它。有关详细信息,请参阅 此 Microsoft KB 文章,包括一个热修复程序,以及 此论坛帖子CrashRptEx 包含 crEnableProcessCallbackFiltercrProcessCallbackFilterStatus 函数,用于检查热修复程序是否存在并启用/禁用负责吞噬异常的回调过滤器。

为确保异常能够通过回调(从而被 CrashRpt 识别),请按以下方式启用热修复程序:

int ret = crEnableProcessCallbackFilter(FALSE);

您可以通过调用随时查询热修复程序的状态:

int ret = crProcessCallbackFilterStatus(); 

返回值为零表示异常将通过回调。

//! 1: Hotfix present and filter active
//! 2: Hotfix not present, filter active if this is an affected system,
//!    not active otherwise
//! 0: Hotfix present, filter not active

Windows 回调后面的异常

如果您的应用程序使用 MFC(可能不仅限于此),还会出现其他问题,这些问题会导致应用程序崩溃,并在 CrashRpt 有机会识别问题之前触发 Microsoft 的 Dr. Watson,因为在调用 Windows 过程的代码中包含了一个异常处理程序(针对某些异常)。当使用 SetWindowsHookEx 挂钩 Windows 过程时,之前未受影响的应用程序也可能出现此行为。解决方案是包含:

历史

  • 2012 年 5 月 6 日:初始版本
© . All rights reserved.