/***************************************************************************** * * ftpgto.cpp - Global timeouts * * Global timeouts are managed by a separate worker thread, whose job * it is to hang around and perform delayed actions on request. * * All requests are for FTP_SESSION_TIME_OUT milliseconds. If nothing happens * for an additional FTP_SESSION_TIME_OUT milliseconds, the worker thread is * terminated. * *****************************************************************************/ #include "priv.h" #include "util.h" #define MS_PER_SECOND 1000 #define SECONDS_PER_MINUTE 60 #define FTP_SESSION_TIME_OUT (10 * SECONDS_PER_MINUTE * MS_PER_SECOND) // Survive 10 minutes in cache BOOL g_fBackgroundThreadStarted; // Has the background thread started? HANDLE g_hthWorker; // Background worker thread HANDLE g_hFlushDelayedActionsEvent = NULL; // Do we want to flush the delayed actions? /***************************************************************************** * * Global Timeout Info * * We must allocate separate information to track timeouts. Stashing * the information into a buffer provided by the caller opens race * conditions, if the caller frees the memory before we are ready. * * dwTrigger is 0 if the timeout is being dispatched. This avoids * race conditions where one thread triggers a timeout manually * while it is in progress. * *****************************************************************************/ struct GLOBALTIMEOUTINFO g_gti = { // Anchor of global timeout info list &g_gti, &g_gti, 0, 0, 0 }; /***************************************************************************** * TriggerDelayedAction * * Unlink the node and dispatch the timeout procedure. *****************************************************************************/ void TriggerDelayedAction(LPGLOBALTIMEOUTINFO * phgti) { LPGLOBALTIMEOUTINFO hgti = *phgti; *phgti = NULL; if (hgti) { ENTERCRITICAL; if (hgti->dwTrigger) { // Unlink the node hgti->hgtiPrev->hgtiNext = hgti->hgtiNext; hgti->hgtiNext->hgtiPrev = hgti->hgtiPrev; hgti->dwTrigger = 0; // Do the callback if (hgti->pfn) hgti->pfn(hgti->pvRef); LEAVECRITICAL; TraceMsg(TF_BKGD_THREAD, "TriggerDelayedAction(%#08lx) Freeing=%#08lx", phgti, hgti); DEBUG_CODE(memset(hgti, 0xFE, (UINT) LocalSize((HLOCAL)hgti))); LocalFree((LPVOID) hgti); } else { LEAVECRITICAL; } } } /***************************************************************************** * FtpDelayedActionWorkerThread * * This is the procedure that runs on the worker thread. It waits * for something to do, and if enough time elapses with nothing * to do, it terminates. * * Be extremely mindful of race conditions. They are oft subtle * and quick to anger. *****************************************************************************/ DWORD FtpDelayedActionWorkerThread(LPVOID pv) { // Tell the caller we started so they can continue. g_fBackgroundThreadStarted = TRUE; for (;;) { DWORD msWait; // Determine how long we need to wait. The critical section // is necessary to ensure we don't collide with SetDelayedAction. ENTERCRITICAL; if (g_gti.hgtiNext == &g_gti) { // Queue is empty msWait = FTP_SESSION_TIME_OUT; } else { msWait = g_gti.hgtiNext->dwTrigger - GetTickCount(); } LEAVECRITICAL; // If a new delayed action gets added, no matter, because // we will wake up from the sleep before the delayed action // is due. ASSERTNONCRITICAL; if ((int)msWait > 0) { TraceMsg(TF_BKGD_THREAD, "FtpDelayedActionWorkerThread: Sleep(%d)", msWait); WaitForMultipleObjects(1, &g_hFlushDelayedActionsEvent, FALSE, msWait); TraceMsg(TF_BKGD_THREAD, "FtpDelayedActionWorkerThread: Sleep finished"); } ENTERCRITICALNOASSERT; if ((g_gti.hgtiNext != &g_gti) && g_gti.hgtiNext && (g_gti.hgtiNext->phgtiOwner)) { // Queue has work // RaymondC made a comment here that there is a race condition but I have never // been able to see it. He made this comment years ago when he owned the code // and I've re-writen parts and ensured it's thread safe. We never found any // stress problems so this is just a reminder that this code is very thread // sensitive. LEAVECRITICAL; TraceMsg(TF_BKGD_THREAD, "FtpDelayedActionWorkerThread: Dispatching"); TriggerDelayedAction(g_gti.hgtiNext->phgtiOwner); } else { CloseHandle(InterlockedExchangePointer(&g_hthWorker, NULL)); CloseHandle(InterlockedExchangePointer(&g_hFlushDelayedActionsEvent, NULL)); LEAVECRITICALNOASSERT; TraceMsg(TF_BKGD_THREAD, "FtpDelayedActionWorkerThread: ExitThread"); ExitThread(0); } } AssertMsg(0, TEXT("FtpDelayedActionWorkerThread() We should never get here or we are exiting the for loop incorrectly.")); return 0; } /***************************************************************************** * SetDelayedAction * * If there is a previous action, it is triggered. (Not cancelled.) * * In principle, we could've allocated into a private pointer, then * stuffed the pointer in at the last minute, avoiding the need to * take the critical section so aggressively. But that would tend * to open race conditions in the callers. So? I should * fix the bugs instead of hacking around them like this. *****************************************************************************/ STDMETHODIMP SetDelayedAction(DELAYEDACTIONPROC pfn, LPVOID pvRef, LPGLOBALTIMEOUTINFO * phgti) { TriggerDelayedAction(phgti); ENTERCRITICAL; if (!g_hthWorker) { DWORD dwThid; g_hFlushDelayedActionsEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (g_hFlushDelayedActionsEvent) { g_fBackgroundThreadStarted = FALSE; g_hthWorker = CreateThread(0, 0, FtpDelayedActionWorkerThread, 0, 0, &dwThid); if (g_hthWorker) { // We need to wait until the thread starts up // before we return. Otherwise, we may return to the // caller and they may free our COM object // which will unload our DLL. The thread won't // start if we are in PROCESS_DLL_DETACH and we // spin waiting for them to start and stop. TraceMsg(TF_BKGD_THREAD, "SetDelayedAction: Thread created, waiting for it to start."); while (FALSE == g_fBackgroundThreadStarted) Sleep(0); TraceMsg(TF_BKGD_THREAD, "SetDelayedAction: Thread started."); } else { CloseHandle(g_hFlushDelayedActionsEvent); g_hFlushDelayedActionsEvent = NULL; } } } if (g_hthWorker && EVAL(*phgti = (LPGLOBALTIMEOUTINFO) LocalAlloc(LPTR, sizeof(GLOBALTIMEOUTINFO)))) { LPGLOBALTIMEOUTINFO hgti = *phgti; // Insert the node at the end (i.e., before the head) hgti->hgtiPrev = g_gti.hgtiPrev; g_gti.hgtiPrev->hgtiNext = hgti; g_gti.hgtiPrev = hgti; hgti->hgtiNext = &g_gti; // The "|1" ensures that dwTrigger is not zero hgti->dwTrigger = (GetTickCount() + FTP_SESSION_TIME_OUT) | 1; hgti->pfn = pfn; hgti->pvRef = pvRef; hgti->phgtiOwner = phgti; // Note that there is no need to signal the worker thread that // there is new work to do, because he will always wake up on // his own before the requisite time has elapsed. // // This optimization relies on the fact that the worker thread // idle time is less than or equal to our delayed action time. LEAVECRITICAL; } else { // Unable to create worker thread or alloc memory LEAVECRITICAL; } return S_OK; } HRESULT PurgeDelayedActions(void) { HRESULT hr = E_FAIL; if (g_hFlushDelayedActionsEvent) { LPGLOBALTIMEOUTINFO hgti = g_gti.hgtiNext; // We need to set all the times to zero so all waiting // items will not be delayed. ENTERCRITICAL; while (hgti != &g_gti) { hgti->dwTrigger = (GetTickCount() - 3); // Don't Delay... hgti = hgti->hgtiNext; // Next... } LEAVECRITICAL; if (SetEvent(g_hFlushDelayedActionsEvent)) { // We can't be in a critical section or our background // thread can't come alive. ASSERTNONCRITICAL; TraceMsg(TF_BKGD_THREAD, "PurgeDelayedActions: Waiting for thread to stop."); // Now just wait for the thread to finish. Someone may kill // the thread so let's make sure we don't keep sleeping // if the thread died. while (g_hthWorker && (WAIT_TIMEOUT == WaitForSingleObject(g_hthWorker, 0))) Sleep(0); TraceMsg(TF_BKGD_THREAD, "PurgeDelayedActions: Thread stopped."); // Sleep 0.1 seconds in order to give enough time for caller // to call CloseHandle(), LEAVECRITICAL, ExitThread(0). // I would much prefer to call WaitForSingleObject() on // the thread handle but I can't do that in PROCESS_DLL_DETACH. Sleep(100); hr = S_OK; } } return hr; } BOOL AreOutstandingDelayedActions(void) { return (g_gti.hgtiNext != &g_gti); }