/* *@@sourcefile timer.c: * XTimers, which can be used to avoid excessive PM * timer usage. * * The functions here allow you to share a single PM * timer for many windows on the same thread. Basically, * these start a "master PM timer", to whose WM_TIMER * message your window procedure must respond by calling * tmrTimerTick, which will "distribute" the timer tick * to those XTimers which have elapsed. * * The main advantage of the XTimers is that these * are not a limited resource (as opposed to PM timers). * I don't know how many PM timers exist on the system, * but PMREF says that the no. of remaining timers can * be queried with WinQuerySysValue(SV_CTIMERS). * * XTimers are used excessively by the default widgets * of XWorkplace's XCenter. * * There are a few limitations with the XTimers though: * * -- tmrTimerTick (which you must call when the * "master" WM_TIMER comes in) does not post WM_TIMER * messages to the windows specified in the subtimers, * but calls the window procedure of the target window * directly. This makes sure that timers work even if * some thread is hogging the SIQ. * * This however requires that all XTimers must run on * the same thread as the owner window of the master * timer which was specified with tmrCreateSet. * * -- Queue timers (with HWND == NULLHANDLE) are not * supported. * * -- When a window is destroyed, its timers are not * automatically cleaned up. tmrTimerTick does * detect invalid windows and removes them from the * timers list before posting, but to be on the safe * side, always call tmrStopAllTimers when WM_DESTROY * comes into a window which has used timers. * * So to use XTimers, do the following: * * 1. Create a timer set with tmrCreateSet. Specify * an owner window and the timer ID of the master * PM timer. * * 2. You can then start and stop XTimers for windows * on the same thread by calling tmrStartXTimer and * tmrStopXTimer, respectively. * * 3. In the window proc of the owner window, respond * to WM_TIMER for the master PM timer by calling * tmrTimerTick. This will call the window procs * of those windows with WM_TIMER messages directly * whose XTimers have elapsed. * * Function prefixes: * -- tmr* timer functions * *@@header "helpers\timer.h" *@@added V0.9.7 (2000-12-04) [umoeller] */ /* * Copyright (C) 2000-2005 Ulrich M”ller. * This file is part of the "XWorkplace helpers" source package. * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, in version 2 as it comes in the * "COPYING" file of the XWorkplace main distribution. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ // OS2 includes #define OS2EMX_PLAIN_CHAR // this is needed for "os2emx.h"; if this is defined, // emx will define PSZ as _signed_ char, otherwise // as unsigned char #define INCL_DOSEXCEPTIONS #define INCL_DOSPROCESS #define INCL_DOSSEMAPHORES #define INCL_DOSMISC #define INCL_DOSERRORS #define INCL_WINWINDOWMGR #define INCL_WINMESSAGEMGR #define INCL_WINTIMER #include #include #include #include "setup.h" // code generation and debugging options #include "helpers\datetime.h" #include "helpers\except.h" #include "helpers\linklist.h" #include "helpers\math.h" #include "helpers\standards.h" #include "helpers\threads.h" #include "helpers\timer.h" // #define DEBUG_XTIMERS /* *@@category: Helpers\PM helpers\Timer replacements * see timer.c. */ /* ****************************************************************** * * Private declarations * ********************************************************************/ /* *@@ XTIMER: * one of these represents an XTimer. * These are stored in a linked list in * an _XTIMERSET. */ typedef struct _XTIMER { USHORT usTimerID; // timer ID, as passed to tmrStartXTimer HWND hwndTarget; // target window, as passed to tmrStartXTimer ULONG ulTimeout; // timer's timeout (in ms) ULONG ulNextFire; // next time scalar (from dtGetULongTime) to fire at } XTIMER, *PXTIMER; /* ****************************************************************** * * Global variables * ********************************************************************/ HMTX G_hmtxTimers = NULLHANDLE; // timers lock mutex /* ****************************************************************** * * Private functions * ********************************************************************/ /* *@@ LockTimers: * *@@added V0.9.12 (2001-05-12) [umoeller] */ STATIC BOOL LockTimers(VOID) { if (!G_hmtxTimers) return !DosCreateMutexSem(NULL, &G_hmtxTimers, 0, TRUE); // request! return !DosRequestMutexSem(G_hmtxTimers, SEM_INDEFINITE_WAIT); } /* *@@ UnlockTimers: * *@@added V0.9.12 (2001-05-12) [umoeller] */ STATIC VOID UnlockTimers(VOID) { DosReleaseMutexSem(G_hmtxTimers); } /* *@@ FindTimer: * returns the XTIMER structure from the global * linked list which matches the given window * _and_ timer ID. * * Internal function. Caller must hold the mutex. */ STATIC PXTIMER FindTimer(PXTIMERSET pSet, // in: timer set (from tmrCreateSet) HWND hwnd, // in: timer target window USHORT usTimerID) // in: timer ID { PLINKLIST pllXTimers; if ( (pSet) && (pllXTimers = (PLINKLIST)pSet->pvllXTimers) ) { PLISTNODE pNode = lstQueryFirstNode(pllXTimers); while (pNode) { PXTIMER pTimer = (PXTIMER)pNode->pItemData; if ( (pTimer->usTimerID == usTimerID) && (pTimer->hwndTarget == hwnd) ) { return pTimer; } pNode = pNode->pNext; } } return NULL; } /* *@@ RemoveTimer: * removes the specified XTIMER structure from * the global linked list of running timers. * * Internal function. Caller must hold the mutex. */ STATIC VOID RemoveTimer(PXTIMERSET pSet, // in: timer set (from tmrCreateSet) PXTIMER pTimer) // in: timer to remove { PLINKLIST pllXTimers; if ( (pSet) && (pllXTimers = (PLINKLIST)pSet->pvllXTimers) ) { #ifdef DEBUG_XTIMERS _Pmpf((__FUNCTION__ ": removing timer %d", pTimer->usTimerID)); #endif lstRemoveItem(pllXTimers, pTimer); // auto-free! } } /* *@@ AdjustPMTimer: * goes thru all XTimers in the sets and starts * or stops the PM timer with a decent frequency. * * Internal function. Caller must hold the mutex. * *@@added V0.9.9 (2001-03-07) [umoeller] *@@changed V0.9.14 (2001-07-07) [umoeller]: added GCD optimizations */ STATIC VOID AdjustPMTimer(PXTIMERSET pSet) { PLINKLIST pllXTimers = (PLINKLIST)pSet->pvllXTimers; ULONG cTimers = lstCountItems(pllXTimers); #ifdef DEBUG_XTIMERS _Pmpf((__FUNCTION__ ": entering")); #endif if (!cTimers) { // no XTimers running: if (pSet->idPMTimerRunning) { // but PM timer running: // stop it WinStopTimer(pSet->hab, pSet->hwndOwner, pSet->idPMTimer); pSet->idPMTimerRunning = 0; } pSet->ulPMTimeout = 0; } else { // we have timers: ULONG ulOldPMTimeout = pSet->ulPMTimeout; PLISTNODE pNode = lstQueryFirstNode(pllXTimers); PXTIMER pTimer1 = (PXTIMER)pNode->pItemData; if (cTimers == 1) { // only one timer: // that's easy #ifdef DEBUG_XTIMERS _Pmpf((" got 1 timer")); #endif pSet->ulPMTimeout = pTimer1->ulTimeout; } else if (cTimers == 2) { // exactly two timers: // find the greatest common denominator PXTIMER pTimer2 = (PXTIMER)pNode->pNext->pItemData; #ifdef DEBUG_XTIMERS _Pmpf((" got 2 timers")); #endif pSet->ulPMTimeout = mathGCD(pTimer1->ulTimeout, pTimer2->ulTimeout); } else { // more than two timers: // run through all timers and find the greatest // common denominator of all frequencies... int *paInts = (int*)_alloca(sizeof(int) * cTimers), i = 0; #ifdef DEBUG_XTIMERS _Pmpf((" got %d timers", cTimers)); #endif // fill an array of integers with the // timer frequencies while (pNode) { pTimer1 = (PXTIMER)pNode->pItemData; #ifdef DEBUG_XTIMERS _Pmpf((" timeout %d is %d", i, pTimer1->ulTimeout)); #endif paInts[i++] = pTimer1->ulTimeout; pNode = pNode->pNext; } pSet->ulPMTimeout = mathGCDMulti(paInts, cTimers); } #ifdef DEBUG_XTIMERS _Pmpf(("--> GCD is %d", pSet->ulPMTimeout)); #endif if ( (!pSet->idPMTimerRunning) // timer not running? || (pSet->ulPMTimeout != ulOldPMTimeout) // timeout changed? ) { // start or restart PM timer pSet->idPMTimerRunning = WinStartTimer(pSet->hab, pSet->hwndOwner, pSet->idPMTimer, pSet->ulPMTimeout); } } } /* ****************************************************************** * * Exported functions * ********************************************************************/ /* *@@ tmrCreateSet: * creates a "timer set" for use with the XTimer functions. * This is the first step if you want to use the XTimers. * * hwndOwner must specify the "owner window", the target * window for the master PM timer. This window must respond * to a WM_TIMER message with the specified usPMTimerID and * invoke tmrTimerTick then. * * Note that the master timer is not started until an XTimer * is started. * * Use tmrDestroySet to free all resources again. * *@@added V0.9.9 (2001-02-28) [umoeller] */ PXTIMERSET tmrCreateSet(HWND hwndOwner, // in: owner window USHORT usPMTimerID) { PXTIMERSET pSet = NEW(XTIMERSET); if (pSet) { pSet->hab = WinQueryAnchorBlock(hwndOwner); pSet->hwndOwner = hwndOwner; pSet->idPMTimer = usPMTimerID; pSet->idPMTimerRunning = 0; pSet->ulPMTimeout = 0; pSet->pvllXTimers = (PVOID)lstCreate(TRUE); } return pSet; } /* *@@ tmrDestroySet: * destroys a timer set previously created using * tmrCreateSet. * * Of course, this will stop all XTimers which * might still be running with this set. * *@@added V0.9.9 (2001-02-28) [umoeller] *@@changed V0.9.12 (2001-05-12) [umoeller]: added mutex protection */ VOID tmrDestroySet(PXTIMERSET pSet) // in: timer set (from tmrCreateSet) { if (LockTimers()) { if (pSet) { PLINKLIST pllXTimers; if (pllXTimers = (PLINKLIST)pSet->pvllXTimers) { PLISTNODE pTimerNode = lstQueryFirstNode(pllXTimers); while (pTimerNode) { PLISTNODE pNext = pTimerNode->pNext; PXTIMER pTimer = (PXTIMER)pTimerNode->pItemData; RemoveTimer(pSet, pTimer); pTimerNode = pNext; } lstFree(&pllXTimers); pSet->pvllXTimers = NULL; } if (pSet->idPMTimerRunning) { WinStopTimer(pSet->hab, pSet->hwndOwner, pSet->idPMTimer); pSet->idPMTimerRunning = 0; } free(pSet); } UnlockTimers(); } } /* *@@ tmrTimerTick: * implements a PM timer tick. * * When your window procs receives WM_TIMER for the * one PM timer which is supposed to trigger all the * XTimers, it must call this function. This will * evaluate all XTimers on the list and "fire" them * by calling the window procs directly with the * WM_TIMER message. * *@@added V0.9.9 (2001-02-28) [umoeller] *@@changed V0.9.12 (2001-05-12) [umoeller]: added mutex protection *@@changed V0.9.12 (2001-05-24) [umoeller]: fixed crash if this got called during tmrTimerTick *@@changed V0.9.14 (2001-08-01) [umoeller]: fixed mem overwrite which might have caused crashes if this got called during tmrTimerTick *@@changed V0.9.14 (2001-08-03) [umoeller]: fixed "half frequency" regression caused by frequency optimizations *@@changed V0.9.16 (2001-12-18) [umoeller]: now using WinDispatchMsg to avoid crashes during win destruction *@@changed V0.9.19 (2002-05-04) [umoeller]: added excpt handling to avoid hanging all timers on the mutex */ VOID tmrTimerTick(PXTIMERSET pSet) // in: timer set (from tmrCreateSet) { volatile BOOL fLocked = FALSE; // XWP V1.0.4 (2005-10-09) [pr] TRY_LOUD(excpt1) { if (fLocked = LockTimers()) { PLINKLIST pllXTimers; if ( (pSet) && (pllXTimers = (PLINKLIST)pSet->pvllXTimers) ) { // go thru all XTimers and see which one // has elapsed; for all of these, post WM_TIMER // to the target window proc PLISTNODE pTimerNode; if (!(pTimerNode = lstQueryFirstNode(pllXTimers))) { // no timers left: if (pSet->idPMTimerRunning) { // but PM timer running: // stop it WinStopTimer(pSet->hab, pSet->hwndOwner, pSet->idPMTimer); pSet->idPMTimerRunning = 0; } pSet->ulPMTimeout = 0; } else { // we have timers: BOOL fFoundInvalid = FALSE; // get current time ULONG ulTimeNow = 0; DosQuerySysInfo(QSV_MS_COUNT, QSV_MS_COUNT, &ulTimeNow, sizeof(ulTimeNow)); #ifdef DEBUG_XTIMERS _Pmpf((__FUNCTION__ ": ulTimeNow = %d", ulTimeNow)); #endif while (pTimerNode) { // get next node first because the // list can get modified while processing // V0.9.12 (2001-05-24) [umoeller] PLISTNODE pNext = pTimerNode->pNext; PXTIMER pTimer = (PXTIMER)pTimerNode->pItemData; #ifdef DEBUG_XTIMERS _Pmpf((" timer %d: ulNextFire = %d", lstIndexFromItem(pllXTimers, pTimer), pTimer->ulNextFire)); #endif if ( (pTimer) // && (pTimer->ulNextFire < ulTimeNow) // V0.9.14 (2001-08-01) [umoeller] // use <= because otherwise we'll get // only half the frequency... // we get here frequently where the // two values are EXACTLY equal due // to the above optimization (DosQuerySysInfo // called once only for the entire loop) && (pTimer->ulNextFire <= ulTimeNow) ) { // this timer has elapsed: // fire! #ifdef DEBUG_XTIMERS _Pmpf((" --> fire!")); #endif if (WinIsWindow(pSet->hab, pTimer->hwndTarget)) { // window still valid: // get the window's window proc QMSG qmsg; PFNWP pfnwp = (PFNWP)WinQueryWindowPtr(pTimer->hwndTarget, QWP_PFNWP); // moved this up V0.9.14 (2001-08-01) [umoeller] pTimer->ulNextFire = ulTimeNow + pTimer->ulTimeout; // call the window proc DIRECTLY // V0.9.16 (2001-12-18) [umoeller]: // now using WinDispatchMsg to avoid crashes // while hwndTarget is being destroyed /* qmsg.hwnd = pTimer->hwndTarget; qmsg.msg = WM_TIMER; qmsg.mp1 = (MPARAM)pTimer->usTimerID; qmsg.mp2 = (MPARAM)0; qmsg.time = 0; qmsg.ptl.x = 0; qmsg.ptl.y = 0; qmsg.reserved = 0; WinDispatchMsg(pSet->hab, &qmsg); */ pfnwp(pTimer->hwndTarget, WM_TIMER, (MPARAM)pTimer->usTimerID, 0); // V0.9.12 (2001-05-24) [umoeller] // if the winproc chooses to start or // stop a timer, pNext still points // to a valid node... // -- if a timer is removed, that's OK // -- if a timer is added, it is added to // the list, so we'll see it in this loop // V0.9.14 (2001-08-01) [umoeller] // DO NOT REFERENCE pTimer AFTER THIS CODE; // the winproc might have removed the timer, // and since the list is auto-free, pTimer // might have been freed!! } else { // window has been destroyed: lstRemoveNode(pllXTimers, pTimerNode); // pNext is still valid fFoundInvalid = TRUE; } } // end if (pTimer->ulNextFire < ulTimeNow) // next timer pTimerNode = pNext; // V0.9.12 (2001-05-24) [umoeller] } // end while (pTimerNode) // destroy invalid timers, if any if (fFoundInvalid) AdjustPMTimer(pSet); } // end else if (!pTimerNode) } // end if (pllXTimers) } // end if (LockTimers()) } CATCH(excpt1) {} END_CATCH(); if (fLocked) UnlockTimers(); } /* *@@ tmrStartXTimer: * starts an XTimer. * * Any window can request an XTimer using * this function. This operates similar to * WinStartTimer, except that the number of * XTimers is not limited. * * Returns the ID of a new timer or resets an * existing timer (if usTimerID is already used * with hwnd). Use tmrStopXTimer to stop the timer. * * Returns 0 if an error occurred. It is thus * invalid to specify a timer ID of 0. * * The timer is _not_ stopped automatically * when the window is destroyed. * * Note: Unless you own the timer set that * your timer runs on, it is strongly recommended * that your timer frequency is set to a multiple * of 125. The PM master timer behind the timer * set will be set to the greatest common divisor * of all frequencies, and if you set one timer * to 2000 and the other one to 2001, you will * cause quite a lot of overhead. This applies * especially to timers started by XCenter widgets. * * For security, all timer frequencies will be * rounded to multiples of 25 anyway. Still, * running two timers at 1000 and 1025 will cause * the master timer to be set to 25, which is * overkill. * *@@changed V0.9.7 (2000-12-08) [umoeller]: got rid of dtGetULongTime *@@changed V0.9.12 (2001-05-12) [umoeller]: added mutex protection *@@changed V0.9.14 (2001-07-12) [umoeller]: now rounding freq's to multiples of 25 */ USHORT XWPENTRY tmrStartXTimer(PXTIMERSET pSet, // in: timer set (from tmrCreateSet) HWND hwnd, // in: target window for XTimer USHORT usTimerID, // in: timer ID for XTimer's WM_TIMER (must be > 0) ULONG ulTimeout) // in: XTimer's timeout { USHORT usrc = 0; // _Pmpf((__FUNCTION__ ": entering")); if (LockTimers()) { PLINKLIST pllXTimers; if ( (pSet) && (pllXTimers = (PLINKLIST)pSet->pvllXTimers) && (hwnd) && (ulTimeout) && (usTimerID) // V0.9.16 (2001-12-18) [umoeller] ) { PXTIMER pTimer; ULONG ulTimeNow; // fix the timeout... we allow only multiples of // 25, and it must be at least 25 (otherwise our // internal master timer calculations will fail) // V0.9.14 (2001-07-07) [umoeller] if (ulTimeout < 25) ulTimeout = 25; else ulTimeout = (ulTimeout + 10) / 25 * 25; DosQuerySysInfo(QSV_MS_COUNT, QSV_MS_COUNT, &ulTimeNow, sizeof(ulTimeNow)); // check if this timer exists already if (pTimer = FindTimer(pSet, hwnd, usTimerID)) { // exists already: reset only pTimer->ulNextFire = ulTimeNow + ulTimeout; usrc = usTimerID; } else { // new timer needed: if (pTimer = NEW(XTIMER)) { pTimer->usTimerID = usTimerID; pTimer->hwndTarget = hwnd; pTimer->ulTimeout = ulTimeout; pTimer->ulNextFire = ulTimeNow + ulTimeout; lstAppendItem(pllXTimers, pTimer); usrc = usTimerID; } } if (usrc) // timer created or reset: AdjustPMTimer(pSet); } // if ((hwnd) && (ulTimeout)) UnlockTimers(); } return usrc; } /* *@@ tmrStopXTimer: * similar to WinStopTimer, this stops the * specified timer (which must have been * started with the same hwnd and usTimerID * using tmrStartXTimer). * * Returns TRUE if the timer was stopped. * *@@changed V0.9.12 (2001-05-12) [umoeller]: added mutex protection */ BOOL XWPENTRY tmrStopXTimer(PXTIMERSET pSet, // in: timer set (from tmrCreateSet) HWND hwnd, USHORT usTimerID) { BOOL brc = FALSE; if (LockTimers()) { PXTIMER pTimer; #ifdef DEBUG_XTIMERS _Pmpf((__FUNCTION__ ": finding timer %d", usTimerID)); #endif if (pTimer = FindTimer(pSet, hwnd, usTimerID)) // FindTimer checks the params { RemoveTimer(pSet, pTimer); // recalculate AdjustPMTimer(pSet); brc = TRUE; } UnlockTimers(); } return brc; }