Introduction
This project demonstrates how one can configure a windows service to run as
multiple services. I discovered that very little code is needed to make the
basic ATL Service project that is created using the Visual Studio Wizard into a
project that could create and run multiple services using the same
executable. For my example I�m only going to create 3 services.
I spent a week trying to come up with a solution to my problem by looking at
other code examples to no avail with every example using hard coded
multiple service names in the SERVICE_TABLE_ENTRY table which was not an option
for me. My example makes it very simple to create and run as many window
services that the Service Component Manager can handle using the same executable
and an INI file for each service you want to create. I even demonstrate that
each service is running its own INI file by writing out to the Event Viewer log
the name of the service running and its INI file.
To get started the _tWinMain method was modified to take a second argument
for installing and uninstalling a windows service which would be a
fully qualified path to an INI file. This is the backbone of the whole
project and is used to create the service name by reading the SERVICENAME
setting from the INI file and appending it to the project name which in this
case is �ThisIs�. The INI file path is also stored in the registry for the
service created and is reloaded in the ServiceMain when the service is
started . I�m sure someone will come up with an alternative solution of
passing any old name instead of an INI file path for the second argument which
is fine depending on your environment.
All of my code changes are tagged as "//DG" and be sure to uninstall a
service before recompiling a new build or it will stay in a "Performing
registration" mode until you kill the process in task manager.
First step is to include the windows.h header file and two global variables
used during service registration and for the creation of a service name at the
top of the ThisIs.cpp file. We need this so we can read the INI files and also
to read and write to the registry
#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];
char sServiceName[30];
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
Second we need to modify the default _tWinMain created by the ATL wizard to
Accept our second argument which will be the full path to our INI config file.
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
HINSTANCE , LPTSTR lpCmdLine, int )
{
lpCmdLine = GetCommandLine();
_Module.Init(ObjectMap, hInstance, IDS_SERVICENAME, &LIBID_THISISLib);
_Module.m_bService = TRUE;
TCHAR szTokens[] = _T("-/");
LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
while (lpszToken != NULL)
{
if (memcmp(lpszToken, "uninstall",9)==0)
{
strcpy(configfile,lpszToken + 10);
if(strlen(configfile) <= 0)
{
MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK);
return 0;
}
_Module.LoadConfigSettings();
strcat(_Module.m_szServiceName,sServiceName);
if (strlen(sServiceName) == 0)
{
MessageBox(NULL, _T("Unable to read SERVICENAME key in config file."), "ThisIs", MB_OK);
return 0;
}
return _Module.UnregisterServer();
}
if (memcmp(lpszToken, "regserver",9)==0)
{
strcpy(configfile,lpszToken + 10);
if(strlen(configfile) <= 0)
{
MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK);
return 0;
}
_Module.LoadConfigSettings();
strcat(_Module.m_szServiceName,sServiceName);
if (strlen(sServiceName) == 0)
{
MessageBox(NULL, _T("Unable to read SERVICENAME key in config file."), "ThisIs", MB_OK);
return 0;
}
return _Module.RegisterServer(TRUE, FALSE);
}
if (memcmp(lpszToken, "install",7)==0)
{
strcpy(configfile,lpszToken + 8);
if(strlen(configfile) <= 0)
{
MessageBox(NULL, _T("You didn't specify a config file argument."), "ThisIs", MB_OK);
return 0;
}
_Module.LoadConfigSettings();
strcat(_Module.m_szServiceName,sServiceName);
if (strlen(sServiceName) == 0)
{
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);
}
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();
return _Module.m_status.dwWin32ExitCode;
}
Next we need to add some code to the Install() function so we can create a
new registry hive location called Parameters so we can save the path of our INI
file for the service we're going to install.
inline BOOL CServiceModule::Install()
{
char regpath[200];
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;
}
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);
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;
}
Next we need to declare the LoadConfigSettings routine for reading values
from our INI file in the CServiceModule class module in the StdAfx.h.
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();
private:
static void WINAPI _ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv);
static void WINAPI _Handler(DWORD dwOpcode);
public:
TCHAR m_szServiceName[256];
SERVICE_STATUS_HANDLE m_hServiceStatus;
SERVICE_STATUS m_status;
DWORD dwThreadID;
BOOL m_bService;
};
extern CServiceModule _Module;
#include
#endif
Next step we add the LoadConfigSettings routine to the ThisIs.cpp file
void CServiceModule::LoadConfigSettings()
{
char szDefault[255];
GetPrivateProfileString("MAIN", "SERVICENAME", szDefault, sServiceName, 255, configfile);
}
Now here is where we come to the most important part and the key to the whole
process of getting multiple services to run. As far as the service executable is
concerned it thinks it is called "ThisIs" but we're going to change it's
internal run time Service Name and give it a new alias within the ServiceMain
routine upon startup by changing the internal public variable m_szServiceName to
the name of the running service. All of the code generated by the ATL service
wizard is dependent upon this variable so by modifying it you force the entire
application to fall in line.
To understand what i'm saying you need to know that the first argument passed
in the ServiceMain argument array is the name of the service that was started.
inline void CServiceModule::ServiceMain(DWORD dwArgc , LPTSTR* lpszArgv )
{
char msg[200];
char regpath[200];
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"));
}
And that's all folks!
In my production version I have a built in timer in the Run() function and a
host of other routines to connect to databases which is where the INI file comes
in or you can choose to save it all in the registry.