编程 - 酷极网 https://blog.kugeek.com Hintay's Blog Sat, 08 Aug 2020 08:58:15 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.6.2 使用 BugTrap 捕获 Win C++ 程序的崩溃信息 https://blog.kugeek.com/catch-bugs-in-cplusplus-by-bugtrap.html https://blog.kugeek.com/catch-bugs-in-cplusplus-by-bugtrap.html#respond Sun, 05 Jan 2020 15:45:00 +0000 https://blog.kugeek.com/?p=1308 由于 C++ 没有标准的崩溃报告系统,而且未处理的异常都会导致程序崩溃。最后操作系统会弹出丑丑的“已停止工作”对话框,也不能存下什么有用的信息。为了解决这个问题,我们一般都会自写代码处理崩溃信息,或像我一样采取第三方库或平台。

通常在程序崩溃时,我们要保存崩溃时程序的运行信息,如导出包含运行栈的 Mini Dump、保存最后一瞬间的截图等,最后如果可以的话将收集的信息传输到自己的服务器上。如果自写代码实现这些功能,工作量会比较大;对于第三方,根据我搜索的资料,实现了类似功能的第三方平台有 BugSplat (付费服务),第三方库有 CrashRptBugTrap

这几个第三方平台各有优缺点,对我而言因为我暂时用在小项目上,因此在对比了之后暂时使用的是 BugTrap,名意为臭虫陷阱。在这里简单记录下使用方法和使用心得。

为什么是 BugTrap?

Windows 内置有一个用于未处理错误的处理程序,但这个对话框到底讲了啥?如果我们不希望把数据发送到 Microsoft,我们基本上获取不到有用的信息。

Microsoft 默认崩溃提示框

而 BugTrap 有以下优点:

  • 简单易用,可快速添加到现有项目而不用更改太多代码。
  • 支持 C++ x86 / x64 及 .Net。
  • 友好的提示界面,元素均可自定义。
  • 崩溃时自动收集崩溃数据, 包含 Mini Dump、软件版本、屏幕截图等。
  • 在用户同意后可通过网络或邮件传输,并可附加自己的日志文件。

使用 BugTrap 后我们可以很简单的把崩溃提示换成如下图所示的样式(敏感信息已隐藏),是不是很棒!

简单模式的错误报告
包含详细信息的错误报告

在代码中增加支持

原生 C++

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 框架

如果使用的是 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 文件小很多。

增加 MAP 文件生成

自定义捕捉

在 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 等。这次也只是做了简单的应用,使用时也请多翻阅使用手册。

基于我个人的使用需求,部分高级功能如服务器上传等都没有研究,请大家多多包含。能帮到你们更是再好不过了,若有更好的错误报告处理模块,欢迎大家告诉我,谢谢!

参考资料

]]>
https://blog.kugeek.com/catch-bugs-in-cplusplus-by-bugtrap.html/feed 0
PyQt5 on Windows 监视串口变化 https://blog.kugeek.com/pyqt5-on-windows-serial-enumerator.html https://blog.kugeek.com/pyqt5-on-windows-serial-enumerator.html#respond Thu, 06 Jun 2019 14:28:02 +0000 https://blog.kugeek.com/?p=1112 因为手上的其中一个项目需要监视 USB 或串口,网上可以搜到很多基于 Windows 通知机制以及 PyQt4 的资料。但在此项目基于的 PyQt5 中,事件函数 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 的类才拥有句柄,并且拥有 nativeEventcloseEvent 等函数的事件机制,而 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_removeddevice_discovered 信号至对应的槽函数即可,当设备变化时将会传送端口的友好名字(例如 COM1)过来。

]]>
https://blog.kugeek.com/pyqt5-on-windows-serial-enumerator.html/feed 0