通常在程序崩溃时,我们要保存崩溃时程序的运行信息,如导出包含运行栈的 Mini Dump、保存最后一瞬间的截图等,最后如果可以的话将收集的信息传输到自己的服务器上。如果自写代码实现这些功能,工作量会比较大;对于第三方,根据我搜索的资料,实现了类似功能的第三方平台有 BugSplat (付费服务),第三方库有 CrashRpt 和 BugTrap。
这几个第三方平台各有优缺点,对我而言因为我暂时用在小项目上,因此在对比了之后暂时使用的是 BugTrap,名意为臭虫陷阱。在这里简单记录下使用方法和使用心得。
Windows 内置有一个用于未处理错误的处理程序,但这个对话框到底讲了啥?如果我们不希望把数据发送到 Microsoft,我们基本上获取不到有用的信息。
而 BugTrap 有以下优点:
使用 BugTrap 后我们可以很简单的把崩溃提示换成如下图所示的样式(敏感信息已隐藏),是不是很棒!
BugTrap 以动态链接库的形式供使用,使用这种形式是因为崩溃后需要独立句柄才能做到发送数据。链接库同时提供了 Unicode 和 ANSI 两种形式,不过现在的应用程序默认都是编译 Unicode 了吧。
以 MFC 应用举例, 下面是从 BugTrap 官方例程中提取并修改的代码:
// stdafx.h // #endif // _AFX_NO_AFXCMN_SUPPORT 后 #include "BugTrap/BugTrap.h" // Link with one of BugTrap libraries. #if defined _M_IX86 #if defined _DEBUG #pragma comment(lib, "BugTrapUD.lib") #else #pragma comment(lib, "BugTrapU.lib") #endif #elif defined _M_X64 #if defined _DEBUG #pragma comment(lib, "BugTrapUD-x64.lib") #else #pragma comment(lib, "BugTrapU-x64.lib") #endif #else #error CPU architecture is not supported. #endif // #ifdef _UNICODE 前
// MFC 应用主类 CBugTrapTestApp::CBugTrapTestApp() { // 设置异常处理程序,以下信息均显示在错误报告对话框中 BT_SetAppName(_T("BugTrap Test")); BT_SetSupportEMail(_T("your@email.com")); BT_SetFlags(BTF_DETAILEDMODE | BTF_EDITMAIL | BTF_ATTACHREPORT| BTF_SCREENCAPTURE); BT_SetSupportURL(_T("http://www.intellesoft.net")); // = BugTrapServer 报告上传服务器 ======================= //BT_SetSupportServer(_T("localhost"), 9999); // 非 VS6 均需要 BT_InstallSehFilter(); }
记得把 .lib 文件放在项目目录下,原生 C++ 也是差不多的思路。
如果使用的是 MFC,框架产生的错误都会被 CException
拦截,但我们也可以使用 BugTrap 代替它。而方法也很简单,几乎不用对代码进行更改,只需要从BTWindow
类派生窗口类即可。
BTWindow
类是一个模板, 以基本窗口类的名称为参数。我们只需要对窗口类使用 BTWindow
模板即可,如:
class CMainFrame : public BTWindow <CFrameWnd>
其他使用上的问题,请参阅使用手册。
在做完上面的设置之后,已经可以正常使用 BugTrap 了。 我们可以 在设置异常处理程序后,使用断点语句 _asm int 3
来测试 BugTrap 是否正常工作。注意不要直接在 Visual Studio 中运行调试,因为 VS 的调试器会在 BugTrap 前捕捉所有错误,导致 BugTrap 根本不起作用(被坑了一小时时间的人语重心长地说道)。
在错误报告中显示符号信息需要 PDB 或 MAP 文件,在需要分发的情况下推荐生成一份 MAP 文件备用。原因是由于 MAP 文件只包含了符号的链接信息,会比 PDB 文件小很多。
在 BugTrap 可以正常使用之后,我们想要对捕捉流程做些自定义也很简单。
如果我们想在程序崩溃前对程序做些处理,比如说增加几条日志提示发生了严重错误,则我们可以使用 BT_SetPreErrHandler
函数设置预处理器。
void CALLBACK logging_err_handler(INT_PTR nErrHandlerParam) { BOOST_LOG_TRIVIAL(fatal) << "---------------------------------------------------------"; BOOST_LOG_TRIVIAL(fatal) << "Fatal Error! Please check the dump files."; } Automaton::Automaton() { ... BT_SetPreErrHandler(logging_err_handler, 0); }
其中第二个参数可以传递一个指针,以便在处理函数中使用。
错误报告对话框在 BugTrap 中是高度可定制的。举例来说,默认对话框的第一句话是 “A Crash has been detected by BugTrap”。如果我们不想显示 BugTrap 的信息,可以使用以下代码来覆盖:
BT_SetDialogMessage(BTDM_INTRO1, L"A Crash has been detected");
我在上面截图中的程序中便使用了这行代码,其中的 BTDM_INTRO1
表示设定的是介绍文本第一行,其他值需要参考头文件里的描述。
有时候,除了某些 BUG,部分逻辑问题也会导致程序崩溃。在这种情况下,标准错误报告可能无济于事。而自定义日志记录可能是最有效的错误预防机制。BugTrap 可以在详细模式下(设定了标志 BTF_DETAILEDMODE
),将任意数量的其他文件附加到报告中。
BT_AddLogFile(log_file_name.c_str());
如果要从 Windows 注册表中导出某些项并将其附加到报告中,则可以利用内置的 BugTrap 函数:
BT_AddRegFile(_T("Settings.reg"), _T("HKEY_CURRENT_USER\\Software\\My Company\\My Application\\Settings"));
BugTrap 还另外提供了一套内置的日志系统,由于我使用的是 boost::log,没有深入对其进行了解。需要的可以自行阅读使用手册相关的内容。
不论是服务器上传或是邮件传输,在收到错误报告后我们都可以通过 BugTrap 附带的 CrashExplorer 小程序来阅览报告。若是收到的报告中只有堆栈地址而没有符号信息,前面我们生成的 MAP 文件就派上了用场。只需要在“Map/Pdb Folder”中设定我们相应的文件就可以显示详细的数据啦,精确到哪一行代码,是不是很方便。
BugTrap 正如其名,是一个方便易用的错误报告处理器。以前有挺多游戏及程序采用了这个模块来收集错误报告,如 CF、LoL 等。这次也只是做了简单的应用,使用时也请多翻阅使用手册。
基于我个人的使用需求,部分高级功能如服务器上传等都没有研究,请大家多多包含。能帮到你们更是再好不过了,若有更好的错误报告处理模块,欢迎大家告诉我,谢谢!
winEvent
已经被 nativeEvent
所取代。为此我基于网络上的文档,在 PyQt 5.12 上重新编写了串口监视模块。
通知的 lParam
参数会包含变化的端口数据,需要转成 DBT 结构体才可以正常读出。DEV_BROADCAST_HDR
其实是一个通用结构体,需要读取设备类型才能判断要用什么具体的结构体来转换。我之前看一些文章里串口也是用的 DEV_BROADCAST_DEVICEINTERFACE
,结果无法正确读取路径,必须要用对应的 DEV_BROADCAST_PORT
才能读出变更端口的名称,注意一定要对应!
DBT 结构体需要参考微软 API 文档进行编写: https://docs.microsoft.com/en-us/windows/desktop/api/dbt/ns-dbt-dev_broadcast_port_w
在 PyQt 中,只有 QWidget
或继承 QWidget
的类才拥有句柄,并且拥有 nativeEvent
和 closeEvent
等函数的事件机制,而 QObject
是没有的,这就是为什么我的模块用的是 QWidget
。
另外,如果你想直接在主窗口使用,而不是像我这样包成一个类,可以不用注册与反注册通知,因为在最顶层的窗口 PyQt 会默认注册这项通知。
import ctypes import logging from ctypes import wintypes from PyQt5 import QtCore, QtWidgets RegisterDeviceNotification = ctypes.windll.user32.RegisterDeviceNotificationW UnregisterDeviceNotification = ctypes.windll.user32.UnregisterDeviceNotification class GUID(ctypes.Structure): _fields_ = [ ('Data1', wintypes.DWORD), ('Data2', wintypes.WORD), ('Data3', wintypes.WORD), ('Data4', wintypes.BYTE * 8), ] def __str__(self): return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format( self.Data1, self.Data2, self.Data3, ''.join(["{:02x}".format(d) for d in self.Data4[:2]]), ''.join(["{:02x}".format(d) for d in self.Data4[2:]]), ) class DEV_BROADCAST_PORT(ctypes.Structure): _pack_ = 1 _fields_ = [("dbcp_size", wintypes.DWORD), ("dbcp_devicetype", wintypes.DWORD), ("dbcp_reserved", wintypes.DWORD), ("dbcp_name", wintypes.WCHAR * 256)] class DEV_BROADCAST_HDR(ctypes.Structure): _fields_ = [("dbch_size", wintypes.DWORD), ("dbch_devicetype", wintypes.DWORD), ("dbch_reserved", wintypes.DWORD)] DBT_DEVTYP_PORT = 0x00000003 DBT_DEVICEREMOVECOMPLETE = 0x8004 DBT_DEVICEARRIVAL = 0x8000 WM_DEVICECHANGE = 0x0219 DBT_DEVTYP_DEVICEINTERFACE = 5 DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000 DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 0x00000004 logger = logging.getLogger("SerialEnumerator") class SerialEnumerator(QtWidgets.QWidget): device_discovered = QtCore.pyqtSignal(str) device_removed = QtCore.pyqtSignal(str) def __init__(self): super().__init__() self.setupNotification() def setupNotification(self): dbp = DEV_BROADCAST_PORT() dbp.dbcp_size = ctypes.sizeof(DEV_BROADCAST_PORT) dbp.dbcp_devicetype = DBT_DEVTYP_DEVICEINTERFACE self.hNofity = RegisterDeviceNotification(int(self.winId()), ctypes.byref(dbp), DEVICE_NOTIFY_WINDOW_HANDLE) if self.hNofity == 0: logger.debug(ctypes.FormatError(), int(self.winId())) logger.debug("RegisterDeviceNotification failed") def closeEvent(self, event): super().closeEvent(event) if self.hNofity: UnregisterDeviceNotification(self.hNofity) def nativeEvent(self, eventType, message): retval, result = super().nativeEvent(eventType, message) if eventType == "windows_generic_MSG": msg = wintypes.MSG.from_address(message.__int__()) if msg.message == WM_DEVICECHANGE: if DBT_DEVICEARRIVAL == msg.wParam or DBT_DEVICEREMOVECOMPLETE == msg.wParam: dbh = DEV_BROADCAST_HDR.from_address(msg.lParam) if dbh.dbch_devicetype == DBT_DEVTYP_PORT: dbd = DEV_BROADCAST_PORT.from_address(msg.lParam) if DBT_DEVICEARRIVAL == msg.wParam: self.device_discovered.emit(dbd.dbcp_name) elif DBT_DEVICEREMOVECOMPLETE == msg.wParam: self.device_removed.emit(dbd.dbcp_name) return retval, result
在程序中基于类定义一个对象,并连接 device_removed
与 device_discovered
信号至对应的槽函数即可,当设备变化时将会传送端口的友好名字(例如 COM1
)过来。