玩轉Windows服務系列——Debug、Release版本的注冊和卸載,及其原理


Windows服務Debug版本

注冊

Services.exe -regserver

卸載

Services.exe -unregserver

Windows服務Release版本

注冊

Services.exe -service

卸載

Services.exe -unregserver

原理

Windows服務的Debug、Release版本的注冊和卸載方式均已明確。但是為什么要這么做呢。

最初我在第一次編寫Windows服務的程序時,並不清楚Windows服務的注冊方式。於是從谷歌搜索后得知,原來是這樣注冊的。

當按照谷歌提供的注冊方式注冊后,我就在想,這些注冊方式是不是Windows操作系統所支持的。后來一想不對,這明明是通過執行編寫的Windows服務程序+命令行參數的方式。

既然是命令行的方式,那么就是說編寫的Services程序,是支持 –regserver、-service 這些命令行參數的。

通過VS模板生成Windows服務項目后,並未寫一句代碼,那么它是如何支持這些命令行的呢,我決定一探究竟。

模板生成后的Windows服務項目概覽

VS2012下生成的Windows服務項目

VS2012生成的Windows服務項目

其中主代碼文件為Services.cpp,“生成的文件”文件夾中的文件為COM模型編譯時生成的文件。

由此圖可見,程序的命令行解析應該就在Services.cpp文件中。

下面是Services.cpp文件的代碼

// Services.cpp : WinMain 的實現


#include "stdafx.h"
#include "resource.h"
#include "Services_i.h"


using namespace ATL;

#include <stdio.h>

class CServicesModule : public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >
{
public :
    DECLARE_LIBID(LIBID_ServicesLib)
    DECLARE_REGISTRY_APPID_RESOURCEID(IDR_SERVICES, "{0794CF96-5CC5-432E-8C1D-52B980ACBE0F}")
        HRESULT InitializeSecurity() throw()
    {
        // TODO : 調用 CoInitializeSecurity 並為服務提供適當的安全設置
        // 建議 - PKT 級別的身份驗證、
        // RPC_C_IMP_LEVEL_IDENTIFY 的模擬級別
        // 以及適當的非 NULL 安全描述符。

        return S_OK;
    }
    };

CServicesModule _AtlModule;



//
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, 
                                LPTSTR /*lpCmdLine*/, int nShowCmd)
{
    return _AtlModule.WinMain(nShowCmd);
}

只有40行左右的代碼,那么命令行解析在哪里,針對不同的命令,又是做了什么操作?至少在這里我是得不到答案了。

既然程序能正確執行,那么我只要從程序的入口點跟蹤就行了。

Windows程序的四個入口函數是

WinMain        //Win32程序
wWinMain    //Unicode版本Win32程序
Main        //控制台程序
Wmain        //Unicode版本控制台程序

編譯后生成的Servers.exe明顯不是控制台程序,再結合代碼來看,那么服務程序的入口點就定位到了這里

extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, 
                                LPTSTR /*lpCmdLine*/, int nShowCmd)
{
    return _AtlModule.WinMain(nShowCmd);
}

_tWinMain函數中直接調用了 _AtlModule.WinMain方法。

那么_AtlModule又是什么呢?

於是我看到了

class CServicesModule : public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >

CServicesModule _AtlModule;

_AtlModule是CServicesModule類的一個實例,而CServicesModule類中沒有實現WinMain方法,實際上就是調用的父類public ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >的WinMain方法。

CAtlServiceModuleT類詳解

下面來看一下CAtlServiceModuleT的WinMain方法

int WinMain(_In_ int nShowCmd) throw()
{
    if (CAtlBaseModule::m_bInitFailed)
    {
        ATLASSERT(0);
        return -1;
    }

    T* pT = static_cast<T*>(this);
    HRESULT hr = S_OK;

    LPTSTR lpCmdLine = GetCommandLine();
    if (pT->ParseCommandLine(lpCmdLine, &hr) == true)
        hr = pT->Start(nShowCmd);

    return hr;
}

可以看到方法中通過調用GetCommandLine方法取得當前程序的命令行,然后通過調用ParseCommandLine方法進行命令行的解析。

// Parses the command line and registers/unregisters the rgs file if necessary
bool ParseCommandLine(
    _In_z_ LPCTSTR lpCmdLine,
    _Out_ HRESULT* pnRetCode) throw()
{
    if (!CAtlExeModuleT<T>::ParseCommandLine(lpCmdLine, pnRetCode))
        return false;

    TCHAR szTokens[] = _T("-/");
    *pnRetCode = S_OK;

    T* pT = static_cast<T*>(this);
    LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
    while (lpszToken != NULL)
    {
        if (WordCmpI(lpszToken, _T("Service"))==0)
        {
            *pnRetCode = pT->RegisterAppId(true);
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = pT->RegisterServer(TRUE);
            return false;
        }
        lpszToken = FindOneOf(lpszToken, szTokens);
    }
    return true;
}

從代碼中可以看出首先調用父類CAtlExeModuleT的ParseCommandLine方法,那么CAtlExeModule中又做了些神馬呢。

bool ParseCommandLine(
    _In_z_ LPCTSTR lpCmdLine,
    _Out_ HRESULT* pnRetCode) throw()
{
    *pnRetCode = S_OK;

    TCHAR szTokens[] = _T("-/");

    T* pT = static_cast<T*>(this);
    LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
    while (lpszToken != NULL)
    {
        if (WordCmpI(lpszToken, _T("UnregServer"))==0)
        {
            *pnRetCode = pT->UnregisterServer(TRUE);
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = pT->UnregisterAppId();
            return false;
        }

        if (WordCmpI(lpszToken, _T("RegServer"))==0)
        {
            *pnRetCode = pT->RegisterAppId();
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = pT->RegisterServer(TRUE);
            return false;
        }

        if (WordCmpI(lpszToken, _T("UnregServerPerUser"))==0)
        {
            *pnRetCode = AtlSetPerUserRegistration(true);
            if (FAILED(*pnRetCode))
            {
                return false;
            }

            *pnRetCode = pT->UnregisterServer(TRUE);
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = pT->UnregisterAppId();
            return false;
        }

        if (WordCmpI(lpszToken, _T("RegServerPerUser"))==0)
        {
            *pnRetCode = AtlSetPerUserRegistration(true);
            if (FAILED(*pnRetCode))
            {
                return false;
            }

            *pnRetCode = pT->RegisterAppId();
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = pT->RegisterServer(TRUE);
            return false;
        }

        lpszToken = FindOneOf(lpszToken, szTokens);
    }

    return true;
}

從代碼中可以找到,程序一共對四個參數進行了解析和執行,分別是UnregServer、RegServer、UnregServerPerUser、RegServerPerUser。由WordCmpI可知,參數是大小寫無關的。當執行某個參數后,會返回false,當參數不是這四個其中之一時,方法的返回值是true。

由之前看到的子類方法中

if (!CAtlExeModuleT<T>::ParseCommandLine(lpCmdLine, pnRetCode))
        return false;

所以當命令行參數為UnregServer、RegServer、UnregServerPerUser、RegServerPerUser其中之一時,子類CServiceModuleT中的ParseCommandLine方法便不再執行。那么當參數不是四個之一的時候,子類CServiceModuleT中的ParseCommandLine方法會執行這樣的操作

if (WordCmpI(lpszToken, _T("Service"))==0)
{
    *pnRetCode = pT->RegisterAppId(true);
    if (SUCCEEDED(*pnRetCode))
        *pnRetCode = pT->RegisterServer(TRUE);
    return false;
}

這里看到了Service參數。於是開篇中介紹的注冊和卸載所使用的參數regserver、unregserver、service就都找到了。至此明白了是底層的ATL框架中的CServiceModuleT為我們完成了注冊和卸載服務所必須的命令行參數的解析。

同時我又充滿了疑惑,為什么Debug、Release模式下注冊服務所用的參數不同,而卸載服務所用參數又相同了呢,不同模式下的命令參數又做了些什么操作呢。帶着這些問題,我又開始了探索。

RegServer參數

RegServer參數是Debug模式下用於注冊服務的參數,它做了哪些操作呢。

*pnRetCode = pT->RegisterAppId();
if (SUCCEEDED(*pnRetCode))
    *pnRetCode = pT->RegisterServer(TRUE);
return false;

根據前面的代碼,看到,傳入RegServer參數時,執行了兩個方法RegisterAppId、RegisterServer兩個方法,分別來看一下。

RegisterAppId
inline HRESULT RegisterAppId(_In_ bool bService = false) throw()
{
    if (!Uninstall())
        return E_FAIL;

    HRESULT hr = T::UpdateRegistryAppId(TRUE);
    if (FAILED(hr))
        return hr;

    CRegKey keyAppID;
    LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_WRITE);
    if (lRes != ERROR_SUCCESS)
        return AtlHresultFromWin32(lRes);

    CRegKey key;

    lRes = key.Create(keyAppID, T::GetAppIdT());
    if (lRes != ERROR_SUCCESS)
        return AtlHresultFromWin32(lRes);

    key.DeleteValue(_T("LocalService"));

    if (!bService)
        return S_OK;

    key.SetStringValue(_T("LocalService"), m_szServiceName);

    // Create service
    if (!Install())
        return E_FAIL;
    return S_OK;
}

RegisterAppId方法的大致流程為

RegisterId流程圖

由於調用方法時傳入的參數是false,即bService為false,所以跳過了安裝服務Install的部分。所以RegisterId主要的操作為創建注冊表信息,Uninstall與注冊表信息后面會詳述。

RegisterServer
// RegisterServer walks the ATL Autogenerated object map and registers each object in the map
// If pCLSID is not NULL then only the object referred to by pCLSID is registered (The default case)
// otherwise all the objects are registered
HRESULT RegisterServer(
    _In_ BOOL bRegTypeLib = FALSE,
    _In_opt_ const CLSID* pCLSID = NULL)
{
    return AtlComModuleRegisterServer(this, bRegTypeLib, pCLSID);
}

RegisterServer又會調用AtlComModuleRegisterServer方法,此方法主要是做一些和Com有關的操作,加之對Com的知識不是很清楚,所以就不在繼續跟蹤下去。

回到WinMain方法
if (pT->ParseCommandLine(lpCmdLine, &hr) == true)
    hr = pT->Start(nShowCmd);

return hr;

由前面跟蹤時可知,方法執行完RegServer參數的操作后,會返回false,所以此處WinMain方法並不會調用Start方法,至此WinMain方法執行解析,這就是通過命令行參數RegServer注冊服務的過程。

總結

通過命令行參數RegServer注冊服務的過程,主要的操作是卸載服務、創建注冊表信息。由於並沒有安裝服務,所以此時通過控制面板中的服務管理器是看不到這個服務的。

Service參數

下面是命令行Service參數時,程序執行的操作

*pnRetCode = pT->RegisterAppId(true);
if (SUCCEEDED(*pnRetCode))
    *pnRetCode = pT->RegisterServer(TRUE);
return false;

由代碼來看,程序執行的操作與RegServer參數並無差異,但仔細觀察可以看出,調用RegisterAppId方法時傳入的參數值是不一樣的。

RegServer參數時,傳入的值是false;而Service參數時,傳入的值是true。

根據前面的RegisterAppId方法的流程圖可知,當傳入的值為true時,會執行安裝服務Install的操作,其實這也就是RegServer參數與Service參數最主要的區別。

那么Install方法又做了些什么呢。

BOOL Install() throw()
{
    if (IsInstalled())
        return TRUE;

    // Get the executable file path
    TCHAR szFilePath[MAX_PATH + _ATL_QUOTES_SPACE];
    ::GetModuleFileName(NULL, szFilePath + 1, MAX_PATH);

    // Quote the FilePath before calling CreateService
    szFilePath[0] = _T('\"');
    szFilePath[dwFLen + 1] = _T('\"');
    szFilePath[dwFLen + 2] = 0;

    ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    ::CreateService(
        hSCM, m_szServiceName, m_szServiceName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
        SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
        szFilePath, NULL, NULL, _T("RPCSS\0"), NULL, NULL);

    ::CloseServiceHandle(hService);
    ::CloseServiceHandle(hSCM);
    return TRUE;
}

這段代碼是Install方法中去掉錯誤處理的代碼。由此可以看出,創建服務所需的三個API為 OpenSCManger、CreateService、CloseServiceHandle。對這三個方法不熟的可以查一下MSDN。

同樣,做完這些操作后,程序就會退出。

總結

通過命令行參數service注冊服務的過程,主要的操作是卸載服務、創建注冊表信息,通過OpenSCManger、CreateService等Windows API安裝服務,這樣就可以通過控制面板的服務管理器查看和管理此服務了。

Service參數注冊后_服務管理器查看

UnregServer參數

下面是命令行UnregServer參數時,程序執行的操作

*pnRetCode = pT->UnregisterServer(TRUE);
if (SUCCEEDED(*pnRetCode))
    *pnRetCode = pT->UnregisterAppId();
return false;

由注冊過程可以猜想,UnregisterServer方法主要是處理Com相關的東西,不再研究。而UnregisterAppId則應該是卸載服務、刪除注冊表信息等操作。下面來看一下。

HRESULT UnregisterAppId() throw()
{
    if (!Uninstall())
        return E_FAIL;
    // First remove entries not in the RGS file.
    CRegKey keyAppID;
    keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_WRITE);

    CRegKey key;
    key.Open(keyAppID, T::GetAppIdT(), KEY_WRITE);

    key.DeleteValue(_T("LocalService"));

    return T::UpdateRegistryAppId(FALSE);
}

上面仍然是去掉了錯誤處理的代碼。由此可以驗證剛才的猜想是對的,接下來繼續查看Uninstall方法,去掉錯誤處理后的代碼如下

BOOL Uninstall() throw()
{
    if (!IsInstalled())
        return TRUE;

    ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

    ::OpenService(hSCM, m_szServiceName, SERVICE_STOP | DELETE);

    SERVICE_STATUS status;
    ::ControlService(hService, SERVICE_CONTROL_STOP, &status);


    ::DeleteService(hService);
    ::CloseServiceHandle(hService);
    ::CloseServiceHandle(hSCM);

    return TRUE;
}

流程圖如下

Uninstall流程圖

程序執行完畢后,服務管理器中就看不到此服務了,這樣此服務就被卸載掉了。

 

新的問題

之前的問題消除了,但是新的問題又產生了。

既然Debug模式下通過RegServer參數注冊服務,實際上只是向注冊表中添加了一些信息,並沒有安裝服務,而且Debug版為了方便調試,運行的時候也是通過啟動exe的方式運行,那么為什么還要通過RegServer方式注冊服務呢,編譯后直接運行exe程序不行嗎?

那么接下來開始繼續研究。

通過VS新建一個服務后,編譯稱為exe,然后直接運行exe,由於此處的服務是無窗口的,所以要通過任務管理器查看exe是否在運行。發現任務管理器中並沒有此服務的進程。

回到WinMain函數

if (pT->ParseCommandLine(lpCmdLine, &hr) == true)
    hr = pT->Start(nShowCmd);

由於直接啟動exe時,ParseCommandLine會返回true,所以接下來會執行Start方法,下面是Start方法的代碼。

HRESULT Start(_In_ int nShowCmd) throw()
{
    T* pT = static_cast<T*>(this);
    // Are we Service or Local Server
    CRegKey keyAppID;
    LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_READ);
    if (lRes != ERROR_SUCCESS)
    {
        m_status.dwWin32ExitCode = lRes;
        return m_status.dwWin32ExitCode;
    }

    CRegKey key;
    lRes = key.Open(keyAppID, pT->GetAppIdT(), KEY_READ);
    if (lRes != ERROR_SUCCESS)
    {
        m_status.dwWin32ExitCode = lRes;
        return m_status.dwWin32ExitCode;
    }

    TCHAR szValue[MAX_PATH];
    DWORD dwLen = MAX_PATH;
    lRes = key.QueryStringValue(_T("LocalService"), szValue, &dwLen);

    m_bService = FALSE;
    if (lRes == ERROR_SUCCESS)
        m_bService = TRUE;

    if (m_bService)
    {
        SERVICE_TABLE_ENTRY st[] =
        {
            { m_szServiceName, _ServiceMain },
            { NULL, NULL }
        };
        if (::StartServiceCtrlDispatcher(st) == 0)
            m_status.dwWin32ExitCode = GetLastError();
        return m_status.dwWin32ExitCode;
    }
    // local server - call Run() directly, rather than
    // from ServiceMain()        
#ifndef _ATL_NO_COM_SUPPORT
    HRESULT hr = T::InitializeCom();
    if (FAILED(hr))
    {
        // Ignore RPC_E_CHANGED_MODE if CLR is loaded. Error is due to CLR initializing
        // COM and InitializeCOM trying to initialize COM with different flags.
        if (hr != RPC_E_CHANGED_MODE || GetModuleHandle(_T("Mscoree.dll")) == NULL)
        {
            return hr;
        }
    }
    else
    {
        m_bComInitialized = true;
    }
#endif //_ATL_NO_COM_SUPPORT

    m_status.dwWin32ExitCode = pT->Run(nShowCmd);
    return m_status.dwWin32ExitCode;
}

從代碼中可以看到,Start方法會首先讀取注冊服務時創建的注冊表信息,如果注冊表信息不存在,Start方法便會立即返回,然后WinMain方法執行結束,這樣程序就會結束、進程退出。

所以雖然Debug模式下的服務程序不需要使用服務管理器進行管理,但是如果不通過RegServer參數進行注冊的話,程序是無法正常運行的。

當然,也可以通過實現自己的Start方法,來避免Debug模式下必須注冊才能運行的問題。

全文總結

Debug版本的程序可以通過命令行參數RegServer來注冊服務,這樣方便調試。

Release版本的程序通過命令行參數Service來注冊服務,方便通過服務管理器進行管理。

相關的Windows API

//打開服務控制管理器句柄
OpenSCManager

//創建服務
CreateService

//打開服務句柄
OpenService

//控制服務的狀態
ControlService

//刪除服務
DeleteService

//關閉服務或者服務管理器的句柄
CloseServiceHandle

系列鏈接

玩轉Windows服務系列——創建Windows服務

玩轉Windows服務系列——Debug、Release版本的注冊和卸載,及其原理

玩轉Windows服務系列——無COM接口Windows服務啟動失敗原因及解決方案

玩轉Windows服務系列——服務運行、停止流程淺析

玩轉Windows服務系列——Windows服務小技巧

玩轉Windows服務系列——命令行管理Windows服務

玩轉Windows服務系列——Windows服務啟動超時時間

玩轉Windows服務系列——使用Boost.Application快速構建Windows服務

玩轉Windows服務系列——給Windows服務添加COM接口


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM