windows-nt/Source/XPSP1/NT/com/svcdlls/trksvcs/common/timer.cxx

805 lines
23 KiB
C++
Raw Permalink Normal View History

2020-09-26 03:20:57 -05:00
// Copyright (c) 1996-1999 Microsoft Corporation
//+-------------------------------------------------------------------------
//
// Microsoft Windows
//
// File: timer.cxx
//
// Contents: Code for a timer.
//
// Classes:
//
// Functions:
//
//
//
// History: 18-Nov-96 BillMo Created.
//
// Notes:
//
// Codework:
//
//--------------------------------------------------------------------------
#include <pch.cxx>
#pragma hdrstop
#include "trklib.hxx"
BOOL
LoadPersistentFileTime(
const TCHAR * ptszStaticRegName,
CFILETIME * pcft
);
void
UpdatePersistentFileTime(
const TCHAR * ptszStaticRegName,
const CFILETIME & cft
);
void
RemovePersistentFileTime(
const TCHAR * ptszStaticRegName
);
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::Initiliaze
//
// Synopsis: Initialize the object and create the timer but don't set it
// yet.
//
// Inputs: [pTimerCallback] (in)
// Who to call when the timer fires.
// [pWorkManager] (in)
// The WorkManager with which the timer will be registered
// [ptszName] (in, optional)
// If specified, the timer is persistent, and the name is
// used to store the timer information in the registry.
// If not specified the timer is not persistent.
// If data already exists in the registry for this name, it
// is used the next time the timer is set.
// [ulTimerContext] (in)
// Passed to pTimerCallback->Timer.
// [ulPeriodInSeconds] (in)
// The length of this timer when it's set.
// [retrytype] (in)
// From the TimerRetryType enumeration. Can
// be no_retry, retry_randomly, or retry_with_backoff.
// [ulLowerRetryTime] (in)
// Only valid when retrytype isn't no_retry.
// [ulUpperRetryTime] (in)
// Only valid when retrytype isn't no_retry.
// [ulMaxLifetime] (in)
// Only valid when retrytype isn't no_retry.
//
// Returns: Void
//
//+----------------------------------------------------------------------------
void
CNewTimer::Initialize( PTimerCallback *pTimerCallback,
const TCHAR *ptszName,
ULONG ulTimerContext,
ULONG ulPeriodInSeconds,
TimerRetryType retrytype,
ULONG ulLowerRetryTime,
ULONG ulUpperRetryTime,
ULONG ulMaxLifetime )
{
NTSTATUS Status = STATUS_SUCCESS;
CFILETIME cftLastTimeSet;
TrkAssert( ulLowerRetryTime <= ulUpperRetryTime );
TrkAssert( NO_RETRY == retrytype
||
0 != ulLowerRetryTime
&&
0 != ulUpperRetryTime );
TrkAssert( NO_RETRY != retrytype || 0 == ulMaxLifetime );
// Initialize our critical section. _fIntitializeCalled is used to
// indicate that this has been done.
_cs.Initialize();
_fInitializeCalled = TRUE;
// Keep the parameters
_pTimerCallback = pTimerCallback;
_ptszName = ptszName;
_ulTimerContext = ulTimerContext;
_ulPeriodInSeconds = ulPeriodInSeconds;
_RetryType = retrytype;
_ulLowerRetryTime = ulLowerRetryTime;
_ulUpperRetryTime = ulUpperRetryTime;
_ulMaxLifetime = ulMaxLifetime;
#if DBG
// Set the workitem signature for use in debug outputs.
_stprintf( _tszWorkItemSig, TEXT("CTimer:%p"), this );
if( NULL != ptszName )
{
_tcscat( _tszWorkItemSig, TEXT("/") );
_tcscat( _tszWorkItemSig, ptszName );
}
TrkAssert( _tcslen(_tszWorkItemSig) < ELEMENTS(_tszWorkItemSig) );
#endif
// Create the NT timer.
Status = NtCreateTimer(
&_hTimer,
TIMER_ALL_ACCESS,
NULL,
SynchronizationTimer ); // this sort of timer becomes un-signalled
// when a wait is satisfied
if (!NT_SUCCESS(Status))
{
_hTimer = NULL;
TrkRaiseNtStatus(Status);
}
// If this is a persistent timer, load the persisted state from the
// registry.
LoadFromRegistry();
// Register a work item with the Win32 thread pool.
_hRegisterWaitForSingleObjectEx
= TrkRegisterWaitForSingleObjectEx( _hTimer, ThreadPoolCallbackFunction,
static_cast<PWorkItem*>(this), INFINITE,
WT_EXECUTELONGFUNCTION | WT_EXECUTEDEFAULT );
if( NULL == _hRegisterWaitForSingleObjectEx )
{
TrkLog(( TRKDBG_ERROR, TEXT("Failed RegisterWaitForSingleObjectEx in CNewTimer::Initialize (%lu) for %s)"),
GetLastError(), GetTimerName() ));
TrkRaiseLastError();
}
else
TrkLog(( TRKDBG_TIMER, TEXT("Registered timer %s with thread pool (%p/%p)"),
GetTimerName(), this, *reinterpret_cast<UINT_PTR*>(this) ));
} // CNewTimer::Initialize
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::SetTimer
//
// Synopsis: Start the timer. If _cftDue isn't already set, we'll use
// _ulPeriodInSeconds (or _ulCurrentRetryTime) to set it. If this
// is a persistent timer, the registry is updated.
//
//+----------------------------------------------------------------------------
void
CNewTimer::SetTimer()
{
NTSTATUS Status;
CFILETIME cftNow, cftMax(0);
TrkAssert(NULL != _hTimer);
TrkAssert(sizeof(LARGE_INTEGER) == sizeof(_cftDue));
// If we're already running and not in retry mode, then there's nothing
// to be done.
if( _fRunning && 0 == _ulCurrentRetryTime )
return;
// Has the due time already been determined?
if( 0 == _cftDue )
{
// Are we in retry mode?
if( 0 != _ulCurrentRetryTime )
{
_cftDue = cftNow;
_cftDue.IncrementSeconds( _ulCurrentRetryTime );
}
else
{
_cftDue = cftNow;
_cftDue.IncrementSeconds( _ulPeriodInSeconds );
_cftSet = cftNow;
}
}
// We already have a non-zero due time. That doesn't mean that we're running,
// though, it might be a persistent timer that's been initialized but not
// started.
else if( _fRunning )
{
TrkAssert( 0 != _ulCurrentRetryTime );
// This timer was in retry mode but is now being started again.
// Restart as if it was being started for the first time.
_ulCurrentRetryTime = 0;
_cftDue = _cftSet = cftNow;
_cftDue.IncrementSeconds( _ulPeriodInSeconds );
}
if( 0 < _ulMaxLifetime )
{
cftMax = _cftSet;
cftMax.IncrementSeconds( _ulMaxLifetime );
if( cftNow > cftMax )
{
TrkLog(( TRKDBG_TIMER, TEXT("Stopping timer %s/%p due to liftime limit"),
(NULL == _ptszName) ? TEXT("") : _ptszName,
this ));
Cancel();
return;
}
else if( _cftDue > cftMax )
{
TrkLog(( TRKDBG_TIMER, TEXT("Shortening timer %s/%p due to lifetime limit"),
(NULL == _ptszName) ? TEXT("") : _ptszName,
this ));
_cftDue = cftMax;
}
}
SaveToRegistry();
// Set the timer, but not if it's currently firing. When the timer
// fires, the DoWork method is called, but that method doesn't hold
// the lock while it calls the Timer callback. Thus, if
// _fTimerSignalInProgress is true, some other thread is currently
// in the callback. When it complets, it will set this timer.
if( !_fTimerSignalInProgress )
{
Status = NtSetTimer ( _hTimer, //IN HANDLE TimerHandle,
(LARGE_INTEGER*) &_cftDue, //IN PLARGE_INTEGER DueTime,
NULL, //IN PTIMER_APC_ROUTINE TimerApcRoutine OPTIONAL,
NULL, //IN PVOID TimerContext OPTIONAL,
FALSE, //IN BOOLEAN ResumeTimer,
0, //IN LONG Period OPTIONAL,
NULL ); //OUT PBOOLEAN PreviousState OPTIONAL
TrkAssert(NT_SUCCESS(Status));
}
_fRunning = TRUE;
} // CNewTimer::SetTimer
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::Cancel
//
// Synopsis: Cancel the timer and remove its persistent state from the
// registry (if it's a persistent timer).
//
//+----------------------------------------------------------------------------
void
CNewTimer::Cancel()
{
NTSTATUS Status;
TrkAssert( _fInitializeCalled );
Lock();
__try
{
Status = NtCancelTimer(_hTimer, NULL);
TrkAssert(NT_SUCCESS(Status));
_fRunning = FALSE;
_ulCurrentRetryTime = 0;
_cftDue = _cftSet = 0;
RemoveFromRegistry();
}
__finally
{
Unlock();
}
}
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::DebugStringize
//
// Synopsis: Stringize the current state of the timer.
//
//+----------------------------------------------------------------------------
#if DBG
void
CNewTimer::DebugStringize( ULONG cch, TCHAR *ptsz ) const
{
ULONG cchUsed = 0;
TCHAR *ptszTimerState;
TrkAssert( _fInitializeCalled );
Lock();
__try
{
if( _fRunning )
{
if( 0 != _ulCurrentRetryTime )
ptszTimerState = TEXT("retrying");
else
ptszTimerState = TEXT("running");
}
else
ptszTimerState = TEXT("stopped");
cchUsed = _stprintf( ptsz, TEXT("Timer %s/%p is %s "),
NULL == _ptszName ? TEXT("") : _ptszName,
this, ptszTimerState );
if( _fRunning )
{
LONGLONG llDelta;
llDelta = static_cast<LONGLONG>(_cftDue - CFILETIME()) / (10*1000*1000);
if( 0 <= llDelta && 120 >= llDelta )
cchUsed += _stprintf( &ptsz[cchUsed], TEXT("(expires in %I64i seconds)"), llDelta );
else
{
cchUsed += _stprintf( &ptsz[cchUsed], TEXT("(expires on ") );
_cftDue.Stringize( cch-cchUsed, &ptsz[cchUsed] );
cchUsed += _tcslen( &ptsz[cchUsed] );
cchUsed += _stprintf( &ptsz[cchUsed], TEXT(" UTC)") );
}
}
TrkAssert( cch >= cchUsed );
}
__finally
{
Unlock();
}
}
#endif
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::DoWork
//
// Synopsis: This method is called by the work manager when the NT timer
// is signaled. We call pTimerCallback->Timer so that the timer
// event can be handled. That Timer method returns a status
// that tells us what we should do next. The returned status
// is a TimerContinuation, that can be Continue (causes
// a recurring timer to be set again), Break (causes a recurring
// timer to be stopped), or Retry.
//
//+----------------------------------------------------------------------------
void
CNewTimer::DoWork()
{
TrkAssert( _fInitializeCalled );
PTimerCallback::TimerContinuation continuation;
Lock();
{
// We were obviously running recently, or we wouldn't have been called.
// But if we're not running now, we must have been canceled after the
// timer object was signaled (waking the WaitForMultiple) but before
// entry into this routine.
if( !_fRunning )
{
TrkLog(( TRKDBG_ERROR, TEXT("Timer %s/%p stopped while firing"),
NULL == _ptszName ? TEXT("") : _ptszName,
this ));
Unlock();
return;
}
// For now, we're no longer running. If someone calls Set* while we're in
// the Timer callback below, this will be set true.
_fRunning = FALSE;
_cftDue = 0;
// Show that the timer has fired and is being processed. This is used
// by SetTimer so that it knows that we're in a call to the Timer
// callback.
_fTimerSignalInProgress = TRUE;
}
TrkAssert( 1 == GetLockCount() );
Unlock();
// Call the timer handler. On return, it tells us how we should
// proceed.
continuation = _pTimerCallback->Timer( _ulTimerContext );
// continuation : {Break, Continue, Retry}
Lock();
__try // __except
{
// We're no longer in the timer callback.
_fTimerSignalInProgress = FALSE;
// If, while we were in the Timer callback, another thread came along
// and set the timer, then that takes priority over the
// continuation that was just returned. In such an case, _fRunning
// will have been set to TRUE.
if( _fRunning )
{
TrkAssert( 0 != _cftDue );
TrkAssert( NULL != _hTimer );
// Show that we're not in retry mode
_ulCurrentRetryTime = 0;
NTSTATUS Status;
// _cftDue was set in the SetTimer call already
Status = NtSetTimer ( _hTimer, //IN HANDLE TimerHandle,
(LARGE_INTEGER*) &_cftDue, //IN PLARGE_INTEGER DueTime,
NULL, //IN PTIMER_APC_ROUTINE TimerApcRoutine OPTIONAL,
NULL, //IN PVOID TimerContext OPTIONAL,
FALSE, //IN BOOLEAN ResumeTimer,
0, //IN LONG Period OPTIONAL,
NULL ); //OUT PBOOLEAN PreviousState OPTIONAL
TrkAssert(NT_SUCCESS(Status));
}
else if( PTimerCallback::BREAK_TIMER == continuation )
{
// Break out of this timer; stop it even if it's a recurring timer.
Cancel();
}
else if( PTimerCallback::CONTINUE_TIMER == continuation )
{
// Continue with this timer; stop it if it's a single shot, set it again
// if it's recurring.
_ulCurrentRetryTime = 0; // If we were retrying, we aren't any longer
if( _fRecurring )
SetTimer();
else
Cancel();
}
else // RETRY_TIMER
{
TrkAssert( PTimerCallback::RETRY_TIMER == continuation );
TrkAssert( _ulLowerRetryTime <= _ulUpperRetryTime );
if( 0 == _ulUpperRetryTime || NO_RETRY == _RetryType )
{
TrkAssert( !TEXT("Attempted to retry a timer with no retry times set") );
Cancel();
}
if( RETRY_WITH_BACKOFF == _RetryType )
{
if( 0 == _ulCurrentRetryTime )
_ulCurrentRetryTime = _ulLowerRetryTime;
else if( (MAXULONG/2) < _ulCurrentRetryTime )
{
TrkLog(( TRKDBG_ERROR, TEXT("Questionable retry time") ));
TrkAssert( FALSE );
_ulCurrentRetryTime = MAXULONG;
}
else
_ulCurrentRetryTime *= 2;
if( _ulCurrentRetryTime > _ulUpperRetryTime )
_ulCurrentRetryTime = _ulUpperRetryTime;
}
else // PTimerCallback::RETRY_RANDOMLY == _RetryType
{
CFILETIME cftNow;
_ulCurrentRetryTime = _ulLowerRetryTime
+
( QuasiRandomDword() % (_ulUpperRetryTime - _ulLowerRetryTime) );
}
TrkLog(( TRKDBG_TIMER, TEXT("Retrying timer %s/%p for %d seconds"),
(NULL == _ptszName) ? TEXT("") : _ptszName,
this,
_ulCurrentRetryTime ));
// Set the timer with the just-calculated retry period
SetTimer();
} // else if( CONTINUE_TIMER == continuation ) ... else
}
__except( BreakOnDebuggableException() )
{
// The exception may have been in the timer, but more likely
// was in the PTimerCallback::Timer routine. As a cure-all,
// just reset the timer to an arbitrary value (we don't want
// to re-use _ulPeriodInSeconds, because it may be zero).
TrkLog(( TRKDBG_ERROR, TEXT("Unexpected timer exception") ));
TrkAssert( FALSE );
__try
{
_ulPeriodInSeconds = TRKDAY;
Cancel();
SetTimer();
}
__except( BreakOnDebuggableException() )
{
}
}
Unlock();
} // CNewTimer::DoWork
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::SaveToRegistry
//
// Synopsis: Save the timer's state to the registry, using _ptszName
// as a value name.
//
//+----------------------------------------------------------------------------
void
CNewTimer::SaveToRegistry()
{
LONG lErr = ERROR_SUCCESS;
HKEY hk = NULL;
// If this isn't a persistent timer, then there's nothing to do.
if( NULL == _ptszName )
return;
Lock();
__try
{
lErr = RegOpenKey( HKEY_LOCAL_MACHINE, s_tszKeyNameLinkTrack, &hk );
PersistentState persist;
persist.cftSet = _cftSet;
persist.cftDue = _cftDue;
persist.ulCurrentRetryTime = _ulCurrentRetryTime;
if ( lErr == ERROR_SUCCESS )
{
lErr = RegSetValueEx( hk,
_ptszName,
0,
REG_BINARY,
(CONST BYTE *)&persist,
sizeof(persist) );
RegCloseKey(hk);
}
}
__finally
{
Unlock();
}
TrkAssert( lErr == ERROR_SUCCESS
||
lErr == ERROR_NOT_ENOUGH_MEMORY
||
lErr == ERROR_NO_SYSTEM_RESOURCES );
}
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::LoadFromRegistry
//
// Synopsis: Load this timer's previously persisted state from the
// registry.
//
//+----------------------------------------------------------------------------
void
CNewTimer::LoadFromRegistry()
{
LONG l;
HKEY hk = NULL;
// If this isn't a persistent timer, then there's nothing to do.
if( NULL == _ptszName )
return;
Lock();
__try
{
// Open the main link-tracking key.
l = RegCreateKey(HKEY_LOCAL_MACHINE, s_tszKeyNameLinkTrack, &hk);
if (l != ERROR_SUCCESS)
{
hk = NULL;
}
else
{
// The main link-tracking key exists. See if we can open this
// timer's value.
PersistentState persist;
DWORD cbData = sizeof(persist);
DWORD dwType = 0;
l = RegQueryValueEx( hk,
_ptszName,
NULL,
&dwType,
(BYTE *)&persist,
&cbData );
if (l == ERROR_SUCCESS)
{
if (dwType == REG_BINARY
&&
cbData == sizeof(persist))
{
// This timer has a persistent value in the registry. Override
// the caller-provided timeout.
_cftDue = persist.cftDue;
_cftSet = persist.cftSet;
_ulCurrentRetryTime = persist.ulCurrentRetryTime;
}
else
{
RegDeleteValue( hk, _ptszName );
l = ERROR_FILE_NOT_FOUND;
}
} // if (l == ERROR_SUCCESS)
} // if (l != ERROR_SUCCESS) ... else
}
__finally
{
if( NULL != hk )
RegCloseKey(hk);
Unlock();
}
if (l != ERROR_SUCCESS && l != ERROR_FILE_NOT_FOUND)
{
TrkLog(( TRKDBG_ERROR, TEXT("Ignoring error %08x in timer %s LoadFromRegistry"),
l, _ptszName ));
}
return;
}
//+----------------------------------------------------------------------------
//
// Method: CNewTimer::RemoveFromRegistry
//
// Synopsis: Remove this timer's persistent state from the registry.
//
//+----------------------------------------------------------------------------
void
CNewTimer::RemoveFromRegistry()
{
LONG lErr = ERROR_SUCCESS;
HKEY hk = NULL;
if( NULL == _ptszName )
return;
Lock();
__try
{
lErr = RegOpenKey( HKEY_LOCAL_MACHINE, s_tszKeyNameLinkTrack, &hk );
if ( lErr == ERROR_SUCCESS )
{
lErr = RegDeleteValue( hk, _ptszName );
RegCloseKey(hk);
if( ERROR_SUCCESS != lErr
&&
ERROR_PATH_NOT_FOUND != lErr
&&
ERROR_FILE_NOT_FOUND != lErr )
{
TrkLog((TRKDBG_ERROR, TEXT("Couldn't delete timer's static reg name (\"%s\", %08x)"),
_ptszName, lErr ));
}
}
}
__finally
{
Unlock();
}
}
//+----------------------------------------------------------------------------
//
// CNewTimer::UnInitialize
//
// Unregister the timer handle from the thread pool, cancel the timer,
// and release it.
//
//+----------------------------------------------------------------------------
void
CNewTimer::UnInitialize()
{
if( _fInitializeCalled )
{
TrkLog(( TRKDBG_TIMER, TEXT("Uninitializing timer %s/%p"), GetTimerName(), this ));
// Take the timer out of the thread pool, which must be
// done before closing the timer.
if( NULL != _hRegisterWaitForSingleObjectEx )
{
if( !TrkUnregisterWait( _hRegisterWaitForSingleObjectEx ))
{
TrkLog(( TRKDBG_ERROR, TEXT("Failed UnregisterWait for CNewTimer (%s/%p, %lu)"),
NULL == _ptszName ? TEXT("") : _ptszName, this,
GetLastError() ));
}
else
{
TrkLog(( TRKDBG_TIMER, TEXT("Unregistered wait for timer (%s/%p)"),
NULL == _ptszName ? TEXT("") : _ptszName, this ));
}
_hRegisterWaitForSingleObjectEx = NULL;
}
// Close the timer handle
TrkVerify( NT_SUCCESS( NtCancelTimer(_hTimer, NULL) ));
NtClose(_hTimer);
// Delete the CNewTimer critical section.
_cs.UnInitialize();
_fInitializeCalled = FALSE;
}
}