編寫(xiě)有圖形界面的 Windows 服務(wù)程序
作者:feitian2007
環(huán)境:Windows 2003,VC 6.0
摘要:從建立一個(gè)COM服務(wù)程序入手,然后將一個(gè)MFC項目改造成服務(wù)程序,最后讓這一程序在啟動(dòng)時(shí)可以顯示圖形界面。
關(guān)鍵字:windows服務(wù)程序 COM服務(wù)程序 開(kāi)機前啟動(dòng) NT服務(wù) 與桌面交互
一、什么是windows的服務(wù)程序?
可以使用下面的幾種方法看到它。
我們會(huì )在打開(kāi)的頁(yè)面中看到一個(gè)大的列表,標題欄上包含有名稱(chēng)、描述、狀態(tài)、啟動(dòng)類(lèi)型、登錄身份等項。其中在狀態(tài)一欄中顯示為“已啟動(dòng)”的是系統中已經(jīng)啟動(dòng)了的服務(wù)。我們先看一下服務(wù)的屬性。舉個(gè)例子,找到Print Spooler這一名稱(chēng),然后用右鍵在上面點(diǎn)擊,選擇“屬性”,可以看到它所執行的命令行是C:\WINDOWS\system32\spoolsv.exe,按下停止后,任務(wù)管理器中spoolsv.exe進(jìn)程退出。我們所見(jiàn)到的這個(gè)列表就是服務(wù)程序的集中地,每一項就是一個(gè)服務(wù)程序。
上面這些標為自啟動(dòng)的服務(wù)程序隨系統一起啟動(dòng)。它與一些修改注冊表:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
項,及類(lèi)似注冊表項的程序不同的是,即使用戶(hù)沒(méi)有登錄到系統中,它們也是會(huì )運行的,或者說(shuō)它們在系統登錄前運行。
二、怎么建立自己的服務(wù)程序?
每一個(gè)服務(wù)程序對應注冊表項HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services下的一個(gè)子項。因此我們可以通過(guò)增加注冊表項的方式增加服務(wù)程序。比如,我現在要增加一個(gè)test1服務(wù)程序,對應的可執行文件是c:\test1.exe。那么我要增加如下注冊表項:
如果test1這一程序只是一個(gè)普通的win32程序,那么這樣做了之后還是不行,服務(wù)程序有它自己的一些結構特點(diǎn)。那么怎么編寫(xiě)這些服務(wù)程序?
建立一個(gè)服務(wù)程序的最簡(jiǎn)單的方法是用VC中的ATL COM向導。主菜單中選擇新建,然后選Projects中的ATL COM AppWizard,輸入一個(gè)項目名,選擇了所在目錄后,點(diǎn)OK按鈕,在出現的對話(huà)框中選擇Service(EXE),點(diǎn)Finish即可。然后編譯生成test1.exe。
運行test1.exe /regserver可以注冊程序為服務(wù),test1.exe /unregserver是取消注冊。test1.exe運行時(shí)的參數是在:
Project->Settings->Debug->Program arguments中設置。
三、怎么在建立的服務(wù)程序中加入自己的代碼?
我們看一下剛才生成的test1項目的結構。
我們看到test1有一個(gè)類(lèi)CServiceModule和一些Globals的內容。Globals包括一個(gè)_tWinMain函數,也就是程序的入口,其中使用了FindOneOf這一與分析命令行有關(guān)的函數,還剩下一個(gè)全局變量_Module。
_tWinMain函數中,_Module初始化并設置m_bService為T(mén)RUE,在一些分析命令行和判斷是否為服務(wù)的代碼之后,使用_Module.Start()進(jìn)入主要的執行部分。CServiceModule::Start()中,結構體SERVICE_TABLE_ENTRY建立了服務(wù)名與相應處理函數的映射。在這里,如果m_bService為T(mén)RUE,則調用StartServiceCtrlDispatcher進(jìn)入一種類(lèi)似win32程序的消息處理的過(guò)程,用SERVICE_TABLE_ENTRY中的處理函數讓程序執行下去。如果m_bService不為T(mén)RUE,則直接執行Run()函數。
在SERVICE_TABLE_ENTRY中,我們看到服務(wù)處理函數為_(kāi)ServiceMain,繼續跟蹤下去,發(fā)現是ServiceMain函數。在ServiceMain中又調用RegisterServiceCtrlHandler為服務(wù)增加了一個(gè)_Handler函數。對服務(wù)程序來(lái)說(shuō),我們可以在前面打開(kāi)的服務(wù)列表中對它們進(jìn)行“啟動(dòng)”,“停止”,“暫停”,“恢復”等操作。這實(shí)際上是由_Handler來(lái)處理不同的信號。_Handler內部調用Handler,在Handler中,對傳入的dwOpcode參數作出處理。比如如果是SERVICE_CONTROL_STOP,也就是我們“停止”服務(wù)時(shí),將使用PostThreadMessage對主線(xiàn)程發(fā)出一個(gè)退出的信號?;氐絊erviceMain函數,在里面同樣是在調用Run()函數。也就是說(shuō)程序以服務(wù)身份和非服務(wù)身份運行時(shí),區別在于以服務(wù)身份運行時(shí)多了一個(gè)Handler函數,處理用戶(hù)對服務(wù)程序發(fā)出的一些信號。
需要注意的是,這個(gè)程序注冊為服務(wù)時(shí)并不是直接寫(xiě)注冊表,而是在Install中使用了OpenSCManager,CreateService等函數來(lái)完成的任務(wù)。顯然,這比直接寫(xiě)注冊表要好一些,因為有時(shí)候我們并不太清楚要怎么去修改注冊表項的值來(lái)適應不同的服務(wù)程序配置,而這些函數有參數可以做到。
說(shuō)到這里,就涉及到我們自己編寫(xiě)的代碼了。
比如現在我們已經(jīng)建立了一個(gè)MFC的程序,想讓它成為一個(gè)服務(wù)程序,那要怎么做呢?
我現在建立一個(gè)MFC EXE的項目mfc1,基于對話(huà)框。那么把它變?yōu)橐粋€(gè)服務(wù)程序的最簡(jiǎn)單的方法就是把CServiceModule給拿過(guò)來(lái)使用。因為我們已經(jīng)看到CServiceModule類(lèi)已經(jīng)把安裝服務(wù),卸載服務(wù),運行服務(wù)這些操作封裝得很好。
打開(kāi)test1的stdafx.h文件,復制CServiceModule的聲明及相關(guān)頭文件和變量到mfc1的stdafx.h中。
然后是把test1的test1.cpp中對CServiceModule類(lèi)的實(shí)現,復制到mfc1中的mfc1.cpp中。
在stdafx.h中CServiceModule類(lèi)聲明前加上#include <winsvc.h>,它里面是對結構體SERVICE_STATUS_HANDLE的聲明。
編譯后出現以下類(lèi)似錯誤:
D:\vc6_test\mfc1\mfc1.cpp(52) : error C2065: ''IDR_Test1'' : undeclared identifierD:\vc6_test\mfc1\mfc1.cpp(336) : error C2065: ''CoInitializeSecurity'' : undeclared identifierD:\vc6_test\mfc1\mfc1.cpp(337) : error C2065: ''EOAC_NONE'' : undeclared identifierD:\vc6_test\mfc1\mfc1.cpp(362) : error C2065: ''IDS_SERVICENAME'' : undeclared identifierD:\vc6_test\mfc1\mfc1.cpp(362) : error C2065: ''LIBID_TEST1Lib'' : undeclared identifier
我們可以在test1中找到IDR_Test1的聲明,放到mfc1中,解決第一條錯誤。但我們也可以去掉CServiceModule中與COM有關(guān)的一些代碼。這里我們刪除RegisterServer,UnregisterServer兩個(gè)函數,并讓Run函數成為
void CServiceModule::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”。現在編譯程序,應該沒(méi)有錯誤了,但加入的CServiceModule還沒(méi)有起到作用。
在mfc1中的IDD_MFC1_DIALOG上加入兩個(gè)按鈕,分別是“安裝服務(wù)”,“卸載服務(wù)”。增加的單擊事件代碼為:
“安裝服務(wù)”按鈕:void CMfc1Dlg::OnButton1() { _Module.Install(); }
“卸載服務(wù)”按鈕:void CMfc1Dlg::OnButton2() { _Module.Uninstall(); }
下面在CMfc1App::InitInstance()中加入一些代碼:
_Module.Init(ObjectMap, this->m_hInstance, IDS_SERVICENAME, NULL);_Module.m_bService = TRUE;_Module.Start();
地點(diǎn)是在原來(lái)產(chǎn)生對話(huà)框的代碼的地方。而原有的生成對話(huà)框的代碼轉移到Run()中,位置是在使用了SetServiceStatus函數設置服務(wù)狀態(tài)之后,并注釋掉其后的消息處理代碼,因對話(huà)框自身有消息處理機制。
編譯時(shí)若出現如下錯誤,將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)
現在可以編譯運行了。然后點(diǎn)擊“安裝服務(wù)”,就可以在服務(wù)列表中看到mfc1了。
四、這一服務(wù)程序運行時(shí)沒(méi)有圖形界面?
不錯,剛才直接運行mfc1.exe時(shí)我們看到了圖形界面,但在服務(wù)列表中用右鍵菜單中的“啟動(dòng)”時(shí)卻看不到任何界面。這該怎么辦?
我們還需要在使用CreateService函數時(shí)(Install()中),加上一個(gè)參數,這樣才能允許程序與桌面交互,也就是可以顯示界面。這個(gè)參數是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,卸載服務(wù)后,安裝服務(wù)。我們可以看到,通過(guò)服務(wù)列表啟動(dòng)mfc1,原有的對話(huà)框出現了。聯(lián)系客服