6. 事件记录

当一个典型的软件应用程序必须在某些特殊情况下让使用者知道时,通常会使用视觉或听觉返回的方式。软件大多数会给予这种类型事件报告的享受,因为它可以建立一个重要的假定:当它正在执行,而一个人类坐在机器的前面。然而,大部份的伺服软件并不能在上述之假设情形中执行。

因此,服务器开发者使用文件或者一些其他类似持久稳固的储存器来保存经由软件报告的事件记录。然后系统管理者便可以经常地察看记录文件并且持续的监看重要事件与错误的情形。如此解决了没有人负责坐在机器前操作的问题,但是它引出了一个新的问题:管理能力。什么样的系统管理者喜欢使用许多会在系统的许多地方以不同文件格式储存许多事件报告的伺服应用程序?而在这个情形下甚至不会考虑由系统本身报告的事件。

Microsoft Windows在介绍一个标准的事件报告机制时,提出了这个管理能力的议题:Event Log服务。Event Log服务加强了标准的记录格式和透过使用所提供之单一事件检视器应用程序来使系统管理者可以容易察看记录并且可以一次就将所有的操作完成。Windows使用Event Log服务报告像硬盘空间不足和尝试登录失败这种的系统事件。当然,您的伺服软件不须要使用事件记录的功能,然而您的使用者将会欣赏一个与实际系统同样好用的另一个伺服软件。

在本章中,我们将学习有关事件记录与如何编写回报事件软件的方法。包含了学习如何去编译和利用讯息文件与事件关联的部份。事件的回报是大部份应用程序开发者所关心的,通常任何管理者都会需要在事件检视器嵌入式管理单元中读取事件,而我们也将会涵盖如何设计一个应用程序来读取事件记录的内容。

那么让我们花点时间先从一个管理者的角度来探索事件记录,然后再从系统的观点来讨论。

事件记录是什么?
 

从系统管理者的角度来看,事件记录是一个由系统或应用程序软件所发布的讯息清单。这个讯息清单被组织至一个称为log files(或logs)逻辑的群组。记录的收集通常被称作 事件记录 。系统管理者对事件记录的窗口即是被安装在Windows 2000中的Microsoft Management Console(MMC)之事件检视器嵌入式管理单元。您可以按下 开启 并指向 程序集  系统管理工具 ,然后选择 事件检视器选项 来开启事件检视器。您也可以在 系统管理工具 中选择 电脑管理 来存取事件检视器。图6-1显示了在电脑管理中的事件检视器嵌入式管理单元。


 

 图6-1 在电脑管理中的事件检视器嵌入式管理单元

在MMC中的事件检视器节点里,您可以看见一组记录。当您选择了一个记录时,右边的窗格会显示关于该记录的事件资讯。在一个事件项目上双按滑鼠会得到关于该事件的详细资讯。接下来将讨论在每一个事件中找到的资讯,但首先必须先讨论不同记录的用途。

在预设值下,您的系统之事件记录会包含叁个记录:应用程序记录档、系统记录档以及安全性记录档。应用程序记录档在系统中添加自己的记录;然而,这并非一般或常见的需求。如果您决定回报至您所拥有的记录档中,那么您要在左边的窗格中选择事件检视器节点并从 执行 功能表中选择 开启记录档 选项,以告知事件检视器嵌入式管理单元。如此会产生一个允许您开启一个记录档的开启对话方块。在事件检视器嵌入式管理单元中察看记录时,您必须至少回报一个事件至自订的记录中。

表6-1定义了叁个标准的事件记录。因为这本书的主题是编写伺服应用程序,所以应用程序记录档将是我们最有兴趣的部份。

 表6-1 在事件检视器中的标准事件记录档
记录名称 说明 应用程序记录档 包含经由应用程序软件与服务产生的事件。 系统记录档 包含经由设备驱动程序与其他的作业系统元件产生的事件。 安全性记录档 包含经由安全稽核产生的事件。

现在让我们花点时间来剖析一个被记录的事件项目。一个事件即是在事件记录中的单一项目,而它由以下的资讯栏位组成:事件类型、被产生的日期与时间、被写入的日期与时间、事件来源、事件类别、事件识别码以及系统。除了这个资讯外,每一个事件可以包含一个详细的文字说明并拥有与它关联的二进位资料。事件检视器嵌入式管理单元能显示最多的资讯。表6-2提供了每个栏位的简单说明。

大部份栏位的意义已经非常清楚,但是事件来源、事件识别码、事件类别与事件类型则需要更多的解释。

事件来源描述了应用程序、服务或系统元件所报告的事件。回报的原因和事件来源间存在着典型的一对一关系。然而,回报的事件代码决定了所回报的来源,所以单一的应用程序能够回报若干来源。同样地,多个程序可以回报一个单一的来源。Windows不透过任何方法来限制这个回报的灵活性。

事件识别码是一个被来源定义的值,指示了一个某些事件的类型。任何的事件皆可以经由一个事件来源与识别码的合成而被确认。例如,Browser服务定义事件识别码8021为「浏览器无法从浏览主控制器 中取回一个网路上的服务器清单……」,还有事件识别码8033为「浏览器被强迫当选……」。

事件类别是一个事件可任意选择的来源定义类别。就它对一个大量的不同类型事件的回报,得进一步中断到逻辑类别里的应用程序和系统元件而言是有益的。

 表6-2 在一个事件记录中的项目栏位
栏位 说明 事件类型 确认事件的类型。系统定义了五个不同的事件类型,列示在表6-3中。 被产生的日期与时间 确认被加入一个记录的事件之所需来源时间。 被写人的日期与时间 确认系统在一个记录档中被记录的项目。 事件来源 确认元件是加入事件至一个记录的来源。通常来源是一个应用程序或是服务。 事件类别 确认一个为了事件而被来源定义的类别。 事件识别码 确认一个被来源定义的号码,该号码为指示事件种类的唯一值,且会导致一个项目被加至记录中。 使用者 确认产生事件项目之使用者帐户内容。这个值是一个使用者的安全识别项(Security Identifier,SID)。请参阅  以取得SID的更多讨论内容。 系统 确认产生事件的机器。

您必须决定哪一个事件种类是必须的或是对您的软件有帮助。如果一个事件来源选择去忽略类别,那么事件检视器嵌入式管理单元将会从来源回报事件没有类别的情形。

事件类型可以是被列在表6-3之五个被系统定义的事件类型之一。回报事件的软件会选择事件类型。

 表6-3 事件类型
事件类型 说明 EVENTLOG_INFORMATION_TYPE 资讯事件表明对应用程序或系统没有发生疑问的情况或者操作—例如,服务应用程序的启动或者停止。 EVENTLOG_WARNING_TYPE 等待可能的重要性事件或未来的问题情况—例如,相当低的内存或磁盘空间,如果资源被继续使用可能会产生问题。 EVENTLOG_ERROR_TYPE 当一个应用程序或系统元件的一部份功能真的失败时,该错误的事件会被记录—例如,不能写入资料到磁盘可能会导致资料遗失。 EVENTLOG_AUDIT_SUCCESS 当成功的完成一个稽核时会被Windows安全性记录档记录一个成功稽查的事件。 EVENTLOG_AUDIT_FAILURE 当一个稽核的动作被执行且失败时,Windows会记录一个失败的稽查事件。

回报事件
 

在进入自己的事件回报前,先讨论一些关于您的软件应该回报什么事件到事件记录中的内容。然后我们会尝试学习如何去编写回报事件的软件。

什么事件应该被回报?
 

如果您正在开发一个服务应用程序,那么您很可能正想去做一些事件回报的动作。在您这么做之前,无论如何您都必须真正地了解事件记录的内容。记得事件记录是系统管理员从您的服务返回的来源。一个回报无意义事件或太多事件的服务就如同一个显示了太多讯息方块给使用者的应用程序一样,会惹恼系统管理员。

大部份开发者发现决定在哪一种情况下可以授权一个错误事件的记录是件容易的事;而同样的,当您的软件需要发布一个警告事件时,也能容易且适度地被决定。然而,您进入了一个资讯事件的灰色地带。什么软件的活动重要得足以记录一个资讯事件?如果您从系统管理者察看的角度来考虑,那么您通常可以回答这个问题。

没有管理者想在一天结束后还要费力地进行察看几百个或甚至更多资讯事件的动作,只为了找到与她的工作有关的一或两个项目。所以您应该考虑一个「重要性」方面的问题即是在特定情况下的共用问题。一个Web服务器很可能不会为了每个它所接收的连结而记录一个事件。另一方面,它可能会想要为每个因为它太繁忙,无法处理要求而被拒绝存取的连结回报一个事件。然而记录这个事件可能并非必要。如果连结被拒绝时,有一个折衷方案可能是每小时即记录一个事件。该记录可能会包含在一些被拒绝存取连结总数的详细说明中。一个可能性是建立此种回报选项,以使管理者能在被回报的说明中挑挑拣拣。

另一个事件资讯的使用将会回报在您的软件状态中罕见的改变。例如,您可能会想在每次您的服务被暂停或继续执行时便产生一个事件。或者假定您的软件进入了待命模式,释放某些资源就某种时间而言,关系着连结的负载何时是处于较低的情形—这种情形可以允许一个资讯事件的发生。状态改变事件资讯可以是有用的,因为它们给予管理者一个在一个警告或错误事件发生前您的软件完成了什么活动的检测功能。

然而,您要避免将您的应用程序当做一个追踪资讯的储存处并成为考虑事件记录的习惯。这个侦错资讯的型态将会克服管理者与大部份可能会使它忽略每个被您的应用程序所建立之事件。每个事件同样会占用您系统之磁盘空间,所以有效的使用储存媒体也是一个重要的事。如果您的服务器将会回报许多的事件,或者会从许多事件来源回报事件,那些您可能会想要考虑将您的事件记录至一个自订的记录档中。最后在这里必须将一般的常识做个统一;然而您可以依照可能的使用者配置而建立您的应用程序回报机制,以提供使用者最好的记录。

我们已经在关于什么事件应该被回报的内容讨论已经足够。现在让我们进入如何将事件回报的部份。

如何回报事件
 

回报软件事件是一个简单的处理程序,但是在您能够确实利用事件记录前,需要了解如何将它们组合起来。一起拼凑它们之最容易的方法要先看最简单的部份,即是那些回报处理程序。

如果您的处理程序想要开始回报事件至事件记录,它需要使用以下的函数去登录一个事件来源:

HANDLE RegisterEventSource(
	PCTSTR pszMachineName,
	PCTSTR pszSourceName);

pszMachineName参数为确认您想要将事件项目加入之包含记录档的系统。传递NULL给此参数会在本机中开启记录档。很少有事件会被回报至一个远端机器上的记录文件中。

pszSourceName参数即是事件来源的名称。此名称是被显示在事件检视器嵌入式管理单元中的来源栏位;它不一定是您的可执行程序名称(但是它通常会是)。

说明

我必须提供一个关于安全性的词语:安全性与系统记录档皆是安全的。为了要使您的应用程序加入事件至这些记录文件之一,当它呼叫RegisterEventSource时,您的处理程序必须在拥有安全性内容的权限下执行。它的规则是:安全性记录档只能在使用本机帐户时才可以被写入,而系统记录档则可以在使用本机帐户与管理者帐户时被写入。

如果RegisterEventSource的执行是成功的,它会回传一个有效的handle。您的应用程序即可使用此handle来回报事件。记得所有的handle都一样,当您已完成它时应该让系统知道。您会使用一个呼叫DeregisterEventSource的函数来做这件事:

BOOL DeregisterEventSource(HANDLE hEventLog);

至目前为止一切皆很顺利,对不对?回报一个事件也相当地繁琐,您可以使用一个呼叫至以下的函数:

BOOL ReportEvent(
	HANDLE	hEventLog,
	WORD	wType,
	WORD	wCategory,
	DWORD	dwEventID,
	PSID	psidUser,
	WORD	wNumStrings,
	DWORD	dwDataSize,
	PCTSTR*	ppszStrings,
	PVOID	pvRawData);

hEventLog参数是被RegisterEventSource回传的handle。wType参数则是表6-3所列之五个事先定义类型之一。wCategory和dwEventID参数是您随意选择的值。dwEventID参数将会唯一地识别您的事件来源。事件识别码可以是任意值,并且以任何您想要的顺序来安排。然而,一个指示事件之被定义来源群组的事件类别必须拥有以1启动的值并从那里开始进行。该0值是被保留来指示没有类别时使用的。psidUser参数确认一个使用者,它没有在事件上隐含安全性的限制。NULL时常被传递以说明与任何使用者无关。

ppszStrings参数指向一个与事件关联的文字字串阵列,而wNumStrings是一些阵列中的字串。(我将会在本章之〈建立讯息DLL与Exe〉一节中讨论ppszStrings参数。)最后,pvRawData参数指向一个与事件关联的二进位资料区块。dwDataSize参数指出以位元组为单位之资料区块尺寸。该资料被回报它的应用程序定义,而且它可以是任何您觉得会对使用者或管理者有用的资料。例如,网路驱动程序把原始材料放在被驱动程序所报告的某些事件里。

根据文件所提,刚刚所描述的函数是关于记录事件至事件记录档之所有您需要知道的事情。技术上来说,这是事实;不过,有一些要点被删掉了。如果您把我们使用ReportEvent传递给Event Log服务的资讯与事件检视器嵌入式管理单元所显示的一个典型事件做比较,您将会发现缺少了一些资料。具体而言,我们传递一个类别号码,而事件检视器嵌入式管理单元显示一个文字字串。同样地,并非依照RegisterEventSource或ReportEvent来说明我们想要报告的记录(即应用程序、系统或安全性)。最后,您必须注意ReportEvent缺少了一个对事件详细说明的参数。

依照我曾解释过在应用程序记录档中放置您的事件之预设的方法。记录档包含了详细说明您的事件之一般文字,然后把您的字串资料附加至文字的末端。我们可以在这里停止,但是如果您另外又实作了您自己的详细说明与类别部份,使用者将会非常感谢您,做这件事有助于理解事件记录之设计目标。

讯息文件
 

Windows事件记录的设计者想要建立一个有效的机制以增加语言的独立性。基于这个目标,一个人类可读的事件部分—那就是,类别与详细说明—是从您的应用程序取出到分开的讯息文件中。这些讯息文件被实作成DLL或EXE文件,而它们包含了一个自订之内含您的讯息文字的二进位码资源。本章后面将会解释关于讯息DLL的详细内容,然而现在有几件事是您必须要知道的。

讯息档可以是一个被给定的事件资讯:事件讯息文件、类别讯息文件以及参数讯息文件。一个单一讯息的DLL(或EXE)可以展示它们之中的任何结合,所以您的应用程序可以在一个讯息DLL中储存您的事件讯息文件、类别讯息文件或参数讯息文件;相反的动作也可以成立,例如,可以使用超过一个的讯息DLL为单一事件资源表示事件讯息文件。

说明

通常DLL与EXE文件分别被称为讯息DLL和讯息EXE。为了容易阅读,本章的其馀部份将使用「讯息DLL」来说明,然而,所有资讯皆适用于讯息EXE的部份。

您透过用与您传递给RegisterEventSource的pszSourceName参数相同的名字在子机码下建立少量的登录值使讯息DLL与您的事件来源联系。事件记录档也用它来指定这个新登录机码到您那一些将被记录的来源事件中。一般安装应用程序的软件会将这些项目加到登录中,但是没有说明您的服务不能够加入他们的规则。当该项目已被加入登录时,登录的布局会跟随着以下的阶层而结束:

HKEY_LOCAL_MACHINE
	SYSTEM
		CurrentControlSet
			Services
				EventLog
					Application
						Event Source
							...
					Security
						Event Source
							...
					System
						Event Source
							...
					CustomLog
							...

请注意CustomLog机码位于EventLog之下。在预设情形下,在EventLog机码下只有Application、Security与System机码,但是您的程序代码也可以在标准的记录机码中添加另一个机码以采用一个自订的记录至事件记录中。当您呼叫RegisterEventSource时,系统会为了与pszSourceName参数相符的机码而搜寻位于HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/EventLog/Log下的机码。每一个记录皆被依照字母排序而搜寻,直到发现一个符合的来源子机码为止。

所以您可以看到,事件来源的命名空间被所有标准或自订的记录所分享。如果系统下的事件来源在应用程序中与事件来源相匹配,则它将会被忽略。

为了使您的讯息文件与您的事件来源联系,要在您的事件来源机码下建立适当的登录值。支援的登录值定义在表6-4中。

 表6-4 支援事件来源子机码的登录值
登录值 类型 说明 TypesSupported REG_DWORD 确认一组显示在表6-3中的标记,指出被讯息DLL支援的事件类型。 EventMessageFile REG_EXPAND_SZ 确认到事件讯息文件的路径名称(被分号分开的)。试着将一个事件识别码转换至人类可阅读的字串时,事件检视器会搜寻这个文件设备。 CategoryMessageFile REG_EXPAND_SZ 确认至类别讯息文件的路径名称(被分号分开的)。试着将一个类别识别码转换至人类可阅读的字串时,事件检视器会搜寻此文件设备。 ParameterMessageFile REG_EXPAND_SZ 确认至参数讯息文件的路径名称(被分号分开的)。试着将一个可置换的字串参数识别码转换至人类可阅读的字串时,事件检视器会搜寻此文件设备。 CategoryCount REG_DWORD 确认被事件来源支援的类别数量。

注意在表6-4中所有讯息文件的值皆是REG_EXPAND_SZ资料型别。它意味着路径名称能够包括将在执行时期被扩张的系统环境变数。下面范例中的路径名称设备是任何讯息文件登录值的一个有效值:

"%SystemRoot%/System32 /msg.dll;c:/messages /msg2.dll"

说明

因为您的使用者可能会从一个远端机器上察看您的记录事件,所以将路径以一个通用命名惯例(Universal Naming Convention,UNC)的格式列示至讯息文件通常会比使用一个驱动程序字母与路径还好。当一个事件检视工具(例如事件检视器嵌入式管理单元)查询了一个DLL讯息文件的路径,并在网路分享上发现它时,事件检视器即可以载入DLL并且阅读讯息。如果事件检视器嵌入式管理单元无法确定该被指定之DLL的位置,那么它便无法将识别码转换至人类可读的字串。这里有一个当识别码无法被转换至字串时,事件检视器嵌入式管理单元所显示的内容:


 

注意事件识别码与类型栏位被显示为数字而非字串。同样请注意说明栏位在这种情况下所能显示之最好的资讯。

顺便一提,在一个单一的EXE或DLL文件中包含所有的事件、类别与参数讯息是很常见的。当产生这个情况时,会同时放置叁个讯息文件登录值。

每一个个别的记录档(如应用程序记录档或一个自订的记录档)会被存放在它所拥有的记录档中,其延伸档名为 .evt。正常情况下,事件检视器会抓取登录子机码,附加 .evt延伸档名,并且尝试使用此文件名称去开启一个记录档。然而,您可以撤消此行为并指定一个自订的路径名称给被加至Logname机码下之文件登录值的记录档。这个改变路径名称的方法需要重新启动系统以使改变生效。注意到之前已改变的记录资讯不会显示在新的记录中。

有二个影响一个记录的登录值应该被提出来说明。第一个为MaxSize,它的型别为REG_DWORD。这个值是系统允许记录档之最大值,以位元组表示。第二个值是Retention,它的型别也是REG_DWORD。这个值指定在它自动地从记录档中删除前,一个事件在短时间内应该多大。您的程序代码可以自由的去修改这些值。如果MaxSize与Retention值不存在一个被给定的登录中,系统会预设最大的记录文件大小为512 KB和7天的保留时间。

您不太可能会在登录中手动地改变这些值。更确切地说,您大概会使用事件检视器嵌入式管理单元选择一个记录并显示它的内容对话方块来改变它们。图6-2显示了一个记录的内容对话方块。


 

 图6-2 应用程序记录档的内容对话方块

既然我们已经讨论了用您的事件来源记录事件和与其联系的讯息DLL,那么现在是将讯息文件的细节讨论与上述两个主题结合的时候。

建立讯息DLL与EXE
 

我们还必须回答的问题如下:

讯息文件中的文字如何与一个事件结合?
  ReportEvent的ppszStrings参数如何获得利用?
  我们如何建立一个讯息DLL?
 

本节中将会回答上述的所有问题。图6-3显示了所有的事件记录结构,包含建立一个讯息DLL所需的步骤。您将会在后续的讨论中参考到此图。

您的第一个建立讯息DLL的步骤是为您的讯息来源建立一个文件。此讯息来源一般被称为「MC」文件,而且,可以使用任何您所选择的名称,只要它的延伸档为 .mc。例如,MyMsgs.mc。

您的 .mc文件被架构为一系列的讯息项目,并以任意的顺序安排。请参阅《Platform SDK》文件以取得完整的讯息文件之语法。以下为一个讯息文件的范例:

;/************************************************************** 
;Module name:MyMsgs.mc
;**************************************************************/

;//*********************MESSAGE SECTION ***********************
MessageIdTypedef=DWORD

MessageId=0x1
SymbolicName=MSG_DATE
Language=English
Today's date is %1. %%536871912
.

MessageId=0x2
SymbolicName=MSG_TIME
Language=English
The current time is %1. %%536871912
.

MessageId=
SymbolicName=MSG_SEC
Language=English
The seconds are %1. %%536871912
.

;//****************STRING PARAMETERS SECTION ******************
MessageIdTypedef=DWORD

MessageId=0x100
Language=English
I hope you enjoy today.
.
;//************************END OF FILE ************************


 

 图6-3 事件记录的架构,包含建立一个讯息DLL的步骤

MessageId是与文字关联的识别码。注意到您可以在讯息文件中为这个MessageId栏位省略一个明确的值。这将会导致编译器产生比之前的讯息识别码多的识别码。SymbolicName栏位参考到产生之标头文件中将被使用的已定义巨集。您可以引入被产生的标头档与您的事件回报控制码,并且使用被SymbolicName栏位定义的巨集去确认一个讯息。Language行定义了该讯息使用的语言。您可以使用此方法建立一个为单一讯息识别码鉴定拥有若干语言支援的单一的讯息资源。因为讯息可能是几行长,所以您会在一个单一时期使用一行来指示讯息的结尾。注意此处需要一个延长的期间,而且在您的 .mc文件中的最后一个项目应该在延长期后回传Return键(Carriage Return)。

当您使用ReportEvent记录一个事件时,dwEventID参数指定了事件的识别码。后来事件检视器嵌入式管理单元会使用事件识别码的值搜寻人类可阅读字串之事件讯息文件。同样地,ReportEvent的wCategory参数会与一个在类别讯息文件中识别码相同的讯息关联。

如您所见,讯息文件的语法相当的简单,但是,有些方面需要某些附加的讨论。如前所述,事件记录的主要目的之一即是语言的独立性。讯息资源允许您容易地为了多种语言所包含的讯息。为了达成目的,您只需在被要求的语言内的讯息后面加入一个额外的Language行至每一个讯息的项目即可。它也常被用来将不同的语言编译至相同的讯息DLL中。例如,您的专案可能包含了一个MsgEnglish.dll和一个MsgFrench.dll。只要为您的讯息文件将这二个DLL含入登录项目,事件检视器嵌入式管理单元就可以找到适当的语言所代表讯息。

到目前为止,您可能会认为对于任何给定的事件识别码来说,事件检视器会报告一个不变的细节讯息。如果您认为由编译时期定义的静态字串所组成的一个事件记录机制不是非常有用,那么您是正确的。我们需要对相同的识别码但是为不同文字之回报多个事件的能力。例如,如果您的应用程序回报它无法开启一个文件,您想要使它在事件的说明文字中动态地引入被指定的文件名称。您也许会猜想事件记录的解决方法即是位于ReportEvent内的ppszString参数。

当您建立一个讯息时,该讯息可能包含了指示特定事件字串应被放置在何处的特别字元序列。例如,以下的事件字串指示了二个可置换的字串:

"The file %1 was replaced with the file %2"

如果您使用ReportEvent的ppszStrings传递一个包含二个字串的阵列,事件检视器嵌入式管理单元会使用「%1」取代第一个字串,「%2」则取代第二个字串。

通常,您应该只传递与语言无关的字串值,例如数字、文件名称以及其他资源的名称。尽管您可以传递任何文字而事件检视器嵌入式管理单元会取代它,但是例如传递一个英文片语时,便会破坏事件记录的语言独立性。

可以与扩充的字串一起使用参数代替来从参数讯息文件包含附加的细节字串。当扩充事件字串时,一个「%%」字元序列会被附加在一个指示参数代替的数字后。例如,以下的事件字串指示一个可取代的参数字串:

"This is an example %%237"

当扩充此字串时,事件检视器嵌入式管理单元会搜寻一个识别码为237的字串,且以参数字串取代事件字串的「%%237」之参数讯息文件的讯息表格资源参数。

此外,参数扩充在字串扩充后才被执行,它考虑到复杂的扩充情形。假设讯息字串「Replace with a dynamic parameter %%%1」与一个值为「237」的字串会被传递至ReportEvent。事件检视器嵌入式管理单元会先取代「%1」,然后继续使用在参数讯息文件中有237的识别码取代「%%237」。

如果您将您的事件来源之ParameterMessageFile登录值设定至Kernel32.dll,那么您可以使用从GetLastError回传的值而动态地插入错误讯息文字。例如,假设Kernel32.dll是您的参数讯息文件,而您的讯息字串是「GetLastError() returned the following error: %%%1」。您可以用一个单一字串值「5」呼叫ReportEvent以指示为拒绝存取。事件讯息的结果将会是:

"GetLastError()returned the following error:Access is denied".

能在您的事件说明中插入最后的错误讯息文字,确实是一个有用的特性。

编译您的讯息
 

在您建立 .mc文件后,您需要使用附加在Microsoft Visual Studio中的Message Compiler(Mc.exe)来编译它。以下的文字显示编译器的使用方法:

C:/>mc.exe
Microsoft (R) Message Compiler Version 1.00.5239
Copyright (c) Microsoft Corp 1992-1995.All rights reserved.

用法:MC	[-?vcdwso] [-m maxmsglen] [-h dirspec] [-e extension]
		[-r dirspec] [-x dbgFileSpec] [-u] [-U] filename.mc
	-? - 显示此讯息。
	-v - 产生冗长的输出。
	-c - 在所有的讯息识别码中设定Customer位元。
	-d - 标头档中的FACILTY与SEVERITY值,以十进制值表示。
			  设定标头中的讯息值为以十进制初始。
	-w - 如果讯息文字中包含非OS/2相容之插入物时,发出警告。
	-s -  插入符号连结名称以视为每一个讯息的第一行。
	-o - 产生OLE2标头档(使用HRESULT定义取代状态控制码定义)。
	-m maxmsglen - 如果任何讯息的大小超过maxmsglen所设定之字元数时,产生一个警告。
	-h pathspec - 设定建立C含入文件之路径,预设值是 ./。
	-e extension -	指定档头文件之延伸名称。
									1到3个字元。
	-r pathspec -	设定建立RC含入文件与含入之二进位码讯息资源文件的路径。
								预设值是 ./。
	-x pathspec - 设定建立 .dbg之讯息识别码与符号名称对应的C含入文件路径。
	-u - 输入文件为Unicode格式。
	-U - 在 .BIN文件中的讯息应为Unicode。
	filename.mc - 设定一个用于编译的讯息文字文件名称。
	产生的文件使得保存(Archive)位元被清除。

讯息编译器会剖析您的 .mc文件并产生叁个文件:

 MSG00001.bin 此文件包含了所有二进位码格式的讯息字串。它也包含了对应一个讯息识别号码至字串的资讯。
   MyMsgs.rc 此资源指令码文件中只有一个参考至包含在MSG00001.bin文件的二进位码讯息。
   MyMsgs.h 此文件是一个C/C++ 标头文件,包含了出现在 .mc文件之任何符号名称的#define。您应该在您的原始码模组中呼叫ReportEvent,以含入此文件。
 

资源文件
 

在经由讯息编译器执行MyMsgs.mc后,我用一个非常简单的资源指令档(MyMsgs.rc)来结束它,该文件看起来像图6-4所示的内容一样:


 

 图6-4 一个被讯息编译器产生的资源指令档

您可能已经很熟悉图示、点阵图以及对话方块范本的资源,一个讯息表格不过是资源的另一种型别而已。就像是图示与点阵图,其讯息表格资源是在资源指令中参照的二进位文件,而对话方块范本与功能表范本则嵌入在指令档中。

如果您在Platform SDK中开启WinUser.h标头文件,您将会发现以下被定义之设备符号:

#define RT_CURSOR	MAKEINTRESOURCE(1) 
#define RT_BITMAP	MAKEINTRESOURCE(2)
#define RT_ICON	MAKEINTRESOURCE(3)
#define RT_MENU	MAKEINTRESOURCE(4)
#define RT_DIALOG	MAKEINTRESOURCE(5)
#define RT_STRING	MAKEINTRESOURCE(6)
#define RT_FONTDIR	MAKEINTRESOURCE(7)
#define RT_FONT	MAKEINTRESOURCE(8)
#define RT_ACCELERATOR	MAKEINTRESOURCE(9)
#define RT_RCDATA	MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE	MAKEINTRESOURCE(11)	// 看这里!
#define RT_GROUP_CURSOR	MAKEINTRESOURCE(12)
#define RT_GROUP_ICON	MAKEINTRESOURCE(14)
#define RT_VERSION	MAKEINTRESOURCE(16)
#define RT_DLGINCLUDE	MAKEINTRESOURCE(17)
#define RT_PLUGPLAY	MAKEINTRESOURCE(19)
#define RT_VXD	MAKEINTRESOURCE(20)
#define RT_ANICURSOR	MAKEINTRESOURCE(21)
#define RT_ANIICON	MAKEINTRESOURCE(22)
#define RT_HTML	MAKEINTRESOURCE(23)

讯息表格资源已经被指定为号码11。当您加入资源至一个资源指令码(.rc)档时,您必须为每一个资源的类型指定一个唯一的数字。例如,我可以加入一个识别码为53的图示至我的资源指令码中,然后加入另一个识别码为172的图示至我的资源指令码中。这些数字实际上并不重要,因为它们是唯一的。

讯息表格资源与其他资源的工作有些不同。一个资源指令码文件可以只拥有一个讯息表格资源,而且该资源必须被指定识别码为1。如果您指定一个不同的识别码给讯息表格资源,在使用像事件检视器嵌入式管理单元时将无法将事件与类别识别码转换成人类可阅读的字串。

说明

如果您的讯息DLL或EXE包含了附加的资源,只要将被讯息编译器产生的 .rc文件内容复制至您拥有的 .rc文件中即可。然后您可以丢弃被讯息编译器产生的 .rc档。

使用Visual Studio建立一个讯息文件之专案
 

虽然您可以在每一次改变您的讯息时,手动地执行讯息编译器工具,然而那么做会变得麻烦且乏味。我强烈地建议为您的DLL或EXE加入讯息编译的步骤至您的Visual Studio专案中。由于一些未知的原因,Visual Studio环境无法察觉到这个讯息编译器与 .mc文件,所以您需要在专案中加入自订的建立步骤。步骤如下:

将您的 .mc文件加入EXE或DLL专案中。 显示Project Settings对话方块。 在Custom Build页签中选择 .mc文件。 在说明文字方块中设定您想要的说明文字。我通常会使用「Message Compiler」。 在Commands文字方块中设定「mc -s -U -h $(ProjDir) -r $(ProjDir) $(InputName)」与「del $(ProjDir)/$(InputName).rc」。 在Outputs部份,加入二个项目:「$(InputName).h」与「Msg00001.bin」。该对话方块看起来应该像图6-5所示的内容。按下OK按钮。
 

 图6-5 为 .mc文件加入讯息编译器命令后的Project Settings对话方块
1 11 MSG00001.bin
在呼叫ReportEvent函数的原始码文件中含入被产生的标头档。 在您专案的 .rc档中加入一行包含资源数字为1、资源类型为11,以及文件名称为MSG00001.bin的内容。例如:

在执行此步骤后,若要产生最后的EXE或DLL,Visual Studio就会知道如何去编译您的讯息文字档与含入的资源。注意到如果您建立了一个只含有资源而没有程序代码的DLL模组,那么您应该使用 /NOENTRY连结器开关来防止连接一个进入点并且减少产生模组的大小。

您必须去编写的唯一程序代码即是对登录的设定部份,因此事件检视器嵌入式管理单元能找到这个讯息EXE或者DLL的位置。

MsgTableDump范例应用程序
 

MsgTableDump范例应用程序(「06 MsgTableDump.exe」)是一个简单的工具程序,它可以开启一个被包含在EXE或DLL中的讯息表格资源并且倾印在表格中的所有字串。此范例应用程序的原始程序代码与资源档存放在随书光碟中的06-MsgTableDump目录中。

这个程序代码非常简单易懂。它把讯息表格资源置于特定的模组并沿着每个存在唯读之编辑控制项中的字串而走。图6-6显示了当MsgTableDump在Kernel32.dll上执行时,其输出的情形。


 

 图6-6 MsgTableDump范例应用程序显示kernel32.dll的讯息字串

AppLog范例应用程序
 

AppLog范例应用程序(「06 AppLog.exe」)显示于列表6-1中,它说明从一个应用程序回报事件的内容。它的原始码与资源文件存放在附赠光碟中的06-AppLog目录中。AppLog范例应用程序被实作如同一个标准的应用程序一样,但是如果控制码属于一个服务的话,它便会与回报事件的控制码相同。

当AppLog启动时,它会开启一个本机的应用程序记录并且加入一个指示应用程序己被启动执行的项目。AppLog也会在终止前加入一个项目。在正常的情形下,为了增进执行效能并且不浪费事件记录资料库空间,您不会将这些资讯事件类型回报至事件记录中。

一旦AppLog执行起来,您便可以在编辑控制项中键入一个Win32之错误码并按下Simulate Error按钮。此按钮会使AppLog增加一个事件至系统的应用程序记录档中。当AppLog执行时,您可以依您的喜好而模拟许多Win32的错误。为了察看错误的情形,可以按下Open Event Viewer按钮,以使事件检视器嵌入式管理单元在MMC中显示那些产生错误的原因。

如果您使用事件检视器嵌入式管理单元来看那些项目,您将会看到类别与讯息识别数字与可置换的字串值(被您模拟的错误码数字)。事件检视器无法将类别与讯息识别码对应到它们的英文字串中,直到登录被适当地设定为止。若要将讯息文件模组资讯安装至登录中,可以按下Install Event Message File In Registry按钮。然后回去事件检视器嵌入式管理单元中察看被AppLog加入的事件。此时,您应该会看见识别号码已被转换至适当的字串中。AppLog也允许您按下Remove Event Message File From Registry按钮去删除登录资讯。图6-7显示了AppLog模拟一个错误的情形。


 

 图6-7 AppLog范例应用程序模拟 "Access is denied" (5) 之Win32错误码
AppLog.cpp
/********************************************************************
模组:AppLog.cpp
通告:Copyright (c)2000 Jeffrey Richter
********************************************************************/

#include "../CmnHdr.h"	// 请察看附录A
#include 

#define EVENTLOG_IMPL
#include "EventLog.h"

#include "Resource.h"
#include "AppLogMsgs.h"	// 被MC.exe产生

///////////////////////////////////////////////////////////////////////////////

CEventLog g_EventLog(TEXT("AppLog "));

///////////////////////////////////////////////////////////////////////////////

BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {
	chSETDLGICONS(hwnd, IDI_APPLOG);
	return(TRUE);
}

///////////////////////////////////////////////////////////////////////////////

void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
	switch (id) {
	case IDCANCEL:
		EndDialog(hwnd, id);
		break;

	case IDC_SPAWNEVENTVIEWER:
		// 产生事件检视器嵌入式管理单元
		ShellExecute(hwnd, TEXT("Open"), TEXT("eventvwr.msc"), TEXT("/s"),
			NULL, SW_SHOWDEFAULT);
		break;
	case IDC_INSTALL:
		// 将事件记录参数文件资讯安装至登录中
		chVERIFY(g_EventLog.Install(
			EVENTLOG_INFORMATION_TYPE | EVENTLOG_ERROR_TYPE, NULL,
			TEXT("Kernel32.dll"), 2, NULL));
		break;
	case IDC_REMOVE:
		// 将事件记录参数文件资讯从登录中移除
		chVERIFY(g_EventLog.Uninstall());
		break;

	case IDC_SIMULATEERROR:
		// 回报一个Win32错误事件
		TCHAR szErrorCode[10];
		PCTSTR pszErrorCode = szErrorCode;
		GetDlgItemText(hwnd,IDC_ERRORCODE,szErrorCode, chDIMOF(szErrorCode));
		chVERIFY(g_EventLog.ReportEvent(EVENTLOG_ERROR_TYPE, CAT_APPEVENT,
			MSG_ERROR, CEventLog::REUSER_NOTAPPLICABLE,
			1, (PCTSTR*) &pszErrorCode));
		break;
	}
}

///////////////////////////////////////////////////////////////////////////////

INT_PTR WINAPI Dlg_Proc(HWND hwnd,UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
	chHANDLE_DLGMSG(hwnd, WM_INITDIALOG,	Dlg_OnInitDialog);
	chHANDLE_DLGMSG(hwnd, WM_COMMAND,	Dlg_OnCommand);
	}
	return(FALSE);
}

///////////////////////////////////////////////////////////////////////////////

int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {
	// 回报一个此应用程序已启动之事件
	g_EventLog.ReportEvent(EVENTLOG_INFORMATION_TYPE,
	CAT_APPEXECSTATUS, MSG_APPSTART);
	DialogBox(hinstExe,MAKEINTRESOURCE(IDD_APPLOG), NULL, Dlg_Proc);

	// 回报一个此应用程序已停止之事件
	g_EventLog.ReportEvent(EVENTLOG_INFORMATION_TYPE,
		CAT_APPEXECSTATUS, MSG_APPSTOP);
	return(0);
}

//////////////////////////////// End of File //////////////////////////////////
EventLog.h
/********************************************************************
模组:EventLog.h

通告:Copyright (c)2000 Jeffrey Richter
********************************************************************/

#pragma once   // 经由编译单元含入此标头档

///////////////////////////////////////////////////////////////////////////////

#include "../CmnHdr.h"	/* 请参阅附录A */

/********************************************************************

加入一个讯息编译文件至Visual Studio的建构环境中,并执行以下的步骤:
	1)	插入 *.mc文件至专案中
	2)	在Project Settings对话方块中选择MC文件
	3)	改变说明的内容为「Message Compiler」
	4)	加入以下二个建构命令:
			"mc -s -U -h $(ProjDir)-r $(ProjDir)$(InputName)"
			"del $(ProjDir)/$(InputName).rc"
	5)	加入以下二个输出文件:
			"$(InputName).h"
			"Msg00001.bin"
	6)	在原始程序代码文件中含入呼叫ReportEvent函数而产生的标头文件
	7)	由于我删除了被MC产生的 .rc文件,所以您必须使用一个值为11的资源类型以及值为
	      1的资源识别码。

********************************************************************/

class CEventLog {
public:
	CEventLog(PCTSTR pszAppName);
	~CEventLog();
	BOOL Install(DWORD dwTypesSupported,
		PCTSTR pszEventMsgFilePaths = NULL,
		PCTSTR pszParameterMsgFilePaths = NULL,
		DWORD dwCategoryCount = 0,
		PCTSTR pszCategoryMsgFilePaths = NULL);
	BOOL Uninstall();

	// 记录一个事件到事件记录档中
	enum REPORTEVENTUSER { REUSER_NOTAPPLICABLE, REUSER_SERVICE, REUSER_CLIENT };
	BOOL ReportEvent(WORD wType, WORD wCategory,
		DWORD dwEventID, REPORTEVENTUSER reu = REUSER_NOTAPPLICABLE,
		WORD wNumStrings = 0, PCTSTR *pStrings = NULL,
		DWORD dwDataSize = 0, PVOID pvRawData = NULL);

private:
	PCTSTR	m_pszAppName;
	HANDLE	m_hEventLog;
};

///////////////////////////////////////////////////////////////////////////////

#ifdef EVENTLOG_IMPL

///////////////////////////////////////////////////////////////////////////////

CEventLog::CEventLog(PCTSTR pszAppName) {
	m_pszAppName = pszAppName;
	m_hEventLog = NULL;
}

CEventLog::~CEventLog() {
	if (m_hEventLog != NULL) {
		::DeregisterEventSource(m_hEventLog);
	}
}

//////////////////////////////////////////////////////////////////////////////

BOOL CEventLog::Install(DWORD dwTypesSupported,
	PCTSTR pszEventMsgFilePaths, PCTSTR pszParameterMsgFilePaths,
	DWORD dwCategoryCount, PCTSTR pszCategoryMsgFilePaths) {

	// 确定至少指定了一个有效的Support Type
	chASSERT(0 != (dwTypesSupported &
		(EVENTLOG_INFORMATION_TYPE | EVENTLOG_WARNING_TYPE |
		EVENTLOG_ERROR_TYPE)));

	BOOL fOk = TRUE;
	TCHAR szSubKey[_MAX_PATH];
	wsprintf(szSubKey,
		TEXT("System//CurrentControlSet//Services//EventLog//Application//%s"),
		m_pszAppName);

	// 如果应用程序没有支援任何类型(预设值)
	// 不要为此服务设置一个事件记录档
	HKEY hkey = NULL;
	__try {
		LONG l;
		l = RegCreateKeyEx(HKEY_LOCAL_MACHINE, szSubKey, 0, NULL,
			REG_OPTION_NON_VOLATILE, KEY_SET_VALUE,NULL, &hkey, NULL);
		if (l != NO_ERROR)__leave;
		l = RegSetValueEx(hkey, TEXT("TypesSupported"), 0, REG_DWORD,
			(PBYTE) &dwTypesSupported, sizeof(dwTypesSupported));
		if (l != NO_ERROR)__leave;
		TCHAR szModulePathname[MAX_PATH];
		GetModuleFileName(NULL, szModulePathname, chDIMOF(szModulePathname));

		if (pszEventMsgFilePaths == NULL)
			pszEventMsgFilePaths = szModulePathname;
		l = RegSetValueEx(hkey, TEXT("EventMessageFile"), 0, REG_EXPAND_SZ,
			(PBYTE)pszEventMsgFilePaths, chSIZEOFSTRING(pszEventMsgFilePaths));
		if (l != NO_ERROR)__leave;
		if (pszParameterMsgFilePaths == NULL)
			pszParameterMsgFilePaths = szModulePathname;
		l = RegSetValueEx(hkey, TEXT("ParameterMessageFile"), 0, REG_EXPAND_SZ,
			(PBYTE)pszParameterMsgFilePaths,
			chSIZEOFSTRING(pszParameterMsgFilePaths));
		if (l != NO_ERROR)__leave;
		if (dwCategoryCount >0) {
			if (pszCategoryMsgFilePaths == NULL)
				pszCategoryMsgFilePaths = szModulePathname;
			l = RegSetValueEx(hkey, TEXT("CategoryMessageFile"), 0,
				REG_EXPAND_SZ, (PBYTE) pszCategoryMsgFilePaths,
				chSIZEOFSTRING(pszCategoryMsgFilePaths));
			if (l != NO_ERROR)__leave;
			l = RegSetValueEx(hkey, TEXT("CategoryCount"), 0, REG_DWORD,
				(PBYTE) &dwCategoryCount, sizeof(dwCategoryCount));
			if (l != NO_ERROR)__leave;
		}
		fOk = TRUE;
	}
	__finally {
		if (hkey != NULL)RegCloseKey(hkey);
	}
	return(fOk);
}

//////////////////////////////////////////////////////////////////////////////

BOOL CEventLog::Uninstall() {
	// 安装每一个服务的事件记录档
	TCHAR szSubKey[_MAX_PATH];
	wsprintf(szSubKey,
		TEXT("System//CurrentControlSet//Services//EventLog//Application//%s"),
		m_pszAppName);
	return(NO_ERROR == RegDeleteKey(HKEY_LOCAL_MACHINE, szSubKey));
}

//////////////////////////////////////////////////////////////////////////////

BOOL CEventLog::ReportEvent(WORD wType, WORD wCategory, DWORD dwEventID,
	REPORTEVENTUSER reu, WORD wNumStrings, PCTSTR* pStrings, DWORD dwDataSize,
	PVOID pvRawData) {
	BOOL fOk = TRUE;   // 假设成功
	if (m_hEventLog == NULL) {
		// 这是ReportEvent第一次被呼叫与开启
		m_hEventLog = ::RegisterEventSource(NULL, m_pszAppName);
	}
	if (m_hEventLog != NULL) {
		PSID psidUser = NULL;
		if (reu != REUSER_NOTAPPLICABLE) {
			HANDLE hToken;
			if (REUSER_SERVICE == reu)
				fOk = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &h Token);
			else
				fOk = OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken);
			if (fOk) {
				BYTE bTokenUser[1024];
				PTOKEN_USER ptuUserSID = (PTOKEN_USER) bTokenUser;
				DWORD dwReturnLength;
				GetTokenInformation(hToken, TokenUser, ptuUserSID,
					sizeof(bTokenUser), &dwReturnLength);
				CloseHandle(hToken);
				psidUser = ptuUserSID->User.Sid;
			}
		}
		fOk = fOk && ::ReportEvent(m_hEventLog, wType, wCategory, dwEventID,
			psidUser, wNumStrings, dwDataSize, pStrings, pvRawData);
	}
	return(fOk);
}

///////////////////////////////////////////////////////////////////////////////

#endif	// SERVICECTRL_IMPL

/////////////////////////////////End of File /////////////////////////////////
AppLogMsgs.h
/**************************************************************
模组:AppLogMsgs.mc
通告:Copyright (c)2000 Jeffrey Richter
**************************************************************/
//********************CATEGORY SECTION ***********************
// 类别识别码为16位元值
//
//Values是32位元值,其格式如下:
//
//  3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//  +---+-+-+-----------------------+-------------------------------+
//  |Sev|C|R|   Facility	            |    Code                |        
//  +---+-+-+-----------------------+-------------------------------+
//
// 地点
//
//	Sev	- 是重要性的控制码
//
//		00 - 成功
//		01 - 资讯的
//		10 - 警告
//		11 - 错误
//
//		C - 是自订控制码标记
//
//		R - 是一个被保留的位元
//
//		Facility - 是设备控制码
//
//		Code - 是设备的状态控制码
//
//
// 定义设备控制码
//

//
// 定义重要性控制码
//

//
// MessageId: CAT_APPEXECSTATUS
//
// MessageText:
//
// 应用程序执行状态
//
#define CAT_APPEXECSTATUS		((WORD)0x20000001L)
//
// MessageId: CAT_APPEVENT
//
// MessageText:
//
// 应用程序事件
//
#define CAT_APPEVENT	((WORD)0x20000002L)
//*********************MESSAGE SECTION ***********************
// 事件识别码是32位元值
//
// MessageId: MSG_APPSTART
//
// MessageText:
//
// 应用程序已启动
//
#define MSG_APPSTART	((DWORD)0x20000064L)
//
// MessageId: MSG_APPSTOP
//
// MessageText:
//
// 应用程序已停止
//
#define MSG_APPSTOP	((DWORD)0x20000065L)
//
// MessageId: MSG_ERROR
//
// MessageText:
//
// 应用程序产生错误控制码 %1: "%%%1"
//
#define MSG_ERROR	((DWORD)0x20000066L)
//************************END OF FILE ************************
AppLogMsgs.mc
;/**************************************************************
;模组:AppLogMsgs.mc
;通告:Copyright (c)2000 Jeffrey Richter
;**************************************************************/

;//********************CATEGORY SECTION ***********************
;//类别识别码为16位元值
MessageIdTypedef=WORD

MessageId=1
SymbolicName=CAT_APPEXECSTATUS
Language=English
Application execution status
.

MessageId=2
SymbolicName=CAT_APPEVENT
Language=English
Application event
.

;//*********************MESSAGE SECTION ***********************
;// 事件识别码为32位元值
MessageIdTypedef=DWORD

MessageId=100
SymbolicName=MSG_APPSTART
Language=English
Application started.
.

MessageId=
SymbolicName=MSG_APPSTOP
Language=English
Application stopped.
.

MessageId=
SymbolicName=MSG_ERROR
Language=English
Application generated error code %1:"%%%1"
.

;//************************END OF FILE ************************
AppLog.rc
//Microsoft Developer Studio产生的资源指令码
//
#include "Resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// 从TEXTINCLUDE 2资源产生
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) 资源
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

/////////////////////////////////////////////////////////////////////////////
//
// 图示
//
// 图示用最小的识别码值放置第一个应用程序图示,以确保在所有系统上保持一// 致。
IDI_APPLOG ICON DISCARDABLE "AppLog.ico"

/////////////////////////////////////////////////////////////////////////////
//
// 对话方块
//

IDD_APPLOG DIALOG DISCARDABLE 15,24,280,41
STYLE DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Application Log"
FONT 8, "MS Sans Serif"
BEGIN
	LTEXT	"Win32 &error code:",IDC_STATIC,4,6,58,8
	EDITTEXT	IDC_ERRORCODE,68,4,40,12,ES_AUTOHSCROLL |ES_NUMBER
	DEFPUSHBUTTON	"&Simulate error",IDC_SIMULATEERROR,112,4,53,14
	PUSHBUTTON	"Open Event &Viewer",IDC_SPAWNEVENTVIEWER,196,4,80,14
	PUSHBUTTON	"&Install event message file in registry",IDC_INSTALL,
				4,24,132,14
	PUSHBUTTON	"&Remove event message file from registry",IDC_REMOVE,
				144,24,132,14
END
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
	"Resource.h /0"
END
2 TEXTINCLUDE DISCARDABLE
BEGIN
	"#include ""afxres.h""/r/n"
	"/0"
END
3 TEXTINCLUDE DISCARDABLE
BEGIN
	"/r/n"
	"/0"
END
#endif   // APSTUDIO_INVOKED

/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE
BEGIN
	IDD_APPLOG,DIALOG
	BEGIN
		RIGHTMARGIN, 276
		BOTTOMMARGIN, 37
	END
END
#endif   // APSTUDIO_INVOKED

/////////////////////////////////////////////////////////////////////////////
//
// 11
//

LANGUAGE 0x9,0x1
1 11 MSG00001.bin

#endif   // English (U.S.) 资源
/////////////////////////////////////////////////////////////////////////////

#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// 从TEXTINCLUDE 3资源产生
//

/////////////////////////////////////////////////////////////////////////////
#endif   // 非APSTUDIO_INVOKED
 列表6-1 AppLog范例应用程序

读取事件记录
 

Windows之事件检视器嵌入式管理单元通常已可满足大部份的事件读取需求。然而,Windows提供了允许您的应用程序去存取事件记录档的函数。有很大的可能会使用到这样的特性—例如,您可以编写一个应用程序,当它侦测到某些识别码被加入事件记录的事件进入时,便传送一个电子邮件。

为了要读取已启动的事件记录,您的应用程序必须先透过呼叫OpenEventLog而取回一个到记录的handle:

HANDLE OpenEventLog(
	PCTSTR pszUNCServerName,
	PCTSTR pszLogName);

pszUNCServerName参数确认了含有您想要存取之事件记录的机器。pszLogName参数则在服务器上识别特定的记录。一旦您拥有一个到记录档之有效handle,即表示您已准备去读取事件。通常,当您完成了对记录档的存取时。您应该透过呼叫CloseEventLog来关闭该handle:

BOOL CloseEventLog(HANDLE hEventLog);

读取事件需要一个对ReadEventLog的呼叫:

BOOL ReadEventLog(
	HANDLE	hEventLog,
	DWORD	dwReadFlags,
	DWORD	dwRecordOffset,
	PVOID	pvBuffer,
	DWORD	nNumberOfBytesToRead,
	PDWORD	pnBytesRead,
	PDWORD	pnMinNumberOfBytesNeeded);

hEventLog参数是从OpenEventLog回传的handle。dwReadFlags参数则确认您将连续地读取记录或是从一个特定的记录开始读取。表6-5列出dwReadFlags可能的标记。一个常见的之被传递的值为EVENTLOG_FORWARDS_READ | EVENTLOG_SEQUENTIAL_READ。

 表6-5 可以被传递至ReadEventLog之dwReadFlags参数的标记
标记 说明 EVENTLOG_SEEK_READ 允许您为了您想要开始读取的事件指定一个零基底的索引。您可以用dwRecordOffset来指定索引。必须选择一个寻找或循序的读取方法。 EVENTLOG_SEQUENTIAL_READ 指示您将以循序的方式读取事件记录,从最近读取记录之事件记录开始。这是大部份常见之被选择的事件记录类别。 EVENTLOG_FORWARDS_READ 指示您将由事件记文件而向前读取。此标记可以被用在循序或寻找的读取方式中。 EVENTLOG_BACKWARDS_READ 指示您将由事件记录文件而向后读取。此标记可使用在循序或寻找的读取方式中。

为了取回事件记录资料,您会提供一个缓冲区指标(pvBuffer)与一个以位元组为单位之缓冲区大小(nNumberOfBytesToRead)给ReadEventLog。如果您的缓冲区不够用来读取记录中的下一个记录,那么该函数会失败,而且GetLastError将会回报ERROR_INSUFFICIENT_BUFFER。被pnMinNumberOfBytesNeeded参数所指的变数将会包含读取一个单一记录所需的位元组大小。然而,如果您的缓冲区足够用来储存一或多个记录,则ReadEventLog将会用所有符合您的缓冲区之资料来填满您的缓冲区。建议您的应用程序在读取一个单一事件时建立一个单一的呼叫ReadEventLog,以找出所需的缓冲区大小,并配置该缓冲区,然后再次呼叫ReadEventLog以读取记录。虽然您可以同时读取多个事件,但是并不会更有效率,而且会使得您的语法剖析变得更复杂,因为被回传的资料具有可变的长度。

如果不再有记录被读取,ReadEventLog将会回传FALSE,而且GetLastError会回报ERROR_HANDLE_EOF。

在您成功地呼叫ReadEventLog后,您的缓冲区将会包含一或多个EVENTLOGRECORD结构。此结构的长度可变且被定义如下:

typedef struct _EVENTLOGRECORD {
	DWORD	Length;
	DWORD	Reserved;
	DWORD	RecordNumber;
	DWORD	TimeGenerated;
	DWORD	TimeWritten;
	DWORD	EventID;
	WORD	EventType;
	WORD	NumStrings;
	WORD	EventCategory;
	WORD	ReservedFlags;
	DWORD	ClosingRecordNumber;
	DWORD	StringOffset;
	DWORD	UserSidLength;
	DWORD	UserSidOffset;
	DWORD	DataLength;
	DWORD	DataOffset;
	//
	// 然后接下来:
	//
	// TCHAR SourceName[]
	// TCHAR Computername[]
	// SID UserSid
	// TCHAR Strings[]
	// BYTE Data[]
	// CHAR Pad[]
	// DWORD Length;
	//
} EVENTLOGRECORD;

这个结构之极少成员是易懂的,而且您应该立即地辨认出它们是EventID、EventType和EventCategory栏位。然而这个结构的另一些成员从这里变得更复杂。

让我们开始使用TimeGenerated和TimeWritten值。如您所猜想的,它们分别与曾被产生且写入记录中的日期与时间事件相符。然而,其时间值并非一般在Win32 API函数中使用的格式,所以第一次看起来可能会觉得很难操纵。这里是文件中有关时间格式的说明:「这个时间是从1970年一月一日00:00:00开始使用国际标准时间测量至现在已流逝的秒数。」除了该时间值是被指定的事件或「事件时间」外,此格式与C runtime的time_t型别相同。它的用意是什么?它意味着您将必须跳过若干个环来将取得的事件时间值转换到例如SYSTEMTIME结构之有用的时间格式中。

如果您已非常熟悉Windows所支援之不同的时间结构,那么您可能会认为事件时间格式与FILETIME格式相似。FILETIME定义为从1601年1月1日00:00:00以来,表示100毫微秒间隔数字的一个64位元值。由于Windows提供了有用的函数来将FILETIME转换至SYSTEMTIME,我们的最佳对策即是要将我们的事件时间转变成一个FILETIME值。(记得事件时间为国际标准时间。)以下的程序代码在一个简单的函数包装了所有关于此部份的逻辑:

void EventTimeToLocalSystemTime(DWORD dwEventTime, 
	SYSTEMTIME* pstTime) {
	SYSTEMTIME st1970;
	// 为00:00:00 January 1, 1970建立一个FILETIME
	st1970.wYear	= 1970;
	st1970.wMonth	= 1;
	st1970.wDay	= 1;
	st1970.wHour	= 0;
	st1970.wMinute	= 0;
	st1970.wSecond	= 0;
	st1970.wMilliseconds	= 0;

	union {
		FILETIME ft;
		LONGLONG ll;
	} u1970;
	SystemTimeToFileTime(&st1970, &u1970.ft);

	union {
		FILETIME ft;
		LONGLONG ll;
	} uUCT;
	// 从一秒转换至100毫微秒
	uUCT.ll = 0;
	uUCT.ft.dwLowDateTime = dwEventTime;
	uUCT.ll *= 10000000;
	uUCT.ll += u1970.ll;

	FILETIME ftLocal;
	FileTimeToLocalFileTime(&uUCT.ft, &ftLocal);
	FileTimeToSystemTime(&ftLocal, pstTime);
}

既然解开了时间值的 密,就让我们继续前进至SourceName和Computername成员的部份。这二个取回字串值的方法有些笨拙。虽然EVENTLOGRECORD结构提供了位于结构中的一些可变位置项目之偏移量,但是系统的设计者显然没有感觉到此需求是一致的。结果,SourceName简单地被定义为直接使用零终止的字串接在结构的DataOffset成员后面。同样地,Computername字串是从第一个跟随在SourceName字串后的字元开始。我匆匆拼凑了一些有用的巨集指令(位于随书光碟中的EventMonitor.cpp中),简化了从EVENTLOGRECORD结构中提取这些值的动作。这些巨集指令将会使用Unicode或ANSI字串格式与内建的来源模组一起工作。

幸运地,EVENTLOGRECORD包含了用来存取UserSid和Strings成员之UserSidOffset与StringOffset值。这些偏移量从结构的启始处开始,并以位元组测量。UserSid是一个确认被记录之事件的使用者SID结构。在第九章中,我将会讨论如何将一个SID结构转换至人类可阅读之使用者名称的内容。Strings阵列为被传递至ReportEvent的ppszStrings参数之一个指向扩充字串的指标阵列。

将一个讯息识别码转换至人类可阅读之字串
 

您很可能会想知道该如何取得事件类别的内容与该事件的细节说明。知道您已了解事件回报的部份后,您可以推断出它可能会至登录中查询事件来源、载入适当的讯息DLL以及手动地从资源提取详细的类别与讯息—但是不需要这么麻烦,不是吗?是的,但是我描述的方法是取回事件内容的唯一的方法。幸运的是,系统实作了一个被命名为FormatMessage之易于使用的函数,以提取资源内容,然而其馀的任务则是我们要做的。本章后面将讨论的EventMonitor范例应用程序会说明如何使用FormatMessage读取事件的方法。该函数定义如下:

DWORD FormatMessage(
	DWORD	dwFlags,
	PCVOID	pSource,
	DWORD	dwMessageId,
	DWORD	dwLanguageId,
	PTSTR	pBuffer,
	DWORD	nSize,
	va_list *Arguments);

我有一对看起来很像的函数,定义如下:

PTSTR GetEventCategory(
	PTSTR	pszLog,
	PEVENTLOGRECORD	pelr);

PTSTR GetEventMessage(
	PTSTR	pszLog,
	PEVENTLOGRECORD	pelr);

这些函数会取得一个从记录文件指向记录档与一个事件记录的指标,然后回传一个包含要求内容的缓冲区。被回传的缓冲区可以随意的在传统的FormatMessage函数中使用LocalFree。然而,系统不会提供这些漂亮的函数给我们。所以我自己将它实作成一个包装我自己的FormatEventMessage函数的巨集指令与函数。此函数的完整程序代码可在随书光碟中的EventMonitor.cpp取得。让我来指出一些重要的部份。

您可能还记得我们之前讨论在登录值名称为EventMessageFil、ParameterMessag或者CategoryMessage中储存的讯息DLL的事件回报内容,它取决于您试图寻找的讯息。如果正被谈论之事件来源被命名为MySource,而且它已被记录至应用程序记录档,则登录看起来会像这样:

HKEY_LOCAL_MACHINE
	SYSTEM
		CurrentControlSet
			Services
				EventLog
					Application
						MySource
							CategoryMessageFile
							EventMessageFile
							ParameterMessageFile

当在读取事件时,为一个或多个包含要求讯息字串之讯息DLL寻找适当的登录值和文法分析是您的应用程序的工作。因为这些登录值类型为REG_EXPAND_SZ,所以记得您必须在将这个字串载入模组前取回的讯息文件将从登录传递至ExpandEnvironmentString的字串以分号隔开。

在取得讯息DLL与EXE的清单后,您必须使用LoadLibraryEx依次(从左至右)将它们载入您的处理程序之位址空间中。因为LoadLibraryEx允许您像资源模组一样透过传递LOAD_LIBRARY_AS_DATAFILE标记来载入一个模组,所以应该使用LoadLibraryEx而非LoadLibrary。在您从LoadLibraryEx取回实例的handle后,您会连同讯息识别码和扩充字串,从EVENTLOGRECORD结构传给FormatMessage。如果对FormatMessage的呼叫成功、表示您的讯息已被配置,您可以载出程序库并回传讯息内容。如果执行不成功的话,您必须载出讯息DLL,并尝试载入下一个讯息DLL。若您将提取多于一个单一事件的讯息内容时,会发现它有助于您的程序代码之最佳化,以避免讯息DLL的重覆载入和载出的动作。

虽然FormatMessage会自动地展开您的字串到讯息中,但是它并不会自动地从ParameterMessageFileDLL展开讯息到您的讯息中。您的程序代码必须使用同一个演算法手动地在被FormatMessage回传的字串中扫描例如「%%」的内容,并用从ParameterMessageFileDLL取出的文字取代它们。本章中的EventMonitor范例应用程序会说明如何正确地做到它的方法。

在我们离开FormatMessage主题前还有最后一个重点:如果讯息文字需要呼叫比您传递至FormatMessage之字串数目更多的字串时,此函数将会盲目地尝试存取这个不存在的字串。更可能发生的是,它将会导致一个违规的存取,表示您预期了必须分析字串与事先计数字串数目的动作、确认被传递之字串的正确数字或者—此方法较容易使用—在一个处理违规存取之结构化例外处理的框架中包装对FormatMessage的呼叫。请记得系统并没有保证透过其他应用程序回报事件的正确性。

当我们到达了健全的事件察看主题时,我应该指出事件来源也可能会拥有一个无效或不存在的事件讯息文件。您的程序代码应该小心的处理这个情况。例如,事件检视器嵌入式管理单元显示了一个不适用的记录事件,如前面以及图6-8所示的内容。


 

 图6-8 在事件检视器嵌入式管理单元中的一个无法找到讯息文件资讯的事件

如您所见,事件的读取不是一个重大的问题。由于这个任务的复杂性,您可以从本章的范例程序代码中发现有助于您编写读取事件之程序代码。

在开始下节的内容前,不得不先提到BackupEventLog、OpenBackupEventLog与ClearEventLog函数,并非因为它们与事件的读取有直接的关系,而是因为许多读取事件的工具皆使用这些函数来提供附加的功能。这些函数的原型如下所示:

BOOL BackupEventLog(
	HANDLE hEventLog,
	PCTSTR pszBackupFileName);
HANDLE OpenBackupEventLog( PCTSTR pszUNCServerName, PCTSTR pszFileName);
BOOL ClearEventLog( HANDLE hEventLog, PCTSTR pszBackupFileName);

BackupEventLog使用被提供的文件名称建立一个文件,然后复制事件记录(透过hEventLog参数确认)的内容到这个新的文件中。此函数允许管理者储存事件的历史记录。

为了开启历史事件记录并读取其中的事件,应用程序会呼叫OpenBackupEventLog,以回传一个事件记录的handle(就像先前讨论过的OpenEventLog函数一样)。使用此handled时,您可以呼叫另一个类似的事件记录函数以取回被储存的事件项目。当您完成取回项目的动作时,应呼叫CloseEventLog来关闭事件记录handle。回到事件可以有助于事件记录的储存以及之后的检查部份,它可以减少包装一个记录的自订工作,以及为了问题的排除而将它传送回去。

最后一个函数为ClearEventLog,它简单地使用OpenEventLog或OpenBackupEventLog从一个被开启的记录文件中清除所有的事件项目。为了便于使用,此函数允许您在清除项目前将事件记录备份起来。您可以传递NULL给pszBackupFileName参数,表示不需产生备份档即清除记录文件。

事件通知
 

当一个事件被加到事件记录档时,可以让系统通知您。例如,若您希望每个客户端在连结以及中断时记录它们与服务器对谈的事件,您可以编写一个工具,用来等待这些通知并维护一个正在执行的记录连结至您的对谈服务器,您必须呼叫NotifyChangeEventLog函数以取回事件记录通知:

BOOL NotifyChangeEventLog(
	HANDLE hEventLog,
	HANDLE hEvent);

hEventLog参数是从一个呼叫OpenEventLog而回传的handle,而hEvent参数是先前已建立之事件核心物件的handle。当系统侦测到一个事件记录的改变时,系统会自动地发出信号给事件核心物件。一个在您的应用程序中的线程将会侦测这个被发出信号的事件核心物件,然后再处理任何被要求之事件记录。

您应该知道几个关于NotifyChangeEventLog的事实。第一,一旦您将事件记录与事件物件关联时,除了呼叫CloseEventLog以外,没有任何方法可以关闭对事件物件的通知。这通常不是问题,您可以简单地选择传递您的事件物件handle给CloseHandle,如果需要的话可以建立一个新的事件核心物件。

第二,当事件记录档被改变时,系统会透过呼叫PulseEvent以发出信号给事件。这意味着您不需重新设定事件,而必须拥有一个固定等待事件的线程,否则可能会错过一些通知。

第叁,系统不会为被加入事件记录档中的每个事件记录保证PulseEvent。更确切地说,如果在五秒的期间里,事件记录档被做了一个或多个改变,那么它大约每五秒便会使用一个脉冲将此改变传送至您的事件中。所以如果您正在等待事件物件,然后由于一个脉冲而读取记录时,您不应假设只有一个事件被改变,而是应该读取它,直到抵达记录档末端为止。

最后,在呼叫NotifyChangeEventLog时,不管您指定要记录哪些事件,在任何的事件记录被加至记录档时,系统皆会产生一个事件的脉冲。所以当一个事件被加至系统记录档时,即使您只想从应用程序记录档中得到通知,您的线程皆有可能会被唤醒。当记录档没有被改变时,您的应用程序必须小心地处理通知事件的接收问题。

EventMonitor范例应用程序
 

EventMonitor范例应用程序(「06 EventMonitor.exe」)说明了如何从一个事件记录档读取事件记录的方法。另外,此应用程序呼叫了NotifyChangeEventLog函数并依照加入事件记录档中的项目而更新它的显示情形。此范例应用程序的原始码与资源文件存放在随书光碟的06-EventMonitor目录中。当使用者执行EventMonitor范例应用程序时,它会显示一个如图6-9所示的对话方块。

在预设的情形下,EventMonitor会显示本机应用程序事件记录档的内容。然而您可以轻易地选择一个不同的机器或记录档,然后按下Monitor按钮倾印并监控被选择机器的最新记录。


 

 图6-9 EventMonitor范例应用程序的对话方块

虽然此范例只允许您察看系统、安全性以及应用程序记录档的内容,但是此原始程序代码可以轻易地被修改,好让应用程序可以监控任何您可能自行建立的记录。

当您开始监控一个事件记录时,EventMonitor会在它的清单方块中为每一个被选择记录的项目建立一个项目。一旦清单方块被填满,EventMonitor会呼叫NotifyChangeEventLog,并且会有一个线程一直在等待新的事件记录项目出现。如同新的事件项目在系统事件记录档中出现一样,EventMonitor会取回新的项目并把它们加到清单方块中,EventMonitor最后说明了如何将类别与讯息识别码转换成适当的字串内容。依照您在清单方块中的选择项目,EventMonitor会在适当的讯息文件中载入或者储存、抓取适当的字串内容并在对话方块下方之唯读编辑控制项中显示字串内容。

 

你可能感兴趣的:(操作系统相关)