Overmind Developing NT

Windows NT Services

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


Listing 1.

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

Listing 2.

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

Listing 3.

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

Listing 4.

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

Listing 5.

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

Listing 6.

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