(最后跟新了一下后来使用到的传 dump时候带客户端参数的应用。)
介绍的地方用了大概几个文章,贴一下先:http://bigasp.com/archives/450 ;http://bigasp.com/archives/458;http://www.cnblogs.com/cswuyg/p/3286244.html
正常来说软件开发使用之后都需要很重要的维护部分,程序的异常崩溃用户可能很难描述清楚,所以这对于开发者是很但蛋疼的,因为很难找到,所以你如果能在软件崩溃的时候,吧当时的情况记录下来,这样对于你的分析修改是很有用处的,其实windows原生的api已经足以你实现这个功能,将当时的环境记录下来传输到你的服务器,在这里我们就只说一下其中我目前用到的一个第三方库 google breakpad 其实查看他的源码也能知道也是利用的那几个关键的windows的api,但是加上了自己的一层机制吧算是。Google breakpad是一个非常实用的跨平台的崩溃转储和分析模块,他支持Windows,Linux和Mac和Solaris。由于他本身跨平台,所以很大的减少我们在平台移植时的工作,毕竟崩溃转储,每个平台下都不同,使用起来很难统一,而Google breakpad就帮我们做到了这一点,不管是哪个平台下的崩溃,都能够进行统一的分析。由于用到的是知识涉及简单的客户端,服务端,所以就暂时不讨论到文本符号生成。而dump解析的话,我利用的是在release中加入调试信息,然后生成pdb配合传上来的dump进行代码的定位解析。可以看一下上一篇文章。
安装的话首先把源码弄下来,然后需要用python windowsSDK编译,我好像用的是python2.7 ,设置好环境之后就是首先需要生成工程文件sln,在命令行先运行一下:set GYP_MSVS_VERSION=2010,因为我是2010的vs,然后是cd到目录去运行gyp.bat:src\tools\gyp\gyp.bat --no-circular-check src\client\windows\breakpad_client.gyp(好像说用全路径会出错,不过我没试过),然后就会有一个sln文件了,下来打开编译,一般是没问题了,程序里自带的一个测试的程序,但记住,dump的默认位置保存在C:Dumps下,必须注意先建立好目录,不然会无法使用,或者自己写一下文件夹建立的判断。启用一个server,一个client 应该就能测试了。
对了然后记得,在qt下的使用还有一些问题,由于qt库的运行时库选项是动态的,所以这里vs编译出来的库也必须改一下运行时的选项,看一下我前面这个文章。然后在pro文件夹里,添加一下相应的lib进来,然后最后就是把需要的源文件包进来了,这里的话我是利用一个pri文件,然后在pro文件里include进来,这样不会太乱:
INCLUDEPATH += $$PWD
INCLUDEPATH += $$PWD/src/
# Windows
win32:HEADERS += $$PWD/src/common/windows/string_utils-inl.h
win32:HEADERS += $$PWD/src/common/windows/guid_string.h
win32:HEADERS += $$PWD/src/client/windows/handler/exception_handler.h
win32:HEADERS += $$PWD/src/client/windows/common/ipc_protocol.h
win32:HEADERS += $$PWD/src/google_breakpad/common/minidump_format.h
win32:HEADERS += $$PWD/src/google_breakpad/common/breakpad_types.h
win32:HEADERS += $$PWD/src/client/windows/crash_generation/crash_generation_client.h
win32:HEADERS += $$PWD/src/common/scoped_ptr.h
win32:SOURCES += $$PWD/src/client/windows/handler/exception_handler.cc
win32:SOURCES += $$PWD/src/common/windows/string_utils.cc
win32:SOURCES += $$PWD/src/common/windows/guid_string.cc
win32:SOURCES += $$PWD/src/client/windows/crash_generation/crash_generation_client.cc
暂时用到的只有这些,所以靠过来的代码也不会太大。下面讲一下大概的结构还有实现吧。
googlebreakpad实现功能可以通过进程外或者进程内的进程捕获两种来实现。这里偷懒贴一下别人的了:最简单的是使用进程内dump捕获,使用者只需要跟ExceptionHandler打交道,在自己的程序里定义一个ExceptionHandler对象,ExceptionHandler会挂上异常处理、CRT参数错误处理、purecall错误处理,当发生crash时,breakpad会写好dump,然后回调通知使用者。进程内dump并不推荐,但也不算太差,它在程序启动时就开启了一个“Handler thread”,等到有crash,触发该线程去写dump,写完回调使用者,从google的久未更新的ClientDesign文档可以猜到以前是只有进程内写dump的,它已经符合了让dump尽可能真实而设置下的规定。进程外写dump,使用者一样要定义一个ExceptionHandler对象,这对象有管道名称。另外还需要写一个server进程,server进程负责:写dump、上传dump,当客户进程发生crash时,只需要通过Event置位通知服务进程。server进程只需要定义一个breakpad提供的CrashGenerationServer类对象。客户进程和服务进程是通过管道通信的,通信可以只发生在客户进程初始化阶段,server进程要先于客户进程启动,否则客户进程就会因为管道连接不上而使用进程内dump捕获。
进程内、外dump捕获,都是异步而阻塞的,异步具体是说,进程内dump会让写dump、回调通知使用者写dump完成在另一个安全的线程中做;进程外dump会让写dump在另一个进程中做、回调通知写dump完成在crash线程中做、dump上传可以放到另一个进程中做。阻塞具体是说,虽然发生crash的线程把dump相关的工作扔给别人做了,但是它会等待别人的工作做完才继续完下走。
这里就说一下,进程内的dump处理,不推荐的原因可能是因为堆坏了导致的崩溃,这时候异常处理函数里又干了堆内存分配的事情,那肯定就又继续crash。然后另外进程外的处理其实,在初始化的时候,客户端就已经把各种消息的地址之类的信息都给了服务端,他们之间只有通过管道的联系,当crash发生时,客户端只是一个事件发出,服务端其实会调用底层readprocessmemory去自己获得需要的信息,生成dump,然后在通知一个生成完毕的事件。
交互的流程其实是大概这样一个图,不过记得服务进程要记得先打开注册,否则客户端可能就会无法注册而转成进程内的dump处理(如果实在需要由客户端来启动服务端,那后面有讲通过检测管道是否已经注册然后来决定是否去打开服务进程):
然后下面就贴一下最简单的功能实现的代码了:
HANDLE pipe_handle = NULL;
if(QProcess::startDetached(pathStr+"/crash_server.exe", QStringList()))
{
pathStr+="/dumps";
qDebug()<<"start crashPro :"+pathStr;
const wchar_t s_pPipeName[] = L"\\\\.\\pipe\\breakpad\\crash_handle_name";
//循环尝试
HANDLE pipe;
for(int i=0;i<kPipeConnectMaxAttempts;i++){
pipe = CreateFile(QString::fromWCharArray(s_pPipeName).toStdWString().c_str(),kPipeDesiredAccess,0,NULL,OPEN_EXISTING,
kPipeFlagsAndAttributes,NULL);
if (pipe != INVALID_HANDLE_VALUE)
break;
Sleep(kPipeSleepTimeMs);
}
//判断是否连接
if(INVALID_HANDLE_VALUE == pipe ){
qDebug()<<"connect to pipe failed";
}else{
pipe_handle = pipe;
qDebug()<<"connect to pipe success";
}
//注册客户端
const std::wstring s_strCrashDir = pathStr.toStdWString();
google_breakpad::ExceptionHandler *pCrashHandler = new google_breakpad::ExceptionHandler(
s_strCrashDir,//path
0,//filter before decide continue pro or not
&PubTools::onMinidumpDumped,//after write
0,//callback content
google_breakpad::ExceptionHandler::HANDLER_ALL,
MiniDumpNormal,
pipe_handle,//if null is in_process dumpcatch
NULL//clientInfo
);
}else
qDebug()<<"start crashPro failed";
}
bool PubTools::onMinidumpDumped(const wchar_t* dump_path,
const wchar_t* minidump_id,
void* context,
EXCEPTION_POINTERS* exinfo,
MDRawAssertionInfo* assertion,
bool succeeded) {
qDebug()<<"onMinidumpDumped";
return succeeded;
}
这里就是用的CreateFile来判断服务端的注册然后决定拉起服务端进程,不需要这个的话,直接先打开服务端,在打开客户端这个就可以了。服务端的话:
QString pathStr = QApplication::applicationDirPath();
pathStr+="/dumps";
ckpathStr = pathStr+"/ckp";
qDebug()<<pathStr;
const wchar_t s_pPipeName[] = L"\\\\.\\pipe\\breakpad\\crash_handler_name";
const std::wstring s_strCrashDir = pathStr.toStdWString();
pCrashServer = new google_breakpad::CrashGenerationServer(
s_pPipeName,
NULL,
onClientConnected,
NULL,
onClientDumpRequest,
this,
onClientExited,
this,
onClientUploadRequest,
NULL,
true,
&s_strCrashDir);
if(!pCrashServer->Start()) {
qDebug()<<"start faild";
delete pCrashServer;
pCrashServer = NULL;
return false;
}else
qDebug()<<"start success";
if (_wmkdir(s_strCrashDir.c_str()) && (errno != EEXIST)) {
qDebug() << "unable to create dmp directory";
return false;
}
if (_wmkdir(ckpathStr.toStdWString().c_str()) && (errno != EEXIST)) {
qDebug() << "unable to create ckp directory";
return false;
}
这其实就是最基本的功能了,然后上传的部分就不细说了,最好在onClientExited中调,这样不会阻塞住crash的客户端的退出:
void sendDmpFile(const std::wstring& checkpointFile, const std::wstring& url,
const std::map<std::wstring, std::wstring>& paras, const std::wstring& dmpFileName,
std::wstring* reportCode)
{
google_breakpad::CrashReportSender rpter(checkpointFile);
google_breakpad::ReportResult res = rpter.SendCrashReport(url, paras, dmpFileName, reportCode);
if(res == google_breakpad::RESULT_SUCCEEDED){
qDebug() << "send success.";
if(QFile::remove(QString::fromWCharArray(dmpFileName.c_str())))
qDebug() << "remove file success";
return;
}
if(res == google_breakpad::RESULT_THROTTLED){
qDebug() << "send failed, exceed the maximum reports per day.";
return;
}
if(res == google_breakpad::RESULT_REJECTED){
qDebug() << "send failed, server reject the request.";
}
else{
qDebug() << "send failed, failed to communicate with the server, try later.";
}
for(int i = 1; i < 4; ++i)
{
qDebug() << "Retry " << i;
if(rpter.SendCrashReport(url, paras, dmpFileName, reportCode)
== google_breakpad::RESULT_SUCCEEDED) {
qDebug() << i<<" times send success.";
break;
}
qDebug() << i<<" times failed";
}
if(QFile::remove(QString::fromWCharArray(dmpFileName.c_str())))
qDebug() << "remove file success";
}
就只是很简单的实现了,需要更多什么,就要自己修改了。这样拿到的dump结合上个文章所说的pdb其实就能很容易的定位了,其实这个方法也是有不好的地方,其它的方法,之后会再尝试。
最后记得如果你上传使用的是googlebreakpad的sender的话,除了包含那几个头文件,还需要,包含window的api:winINet.lib。
后续跟新:
关于使用的时候,可能是多个客户端来使用同一个的服务端分析,那么就需要进行客户端信息的记录传递来给予服务端分析,或者后续上传的分析依据。
首先在客户端的时候,就需要有一个全局的结构来存储这些信息,类似于(这里我是用了个函数然后来进行返回客户端的信息,可以继续添加需要的键值对):
//客户端自定义信息的函数,用于注册时获取信息对象(可继续添加键值对)
google_breakpad::CustomClientInfo* GetCustomInfo() {
static google_breakpad::CustomInfoEntry name_entry(L"appName", APPNAME);
static google_breakpad::CustomInfoEntry ver_entry(L"appVersion", APPVER);
static google_breakpad::CustomInfoEntry entries[] = {name_entry, ver_entry};
static google_breakpad::CustomClientInfo custom_info = { entries, ARRAYSIZE(entries) };
return &custom_info;
}
然后就是简单的修改客户端注册时候的参数:
google_breakpad::ExceptionHandler *pCrashHandler = new google_breakpad::ExceptionHandler(
s_strCrashDir,//path
0,//filter before decide continue pro or not
&PubTools::onMinidumpDumped,//after write
0,//callback content
google_breakpad::ExceptionHandler::HANDLER_ALL,
MiniDumpNormal,
pipe_handle,//if null is in_process dumpcatch
GetCustomInfo()//clientInfo
);
这里顺带说一下实现例如服务端检测到客户端数量没有后可以自动结束,只要在onconnect里面实现一个计数就好了,然后exit的时候做一个判断:
对了其中的PopulateCustomInfo函数,是由于使用中发现,在正常情况下breakpad不会受到客户端的dump请求,然后exit的时候居然拿不到client_info信息,一看源码发现居然connect里面没有去取这个客户端的信息结构... 而是要到接到dump请求才做.. 所以对于无论什么情况都要获取到信息的话,那就需要在onconnect中去取信息,查看源码就会发现上述的函数就是做的这个工作,但是由于要修改到里面的这个结构,而传入的参数是const,所以要转一下。
void onClientConnected(void* context,const google_breakpad::ClientInfo* client_info){
google_breakpad::ClientInfo* temp = const_cast<google_breakpad::ClientInfo*>(client_info);
if(!temp->PopulateCustomInfo()) //这里populatecustominfo函数
qDebug()<<"PopulateCustomInfo failed";
qDebug()<<"onClientConnected";
//服务端基数增加
((CrashHandle*)context)->clientNum +=1;
qDebug()<<"clientNum"<<((CrashHandle*)context)->clientNum;
}
而后就是在接收到dump请求之后的处理,原理和之前讲的一样,就是小小的修改了一下关于接受客户端信息的,这里我是通过建立一个服务端的成员变量map来记录不同的客户端的crash,以及路径。(可以通过传来的参数如appname做一个区别)至于传到回掉函数里就可以通过调用的void* context这个参数了,强转一下类型就能处理服务端的成员了。然后最后exit的时候就可以通过判断是否在map中有存在这个客户端的信息来判断是不是发生了crash:
void onClientDumpRequest(void* context,const google_breakpad::ClientInfo* client_info,const std::wstring* file_path){
qDebug()<<"onClientDumpRequest";
//取crash程序的自定用户信息
google_breakpad::CustomClientInfo custom_info = client_info->GetCustomInfo();
QString appname = QString::fromWCharArray(client_info->GetCustomInfo().entries[0].value);
qDebug()<<"inseart in the map with appname:"<<appname;
//做crash标记,传路径
((CrashHandle*)context)->dumpFile_pathMAP.insert(appname,QString::fromStdWString(std::wstring(*file_path)));
// ((CrashHandle*)context)->dumpFile_path = QString::fromStdWString(std::wstring(*file_path));
}
然后就是最后的exit了,记得之前的计数吧,这里可以响应的减少,调用的函数中判断到减少为0就能自动的结束程序。而这里主要是因为发信号参数类型需要定义(map这些默认是不行的,需要注册),所以避免麻烦我就拆开成两个list发。
void onClientExited(void* context,const google_breakpad::ClientInfo* client_info){
qDebug()<<"onClientExited";
//取crash程序的自定用户信息
google_breakpad::CustomClientInfo custom_info = client_info->GetCustomInfo();
QStringList nameList;
QStringList valueList;
for(int i =0;i<custom_info.count;i++){
nameList<<QString::fromWCharArray(custom_info.entries[i].name);
valueList<<QString::fromWCharArray(custom_info.entries[i].value);
}
//计数-1
((CrashHandle*)context)->clientNum -=1;
//发信号提示是否上传,调用上传的函数
((CrashHandle*)context)->clientExit(nameList,valueList);
qDebug()<<"emit";
}
而最后就是发送的东西了,判断map来判断是否crash,没有则是正常的推出,然后是用一个map类型去作为参数,调用sendDmpFile就成功啦~最后记得可以通过检查client数来决定自己结束程序否。
const std::wstring checkpoint_path = ckpathStr.toStdWString();//L"C:\\Dumps\\ckp";
std::wstring response[RESPONSEBUFFER];
//加入参数
std::map<std::wstring, std::wstring> paras;
for(int i =0;i<nameList.count();i++)
{
wchar_t name[64]={0};
nameList.at(i).toWCharArray(name);
wchar_t value[64]={0};
valueList.at(i).toWCharArray(value);
paras[name] = value;
}
sendDmpFile(checkpoint_path, url.toStdWString(), paras, ((QString)(it.value())).toStdWString(), response);
dumpFile_pathMAP.remove(appname);
send的函数上面之前有写了,最后就是自己要接受的一个服务器,接收到post,然后利用键值对的name去获取相应的值就可以了哈~