1. 程式人生 > >玩轉Windows服務系列——Windows服務小技巧

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

伴隨著研究Windows服務,逐漸掌握了一些小技巧,現在與大家分享一下。

將Windows服務轉變為控制檯程式

由於預設的Windows服務程式,編譯後為Win32的視窗程式。我們在程式啟動或執行過程中,如果想看到一些除錯資訊,那麼就只能通過DebugView或者輸出到日誌的方式了。因為如果我們通過printf或者std::cout輸出除錯資訊的話,Win32視窗程式是無法顯示的。

此時,我們是多麼懷念我們的經典的控制檯程式啊,它可以很方便的將我們的除錯資訊輸出出來,簡直是太方便了。既然如此,那我們就讓它一秒鐘變格格吧,額,應該是一秒鐘變控制檯。

下面分享一下我的實現程式碼

#ifdef _DEBUG
//Debug版本,直接輸出到控制檯 #define OUT(s) printf_s(s); #define OUT_LN(s) printf_s(s##"\r\n"); #else //非Debug版本,則輸出到偵錯程式,一般使用DebugView #define OUT(s) OutputDebugString(s); #define OUT_LN(s) OutputDebugString(s); #endif 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() { return S_OK; } //服務啟動 HRESULT Load(); //服務停止 HRESULT UnLoad(); HRESULT Run(_In_
int nShowCmd = SW_HIDE) throw() { HRESULT hr = S_OK; OUT_LN("準備啟動服務"); hr = Load(); if(hr) { OUT_LN("啟動服務失敗"); return hr; } OUT_LN("Services服務已啟動"); hr = ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >::Run(nShowCmd); hr = UnLoad(); OUT_LN("Services服務正常退出"); return hr; } }; CServicesModule _AtlModule; // #ifndef _DEBUG //非Debug版本,編譯為Win32程式 extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpCmdLine*/, int nShowCmd) { return _AtlModule.WinMain(nShowCmd); } #else //Debug版本,編譯為控制檯程式 int _tmain(int argc, _TCHAR* argv[]) { return _AtlModule.WinMain(SW_SHOW); } #endif HRESULT CServicesModule::Load() { OUT_LN("服務正在啟動"); return 0; } HRESULT CServicesModule::UnLoad() { OUT_LN("服務正在停止"); return 0; }

通過_DEBUG巨集來區分是否編譯成控制檯程式。

當指定編譯Debug版本時,可以將程式編譯為控制檯程式,通過RegServer註冊服務,然後直接執行服務exe程式,這樣通過printf輸出的資訊,就可以在控制檯上顯示了,如下圖。

當指定編譯Release版本時,將程式編譯為Win32程式,通過Service註冊服務,通過服務管理器管理服務的執行和停止。

Windows服務控制檯輸出資訊

當然,這還不是全部,還有一個地方需要設定,下面分別是Debug和Release下的設定

Windows服務Debug配置

Windows服務Release配置

當然,還有一種更簡單的方法,可以將Debug和Release模式下的“子系統”項修改為“未設定”。這樣編譯器在編譯連結時,會根據程式碼中的入口函式,自動將程式碼連結為對應的程式。如圖

Windows服務子系統未設定

註冊服務為自動啟動服務

註冊服務的流程已經在前面的部落格玩轉Windows服務系列——Debug、Release版本的註冊和解除安裝,及其原理中有介紹,這裡就不再多說。重點說一下建立服務的Windows API CreateService。

如下是MSDN中的API宣告

SC_HANDLE WINAPI CreateService(
  _In_       SC_HANDLE hSCManager,
  _In_       LPCTSTR lpServiceName,
  _In_opt_   LPCTSTR lpDisplayName,
  _In_       DWORD dwDesiredAccess,
  _In_       DWORD dwServiceType,
  _In_       DWORD dwStartType,
  _In_       DWORD dwErrorControl,
  _In_opt_   LPCTSTR lpBinaryPathName,
  _In_opt_   LPCTSTR lpLoadOrderGroup,
  _Out_opt_  LPDWORD lpdwTagId,
  _In_opt_   LPCTSTR lpDependencies,
  _In_opt_   LPCTSTR lpServiceStartName,
  _In_opt_   LPCTSTR lpPassword
);

引數太多,不一一介紹,詳細介紹可以檢視MSDN。

其中第六個引數,代表啟動方式

dwStartType [in] 
The service start options. This parameter can be one of the following values.

Value Meaning 
SERVICE_AUTO_START 
0x00000002 A service started automatically by the service control manager during system startup. For more information, see Automatically Starting Services.
 
SERVICE_BOOT_START 
0x00000000 A device driver started by the system loader. This value is valid only for driver services.
 
SERVICE_DEMAND_START 
0x00000003 A service started by the service control manager when a process calls the StartService function. For more information, see Starting Services on Demand.
 
SERVICE_DISABLED 
0x00000004 A service that cannot be started. Attempts to start the service result in the error code ERROR_SERVICE_DISABLED.
 
SERVICE_SYSTEM_START 
0x00000001 A device driver started by the IoInitSystem function. This value is valid only for driver services.

SERVICE_AUTO_START 表示自動啟動,這個引數就是我們想要的。

SERVICE_BOOT_START 也屬於自動啟動,但是隻能用於核心服務。

SERVICE_DEMAND_START 手動啟動,這是目前服務的預設啟動方式。

SERVICE_DISABLED 禁止啟動。

SERVICE_SYSTEM_START 屬於自動啟動,但只能用於核心服務。

所以,我們只需要在呼叫CreateService方法時,設定dwStartType引數為SERVICE_AUTO_START即可實現服務自動啟動,而CreateService的其他引數,則參考現在的呼叫引數

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

接下來,我們只需要過載命令列解析方法,新增引數用於確定是否自動啟動即可。

註冊服務時設定服務的依賴關係

設定服務的依賴關係仍然要看CreateService服務,這次我們看倒數第三個引數lpDependencies

lpDependencies [in, optional] 
A pointer to a double null-terminated array of null-separated names of services or load ordering groups that the system must start before this service. Specify NULL or an empty string if the service has no dependencies. Dependency on a group means that this service can run if at least one member of the group is running after an attempt to start all members of the group.

You must prefix group names with SC_GROUP_IDENTIFIER so that they can be distinguished from a service name, because services and service groups share the same name space.

lpDependencies是一個指標,指標指向了一個以’\0\0’結尾,並且以’\0’分割開的字串,由’\0’分割開的字串為依賴服務的名字。

比如,如果設定當前服務依賴RPCSS 和DependTest服務的,則可以這樣呼叫CreateService方法

::CreateService(
            hSCM, m_szServiceName, m_szServiceName,
            SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
            SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
            szFilePath, NULL, NULL, _T("RPCSS\0DependTest\0"), NULL, NULL);

由於字串自身會以’\0’結尾,所以字串內容中的結尾處只需寫一個’\0’。

接下來,我們只需要過載命令列解析方法,新增引數用於確定是否自動啟動即可

新增自定義命令列引數

新增自定義命令列引數 Auto, 用來設定啟動方式為自動啟動, 並且給Service引數新增依賴項,實現程式碼如下

// Services.cpp : WinMain 的實現


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


using namespace ATL;

#include <stdio.h>

#ifdef _DEBUG
#define OUT(s) printf_s(s); 
#define OUT_LN(s) printf_s(s##"\r\n"); 
#else
#define OUT(s) OutputDebugString(s);
#define OUT_LN(s) OutputDebugString(s);
#endif

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;
    }
    //服務啟動
    HRESULT Load();
    //服務停止
    HRESULT UnLoad();

    // Parses the command line and registers/unregisters the rgs file if necessary
    bool ParseCommandLine(
        _In_z_ LPCTSTR lpCmdLine,
        _Out_ HRESULT* pnRetCode) throw();
    //註冊服務
    BOOL Install() throw();

    //重寫此方法,主要是為了呼叫重寫的Install方法
    inline HRESULT RegisterAppId(_In_ bool bService = false) throw()
    {
        if (!Uninstall())
            return E_FAIL;

        CServicesModule::UpdateRegistryAppId(TRUE);

        CRegKey keyAppID;
        keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_WRITE);

        CRegKey key;

        key.Create(keyAppID, CServicesModule::GetAppIdT());

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

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

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

    HRESULT Run(_In_ int nShowCmd = SW_HIDE) throw()
    {
        HRESULT hr = S_OK;
        OUT_LN("準備啟動服務");
        hr = Load();
        if(hr)
        {
            OUT_LN("啟動服務失敗");
            return hr;
        }
        OUT_LN("Services服務已啟動");

        hr = ATL::CAtlServiceModuleT< CServicesModule, IDS_SERVICENAME >::Run(nShowCmd);

        hr = UnLoad();
        OUT_LN("Services服務正常退出");
        return hr;
    }

private:
    _TCHAR dependServices[256];
    DWORD size;
    DWORD dwStartType;
};

CServicesModule _AtlModule;

//
#ifndef _DEBUG
extern "C" int WINAPI _tWinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, 
                                LPTSTR /*lpCmdLine*/, int nShowCmd)
{
    return _AtlModule.WinMain(nShowCmd);
}
#else
int _tmain(int argc, _TCHAR* argv[])
{
    return _AtlModule.WinMain(SW_SHOW);
}
#endif

HRESULT CServicesModule::Load()
{
    memset(dependServices, 0, sizeof(dependServices));
    dwStartType = SERVICE_DEMAND_START;

    OUT_LN("服務正在啟動");
    return 0;
}

HRESULT CServicesModule::UnLoad()
{
    OUT_LN("服務正在停止");
    return 0;
}

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

    TCHAR szTokens[] = _T("-/");
    *pnRetCode = S_OK;
    LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
    while (lpszToken != NULL)
    {
        if (WordCmpI(lpszToken, _T("Service"))==0)
        {
            lpszToken += _tcslen(_T("Service"));
            //迴圈讀取依賴項
            while (true)
            {
                LPCTSTR lpszTokenBegin = lpszToken;
                while(isprint(*lpszToken) && *lpszToken != _T(' ') && *lpszToken != _T('-') && *lpszToken != _T('/'))
                {
                    lpszToken++;
                }
                DWORD tokenSize = (lpszToken - lpszTokenBegin);
                memcpy_s(dependServices + size, sizeof(dependServices) - size, lpszTokenBegin, tokenSize * sizeof(_TCHAR));
                size += tokenSize;
                dependServices[size] = _T('\0');
                if(tokenSize)
                {
                    size++;
                }

                while(isprint(*lpszToken) && *lpszToken == ' ')
                {
                    lpszToken++;
                }
                if((*lpszToken == _T('\0') || *lpszToken == _T('-') || *lpszToken == _T('/')))
                {
                    dependServices[size + 1] = _T('\0');
                    break;
                }
            }
            *pnRetCode = this->RegisterAppId(true);
            if (SUCCEEDED(*pnRetCode))
                *pnRetCode = this->RegisterServer(TRUE);
            return false;
        }

        //設定啟動型別
        if (WordCmpI(lpszToken, _T("Auto"))==0)
        {
            dwStartType = SERVICE_AUTO_START;
        }
        lpszToken = FindOneOf(lpszToken, szTokens);
    }
    return true;
}

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

    // Get the executable file path
    TCHAR szFilePath[MAX_PATH + _ATL_QUOTES_SPACE];
    DWORD dwFLen = ::GetModuleFileName(NULL, szFilePath + 1, MAX_PATH);
    if( dwFLen == 0 || dwFLen == MAX_PATH )
        return FALSE;

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

    SC_HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

    //建立服務、根據命令列設定啟動方式,設定依賴關係
    SC_HANDLE hService = ::CreateService(
        hSCM, m_szServiceName, m_szServiceName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
        dwStartType, SERVICE_ERROR_NORMAL,
        szFilePath, NULL, NULL, dependServices, NULL, NULL);

    if (hService == NULL)
    {
        ::CloseServiceHandle(hSCM);
        return FALSE;
    }

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

自定義命令列引數演示

註冊服務時使用如下命令列

Services.exe -Auto -service CryptSvc RPCSS DcomLaunch

註冊後,效果如下

Services服務自動啟動

Services服務依賴關係

系列連結