636 lines
14 KiB
C
636 lines
14 KiB
C
|
/*++
|
||
|
|
||
|
Copyright (c) 2000, Microsoft Corporation
|
||
|
|
||
|
Module Name:
|
||
|
|
||
|
svcmain.c
|
||
|
|
||
|
Abstract:
|
||
|
|
||
|
This module contains code for starting off, shutting down and
|
||
|
handling device addition/removal request for the EAPOL module.
|
||
|
|
||
|
|
||
|
Revision History:
|
||
|
|
||
|
sachins, Apr 25 2000, Created
|
||
|
|
||
|
Notes:
|
||
|
EAPOL_SERVICE if defined, on compilation, a .exe version is created.
|
||
|
If not defined, on compilation, a .lib is created, with entry
|
||
|
points defined, which netman calls into.
|
||
|
|
||
|
--*/
|
||
|
|
||
|
#include "pcheapol.h"
|
||
|
#pragma hdrstop
|
||
|
|
||
|
extern
|
||
|
VOID
|
||
|
EAPOLServiceMain (
|
||
|
IN DWORD argc,
|
||
|
IN LPWSTR *lpwsServiceArgs
|
||
|
);
|
||
|
|
||
|
DWORD
|
||
|
WINAPI
|
||
|
EAPOLServiceMainWorker (
|
||
|
IN PVOID pvContext
|
||
|
);
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
//
|
||
|
// main
|
||
|
//
|
||
|
// Description: Will simply register the entry point of the EAPOL
|
||
|
// service with the service controller. The service controller
|
||
|
// will capture this thread. It will be freed only when
|
||
|
// the service is shutdown. At that point we will simply exit
|
||
|
// the process.
|
||
|
//
|
||
|
// Return values: none
|
||
|
//
|
||
|
|
||
|
void
|
||
|
_cdecl
|
||
|
main ( int argc, unsigned char * argv[] )
|
||
|
{
|
||
|
SERVICE_TABLE_ENTRY EapolServiceDispatchTable[2];
|
||
|
|
||
|
UNREFERENCED_PARAMETER( argc );
|
||
|
UNREFERENCED_PARAMETER( argv );
|
||
|
|
||
|
EapolServiceDispatchTable[0].lpServiceName = EAPOL_SERVICE_NAME;
|
||
|
EapolServiceDispatchTable[0].lpServiceProc = EAPOLServiceMain;
|
||
|
EapolServiceDispatchTable[1].lpServiceName = NULL;
|
||
|
EapolServiceDispatchTable[1].lpServiceProc = NULL;
|
||
|
|
||
|
if ( !StartServiceCtrlDispatcher( EapolServiceDispatchTable ) )
|
||
|
{
|
||
|
}
|
||
|
|
||
|
ExitProcess(0);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// EAPOLAnnounceServiceStatus
|
||
|
//
|
||
|
// Description: Will simly call SetServiceStatus to inform the service
|
||
|
// control manager of this service's current status.
|
||
|
//
|
||
|
// Return values: none
|
||
|
//
|
||
|
|
||
|
VOID
|
||
|
EAPOLAnnounceServiceStatus (
|
||
|
VOID
|
||
|
)
|
||
|
{
|
||
|
BOOL dwRetCode;
|
||
|
|
||
|
//
|
||
|
// Increment the checkpoint in a pending state:
|
||
|
//
|
||
|
|
||
|
switch( g_ServiceStatus.dwCurrentState )
|
||
|
{
|
||
|
case SERVICE_START_PENDING:
|
||
|
case SERVICE_STOP_PENDING:
|
||
|
|
||
|
g_ServiceStatus.dwCheckPoint++;
|
||
|
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
dwRetCode = SetServiceStatus( g_hServiceStatus,
|
||
|
&g_ServiceStatus );
|
||
|
|
||
|
if ( dwRetCode == FALSE )
|
||
|
{
|
||
|
TRACE1 (INIT, "Error: SetServiceStatus returned %d\n",
|
||
|
GetLastError() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
|
||
|
//
|
||
|
// EAPOLCleanUp
|
||
|
//
|
||
|
// Description: Will free any allocated memory, deinitialize RPC, deinitialize
|
||
|
// the kernel-mode server and unload it if it was loaded.
|
||
|
// This could have been called due to an error on SERVICE_START
|
||
|
// or normal termination.
|
||
|
//
|
||
|
// Return values: none
|
||
|
//
|
||
|
|
||
|
VOID
|
||
|
EAPOLCleanUp (
|
||
|
IN DWORD dwError
|
||
|
)
|
||
|
{
|
||
|
DWORD dwEventStatus = 0;
|
||
|
SERVICE_STATUS ServiceStatus;
|
||
|
DWORD dwRetCode = NO_ERROR;
|
||
|
|
||
|
if (g_hEventTerminateEAPOL == NULL)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Check if have already gone through EAPOLCleanUp before
|
||
|
// Return if so
|
||
|
//
|
||
|
|
||
|
if (( dwEventStatus = WaitForSingleObject (
|
||
|
g_hEventTerminateEAPOL,
|
||
|
0)) == WAIT_FAILED)
|
||
|
{
|
||
|
dwRetCode = GetLastError ();
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "EAPOLCleanUp: WaitForSingleObject failed with error %ld, Terminating cleanup",
|
||
|
dwRetCode);
|
||
|
}
|
||
|
|
||
|
// log
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (dwEventStatus == WAIT_OBJECT_0)
|
||
|
{
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE0 (INIT, "EAPOLCleanUp: g_hEventTerminateEAPOL already signaled, returning");
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
//
|
||
|
// Announce that we are stopping
|
||
|
//
|
||
|
|
||
|
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
|
||
|
g_ServiceStatus.dwControlsAccepted = 0;
|
||
|
g_ServiceStatus.dwCheckPoint = 1;
|
||
|
g_ServiceStatus.dwWaitHint = 200000;
|
||
|
|
||
|
EAPOLAnnounceServiceStatus();
|
||
|
|
||
|
#endif
|
||
|
|
||
|
//
|
||
|
// Tear down and free everything
|
||
|
//
|
||
|
|
||
|
//
|
||
|
// Set event to indicate to waiting threads to terminate
|
||
|
//
|
||
|
|
||
|
if ( !SetEvent (g_hEventTerminateEAPOL) )
|
||
|
{
|
||
|
dwRetCode = GetLastError();
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "EAPOLCleanUp: SetEvent for g_hEventTerminateEAPOL failed with error %ld",
|
||
|
dwRetCode);
|
||
|
}
|
||
|
|
||
|
// log
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Shutdown device related stuff
|
||
|
// Close handles to NDISUIO
|
||
|
// Shutdown EAPOL State machine
|
||
|
//
|
||
|
if ( ( dwRetCode = ElMediaDeInit()) != NO_ERROR )
|
||
|
{
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "Media DeInit failed with dwRetCode = %ld\n",
|
||
|
dwRetCode );
|
||
|
}
|
||
|
|
||
|
dwRetCode = NO_ERROR;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "Media DeInit succeeded with dwRetCode = %ld\n",
|
||
|
dwRetCode );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (READ_WRITE_LOCK_CREATED(&(g_PolicyLock)))
|
||
|
{
|
||
|
ACQUIRE_WRITE_LOCK (&g_PolicyLock);
|
||
|
if (g_pEAPOLPolicyList != NULL)
|
||
|
{
|
||
|
ElFreePolicyList (g_pEAPOLPolicyList);
|
||
|
g_pEAPOLPolicyList = NULL;
|
||
|
}
|
||
|
RELEASE_WRITE_LOCK (&g_PolicyLock);
|
||
|
}
|
||
|
|
||
|
if (READ_WRITE_LOCK_CREATED(&(g_PolicyLock)))
|
||
|
{
|
||
|
DELETE_READ_WRITE_LOCK(&(g_PolicyLock));
|
||
|
}
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
if ( dwError == NO_ERROR )
|
||
|
{
|
||
|
g_ServiceStatus.dwWin32ExitCode = NO_ERROR;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
g_ServiceStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
|
||
|
}
|
||
|
|
||
|
g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
|
||
|
g_ServiceStatus.dwControlsAccepted = 0;
|
||
|
g_ServiceStatus.dwCheckPoint = 0;
|
||
|
g_ServiceStatus.dwWaitHint = 0;
|
||
|
g_ServiceStatus.dwServiceSpecificExitCode = dwError;
|
||
|
|
||
|
EAPOLAnnounceServiceStatus();
|
||
|
|
||
|
if (!CloseHandle(g_hStopService))
|
||
|
{
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "EAPOLCleanup: CloseHandle failed with error %ld",
|
||
|
GetLastError());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
if ( g_dwTraceId != INVALID_TRACEID )
|
||
|
{
|
||
|
TRACE1 (INIT, "EAPOLCleanup completed with error %d\n", dwError );
|
||
|
TraceDeregisterA( g_dwTraceId );
|
||
|
g_dwTraceId = INVALID_TRACEID;
|
||
|
}
|
||
|
|
||
|
EapolLogInformation (EAPOL_LOG_SERVICE_RUNNING, 0, NULL);
|
||
|
|
||
|
if ( g_hLogEvents != NULL)
|
||
|
{
|
||
|
EapolLogInformation (EAPOL_LOG_SERVICE_STOPPED, 0, NULL);
|
||
|
RouterLogDeregisterW( g_hLogEvents );
|
||
|
g_hLogEvents = NULL;
|
||
|
}
|
||
|
|
||
|
if (g_hEventTerminateEAPOL != NULL)
|
||
|
{
|
||
|
CloseHandle (g_hEventTerminateEAPOL);
|
||
|
g_hEventTerminateEAPOL = NULL;
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
//
|
||
|
// ServiceHandlerEx
|
||
|
//
|
||
|
// Description: Will respond to control requests from the service controller.
|
||
|
//
|
||
|
// Return values: none
|
||
|
//
|
||
|
//
|
||
|
|
||
|
DWORD
|
||
|
ServiceHandlerEx (
|
||
|
IN DWORD dwControlCode,
|
||
|
IN DWORD dwEventType,
|
||
|
IN LPVOID lpEventData,
|
||
|
IN LPVOID lpContext
|
||
|
)
|
||
|
{
|
||
|
DWORD dwRetCode = NO_ERROR;
|
||
|
|
||
|
switch( dwControlCode )
|
||
|
{
|
||
|
case SERVICE_CONTROL_STOP:
|
||
|
case SERVICE_CONTROL_SHUTDOWN:
|
||
|
|
||
|
if ( ( g_ServiceStatus.dwCurrentState == SERVICE_STOP_PENDING)
|
||
|
||
|
||
|
( g_ServiceStatus.dwCurrentState == SERVICE_STOPPED ))
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
TRACE0 (INIT, "ServiceHandlerEx: SERVICE_CONTROL_ STOP or SHUTDOWN event called");
|
||
|
|
||
|
//
|
||
|
// Announce that we are stopping
|
||
|
//
|
||
|
|
||
|
g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
|
||
|
g_ServiceStatus.dwControlsAccepted = 0;
|
||
|
g_ServiceStatus.dwCheckPoint = 1;
|
||
|
g_ServiceStatus.dwWaitHint = 200000;
|
||
|
|
||
|
EAPOLAnnounceServiceStatus();
|
||
|
|
||
|
SetEvent( g_hStopService );
|
||
|
|
||
|
return( NO_ERROR );
|
||
|
|
||
|
break;
|
||
|
|
||
|
case SERVICE_CONTROL_DEVICEEVENT:
|
||
|
if ( ( g_ServiceStatus.dwCurrentState == SERVICE_STOP_PENDING)
|
||
|
||
|
||
|
( g_ServiceStatus.dwCurrentState == SERVICE_STOPPED ))
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
TRACE0 (INIT, "ServiceHandlerEx: SERVICE_CONTROL_DEVICEEVENT event called");
|
||
|
// Received notification that some LAN interface was added or deleted
|
||
|
|
||
|
if (lpEventData != NULL)
|
||
|
{
|
||
|
// Call device notification handler
|
||
|
|
||
|
if ((dwRetCode = ElDeviceNotificationHandler (
|
||
|
lpEventData, dwEventType)) != NO_ERROR)
|
||
|
{
|
||
|
|
||
|
TRACE1 (INIT, "ServiceHandlerEx: ElDeviceNotificationHandler faield with error %ld",
|
||
|
dwRetCode);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
|
||
|
return( ERROR_CALL_NOT_IMPLEMENTED );
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return( dwRetCode );
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
|
||
|
//
|
||
|
// EAPOLServiceMain
|
||
|
//
|
||
|
// Description: This is the main procedure for the EAPOL Server Service. It
|
||
|
// will be called when the service is supposed to start itself.
|
||
|
// It will do all service wide initialization.
|
||
|
//
|
||
|
// Return values: none
|
||
|
//
|
||
|
|
||
|
VOID
|
||
|
WINAPI
|
||
|
EAPOLServiceMain (
|
||
|
IN DWORD argc, // Command line arguments. Will be ignored.
|
||
|
IN LPWSTR * lpwsServiceArgs
|
||
|
)
|
||
|
{
|
||
|
DWORD dwRetCode = NO_ERROR;
|
||
|
|
||
|
UNREFERENCED_PARAMETER( argc );
|
||
|
UNREFERENCED_PARAMETER( lpwsServiceArgs );
|
||
|
|
||
|
//
|
||
|
// Initialize globals
|
||
|
//
|
||
|
|
||
|
g_hEventTerminateEAPOL = NULL;
|
||
|
g_lWorkerThreads = 0;
|
||
|
g_lPCBContextsAlive = 0;
|
||
|
g_hLogEvents = NULL;
|
||
|
g_dwTraceId = INVALID_TRACEID;
|
||
|
g_hStopService = NULL;
|
||
|
g_dwModulesStarted = 0;
|
||
|
g_hNLA_LPC_Port = NULL;
|
||
|
g_fUserLoggedOn = FALSE;
|
||
|
g_hTimerQueue = NULL;
|
||
|
g_hDeviceNotification = NULL;
|
||
|
g_fTrayIconReady = FALSE;
|
||
|
g_dwmaxStart = EAPOL_MAX_START;
|
||
|
g_dwstartPeriod = EAPOL_START_PERIOD;
|
||
|
g_dwauthPeriod = EAPOL_AUTH_PERIOD;
|
||
|
g_dwheldPeriod = EAPOL_HELD_PERIOD;
|
||
|
g_dwSupplicantMode = EAPOL_DEFAULT_SUPPLICANT_MODE;
|
||
|
g_dwEAPOLAuthMode = EAPOL_DEFAULT_AUTH_MODE;
|
||
|
g_pEAPOLPolicyList = NULL;
|
||
|
g_dwCurrentSessionId = 0xffffffff;
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
g_hServiceStatus = RegisterServiceCtrlHandlerEx(
|
||
|
TEXT("EAPOL"),
|
||
|
ServiceHandlerEx,
|
||
|
NULL );
|
||
|
|
||
|
if ( !g_hServiceStatus )
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
g_ServiceStatus.dwServiceType = SERVICE_WIN32_SHARE_PROCESS;
|
||
|
g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
|
||
|
|
||
|
EAPOLAnnounceServiceStatus();
|
||
|
|
||
|
#endif
|
||
|
|
||
|
//
|
||
|
// Create event that will be used to indicate EAPOL shutdown
|
||
|
//
|
||
|
|
||
|
g_hEventTerminateEAPOL = CreateEvent( NULL, TRUE, FALSE, NULL );
|
||
|
|
||
|
if ( g_hEventTerminateEAPOL == (HANDLE)NULL )
|
||
|
{
|
||
|
dwRetCode = GetLastError ();
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Register for debug tracing via rtutils.dll
|
||
|
//
|
||
|
|
||
|
g_dwTraceId = TraceRegister (L"EAPOL");
|
||
|
|
||
|
if ( g_dwTraceId == INVALID_TRACEID )
|
||
|
{
|
||
|
dwRetCode = GetLastError ();
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Register for event logging via rtutils.dll
|
||
|
//
|
||
|
|
||
|
g_hLogEvents = RouterLogRegisterW(L"EAPOL");
|
||
|
|
||
|
if ( g_hLogEvents == NULL )
|
||
|
{
|
||
|
dwRetCode = GetLastError ();
|
||
|
TRACE1 (INIT, "EAPOLServiceMainWorker: RouterLogRegisterW failed with error %ld",
|
||
|
dwRetCode);
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (dwRetCode = CREATE_READ_WRITE_LOCK(&(g_PolicyLock), "PCY") != NO_ERROR)
|
||
|
{
|
||
|
TRACE1(INIT, "EAPOLServiceMainWorker: Error %d creating g_PolicyLock read-write-lock", dwRetCode);
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Queue a worker item to do the heavy-duty work during initialization
|
||
|
// This will not hold up the main service thread
|
||
|
|
||
|
InterlockedIncrement (&g_lWorkerThreads);
|
||
|
|
||
|
if (!QueueUserWorkItem(
|
||
|
(LPTHREAD_START_ROUTINE)EAPOLServiceMainWorker,
|
||
|
NULL,
|
||
|
WT_EXECUTELONGFUNCTION))
|
||
|
{
|
||
|
dwRetCode = GetLastError();
|
||
|
InterlockedDecrement (&g_lWorkerThreads);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
DWORD
|
||
|
WINAPI
|
||
|
EAPOLServiceMainWorker (
|
||
|
IN PVOID pvContext
|
||
|
)
|
||
|
{
|
||
|
|
||
|
DWORD dwRetCode = NO_ERROR;
|
||
|
|
||
|
do
|
||
|
{
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
//
|
||
|
// Announce that we have successfully started.
|
||
|
//
|
||
|
|
||
|
g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
|
||
|
g_ServiceStatus.dwCheckPoint = 0;
|
||
|
g_ServiceStatus.dwWaitHint = 0;
|
||
|
g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
|
||
|
|
||
|
EAPOLAnnounceServiceStatus();
|
||
|
|
||
|
//
|
||
|
// Create event that will be used to shutdown the EAPOL service
|
||
|
//
|
||
|
|
||
|
g_hStopService = CreateEvent ( NULL, TRUE, FALSE, NULL );
|
||
|
|
||
|
if ( g_hStopService == (HANDLE)NULL )
|
||
|
{
|
||
|
dwRetCode = GetLastError ();
|
||
|
TRACE1 (INIT, "EAPOLServiceMainWorker: CreateEvent failed with error %ld",
|
||
|
dwRetCode);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
if ((dwRetCode = ElUpdateRegistry ()) != NO_ERROR)
|
||
|
{
|
||
|
TRACE1 (INIT, "ElUpdateRegistry failed with error (%ld)",
|
||
|
dwRetCode);
|
||
|
// Ignore registry update errors
|
||
|
dwRetCode = NO_ERROR;
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Initialize media related stuff
|
||
|
// Interfaces will be enumerated, handles to NDISUIO driver will be opened,
|
||
|
// EAPOL will be initialized
|
||
|
//
|
||
|
|
||
|
if ( ( dwRetCode = ElMediaInit ()) != NO_ERROR )
|
||
|
{
|
||
|
TRACE1 (INIT, "Media Init failed with dwRetCode = %d\n",
|
||
|
dwRetCode );
|
||
|
break;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
TRACE1 (INIT, "Media Init succeeded with dwRetCode = %d\n",
|
||
|
dwRetCode );
|
||
|
}
|
||
|
|
||
|
TRACE0 (INIT, "EAPOL started successfully\n" );
|
||
|
|
||
|
// EapolLogInformation (EAPOL_LOG_SERVICE_STARTED, 0, NULL);
|
||
|
|
||
|
#ifdef EAPOL_SERVICE
|
||
|
|
||
|
//
|
||
|
// Just wait here for EAPOL service to terminate.
|
||
|
//
|
||
|
|
||
|
dwRetCode = WaitForSingleObject( g_hStopService, INFINITE );
|
||
|
|
||
|
if ( dwRetCode == WAIT_FAILED )
|
||
|
{
|
||
|
dwRetCode = GetLastError();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
dwRetCode = NO_ERROR;
|
||
|
}
|
||
|
|
||
|
TRACE0 (INIT, "Stopping EAPOL gracefully\n" );
|
||
|
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
|
||
|
#endif
|
||
|
|
||
|
}
|
||
|
while (FALSE);
|
||
|
|
||
|
InterlockedDecrement (&g_lWorkerThreads);
|
||
|
|
||
|
if (dwRetCode != NO_ERROR)
|
||
|
{
|
||
|
EAPOLCleanUp ( dwRetCode );
|
||
|
}
|
||
|
|
||
|
return dwRetCode;
|
||
|
}
|