玩转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服务项目

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理_第1张图片

其中主代码文件为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方法的大致流程为

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理_第2张图片

由于调用方法时传入的参数是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安装服务,这样就可以通过控制面板的服务管理器查看和管理此服务了。

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理_第3张图片

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;
}

流程图如下

玩转Windows服务系列——Debug、Release版本的注册和卸载,及其原理_第4张图片

程序执行完毕后,服务管理器中就看不到此服务了,这样此服务就被卸载掉了。

 

新的问题

之前的问题消除了,但是新的问题又产生了。

既然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)