服务是含有附加基础,允许它根据作业系统而受到特别处理的一般Windows应用程序,例如,远端管理的能力,允许管理者从一个远端的机器来启动或停止应用程序的执行。您将免费取得将您的服务器应用程序转成一个服务程序的方式以及其他相关的特性。本章将会解释服务的内容、如何设计一个服务应用程序与作业系统为服务提供了什么附加的工具等。
Windows内包含了一些服务。表3-1列出了一些服务,安装在笔者执行Microsoft Windows 2000作业系统的机器上,而其可执行档名称包含了服务程序代码。
表3-1 服务与其相关的可执行文件 |
服务名称 | 说明 | 可执行文件名称 |
---|---|---|
Alerter | 当管理内容改变时,通知用户和电脑。 | Services.exe |
ClipBook | 允许使用远端ClipBook工具来观看记录档。 | ClipSrv.exe |
Computer Browser | 维护一个位于您网路上的电脑表。 | Services.exe |
DHCP | 客户端登录以及更新IP位址与DNS名称。 | Services.exe |
Distributed Transaction Coordinator |
协调跨越二个或多个分散式资料库的异动、 讯息伫列、文件系统或管理其他异动保护的 资源。 | MSDTC.exe |
Event Log | 经用由程序与Windows来发布事件记录讯息。 | Services.exe |
Messenger | 发送与接收由管理者或Alerter服务传送出来的讯息。 | Services.exe |
Net Logon | 支援通过网域内电脑登录帐号事件的验证。 | LSASS.exe |
Plug and Play | 管理设备的安装及设定,以及将设备改变的讯息通知给程序。 | Services.exe |
Remote Procedure Call (RPC) | 提供点对对点以及许多其他的RPC服务。 | SvcHost.exe |
Remote Procedure Call (RPC) Locator | 管理RPC名称服务之资料库。 | Locator.exe |
Server | 提供支援RPC以及文件、打印机与命名管道(named pipe)的分享。 | Services.exe |
Task Scheduler | 使一个程序能在指定的时间内执行。 | MSTask.exe |
Telephony | 提供Telephony API (TAPI) 的支援。 | SvcHost.exe |
Uninterruptible Power Supply | 管理一个连接到电脑上的不断电供电系统(Uninterruptible Power Supply, UPS)。 | UPS.exe |
Windows Installer | 根据包含在 .msi文件内的指令来做软件的安装、修复以及移除动作。 | MSIExec.exe |
Windows Management Instrumentation |
提供系统管理资讯。 | WinMgmt.exe |
Workstation | 提供网路连结与通讯。 | Services.exe |
Microsoft还提出了许多没有列在表3-1中的服务,像是Microsoft Exchange Server、Microsoft Merchant Server以及Microsoft SQL Server等,所有的这些服务应用程序皆被实作成服务,并且是Microsoft BackOffice套件的一部份。
首先,最重要的是一个服务应程序只是一个32位元或64位元的可执行档,所以任何您已知之有关DLLs、结构化例外处理、内存应对档、虚拟内存、设备I/O、本机线程储存、线程同步、Unicode以及其他Windows工具皆可用于服务上。这表示对您来说,要转换一个已存在的伺服应用程序到服务应用程序应该相当容易且简单。
说明
在Microsoft Windows 2000 Resource Kit中包含了一个工具程序,叫做SrvAny.exe,它允许从远端启动一个已存在的应用程序,就像是一个真实的服务应用程序一般。然而,SrvAny并不允许以任何方式透过远端来管理应用程序,因此应该仅仅当作一个短期的解决办法使用。强烈地建议您将应用程序代码转成一个成熟的服务应用程序并且不要使用SrvAny工具程序。
第二,您应该要知道一个服务完全没有使用者介面的部份。大部份的服务锁在一个隐密地方的服务器上执行。所以您的服务若出现任何的使用者介面元件,像讯息对话方块,此时不会有使用者在机器前面看见它并按下它的按钮。就如您稍后会在本章看到的一样,任何出现的视窗可能会展现在一个时常变换使用者的工作站或是桌面上,如此一来,这个讯息的显示对使用者来说并没有意义。因为服务不会有使用者介面,所以无论您选择将您的服务实作成一个图形使用者介面(Graphical User Interface, GUI)应用程序(以(w)WinMain做为程序的进入点)或是Console User Interface(CUI)应用程序(以(w)main做为程序的进入点)都没有关系。
假如一个服务不会显示任何的使用者介面,您要如何设定它呢?您要如何启动与停止一个服务?要如何送出服务的警告或错误讯息呢?如何使服务将关于执行效能的统计资料报告出来?这些问题的答案即是:一个服务可以被远端地管理。Windows提供一些管理的工具,这些工具能从其他连接到网路上的机器管理一个服务的执行,所以不需要为了某人而去实际地确认(甚至是实际去存取)执行服务的电脑。您可能早已熟悉以下这些工具:Microsoft Management Console(MMC)与其服务、事件检视器与系统监视器之嵌入式管理单元、登录编辑器与Net.exe命令列工具。
这些工具是Windows为了简化服务应用程序编写者之开发所提供的工具,也能使一个管理者以一致的方式去管理本机与远端的机器。请注意,这些工具并非专门用在服务中,任何应用程序(或设备驱动程序)都可以利用它们。本书将在章节里讨论这些工具。
Windows服务通讯架构
建立服务的工作需要以下叁种元件:
图3-1说明了那些元件间彼此沟通的方式。注意SCP应用程序并没有直接与服务沟通;所有的通讯都会通过SCM。这里明确地说明了建立通过SCP与服务应用程序之远端管理的架构。实作一个使您的SCP应用程序直接与您的服务应用程序沟通的架构与通讯协定是可以的,但是您必须自己编写它们的讯程序代码。
图3-1 Windows服务的通讯机制 |
在这叁个部分中, 您绝对不会执行到SCM。Microsoft实作了SCM并将它封装到每一个Windows 2000的版本中。您要实作的是服务与SCPs。本章会涵盖您应该了解之设计与实作服务的内容,而在下一章则会说明编写SCP的细节。
包含在Windows中的服务控制程序(SCP)
在我们探讨如何编写一个服务之前,您至少必需知道一个SCP可以控制一个服务。所以我会开始检查一些包含在Windows中的SCP应用程序。
服务嵌入式管理单元
您应该要最熟悉的SCP应用程序是服务嵌入式管理单元,如图3-2所示。这个嵌入式管理单元显示了所有安装在目的机器上的服务清单。在名称与描述栏中可以发现每个服务的名称并提供服务函数的说明资讯。状态栏显示着哪一个服务是启动、暂停或停止(空白项表示「已停止」)。启动类型栏显示着什么时候应该唤起服务,而登入身份栏则显示了当服务执行时所使用的安全性内容。
这个资讯保存在SCM的资料库中,并存在以下子机码(Subkey)的登录中:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services
您应该不会直接存取到这个子机码;反之,SCP应该呼叫Windows函数(在下一章讨论)并操作这个子机码中的资料库。直接地修改这个机码内容会发生不可预料的结果。当您安装一个包含服务的产品时,该产品的安装程序即是一个增加服务资讯到SCM资料库的SCP应用程序。
您可以在电脑管理主控台左边窗格中选择电脑管理节点,以及从执行功能表中选择连线到另一台电脑,来观看一个远端SCM的服务资料库。
图3-2 服务嵌入式管理单元 |
说明
所有包含在Windows中的服务皆会以本机安全内容登入。这是一个享有高度特权的帐号,而且强烈建议您所编写的任何服务也要使用本机帐号。
现在您会开始看到服务嵌入式管理单元,您可能会怀疑是否能够用它来完成所有的任务。这里是一些普通的操作:
前面的清单说明了百分之九十九的管理者所做的服务嵌入式管理单元工作,但是嵌入式管理单元也可以被用来重新设定一个服务。要改变一个服务器的设定时,您要选择服务并且显示出它的内容对话方块。这个对话方块包含了四个页签,每一个页签皆允许管理者去重新设定所选的服务。以下的章节中会讨论更改设定的部份。
一般页签的内容
一般页签(如图3-3所示)允许管理者去检查与重新设定服务的一般资讯。您需要去了解的第一个真相是,每一个服务都有两个字串名称:一个内部名称(使用于计划性的目的)以及一个显示名称(一个显示给管理者与使用者看的字串)。在被加入服务资料库后,服务的内部名称即不能改变,但是管理者可以修改该服务的显示名称与内容。一般页签中也显示服务的执行程序路径,而它并不允许管理者改变它(这是一个使用页签的限制,并非系统的)。管理者可以改变服务的启动类型至以下所列之一:
图3-3 Windows Installer服务的一般页签内容 |
登入页签内容
除了设定真实的服务之外,管理者可以在该服务之登入页签内重新设定安全内容,如图3-4所示。安全内容可以为以下所列之一:
在本章之〈服务议题〉一节中,会讨论更多有关本机系统与使用者帐户的部份。
登入页签也允许管理者具体指定一个硬体设定档,以让服务能在此设定环境下执行。硬体设定档可让您依据您的硬体设定来设定服务。例如,您想在膝上型电脑被放入船坞时执行Fax Service服务;反之,则不执行该服务。
图3-4 Distributed Link Tracking服务之登入页签内容 |
修复页签内容
如图3-5所示之修复页签内容,它允许管理者告诉SCM当服务发生异常中断情形时,该执行什么动作。异常中断表示该服务没有回报一个SERVICE_STOPPED的状态即停止执行。在第一次、第二次以及后续的尝试执行失败时,SCM可以不执行动作、重新执行服务、执行文件或重新启动电脑。注意到如果执行该服务的帐户没有适当的存取权限或者许可,则在执行文件和重新启动电脑的动作时,该动作的执行会失败。
图3-5 Fax Service服务之修复页签内容 |
依存关系页签内容
如图3-6所示之依存关系页签说明了被选择的服务依存于何种服务或是什么服务依存于该服务。在图中,您会看到五个服务依存于Workstation服务。假如管理者试图去停止Workstation服务且附属于它的服务正在执行,则SCM会呼叫失败。许多SCP程序会通知使用者该附属服务正在执行,并可让使用者选择哪一个附属的服务要一并停止。依存关系页签不允许管理者修改任何的依存关系(我会在下一章讨论更多有关服务依存关系的内容)。
图3-6 Workstation服务之依存关系页签内容 |
Net.exe与SC.exe
除了服务嵌入式管理单元外,Windows还附了一个称为Net.exe的SCP命令列工具。这个工具限制您只能控制那些存在于本机上的服务。使用Net.exe,您可以使用下列的语法来启动、停止、暂停以及继续执行服务:
NET START servicename NET STOP servicename NET PAUSE servicename NET CONTINUE servicename
您也可以不需指定一个servicename,只需简单地键入以下的指令即可显示正在本机上执行的服务清单:
NET START
您可以将这些呼叫指令放入一个批次档或其他的指令码文件中,所以Net.exe工具非常容易使用在侦错上。
另一个Microsoft提供的SCP应用程序是一个称做SC.exe的命令列工具程序。这个工具包含在Microsoft Windows 2000 Resource Kit中。当执行这个工具而没有传入任何参数时,它会显示用法与语法,列示如下:
说明: SC是一个命令列程序,用在与NT服务控制器及服务沟通上。 用法: sc[command] [service name] ... 类型、方法、型式 选项的型式为 // ServerName 在键入「sc [command]」时可以获得进一步的帮助。 命令: query-----------询问一个服务状态或列举出多个服务类型的状态。 queryex---------询问一个服务的延伸状态或列举出多个服务类型 的状态。 start-----------启动一个服务。 pause-----------传送一个PAUSE控制要求给服务。 interrogate-----传送一个INTERROGATE控制要求给服务。 continue--------传送一个CONTINUE控制要求给服务。 stop------------传送一个STOP要求给服务。 config----------改变服务的永久设定。 description-----改变服务的说明。 failure---------改变服务失败后的动作。 qc--------------询问服务的说明资讯。 qdescription----询问服务的内容。 qfailure--------询问服务失则时所做的动作。 delete----------从登录中删除服务。 create----------建立一个服务(新增至登录中)。 control---------传送一个控制给服务 sdshow----------显示服务的安全描述。 sdset-----------设定服务的安全描述。 GetDisplayName-取得服务的DisplayName。 GetKeyName------取得服务的ServiceKeyName。 EnumDepend------列举服务的依存关系。 以下的命令不需要提供服务名称: sc
由于这个工具对所有的服务控制选项提供了相当丰富的命令列介面且能简单地使用在一个指令码文件中,所以当我们在开发或侦错一个服务时,可以得到极大的帮助。
Windows服务应用程序架构
本节会解释那些将伺服应用程序转换成服务的附加基础,从而允许您们的应用程序可以被远端地控管。要了解Microsoft的服务架构有一些困难,这是由于每一个服务程序总是包含着至少二个线程,而那些线程在彼此间必须互相沟通所致。所以您必须要应付线程同步与线程间通讯的议题。
一个单一执行档可以包含多个服务的情形是另一个您需要去考虑的议题。假如您回头看
要设计一个可执行的服务时,您必须自己考虑以下的叁个函数:
图3-7可以帮助您正确地了解这个架构。它说明了一个可执行之服务内存在的二个服务以及处理程序间通讯(ITC)与线程间通讯(IPC)。在未来的章节中,会仔细的检视这叁个函数以及说明它们的责任为何。建议您在阅读时可以参考此图。
图3-7 Windows服务应用程序架构 |
处理程序之进入点函数:(w)main或(w)WinMain
当管理者想要启动一个服务时,不管该执行文件所包含的服务是否已执行,都由SCM来决定。如果它没有被执行,则SCM会将该执行档产生出来。此处理程序之主要线程的主要任务是执行整个程序的初始化(应该在服务之ServiceMain函数中完成初始化的动作)。
在初始化完成后,该进入点函数必须与SCM联系,以决定现在应该接管哪一个程序的控制。为了要与SCM接触,首先进入点函数必须配置和初始化一个SERVICE_TABLE_ENTRY结构的阵列:
typedef struct _SERVICE_TABLE_ENTRY { PTSTR lpServiceName; // 服务的内部名称 LPSERVICE_MAIN_FUNCTION lpServiceProc; // 服务的ServiceMain } SERVICE_TABLE_ENTRY, *LPSERVICE_TABLE_ENTRY;
第一个成员表示内部、服务的计划性名称,第二个成员则是服务之ServiceMain回呼函数的位址。如果该执行档内只包含了一个服务,则SERVICE_TABLE_ENTRY结构的阵列必须利用以下的方式初始化:
SERVICE_TABLE_ENTRY ServiceTable[] = { { TEXT("ServiceName1"), ServiceMain1 }, { NULL, NULL} // Marks end of array };
假如您的执行档内包含了叁个服务,您必须使用以下的方法来初始化它,就像这样:
SERVICE_TABLE_ENTRY ServiceTable[] = { { TEXT("ServiceName1"), ServiceMain1 }, { TEXT("ServiceName2"), ServiceMain2 }, { TEXT("ServiceName3"), ServiceMain3 }, { NULL, NULL } // Marks end of array };
存在阵列中的最后一个结构必须将二个成员皆设定为NULL,以表示它为该阵列的结尾。现在该处理程序经由呼叫StartServiceCtrlDispatcher来与SCM连接:
BOOL StartServiceCtrlDispatcher( CONST SERVICE_TABLE_ENTRY* pServiceTable);
在可执行之处理程序中呼叫这个函数与传递服务阵列表内的位址可以指出哪些服务包含在该处理程序内。如此,SCM会知道哪些服务曾经尝试启动以及哪些服务会反覆地经由阵列来寻找它。一旦服务被发现,则会建立一个线程并执行该服务的ServiceMain函数(由SERVICE_TABLE_ENTRY阵列获得位址者)。
说明
SCM保持着关闭的状态页签,以表示服务正以何种方式执行。例如,当SCM产生一个可执行的服务,SCM会等待可执行之主要线程去呼叫StartServiceCtrlDispatcher,如果StartServiceCtrlDispatcher在30秒内没有被呼叫,则SCM会认为该服务发生故障并且会呼叫TerminateProcess来强迫删除该处理程序。由于此原因,如果您的处理程序需要超过30秒的时间来初始化,您必须产生另一个线程去处理该初始化动作,所以主要的线程可以快速地呼叫StartServiceCtrlDispatcher。注意此处我所提出的是于全程序范围的初始化动作。个别的服务应该自己使用它们的ServiceMain函数来做初始化。
StartServiceCtrlDispatcher在所有服务执行结束前并不会返回。当至少一个服务正在执行时,SCM会控制该处理程序的主要线程。通常这个线程并没有做什么动作而且只是处于静止状态,所以不会浪费CPU时间。如果管理者试图去启动另一个实作在同一个执行档内的服务时,SCM不会产生另一个可执行档实例。反之,SCM会与可执行档之主要线程通讯并且再次重复服务的清单,并寻找此服务。一旦找到,一个新的线程会被产生,它会执行适当服务的ServiceMain函数。
说明
当决定是否要产生一个新的服务程序或是一个已存在于服务处理程序的线程时,SCM会与一个服务执行路径字串做详细的比较。例如,在同一个可执行档MyServices.exe内执行二个服务。使用一个可执行的路径名称「%windir%/System32/MyService.exe」将第一个服务加到SCM资料库内,而第二个服务则是使用「C:/WinNT/System32/MyService.exe」而加到资料库内。如果这二个服务被启动,则SCM会产生二个分开的处理程序,二者皆执行同一个MyService.exe服务应用程序。在服务被加到SCM资料库时使用相同的路径名称字串是为了确定SCM将所有在单一可执行档内的服务用于一个处理程序上。
在系统内不会记录在处理程序中的哪些服务正在执行。当每个服务离开时(通常是因为ServiceMain返回的缘故),系统会检查并察看哪些服务仍然在执行。如果没有服务正在执行,则只有当时执行进入点函数呼叫者会被返回。因为处理程序中断执行的缘故,您的程序代码必须在全程序范围内执行清除的动作,并将进入点函数回传。注意您必须在30秒内或是在SCM删除该处理程序前完成您的清除程序代码。
ServiceMain函数
每一个在执行档内的服务必须拥有它们自己的ServiceMain函数:
VOID WINAPI ServiceMain( DWORD dwArgc, PTSTR* pszArgv);
SCM经由建立一个新的线程来启动一个服务,此线程使用它的ServiceMain函数执行。像之前所提到的,呼叫这个特定的ServiceMain函数,却可以将此函数命名为任何您想要的。您将此函数命名成任何名称皆不重要,因为您传递的是存在SERVICE_TABLE_ENTRY内的lpServiceProc成员。然而您不能在一个单一执行档内使用二个相同的ServiceMain函数;如果这样的话,当您试着建立您的专案时,编译器或连结器会产生错误。
ServiceMain函数需要传递二个参数,这些参数建立了一个允许管理者经由StartService函数使用一些命令列工具来启动服务的机制(在下一章中讨论)。就个人而言,我不知道有任何服务参考到这些参数并且建议您忽略它们。一个可经由读取下列所示之登录子机码内容来自我设定的服务比使用传递参数到ServiceMain函数的方法要好。
HKEY_LOCAL_MACHINE /SYSTEM /CurrentControlSet /Services /ServiceName /Parameters
许多服务包含在允许管理者修改服务设定的客户端应用程序中。该客户端简单地储存了那些存在登录子机码中的设定。当服务启动时,它会至登录中检查设定。
假如改变了一个正在执行之服务的架构时,可使用以下叁个选项:
ServiceMain必须完成的第一个任务是将服务的HandlerEx回呼函数的位址告诉SCM。它利用呼叫RegisterServiceCtrlHandlerEx来完成这个任务:
SERVICE_STATUS_HANDLE RegisterServiceCtrlHandlerEx( PCTSTR pszServiceName, // 服务的内部名称 LPHANDLER_FUNCTION_EX pfnHandler, // 服务的HandlerEx函数 PVOID pvContext); // 使用者定义值
第一个参数表示您放置了一个HandlerEx函数的服务,而第二个参数是HandlerEx函数的位址。当SERVICE_TABLE_ENTRY阵列被初始化并被传递到StartService-CtrlDispatcher时,pszServiceName参数必须与符合所使用的名称。最后一个参数pvContext是一个使用者定义的值,它会被传递到服务的HandlerEx函数中。下一节我们将会讨论HandlerEx函数的内容。
RegisterServiceCtrlHandlerEx返回了一个唯一可以从服务到SCM识别的SERVICE_STATUS_HANDLE值。所有将来从服务到SCM的通讯皆会要求以这个Handle来取代服务的内部字串名称。
说明
不像大部份在系统中的Handle一般,从RegisterServiceCtrlHandlerEx回传的Handle不会为了您而关闭。
在RegisterServiceCtrlHandlerEx回传后,ServiceMain线程应该立即地告知SCM该服务已开始执行初始化。这个工作由呼叫SetServiceStatus函数来完成:
BOOL SetServiceStatus( SERVICE_STATUS_HANDLE hService, LPSERVICE_STATUS pServiceStatus);
这个函数要求您传递该识别您的服务之Handle(那些已经从呼叫RegisterServiceCtrlHandlerEx返回者)以及一个已初始化之SERVICE_STATUS之结构的位址:
typedef struct _SERVICE_STATUS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; } SERVICE_STATUS, *LPSERVICE_STATUS;
SERVICE_STATUS结构包含了影响服务之现行状态的七个成员。这些成员定义在如下所示清单,在您传递此结构到SetServiceStatus前,它们必须被正确地设定。
由于dwCheckPoint成员的存在而使您拥有优势。它允许一个服务回报它已经处理了多少进度。每一次您呼叫SetServiceStatus时,您应该增加dwCheckPoint到一个数字,以指示您的服务已经执行了什么「步骤」。要多常去回报服务执行进度完全取决于您。如果您决定在您的服务初始化的每个?骤皆回报执行进度,则dwWaitHint成员应该被设定为指示您认为到达下一个步骤(检查点(Checkpoint))所花的毫秒数,而不是指定到完成服务时所需的毫秒数。
说明
ServiceMain函数必须在启动后80秒内或SCM认为该服务启动失败时呼叫SetServiceStatus。假如没有其他服务正在服务处理程序内执行,则SCM会删除该处理程序。
说明
在建立您的服务线程前,SCM会设定您的服务状态到显示一个START_PENDING的现行状态、一个为0的检查点以及一个等待2000毫秒的提示。如果您的ServiceMain函数要求超过2000毫秒的时间来做初始化的动作,则在您第一次呼叫SetServiceStatus时,应该指示一个SERVICE_START_PENDING之现行状态、一个为1的检查点以及一个被要求等待的提示。注意检查点应该被设定为1,一个程序开发者常犯的一个非常普通的错误是在第一次呼叫SetServiceStatus时将检查点设定为0,这会使管理者的SCP程序混淆,认为服务没有正确地回应。如果您的服务要求更多的初始化动作,您可以继续回报一个增加中的检查点,并依需要设定等待提示时间之SERVICE_START_PENDING状态。
在您的服务初始化完成后,您的服务会呼叫SetServiceStatus以指定为SERVICE_RUNNING(检查点与等待提示时间皆设定为0)。现在您的服务正在执行。通常一个服务会将它自己放在一个回圈内执行。在回圈内,该服务线程会自己暂停执行,并等待一个网路要求或一个指定服务应该暂停的通知、继续停止、关机等等。如果一个网路要求到来,该服务线程会醒来、处理该要求以及回到回圈中等待下一个要求或通知。
假如服务被一个通知唤醒,则它会处理该通知。如果服务收到一个停止或关机的通知,则该回圈会结束以及该服务的ServiceMain函数会返回、并删除线程。若服务为处理程序中最后一个被执行,则该处理程序也会停止执行。
说明
SetServiceStatus函数会检查SERVICE_STATUS结构的+成员。若此成员被设定为SERVICE_STOPPED,则SetServiceStatus会关闭该服务的Handle(第一个传递至SetServiceStatus的参数)。这就是为什么您从来不必很明确地关闭从RegisterServiceCtrlHandlerEx回传的Handle,以及更重要的是为什么在呼叫它与一个SERVICE_STOPPED的现行状态后不用呼叫SetServiceStatus的答案。当在除错器之下执行服务时,试图那么做会引起一个无效的Handle例外。
HandlerEx函数
每一个在可执行文件中的服务必须与一个HandlerEx关联:
DWORD WINAPI HandlerEx( DWORD dwControl, DWORD dwEventType, PVOID pvEventData, PVOID pvContext);
虽然我称呼这个函数为HandlerEx,然而您可以为此函数另外命名为任何您选择的名称。它的实际名称为何并不重要,因为您将该函数之位址当成一个参数而传递到RegisterServiceCtrlHandlerEx函数中。不过,您不能在一个单一可执行档内使用二个名称相同的HandlerEx函数;如果您这么做了,在建构您的专案时,编译器或连结器会产生一个错误。
在大部份的时间里,呼叫至StartServiceCtrlDispatcher内之处理程序的主要线程处于暂停状态。当一个SCP程序想要去控制一个服务时,SCM会与该处理程序之主要线程通讯。线程会醒来并且呼叫适当服务的HandlerEx函数。例如,当一个管理者使用服务嵌入式管理单元去停止一个服务时,该嵌入式管理单元会与管理者要求的本机或远端SCM沟通。接着SCM会唤醒该可执行服务之主要线程、呼叫服务的HandlerEx函数并传递一个SERVICE_CONTROL_STOP控制码。
SCM也传送设备、硬体设定档以及电源事件通知给服务之HandlerEx函数。这些通知允许服务适当地重新设定自己并且同意或拒绝加入系统的变更。
说明
因为处理程序的主要线程在每一个服务的HandlerEx函数执行,所以您应该要实作那些HandlerEx函数,以使它们快速地执行。不那么做会妨碍另一个在同一个处理程序内的服务在一个合理的时间内取得它们所需之动作。
若主要线程执行了HandlerEx函数,而服务由另一个线程执行时,它可能会需要使HandlerEx与行动沟通或者通知服务线程。没有标准方法可以完成这个通讯;此方法完全依据您如何实作服务而定。您可以排队等候一个Asynchronous Procedure Call(APC)、宣告一个I/O完成端口状态、宣布一个像刚才所提之避免将HandleEx线程与ServiceMain做同步化动作之需求机制。
HandlerEx函数传递了四个参数。第一个dwControl参数表示被要求的动作或通知。假如此参数指示为一个设备、硬体设定档或是电源事件通知,则dwEventType以及pvEventData参数会提供有关动作或通知的更多特殊资讯。pvContext参数是一个原来传送至RegisterServiceCtrlHandlerEx函数中的使用者定义值。在使用这个值时,您可以建立一个在单一可执行档内被所有服务使用之单一HandlerEx函数。pvContext值可被需要与之通讯的HandlerEx函数用来决定特定的服务。在下一节里,我会讨论如何处理这些控制码以及通知的方法。
HandlerEx函数的回传值允许一个服务的控制者回传一些资讯至SCM。如果此函数没有处理一个特定的控制码,则回传ERROR_CALL_NOT_IMPLEMENTED;若此函数处理了一个设备、硬体设定档或是电源事件要求,则回传NO_ERROR。要拒绝一个要求,则回传任何其他的Win32之错误码。对任何其他的控制码,HandlerEx应该回传NO_ERROR。
控制码与状态回报
HandlerEx的责任是控制所有被要求之服务与所有通知的动作。它的第一个参数是指示一个动作要求或通知的控制码。表3-2描述了指示一个动作要求的控制码内容。一个动作要求告知服务去完成一些动作以改变执行状态。
表3-2 指示一个动作要求的控制码 |
控制码 | 说明 |
---|---|
SERVICE_CONTROL_STOP | 要求服务停止。 |
SERVICE_CONTROL_PAUSE | 要求服务暂停。 |
SERVICE_CONTROL_CONTINUE | 要求被暂停的服务继续执行。 |
SERVICE_CONTROL_INTERROGATE | 要求服务立即地更新目前状态资讯至SCM。这是所有服务必须回应的控制码。 |
表3-3说明了指示为通知的控制码内容。该通知会通报存在系统中令人注目之事件的服务。然而服务通常不会因为回应一个通知而改变它们的执行状态(尽管一个服务可能会选择改变它的执行状态)。
表3-3 指示一个通知的控制码 |
控制码 | 说明 |
---|---|
SERVICE_CONTROL_PARAMCHANGE | 定义参数被改变时通告服务。当一个服务正在执 行时,可以忽略或重新设定自己。 |
SERVICE_CONTROL_DEVICEEVENT | 通告一个设备事件的服务。该服务必须呼叫RegisterDeviceNotification以取回这些通知讯息。 |
SERVICE_CONTROL_HARDWAREPROFILECHANGE | 当一个硬体设定档改变时通告服务。 |
SERVICE_CONTROL_POWEREVENT | 通告一个电源事件给服务。 |
一个介于在128与255(包括)之间的数字 | 通告一个使用者定义事件给服务。 |
控制码要求回报状态
被HandlerEx函数执行的工作依据取回的控制码而有所不同。尤其是,动作要求控制码会要求在您的程序代码中得到特定的注意。当HandlerEx函数取回一个SERVICE_CONTROL_STOP、SERVICE_CONTROL_SHUTDOWN、SERVICE_ CONTROL_PAUSE或SERVICE_CONTROL_CONTINUE控制码时,SetServiceStatus必须被呼叫以告知对方已收到该控制码并且详细说明服务需要使用多久的时间以考虑自己开始从事状态改变之处理程序。例如,您告知经SERVICE_STATUS结构之dwCurrentState成员设定至SERVICE_STOP_PENDING、SERVICE_PAUSE_ PENDING或SERVICE_CONTINUE_PENDING的控制码已被接收。另外,HandlerEx函数必须在30秒内回传,或是SCP应用程序会再次认为该服务已经停止回应。如果SCM认为服务已停止回应,则它不会删除该服务,它只会回传一个失败至SCP以对服务控制码做初始化的动作。
当一个停止、关机、暂停或继续的操作是悬而未决的,您必须也要指定您认为该操作会需要多久的时间来完成该工作。指定持续时间是有用的,因为一个服务可能无法立即地改变它自己的状态—即它可能必须等待一个网路要求完成或是资料被注入一个设备。您可以使用SERVICE_STATUS结构的dwCheckPoint与dwWaitHint成员来指示需要多久的时间以完成该状态的改变,就像您回报一个服务之第一次启动一样。如果您想要的话,您可以回报一个被dwCheckPoint成员增加与dwWaitHint成员设定去指示您预期该服务需要多久才能开始从事下一个步骤之周期性的进度。
在服务完成了所有要求停止、关机、暂停或继续的动作时,SetServiceStatus应该再次被呼叫。此时您会设定dwCurrentStatus成员至SERVICE_STOPPED、SERVICE_PAUSED或SERVICE_RUNNING之控制码。当您回报了这叁个状态控制码之一时,因为服务已经完成了它的状态改变动作,所以dwCheckPoint与dwWaitHint成员应为0。
说明
在服务呼叫SetServiceStatus回报SERVICE_STOPPED时,SCM允许该服务可以执行30秒之久。如果服务在30秒后仍在执行中,若当时并没有其他的服务正在该处理程序中执行,则SCM会终止该服务的处理程序。
当HandlerEx函数收到一个SERVICE_CONTROL_INTERROGATE控制码时,该服务应该简单地告知设定dwCurrentState至服务的当前状态以及呼叫的SetServiceStatus已被接收(再一次说明,在建立SetServiceStatus呼叫前,设定dwCheckPoint与dwWaitHint为0)。
当系统关机时,HandlerEx会接收一个SERVICE_CONTROL_SHUTDOWN的通知控制码。服务应该完成将任何资料储存所必需之行动的最小限度,并且最后应该呼叫SetServiceStatus以回报SERVICE_STOPPED。为了及时地确认一个机器的关机动作,只要必须做的话,一个服务应该处理这个控制码。在预设的情形下,系统只会给20秒的时间以关闭所有的服务。在20秒之后,SCM处理程序(Service.exe)会被删除,而且机器会继续完成关机的动作。这个20秒的期间是经由设定WaitToKillServiceTimeout值而来的,它包含在以下的登录子机码中:
HKEY_LOCAL_MACHINE /SYSTEM /CurrentControlSet /Control
说明
当系统正在关机时,SCM会通知所有服务接受SERVICE_CONTROL_SHUTDOWN通知控制码。有一些服务可能会忽略这个控制码,一些可能会储存资料至磁盘中,另一些则可能会自己停止并终止执行。您必须非常小心地不去执行任何在您的服务中需要与其他服务关联的动作。这些其他的服务可能会存在一个「不适当」的状态或甚至必须终止执行。当在关机时,系统会完全地忽视服务的依存关系。Microsoft的目标是使系统能以尽可能快的速度关机。事实上,在您的服务收到它的通知前,其他的服务可能也会收到它们的关机通知。与这个关机通知顺序的问题是,您的服务所依赖的服务可能会在任何时间停止,而且您的服务必须处理这个情形。
通知控制码在之前已经列在表3-3中,您的HandlerEx函数应该处理该通知并且回传。除非通报回应迫使服务改变它的实施状态,否则不要呼叫SetServiceStatus函数。如果服务将要改变它的执行状态,则SetServiceStatus被呼叫去设定dwCurrentState、dwCheckPoint与dwWaitHint成员为像之前所讨论的适当值。
处理线程内部通讯议题
尽管主要线程执行了收到动作要求的HandlerEx函数,一个服务还是很难编写的,通常服务线程需要去做现实的工作以处理要求。例如,您也许想编写一个处理从命名管道上传来之客户端要求的服务。您的服务线程会自己暂停以等待一个客户端连结。如果您的HandlerEx线程收到一个SERVICE_CONTROL_STOP控制码,您该如何停止该服务?我曾经看过许多开发者只是简单地从HandlerEx函数中呼叫TerminateThread来强迫删除该服务线程。现在,您应该知道TerminateThread是您可能呼叫的函数中最糟的一个,因为线程不能取得一个清除的机会:线程的堆叠没有被删除、线程无法释放任何必须被等待的核心物件、DLLs没有收到线程已被删除的通知等等。
使服务停止之适当方法是以某种方式醒来、察看哪个服务应被停止、正确地清除以及从它的ServiceMain函数回传。为了建立一个服务执行它,您必须实作一些在您的HandlerEx与您的ServiceMain函数间的内部线程通讯。您可以使用任何您喜欢的要求内部线程通讯机制,包括APC伫列、套接字以及视窗讯息。我总是使用I/O完成端口。
为了更新目前的状态,一个服务必须频繁地呼叫SetServiceStatus。所有的这一切状态回报在服务编码方面可能是很困难的。服务的实作通当会思考在哪里放置呼叫SetServiceStatus的函数。以下是一些可能的方式:
所有以上所述的情形有正面的也有反面的。我曾对这些可能的方案做过实验并且大胆的建议使用最后的选项,理由是:
第一,SCP呼叫一个函数以控制一个服务,而且SCM传递此控制给服务。由于这点,SCP会暂停执行,等待服务呼叫SetServiceStatus以指示服务已经接收该控制码。假如服务的HandlerEx函数没有在30秒内回传,SCM会允许SCP醒来并且SCP的函数会呼叫去控制服务回传失败。
第二,HandlerEx函数经由服务处理程序的主要线程而被执行(所有在一个单一处理程序中的服务皆拥有它们的HandlerEx函数,而且它们被主要线程执行)。如果HandlerEx在等候ServiceMain于回传之前完成该动作,任何在同一个处理程序中的其他服务皆不能接收动作要求或通知。这可以使所有其他的服务出现没有回应的情形,这是不被接受的。
所以我优先选择第叁种方法—HandlerEx函数建立初始呼叫至SetServiceStatus,线程内部通讯被用来取得ServiceMain线程的控制码,而且由ServiceMain线程完成工作与呼叫SetServiceStatus以回报新的执行状态。然而,这个方法也有一个问题:存在着一个潜在的?赛条件(race condition)。想像一个服务的HandlerEx函数接收了一个SERVICE_CONTROL_PAUSE控制码,并以SERVICE_PAUSE_PENDING回答,然后传递控制码至ServiceMain线程。当ServiceMain线程开始处理该控制码时,突然间,HandlerEx线程先取得ServiceMain线程并且接收一个SERVICE_CONTROL_STOP控制码。HandlerEx函数现在回应一个SERVICE_STOP_PENDING控制码并且新的控制码伫列至ServiceMain线程中。当ServiceMain绪行绪再次取得CPU时间,它会完成自己的SERVICE_CONTROL_PAUSE控制码过程并且回报SERVICE_PAUSED。然后线程会察看被伫列之SERVICE_CONTROL_STOP控制码、停止服务以及回报SERVICE_STOPPED。在这些以后,SCM会接收以下的更新状态:
SERVICE_PAUSE_PENDING SERVICE_STOP_PENDING SERVICE_PAUSED SERVICE_STOPPED
就像您看到的,这些更新毫无意义,只会让管理者相当困惑。请注意,不管怎样,服务还是执行得很好。您会对我曾经见过可以实际地回执这个序列的数量感到惊讶。这些服务的开发者从未修复这些问题,它是不太可能会发生的,因为一个管理者会快速地发布动作要求至服务中—但是它还是可能发生。为了解决这个序列问题您必须使用一个同步线程机制。在本章后面的TimeService应用程序范例中使用了一个CGAte的C++ 类别来有效地解决这个问题。
当我开始使用服务时,认为SCM可能是预防发生竞赛条件的原因。但是经验告诉我SCM对确定一个适当地接受控制码的服务来说是没有帮助的。事实上,它真的没有什么帮助。意思是说:当一个服务已被暂停时,试着传送给服务一个SERVICE_CONTROL_PAUSE控制码。因为嵌入式管理单元一旦知道服务已被暂停即会使暂停按钮失效,所您不能在服务嵌入式管理单元中使用它。但是如果您使用SC.exe命令列工具程序,任何传送一个暂停控制码至已经被暂停的服务并不会被停止。我曾预期SCM回报失败至SC.exe工具程序中,但是SCM只会呼叫服务的HandlerEx函数,并传送SERVICE_CONTROL_PAUSE控制码。您的服务必须能够小心地处理这些不正确的控制码。
我曾经见过许多没有对存在于一个资料列中被多次传送至服务之相同控制码的可能性做处理。例如,我知道当服务被闲置时,它关闭了命名管道的handle。这个服务接下来会建立另一个核心物件,碰巧的是,它取得了与原始命名管道同样的handle值。然后服务会接收另一个暂停控制码并且呼叫CloseHandle与传递旧管道的handle值。由于这个值刚好和另一个核心物件的handle相同,所以新的核心物件会被删除,而其馀的服务则由于奇怪且不可思议的方法而失败。我无法告诉您该如何愉快的调整这个混乱的情形。
为了要修复这个多重的停止、暂停或继续执行控制码的问题,第一件是即是察看您的哪一些服务已经位于需求状态。如果它是的话,不要呼叫SetServiceStatus,也不要执行您的改变状态之程序代码—只要回传即可。这里有一些我常用在服务的逻辑。当HandlerEx函数接收一个SERVICE_CONTROL_PAUSE控制码时,HandlerEx函数会呼叫SetServiceStatus以回报SERVICE_PAUSE_ PENDING,呼叫SuspendThread以使服务的线程暂停执行,然后再次呼叫SetServiceStatus以回报SERVICE_PAUSED控制码。这一系列的呼叫是为了避免竞赛条件的产生,因为所有的工作被一个线程完成,但是这个控制码做了什么?闲置服务线程会使该服务暂停执行吗?对于这些,我想必须回答「是的」。然而,对于服务来说,暂停它代表什么意思?答案是依据服务而定。
如果我正在编写一个处理网路上之客户端要求的服务,对我来说,暂停代表停止接受新的要求,但是要如何处理正在处理中的要求呢?也许我应该完成它以使客户端不会被无限期地悬置。如果我的HandlerEx函数简单地呼叫了SuspendThread,该服务线程可能会在任何的状态中。也许该线程正呼叫至malloc,并试着去配置一些内存。假如另一个服务在也呼叫malloc的同一个处理程序中执行,这个服务也会被悬置(直到该存取动作被序列化为止)。这必定是我们不想要产生的情形。
看看这个如何:您认为您应该被允许去停止的一个已被暂停的服务吗?我想是的,而且显然Microsoft也是这样想的,因为即使该服务已被暂停,服务嵌入式管理单元还是允许我去按下停止按钮来停止它的执行。但是我要如何停止一个因为它的线程已被悬置之已被暂停的服务呢?请不要回答TerminateThread。
这些是关于向建立服务开发挑战的议题。
关于服务的议题
当您第一次开发服务程序时,您将会注意到有一些程序并没有照您的预期的状况执行。服务是一个在一个特殊的作业环境中执行的野兽。本节将会讨论一些您可能会遭遇的情形。然而,并不会花太多时间在它们上面,因为本书的各个章节中会对它们做更详细的说明。在此只是先给您一个大略的概念而已。
本机帐户与特定的使用者帐户
本节会开始解释在本机帐户中与在一个特定的使用者帐户中执行服务有什么不同。本机帐户是一个被作业系统给予的帐户,作业系统不会对它限制资源的存取权限。在本机帐户中执行一个服务时,可以存取任何的目录或文件、改变系统时间、启动或停止任何的服务、关闭机器以及没有任何的障碍即可以执行所有其他被限制的正常动作。一个本机服务被认为是系统之Trusted Computing Base(TCB)的一部份。
说明
这里是我在http://nsi.org/Library/Compsec/compglos.txt找到对Trusted Computing Base(TCB)的定义内容:「一个电脑系统中的总体保护机制」—包括了硬体、韧体以及软件—负责加强一个安全策略的结合。一个TCB由一个或多个在一个产品或系统上实施统一的安全策略之元件所组成。一个TCB的能力为正确地执行那些依据TCB以及由系统管理者所输入与安全策略有关之正确参数的安全策略。(例如,清除一个使用者帐户)。」
当然,所有的核心模式程序代码—硬体设备、内存管理、文件系统、安全控管、线程的工作排程等等,皆是系统之TCB的一部份。在TCB中执行的服务具有非常大的权力,这就是为什么只有机器的管理者拥有安装服务权利的缘故。
所以如果本机帐户拥有很大的权力,为何您还会想让服务在一个特定的使用者帐户中执行呢?的确,本机服务拥有在本机上的所有权力,在预设的情形下,它们不能在整个网路中被使用。例如,因为本地端机器的本机帐户无法被远端机器验证,所以本机服务不能存取另一台机器上被分享的目录、文件或打印机。在Windows 2000中,Microsoft对这个情况做了改善:当一台电脑位于网域中,您可以将它视为一个使用者帐户并取得它的存取权限。
如果您正在执行服务的机器并不存在一个网域中,而且您的服务需要存取网路资源时,以下是您可以做的:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services /LanmanServer/Parameters
您也能够允许由所有NULL工作阶段的连结而把资料值设置成0(位于相同的子机码下)以存取机器上的所有管道与被分享的资料。虽然您可以这么做,但您却不应该做它,因为它会使系统上产生一个巨大的安全漏洞。
本机与特定使用者之登录子机码
登录被分为二个主要的机码。第一个为HKEY_LOCAL_MACHINE,它被用来储存所有的系统设定。一个服务或是一个应用程序可以经常读取任何位于此机码中的设定值。
第二个机码为HKEY_USERS,它被用来储存每一个使用者之特定设定值。这个机码会进一步分成二个类型之子机码。第一个类型是一个特定的使用者子机码。机器上的每一个使用者帐户皆有一个在HKEY_USERS之下对应至子机码之登录设定集合。当特定的使用者登入并且成为一个互动的使用者时,常见的HKEY_CURRENT_USER机码会对应至位于HKEY_USERS下的特定使用者之子机码。
第二个位于HKEY_USERS中的子机码称为 .DEFAULT,它包含了一个使用者的内定设定值。当一个新的使用者帐户在系统中被建立时,在HKEY_USERS之中也会建立一个新的子机码而且该子机码的设定值会与目前存放在 .DEFAULT子机码中的设定相同。
就像位于HKEY_LOCAL_MACHINE中的设定一般,虽然一个服务并不需要存取它,但是存在HKEY_USERS/.DEFAULT中的设定值还是可以被服务与应用程序使用。一个存放于HKEY_USERS下之特定的使用者设定不能被使用,除非该使用者登入至系统中。由于服务通常会在本机帐户下执行,所以服务不该试图去存取任何储存在HKEY_USERS中特定使用者帐户内容。相同的一个本机帐户服务也不该使用HKEY_CURRENT_USER来存取登录之内容。关于登录设定与使用者设定档的更多资讯,请参阅
核心物件之安全性
本节会介绍许多开发者皆会遇到的一个常见问题。即当他们的客户端应用程序与服务在同一台机器上执行而且客户端与服务尝试分享一个核心物件时所发生的问题。
以下为解决方案。您的服务开始执行并且呼叫CreateFileMapping去建立一个文件对应的物件。CreateFileMapping建立了一个核心物件,所以它的参数之一是一个SECURITY_ATTRIBUTES资料结构的位址。假如您像多数的程序开发者一样的话,则您会传递NULL给此参数,因为核心物件会被以「预设的安全性」建立。请注意我所提的是「预设的安全性」,而不是「没有安全性」。预设的安全性表示在物件被建立时经由安全性条件而定义之物件存取控制。
例如,一个核心物件会被本机服务所建立,在预设的情形下,它被允许可以完全的存取任何不在执行中的本机帐户,并只允许可以读取与执行本端管理者之成员群组。所以如没有一个本机服务以预设的安全性建立了一个文件对应的物件,则一个在本机管理员帐户下执行的应用程序可以从读取文件对应物件,但不能以任何方式对它做写入的动作。在任何其他帐户下执行的应用程序不能完全存取文件对应物件。
现在,我只是想要使您知道这个议题。有很多关于客户端与服务之核心物件安全性的内容可以谈,而且有很多方法可以处理它,但是必须是在您更了解Windows安全性的前提下。所以建议您阅读本书第五篇中有关安全性的章节,以取得所有的细节。
互动式的服务、视窗配置与桌面
本节只讨论有关服务如何影响视窗配置与桌面之内部操作的议题。有关视窗配置与桌面的更多资讯,请参阅 第十章 。
当您建立了一个核心物件时,您可以指定它应该被安全地经由一个SECURITY_ATTRIBUTES结构传送。但是像视窗与功能表这种使用者物件要如何做呢?使用者物件使用了一个不同的型式;它们不会被开启或关闭—只要在您需要的时候存取它们即可,如此可使程序代码容易编写以及增进效能。另外,使用存在于16位元之Windows作业系统中的物件并不以任何形式来支援安全性。如果Microsoft加入SECURITY_ATTRIBUTES结构至CreateWindow以及CreateMenu中,开发者在放置他们的16位元程序代码时会产生困难。
Microsoft需要一个在不影响已存在之函数以及不影响您使用物件的前提下,让使用者物件变得安全的方式。这包括了所有关于视窗配置与桌面的部份。一个视窗配置为一个 逻辑 的键盘、滑鼠以及显示器之集合体。「逻辑」一字表示设备不一定真的存在。当系统被启动时,它会建立互动式的视窗配置,称为「WinSta0」;实体的键盘、滑鼠以及显示器会与这个视窗配置关联。一个视窗配置也可以包含它所拥有的记事本、一个通用元素的集合以及一个桌面物件群组。
一个桌面由一个逻辑的显示外观以及一个使用者物件的集合所组成:视窗、功能表以及拦截程序(Hook)。线程也会与桌面联系(请参阅Platform SDK文件中的SetThreadDesktop与GetThreadDesktop函数的内容)。假如一个已与桌面关联的线程试图传送一个讯息至被另一个桌面建立的视窗时,线程不能在属于另一个桌面之线程上安装拦截程序。
就像视窗配置一样,桌面会被它的字串名称所确认。WinLogon.exe会建立叁个桌面:
系统自己拥有本身的特定使用者帐户。所以实际上有二个「使用者」曾经存取机器:即本机使用者及已登入的使用者。当然,如果这两个使用者正在单一机器上执行应用程序,您不用等待所有应用程序之视窗在单一的显示设备中显现,因为二个使用者皆会要求分配一个属于自己的视窗配置。
由于这个原因,本机帐户会被给予一个属于它的视窗配置,称为「Service-0x0-3e7$」(那些数字是服务的登入SID),以及属于它的桌面,称为Default。视窗配置为非互动式而且没有一个实体的键盘、滑鼠与显示器—即本机「使用者」不是一个真实的人类,因此不需要去打字、按下滑鼠或「察看」任何东西。任何正在此视窗配置下之桌面上执行的应用程序皆可以建立一个视窗,但是该视窗不会被显示给已登录的使用者看。这就是为什么服务不应显示一个使用者介面的原因:没有人会看到它,而且线程会为了等待输入而被悬置,因此可能永远都无法被执行。
使用服务嵌入式管理单元,您可以显示一个服务的内容。您可以回想一下登入身份页签内容包含了一个允许服务与桌面互动的核取方块。当它被选择时,这个选项会让SCM以互动式视窗配置之预设桌面的方式启动一个服务:「WinSta0/Default」。注意您只能在您的服务执行于本机帐户的情形下才能选择该选项。因为本机帐户拥有较高的存取权限并且它能够存取互动式的使用者视窗配置与桌面。
有一个与服务跟桌面互动有关的问题即是预设值。桌面不会经常被显示。当萤幕保护程序执行或是使用者登出系统时,服务的使用者介面还保留在预设的桌面上,但是在下一个使用者登入前,该使用者介面不会被显示。另一个问题是它使系统相当的不安全。例如,一个标准的应用程序可以传送视窗讯息至服务的视窗中。该应用程序使用已登入使用者的安全条件执行,但是此应用程序现在正与一个经由一个不安全通道执行本机帐户之处理程序互相通讯。一个被限制的使用者可以轻易地存取不应被他们取得的系统资源。
这里有一个范例:一个服务正以本机帐户执行而且它正显示在互动式预设桌面的视窗中。正常的情形下,当已登入的使用者试图使用工作管理员的处理程序页签去删除一个服务时,会出现一个拒绝存取或类似的讯息而且该服务会继续执行。这个功能当然是被需要的。您不会想要让一个以Guest帐户登入至系统上的使用者删除一个服务器的服务、妨碍位于网路上的其他使用者存取目录、文件与打印机(特别是当服务器服务在Service.exe中执行时)。然而,假如本机服务建立了一个互动式的视窗,则已登入的使用者会在工作管理员的应用程序页签中看见它。此时选择工作结束会传送一个WM_CLOSE讯息至视窗中,使得一个服务能被删除。
说明
由于上述的所有理由,Microsoft强烈地建议使用允许服务与桌面互动核取方块。事实上,一个管理员可以经由与桌面互动的方式将以下所示之登录子机码NoInteractiveServices值设定为非0值以禁止服务执行:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Windows
所以在一个特定的使用者帐户下执行服务如何?当SCM在一个特定的使用者帐户下执行一个可执行的服务时,SCM会先对使用者做验证,请使用者提供使用者名称与密码会成为服务设定资讯的一部份(使用者名称被储存在登录中,而密码则储存在一个安全的LSA文件中)。这个验证过程建立了一个登入工作阶段,表示哪一个取得自己拥有之非互动式视窗与桌面。可执行服务现在已包含了使用这个专用的视窗配置与桌面,其名称大概是「UserAccountLogonSID/ Default」,「UserAccountLogonSID」是一个在验证期间产生的唯一数字。
这个唯一的鉴定表示如果您拥有二个或多个设定在同一个使用者帐户下执行的服务,则每一个皆会取得它自己所拥有的登入SID、视窗配置以及桌面(如果它们在不同的处理程序中)—位于这些可执行服务中的线程不能经由使用者物件而互相通讯。这个唯一的鉴定也表示如果一个正在目前已使用互动式登入的使用者帐户下执行服务,则该服务的使用者介面不会被显示出来。反之,本机帐户只会被验证一次,所以所有分享同一个视窗配置与桌面以及可以经由使用者物件通讯的许多可执行服务会在本机帐户下执行。
Microsoft已经加入一些特定的服务特性至常见的MessageBox(Ex)函数中。第一,当您传递了MB_SERVICE_NOTIFICATION标记时,不管哪一个桌面为WinLogon、预设或是正在执行萤幕保护程序,该函数皆会在互动式视窗配置之活动中桌面上显示讯息方块。这保证讯息方块会显示在显示设备中。
第二,MessageBox(Ex)支援MB_DEFAULT_DESKTOP_ONLY标记。除了它只在互动式视窗配置的预设桌面上显示讯息方块外,此标记与MB_SERVICE_ NOTIFICATION是很类似的:一个使用者必须登入才能看见该讯息方块。注意除非一个使用者已经看见讯息方块并且已按下按钮使其离开,否则MessageBox(Ex)不会返回。顺便一提,若要使一个服务不需在本机帐户下执行,也没有核取允许服务与桌面互动之核取方块时,不是使用MB_DEFAULT_DESKTOP_ONLY就是使用MB_SERVICE_NOTIFICATION标记来达到这个目的。系统会保证该讯息方块会被显示。
如果您一直谨慎地跟随着我们的进度,则您应该会注意到在我使用为了创造互动式服务时所建立的每个方法时,使您的意志受挫了。如果您想制作一个提供使用者介面的服务时,应该如何做呢?这个回答很简单:建立一个分开的、使用一些IPC机制(RPC、COM、命名管道、套接字、内存映对文件等)与服务对谈的客户端应用程序。我知道有许多人不想这么做,因为他们必须建立一个可以自我执行的全新专案,但是建立一个分开的应用程序是正确且可得到最多支援的方法。如果您这么做的话,那么Microsoft便无法简单地打破您的架构。
客户端应用程序应该在Windows 2000、Windows 98、UNIX或任何其他作业系统上执行。您可以建立一个以HTML为基础的使用者介面以与使用ActiveX控制或Active Server Pages的服务沟通。您也应该为了MMC而考虑建立您的使用者介面之嵌入式管理单元。想想这个客户端应用程序为了您而开启的可能性,不是因为所有使用者介面被发布而限制它们。
在移至这个主题之前,我想要多讨论一些有关使用者介面的议题:一些Windows函数会产生硬体错误的讯息方块。例如,如果您正从光碟执行一个应用程序并且移除了光碟片,此时系统会自动地显示一个讯息方块。如果系统没有显示讯息方块,它的另一个选择即是删除该处理程序。
它有被系统修改的可能性,所以硬体错误会被记录至事件记录中并且不会产生讯息方块。要改变系统的行为,您必须在以下所列之登录子机码中修改ErrorMode的值:
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Windows
表3-4列出了ErrorMode的可能值。
表3-4 定义是否显示硬体错误之讯息方块的ErrorMode值 |
值 | 说明 |
---|---|
0(预设值) | 系统会显示错误讯息方块。 |
1 | 对于非系统产生的错误,会显示一个错误讯息方块。若为系统产生的错误,则会在事件记录中加入一个项目并且不会显示错 误讯息方块。 |
2 (一个没有人看顾的服务器之最佳选择) | 不管为系统产生或非系统产生的错误,皆加入事件记录中,并且不会显示错误讯息方块。 |
对服务侦错
在对一个服务侦错时比对一般应用程序的侦错更难处理,原因有许多个。第一,除错器无法启动服务,必须由SCM启动服务。第二,许多服务在使用者登入前即已启动。由于这个原因,当您在对服务做侦错时,将设定为自动启动的服务改为手动启动的方式是一个好方法。第叁,服务在它们各自拥有的视窗配置与桌面中执行,而它们不会为了互动式使用者而显示。
所以您要如何对服务侦错呢?最好的方法是像服务那样固定地执行一个固定的可执行程序。在您服务内的(w)main或(w)WinMain函数会为一个特定的命令列打开您所拥有的设计,如果该开关已被打开,则以直接地呼叫您服务之ServiceMain函数的方式取代呼叫StartServiceCtrlDispatcher。这个技术当然会有很多缺点:
刚才所提的方法可以使您较容易的对您的服务做侦错,但是有一个较好的方法是当服务正在执行时,将侦错器与服务连接起来。大部份的侦错器皆提供连接至正在执行之处理程序的能力。如果您已经在系统上安装了一个侦错器,那么您便可以开启工作管理员,在服务处理程序的名称上敲击滑鼠右键,并且由在功能表中选择侦错选项。这个动作会使侦错器连接至您的服务。现在您可以设定中断点、对服务程序代码侦错甚至可以测试您的程序如何回应控制码的通知。以下为将侦错器与服务连接所产生的几个问题:
如果您真的想要在使用这个方法来对您的服务之初始化程序代码做侦错,只要在您的(w)main或(w)WinMain中增加一个对DebugBreak函数的呼叫即可以简单地达到这个目的。然而这个技术只在您的服务以本机帐户执行时才能产生作用。如果您在一个不同的使用者帐户下执行您的服务时,此时您的侦错器不会正常地执行,因为系统不允许它与互动式视窗配置及桌面沟通。
这里有另一个您可以用来对服务侦错的技巧:Windows提供了一个每当一个处理程序启动时即唤起侦错器的能力。要使系统达到这个功能,您必须先建立一个以下所列之登录机码中的子机码:
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/ CurrentVersion/Image File Execution Options
在这个机码下建立一个与可执行之服务名称相同的子机码(不用加入路径)。在该可执行名称之子机码中,加入一个名称为Debugger的字串值,并且设定其值为您的侦错器之完整的执行路径(例如,「C:/Program Files/Microsoft Visual Studio/Common/MSDev98/Bin/msdev.exe」)。
一旦您将这些设定完成后,您便可以到服务嵌入式管理单元中启动服务。此时SCM会启动侦错器而非可执行之服务。由此,您可以开启您的服务程序代码文件、设定中断点然后让服务执行。注意在SCM强迫终止侦错器前,您有30秒的时间可以做您的侦错工作(因为服务不会呼叫StartServiceCtrlDispatcher)。
如果所有的这些限制令您烦恼,而且您想要能够在最自然的情形下对服务侦错,不用与桌面互动或担心使用者帐户的问题,那么您可以使用核心的侦错器。
TimeService服务范例
在本节最后面的列表3-1中说明了TimeService服务范例(「03 TimeService.exe」),其中包括了所有建立一个服务所需的元件。该应用程序的程序代码与资源文件皆存放在随书光碟中的03-TimeService目录中。这是一个非常简单的服务,当一个客户端连结至此服务时,它会回传执行服务之机器的日期以及时间。此服务假设您已经稍微对命名管道与I/O完成端口有所了解(在第二章中讨论)。
如果您检查_tWinMain函数,您将会看见这个服务拥有依据命令列参数所传送之「-install」或「-remove」可以选择从SCM的资料库中安装或移除它自己的能力。一旦您建立了服务并且第一次从命令列环境中执行它时,请传递「-install」参数给它。当您不想让该服务保存在您的机器上时,请从命令列环境中执行它,并传递「-remove」参数给它。我将会在下一章中讨论如何从SCM增加或移除一个服务之函数的细节。
_tWinMain之最重要的观念是去通知一个被二个成员初始化的SERVICE_TABLE_ENTRY结构阵列:一个是服务,另一个是使用NULL项目来识别最后一个服务。那些为了服务而建立的线程之服务表格阵列位址会被传递到StartServiceCtrlDispatcher中。这个新的线程会与TimeServiceMain函数一起开始执行。注意除非TimeServiceMain函数已离开且它的线程已终止执行,否则StartServiceCtrlDispatcher不会返回至_tWinMain函数。
TimeServiceMain函数实作了真实的程序代码以处理客户端的要求。经由建立一个I/O完成端口可以启动它。该服务线程会在一个等待要求以进入完成连接埠的回圈中执行。有二种可能的要求类型:一个客户端已连接至管道并且等待机器的日期与时间资讯;或是该服务需要去处理一个例如暂停、继续或停止的动作要求。
一旦完成连接埠被建立后,我初始化了一个全域的CserviceStatus物件—g_ssTime。该CserviceStatusC++ 类别是我自己建立的,而且它只是简单地回报服务的更新状态。这个类别是来自Windows的SERVICE_STATUS结构而且基本上它使用了一点点的抽象化概念,用来放置被更新的成员变数之一些逻辑的状态。CserviceStatus拥有一个被使用来让TimeHandlerEx线程与TimeServiceMain达到同?以及允许ServiceMain线程在同一时间处理一个单一动作之CGate类别物件成员。
在CserviceStatus的Initialize方法内呼叫了RegisterServiceCtrlHandlerEx以通知HandlerEx函数(TimeHandlerEx)的SCM,而且它会传递C++ 类别物件之I/O完成端口的位址iocp至它的pvContext中以控制函数。Initialize方法中也设定了dwServiceType成员,以表示在服务的执行期间中没有被改变的内容。
接下来,我呼叫了AcceptControls方法,以设定dwControlsAccepted成员。当服务执行至接受或拒绝被要求的控制时,AcceptControls方法可以被周期性地呼叫。在执行期间TimeService接受停止、暂停、继续以及关闭之控制码。
您将会注意到TimeHandlerE x函数经由呼叫CIOCP的PostStatus方法来传递控制码给服务线程,并经由使用I/O完成端口的Handle来由内部呼叫PostQueuedCompletionStatus。一个代表完成的CK_SERVICECONTROL控制码会被指定以指示ServiceMain已经因为一个服务之要求而醒来,然后TimeHandlerEx函数会尽快地返回。服务线程的责任为醒来,处理控制码,然后等待更多的客户端要求。
在TimeServiceMain函数中,一个do-while回圈开始启动。在该回圈中,我检查了CompKey变数的值察看服务下一个需要回应的动作。由于此变数被初始化到CK_SERVICECONTROL中,而dwControl变数被初始化至SERVICE_CONTROL_ CONTINUE中,所以此服务的第一件工作即是建立一个命名管道,然后客户端应用程序会使用它来建立对服务的要求。接下来会使用一个CK_PIPE的完成控制码来使这个管道与完成连接埠关联,并且建立一个对ConnectNamedPipe的非同步呼叫。此服务现在会经由呼叫g_ssTime物件之ReportUltimateState方法中以呼叫内部的SetServiceStatus方式将SERVICE_RUNNING回报至SCM中。
服务呼叫iocp物件之GetStatus方法(内部呼叫GetQueuedCompletionStatus)。这导致服务线程进入睡眠状态,直到一个事件在完成连接埠中出现为止。如果出现了一个服务控制码(因为TimeHandlerEx已经呼叫PostQueuedCompletion Status),则服务线程会醒来,适当地处理控制码以及再次将作业完成的状态回报至SCM。请注意,TimeHandlerEx的责任是回报动作的悬置状态,而TimeServiceMain的义务则是回报服务的最后执行状态(即是我曾在之前的〈处理线程内部通讯议题〉一节中讨论的第叁个方法)。
当客户端已经连结至管道,而服务线程因为GetQueuedCompletionStatus的返回而醒来时, CK_PIPE的完成控制码会被回传。由此,服务会取得系统时间并呼叫WriteFile以传送时间至客户端。然后服务会中断客户端与发布另一个对ConnectNamedPipe的非同步呼叫的连结,以让其他的客户端可以与它连接。
当服务线程因为SERVICE_CONTROL_STOP或SERVICE_CONTROL_ SHUTDOWN控制码而醒来时,它会关闭管道并终止执行。这会导致完成连接埠关闭、TimeServiceMain函数返回、并且将该服务线程删除。由此,StartServiceCtrlDispatcher会返回至_tWinMain中,而它也会返回并删除该处理程序。
在您建立了服务后,您必须在命令列环境中传递「-install」以将服务安装至SCM的资料库中。因为在执行档名称中包含了空白字元,所以请确定在可执行档名称前后包括了引号,即「"03 TimeService.exe"」。而且,您会想要使用服务嵌入管理单元去启动以及管理该「Programming Server-Side Applications Time」服务。
TimeService.cpp /******************************************************************** /模组:TimeService.cpp 通告:Copyright (c)2000 Jeffrey Richter ********************************************************************/ #include "../CmnHdr.h" /* 请参阅附录A */ #include "../ClassLib/IOCP.h" /* 请参阅附录B */ #include "../ClassLib/EnsureCleanup.h" /* 请参阅附录B */ #define SERVICESTATUS_IMPL #include "ServiceStatus.h" ////////////////////////////////////////////////////////////////////////////// TCHAR g_szServiceName[] = TEXT("Programming Server-Side Applications Time"); CServiceStatus g_ssTime; ////////////////////////////////////////////////////////////////////////////// // 完成连接埠因为以下二个原因之一而醒来 enum COMPKEY { CK_SERVICECONTROL, // 一个服务控制码 CK_PIPE // 一个客户端连接至我们的管道 }; ////////////////////////////////////////////////////////////////////////////// DWORD WINAPI TimeHandlerEx(DWORD dwControl, DWORD dwEventType, PVOID pvEventData, PVOID pvContext) { DWORD dwReturn = ERROR_CALL_NOT_IMPLEMENTED; BOOL fPostControlToServiceThread = FALSE; switch (dwControl) { case SERVICE_CONTROL_STOP: case SERVICE_CONTROL_SHUTDOWN: g_ssTime.SetUltimateState(SERVICE_STOPPED, 2000); fPostControlToServiceThread = TRUE; break; case SERVICE_CONTROL_PAUSE: g_ssTime.SetUltimateState(SERVICE_PAUSED, 2000); fPostControlToServiceThread = TRUE; break; case SERVICE_CONTROL_CONTINUE: g_ssTime.SetUltimateState(SERVICE_RUNNING, 2000); fPostControlToServiceThread = TRUE; break; case SERVICE_CONTROL_INTERROGATE: g_ssTime.ReportStatus(); break; case SERVICE_CONTROL_PARAMCHANGE: break; case SERVICE_CONTROL_DEVICEEVENT: case SERVICE_CONTROL_HARDWAREPROFILECHANGE: case SERVICE_CONTROL_POWEREVENT: break; case 128: // 一个测试用的使用者定义控制码 // 注意:正常的情况下,一个服务不该显示使用者介面 MessageBox(NULL, TEXT("In HandlerEx processing user-defined code."), g_szServiceName, MB_OK |MB_SERVICE_NOTIFICATION); break; } if (fPostControlToServiceThread) { // Handler线程非常简单而且执行的非常快速 // 它只传递控制码以离开ServiceMain线程 CIOCP* piocp = (CIOCP*) pvContext; piocp->PostStatus(CK_SERVICECONTROL, dwControl); dwReturn = NO_ERROR; } return(dwReturn); } ////////////////////////////////////////////////////////////////////////////// void WINAPI TimeServiceMain(DWORD dwArgc, PTSTR* pszArgv) { ULONG_PTR CompKey = CK_SERVICECONTROL; DWORD dwControl = SERVICE_CONTROL_CONTINUE; CEnsureCloseFile hpipe; OVERLAPPED o, *po; SYSTEMTIME st; DWORD dwNumBytes; // 建立完成端口并储存在整体变数中它的handle //所以Handler函数可以存取它 CIOCP iocp(0); g_ssTime.Initialize(g_szServiceName, TimeHandlerEx, (PVOID) &iocp, TRUE); g_ssTime.AcceptControls( SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE); do { switch (CompKey) { case CK_SERVICECONTROL: // 我们取得一个控制码 switch (dwControl) { case SERVICE_CONTROL_CONTINUE: // 当它正在执行时,建立一个管道以让客户端连结 hpipe = CreateNamedPipe(TEXT("////.//pipe//TimeService"), PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE, 1, sizeof(st), sizeof(st), 1000, NULL); // 建立管道与完成连接埠之关联 iocp.AssociateDevice(hpipe, CK_PIPE); // 相对于管道,此处悬置了一个非同步连接 ZeroMemory(&o, sizeof(o)); ConnectNamedPipe(hpipe, &o); g_ssTime.ReportUltimateState(); break; case SERVICE_CONTROL_PAUSE: case SERVICE_CONTROL_STOP: // 若没有在执行中,则关闭管道以使客户端无法与之连结 hpipe.Cleanup(); g_ssTime.ReportUltimateState(); break; } break; case CK_PIPE: if (hpipe.IsValid()) { // 我们取得了一个客户端要求:传送我们的系统时间至客户端 GetSystemTime(&st); WriteFile(hpipe, &st, sizeof(st), &dwNumBytes, NULL); FlushFileBuffers(hpipe); DisconnectNamedPipe(hpipe); // 允许另一个客户端连结 ZeroMemory(&o, sizeof(o)); ConnectNamedPipe(hpipe, &o); } else { // 当管道被关闭时,我们会执行到此处 } } if (g_ssTime != SERVICE_STOPPED) { // 除非一个控制码进来或是有一个客户端连接上来,否则即处于睡眠状态 iocp.GetStatus(&CompKey, &dwNumBytes, &po); dwControl = dwNumBytes; } } while (g_ssTime != SERVICE_STOPPED); } ///////////////////////////////////////////////////////////////////////////////////////// void InstallService() { // 在此机器上开启SCM CEnsureCloseServiceHandle hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); // 取得我们的路径名称 TCHAR szModulePathname[_MAX_PATH * 2]; GetModuleFileName(NULL, szModulePathname, chDIMOF(szModulePathname)); // 因为处理程序去执行一个服务,所以增加转换switch lstrcat(szModulePathname, TEXT(" /service")); // 将此服务加入SCM的资料库中 CEnsureCloseServiceHandle hService = CreateService(hSCM, g_szServiceName, g_szServiceName, SERVICE_CHANGE_CONFIG, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szModulePathname, NULL, NULL, NULL, NULL, NULL); SERVICE_DESCRIPTION sd = { TEXT("Sample Time Service from") TEXT("Programming Server-Side Applications for Microsoft Windows Book") }; ChangeServiceConfig2(hService, SERVICE_CONFIG_DESCRIPTION, &sd); } ////////////////////////////////////////////////////////////////////////////// void RemoveService() { // 在此机器上开启SCM CEnsureCloseServiceHandle hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); // 为了DELETE的存取而开启这个服务 CEnsureCloseServiceHandle hService = OpenService(hSCM, g_szServiceName, DELETE); // 从SCM资料库中移除此服务 DeleteService(hService); } ////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { int nArgc = __argc; #ifdef UNICODE PCTSTR *ppArgv = (PCTSTR*) CommandLineToArgvW(GetCommandLine(), &nArgc); #else PCTSTR *ppArgv = (PCTSTR*) __argv; #endif if (nArgc < 2) { MessageBox(NULL, TEXT("Programming Server-Side Applications for Microsoft Windows:") TEXT("Time Service Sample/n/n") TEXT("Usage:TimeService.exe [/install] [/remove] [/debug] ") TEXT("[/service]/n") TEXT(" //install/t/tInstalls the service in the SCM's database./n") TEXT(" //remove/t/tRemoves the service from the SCM's database./n") TEXT(" //debug /t /tRuns the service as a normal process for ") TEXT("debugging./n ") TEXT(" //service /t/tRuns the process as a service") TEXT("(should only be set in the SCM's database)."), g_szServiceName, MB_OK); } else { for (int i = 1; i < nArgc; i++) { if ((ppArgv[i][0] == TEXT('-')) || (ppArgv[i][0] == TEXT('/'))) { // 命令列开关 if (lstrcmpi(&ppArgv[i][1], TEXT("install")) == 0) InstallService(); if (lstrcmpi(&ppArgv[i][1], TEXT("remove")) == 0) RemoveService(); if (lstrcmpi(&ppArgv[i][1], TEXT("debug")) == 0) { // 执行服务控制码 TimeServiceMain(0, NULL); } if (lstrcmpi(&ppArgv[i][1], TEXT("service")) == 0) { // 连接至服务控制分发器 SERVICE_TABLE_ENTRY ServiceTable [] == { { g_szServiceName, TimeServiceMain }, { NULL, NULL } // End of list }; chVERIFY(StartServiceCtrlDispatcher(ServiceTable)); } } } } #ifdef UNICODE HeapFree(GetProcessHeap(), 0, (PVOID) ppArgv); #endif return(0); } ///////////////////////////////// End of File //////////////////////////////// ServiceStatus.h /******************************************************************* 模组:ServiceStatus.h 注意:Copyright (c)2000 Jeffrey Richter 目的:这个类别封装了一个SERVICE_STATUS结构以适当的使用 ********************************************************************/ #pragma once /////////////////////////////////////////////////////////////////////////////// #include "../CmnHdr.h" /*See Appendix A. */ #include "Gate.h " /////////////////////////////////////////////////////////////////////////////// class CServiceStatus : public SERVICE_STATUS { public: CServiceStatus(); BOOL Initialize(PCTSTR szServiceName, LPHANDLER_FUNCTION_EX pfnHandler, PVOID pvContext, BOOL fOwnProcess, BOOL fInteractWithDesktop = FALSE); VOID AcceptControls(DWORD dwFlags, BOOL fAccept = TRUE); BOOL ReportStatus(); BOOL SetUltimateState(DWORD dwUltimateState, DWORD dwWaitHint = 0); BOOL AdvanceState(DWORD dwWaitHint, DWORD dwCheckPoint = 0); BOOL ReportUltimateState(); BOOL ReportWin32Error(DWORD dwError); BOOL ReportServiceSpecificError(DWORD dwError); operator DWORD() const { return(dwCurrentState); } private: SERVICE_STATUS_HANDLE m_hss; CGate m_gate; }; /////////////////////////////////////////////////////////////////////////////// inline CServiceStatus::CServiceStatus() { ZeroMemory(this, sizeof(SERVICE_STATUS)); m_hss = NULL; } /////////////////////////////////////////////////////////////////////////////// inline VOID CServiceStatus::AcceptControls(DWORD dwFlags, BOOL fAccept) { if (fAccept) dwControlsAccepted |= dwFlags; else dwControlsAccepted &= ~dwFlags; } /////////////////////////////////////////////////////////////////////////////// inline BOOL CServiceStatus::ReportStatus() { BOOL fOk = ::SetServiceStatus(m_hss, this); chASSERT(fOk); return(fOk); } /////////////////////////////////////////////////////////////////////////////// inline BOOL CServiceStatus::ReportWin32Error(DWORD dwError) { dwWin32ExitCode = dwError; dwServiceSpecificExitCode = 0; return(ReportStatus()); } inline BOOL CServiceStatus::ReportServiceSpecificError(DWORD dwError) { dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; dwServiceSpecificExitCode = dwError; return(ReportStatus()); } /////////////////////////////////////////////////////////////////////////////// #ifdef SERVICESTATUS_IMPL /////////////////////////////////////////////////////////////////////////////// BOOL CServiceStatus::Initialize(PCTSTR szServiceName, LPHANDLER_FUNCTION_EX pfnHandler, PVOID pvContext, BOOL fOwnProcess, BOOL fInteractWithDesktop) { m_hss = RegisterServiceCtrlHandlerEx(szServiceName, pfnHandler, pvContext); chASSERT(m_hss != NULL); dwServiceType = fOwnProcess ? SERVICE_WIN32_OWN_PROCESS : SERVICE_WIN32_SHARE_PROCESS; if (fInteractWithDesktop) dwServiceType |= SERVICE_INTERACTIVE_PROCESS; dwCurrentState = SERVICE_START_PENDING; dwControlsAccepted = 0; dwWin32ExitCode = NO_ERROR; dwServiceSpecificExitCode = 0; dwCheckPoint = 0; dwWaitHint = 2000; return(m_hss != NULL); } /////////////////////////////////////////////////////////////////////////////// BOOL CServiceStatus::SetUltimateState(DWORD dwUltimateState, DWORD dwWaitHint) { DWORD dwPendingState = 0; // 一个无效的状态值 switch (dwUltimateState) { case SERVICE_STOPPED: dwPendingState = SERVICE_STOP_PENDING; break; case SERVICE_RUNNING: dwPendingState = (dwCurrentState == SERVICE_PAUSED) ? SERVICE_CONTINUE_PENDING :SERVICE_START_PENDING; break; case SERVICE_PAUSED: dwPendingState = SERVICE_PAUSE_PENDING; break; default: chASSERT(dwPendingState != 0); // Invalid parameter break; } // 当建立了一个新的ServiceMain线程,系统假设 // dwCurrentState=SERVICE_START_PENDING, dwCheckPoint=0, dwWaitHint=2000 // So,since we must always increment the checkpoint, let's start at 1 dwCheckPoint = 1; this->dwWaitHint = dwWaitHint; // 没有错误被回报 dwWin32ExitCode = NO_ERROR; dwServiceSpecificExitCode = 0; BOOL fOk = FALSE; // 假设失败的情形 if (dwPendingState != 0) { // 如果另一个悬置中的作业没有被完成,即等待它 m_gate.WaitToEnterGate(); dwCurrentState = dwPendingState; // Update the state in the structure // 如果没有等待提示,则到达要求状态 fOk = (dwWaitHint != 0) ? ReportStatus() : ReportUltimateState(); } return(fOk); } /////////////////////////////////////////////////////////////////////////////// BOOL CServiceStatus::AdvanceState(DWORD dwWaitHint, DWORD dwCheckPoint) { // 一个为0的检查点为无效的,所以我们会增加检查点至1 this->dwCheckPoint = (dwCheckPoint == 0) ? this->dwCheckPoint + 1 : dwCheckPoint; this->dwWaitHint = dwWaitHint; // 没有错误被回报 dwWin32ExitCode = NO_ERROR; dwServiceSpecificExitCode = 0; return(ReportStatus()); } /////////////////////////////////////////////////////////////////////////////// BOOL CServiceStatus::ReportUltimateState() { DWORD dwUltimateState = 0; // An invalid state value switch (dwCurrentState) { case SERVICE_START_PENDING: case SERVICE_CONTINUE_PENDING: dwUltimateState = SERVICE_RUNNING; break; case SERVICE_STOP_PENDING: dwUltimateState = SERVICE_STOPPED; break; case SERVICE_PAUSE_PENDING: dwUltimateState = SERVICE_PAUSED; break; } dwCheckPoint = dwWaitHint = 0; // We reached the ultimate state // 没有错误被回报 dwWin32ExitCode = NO_ERROR; dwServiceSpecificExitCode = 0; BOOL fOk = FALSE; // 假设为失败的情形 if (dwUltimateState != 0) [ dwCurrentState = dwUltimateState; // 更新结构中的状态 fOk = ReportStatus(); // 我们的状态改变完成,允许一个新的状态被改变 m_gate.LiftGate(); } return(fOk); } /////////////////////////////////////////////////////////////////////////////// #endif // SERVICESTATUS_IMPL //////////////////////////////// End of File /////////////////////////////////
Gate.h /****************************************************************************** 模组:Gate.h 通告:Copyright (c)2000 Jeffrey Richter 目的:这个类别建立了一个正常开启的通讯闸,同一个时间内只有一个线程由此经过*******************************************************************************/ #pragma once // 在完成单元前包含这个标头文件 /////////////////////////////////////////////////////////////////////////////// #include "../CmnHdr.h" /* 请参阅附录A */ /////////////////////////////////////////////////////////////////////////////// class CGate { public: CGate(BOOL fInitiallyUp = TRUE, PCTSTR pszName = NULL) { m_hevt = ::CreateEvent(NULL, FALSE, fInitiallyUp, pszName); } ~CGate() { ::CloseHandle(m_hevt); } DWORD WaitToEnterGate(DWORD dwTimeout = INFINITE, BOOL fAlertable = FALSE) { return(::WaitForSingleObjectEx(m_hevt, dwTimeout, fAlertable)); } VOID LiftGate() {::SetEvent(m_hevt); } private: HANDLE m_hevt; }; ///////////////////////////////// End of File /////////////////////////////////
列表3-1 TimeService服务范例 |
TimeClient应用程序范例
TimeClient范例程序(「03 TimeClient.exe」),在列表3-2中说明,用来测试TimeService服务。应用程序之原始码与资源档可在附赠光碟之03-TimeClient目录中找到。当您开始执行程序,会显示一个如图3-8所示的对话方块。
图3-8 在TimeClient范例应用程序之起始对话方块 |
为了察看客户端与服务器端之通讯,您必须在对话方块上方之编辑控制项中键入服务器的名称。如果您在同一个机器上执行客户端与服务器之处理程序,请键入如图3-8所示之句号并以其为伺服名称。当您按下Request Server's Time之按钮时,客户端应用程序会呼叫CreateFile,以决定要连接哪一个服务器与客户端,并使服务器醒来并且处理客户端的要求。如果伺服端并没有执行,则CreateFile会执行失败并显示一个如图3-9所示之讯息方块。
图3-9 当TimeService没有执行时,经由TimeClient所显示之讯息方块 |
如果服务已经执行,CreateFile会回传一个有效的handle;而客户端会等待时间资料经由一个非同步呼叫ReadFile之管道传送回来。在客户端取得资料后,客户端的管道handle会被关闭,由服务器传送过来的时间会被转换成客户端的本地时间,并且会更新初始对话方块的内容,其结果如图3-10所示。
图3-10 TimeClient范例应用程序中被更新的对块方块内容 |
TimeClient.cpp /******************************************************************** 模组:TimeClient.cpp 通告:Copyright (c)2000 Jeffrey Richter ********************************************************************/ #include "../CmnHdr.h" // 请参阅附录A #include#include "../ClassLib/EnsureCleanup.h" // 请参阅附录B #include "Resource.h" ////////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_TIMECLIENT); // 假设服务与客户端程序在同一台机器上执行 SetDlgItemText(hwnd, IDC_SERVER, TEXT(".")); return(TRUE); } ////////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDOK: // 建构管道的平台 TCHAR sz[500]; sz[0] = sz[1] = TEXT('//'); GetDlgItemText(hwnd, IDC_SERVER, &sz[2], chDIMOF(sz) - 2); lstrcat(sz, TEXT("//pipe//TimeService")); // 尝试与管道连结 // 取得一个handle以与管道沟通 CEnsureCloseFile hpipe = CreateFile(sz, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hpipe.IsValid()) { // 有效的handle,从管道中读取时间 SYSTEMTIME st; DWORD cbRead = 0; ReadFile(hpipe, &st, sizeof(st), &cbRead, NULL); // 转换UTC时间至客户端机器的本地时间并显示它 SystemTimeToTzSpecificLocalTime(NULL, &st, &st); GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, sz, chDIMOF(sz)); SetDlgItemText(hwnd, IDC_DATE, sz); GetTimeFormat(LOCALE_USER_DEFAULT, LOCALE_NOUSEROVERRIDE, &st, NULL, sz, chDIMOF(sz)); SetDlgItemText(hwnd, IDC_TIME, sz); } else { // 无效的handle,回执一个错误 SetDlgItemText(hwnd, IDC_DATE, TEXT("Error")); SetDlgItemText(hwnd, IDC_TIME, TEXT("Error")); // 取得错误的文字说明 HLOCAL hlocal = NULL; // 取得错误讯息字串之缓冲器 FormatMessageA( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, NULL, GetLastError(), MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), (PSTR) &hlocal, 0, NULL); if (hlocal != NULL) { chMB((PCSTR)LocalLock(hlocal)); LocalFree(hlocal); } } 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) { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_TIMECLIENT), NULL, Dlg_Proc); return(0); } //////////////////////////////// End of File /////////////////////////////////
列表3-2 TimeClient范例应用程序
|