/*
* /+\
* +\ Copyright 1995, 2000 Perforce Software. All rights reserved.
* \+/
*
* This file is part of Perforce - the FAST SCM System.
*/
# define NEED_WINDOWSH
# include <stdhdrs.h>
# include <strbuf.h>
# include <error.h>
# include <filesys.h>
# include <errorlog.h>
# include <threading.h>
# include "ntservice.h"
#define ASSERT(f)
//
// The static global_this pointer is required so that static callbacks can
// access the instances' data. This scheme fails if there is more than one
// instance. This is not a problem for services of the OWN process type.
//
NtService *NtService::global_this = NULL;
// Implementation of NtService class.
//
// Construction:
//
// In order to create an NtService object an entry point must be specified.
// The entry_point is the function where the service begins execution when it
// is started, and it cannot be main because main attaches to the SCM.
// The SCM ( Service Control Manager ) ultimately calls entry_point.
//
NtService::NtService()
{
global_this = this;
// We want to allow our service to stop on user request and during
// a system shutdown. The system shutdown will not issue a stop
// request. We have to allow and process the shutdown request.
status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
status.dwCurrentState = SERVICE_START_PENDING;
status.dwControlsAccepted =
SERVICE_ACCEPT_STOP|SERVICE_ACCEPT_SHUTDOWN; // no pause
status.dwWin32ExitCode = 0;
status.dwServiceSpecificExitCode = 0;
// Start with a check point of zero, the initial value only matters
// if we have an error. WaitHint=0 and CheckPoint=0 is a special
// indicator to the SCM that we died on startup. We use an initial
// of 1000ms as a marker. Normally we should never see this
// value since the Perforce service should be using a higher value.
// The SCM default wait hint seems to be 2000ms.
// Using the svcinst tool with -d will show you the wait hints.
status.dwCheckPoint = 0;
status.dwWaitHint = 1000;
entry_point = 0;
statusHandle = 0;
}
NtService::~NtService()
{
global_this = NULL;
}
/*
* NtService::Start()
*
* The dispatcher must be called with 8 to 10 seconds upon execution.
* This function begins the process of bringing up the Service. When the
* dispatch table is registered with the Service Control Manager, the
* StaticRun entry point is called. We set the start_pending status
* with a hint indicating we expect to take 10 seconds to start.
*
* Since this class only supports a single service per executable, it
* simply creates a dispatch table with a single entry and starts the
* dispatcher.
*
* StaticRun() calls global_this->run(). This added complexity
* allows the startup behavior to be determined by the objects class
* through the virtual run() method. The global_this works because
* there is only one NtService per process; however, multiple
* services per process requires a different approach.
*/
void NtService::Start( int (*entryPt)( DWORD, char ** ), char *svc, Error *e )
{
SERVICE_TABLE_ENTRY dispatch_table[] =
{
{ TEXT(""), (void (__stdcall *)( DWORD, char **) )StaticRun },
{ NULL, NULL }
} ;
entry_point = entryPt;
// This API essentially does a CreateThread with StaticRun as the
// entry point. This thread will wait until the Windows Service
// enters a stopped state. Then we return to main / ntService,
// which returns to the CRT, which then calls doexit. We want to
// make sure nt_main is done before this thread continues. We
// only set the Service state to stopped at the end of nt_main.
if( StartServiceCtrlDispatcher( dispatch_table ) )
return;
e->Sys( "StartServiceCtrlDispatcher", svc );
e->Set( E_FATAL, "Can not start the Perforce Service" );
}
/*
* NtService::SetStatus()
*
* Call this function to inform the service control manager of changes in the
* status of the service and to report errors. There are various points at which
* this function must be called. For example the entry_point function should
* call set_status( running ) as soon as the service has successfully started.
* If initialization takes more than about 20 seconds set_status( start_pending)
* should be called by entry_point at regular intervals.
*
* The check_point and wait_hint parameters can be used to inform the SCM of
* progress if initialization takes a particularly long time. The wait_hint is
* in milliseconds and the check_point is simply incremented as things progress.
* This holds true for pause_pending and continue_pending states too.
*
* The win32_exitcode allows you to report an error from GetLastError() and
* the specific_code allows you to specify an application specific error.
*/
void NtService::SetStatus(
states state,
DWORD win32_exitcode,
DWORD specific_code,
DWORD wait_hint )
{
ASSERT( statusHandle );
switch( state )
{
case no_change:
break;
case running:
status.dwCurrentState = SERVICE_RUNNING;
break;
case stopped:
status.dwCurrentState = SERVICE_STOPPED;
break;
case paused:
status.dwCurrentState = SERVICE_PAUSED;
break;
case start_pending:
status.dwCurrentState = SERVICE_START_PENDING;
break;
case stop_pending:
status.dwCurrentState = SERVICE_STOP_PENDING;
break;
case pause_pending:
status.dwCurrentState = SERVICE_PAUSE_PENDING;
break;
case continue_pending:
status.dwCurrentState = SERVICE_CONTINUE_PENDING;
break;
default:
ASSERT(0);
break;
}
if( state != no_change )
{
status.dwWin32ExitCode = win32_exitcode;
status.dwServiceSpecificExitCode = specific_code;
// The wait hint must always increase in value. If you
// use 10000, then send a 5000, the SCM might get upset.
// 0 wait hint is only ok if the service is not in transition.
if( status.dwWaitHint < wait_hint || state == stopped )
status.dwWaitHint = wait_hint;
}
if( ! SetServiceStatus( statusHandle, &status ) )
{
StrBuf msg;
DWORD err;
Error e;
err = GetLastError( );
e.Sys( "SetServiceStatus", svcName );
e.Set( E_FATAL, "Cannot communicate with Service Control Manager" );
e.Fmt( msg, EF_PLAIN );
AssertLog.SysLog( NULL, 0, "Perforce Service", msg.Text() );
}
if( state == running )
{
// Set these back to initial values in prep for the stop.
status.dwCheckPoint = 0;
status.dwWaitHint = 1000;
}
else
{
// We manage the check point internally.
status.dwCheckPoint++;
}
}
/*
* NtService::ControlHandler()
*
* This static member function is passed a call back to the SCM which invokes
* it to control the service. It examines the opcode and invokes the
* appropriate virtual member function through the global this pointer.
*
*/
void WINAPI
NtService::ControlHandler( DWORD opcode )
{
if( ! global_this )
return;
switch( opcode )
{
// No support for pause/resume or parameter change.
case SERVICE_CONTROL_PAUSE: break;
case SERVICE_CONTROL_CONTINUE: break;
// We accept and process stop and shutdown requests. A stop
// request is not issued at system shutdown time, only a shutdown.
#ifdef SERVICE_CONTROL_PRESHUTDOWN
case SERVICE_CONTROL_PRESHUTDOWN:
#endif
case SERVICE_CONTROL_SHUTDOWN:
case SERVICE_CONTROL_STOP: global_this->Stop(); break;
// We always report the last known status of our service. Only
// the check point value is increasing.
case SERVICE_CONTROL_INTERROGATE: global_this->SetStatus(); break;
default: ASSERT(0);
}
}
/*
* NtService::StaticRun()
*
* This static function is passed as a callback to the SCM because member
* functions can not be used in callbacks. It then uses the global this
* pointer to invoke the appropriate virtual member function.
*/
void
NtService::StaticRun( DWORD argc, char **argv )
{
if( ! global_this )
return;
global_this->Run( argc, argv );
}
/*
* Starts the service by invoking entry_point.
*/
void
NtService::Run( DWORD argc, char **argv )
{
StrBuf msg;
// Store the service name for the shutdown message.
strcpy(svcName, argv[0]);
statusHandle =
RegisterServiceCtrlHandler( TEXT(svcName), ControlHandler );
if( statusHandle == NULL )
{
DWORD err;
Error e;
err = GetLastError( );
e.Sys( "RegisterServiceCtrlHandler", svcName );
e.Set( E_FATAL, "Can not register the Perforce Service" );
e.Fmt( msg, EF_PLAIN );
AssertLog.SysLog( NULL, 0, "Perforce Service", msg.Text() );
// If the check point and wait hint are both zero, with
// a service state of stopped, the SCM knows we failed.
SetStatus( stopped, err, err, 0 );
return;
}
// Let the Service Mangler know we plan on taking 10 seconds.
SetStatus( start_pending, 0, 0, 10000 );
// We want this in the Windows Event log, use SysLog directly.
// Using Syslog bypasses the logfile setting.
msg = "Starting the Perforce Service ";
msg << svcName;
AssertLog.SysLog( NULL, 0, "Perforce Service", msg.Text() );
// Start the Perforce server
entry_point( argc, argv );
}
/*
* Stops the service. It's not enough to simply set the status to
* stopped and return -- this will only cause the service to hang for
* 30 seconds since the service main (the entry_point) thread is still
* running. So bring down the service main thread by signalling the
* event and all will be well.
*/
void
NtService::Stop()
{
StrBuf msg;
// We want this in the Windows Event log, use SysLog directly.
// Using Syslog bypasses the logfile setting.
msg = "End of the Perforce Service ";
msg << svcName;
AssertLog.SysLog( NULL, 0, "Perforce Service", msg.Text() );
// We signal the main thread to stop. It eventually returns
// and program flow goes through the SCM back to us where we
// unwind our main via a return. The SCM is expecting to
// close everything down. We must not call ExitProcess().
Threading::Cancel();
}