使用 INI 配置文件创建多个 Windows 服务






4.09/5 (5投票s)
2006 年 3 月 11 日
3分钟阅读

43944

494
多个 Windows 服务
引言
这个项目演示了如何将一个 Windows 服务配置为作为多个服务运行。我发现,使用 Visual Studio 向导创建的基本 ATL 服务项目只需要很少的代码就可以创建一个使用同一可执行文件创建和运行多个服务的项目。对于我的示例,我只创建 3 个服务。
我花了一个星期试图通过查看其他代码示例来找到解决我的问题的方法,但无济于事,每个示例都使用在 SERVICE_TABLE_ENTRY 表中硬编码的多个服务名称,这对我来说不是一个选项。我的示例使得使用相同可执行文件和一个 INI 文件(用于您要创建的每个服务)来创建和运行服务组件管理器可以处理的尽可能多的 Windows 服务变得非常简单。我甚至演示了每个服务都在运行其自己的 INI 文件,方法是向事件查看器日志写入正在运行的服务的名称及其 INI 文件。
首先,修改 _tWinMain 方法以获取第二个参数,用于安装和卸载 Windows 服务,该参数将是 INI 文件的完全限定路径。这是整个项目的骨干,用于通过从 INI 文件读取 SERVICENAME 设置并将其附加到项目名称(在本例中为“ThisIs”)来创建服务名称。INI 文件路径也存储在为服务创建的注册表中,并在 ServiceMain 中重新加载,当服务启动时。我相信有人会提出一个替代解决方案,即传递任何旧名称而不是 INI 文件路径作为第二个参数,这取决于您的环境,这很好。
我所有的代码更改都标记为“//DG”,并且在重新编译新版本之前,请确保卸载服务,否则它将保持在“正在执行注册”模式,直到您在任务管理器中杀死该进程。
第一步是在 ThisIs.cpp 文件的顶部包含 windows.h 头文件和两个全局变量,这些变量用于服务注册和创建服务名称。我们需要这样做,以便我们可以读取 INI 文件以及读取和写入注册表
#include "stdafx.h" #include "resource.h" #include#include "ThisIs.h" #include <windows.h> //DG #include "ThisIs_i.c" #include <stdio.h> CServiceModule _Module; char configfile[100]; //DG char sServiceName[30]; //DG BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP()
第二步,我们需要修改由 ATL 向导创建的默认 _tWinMain 以接受我们的第二个参数,这将是我们的 INI 配置文件的完整路径。
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine, int /*nShowCmd*/) { /* USAGE SECTION TO INSTALL SERVICEA thisis -install C:\ThisIs\ServiceA.ini (This will create a service called ThisIsServiceA" TO INSTALL SERVICEB thisis -install C:\ThisIs\ServiceB.ini (This will create a service called ThisIsServiceB" TO INSTALL SERVICEC thisis -install C:\ThisIs\ServiceC.ini (This will create a service called ThisIsServiceC" TO UNINSTALL SERVICEA thisis -uninstall C:\ThisIs\ServiceA.ini Please note that the SERVICENAME in the ini file is appended to the project name so when you open up the Services window in Administrative Tools you should see a service called "ThisIsServiceA" if you run the "thisis -install C:\ThisIs\ServiceA.ini" command from a DOS prompt. */ lpCmdLine = GetCommandLine(); //this line necessary for _ATL_MIN_CRT _Module.Init(ObjectMap, hInstance, IDS_SERVICENAME, &LIBID_THISISLib); _Module.m_bService = TRUE; TCHAR szTokens[] = _T("-/"); //lpCmdLine = "-install C:\\ThisIs\\ServiceA.ini"; LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens); while (lpszToken != NULL) { if (memcmp(lpszToken, "uninstall",9)==0) //DG { strcpy(configfile,lpszToken + 10); //DG if(strlen(configfile) <= 0) //DG { MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK); return 0; } _Module.LoadConfigSettings(); //DG strcat(_Module.m_szServiceName,sServiceName); //DG if (strlen(sServiceName) == 0) //DG { MessageBox(NULL, _T("Unable to read SERVICENAME key in config file."), "ThisIs", MB_OK); return 0; } return _Module.UnregisterServer(); } // Register as Local Server if (memcmp(lpszToken, "regserver",9)==0) //DG { strcpy(configfile,lpszToken + 10); //DG if(strlen(configfile) <= 0) //DG { MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK); return 0; } _Module.LoadConfigSettings(); //DG strcat(_Module.m_szServiceName,sServiceName); //DG if (strlen(sServiceName) == 0) //DG { MessageBox(NULL, _T("Unable to read SERVICENAME key in config file."), "ThisIs", MB_OK); return 0; } return _Module.RegisterServer(TRUE, FALSE); } // Register as Service if (memcmp(lpszToken, "install",7)==0) //DG { strcpy(configfile,lpszToken + 8); //DG if(strlen(configfile) <= 0) //DG { MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK); return 0; } _Module.LoadConfigSettings(); //DG strcat(_Module.m_szServiceName,sServiceName); //DG if (strlen(sServiceName) == 0) //DG { MessageBox(NULL, _T("Unable to read SERVICENAME key in config file."), "ThisIs", MB_OK); return 0; } return _Module.RegisterServer(TRUE, TRUE); } lpszToken = FindOneOf(lpszToken, szTokens); } // Are we Service or Local Server CRegKey keyAppID; LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T("AppID"), KEY_READ); if (lRes != ERROR_SUCCESS) return lRes; CRegKey key; lRes = key.Open(keyAppID, _T("{0D327CD4-741C-4E5D-BD84-4DB303595E0A}"), KEY_READ); if (lRes != ERROR_SUCCESS) return lRes; TCHAR szValue[_MAX_PATH]; DWORD dwLen = _MAX_PATH; lRes = key.QueryValue(szValue, _T("LocalService"), &dwLen); _Module.m_bService = FALSE; if (lRes == ERROR_SUCCESS) _Module.m_bService = TRUE; _Module.Start(); // When we get here, the service has been stopped return _Module.m_status.dwWin32ExitCode; }
接下来,我们需要向 Install() 函数添加一些代码,以便我们可以创建一个名为 Parameters 的新注册表 hive 位置,以便我们可以保存我们要安装的服务的 INI 文件的路径。
inline BOOL CServiceModule::Install() { char regpath[200]; //DG if (IsInstalled()) return TRUE; SC_HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (hSCM == NULL) { MessageBox(NULL, _T("Couldn't open service manager"), m_szServiceName, MB_OK); return FALSE; } // Get the executable file path TCHAR szFilePath[_MAX_PATH]; ::GetModuleFileName(NULL, szFilePath, _MAX_PATH); SC_HANDLE hService = ::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); if (hService == NULL) { ::CloseServiceHandle(hSCM); MessageBox(NULL, _T("Couldn't create service"), m_szServiceName, MB_OK); return FALSE; } ::CloseServiceHandle(hService); ::CloseServiceHandle(hSCM); /*DG BEGIN save config file path to registry*/ DWORD maxlen = 100; strcpy(regpath,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(regpath,m_szServiceName); strcat(regpath,"\\Parameters"); CRegKey keyAppID; keyAppID.Create(HKEY_LOCAL_MACHINE,_T(regpath)); keyAppID.SetValue(configfile, "CONFIGFILE"); /*DG END*/ return TRUE; }
接下来,我们需要在 StdAfx.h 中的 CServiceModule 类模块中声明用于从我们的 INI 文件读取值的 LoadConfigSettings 例程。
class CServiceModule : public CComModule { public: HRESULT RegisterServer(BOOL bRegTypeLib, BOOL bService); HRESULT UnregisterServer(); void Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h, UINT nServiceNameID, const GUID* plibid = NULL); void Start(); void ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv); void Handler(DWORD dwOpcode); void Run(); BOOL IsInstalled(); BOOL Install(); BOOL Uninstall(); LONG Unlock(); void LogEvent(LPCTSTR pszFormat, ...); void SetServiceStatus(DWORD dwState); void SetupAsLocalServer(); void LoadConfigSettings(); //DG //Implementation private: static void WINAPI _ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv); static void WINAPI _Handler(DWORD dwOpcode); // data members public: TCHAR m_szServiceName[256]; SERVICE_STATUS_HANDLE m_hServiceStatus; SERVICE_STATUS m_status; DWORD dwThreadID; BOOL m_bService; }; extern CServiceModule _Module; #include//{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__D0A5ADBC_7B3A_4E70_9027_BD4E9D5562D5__INCLUDED)
下一步,我们将 LoadConfigSettings 例程添加到 ThisIs.cpp 文件中
void CServiceModule::LoadConfigSettings() //DG { char szDefault[255]; GetPrivateProfileString("MAIN", "SERVICENAME", szDefault, sServiceName, 255, configfile); /* Continue to add more entries as needed to read INI file settings */ }
现在,这就是我们来到最重要部分并获得多个服务运行的关键的地方。就服务可执行文件而言,它认为自己被调用为“ThisIs”,但我们将在启动时通过更改内部公共变量 m_szServiceName 到正在运行的服务的名称来更改其内部运行时服务名称,并在 ServiceMain 例程中为其提供一个新的别名。ATL 服务向导生成的所有代码都依赖于此变量,因此通过修改它,您可以迫使整个应用程序保持一致。
要理解我所说的内容,您需要知道在 ServiceMain 参数数组中传递的第一个参数是启动的服务的名称。
inline void CServiceModule::ServiceMain(DWORD dwArgc , LPTSTR* lpszArgv ) //DG { /*DG BEGIN */ char msg[200]; char regpath[200]; /*Set Internal m_szServiceName variable to whatever service was started*/ strcpy(m_szServiceName,(char *)lpszArgv[0]); strcpy(sServiceName,m_szServiceName); DWORD maxlen = 100; strcpy(regpath,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(regpath,m_szServiceName); strcat(regpath,"\\Parameters"); CRegKey keyAppID; keyAppID.Create(HKEY_LOCAL_MACHINE,_T(regpath)); keyAppID.QueryValue(configfile,"CONFIGFILE",&maxlen); //LoadConfigSettings(); //DG strcpy(msg,m_szServiceName); strcat(msg," is configured to run with INI file: "); strcat(msg,configfile); LogEvent(_T(msg)); /*DG END*/ // Register the control request handler m_status.dwCurrentState = SERVICE_START_PENDING; m_hServiceStatus = RegisterServiceCtrlHandler(m_szServiceName, _Handler); if (m_hServiceStatus == NULL) { LogEvent(_T("Handler not installed")); return; } SetServiceStatus(SERVICE_START_PENDING); m_status.dwWin32ExitCode = S_OK; m_status.dwCheckPoint = 0; m_status.dwWaitHint = 0; // When the Run function returns, the service has stopped. Run(); SetServiceStatus(SERVICE_STOPPED); LogEvent(_T("Service stopped")); }
就这样,伙计们!
在我的生产版本中,我在 Run() 函数中有一个内置计时器,以及连接到数据库的许多其他例程,这就是 INI 文件出现的地方,或者您可以选择将其全部保存在注册表中。