Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CREATING MULTIPLE WINDOW SERVICES USING INI CONFIG FILE

0.00/5 (No votes)
11 Mar 2006 1  
MULTIPLE WINDOW SERVICES

Sample Image - multiservices.jpg

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];	//DG

char sServiceName[30];	//DG


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 /*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;
}

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];	//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;
}

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(); //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)

Next step we add the LoadConfigSettings routine to the ThisIs.cpp file

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 */
}

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 )	//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"));
}

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here