向操作系统的事件管理器报告重大信息是一种非常有用的方式,特别是对于没有界面的后台服务而言。如果你对Windows编程有一定了解,应该很快就能想到使用ReportEvent这个API,然后快速写出下面的程序:

VOID TestReportEvent1()
{
// Get a handle to the event log.
HANDLE h = RegisterEventSource(NULL, // Use local computer.
_T(“EventLogDemo”)); // Event source name.
if (h == NULL)
{
printf("Cannot register the event source.");
return;
} TCHAR szEventMsg[] = _T("文件备份服务已启动.");
LPCTSTR lpInput[] = {szEventMsg} ; if (!ReportEvent(h, // Event log handle.
EVENTLOG_INFORMATION_TYPE, // Event type.
NULL, // Event category.
, // Event identifier.
NULL, // No user security identifier.
, // Number of substitution strings.
, // No data.
lpInput, // Pointer to strings.
NULL)) // No data.
{
printf("Cannot report the event.");
}
else
{
printf("Report Event Successfully.\n");
} DeregisterEventSource(h);
return;
}

这个程序运行正常,到事件管理器中查看,确实多了一条记录。

实际上我们报告的内容只有红框中的一个字符串布局,但是在查看日志时,日志管理器会告诉我们一堆东西,说事件描述未找到之类的,反正就是不太正常。

那正常的应该是什么样的呢?看下图:

干干净净,清清爽爽,这才是正确的结果。

那如何实现这个效果呢?需要分下面几步来实现。

一、编写mc文件

mc文件是一个消息描述文件,它定义了消息的ID和消息的格式。不知道怎么写没关系,参考MSDN中的例子,直接拷贝以下模板就行了。

 ; // ***** TestEventLog.mc *****

 ; // This is the header.

 MessageIdTypedef=DWORD

 SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
) FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
) LanguageNames=(Chinese=0x804:MSG00804) ; // The following are message definitions. MessageId=
Severity=Informational
Facility=Runtime
SymbolicName=MSG_SERVICE_START
Language=Chinese
文件备份服务已启动.
. MessageId=
Severity=Informational
Facility=Runtime
SymbolicName=MSG_SERVICE_STOP
Language=Chinese
文件备份服务已停止.
.

上面的部分不需要改动,LanguageNames往下才是自己要定义的消息内容。注意消息的内容并不是以换行结束的,必须在后面起一个新行写上一个"."作为结束符(第30、38行),否则会提示 EventLog.mc(linenum) : error : Unterminated message definition,根据错误提示的行号进行修改就可以了。

通常来说,程序报告给事件管理器的事件都是预先定义好的事件,比如服务启动了、停止了,或者出现了某错误等等。上面的mc文件中定义了两个消息,MessageId就是将来在事件管理器中看到的事件ID,Serverity表示事件的严重程度,可以从上面的SeverityNames中选一个,常用的是Informational表示成功或普通信息,Warning为警告信息,Error为错误信息。SymbolicName即符号名称,它由用户根据自己的需要进行命名,比如服务启动事件可以命名为MSG_SERVICE_START,在编写代码时会用到这个名字,具体怎么用的,后面再讲。

这时读者可能会有一个疑问,就是这些消息内容都是固定的,但是实际运行过程中会有一些变化的信息,比如本例作为一个文件备份服务,想在备份成功时报告成功的是哪个文件,备份失败的时候报告操作失败的文件名称和错误代码,这些变化的信息怎么加入到消息内容中去呢?

事实上,事件消息的正文可以是固定的一句话,也可以是模板,其中需要替换的内容按顺序用%n替换,n的取值范围是1到99。

比如:将目标文件备份到 %1 时出错,错误代码为 %2.

在使用这个模板时,只需要提供两个变量就可以了,事件系统会自动分别替换模板中的%1和%2。具体如何操作,后面会讲到。

保存mc文件时可以使用与操作系统相同的语言编码,也可以使用Unicode编码。如果mc文件中使用了多种语言,那么最好使用Unicode编码保存该文件。

二、编译mc文件

Visual Studio提供了一个程序mc.exe专门用于编译mc文件。如果VS的可执行程序路径已经添加到了PATH环境变量中,那么直接在存放mc文件的目录中打开cmd窗口,执行如下命令(本例中文件名为EventLog.mc):

如果mc文件编写没有问题,那么编译通过时没有其它的额外提示,否则会提示你错误信息及错误所在的行号,根据具体提示进行修改就可以了。

如果mc文件的保存格式是Unicode的,这时的编译参数要变成mc –u –U EventLog.mc

第一个-u表示输入文件是Unicode编码,第二个-U表示输出文件要使用Unicode编码,如果软件支持多国语言,那最好是使用Unicode编码。

编译成功后,会生成至少三个文件,分别是一个h文件,一个rc文件,一个MSG开头的bin文件,具体文件名与mc文件中定义的LanguageNames有关,比如中文就是MSG00804.bin。

接下来要做的,就是把这个rc文件编译到程序中去,它将成为资源的一部分。通常有两种做法,一种是编译为纯资源dll,一种是编译到exe本身,选择哪种纯属个人爱好。如果你的程序本来就是由多个模块组成的,那么就编译成dll好一些,如果你只有一个exe,不想额外携带dll,那就编译到exe本身。

编译为纯资源dll的方法仍然是使用命令行:

先编译rc文件,最后使用Link命令编译为纯资源dll。

如果要编译到exe,那么在exe本身的rc文件中添加一行:

#include "EventLog.rc" 

就可以了。

EventLog.rc的内容其实很简单,如下:

LANGUAGE 0x4,0x2

  MSG00804.bin 

实际是一个类型为11,名称为1的资源自定义资源文件,文件指向MSG00804.bin。

所以,如果使用包含方式有问题的话,可以直接把上面两行内容添加到exe本身的rc文件中。

如果把mc文件加入到工程中的话,需要设置自定义编译操作(注意选择"所有设置",就不需要每个编译配置都重新设置一次了,除非你需要不同的配置选项)。

VC6设置如下(原谅我还在用VC6写一些小程序):

VS2010的设置如下:

小提示:

由于每次mc文件编译后会重新生成这几个文件,如果把这几个文件添加到了工程里就会导致IDE重复提示"文件已修改,是否重新加载"的情况。所以,只添加mc文件到工程中,但不要添加生成的h文件和rc文件。h文件只是不添加到工程中,包含使用是没有问题的。

三、注册事件源

这个按MSDN中的操作来就可以了。

具体来说就是:

在KEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog下面添加相应的键值。这里有几个大类,主要的是"Application","Security"和"System",分别对应于系统的三个日志频道。通常,我们自己的应用程序报告的事件写在Application频道下,当然也可以定义自己的频道。

比如,测试程序名为TestEventLog,那么就注册到Application\TestEventLog下面,具体来说就是添加几个键值,如下:

具体代码可以直接抄MSDN:

BOOL AddEventSource(LPCTSTR lpszChannelName, LPCTSTR lpszSourceName , LPCTSTR lpModulePath)
{
BOOL bResult = FALSE ;
DWORD dwCategoryNum = ;
HKEY hk;
DWORD dwData, dwDisp;
TCHAR szBuf[MAX_PATH] = {};
size_t cchSize = MAX_PATH; __try
{
// Create the event source as a subkey of the log.
_stprintf(szBuf,
_T("SYSTEM\\CurrentControlSet\\Services\\EventLog\\%s\\%s"),
lpszChannelName, lpszSourceName); if (RegCreateKeyEx(HKEY_LOCAL_MACHINE, szBuf,
, NULL, REG_OPTION_NON_VOLATILE,
KEY_WRITE, NULL, &hk, &dwDisp))
{
printf("Could not create the registry key.\n");
__leave;
} // Set the name of the message file. if (RegSetValueEx(hk, // subkey handle
_T("EventMessageFile"), // value name
, // must be zero
REG_EXPAND_SZ, // value type
(LPBYTE) lpModulePath, // pointer to value data
(DWORD) (lstrlen(lpModulePath)+)*sizeof(TCHAR))) // data size
{
printf("Could not set the event message file.\n");
__leave;
} // Set the supported event types. dwData = EVENTLOG_ERROR_TYPE | EVENTLOG_WARNING_TYPE | EVENTLOG_INFORMATION_TYPE; if (RegSetValueEx(hk, // subkey handle
_T("TypesSupported"), // value name
, // must be zero
REG_DWORD, // value type
(LPBYTE) &dwData, // pointer to value data
sizeof(DWORD))) // length of value data
{
printf("Could not set the supported types.\n");
__leave;
} // Set the category message file and number of categories. if (RegSetValueEx(hk, // subkey handle
_T("CategoryMessageFile"), // value name
, // must be zero
REG_EXPAND_SZ, // value type
(LPBYTE) lpModulePath, // pointer to value data
(DWORD) (lstrlen(lpModulePath)+)*sizeof(TCHAR))) // data size
{
printf("Could not set the category message file.\n");
__leave;
} if (RegSetValueEx(hk, // subkey handle
_T("CategoryCount"), // value name
, // must be zero
REG_DWORD, // value type
(LPBYTE) &dwCategoryNum, // pointer to value data
sizeof(DWORD))) // length of value data
{
printf("Could not set the category count.\n");
__leave;
} bResult = TRUE;
}
__finally
{
if (hk != NULL)
{
RegCloseKey(hk);
}
} return bResult;
}

然后调用:

AddEventSource(_T("Application"),_T("EventLogDemo"),szModulePath);

szModulePath就是包含了消息定义资源的dll或者exe的路径。

这样,事件源就注册好了。

四、如何报告日志

到这一步,才算做好了所有的准备工作。

报告事件仍然使用以下三个API:

RegisterEventSource得到事件管理器的句柄

ReportEvent进行报告

DeregisterEventSource关闭事件源句柄

注意再来看一下ReportEvent的参数:

BOOL ReportEvent(
__in HANDLE hEventLog, //事件源句柄
__in WORD wType, //消息类型:信息、警告、错误
__in WORD wCategory,
__in DWORD dwEventID,//事件ID
__in PSID lpUserSid,
__in WORD wNumStrings, //要插入到模板中的字符串数量
__in DWORD dwDataSize,
__in LPCTSTR* lpStrings, //变量字符串组
__in LPVOID lpRawData
);

其中wNumStrings就是模板中要插入的变量字符串数量,lpStrings就是变量字符串数组,在显示日志内容时,事件查看器会使用数组中的内容依次替换掉事件字符串模板中的%1、%2这些变量;而dwEventID就是前面在mc文件中定义的MessageId,要使用它,需要包含mc文件编译生成的那个头文件。它的部分内容如下:

除了模板之外,还可以有附加数据,以二进制形式提供,需要使用到dwDataSize和lpRawData两个参数,相信对于这两个参数如何使用应该不会陌生。

由于每次报告事件都需要得到事件源的句柄,所以可以把这一过程简单包装下:

BOOL ReportEventDebugMsg(WORD EventType,DWORD dwEventId,WORD cInserts,LPCTSTR *lpStrings)
{
BOOL bResult = FALSE ;
HANDLE h = RegisterEventSource(NULL, // Use local computer.
g_szEventSourceName); // Event source name.
if (h == NULL)
{
printf("Cannot register the event source.");
return FALSE;
} SetLastError(ERROR_SUCCESS);
bResult = ReportEvent(h,EventType,NULL,dwEventId,NULL,cInserts,0,lpStrings,NULL);
printf("Report Event %s , Errcode = %d .\n",bResult?"Success":"Failed",GetLastError());
DeregisterEventSource(h);
return bResult;
}

接下来可以这么调用(根据前面的定义,共报告服务启动、操作成功、操作失败、服务停止四个事件):

//正确的报告方式
VOID TestReportEvent3()
{
//Report Service Start
ReportEventDebugMsg(EVENTLOG_INFORMATION_TYPE,MSG_SERVICE_START,0,NULL); //使用模板报告特定事件
LPCTSTR lpInputStrings[2] = {0};
TCHAR szFilePath[MAX_PATH] = _T("D:\\test.dat");
TCHAR szErrCode[32] = _T("32"); //Report something Success
lpInputStrings[0] = szFilePath;
ReportEventDebugMsg(EVENTLOG_INFORMATION_TYPE,MSG_BACKUP_DONE,1,lpInputStrings); //Report something Error
lpInputStrings[0] = szFilePath;
lpInputStrings[1] = szErrCode;
ReportEventDebugMsg(EVENTLOG_ERROR_TYPE,MSG_BACKUP_FAIL,2,lpInputStrings); //Report Service Stop
ReportEventDebugMsg(EVENTLOG_INFORMATION_TYPE,MSG_SERVICE_STOP,0,NULL);
printf("Report Finished.\n");
}

最后来看一下效果:

共产生了4个事件。

再看一下其中含有变量的两个事件:

达到了预期效果,这才是完整的、正确的ReportEvent的使用方式。

ReportEvent的正确使用方式的更多相关文章

  1. iOS开发小技巧--相机相册的正确打开方式

    iOS相机相册的正确打开方式- UIImagePickerController 通过指定sourceType来实现打开相册还是相机 UIImagePickerControllerSourceTypeP ...

  2. const、static和extern的正确使用方式

    我们在看一些大牛的第三方时,里面会出现很多const.static和extern,尤其是const和static,const和extern的结合使用,直接令很多小伙伴懵逼了,今天就详细讲解一下这三个关 ...

  3. @synthesize的正确使用方式

    @synthesize的正确使用方式 一. @synthesize的错误使用方式 类1和类2是继承关系, name是类1的属性 但是类2的实现里加入了@synthesize name = _name; ...

  4. Xcode 的正确打开方式——Debugging(转载)

    Xcode 的正确打开方式——Debugging   程序员日常开发中有大量时间都会花费在 debug 上,从事 iOS 开发不可避免地需要使用 Xcode.这篇博客就主要介绍了 Xcode 中几种能 ...

  5. 以正确的方式开源 Python 项目

    以正确的方式开源 Python 项目 大多数Python开发者至少都写过一个像工具.脚本.库或框架等对其他人也有用的工具.我写这篇文章的目的是让现有Python代码的开源过程尽可能清 晰和无痛.我不是 ...

  6. 以正确的方式开源 Python 项目 - 技术翻译 - 开源中国社区

    以正确的方式开源 Python 项目 - 技术翻译 - 开源中国社区 以正确的方式开源 Python 项目 英文原文:Open Sourcing a Python Project the Right ...

  7. 以正确的方式开源 Python 项目(转)

    大多数Python开发者至少都写过一个像工具.脚本.库或框架等对其他人也有用的工具.我写这篇文章的目的是让现有Python代码的开源过程尽可能清晰和无痛.我不是简单的指——“创建一个GitHub库,提 ...

  8. IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式

    转载请注明来源:四个空格 » IntelliJ IDEA中Mapper接口通过@Autowired注入报错的正确解决方式: 环境 ideaIU-2018.3.4.win: 错误提示: Could no ...

  9. C#语法——泛型的多种应用 C#语法——await与async的正确打开方式 C#线程安全使用(五) C#语法——元组类型 好好耕耘 redis和memcached的区别

    C#语法——泛型的多种应用   本篇文章主要介绍泛型的应用. 泛型是.NET Framework 2.0 版类库就已经提供的语法,主要用于提高代码的可重用性.类型安全性和效率. 泛型的定义 下面定义了 ...

随机推荐

  1. Emgu安装配置及使用

    前言:项目需要,需使用图像处理来完成机械臂从运动的皮带上抓取物体的功能,所以又重拾视觉与图像处理内容. 内容:Emgu是OpenCV的一个跨平台的.NET封装,结构如下图所示: 下载地址:http:/ ...

  2. springcloud微服务实战--笔记--1、基础知识

    微服务的问题: 分布式事务和数据一致性. 由于分布式事务本身第实现难度就非常大,所以在微服务架构中,我们更强调在各服务之间进行无事务第调用,而对于数据一致性,只要求数据在最后第处理状态是一致第即可:若 ...

  3. Ajax的跨域问题

    •跨域问题概述 •出于安全考虑,浏览器不允许ajax跨域获取数据 •可以通过script的src加载js的方式传递数据 fn({"a":"1","b& ...

  4. android菜鸟学习笔记17----Android数据存储(一)文件读写

    假如有如下需求,要求能够记录用户输入的用户名和密码,下次登录时,能直接获取之前保存的用户名密码,并在相应的EditText中显示. 要保存用户输入的数据,最先想到的应该就是文件读写了. 通过对andr ...

  5. VMware虚拟机下安装RedHat Linux 9.0

    从这一篇文章开始我和大家一起学习Linux系统.不管是什么样的系统,必须安装上才能谈使用对吧. Linux版本 安装Linux之前需要了解一下Linux系统的安装版本. Linux的版本分为内核版本和 ...

  6. 微信小程序 原生代码 转wepy 字符串处理

    import globimport os cwd = os.getcwd()sep = os.septarget = cwd + sep + 'pages' + sep + '*' + sep + ' ...

  7. UVa 10828 Back to Kernighan-Ritchie 高斯消元+概率DP

    题目来源:UVa 10828 Back to Kernighan-Ritchie 题意:从1開始 每次等概率从一个点到和他相邻的点 有向 走到不能走停止 求停止时每一个点的期望 思路:写出方程消元 方 ...

  8. redux和mobx比较(二)

    Redux Redux 是 JavaScript 状态容器,提供可预测化的状态管理. 三大核心 在 Redux 中,最为核心的概念就是 action .reducer.store 以及 state,那 ...

  9. CentOS已经安装命令,但提示找不到

    今天在虚机上装了个CENTOS.装好后,好多命令都提示找不到,如tcpdump.arp.ifconfig.查看安装包,都已经安装过. ------------无敌分割线------------- # ...

  10. VLAN虚拟局域网技术(三)-计算机网络

    本文主要知识来源于学校课程,部分知识来自于H3C公司教材,未经许可,禁止转载.如需转载,请联系作者并注明出处. 本节主要介绍 pVLAN和 动态VLAN. 1.   pVLAN:英文全称Private ...