Freigeben über


Windows service example in C++

Implementing a Windows service is not an easiest thing. There are examples on MSDN but they're not exactly straightforward. There is an example in classic C, and I've seen the examples in C# as well (there is actually a C# service boilerplate in Visual Studio that can be used as a project starter). And after you've created a service you need to install it. The C# boilerplate includes the service install code but in reality it only confuses things: there is no good reason to use that code, it's much easier to install a service using sc.exe (by the way, this also applies to the drivers: I've seen the driver examples that provide their own code for loading the driver but that's just wrong, use sc.exe instead). I have shown how to use sc.exe in the previous post, here I'm going to talk about how to write a service in an easier way.

First, a short summary: what is a service? It's basically a program that runs somewhere in the background and does something useful. Windows provides a way to start and stop these programs as the system need, more specifically this is done in Windows by the Service Controller. And more exactly, a service is not necessarily a stand-alone program but might be a DLL with certain defined entry points.

A service may be written to run as either a stand-alone process or as a part of the Service Controller's process (which creates a thread per service, and the service is allowed to create more threads). If the service runs in SC, the SC creates the thread for a service then loads its DLL (or again, possibly a dynamically-linked EXE, the line between a DLL and an EXE is thin), and calls the entry points to move the service though its states (first start, then eventually stop). If the service runs in a separate process, the SC just starts its EXE. The EXE uses the SC API functions to start the service controller stub in this process. The EXE also contains the exactly same entry points as a service without its own process. So when the SC stub starts, it establishes the communications with the main SC and then on its command moves the service though its states in exactly the same way, calling the same entry points in exactly the same way.

I highly recommend running your service in a separate process. This way if something very bad happens and the service crashes, it's much easier to debug. I would also highly recommend making your EXE dual-purpose: runnable both as a service and directly from command-line. This way you can test it from the command-line and hammer out all the bugs, and then run it in production as a service. Your code can make the decision on the starting mode based on the command-line arguments: you can either start without arguments as a service, or the other way around, you can specify some particular argument when defining your service for sc.exe. You can provide the other arguments for a service on the command line too (and I have an example planned that does just that) but the more traditional way is to place them into Registry under HKLM\SYSTEM\CurrentControlSet\Services\YourServiceName\Parameters. Another difference for the service mode is that it definitely should do the logging through the WMI rather than writing messages to stdout and stderr, or at the very least write its log into a file.

So, since I've found that above-mentioned example too difficult to use, I've made my own C++ class that wraps all the service communication, and that I think is much easier to use. The basic usage happens like this: you define your own subclass of the class Service, with the virtual methods that know how to start and how to stop your application logic. Then in wmain() you create an instance of this class (it's really a singleton), call the method run() on it and wait for the completion. That's it, everything else gets taken care of.

Here is a simple example of the subclass:

 class MyService: public Service
{
protected:
    // The background thread that will be executing the application.
    // This handle is owned by this class.
    HANDLE appThread_;

public:
    // The exit code that will be set by the application thread on exit.
    DWORD exitCode_;

    // name - service name
    MyService(
        __in const std::wstring &name
    )
        : Service(name, true, true, false),
        appThread_(INVALID_HANDLE_VALUE),
        exitCode_(1) // be pessimistic
    { }

    ~MyService()
    {
        if (appThread_ != INVALID_HANDLE_VALUE) {
            CloseHandle(appThread_);
        }
    }

    virtual void onStart(
        __in DWORD argc,
        __in_ecount(argc) LPWSTR *argv)
    {
        setStateRunning();

        // start the thread that will execute the application
        appThread_ = CreateThread(NULL, 
            0, // do we need to change the stack size?
            &serviceMainFunction,
            (LPVOID)this,
            0, NULL);

        if (appThread_ == INVALID_HANDLE_VALUE) {
            log(WaSvcErrorSource.mkSystem(GetLastError(), 1, L"Failed to create the application thread:"),
                Logger::SV_ERROR);

            setStateStopped(1);
            return;
        }
    }

    virtual void onStop()
    {
        ... somehow tell the application thread to stop ...

        DWORD status = WaitForSingleObject(appThread_, INFINITE);
        if (status == WAIT_FAILED) {
            log(WaSvcErrorSource.mkSystem(GetLastError(), 1, L"Failed to wait for the application thread:"),
                Logger::SV_ERROR);
            // presumably exitCode_ already contains some reason at this point
        }

        // exitCode_ should be set by the application thread on exit
        setStateStopped(exitCode_);
    }
};

The logging in this example uses the error objects from an earlier post. The only tricky part here is that the method onStart() is called in the thread of the service controller, so it can't just execute the application code right there. Instead it has to create a separate application thread and then return success. The method onStop() then has to somehow tell this background application thread to stop, the exact way is up to your application. Then it must wait for the application to get actually stopped and only then return, with the exit code set by the application thread.  This simple stopping code works if the wait for the application to stop is within 20 minutes or so. If the wait is longer, it would also have to send the periodic status updates to the service controller.

The basic wmain() is pretty simple too:

 int
__cdecl
wmain(
    __in long argc,
    __in_ecount(argc) PWSTR argv[]
    )
{
    ... initialize the logger, parse the arguments etc ...

    auto svc = make_shared<MyService>("MyService");
    svc->run(err);
    if (err) {
        logger->log(err, Logger::SV_ERROR, NULL);
        exit(1);
    }
    
    return 0;
}

Now let's look at the Service class API in a bit more detail:

 class DLLEXPORT Service
{
public:
    // The way the services work, there can be only one Service object
    // in the process. 
    Service(const std::wstring &name,
        bool canStop,
        bool canShutdown,
        bool canPauseContinue);

    virtual ~Service();

    // Run the service. Returns after the service gets stopped.
    // When the Service object gets started,
    // it will remember the instance pointer in the instance_ static
    // member, and use it in the callbacks.
    // The errors are reported back in err.
    void run(Erref &err);

    // Change the service state. Don't use it for SERVICE_STOPPED,
    // do that through the special versions.
    // Can be called only while run() is running.
    void setState(DWORD state);
    // The convenience versions.
    void setStateRunning()
    {
        setState(SERVICE_RUNNING);
    }
    void setStatePaused()
    {
        setState(SERVICE_PAUSED);
    }
    // The stopping is more compilcated: it also sets the exit code.
    // Which can be either general or a service-specific error code.
    // The success indication is the general code NO_ERROR.
    // Can be called only while run() is running.
    void setStateStopped(DWORD exitCode);
    void setStateStoppedSpecific(DWORD exitCode);

    // On the lengthy operations, periodically call this to tell the
    // controller that the service is not dead.
    // Can be called only while run() is running.
    void bump();

    // Can be used to set the expected length of long operations.
    // Also does the bump.
    // Can be called only while run() is running.
    void hintTime(DWORD msec);

    // Methods for the subclasses to override.
    // The base class defaults set the completion state, so the subclasses must
    // either call them at the end of processing (maybe after some wait, maybe
    // from another thread) or do it themselves.
    // The pending states (where applicable) will be set before these methods
    // are called.
    // onStart() is responsible for actually starting the application
    virtual void onStart(
        __in DWORD argc,
        __in_ecount(argc) LPWSTR *argv);
    virtual void onStop(); // sets the success exit code
    virtual void onPause();
    virtual void onContinue();
    virtual void onShutdown(); // calls onStop()

};

The constructor contains the list of capabilities: what "on" virtual functions does the service support (the default implementations just report that the transition succeeded and do nothing, except for onShutdown() that goes one step lazier and by default just calls onStop()). Well, and obviously it always has to support onStart(). I'm not sure what would happen is the service sets canStop=false, I suppose the service controller will just kill its process without asking it to stop nicely.

Just to set the record straight, the "on" methods get called by the service controller code when it wants to change the state of the service in some way, and the subclass has to react to these virtual method calls  in whatever way it finds proper. All the "on" methods can spend some time doing the transitions but should not get stuck forever. The end of the transition is marked not by the return from these methods but by the call of setState methods. The simple example above sets the state changes directly from the "on" methods but you can do any kind of the complex asynchronous logic, calling the setState from various background threads.

run() is the method called by main() to run the whole service sequence, as shown in the example above.

The setState methods provide the ways to tell the controller what is going on within the service. There is the basic setState(), and the convenient wrappers for it setStateRunning() and setStatePaused(). Setting the stopped state is more complicated, since it needs to convey the service exit code. Two versions of it provide the way to set either a generic Windows exit code or a service-specific one. The setStateStopped() and setStateStoppedSpecific() also are wrappers around setState() but they also know how to update the exit code state before pushing it to the service controller with setState().

The final two methods bump() and hintTime() can be used when some transition takes a long time. bump() just tells the service controller "I'm busy but not dead yet",  hintTime() also gives an estimation of how much longer will it take to complete the transition. hintTime(0) uses the service controller's default timeout (normally about 20 minutes) and is the equivalent of bump().

Since there seems to be no easy way to add an attachment any more like in an older post about the error handling used here, the code of the implementation goes right here:

 /* ---------------------- SimpleService.hpp ---------------------- */
/*++
Copyright (c) 2016 Microsoft Corporation
--*/

#pragma once

// -------------------- Service---------------------------------
// The general wrapper for running as a service.
// The subclasses need to define their virtual methods.

class DLLEXPORT Service
{
public:
    // The way the services work, there can be only one Service object
    // in the process. 
    Service(const std::wstring &name,
        bool canStop,
        bool canShutdown,
        bool canPauseContinue);

    virtual ~Service();

    // Run the service. Returns after the service gets stopped.
    // When the Service object gets started,
    // it will remember the instance pointer in the instance_ static
    // member, and use it in the callbacks.
    // The errors are reported back in err.
    void run(Erref &err);

    // Change the service state. Don't use it for SERVICE_STOPPED,
    // do that through the special versions.
    // Can be called only while run() is running.
    void setState(DWORD state);
    // The convenience versions.
    void setStateRunning()
    {
        setState(SERVICE_RUNNING);
    }
    void setStatePaused()
    {
        setState(SERVICE_PAUSED);
    }
    // The stopping is more compilcated: it also sets the exit code.
    // Which can be either general or a service-specific error code.
    // The success indication is the general code NO_ERROR.
    // Can be called only while run() is running.
    void setStateStopped(DWORD exitCode);
    void setStateStoppedSpecific(DWORD exitCode);

    // On the lengthy operations, periodically call this to tell the
    // controller that the service is not dead.
    // Can be called only while run() is running.
    void bump();

    // Can be used to set the expected length of long operations.
    // Also does the bump.
    // Can be called only while run() is running.
    void hintTime(DWORD msec);

    // Methods for the subclasses to override.
    // The base class defaults set the completion state, so the subclasses must
    // either call them at the end of processing (maybe after some wait, maybe
    // from another thread) or do it themselves.
    // The pending states (where applicable) will be set before these methods
    // are called.
    // onStart() is responsible for actually starting the application
    virtual void onStart(
        __in DWORD argc,
        __in_ecount(argc) LPWSTR *argv);
    virtual void onStop(); // sets the success exit code
    virtual void onPause();
    virtual void onContinue();
    virtual void onShutdown(); // calls onStop()

protected:
    // The callback for the service start.
    static void WINAPI serviceMain(
        __in DWORD argc,
        __in_ecount(argc) LPWSTR *argv);
    // The callback for the requests.
    static void WINAPI serviceCtrlHandler(DWORD ctrl);

    // the internal version that expects the caller to already hold statusCr_
    void setStateL(DWORD state);

protected:
    static Service *instance_;

    std::wstring name_; // service name

    Critical statusCr_; // protects the status setting
    SERVICE_STATUS_HANDLE statusHandle_; // handle used to report the status
    SERVICE_STATUS status_; // the current status

    Critical errCr_; // protects the error handling
    Erref err_; // the collected errors

private:
    Service();
    Service(const Service &);
    void operator=(const Service &);
};

/* ---------------------- SimpleService.cpp ---------------------- */
/*++
Copyright (c) 2016 Microsoft Corporation
--*/

// ... use the right includes ...

static ErrorMsg::MuiSource ServiceErrorSource(L"Service", NULL);

// -------------------- Service---------------------------------

Service *Service::instance_;

Service::Service(const wstring &name,
    bool canStop,
    bool canShutdown,
    bool canPauseContinue
):
    name_(name), statusHandle_(NULL)
{

    // The service runs in its own process.
    status_.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

    // The service is starting.
    status_.dwCurrentState = SERVICE_START_PENDING;

    // The accepted commands of the service.
    status_.dwControlsAccepted = 0;
    if (canStop) 
        status_.dwControlsAccepted |= SERVICE_ACCEPT_STOP;
    if (canShutdown) 
        status_.dwControlsAccepted |= SERVICE_ACCEPT_SHUTDOWN;
    if (canPauseContinue) 
        status_.dwControlsAccepted |= SERVICE_ACCEPT_PAUSE_CONTINUE;

    status_.dwWin32ExitCode = NO_ERROR;
    status_.dwServiceSpecificExitCode = 0;
    status_.dwCheckPoint = 0;
    status_.dwWaitHint = 0;
}

Service::~Service()
{ }

void Service::run(Erref &err)
{
    err_.reset();
    instance_ = this;

    SERVICE_TABLE_ENTRY serviceTable[] = 
    {
        { (LPWSTR)name_.c_str(), serviceMain },
        { NULL, NULL }
    };

    if (!StartServiceCtrlDispatcher(serviceTable)) {
        err_ = ServiceErrorSource.mkMuiSystem(GetLastError(), EPEM_SERVICE_DISPATCHER_FAIL, name_.c_str());
    }

    err = err_.copy();
}

void WINAPI Service::serviceMain(
    __in DWORD argc,
    __in_ecount(argc) LPWSTR *argv)
{
    REAL_ASSERT(instance_ != NULL);

    // Register the handler function for the service
    instance_->statusHandle_ = RegisterServiceCtrlHandler(
        instance_->name_.c_str(), serviceCtrlHandler);
    if (instance_->statusHandle_ == NULL)
    {
        instance_->err_.append(ServiceErrorSource.mkMuiSystem(GetLastError(),
            EPEM_SERVICE_HANDLER_REGISTER_FAIL, instance_->name_.c_str()));
        instance_->setStateStoppedSpecific(EPEM_SERVICE_HANDLER_REGISTER_FAIL);
        return;
    }

    // Start the service.
    instance_->setState(SERVICE_START_PENDING);
    instance_->onStart(argc, argv);
}

void WINAPI Service::serviceCtrlHandler(DWORD ctrl)
{
    switch (ctrl)
    {
    case SERVICE_CONTROL_STOP:
        if (instance_->status_.dwControlsAccepted & SERVICE_ACCEPT_STOP) {
            instance_->setState(SERVICE_STOP_PENDING);
            instance_->onStop();
        }
        break;
    case SERVICE_CONTROL_PAUSE:
        if (instance_->status_.dwControlsAccepted & SERVICE_ACCEPT_PAUSE_CONTINUE) {
            instance_->setState(SERVICE_PAUSE_PENDING);
            instance_->onPause();
        }
        break;
    case SERVICE_CONTROL_CONTINUE:
        if (instance_->status_.dwControlsAccepted & SERVICE_ACCEPT_PAUSE_CONTINUE) {
            instance_->setState(SERVICE_CONTINUE_PENDING);
            instance_->onContinue();
        }
        break;
    case SERVICE_CONTROL_SHUTDOWN:
        if (instance_->status_.dwControlsAccepted & SERVICE_ACCEPT_SHUTDOWN) {
            instance_->setState(SERVICE_STOP_PENDING);
            instance_->onShutdown();
        }
        break;
    case SERVICE_CONTROL_INTERROGATE:
        SetServiceStatus(instance_->statusHandle_, &instance_->status_);
        break;
    default:
        break;
    }
}

void Service::setState(DWORD state)
{
    ScopeCritical sc(statusCr_);

    setStateL(state);
}

void Service::setStateL(DWORD state)
{
    status_.dwCurrentState = state;
    status_.dwCheckPoint = 0;
    status_.dwWaitHint = 0;
    SetServiceStatus(statusHandle_, &status_);
}

void Service::setStateStopped(DWORD exitCode)
{
    ScopeCritical sc(statusCr_);

    status_.dwWin32ExitCode = exitCode;
    setStateL(SERVICE_STOPPED);
}

void Service::setStateStoppedSpecific(DWORD exitCode)
{
    ScopeCritical sc(statusCr_);

    status_.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
    status_.dwServiceSpecificExitCode = exitCode;
    setStateL(SERVICE_STOPPED);
}

void Service::bump()
{
    ScopeCritical sc(statusCr_);

    ++status_.dwCheckPoint;
    ::SetServiceStatus(statusHandle_, &status_);
}

void Service::hintTime(DWORD msec)
{
    ScopeCritical sc(statusCr_);

    ++status_.dwCheckPoint;
    status_.dwWaitHint = msec;
    ::SetServiceStatus(statusHandle_, &status_);
    status_.dwWaitHint = 0; // won't apply after the next update
}

void Service::onStart(
    __in DWORD argc,
    __in_ecount(argc) LPWSTR *argv)
{
    setState(SERVICE_RUNNING);
}
void Service::onStop()
{
    setStateStopped(NO_ERROR);
}
void Service::onPause()
{
    setState(SERVICE_PAUSED);
}
void Service::onContinue()
{
    setState(SERVICE_RUNNING);
}
void Service::onShutdown()
{
    onStop();
}

Comments

  • Anonymous
    January 07, 2017
    Or you could keep your app as an executable and wrap it with nssm https://nssm.cc to launch and control it as a service.Very easy and can avoid win specific code in a cross platform exe.Nice explanation about the implementation details though.
    • Anonymous
      January 09, 2017
      Ha, I didn't realize at first about which post this reply was for. I have another post that describes a wrapper that runs an arbitrary binary as a service. Thanks, I wasn't aware of NSSM. For all I can tell, it does pretty much the same thing.