288 lines
9.9 KiB
C++
288 lines
9.9 KiB
C++
/*****************************************************************************
|
|
*
|
|
* 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);
|
|
}
|