托管代码错误报告
生成未处理错误时的 MiniDump 并允许用户处理错误的模块。

引言
这是一个可以附加到任何项目以提供错误报告的 DLL。它的设计考虑了托管代码项目的错误报告。它引用了 dbghelp.dll 及其 MiniDumpWriteDump
函数。此外,它还处理未处理的异常和线程异常。所有这些信息都会在一个整洁的框中显示给用户,以便他们采取行动。框中还可以包含有关他们应该做什么的说明。
背景
经过数小时的研究,我终于发现 MiniDump
例程对于我们这些从事托管代码工作的人来说用处非常有限。我发现了一篇非常有用的文章,并决定在此基础上进行扩展。
在使其工作并将一些 TechNet 文章集成进来之后,我发现我无法使用 MiniDump
文件,因为它们与托管代码不兼容,没有 sos.dll 和一大堆输入,即使这样,也只在非常有限的方式下。
我想看到错误的堆栈和行号,所以我研究了异常处理中涉及的各种变量,并提取了大量有用的信息。然后我将其包装在一个类中,并为其提供了一个接口,该接口可以导出 minidump 以及各种其他可读信息。
我还在追求并实现的一件事是,如果可能的话,代码在错误发生后能够恢复。当我们将代码分解成小的例程时,这效果最好,这样如果一个失败了,其他的就有可能继续。此外,将应用程序多线程化有助于应用程序即使在发生严重的未处理异常后也能继续运行。
使用代码 - 主线程
如果您只是将此 DLL 添加到您的项目中,您只需几行代码即可使用它。只有少数 public
方法,以免混淆。我将向您展示示例应用程序的代码,以了解如何使用它。
这是一个非常简单的演示应用程序,它已进行注释,方便您跟踪。它显示了一个只有两个按钮的窗体,每个按钮都会导致一个异常,一个已处理,另一个未处理。您会注意到,您可以继续单击按钮,整个程序不会崩溃,即使其中一个错误是完全未处理的。
Public Class frmDemo
Private Sub frmDemo_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
'optional, but highly suggested parameters to set
'could be any process that you want to kick off, web address,
'basically anything you can type in a 'run' box
ErrorHandler.ErrorReporter.BugReportPath = _
"mailto:support@me.net?subject=<subject>&body=<body>"
ErrorHandler.ErrorReporter.SubjectPreface = "Demo Application: "
'either true or false depending on if the Message is
'plain text or is in RTF format
ErrorHandler.ErrorReporter.IsRTF = False
'either plain text or rich text
ErrorHandler.ErrorReporter.Message = _
"This is a message that can be an RTF reference. " & _
"If you want to display an RTF reference here just set _
ErrorHandler.ErrorReporter.Message = My.Resources.[ResourceName]"
'optionally set the window dimensions (default: 600x370)
ErrorHandler.ErrorReporter.WindowHeight = 500
ErrorHandler.ErrorReporter.WindowWidth = 500
'Use MAPI to send a message (this make the above BugReportPath unused)
ErrorHandler.ErrorReporter.SendEmail = True
ErrorHandler.ErrorReporter.ToAddress = New String(0) {"support@me.net"}
'Specify the preface to the body
ErrorHandler.ErrorReporter.BodyPreface = _
"Please include a detailed description of what you were doing when _
this error occurred:" & _
Environment.NewLine & Environment.NewLine & Environment.NewLine & _
"========================================" & Environment.NewLine
'add in the handlers for the unhandled exceptions
AddHandler AppDomain.CurrentDomain.UnhandledException, _
AddressOf ErrorHandler.ErrorProcesser.UnhandledException
AddHandler Application.ThreadException, _
AddressOf ErrorHandler.ErrorProcesser.UnhandledException
End Sub
Private Sub btnHandled_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnHandled.Click
Dim a As Integer = 8
Dim b As Integer = 0
Try
a = a / b
Catch ex As Exception
'handled exception
MessageBox.Show(ErrorHandler.ErrorProcesser.BuildExceptionMessage(ex))
End Try
End Sub
Private Sub btnUnhandled_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnUnhandled.Click
Dim a As Integer = 8
Dim b As Integer = 0
'unhandled exception
a = a / b
End Sub
End Class
使用代码 - BackgroundWorker
关于尝试处理 BackgroundWorker
例程中的异常,需要注意的一点是,您必须在此模块的前面加上处理 DoWork
事件的模块。
<System.Diagnostics.DebuggerNonUserCodeAttribute()>
为了做到这一点,我通常会得到类似这样的代码:
<system.diagnostics.debuggernonusercodeattribute() /> _
Private Sub workerMyLongTask_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles workerMyLongTask.DoWork
...
...
End Sub
现在,如果您想处理在 BackgroundWorker
线程中发生的错误,您应该将错误处理放在 RunWorkerCompleted
事件处理例程中。在 DoWork
事件处理例程中发生的任何错误都可以在 DoWork
例程中作为 e.Error
访问。为了确保错误在 BackgroundWorker
线程中发生时得到正确处理,以下代码通常效果很好:
Private Sub workerMyLongTask_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles workerMyLongTask.RunWorkerCompleted
If e.Error IsNot Nothing Then
...
MessageBox.Show_
(ErrorHandler.ErrorProcesser.BuildExceptionMessage(e.Error))
...
Else
...
End If
End Sub
代码工作原理
实际处理应用程序的代码包含在几个模块中,这些模块在这里被分解。第一部分是基本的声明等。Enum
基于 Microsoft 自身文档中的信息。
Private Declare Function MiniDumpWriteDump Lib "dbghelp.dll" (
ByVal hProcess As IntPtr, ByVal ProcessId As Int32, ByVal hFile As IntPtr,
ByVal DumpType As MINIDUMP_TYPE, ByVal ExceptionParam As IntPtr,
ByVal UserStreamParam As IntPtr, ByVal CallackParam As IntPtr) As Boolean
Private Enum MINIDUMP_TYPE
'Include just the information necessary to capture stack traces for all
'existing threads in a process.
MiniDumpNormal = 0
'Include the data sections from all loaded modules. This results in the
'inclusion of global variables, which can make the minidump file significantly
'larger. For per-module control, use the ModuleWriteDataSeg enumeration
'value from MODULE_WRITE_FLAGS.
MiniDumpWithDataSegs = 1
'Include all accessible memory in the process. The raw memory data is included
'at the end, so that the initial structures can be mapped directly without the
'raw memory information. This option can result in a very large file.
MiniDumpWithFullMemory = 2
'Stack and backing store memory written to the minidump file should be filtered
'to remove all but the pointer values necessary to reconstruct a stack trace.
'Typically, this removes any private information.
MiniDumpWithHandleData = 4
'Include high-level information about the operating system handles that
'are active when the minidump is made.
MiniDumpFilterMemory = 8
'Stack and backing store memory should be scanned for pointer references
'to modules in the module list. If a module is referenced by stack or backing
'store memory, the ModuleWriteFlags member of the MINIDUMP_CALLBACK_OUTPUT
'structure is set to ModuleReferencedByMemory.
MiniDumpScanMemory = 10
'the following aren't supported under XP
'Include information from the list of modules that were recently unloaded,
'if this information is maintained by the operating system.
MiniDumpWithUnloadedModules = 20
'Include pages with data referenced by locals or other stack memory. This
'option can increase the size of the minidump file significantly.
MiniDumpWithIndirectlyReferencedMemory = 40
'Filter module paths for information such as user names or important
'directories. This option may prevent the system from locating the image
'file and should be used only in special situations.
MiniDumpFilterModulePaths = 80
'Include complete per-process and per-thread information from the operating
'system.
MiniDumpWithProcessThreadData = 100
'Scan the virtual address space for other types of memory to be included.
MiniDumpWithPrivateReadWriteMemory = 200
'Reduce the data that is dumped by eliminating memory regions that are not
'essential to meet criteria specified for the dump. This can avoid dumping
'memory that may contain data that is private to the user. However, it is not
'a guarantee that no private information will be present.
MiniDumpWithoutOptionalData = 400
'Include memory region information. For more information,
'see MINIDUMP_MEMORY_INFO_LIST.
MiniDumpWithFullMemoryInfo = 800
'Include thread state information. For more information,
'see MINIDUMP_THREAD_INFO_LIST.
MiniDumpWithThreadInfo = 1000
'Include all code and code-related sections from loaded modules to
'capture executable content. For per-module control, use the ModuleWriteCodeSegs
'enumeration value from MODULE_WRITE_FLAGS.
MiniDumpWithCodeSegs = 2000
End Enum
下一部分是调用外部函数生成 MiniDump
的例程。如果 MiniDump
成功,它将返回保存 MiniDump
的路径。
Private Shared Function SaveMiniDump()
Using dumpProcess As System.Diagnostics.Process =
System.Diagnostics.Process.GetCurrentProcess
Dim strDumpFile As String = Path.ChangeExtension(
Path.GetTempFileName.ToString, ".mdmp")
Dim bolResult As Boolean
Using objDumpFile As FileStream = _
New FileStream(strDumpFile, FileMode.Create)
'Call the API
bolResult = MiniDumpWriteDump(dumpProcess.Handle, dumpProcess.Id,
objDumpFile.SafeFileHandle.DangerousGetHandle,
MINIDUMP_TYPE.MiniDumpWithDataSegs, 0, 0, 0)
End Using
If bolResult Then
Return strDumpFile
Else
Return ""
End If
End Using
End Function
然后我们有了专门用于处理未处理异常和线程异常的代码。为了方便我,它是一个重载函数。您还会注意到这里包含了一些不属于异常消息的内容。我曾试图弄清楚如何将局部变量也转储到这里,但徒劳无功(如果您知道如何,请告诉我,我会更新此代码)。
Public Shared Sub UnhandledException(ByVal sender As Object,
ByVal e As UnhandledExceptionEventArgs)
Dim strAdditionalInfo As String = ""
Dim strMiniDumpLocation As String = ""
strAdditionalInfo &= sender.ToString() & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= "Open Forms: "
For x = 0 To Application.OpenForms.Count - 1
strAdditionalInfo &= _
Application.OpenForms.Item(x).Name & Environment.NewLine
Next
strAdditionalInfo &= Environment.NewLine
strAdditionalInfo &= BuildExceptionMessage(e)
strMiniDumpLocation = SaveMiniDump()
ShowErrorForm(strAdditionalInfo, strMiniDumpLocation, e.ToString)
End Sub
Public Shared Sub UnhandledException(ByVal sender As Object,
ByVal e As System.Threading.ThreadExceptionEventArgs)
Dim strAdditionalInfo As String = ""
Dim strMiniDumpLocation As String = ""
strAdditionalInfo &= sender.ToString() & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= Application.ProductVersion & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= "Open Forms: "
For x = 0 To Application.OpenForms.Count - 1
strAdditionalInfo &= Application.OpenForms.Item(x).Name &
Environment.NewLine
Next
strAdditionalInfo &= Environment.NewLine
strAdditionalInfo &= BuildExceptionMessage(e)
strMiniDumpLocation = SaveMiniDump()
ShowErrorForm(strAdditionalInfo, strMiniDumpLocation, e.Exception.Message)
End Sub
显示窗体的代码。您会注意到它将 MiniDump
文件的路径、Error.Message
以及有关错误的附加信息传递给窗体,供用户访问。
Private Shared Sub ShowErrorForm(ByVal strAdditionalInfo As String,
ByVal strMiniDumpLocation As String, ByVal strErrorMessage As String)
Dim frmError As New ErrorReporter(strAdditionalInfo, strMiniDumpLocation,
strErrorMessage)
frmError.Show()
End Sub
最后,收集异常对象中所有我们可以获取的数据的重载例程。
Public Shared Function BuildExceptionMessage(
ByVal ex As UnhandledExceptionEventArgs) As String
BuildExceptionMessage = ""
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ExceptionObject IsNot Nothing Then BuildExceptionMessage &=
"ExceptionObject: " & ex.ExceptionObject.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "IsTerminating: " & ex.IsTerminating.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
Public Shared Function BuildExceptionMessage(
ByVal ex As System.Threading.ThreadExceptionEventArgs) As String
BuildExceptionMessage = ""
Dim x As Integer
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.Exception.GetType IsNot Nothing Then BuildExceptionMessage &=
"Exception Type: " & ex.Exception.GetType.ToString & Environment.NewLine &
Environment.NewLine
If ex.Exception.Message IsNot Nothing Then BuildExceptionMessage &=
"Exception Message: " & ex.Exception.Message & Environment.NewLine &
Environment.NewLine
If ex.Exception.TargetSite IsNot Nothing Then BuildExceptionMessage &=
"Exception TargetSite: " & ex.Exception.TargetSite.ToString &
Environment.NewLine & Environment.NewLine
For x = 0 To ex.Exception.Data.Count - 1
BuildExceptionMessage &= "Exception Data " & x & ": " &
ex.Exception.Data.Item(x).ToString & Environment.NewLine
Next
If ex.Exception.Data.Count <> 0 Then BuildExceptionMessage &= Environment.NewLine
If ex.Exception.Data IsNot Nothing Then BuildExceptionMessage &= "Data: " &
ex.Exception.Data.ToString & Environment.NewLine & Environment.NewLine
If ex.Exception.Source IsNot Nothing Then BuildExceptionMessage &=
"Exception Source: " & ex.Exception.Source & Environment.NewLine &
Environment.NewLine
If ex.Exception.InnerException IsNot Nothing Then BuildExceptionMessage &=
"Exception InnerException: " & ex.Exception.InnerException.ToString &
Environment.NewLine & Environment.NewLine
If ex.Exception.StackTrace IsNot Nothing Then BuildExceptionMessage &=
"Exception StackTrace: " & ex.Exception.StackTrace & Environment.NewLine &
Environment.NewLine
If ex.Exception.GetBaseException IsNot Nothing Then BuildExceptionMessage &=
"Exception BaseException: " & ex.Exception.GetBaseException.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "Exception HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &=
"Exception ToString: " & ex.ToString & Environment.NewLine &
Environment.NewLine
BuildExceptionMessage &= "GetHashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "GetType: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
Public Shared Function BuildExceptionMessage(ByVal ex As System.Exception) As String
BuildExceptionMessage = ""
Dim x As Integer
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Exception Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.Message IsNot Nothing Then BuildExceptionMessage &= "Exception Message: " &
ex.Message & Environment.NewLine & Environment.NewLine
If ex.TargetSite IsNot Nothing Then BuildExceptionMessage &=
"Exception TargetSite: " & ex.TargetSite.ToString & Environment.NewLine &
Environment.NewLine
For x = 0 To ex.Data.Count - 1
BuildExceptionMessage &= "Exception Data " & x & ": " &
ex.Data.Item(x).ToString & Environment.NewLine
Next
If ex.Data.Count <> 0 Then BuildExceptionMessage &= Environment.NewLine
If ex.Data IsNot Nothing Then BuildExceptionMessage &= "Data: " &
ex.Data.ToString & Environment.NewLine & Environment.NewLine
If ex.Source IsNot Nothing Then BuildExceptionMessage &=
"Exception Source: " & ex.Source & Environment.NewLine & Environment.NewLine
If ex.InnerException IsNot Nothing Then BuildExceptionMessage &=
"Exception InnerException: " & ex.InnerException.ToString &
Environment.NewLine & Environment.NewLine
If ex.StackTrace IsNot Nothing Then BuildExceptionMessage &=
"Exception StackTrace: " & ex.StackTrace & Environment.NewLine &
Environment.NewLine
If ex.GetBaseException IsNot Nothing Then BuildExceptionMessage &=
"Exception BaseException: " & ex.GetBaseException.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "Exception HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &=
"Exception ToString: " & ex.ToString & Environment.NewLine &
Environment.NewLine
BuildExceptionMessage &= "GetHashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "GetType: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
驱动窗体的代码非常简单且注释良好。有一个例程值得稍微解释一下。以下例程在按下“报告 Bug”按钮时执行。它将根据已设置的变量确定要执行的操作是 SendMail
还是 Process.Start
。如果操作是 SendMail
,那么这里找到的模块 用于调用 MAPI32
来打开默认电子邮件客户端并相应地填充字段。
我曾尝试在不同的线程中运行 SendToMAPI
例程,但发现由于某种原因,它无法正常工作。如果您有任何建议,我很乐意实施。
Private Sub btnBug_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnBug.Click
If SendEmail Then
Dim myMessage As New SendToMAPI.SendToMAPI.MAPI
Dim strAddress As String
If IncludeDumpAttachment Then
myMessage.AddAttachment(_strMiniDumpLocation)
End If
If ToAddress IsNot Nothing Then
For Each strAddress In ToAddress
myMessage.AddRecipientTo(strAddress)
Next
End If
If CCAddress IsNot Nothing Then
For Each strAddress In CCAddress
myMessage.AddRecipientCC(strAddress)
Next
End If
If BCCAddress IsNot Nothing Then
For Each strAddress In BCCAddress
myMessage.AddRecipientBCC(strAddress)
Next
End If
myMessage.SendMailPopup(SubjectPreface & _strErrorMessage, _
BodyPreface & _strAdditionalInfo)
Else
Dim strBugReportPathModified As String = BugReportPath
strBugReportPathModified = strBugReportPathModified.Replace_
("", SubjectPreface & _strErrorMessage)
strBugReportPathModified = strBugReportPathModified.Replace_
("", BodyPreface & _strAdditionalInfo)
System.Diagnostics.Process.Start(strBugReportPathModified)
Me.WindowState = FormWindowState.Minimized
End If
End Sub
关注点
关于这一点,有一个重要的事情需要注意,几乎所有内容都是 Public Shared
或 Private Shared
例程或变量。这是因为未处理异常或线程异常的处理程序必须是 Public Shared
。因此,不应实例化这些类,正如示例应用程序中正确所示的那样。
历史
- 2009 年 10 月 22 日:代码得到了大幅更新,以支持更好的错误电子邮件发送。尝试进行了多线程处理,但由于电子邮件处理的新方式,因此未能实现。
- 2009 年 2 月 17 日:第二次修订增加了关于如何处理
BackgroundWorker
线程中生成的错误的说明。 - 2009 年 2 月 9 日:这是该代码的第一次修订,非常基础。
它的设计目的是在用户崩溃您的应用程序时返回至少一些数据,以便您可以看到更多关于发生情况的信息。如果有人提出扩展此功能的建议,我将很乐意考虑实施。