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服务项目
其中主代码文件为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();
return -;
} 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"))==)
{
*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"))==)
{
*pnRetCode = pT->UnregisterServer(TRUE);
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->UnregisterAppId();
return false;
} if (WordCmpI(lpszToken, _T("RegServer"))==)
{
*pnRetCode = pT->RegisterAppId();
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->RegisterServer(TRUE);
return false;
} if (WordCmpI(lpszToken, _T("UnregServerPerUser"))==)
{
*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"))==)
{
*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"))==)
{
*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方法的大致流程为
由于调用方法时传入的参数是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 + , MAX_PATH); // Quote the FilePath before calling CreateService
szFilePath[] = _T('\"');
szFilePath[dwFLen + ] = _T('\"');
szFilePath[dwFLen + ] = ; ::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安装服务,这样就可以通过控制面板的服务管理器查看和管理此服务了。
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;
}
流程图如下
程序执行完毕后,服务管理器中就看不到此服务了,这样此服务就被卸载掉了。
新的问题
之前的问题消除了,但是新的问题又产生了。
既然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) == )
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服务系列——Debug、Release版本的注册和卸载,及其原理
玩转Windows服务系列——无COM接口Windows服务启动失败原因及解决方案
玩转Windows服务系列——Windows服务启动超时时间