// 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 #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(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(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(_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; } }