• 玩转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"), 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接口

  • 相关阅读:
    在java中写出完美的单例模式
    Zookeeper的功能以及工作原理
    java面试题之int和Integer的区别
    ActiveMQ面试专题
    并发队列ConcurrentLinkedQueue、阻塞队列AraayBlockingQueue、阻塞队列LinkedBlockingQueue 区别和使用场景总结
    Python requests 指定网卡ip发出请求
    Python 打包发布exe可执行文件
    Power BI 图标设置
    python git 基础操作
    C#通过SFTP协议操作文件
  • 原文地址:https://www.cnblogs.com/hbccdf/p/3486565.html
Copyright © 2020-2023  润新知