MFC Application: Adding Service Mode Support

Introduction

Quite often an application has an opportunity to extend its existence on the market but must first meet new requirements, such as being able to run service mode. Imagine that you have developed a huge MFC based server application for years. You've already put a great deal of effort into teaching it various tricks and features with a lot of useful functionality but missed one point during all the years of your product's life; your app is able to run in interactive user session only. Now, your customers are no longer allowed to have endless interactive sessions, therefore they want 24/7, they want remote system reboots with your server to start automatically, without any administrator intervention.

What options do you have now? First, you can fully rewrite your server using some framework or programming language, like C#, that provides the ability to run the required mode. Full rewrite; you think--disaster.

Second, you can split your server into a number of elementary libraries, and reuse those in your new service project. A little better, you think, but not good enough.

Third, you can make your current app to be launched in service session, with tools like the well-known SRVANY, which never makes your app to be a real service...

I have to confess here. Some time ago I was in a situation exactly like this. None of the above appeared to be a viable alternative, as I wanted to make my server become a dual mode app able to run both interactive and service modes. I'm going to give you an idea of the enhancement.

The Server as it Now is

To start this tutorial with something material, let's see how a simple MFC server might perform a simple function running interactive mode. The function will be logging timestamps to a file with 1 second intervals. This means no rich GUI at all, to let us focus on the server part only.

The server starts, spawns a worker thread that performs iterative logging to the file. Closing the main server window results in stopping the worker thread with further application closure.

#include <afxwin.h>
#include <atltime.h>
#include <share.h>
#include <process.h>

CRITICAL_SECTION g_csLog;
CString g_log;

struct LogInit
{
    LogInit() { InitializeCriticalSection(&g_csLog); }
    ~LogInit() { DeleteCriticalSection(&g_csLog); }
} logInit;

void PutLog(LPCTSTR entry)
{
    EnterCriticalSection(&g_csLog);
    FILE* f = _tfsopen(g_log, TEXT("at+"), _SH_DENYWR);
    if (f)
        _ftprintf(f, TEXT("%s\n"), entry);
    fclose(f);
    LeaveCriticalSection(&g_csLog);
}

class CMainFrame: public CFrameWnd
{
    HANDLE  m_hStop;
    HANDLE  m_hWorker;

    DECLARE_MESSAGE_MAP()

public:
    CMainFrame(): 
        m_hStop(NULL), 
        m_hWorker(NULL)
    {}

private:
    static UINT WINAPI WorkerThread(LPVOID pVoid)
    {
        CMainFrame* pThis = (CMainFrame*)pVoid;
        if (pThis)
            _endthreadex(pThis->Worker());
        return 0;
    }

    DWORD Worker()
    {
        HANDLE hStop = m_hStop;
        PutLog(TEXT("\n++ Worker started ++"));
        BOOL loop = TRUE;
        while (loop)
        {
            DWORD waitRes = WaitForSingleObject(hStop, 1000);
            switch (waitRes)
            {
            case WAIT_OBJECT_0:
                PutLog(TEXT("Worker stop requested"));
                loop = FALSE;
                break;

            case WAIT_TIMEOUT:
                PutLog(CTime::GetCurrentTime().Format(TEXT("%#c")));
                break;
            }   
        }
        PutLog(TEXT("-- Worker finished --\n"));
        return 0;
    }

    BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pcx)
    {
        TCHAR path[MAX_PATH] = {0};
        if (GetModuleFileName(NULL, path, MAX_PATH))
        {
            g_log = path;
            g_log += TEXT(".log");
        }

        m_hStop = CreateEvent(NULL, TRUE, FALSE, NULL);

        UINT threadId;
        m_hWorker = (HANDLE)_beginthreadex(NULL, 0, WorkerThread, this, 0, &threadId);

        return TRUE;
    }

    void CloseWorker()
    {
        SetEvent(m_hStop);
        WaitForSingleObject(m_hWorker, INFINITE);
    }

    void OnClose()
    {
        PutLog(TEXT("Closing..."));
        CloseWorker();
        CWnd::OnClose();
    }

};

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

class CMyApp: public CWinApp
{
    BOOL InitInstance()
    {
        CMainFrame* pFr = new CMainFrame;
        if (!pFr->Create(NULL, TEXT("EasyStart")))
        {
            return FALSE;
        }

        m_pMainWnd = pFr;
        pFr->ShowWindow(SW_SHOW);
        pFr->UpdateWindow();
        return TRUE;
    }
};

CMyApp theApp;

The log produced by the server: (to be compared with the service version later).

++ Worker started ++
Sunday, January 22, 2012 18:28:27
Sunday, January 22, 2012 18:28:28
Sunday, January 22, 2012 18:28:29
Closing...
Worker stop requested
-- Worker finished --

The code above seems pretty simple. The application class creates the frame instance. The frame class creates the worker thread and signals it to stop when it gets the WM_CLOSE message. The worker thread waits 1 second to put another log entry. Trivial. But great for our purpose nevertheless. Now it's time to turn it into a service.

MFC Application: Adding Service Mode Support

Missing Parts

The frame class is going to host all of the service stuff, so it will be enhanced step by step.

The service name must be registered along with ServiceMain procedure. Additionally, the service status structure, along with service status handle, provides the ability to control and report service status changes to SCM.

To become a service, i.e. a process connected to SCM and controlled by the one, the application needs to call StartServiceCtrlDispatcher on its start. Bad luck, the call blocks current thread until service shutdown. The MSDN article says:

Connects the main thread of a service process to the service control manager, which causes the thread to be the service control dispatcher thread for the calling process.

'Okay, main thread,' you think. 'But my main thread pumps all the message stuff! It must not be blocked!' Fortunately, the main service thread is not the same as the main GUI thread. It's just the thread that calls StartServiceCtrlDispatcher. Great.

StartServiceCtrlDispatcher results in SCM calling ServiceMain in a separate thread. In its turn ServiceMain registers the HandlerEx routine that will be responsible for processing control codes coming from SCM. In fact, it implements service reaction to outer control commands.

So, to control the service life cycle, CMainFrame class needs the following:

class CMainFrame: public CFrameWnd
{
. . .
    // Service stuff

    static CString svcName;
    static SERVICE_STATUS m_status;
    static SERVICE_STATUS_HANDLE m_hStatus;

    static UINT  WINAPI ServiceThread(LPVOID pVoid);
    static VOID  WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
    static DWORD WINAPI HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);

    BOOL InitService();
    BOOL ShutService();
};

UINT WINAPI CMainFrame::ServiceThread(LPVOID pVoid)
{
    CMainFrame* pThis = (CMainFrame*)pVoid;

    PutLog(TEXT(__FUNCTION__));

    CString svcName = pThis->svcName;
    SERVICE_TABLE_ENTRY svcTable[] = 
    {
        { (LPTSTR)svcName.GetString(),  ServiceMain },
        { NULL, NULL }
    };

    PutLog(svcName);

    BOOL res = ::StartServiceCtrlDispatcher(svcTable);

    PutLog(TEXT("Shutdown"));

    // Service stopped, so close main GUI and worker
    pThis->SendMessage(WM_CLOSE);

    _endthreadex(0);
    return 0;
}

VOID WINAPI CMainFrame::ServiceMain(DWORD argc, LPTSTR* argv)
{
    CMainFrame* pThis = (CMainFrame*)AfxGetMainWnd();
    ZeroMemory(&pThis->m_status, sizeof(pThis->m_status));
    pThis->m_status.dwCurrentState = SERVICE_START_PENDING;
    pThis->m_status.dwServiceType  = SERVICE_WIN32_OWN_PROCESS;
    pThis->m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
    pThis->m_status.dwWaitHint = 1000;
    pThis->m_status.dwCheckPoint = 1;

    pThis->m_hStatus = 
        ::RegisterServiceCtrlHandlerEx(svcName, CMainFrame::HandlerEx, (LPVOID)pThis);

    if (!pThis->m_hStatus)
    {
        PutLog(TEXT("RegisterServiceCtrlHandlerEx error"));
        exit(1);
    }

    BOOL retCode = pThis->InitService();
    if (!retCode)
    {
        pThis->m_status.dwCurrentState = SERVICE_STOPPED;
        PutLog(TEXT("Initialization failed"));
        ::SetServiceStatus(pThis->m_hStatus, &pThis->m_status);
        return;
    }

    pThis->m_status.dwCurrentState = SERVICE_RUNNING;
    ::SetServiceStatus(pThis->m_hStatus, &pThis->m_status);
    PutLog(TEXT("Start succeeded"));

}

DWORD WINAPI CMainFrame::HandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    DWORD retCode = ERROR_CALL_NOT_IMPLEMENTED;
    CMainFrame* pThis = (CMainFrame*)lpContext;
/*
    if (128 <= dwControl)
        return pThis->HandlerCustom(dwControl);
*/
    switch (dwControl)
    {
    case SERVICE_CONTROL_STOP:
    case SERVICE_CONTROL_SHUTDOWN:
        PutLog(TEXT("Shutdown requested"));
        pThis->ShutService();
        pThis->m_status.dwCurrentState = SERVICE_STOPPED;
        ::SetServiceStatus(pThis->m_hStatus, &pThis->m_status);
        retCode = NO_ERROR;
        break;
    }

    return retCode;
}

BOOL CMainFrame::InitService()
{
    return TRUE;
}

BOOL CMainFrame::ShutService()
{
    return TRUE;
}

CString               CMainFrame::svcName   = TEXT("MFCapp.EasyStart.Dual");
SERVICE_STATUS        CMainFrame::m_status  = {0};
SERVICE_STATUS_HANDLE CMainFrame::m_hStatus = NULL;

Well, this is all that CMainFrame needs--except one thing. Remember, the intention was to support both interactive and service modes. So, the frame needs something that would tell it: 'You're running service mode.'

Here CMyApp class goes. To distinguish between the modes the app class analyses command arguments. In case -service or /service is provided, the application will instruct the frame class to run service mode.

class CMyApp: public CWinApp
{
. . .
    // Service stuff

    BOOL IsServiceMode()
    {
        int argc = 0;
        LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
        if (!argc)
            exit(1);

        for (int idx = 1; idx < argc; idx++)
        {
            CStringW arg = argv[idx];
            if (L'-' != arg.GetAt(0) && L'/' != arg.GetAt(0))
                continue;
            if (0 == arg.Mid(1).CompareNoCase(L"service"))
                return TRUE;
        }
        return FALSE;
    }


};

Now it's time to glue it all together...

MFC Application: Adding Service Mode Support

Integrating the Parts

Please don't be confused about dummy InitService and ShutService members. The functions just give you an idea of where to control your specific resources. In case you never need that, you are free to remove that stuff, or leave the dummies as they are.

CMyApp needs a little change in InitInstance. This is where it instructs CMainFrame which mode to run:

class CMyApp: public CWinApp
{
    BOOL InitInstance()
    {
        CMainFrame* pFr = new CMainFrame(IsServiceMode());
        . . .
    }

In its turn CMainFrame needs a few modifications. It must store service mode flag, and on the flag being set it must run service thread as well.

class CMainFrame: public CFrameWnd
{
. . .
    BOOL    m_serviceMode;

public:
    CMainFrame(BOOL serviceMode): 
        m_hStop(NULL), 
        m_hWorker(NULL), 
        m_serviceMode(serviceMode) 
    {}

    BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pcx)
    {
        . . .
        if (m_serviceMode)
        {
            HANDLE hThreadSvc = (HANDLE)_beginthreadex(NULL, 0, ServiceThread, this, 0, &threadId);
            CloseHandle(hThreadSvc);
        }

        return TRUE;
    }

Now everything is in place. To build the app you can run make.bat in Visual Studio command prompt window. To register the service you run the following command. (Windows Vista/7 users must remember, service registration requires elevated rights.)

sc create MFCapp.EasyStart.Dual binPath= "<full path>\MFCapp.EasyStart.Dual.exe /service"

Note: All spaces in the command matter.

Running service mode is easily noticeable from the produced log:

++ Worker started ++
CMainFrame::ServiceThread
MFCapp.EasyStart.Dual
Start succeeded
Sunday, January 22, 2012 18:38:26
Sunday, January 22, 2012 18:38:27
Sunday, January 22, 2012 18:38:28
Sunday, January 22, 2012 18:38:29
Sunday, January 22, 2012 18:38:30
Sunday, January 22, 2012 18:38:31
Shutdown requested
Shutdown
Closing...
Worker stop requested
-- Worker finished --

Both versions, original and enhanced, are attached to this article. You're free to modify the code to play with the concept a bit, or use it in your projects with no limitation.

Note:

A little warning here is mandatory. The code should work okay for servers that are able to start/stop in 30 seconds. This is the interval SCM agrees to wait until your process confirms normal start (SERVICE_RUNNING) or shutdown (SERVICE_STOPPED). Once the time is exceeded, the application is terminated with no mercy.

Specifically this relates to InitService and ShutService functions that must not take more than 30 seconds to finish. Maybe we'll continue with solving this problem, at a later date...



Related Articles

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • When it comes to desktops – physical or virtual – it's all about the applications. Cloud-hosted virtual desktops are growing fast because you get local data center-class security and 24x7 access with the complete personalization and flexibility of your own desktop. Organizations make five common mistakes when it comes to planning and implementing their application management strategy. This eBook tells you what they are and how to avoid them, and offers real-life case studies on customers who didn't …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds