1. 程式人生 > >玩轉Windows服務系列——Debug、Release版本的註冊和解除安裝,及其原理

玩轉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

系列連結