//
//  Copyright © 2001 - 2002, RESON Inc. All Rights Reserved.
//
//  No part of this file may be reproduced or transmitted in any form or by
//  any means, electronic or mechanical, including photocopy, recording, or
//  information storage or retrieval system, without permission in writing
//  from RESON Inc.
//
//  Filename:   6046.cpp
//
//  Project:    6046.
//
//  Author(s):  W. Arcus    - Extensive rework for Thales and MBARI AUV projects and general
//                            clean up for extensibility and encapsulation.
//                          - Most of Patrik's initial code moved to CLogger class.
//
//  Purpose:    Defines the main service module and support routines. Defines also the 
//              console application.
//
//  Notes:      

#pragma warning( disable : 4786 )       // Disable this little gem from Microsoft. Keep
                                        // before the first header is included.
#include "StdAfx.h"
#include "6046.h"

#include "Instance.h"
#include "Service.h"
#include "ErrorMsg.h"
#include "Logger.h"

#include <afxsock.h>

#ifdef _DEBUG
#pragma comment( lib, "..\\Utils\\NetUtils\\Debug\\NetUtils.lib" )
#else
#pragma comment( lib, "..\\Utils\\NetUtils\\Release\\NetUtils.lib" )
#endif

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

////////////////////////////////////////////////////////////////////
// Service Control Manager (SCM) interface function definitions -- must have C linkage.

extern "C"  int                 main                            (   int         argc,
                                                                    char       *argv[] );

extern "C"  void    WINAPI      ServiceMain                     (   DWORD       dwArgc,
                                                                    LPTSTR     *lpArgv );

extern "C"  void    WINAPI      ControlHandler                  (   DWORD       dwCommand );        // SCM's contol handler (callback).

extern "C"  BOOL    WINAPI      ConsoleCtrlHandler              (   DWORD       dwEventType );      // Console's control handler (callback).

////////////////////////////////////////////////////////////////////
// Internal helper function prototypes.

static DWORD                    StartServer                     (   void );


static void                     StopServer                      (   DWORD       dwErrorCode,
                                                                    LPTSTR      lpszErrorMsg );

static void                     ReportEvent                     (   DWORD       dwErrorCode,
                                                                    LPTSTR      lpszErrorMsg,
                                                                    BOOL        bError );

static BOOL                     CreateGlobalComponents          (   void );

static void                     DestroyGlobalComponents         (   void );

static DWORD                    WaitForTermination              (   DWORD       dwWaitPeriod );

static void                     TerminateService                (   void );

static void                     InitiateDataLogger              (   void );

static void                     InstallService                  (   void );

static void                     RemoveService                   (   void );

static void                     NotifySecondInstanceRunning     (   void );

static void                     NotifyEventLogOfAttemptToStart  (   const bool &rbConsoleMode );

using std::auto_ptr;

namespace                                                               // Begin unnamed namespace.
{
    ////////////////////////////////////////////////////////////////////
    // Constants.

    const LPTSTR lpszServiceName_c          = _T( "6046 PLC" );
    const LPTSTR lpszProgramName_c          = _T( "6046.exe" );
    const LPTSTR lpszTerminationEventName_c = _T( "6046_DATA_LOGGER_TERMINATE" );
    const LPTSTR lpszServiceDescription_c   = _T( "RESON 6046 Payload Controller" );

    //////////////////////////////////////////////////////////////////////////////
    // Module scope objects.

    CWinApp                     g_MyApp;                                // Our dummy MFC app.
    CInstance                   g_Instance( lpszServiceName_c );
    CConsole                    g_Console( false );
                        
    CComponentService          *g_pThisComponent    = NULL;             // NT service support class.
                        
    DWORD                       g_dwLastError       = NO_ERROR;         // Last know error code if any.
    HANDLE                      g_hTerminationEvent = NULL;             // Termination event.

}                                                                       // end unnamed namespace.

////////////////////////////////////////////////////////////////////
// Function implementations.

extern "C"
int main( int argc, char *argv[] )
{
    // Explicitly initialize MFC and windows sockets.

    if ( ! AfxWinInit( GetModuleHandle( NULL ), NULL, GetCommandLine(), 0 ) )
    {
        TRACE( _T( "Failure to initialize MFC.\n" ) );
        return -1;
    }

    if ( ! AfxSocketInit() )
    {
        TRACE( _T( "Failure to initialize WinSock.\n" ) );
        return -1;
    }

    // Assume it's a service initially.

    g_Console.ConsoleProgram( false );

    // Check for command line directives.

    for ( int iParam = 1; iParam < argc; iParam++ )
    {
        if ( ( argv[ iParam ] [ 0 ] == '-' ) || 
             ( argv[ iParam ] [ 0 ] == '/' )  )
        {
            switch ( argv[ iParam ][ 1 ] )
            {
                case 'c':                   // It's a console program
                case 'C':

                    g_Console.ConsoleProgram( true );
                    break;

                case 'i':
                case 'I':

                    g_Console.ConsoleProgram( true );
                    InstallService();
                    return 0;
                    break;

                case 'r':
                case 'R':

                    g_Console.ConsoleProgram( true );
                    RemoveService();
                    return 0;
                    break;

                case 'h':                   // Usage info requested.
                case 'H':
                case '?':
                default:

                    printf( "Usage: 6046 [-c | -i | -r]\n" );
                    printf( "           c => runs the Payload Controller as a console program\n"
                            "\t\t\totherwise service is assumed.\n" );
                    printf( "           i => installs program as a NT service.\n" );
                    printf( "           r => stops (if necessary) then removes service.\n" );
                    return 0;
            }
        }
    }

    TRACE( _T( "In service main()\n" ) );

    // Allow only one instance of this application to run per host.

    if ( ! g_Instance.IsFirst() )
    {
        NotifySecondInstanceRunning();
        return -1;
    }

    TRACE( _T( "Starting...\n" ) );

    if ( ! CreateGlobalComponents() )
    {
        TRACE( _T( "Failed to create internal objects\n" ) );
        DestroyGlobalComponents();
        return -1;
    }

    // Next we need to treat services and console programs differently since a service
    // executes within the context of NT's Serice Control Manager (SCM) (via callback).

    NotifyEventLogOfAttemptToStart( g_Console.IsConsoleProgram() );

    if ( g_Console.IsConsoleProgram() )
    {
        StartServer();                                  // Start the server and block until done.
        DestroyGlobalComponents();
    }
    else
    {
        // Setup our service entry table.

        SERVICE_TABLE_ENTRY ServiceTable[] =
            {
                { lpszServiceName_c,    ServiceMain },
                { NULL,                 NULL }
            };

        TRACE( _T( "Starting service: %s\n" ), lpszServiceName_c );

        // Tell NT to start our service (implemented in ServiceMain()).

        if ( ! StartServiceCtrlDispatcher( ServiceTable ) )
        {
            StopServer( GetLastError(), _T( "CService::Start()" ) );
        }

        TRACE( _T( "Exiting main(), return code: %d\n" ), g_dwLastError );
    }

    return 0;
}

extern "C"
void WINAPI ServiceMain( DWORD dwArgc, LPTSTR *lpArgv )
{
    UNREFERENCED_PARAMETER( dwArgc );
    UNREFERENCED_PARAMETER( lpArgv );

    try
    {
        LPTSTR  lpFailedFunction = NULL;
        BOOL    bSuccess         = TRUE;

        TRACE( _T( "Executing ServiceMain()\n" ) );

        try
        {
            // Register the control handler.

            if ( ! g_pThisComponent->RegisterHandler( lpszServiceName_c, ControlHandler ) )
            {
                throw _T( "CComponentService::RegisterHandler()" );
            }

            // Tell the SCM we are starting up by reporting status as SERVICE_START_PENDING.

            if ( ! g_pThisComponent->ReportStatusChange( SERVICE_START_PENDING, 0 ) )
            {
                throw _T( "SetServiceStatus()" );
            }

            // Tell NT that we're up and running and that we'll accept the stop and system
            // shutdown control commands.

            if ( ! g_pThisComponent->ReportStatusChange( SERVICE_RUNNING, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN ) )
            {
                throw _T( "SetServiceStatus()" );
            }

            // Note that StartServer(), if successfull, will block until the dispatcher object 
            // is shutdown either upon receipt of a stop or system shutdown message from the SCM. At which time
            // the g_hTerminationEvent object will be set and then this thread unblocked and subsequently 
            // terminated.

            StartServer();                  // Now, start up the server and block this thread until done.
        }
        catch ( LPTSTR lpszErrorMessage )
        {
            bSuccess = FALSE;
            lpFailedFunction = lpszErrorMessage;
        }
        catch (...)
        {
            bSuccess = FALSE;
        }

        // We're about to terminate so clean up. We've either unblocked and terminated our threads
        // in response to a shutdown or stop message or we've had an initialisation failure.

        // Terminate the server if its a normal termination.

        if ( ! bSuccess )
        {
            StopServer( g_dwLastError, lpFailedFunction );
        }

        //g_pThisComponent->ReportStatusChange( SERVICE_STOPPED, 0, 0, g_dwLastError );

        DestroyGlobalComponents();
    }
    catch ( ... )
    {
        TRACE( _T( "6046 ServiceMain(), Unspecified exception caught\n" ) );
    }
}

extern "C" 
void WINAPI ControlHandler( DWORD dwCommand )
{
    // Handles SCM messages we are interested in.

    switch ( dwCommand )
    {
        case SERVICE_CONTROL_STOP:
        case SERVICE_CONTROL_SHUTDOWN:

            TRACE( _T( "Service received a STOP or SHUTDOWN message\n" ) );

            g_pThisComponent->ReportStatusChange( SERVICE_STOP_PENDING, 0, 3000 );

            // Signal our main worker thread and subordinates to terminate.

            TerminateService();

            StopServer( 0, NULL );

            break;
    }

    g_pThisComponent->UpdateStatus();
}

void StopServer( DWORD dwErrorCode, LPTSTR lpszErrorMsg )
{
    g_dwLastError = dwErrorCode;

    if ( dwErrorCode != NO_ERROR )
    {
        ReportEvent( dwErrorCode, lpszErrorMsg, TRUE );
    }
}

DWORD StartServer( void )
{
    // Main body of this process. Essentially, we kick off the required thread(s) and 
    // then block until a termination event (g_hTerminationEvent) is set.

    DWORD dwError = NO_ERROR;

    // The following are pointers to our three core RPC server objects for distributed
    // client components.

    try
    {
        InitiateDataLogger();
    }
    catch ( LPCTSTR lpszErrorMsg )
    {
        TRACE( _T( "StartServer(), %s\n" ), lpszErrorMsg );
    }
    catch ( ... )
    {
        TRACE( _T( "StartServer(), Unspecified exception caught\n" ) );
    }

    return dwError;
}

void ReportEvent(   DWORD   dwErrorCode,
                    LPTSTR  lpszErrorMsg,
                    BOOL    bError )
{
    HANDLE  hSource;
    TCHAR   szLastErrorMsg      [ 1024 ];
    LPTSTR  lpszMessageStrings  []          = { lpszErrorMsg, szLastErrorMsg };
    LPCTSTR lpszMachineName                 = NULL;     // NULL = local machine, otherwise UNC name (ie. "\\<machine>\<share>" )

    // Look up message string for system error.

    FormatMessage(  FORMAT_MESSAGE_FROM_SYSTEM,
                        NULL,
                            dwErrorCode,
                                LANG_USER_DEFAULT,
                                szLastErrorMsg,
                                    sizeof( szLastErrorMsg ),
                                        NULL );

    if ( ( hSource = RegisterEventSource( lpszMachineName, lpszServiceName_c ) ) != NULL )
    {
        // The lpszMessageStrings array that we pass to ReportEvent() has the 
        // insertion sequences for our message.

        ReportEvent(    hSource,
                            (unsigned short) (  bError ?    EVENTLOG_ERROR_TYPE :       // Severity.
                                                EVENTLOG_WARNING_TYPE ),
                                (unsigned short) 0,                                     // Category, not used.
                                    (unsigned long ) MSG_SERVER_FAILED,                 // Message identifier.
                                        NULL,                                           // Security ID, not used.
                                        (unsigned short) 2,                             // Number of insertion strings in lpszMessageStrings array.
                                            0UL,                                        // Bytes of binary data to include.
                                                (LPCTSTR *) lpszMessageStrings,         // Messages to log.
                                                    NULL );                             // Binary data to log, not used.

        DeregisterEventSource( hSource );
    }
}

BOOL CreateGlobalComponents( void )
{
    BOOL bSuccess = FALSE;

    if ( ( g_hTerminationEvent = CreateEvent( NULL, TRUE, FALSE, lpszTerminationEventName_c ) ) == NULL )
    {
        bSuccess = FALSE;
    }
    else if ( g_Console.IsConsoleProgram() )
    {
        bSuccess = TRUE;
    }
    else
    {
        bSuccess = ( ( g_pThisComponent = new CComponentService() ) != NULL );
    }

    return bSuccess;
}

void DestroyGlobalComponents( void )
{
    if  ( g_hTerminationEvent != NULL )
    {
        CloseHandle( g_hTerminationEvent );
        g_hTerminationEvent = NULL;
    }

    if ( g_pThisComponent != NULL )
    {
        g_pThisComponent->ReportStatusChange( SERVICE_STOPPED, 0, 0, g_dwLastError );
        delete g_pThisComponent;
        g_pThisComponent = NULL;
    }
}

//////////////////////////////////////////////////////////////////////////////
// Helper functions.

DWORD WaitForTermination( DWORD dwWaitPeriod )
{
    // Block waiting for our termination event to be set.
    
    // In the case of a service, this will be through the SCM (for now). When running 
    // as a console program or general testing etc, the kill utility may be used to 
    // set the named event.

    TRACE( _T( "Waiting for termination...\n" ) );

    DWORD dwStatus = WaitForSingleObject( g_hTerminationEvent, dwWaitPeriod );

    TRACE( _T( "Termination request detected\n" ) );

    return dwStatus;
}

void TerminateService( void )
{
    // Use SetEvent since the main service thread may not be at the blocking point 
    // when this event is signalled and we don't want to miss it.

    SetEvent( g_hTerminationEvent );
}

void InitiateDataLogger( void )
{
    // Initiates our main threaded worker class and then blocks this thread until
    // a termination event is set in one of the following manners. Note that the logger object
    // is internally threaded; its destructor initiates its internal worker thread shutdown.
    //
    //      a)  Buy using the Kill.exe utility.
    //      b)  Callback from the SCM's control handler.
    //      c)  Callback from the console's control handler.
    //      d)  Callback from the logger object as a result of a remote shutdown request.

    try
    {
        g_Console.Print( "The Payload Controller is about to start...\n" );

        if ( g_Console.IsConsoleProgram() )
        {
            // For console mode, install the control handler to be able to set the termination event when
            // a console termination event occurs.

            if ( ! SetConsoleCtrlHandler( ConsoleCtrlHandler, TRUE ) )
            {
                g_Console.Print( "The Payload Controller has failed to set it's control handler (error code: %lu).\n", GetLastError() );
            }
        }

        // Instantiate our main PLC object. Note: because it's a auto_ptr so destruction is handled automatically
        // when it goes out of scope (even if an exception occurs).

        auto_ptr<CLogger> pLogger( new CLogger( TerminateService ) );

        ASSERT( pLogger.get() != NULL );

        if ( pLogger.get() == NULL )
        {
            g_Console.Print( "The Payload Controller failed to start.\n" );
        }
        else if ( pLogger->IsInitialized() )
        {
                g_Console.Print( "The Payload Controller has started... waiting for termination.\n" );

                // Block this thread until we receive a termination event.

                ::WaitForTermination( INFINITE );
        }
        else
        {
            g_Console.Print( "The Payload Controller has failed to initialize.\n" );
        }
    }
    catch ( ... )
    {
        g_Console.Print( "The Payload Controller has detected an unspecified error.\n" );
    }

    g_Console.Print( "The Payload Controller has terminated.\n" );
}

void InstallService( void )
{
    RemoveService();

    SC_HANDLE       hService,
                    hServiceControlManager;

    CHAR            szServiceExecutable [ MAX_PATH + 100 ],
                    szDefaultDirectory  [ MAX_PATH + 10  ];

    LPSTR           lpszDirectory        = szDefaultDirectory;

    OSVERSIONINFO   sOSVersion;

    // Only NT supported currently.

    sOSVersion.dwOSVersionInfoSize = sizeof ( sOSVersion );
    GetVersionEx( &sOSVersion );

    if ( sOSVersion.dwPlatformId != VER_PLATFORM_WIN32_NT )
    {
        g_Console.Print( "Only Windows NT or later are currently supported.\n" );
        return;
    }

    // Get the default directory name.

    GetModuleFileName( NULL, szDefaultDirectory, sizeof ( szDefaultDirectory ) );

    // Remove file name component

    char *pFileName = strrchr( szDefaultDirectory, '\\' );

    if ( pFileName != NULL )
    {
        *pFileName = '\0';
    }

    sprintf( szServiceExecutable, "%s\\%s", lpszDirectory, lpszProgramName_c );

    // Install the Register RPC Server in the Service Control Manager's database.

    hServiceControlManager = OpenSCManager( NULL, _T( "ServicesActive" ), SC_MANAGER_ALL_ACCESS );

    if ( hServiceControlManager != NULL )
    {
        hService = CreateService( hServiceControlManager,
                                    lpszServiceName_c,
                                        lpszServiceName_c,
                                            SERVICE_ALL_ACCESS,
                                                SERVICE_WIN32_OWN_PROCESS,
                                                    SERVICE_DEMAND_START,       //SERVICE_AUTO_START,
                                                        SERVICE_ERROR_IGNORE,   //SERVICE_ERROR_NORMAL,
                                                            szServiceExecutable,
                                                                NULL, NULL, NULL, NULL, NULL );

        if ( hService != NULL )
        {
            SERVICE_DESCRIPTION sDescription;

            sDescription.lpDescription = lpszServiceDescription_c;

            if ( ! ChangeServiceConfig2( hService, SERVICE_CONFIG_DESCRIPTION, &sDescription ) )
            {
                g_Console.Print( "Unable to set service description.\n" );
            }

            CloseServiceHandle( hService );

            g_Console.Print( "Service successfully installed.\n");
        }
        else
        {
            g_Console.Print( "Unable to install service (error code: %d).\n", GetLastError());
        }

        CloseServiceHandle( hServiceControlManager );
    }
    else
    {
        g_Console.Print( "Unable to access SCM (error code: %d).\n", GetLastError());
    }
}

void RemoveService( void )
{
    SC_HANDLE       hService,
                    hServiceControlManager;

    CHAR            szServiceExecutable [ MAX_PATH + 100 ],
                    szDefaultDirectory  [ MAX_PATH + 10  ];

    OSVERSIONINFO   sOSVersion;

    // Only NT supported currently.

    sOSVersion.dwOSVersionInfoSize = sizeof ( sOSVersion );
    GetVersionEx( &sOSVersion );

    if ( sOSVersion.dwPlatformId != VER_PLATFORM_WIN32_NT )
    {
        g_Console.Print( "Only NT is currently supported.\n" );
        return;
    }

    // First, terminate the service, if it's running.

    TerminateService();

    // Get the default directory name.

    GetModuleFileName( NULL, szDefaultDirectory, sizeof ( szDefaultDirectory ) );

    // Remove file name component

    char *pFileName = strrchr( szDefaultDirectory, '\\' );

    if ( pFileName != NULL )
    {
        *pFileName = '\0';
    }

    sprintf( szServiceExecutable, "%s\\%s", szDefaultDirectory, lpszProgramName_c );

    // Install the Register RPC Server in the Service Control Manager's database.

    hServiceControlManager = OpenSCManager( NULL, _T( "ServicesActive" ), SC_MANAGER_ALL_ACCESS );

    if ( hServiceControlManager != NULL )
    {
        hService = OpenService( hServiceControlManager, lpszServiceName_c, SERVICE_ALL_ACCESS );

        if ( hService != NULL )
        {
            SERVICE_STATUS ServiceStatus;
            DWORD          dwCheckPoint = 0xffffffff;
            DWORD          dwStartTime  = GetTickCount();
         
            // Stop it by sending it a STOP command

            if ( ControlService( hService, SERVICE_CONTROL_STOP, &ServiceStatus ) )
            {
                while ( ( ServiceStatus.dwCurrentState != SERVICE_STOPPED ) &&
                        ( ( GetTickCount() - dwStartTime ) < 10000 ) )
                {
                    // Give up--check point hasn't been incremented

                    if ( dwCheckPoint == ServiceStatus.dwCheckPoint )
                    {
                        break;
                    }

                    dwCheckPoint = ServiceStatus.dwCheckPoint;
                    Sleep( ServiceStatus.dwWaitHint );
                    QueryServiceStatus( hService, &ServiceStatus );
                }

                if ( ServiceStatus.dwCurrentState == SERVICE_STOPPED )
                {
                    DeleteService( hService );
                }
            }
            else
            {
                DeleteService( hService );
            }

            CloseServiceHandle( hService );
        }

        CloseServiceHandle( hServiceControlManager );
    }
    else
    {
        g_Console.Print( "Unable to access SCM (error code: %d).\n", GetLastError());
    }
}

void NotifySecondInstanceRunning( void )
{
    try
    {
        CString AppPath;
        CString Msg;

        Msg.Format( _T( "Another instance of %s is running -- shutting down!" ), lpszServiceName_c );

        if ( CLogger::SetApplicationPath( AppPath ) )
        {
            CSurveyEventLog SurveyEventLog;

            SurveyEventLog.Start( AppPath );

            SurveyEventLog.ReportEvent( eventLogAdvisory,
                                            iUnspecified_c,
                                                iUnspecified_c,
                                                    const_cast<char *>( static_cast<LPCTSTR>( Msg ) ) );

            SurveyEventLog.Stop();
        }

        if ( g_Console.IsConsoleProgram() )
        {
            Msg += _T( "\n" );
            g_Console.Print( const_cast<char *>( static_cast<LPCTSTR>( Msg ) ) );
            Sleep( 2000 );
        }
    }
    catch ( ... )
    {
        TRACE( _T( "NotifySecondInstanceRunning(), Unspecified exception caught\n" ) );
    }
}

void NotifyEventLogOfAttemptToStart( const bool &rbConsoleMode )
{
    CString AppPath;
    CString Msg;

    Msg.Format( "Attempting to start %s %s.", lpszServiceName_c, ( ( rbConsoleMode ) ? "in console mode" : "as a service" ) );

    TRACE( _T( "%s\n" ), static_cast<LPCTSTR>( Msg ) );

    if ( CLogger::SetApplicationPath( AppPath ) )
    {
        CSurveyEventLog SurveyEventLog;

        SurveyEventLog.Start( AppPath );

        SurveyEventLog.ReportEvent( eventLogAdvisory,
                                        iUnspecified_c,
                                            iUnspecified_c,
                                                const_cast<char *>( static_cast<LPCTSTR>( Msg ) ) );

        SurveyEventLog.Stop();
    }
}

extern "C"
BOOL WINAPI ConsoleCtrlHandler( DWORD dwEventType )
{
    // Console mode termination handler.

    UNREFERENCED_PARAMETER( dwEventType );

    // Set the termination event to allow the logger object to be destroyed and the
    // main thread released.

    TerminateService();

    // Return TRUE so that the OS doesn't invoke the cascaded default handler
    // and call ExitProcess() before the logger object is destroyed gracefully.

    return TRUE;
}

//////////////////////////////////////////////////////////////////////////////
// Externally visible functions.

CConsole & GetConsole( void )
{
    return g_Console;
}

