NT Service: An OLE Control for Creating Windows NT Services in Visual Basic

Mauricio Ordóñez
Microsoft Consulting Services

June 1996

Click to open or copy the files in the NTSRVOCX sample application for this technical article.

Abstract

This article describes an OLE Control that enables developers to create Microsoft® Visual Basic® applications that function as Microsoft Windows NT® services. With the NTService control, you can install a service, log events, and respond to start, stop, pause, and continue events.

The interface between the control and the operating system was based on the C++ class described in Nigel Thompson’s technical article “Creating a Simple Windows NT Service in C++,” in the Microsoft Developer Network Library.

Introduction

Microsoft® Windows NT® services are processes that run without requiring a user to be logged on to the system. Typically, we see such services being used for industrial-strength application servers, such as Microsoft SQL Server and Microsoft Exchange Server, or for parts of the operating system, as in the case of Dynamic Host Interface Protocol (DHCP) and Windows® Internet Name Service (WINS) servers.

The services architecture also offers an attractive solution when you need an application to:

Previously, developers could write services only in C or C++. The operating system interface for services requires a callback function and blocks the main thread of execution. Because Visual Basic 4.0 has neither threads nor callback functions, developers cannot call these application programming interfaces (APIs) directly. Consequently, developers of Visual Basic applications relied on the SRVANY utility provided in the Windows NT Resource Kit. This tool allows any executable to be started by the system at boot time; however, because the communication between SRVANY and the application is limited to standard window messages, the solution is not very robust. Moreover, SRVANY lacks support for the pause and continue functions.

The NTService OLE Control eliminates the need for helper processes like SRVANY by enabling Visual Basic applications to directly interface with the services’ APIs. The control translates signals from the operating system into events that can be processed by the Visual Basic application. It also provides support for functions that services typically require, such as installation and event logging.

Creating a Service with Visual Basic

Creating a service begins by dropping the NTService control on a form. Services typically do not have any user interface, but in Visual Basic the control needs a form to serve as a container. At a minimum, you will need to set some properties and implement the installation routine and handlers for the Start and Stop events.

Error Handling

Your service must never depend on being able to display user interface elements. For example, your code should prevent any message box from appearing that would require the user to clear it. If a message box appears while the service is not interactive, the process will wait indefinitely without anybody to clear the message box.

The proper way to avoid this problem is to set On Error handlers on your service. Because you will probably be interested in seeing any errors that occur, you should use the event-logging function, LogEvent, to preserve error messages and conditions.

Initialization

Your service will need to carry out installation, running, and removal. One way to accomplish this is by testing command-line parameters in the Form Load event (Figure 1). For example:

By default, the service will assume it is being started by the services controller to run as a service.

Figure 1. Service Initialization in Form Load Event

Private Sub Form_Load()
    Dim strDisplayName As String

On Error Goto Err_Load

    strDisplayName = NTService1.DisplayName
    
    If Command = "-install" Then
        ' Enable interaction with desktop.
        NTService1.Interactive = True
        
        If NTService1.Install Then
MsgBox strDisplayName & " installed successfully"
        Else
            MsgBox strDisplayName & " failed to install"
        End If
        End
    
    ElseIf Command = "-uninstall" Then
        If NTService1.Uninstall Then
            MsgBox strDisplayName & " uninstalled successfully"
        Else
            MsgBox strDisplayName & " failed to uninstall"
        End If
        End
    
    ElseIf Command = "-debug" Then
        NTService1.Debug = True
    
    ElseIf Command <> "" Then
        MsgBox "Invalid command option"
        End
    
    End If
    
    ' Connect service to Windows NT services controller.
    NTService1.StartService
    
Err_Load:
    ' Error starting service
    
End Sub

To install the service, you need to set some properties at design or run time and call the Install method:

To run the service so that it can begin processing, call the StartService method. This method sets up the service framework that calls the Start event for initialization and the Stop event for shutdown.

Events

At a minimum, your service needs to process the Start and Stop events. You can use these events to begin or halt processing in your service.

The Start event (Figure 2) receives a Boolean parameter that must be set to True if the event was processed successfully. By default, the control will assume that the event is not handled or that an error has occurred.

Figure 2. Start Event Handler

Private Sub NTService1_Start(Success As Boolean)

On Error Goto Err_Start

    ' TODO: Begin processing

    Success = True       ' Report success

Err_Start:
    Call NTService1.LogEvent(svcMessageError, svcEventError, "[" & _
         Err.Number & "] " & Err.Description)
End Sub

The Stop event (Figure 3) is the signal indicating that the service should terminate. You are responsible for freeing resources and terminating your processing.

Figure 3. Stop Event Handler

Private Sub NTService1_Stop()
On Error Goto Err_Stop

    ' TODO: Suspend processing and release resources
    
    Unload Me            ' End process

Err_Stop:
    Call NTService1.LogEvent(svcMessageError, svcEventError, "[" & _
         Err.Number & "] " & Err.Description)
End Sub

Debugging

When the service starts, you need to determine if the service should attempt to communicate with the Windows NT services controller. The difference between a service and other processes is that the service communicates with the services controller. Otherwise, the same application could run as a console or graphical user interface (GUI) application.

If you want to run the service from the Visual Basic development environment or from the console, you must set the Debug property to True. You will still need to call the StartService method in order to trigger the normal events.

You may find it very useful to set the Advanced Options for the project to include this command-line parameter.

Control Internals

Many of the details of creating services in C or C++ are well documented. These examples assume that the entire application is designed around the Windows NT service framework defined by the Win32® APIs. The NTService control must bridge the services interfaces with the Visual Basic application model, which presents some interesting challenges:

Service Thread

During initialization, services call the StartServiceCtrlDispatcher API, passing a callback function that is used by the service controller for service initialization. The control cannot call the dispatcher function in a method because this function blocks the caller until the service is stopped. To solve this problem, the control creates a separate thread that is responsible for calling the dispatcher. This allows the primary thread used by Visual Basic to continue processing while the other thread is blocked by the services dispatcher.

While the application is running as a service, events will fire from the service thread and the control request handler. Both of these functions execute on different thread contexts from the Visual Basic code. Because Visual Basic is not thread-safe, the apartment-model OLE control rules specify that all calls must occur in the same thread where the object was created. The effect on the NTService control is that it must transfer execution to the primary thread in order to generate notification events. This is done by posting window messages to the control’s invisible window and letting the window message dispatch loop inside Visual Basic transfer execution to the control.

The control’s window is critical to the routing of messages from the services dispatcher to the Visual Basic application. Because OLE control containers have some discretion as to when the control window is created, the control informs the container that the window must exist always. This is done by marking the control with the OLEMISC_SIMPLEFRAME attribute and enabling the simple frame interface in the control constructor.

The StartService method (Figure 4) checks the Debug property to determine if the service thread should be created. If the service was not started by the services controller, there is no need to call the dispatcher, so the control triggers the Start event immediately.

Figure 4. StartService Method

BOOL CNtSvcCtrl::StartService()
{
    BOOL bSuccess = FALSE;

    // Initialize service status.
    m_Status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    m_Status.dwCurrentState = SERVICE_STOPPED;
    m_Status.dwControlsAccepted = SERVICE_ACCEPT_STOP |
            m_controlsAccepted;
    m_Status.dwWin32ExitCode = 0;
    m_Status.dwServiceSpecificExitCode = 0;
    m_Status.dwCheckPoint = 0;
    m_Status.dwWaitHint = 0;

    // In debug mode, don’t create a service thread.
    if (m_bIsDebug)
    {
        m_pThis->PostMessage(CWM_START, 0, 0);
        bSuccess = TRUE;
    }
    else
    {
        DWORD dwThreadId;

        // Create a service thread to communicate with
        // the services dispatcher.
        m_hServiceThread = CreateThread(
                NULL,
                0, 
                ServiceThread, 
                NULL,
                0,
                &dwThreadId);

        if (m_hServiceThread)
            bSuccess = TRUE;
    }

    return bSuccess;
}

The service thread (Figure 5) exists solely for the purpose of making the blocking call to the dispatcher. The real initialization of the control takes place in the ServiceMain function.

Figure 5. Service Thread

DWORD CNtSvcCtrl::ServiceThread(LPVOID)
{
    BOOL b;

    // Get a pointer to the C++ object.
    CNtSvcCtrl* pService = m_pThis;

    SERVICE_TABLE_ENTRY st[] = {
        {m_pThis->m_szServiceName, ServiceMain},
        {NULL, NULL}
    };

    m_pThis->DebugMsg(
        TEXT("Calling StartServiceCtrlDispatcher()"));

    // Call the services dispatcher. This call blocks
    // until the service stops.
    b = ::StartServiceCtrlDispatcher(st);

    m_pThis->DebugMsg(
        TEXT("Returned from StartServiceCtrlDispatcher()"));

    return b;
}

ServiceMain Function

The services dispatcher calls the ServiceMain function (Figure 6) with the expectation that the function will not return until the service has stopped. The function is responsible for:

Figure 6. ServiceMain Function

void CNtSvcCtrl::ServiceMain(DWORD dwArgc, LPTSTR* lpszArgv)
{
    // Get a pointer to the C++ object.
    CNtSvcCtrl* pService = m_pThis;

    pService->DebugMsg(TEXT("Entering CNtSvcCtrl::ServiceMain()"));

    // Register the control request handler.
    pService->m_Status.dwCurrentState = SERVICE_START_PENDING;
    pService->m_hServiceStatus = RegisterServiceCtrlHandler(
            pService->m_szServiceName,
            Handler);

    if (pService->m_hServiceStatus == NULL) 
    {
        pService->LogEvent(EVENTLOG_ERROR_TYPE,
            EVMSG_CTRLHANDLERNOTINSTALLED, 
            NULL);
        return;
    }

    pService->m_hStopEvent = CreateEvent(
        NULL,    // no security attributes
        TRUE,    // manual reset event
        FALSE,   // not-signaled
        NULL);   // no name

    // Notify the user that the service is starting.
    m_pThis->PostMessage(CWM_START, 0, 0);

    // The service thread will wait indefinitely until the service 
    // control manager or the user signals the stop event. 
    // This event is set inside the StopService method.
    WaitForSingleObject(m_pThis->m_hStopEvent, INFINITE);

    // Tell the service manager we are stopped.
    pService->SetStatus(SERVICE_STOPPED);
    pService->ReportStatus();

    pService->DebugMsg(TEXT("Leaving CNtSvcCtrl::ServiceMain()"));
}

The ServiceMain function posts a message back to the control to indicate the start notification. This ensures that the control will fire the Start event from the primary thread without violating OLE threading rules.

Request Handler Function

The services controller notifies the service via the request handler function (Figure 7) that was registered in the initialization phase. This function also executes in a different thread context—requiring transfer of execution with window messages.

Figure 7. Request handler function

void CNtSvcCtrl::Handler(DWORD dwOpcode)
{
    // Get a pointer to the object.
    CNtSvcCtrl* pService = m_pThis;

    pService->DebugMsg("CNTService::Handler(%lu)", dwOpcode);
    switch (dwOpcode) {
    case SERVICE_CONTROL_STOP: // 1
        pService->SetStatus(SERVICE_STOP_PENDING);
        break;

    case SERVICE_CONTROL_PAUSE:
        pService->SetStatus(SERVICE_PAUSE_PENDING);
        break;
    }

    // Report current status.
    m_pThis->ReportStatus();

    // Defer message.
    m_pThis->PostMessage (CWM_HANDLER,dwOpcode, 0);
}

// Window message handler for CWM_HANDLER.
LRESULT CNtSvcCtrl::OnHandler(WPARAM wParam, LPARAM lParam)
{
    // Get a pointer to the C++ object.
    CNtSvcCtrl* pService = m_pThis;

    BOOL bSuccess = FALSE;

    // Report current status.
    ReportStatus();

    switch (wParam)
    {
    case SERVICE_CONTROL_STOP: // 1
        m_pThis->StopService();
        break;

    case SERVICE_CONTROL_PAUSE: // 2
        pService->FirePause(&bSuccess);
        if (bSuccess)
            SetStatus (SERVICE_PAUSED);
        break;

    case SERVICE_CONTROL_CONTINUE: // 3
        pService->FireContinue(&bSuccess);
        if (bSuccess)
            SetStatus (SERVICE_RUNNING);
        break;

    default:
        pService->FireControl(wParam);
        break;
    }

    // Report current status.
    ReportStatus();

    return 0L;
}

Uses

Just because your application is running as a service does not mean that it can handle the processing requirements of application servers such as Microsoft SQL Server or Microsoft Exchange Server. Visual Basic services are single-threaded and do not expose programming interfaces. The introduction of the Distributed Component Object Model (DCOM) in Windows NT version 4.0 will allow the service to be callable through OLE Automation interfaces locally or from a remote computer.

The best use of this type of service is to augment application servers. For example, if you need to capture a real-time data broadcast in a SQL database, you could add Windows NT service support to make your application easier to administer. Likewise, you could write Microsoft Exchange Server mailbox agents in a higher-level language, taking advantage of existing components, such as OLE Messaging, to simplify development.

Summary

The NTService control is primarily designed to let developers create Visual Basic applications that install and run as Windows NT services. These applications benefit from automatic startup, remote administration, and event logging. Server processes can be a useful tool in the client/server developer’s toolbox.