By Peter Axelsson,
CAP Gemini, Sweden
When I was assigned a project earlier this summer designing and developing a program for Windows NT, which should run as a service in the NT environment, I first thought of the Internet Web pages as a key source for knowledge and wisdom. My experience in NT services wasn't that good... in fact it was non-existent, and combined with the fact that I had a tight schedule for my project, forced me to search information in in any place I could think of. When I started searching for information about the Windows NT Services I was surprised how little information there were on the web about this, and I really put some effort in it. But, although I didn't find as much information on the web as to get my project finished (I managed it by 'reading' some great CD's ie Computer Select CD, MSDN), I ran into some great NT development pages. It's these pages and other matters of interest regarding Windows NT developing, like how you actually write an NT Service, I would like to share with you...
The service program code can be divided in four parts:
- Installing the service
- Controlling the service
- Running the service
- Removing the service
Installing the service is a must before the service can be started. In the system all the services are kept in a list in a service database. The installed services are listed when running the Control Panel's Service Control Manager (SCM), and can be started, stopped and in other ways manipulated. Installing the (small and basic) service is very easy. You must supply the names of the service (hidden and displayed) which the users sees when he/she runs the SCM, the path to the exe file where it resides on the disk, the user account and password in which the service will be run, and finally a switch which tells whether the service is to be started manually or automatically (during system start up). The install procedure is (in a small system with just one exe program) usually taken place in the same exe file as the service execution code. This simplifies things by letting the user administer just one program instead of two. As we will see later it is not a problem to have the program serve as a utility, beeing able to run from the command line at the same time as it is able to run as a service started from the SCM. There are some things to keep in mind though, to get these two different start interfaces to run smothly. The install code in Listing 1 shows the basic procedure for installing a service. The service installed will be the program executing the install function, and it's installed with the parameters specified as arguments in the function. The arguments passed to the CreateService() function specifies the service with these: 1) control processes will be able to perform any kind of action upon the service (SERVICE_ALL_ACCESS). 2) the process containing the service will only contain one service (SERVICE_WIN32_OWN_PROCESS). 3) The start method is either manual (SERVICE_DEMAND_START) or automatic (SERVICE_AUTO_START) which means that the service will be started upon system start up. 4) if the service is started during startup and an error occurs, we want the error to be logged but the startup to continue (SERVICE_ERROR_NORMAL). After you have installed the service with this function the service is installed and is visible in the SCM list of services. You send control codes to the service from the SCM to start it.
Controlling the service is an optional feature of a service program. Control can be made from within the Windows NT system using the SCM, which is the most common way to administer services. In the SCM you can start, stop and pause the service, and you can modify the parameters regarding the service's behaviour, such as the user account/password and the automatic/manually start switch. Additionally supplying the features for service control in your program can be very useful when installing and testing the service, both as developer and administrator. See Listing 2 for start and stop functions.
Running the service can be somewhat tricky. There are a
number of steps the service have to take to start up as a
service, and then act as a service upon events from controlling
processes (as the SCM), and lastly terminate as a service when
signaled to. In addition to these obligatory actions the service
should perform some useful work while obeying the rules for
services interfaces.
The first thing to do when starting up the application as a
service is to start the control dispatcher thread (see Listing 3). The control dispatcher creates a
thread which will execute the service main function specified in
the table. The service main function accepts arguments (argc,
argv) just like the command line in a normal main(). The
difference is that the arguments in the service main function are
supplied from the control process when starting the service. See Listing 4. The service main function
registers the function that will handle control requests for the
service (ie from SCM), see Listing 5.
When service control are requested the requests will be handled
by that function. Before the loop the service main function
creates a thread that runs a small function, which only calls the
function that was sent as an argument into the OmInitService()
function. This function, called application function, which the
developer wants to have called from within the service core
functions, must loop forever while doing its duty. When that
function returns it terminates, but the process keeps on running
which means that the SCM, or any control process, will not be
able to see what has happened. To stop the service a service
control process (ie SCM) sends a stop request to the service.
That request is handled by the service control handler function,
and to be able to stop the service when receiving this request,
the service main function creates an event that the control
handler function will use to signal that the service should be
stopped. After initializing the service main function goes into
an indefinite loop waiting for this event to be signaled.
Some synchronization should be written to make the application
thread aware of a stopping request in order to close any open
file or other that is open. When the service main function exits
the service process exits and control is returned to the function
that called the StartServiceCtrlDispatcher(). At this point the
program should exit.
Removing the service is even easier than installing. The only tricky thing is to be sure that the service is stopped in a polite manner before the removal. See Listing 6 for removal code.
The listings below are for demonstration purpouse only, and must be modified for compilation. I intend to add some functionality to these examples (event loggin etc) and put the whole package out here...
When developing services there are some hints to be aware of to avoid getting in trouble:
Links for more info on Windows NT Services:
SUSRV
A su service for Windows NT 3.51, by Steffen Krause.
With source code and Intel binaries is it a good example on how
to write a Windows NT service.
How to write an
NT Service, by Paula Tomlinson, Windows Developers Journal Feb
1996.
A very good article written by a very good author! Availible on
the Computer Select CD.
Win32 SDK books about services. Starting with
"In the Microsoft© Win32© application programming
interface (API), a service is an executable object that is
installed in a registry database maintained by the service
control manager."
© 1997 peter.axelsson@capgemini.se
BOOL OmInstallService(LPCTSTR serviceName,
LPCTSTR account,
LPCTSTR password,
BOOL autostart)
{
SC_HANDLE schService;
SC_HANDLE schSCManager;
BOOL bCreateStatus;
CHAR szPath[512];
/* -- Reset the status -- */
bCreateStatus = FALSE;
/* -- Get the exe file path -- */
if (!GetModuleFileName(NULL, szPath, 512))
return FALSE;
/* -- Establish a connection to the SCM -- */
schSCManager = OpenSCManager(
NULL, /* LocalMachine */
NULL, /* ServiceDatabase */
SC_MANAGER_ALL_ACCESS); /* Access types */
/* -- If the connection is ok -- */
if (schSCManager)
{
/* -- Create a service object -- */
schService = CreateService(
schSCManager, /* SCManager handle */
TEXT(serviceName), /* Name of service */
TEXT(serviceName), /* Name to display */
SERVICE_ALL_ACCESS, /* Access types */
SERVICE_WIN32_OWN_PROCESS, /* Service type */
(autostart) ? SERVICE_AUTO_START : /* Start type is auto */
SERVICE_DEMAND_START, /* Start type is manual */
SERVICE_ERROR_NORMAL, /* Error control at start up */
szPath, /* Exe file path */
NULL, /* no load ordering group */
NULL, /* No tag identifier */
TEXT(""), /* No dependencies */
account, /* Account */
password); /* Password */
/* -- If the service was installed ok -- */
if (schService)
{
/* -- Close the service object -- */
CloseServiceHandle(schService);
/* -- Set the status to TRUE -- */
bCreateStatus = TRUE;
}
else
printf("Error %d creating service %s!\n",
GetLastError(), serviceName);
/* -- Close the SCM connection -- */
CloseServiceHandle(schSCManager);
}
else
printf("Error %d connecting to SCM!\n",
GetLastError());
return(bCreateStatus);
}
BOOL OmStartService(LPCTSTR serviceName) { SC_HANDLE schService; SC_HANDLE schSCManager; BOOL bStartStatus; /* -- Reset the status -- */ bStartStatus = FALSE; /* -- Establish a connection to the SCM -- */ schSCManager = OpenSCManager( NULL, /* LocalMachine */ NULL, /* ServiceDatabase */ SC_MANAGER_ALL_ACCESS); /* Access types */ /* -- If the connection is ok -- */ if (schSCManager) { schService = OpenService( schSCManager, /* SCManager handle */ serviceName, /* Name of service */ SERVICE_ALL_ACCESS); /* Access types */ if (schService) { /* -- Start the service -- */ if (StartService( schService, /* Service handle */ 0, /* Number of arguments */ NULL)) /* Argument list */ { printf("Starting service %s.", serviceName); /* -- Give the service some time to start -- */ Sleep(1000); /* -- Query the service's status -- */ while (QueryServiceStatus(schService, &ssStatus)) { printf("."); /* -- While START pending keep waiting -- */ if (ssStatus.dwCurrentState == SERVICE_START_PENDING) Sleep(1000); else break; } printf("\n"); /* -- Check if the service was stopped properly -- */ if (ssStatus.dwCurrentState == SERVICE_RUNNING) bStartStatus = TRUE; else printf("The service %s could not be started!\n", serviceName); } else if (QueryServiceStatus(schService, &ssStatus)) if (ssStatus.dwCurrentState == SERVICE_RUNNING) { printf("The servicen is already running.\n"); bStartStatus = TRUE; } } else printf("Error %d opening service %s!\n", GetLastError(), serviceName); } else printf("Error %d connecting to SCM!\n", GetLastError()); return(bStartStatus); } BOOL OmStopService(LPCTSTR serviceName) { SC_HANDLE schService; SC_HANDLE schSCManager; BOOL bStopStatus; /* -- Reset the status -- */ bStopStatus = FALSE; /* -- Establish a connection to the SCM -- */ schSCManager = OpenSCManager( NULL, /* LocalMachine */ NULL, /* ServiceDatabase */ SC_MANAGER_ALL_ACCESS); /* Access types */ /* -- If the connection is ok -- */ if (schSCManager) { schService = OpenService( schSCManager, /* SCManager handle */ serviceName, /* Name of service */ SERVICE_ALL_ACCESS); /* Access types */ if (schService) { /* -- Stop the service -- */ if (ControlService( schService, SERVICE_CONTROL_STOP, &ssStatus)) { printf("Stopping service %s.", serviceName); /* -- Give the service some time to stop -- */ Sleep(1000); /* -- Query the service's status -- */ while (QueryServiceStatus(schService, &ssStatus)) { printf("."); /* -- While STOP pending keep waiting -- */ if (ssStatus.dwCurrentState == SERVICE_STOP_PENDING) Sleep(1000); else break; } printf("\n"); /* -- Check if the service was stopped properly -- */ if (ssStatus.dwCurrentState == SERVICE_STOPPED) bStopStatus = TRUE; else printf("The service %s could not be stopped!\n", serviceName); } else if (QueryServiceStatus(schService, &ssStatus)) if (ssStatus.dwCurrentState == SERVICE_STOPPED) { printf("The service is already stopped.\n"); bStopStatus = TRUE; } } else printf("Error %d opening service %s!\n", GetLastError(), serviceName); } else printf("Error %d connecting to SCM!\n", GetLastError()); return(bStopStatus); }
VOID (*lpProgramCallbackFunction)(VOID); BOOL OmInitService(VOID (*callback_function)(VOID)) { CHAR szTmpErr[12]; SERVICE_TABLE_ENTRY dispatchTable[] = { /* -- In this example it's just one service -- */ /* -- so we don't need to specify the service's name -- */ { TEXT(""), (LPSERVICE_MAIN_FUNCTION) OmServiceMain }, { NULL, NULL } }; /* -- Write info on console if run from command line -- */ printf("The program should be run as a service!\n"); /* -- Save the function pointer -- */ lpProgramCallbackFunction = callback_function; /* -- Start the service control dispatcher -- */ if (!StartServiceCtrlDispatcher(dispatchTable)) { /* -- ERROR_FAILED_SERVICE_CONTROLLER_CONNECT indicates that -- */ /* -- the program was not started as a service! -- */ if (GetLastError() != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) OmServiceDumpEvent(...); return(FALSE); } return(TRUE); }
SERVICE_STATUS ssStatus; SERVICE_STATUS_HANDLE sshStatusHandle; DWORD TID = 0; HANDLE threadHandle = NULL; HANDLE hServDoneEvent = NULL; DWORD dwGlobalErr; VOID OmServiceThread(VOID *notused) { (*lpProgramCallbackFunction)(); } VOID OmServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { /* -- The name of the service is stored in the first argument-- */ strcpy(szServiceName, lpszArgv[0]); /* -- Register the service control handler -- */ sshStatusHandle = RegisterServiceCtrlHandler( TEXT(lpszArgv[0]), OmServiceCtrl); if (!sshStatusHandle) goto cleanup; /* Set some status variables */ ssStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; ssStatus.dwServiceSpecificExitCode = 0; /* -- Report that start is pending -- */ if (!OmReportStatusToSCMgr( SERVICE_START_PENDING, /* Service state */ NO_ERROR, /* Exit code */ 1, /* Checkpoint */ 3000)) /* Wait hint */ goto cleanup; /* -- Create an event for stop signals -- */ hServDoneEvent = CreateEvent( NULL, /* No Security Attributes */ TRUE, /* A manual reset event */ FALSE, /* Initialize as not signaled */ NULL); /* No need for a name */ if (hServDoneEvent == (HANDLE) NULL) goto cleanup; /* -- Report that start is pending -- */ if (!ReportStatusToSCMgr( SERVICE_START_PENDING, /* Service state */ NO_ERROR, /* Exit code */ 2, /* Checkpoint */ 3000)) /* Wait hint */ goto cleanup; /* -- Start the thread that calls the application function -- */ threadHandle = CreateThread( NULL, /* No Security Attributes */ 0, /* Same as parent's stack size */ (LPTHREAD_START_ROUTINE) OmServiceThread, NULL, /* No arguments to the thread */ 0, /* No creation flags */ &TID); /* Thread ID */ if (!threadHandle) goto cleanup; /* -- Report that service is up and running -- */ if (!ReportStatusToSCMgr( SERVICE_RUNNING, /* Service state */ NO_ERROR, /* Exit code */ 0, /* Checkpoint */ 0)) /* Wait hint */ goto cleanup; /* -- Wait for the stop event to be signaled -- */ dwWait = WaitForSingleObject(hServDoneEvent, INFINITE); /* -- Report that we are stopping -- */ ReportStatusToSCMgr( SERVICE_STOP_PENDING, /* Service state */ NO_ERROR, /* Exit code */ 1, /* Checkpoint */ 30000); /* Waithint */ /* -- Write some synchronization with the callback thread here... -- */ cleanup: /* -- Close the stop event-- */ if (hServDoneEvent != NULL) CloseHandle(hServDoneEvent); /* -- Report that we have stopped the service -- */ if (sshStatusHandle) ReportStatusToSCMgr( SERVICE_STOPPED, /* Service state */ dwGlobalErr, /* Exit code */ 0, /* Checkpoint */ 0); /* Wait hint */ return; }
VOID WINAPI OmServiceCtrl(DWORD dwCtrlCode) { DWORD dwState = SERVICE_RUNNING; /* -- Check which request we have received -- */ switch(dwCtrlCode) { /* -- Put the thread in a paused state -- */ case SERVICE_CONTROL_PAUSE: if (ssStatus.dwCurrentState == SERVICE_RUNNING) { SuspendThread(threadHandle); dwState = SERVICE_PAUSED; } break; /* -- Start the thread after the pause -- */ case SERVICE_CONTROL_CONTINUE: if (ssStatus.dwCurrentState == SERVICE_PAUSED) { ResumeThread(threadHandle); dwState = SERVICE_RUNNING; } break; /* -- Stop the service -- */ case SERVICE_CONTROL_STOP: case SERVICE_CONTROL_SHUTDOWN: dwState = SERVICE_STOP_PENDING; /* -- Report the stop pending -- */ ReportStatusToSCMgr( SERVICE_STOP_PENDING, /* Service state */ NO_ERROR, /* Exit code */ 1, /* Checkpoint */ 3000); /* Wait hint */ /* -- Signal stop event to stop the service main func. -- */ SetEvent(hServDoneEvent); return; /* -- Just report the service status -- */ case SERVICE_CONTROL_INTERROGATE: break; default: break; } /* -- Report the service status -- */ ReportStatusToSCMgr(dwState, NO_ERROR, 0, 0); } BOOL OmReportStatusToSCMgr(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwCheckPoint, DWORD dwWaitHint) { BOOL fResult; /* -- While pending start don't take any control requests -- */ if (dwCurrentState == SERVICE_START_PENDING) ssStatus.dwControlsAccepted = 0; else ssStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE; /* -- Put the status values into the status structure -- */ ssStatus.dwCurrentState = dwCurrentState; ssStatus.dwWin32ExitCode = dwWin32ExitCode; ssStatus.dwCheckPoint = dwCheckPoint; ssStatus.dwWaitHint = dwWaitHint; /* -- Report the status -- */ if (!(fResult = SetServiceStatus( sshStatusHandle, /* The service control handler handle */ &ssStatus))) /* The service status structure */ { /* -- Stop the service if status report failed! -- */ OmServiceDumpEvent(...); /* -- Signal the stop event -- */ SetEvent(hServDoneEvent); } /* -- Return the result -- */ return fResult; }
BOOL OmRemoveService(LPCTSTR serviceName) { SC_HANDLE schService; SC_HANDLE schSCManager; BOOL bRemoveStatus; /* -- Reset the status -- */ bRemoveStatus = FALSE; /* -- Establish a connection to the SCM -- */ schSCManager = OpenSCManager( NULL, /* LocalMachine */ NULL, /* ServiceDatabase */ SC_MANAGER_ALL_ACCESS); /* Access types */ /* -- If the connection is ok -- */ if (schSCManager) { schService = OpenService( schSCManager, /* SCManager handle */ serviceName, /* Name of service */ SERVICE_ALL_ACCESS); /* Access types */ if (schService) { /* -- Stop the service before removing -- */ if (ControlService( schService, SERVICE_CONTROL_STOP, &ssStatus)) { printf("Stopping service %s.", serviceName); /* -- Give the service some time to stop -- */ Sleep(1000); /* -- Query the service's status -- */ while (QueryServiceStatus(schService, &ssStatus)) { printf("."); /* -- While STOP pending keep waiting -- */ if (ssStatus.dwCurrentState == SERVICE_STOP_PENDING) Sleep(1000); else break; } printf("\n"); /* -- Check if the service was stopped properly -- */ if (ssStatus.dwCurrentState != SERVICE_STOPPED) printf("The service %s could not be stopped!\n", serviceName); } /* -- Remove the service -- */ if (DeleteService(schService)) bRemoveStatus = TRUE; } else printf("Error %d opening service %s!\n", GetLastError(), serviceName); } else printf("Error %d connecting to SCM!\n", GetLastError()); return(bRemoveStatus); }