编写有图形界面的 Windows 服务程序
作者:feitian2007
环境:Windows 2003,VC 6.0
摘要:从建立一个COM服务程序入手,然后将一个MFC项目改造成服务程序,最后让这一程序在启动时可以显示图形界面。
关键字:windows服务程序 COM服务程序 开机前启动 NT服务 与桌面交互
一、什么是windows的服务程序?
可以使用下面的几种方法看到它。
- 打开控制面板,然后是管理工具,里面有一个“服务”,双击后打开;
- 或者是通过输入命令的方式,打开开始菜单,点击运行,输入mmc services.msc(mmc可省略),也可打开;
我们会在打开的页面中看到一个大的列表,标题栏上包含有名称、描述、状态、启动类型、登录身份等项。其中在状态一栏中显示为“已启动”的是系统中已经启动了的服务。我们先看一下服务的属性。举个例子,找到Print Spooler这一名称,然后用右键在上面点击,选择“属性”,可以看到它所执行的命令行是C:/WINDOWS/system32/spoolsv.exe,按下停止后,任务管理器中spoolsv.exe进程退出。我们所见到的这个列表就是服务程序的集中地,每一项就是一个服务程序。
上面这些标为自启动的服务程序随系统一起启动。它与一些修改注册表:
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/CurrentVersion/Run
项,及类似注册表项的程序不同的是,即使用户没有登录到系统中,它们也是会运行的,或者说它们在系统登录前运行。
二、怎么建立自己的服务程序?
每一个服务程序对应注册表项HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services下的一个子项。因此我们可以通过增加注册表项的方式增加服务程序。比如,我现在要增加一个test1服务程序,对应的可执行文件是c:/test1.exe。那么我要增加如下注册表项:
- [HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/]下增加子项test1;
- test1下增加:
字符串型:"Description"="测试服务1"
字符串型:"DisplayName"="test1-displayname"
DWORD型:"ErrorControl"=dword:00000001
可扩充字符串值(即文件所在路径):
"ImagePath"=hex(2):43,00,3a,00,5c,00,74,00,65,00,73,00,74,00,31,00,2e,00,65,00,/
78,00,65,00,00,00
字符串型:"ObjectName"="LocalSystem"
DWORD型,值为3表示是手动:"Start"=dword:00000003
DWORD型:"Type"=dword:00000020 - test1下增加子项:
[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/test1/Security]
里面的键值从其他的服务程序注册表值中复制。
如果test1这一程序只是一个普通的win32程序,那么这样做了之后还是不行,服务程序有它自己的一些结构特点。那么怎么编写这些服务程序?
建立一个服务程序的最简单的方法是用VC中的ATL COM向导。主菜单中选择新建,然后选Projects中的ATL COM AppWizard,输入一个项目名,选择了所在目录后,点OK按钮,在出现的对话框中选择Service(EXE),点Finish即可。然后编译生成test1.exe。
运行test1.exe /regserver可以注册程序为服务,test1.exe /unregserver是取消注册。test1.exe运行时的参数是在:
Project->Settings->Debug->Program arguments中设置。
三、怎么在建立的服务程序中加入自己的代码?
我们看一下刚才生成的test1项目的结构。
我们看到test1有一个类CServiceModule和一些Globals的内容。Globals包括一个_tWinMain函数,也就是程序的入口,其中使用了FindOneOf这一与分析命令行有关的函数,还剩下一个全局变量_Module。
_tWinMain函数中,_Module初始化并设置m_bService为TRUE,在一些分析命令行和判断是否为服务的代码之后,使用_Module.Start()进入主要的执行部分。CServiceModule::Start()中,结构体SERVICE_TABLE_ENTRY建立了服务名与相应处理函数的映射。在这里,如果m_bService为TRUE,则调用StartServiceCtrlDispatcher进入一种类似win32程序的消息处理的过程,用SERVICE_TABLE_ENTRY中的处理函数让程序执行下去。如果m_bService不为TRUE,则直接执行Run()函数。
在SERVICE_TABLE_ENTRY中,我们看到服务处理函数为_ServiceMain,继续跟踪下去,发现是ServiceMain函数。在ServiceMain中又调用RegisterServiceCtrlHandler为服务增加了一个_Handler函数。对服务程序来说,我们可以在前面打开的服务列表中对它们进行“启动”,“停止”,“暂停”,“恢复”等操作。这实际上是由_Handler来处理不同的信号。_Handler内部调用Handler,在Handler中,对传入的dwOpcode参数作出处理。比如如果是SERVICE_CONTROL_STOP,也就是我们“停止”服务时,将使用PostThreadMessage对主线程发出一个退出的信号。回到ServiceMain函数,在里面同样是在调用Run()函数。也就是说程序以服务身份和非服务身份运行时,区别在于以服务身份运行时多了一个Handler函数,处理用户对服务程序发出的一些信号。
需要注意的是,这个程序注册为服务时并不是直接写注册表,而是在Install中使用了OpenSCManager,CreateService等函数来完成的任务。显然,这比直接写注册表要好一些,因为有时候我们并不太清楚要怎么去修改注册表项的值来适应不同的服务程序配置,而这些函数有参数可以做到。
说到这里,就涉及到我们自己编写的代码了。
比如现在我们已经建立了一个MFC的程序,想让它成为一个服务程序,那要怎么做呢?
我现在建立一个MFC EXE的项目mfc1,基于对话框。那么把它变为一个服务程序的最简单的方法就是把CServiceModule给拿过来使用。因为我们已经看到CServiceModule类已经把安装服务,卸载服务,运行服务这些操作封装得很好。
打开test1的stdafx.h文件,复制CServiceModule的声明及相关头文件和变量到mfc1的stdafx.h中。 注意头文件的顺序。
然后是把test1的test1.cpp中对CServiceModule类的实现,复制到mfc1中的mfc1.cpp中。靠近头文件放
在stdafx.h中CServiceModule类声明前加上#include <winsvc.h>,它里面是对结构体SERVICE_STATUS_HANDLE的声明。
编译后出现以下类似错误:
D:/vc6_test/mfc1/mfc1.cpp(52) : error C2065: ''IDR_Test1'' : undeclared identifier
D:/vc6_test/mfc1/mfc1.cpp(336) : error C2065: ''CoInitializeSecurity'' : undeclared identifier
D:/vc6_test/mfc1/mfc1.cpp(337) : error C2065: ''EOAC_NONE'' : undeclared identifier
D:/vc6_test/mfc1/mfc1.cpp(362) : error C2065: ''IDS_SERVICENAME'' : undeclared identifier
D:/vc6_test/mfc1/mfc1.cpp(362) : error C2065: ''LIBID_TEST1Lib'' : undeclared identifier
我们可以在test1中找到IDR_Test1的声明,放到mfc1中,解决第一条错误。但我们也可以去掉CServiceModule中与COM有关的一些代码。这里我们删除RegisterServer,UnregisterServer两个函数,并让Run函数成为
{
_Module.dwThreadID = GetCurrentThreadId();
LogEvent(_T("Service started"));
if (m_bService)
SetServiceStatus(SERVICE_RUNNING);
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
}
增加资源IDS_SERVICENAME为“mfc1”。、点资源添加字符串资源
注释掉CServiceModule::Init中“CComModule::Init(p, h, plibid);”一行。
注释_tWinMain函数(技巧:用#if 0和#endif注释)。找到_tWinMain 如下注释
#if 0
_tWinMain
#enfif
现在编译程序,应该没有错误了,但加入的CServiceModule还没有起到作用。
在mfc1中的IDD_MFC1_DIALOG上加入两个按钮,分别是“安装服务”,“卸载服务”。增加的单击事件代码为:
“安装服务”按钮:void CMfc1Dlg::OnButton1() { _Module.Install(); }
“卸载服务”按钮:void CMfc1Dlg::OnButton2() { _Module.Uninstall(); }
下面在CMfc1App::InitInstance()中加入一些代码:在函数里面靠下放
_Module.Init(ObjectMap, this->m_hInstance, IDS_SERVICENAME, NULL);
_Module.m_bService = TRUE;
_Module.Start();
地点是在原来产生对话框的代码的地方。而原有的生成对话框的代码转移到Run()中,位置是在使用了SetServiceStatus函数设置服务状态之后,并注释掉其后的消息处理代码,因对话框自身有消息处理机制。
编译时若出现如下错误,将Install()和Uninstall()前的inline参数去掉即可:
mfc1Dlg.obj : error LNK2001: unresolved external symbol "public: int __thiscall CServiceModule::Install(void)" (?Install@CServiceModule@@QAEHXZ)
mfc1Dlg.obj : error LNK2001: unresolved external symbol "public: int __thiscall CServiceModule::Uninstall(void)" (?Uninstall@CServiceModule@@QAEHXZ)
现在可以编译运行了。然后点击“安装服务”,就可以在服务列表中看到mfc1了。
四、这一服务程序运行时没有图形界面?
不错,刚才直接运行mfc1.exe时我们看到了图形界面,但在服务列表中用右键菜单中的“启动”时却看不到任何界面。这该怎么办?
我们还需要在使用CreateService函数时(Install()中),加上一个参数,这样才能允许程序与桌面交互,也就是可以显示界面。这个参数是SERVICE_INTERACTIVE_PROCESS。
填加后的CreateService:
SC_HANDLE hService = ::CreateService(
hSCM, m_szServiceName, m_szServiceName,
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
szFilePath, NULL, NULL, _T("RPCSS/0"), NULL, NULL);
再次编译mfc1,卸载服务后,安装服务。我们可以看到,通过服务列表启动mfc1,原有的对话框出现了。如需将服务设为自动启动,则将 SERVICE_DEMAND_START 改为 SERVICE_AUTO_START。