You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1317 lines
33 KiB
1317 lines
33 KiB
// RotObj.cpp : Implementation of CContentRotator class, which does all the
|
|
// interesting work
|
|
|
|
// George V. Reilly [email protected] [email protected] Nov/Dec 96
|
|
|
|
// Shortcomings: this works fine for small-to-medium sized tip files
|
|
// (under 2000 lines), but it's not very efficient for large ones.
|
|
|
|
|
|
#include "stdafx.h"
|
|
#include <new>
|
|
#include "ContRot.h"
|
|
#include "RotObj.h"
|
|
|
|
#include "debug.h"
|
|
#include <time.h>
|
|
#include "Monitor.h"
|
|
|
|
#define MAX_WEIGHT 10000
|
|
#define INVALID_WEIGHT 0xFFFFFFFF
|
|
|
|
|
|
extern CMonitor* g_pMonitor;
|
|
|
|
//
|
|
// forward declaration of some utility functions
|
|
//
|
|
|
|
LPTSTR TcsDup(LPCTSTR ptsz);
|
|
LPTSTR GetLine(LPTSTR& rptsz);
|
|
BOOL IsBlankString(LPCTSTR ptsz);
|
|
UINT GetWeight(LPTSTR& rptsz);
|
|
LPTSTR GetTipText(LPTSTR& rptsz);
|
|
HRESULT ReportError(DWORD dwErr);
|
|
HRESULT ReportError(HRESULT hr);
|
|
|
|
|
|
#if DBG
|
|
#define ASSERT_VALID(pObj) \
|
|
do {ASSERT(pObj != NULL); pObj->AssertValid();} while (0)
|
|
#else
|
|
#define ASSERT_VALID(pObj) ((void)0)
|
|
#endif
|
|
|
|
|
|
class CTipNotify : public CMonitorNotify
|
|
{
|
|
public:
|
|
CTipNotify();
|
|
virtual void Notify();
|
|
bool IsNotified();
|
|
private:
|
|
long m_isNotified;
|
|
};
|
|
|
|
DECLARE_REFPTR( CTipNotify,CMonitorNotify )
|
|
|
|
CTipNotify::CTipNotify()
|
|
: m_isNotified(0)
|
|
{
|
|
}
|
|
|
|
void
|
|
CTipNotify::Notify()
|
|
{
|
|
::InterlockedExchange( &m_isNotified, 1 );
|
|
}
|
|
|
|
bool
|
|
CTipNotify::IsNotified()
|
|
{
|
|
return ( ::InterlockedExchange( &m_isNotified, 0 ) ? true : false );
|
|
}
|
|
|
|
|
|
//
|
|
// "Tip", as in tip of the day
|
|
//
|
|
|
|
class CTip
|
|
{
|
|
public:
|
|
CTip(
|
|
LPCTSTR ptszTip,
|
|
UINT uWeight)
|
|
: m_ptsz(ptszTip),
|
|
m_uWeight(uWeight),
|
|
m_cServingsLeft(uWeight),
|
|
m_pPrev(NULL),
|
|
m_pNext(NULL)
|
|
{
|
|
ASSERT_VALID(this);
|
|
}
|
|
|
|
~CTip()
|
|
{
|
|
ASSERT_VALID(this);
|
|
if (m_pPrev != NULL)
|
|
m_pPrev->m_pNext = NULL;
|
|
if (m_pNext != NULL)
|
|
m_pNext->m_pPrev = NULL;
|
|
}
|
|
|
|
#if DBG
|
|
void
|
|
AssertValid() const;
|
|
#endif
|
|
|
|
LPCTSTR m_ptsz; // data string
|
|
UINT m_uWeight; // weight of this tip, 1 <= m_uWeight <= MAX_WEIGHT
|
|
UINT m_cServingsLeft;// how many servings left: no more than m_uWeight
|
|
CTip* m_pPrev; // Previous in tips list
|
|
CTip* m_pNext; // Next in tips list
|
|
};
|
|
|
|
|
|
//
|
|
// A list of CTips, which are read from a datafile
|
|
//
|
|
|
|
class CTipList
|
|
{
|
|
public:
|
|
CTipList()
|
|
: m_ptszFilename(NULL),
|
|
m_ptszData(NULL),
|
|
m_cTips(0),
|
|
m_uTotalWeight(0),
|
|
m_pTipsListHead(NULL),
|
|
m_pTipsListTail(NULL),
|
|
m_fUTF8(false)
|
|
{
|
|
m_pNotify = new CTipNotify;
|
|
ASSERT_VALID(this);
|
|
}
|
|
|
|
~CTipList()
|
|
{
|
|
ASSERT_VALID(this);
|
|
|
|
// check for both a valid Filename ptr as well as a valid Monitor ptr.
|
|
// If the ContRotModule::Unlock is called prior to this destructor,then
|
|
// the Monitor object has already been cleaned up and deleted.
|
|
|
|
DeleteTips();
|
|
ASSERT_VALID(this);
|
|
}
|
|
|
|
HRESULT
|
|
ReadDataFile(
|
|
LPCTSTR ptszFilename);
|
|
|
|
HRESULT
|
|
SameAsCachedFile(
|
|
LPCTSTR ptszFilename,
|
|
BOOL& rfIsSame);
|
|
|
|
UINT
|
|
Rand() const;
|
|
|
|
void
|
|
AppendTip(
|
|
CTip* pTip);
|
|
|
|
void
|
|
RemoveTip(
|
|
CTip* pTip);
|
|
|
|
HRESULT
|
|
DeleteTips();
|
|
|
|
#if DBG
|
|
void
|
|
AssertValid() const;
|
|
#endif
|
|
|
|
LPTSTR m_ptszFilename; // Name of tips file
|
|
LPTSTR m_ptszData; // Buffer containing contents of file
|
|
UINT m_cTips; // # tips
|
|
UINT m_uTotalWeight; // sum of all weights
|
|
CTip* m_pTipsListHead; // Head of list of tips
|
|
CTip* m_pTipsListTail; // Tail of list of tips
|
|
CTipNotifyPtr m_pNotify;
|
|
bool m_fUTF8;
|
|
};
|
|
|
|
|
|
|
|
//
|
|
// A class that allows you to enter a critical section and automatically
|
|
// leave when the object of this class goes out of scope. Also provides
|
|
// the means to leave and re-enter as needed while protecting against
|
|
// entering or leaving out of sync.
|
|
//
|
|
|
|
class CAutoLeaveCritSec
|
|
{
|
|
public:
|
|
CAutoLeaveCritSec(
|
|
CRITICAL_SECTION* pCS)
|
|
: m_pCS(pCS), m_fInCritSec(FALSE)
|
|
{Enter();}
|
|
|
|
~CAutoLeaveCritSec()
|
|
{Leave();}
|
|
|
|
// Use this function to re-enter the critical section.
|
|
void Enter()
|
|
{if (!m_fInCritSec) {EnterCriticalSection(m_pCS); m_fInCritSec = TRUE;}}
|
|
|
|
// Use this function to leave the critical section before going out
|
|
// of scope.
|
|
void Leave()
|
|
{if (m_fInCritSec) {LeaveCriticalSection(m_pCS); m_fInCritSec = FALSE;}}
|
|
|
|
protected:
|
|
CRITICAL_SECTION* m_pCS;
|
|
BOOL m_fInCritSec;
|
|
};
|
|
|
|
|
|
|
|
//
|
|
// Wrapper class for handles to files opened for reading
|
|
//
|
|
|
|
class CHFile
|
|
{
|
|
public:
|
|
CHFile(LPCTSTR ptszFilename)
|
|
{
|
|
m_hFile = ::CreateFile(ptszFilename, GENERIC_READ, FILE_SHARE_READ,
|
|
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
|
|
NULL);
|
|
}
|
|
|
|
~CHFile()
|
|
{
|
|
if (m_hFile != INVALID_HANDLE_VALUE)
|
|
::CloseHandle(m_hFile);
|
|
}
|
|
|
|
operator HANDLE() const
|
|
{return m_hFile;}
|
|
|
|
BOOL
|
|
operator!() const
|
|
{return (m_hFile == INVALID_HANDLE_VALUE);}
|
|
|
|
private:
|
|
// private, unimplemented default ctor, copy ctor, and op= to prevent
|
|
// compiler synthesizing them
|
|
CHFile();
|
|
CHFile(const CHFile&);
|
|
CHFile& operator=(const CHFile&);
|
|
|
|
HANDLE m_hFile;
|
|
};
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CContentRotator Public Methods
|
|
|
|
//
|
|
// ctor
|
|
//
|
|
|
|
CContentRotator::CContentRotator()
|
|
: m_ptl(NULL),
|
|
m_ptlUsed(NULL)
|
|
{
|
|
TRACE0("CContentRotator::CContentRotator\n");
|
|
|
|
InitializeCriticalSection(&m_CS);
|
|
#if (_WIN32_WINNT >= 0x0403)
|
|
SetCriticalSectionSpinCount(&m_CS, 1000);
|
|
#endif
|
|
|
|
// Seed the random-number generator with the current time so that
|
|
// the numbers will be different each time that we run
|
|
::srand((unsigned) time(NULL));
|
|
|
|
ATLTRY(m_ptl = new CTipList);
|
|
ATLTRY(m_ptlUsed = new CTipList);
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// dtor
|
|
//
|
|
|
|
CContentRotator::~CContentRotator()
|
|
{
|
|
TRACE0("CContentRotator::~CContentRotator\n");
|
|
|
|
DeleteCriticalSection(&m_CS);
|
|
delete m_ptl;
|
|
delete m_ptlUsed;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// ATL Wizard generates this
|
|
//
|
|
|
|
STDMETHODIMP CContentRotator::InterfaceSupportsErrorInfo(REFIID riid)
|
|
{
|
|
static const IID* arr[] =
|
|
{
|
|
&IID_IContentRotator,
|
|
};
|
|
|
|
for (int i=0; i<sizeof(arr)/sizeof(arr[0]); i++)
|
|
{
|
|
if (InlineIsEqualGUID(*arr[i],riid))
|
|
return S_OK;
|
|
}
|
|
return S_FALSE;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Read in the tips in bstrDataFile (a logical name), and return a random
|
|
// tip in pbstrRetVal
|
|
//
|
|
|
|
STDMETHODIMP
|
|
CContentRotator::ChooseContent(
|
|
BSTR bstrDataFile,
|
|
BSTR* pbstrRetVal)
|
|
{
|
|
HRESULT hr = E_FAIL;
|
|
|
|
try
|
|
{
|
|
// TRACE1("ChooseContent(%ls)\n", bstrDataFile);
|
|
|
|
if (bstrDataFile == NULL || pbstrRetVal == NULL)
|
|
return ::ReportError(E_POINTER);
|
|
else
|
|
*pbstrRetVal = NULL;
|
|
|
|
CContext cxt;
|
|
hr = cxt.Init( CContext::get_Server );
|
|
if ( !FAILED(hr) )
|
|
{
|
|
// Do we have valid CTipLists?
|
|
if ((m_ptl != NULL) && (m_ptlUsed != NULL))
|
|
{
|
|
// Map bstrDataFile (a logical name such as /controt/tips.txt) to
|
|
// a physical filesystem name such as d:\inetpub\controt\tips.txt.
|
|
CComBSTR bstrPhysicalDataFile;
|
|
hr = cxt.Server()->MapPath(bstrDataFile, &bstrPhysicalDataFile);
|
|
|
|
if (SUCCEEDED(hr))
|
|
hr = _ChooseContent(bstrPhysicalDataFile, pbstrRetVal);
|
|
}
|
|
else
|
|
{
|
|
hr = ::ReportError(E_OUTOFMEMORY);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hr = ::ReportError(E_NOINTERFACE);
|
|
}
|
|
}
|
|
catch ( std::bad_alloc& )
|
|
{
|
|
hr = ::ReportError(E_OUTOFMEMORY);
|
|
}
|
|
catch ( ... )
|
|
{
|
|
hr = E_FAIL;
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Writes all of the entries in the tip file, each separated by an <hr>, back
|
|
// to the user's browser. This can be used to proofread all of the entries.
|
|
//
|
|
|
|
STDMETHODIMP
|
|
CContentRotator::GetAllContent(
|
|
BSTR bstrDataFile)
|
|
{
|
|
HRESULT hr = E_FAIL;
|
|
try
|
|
{
|
|
if (bstrDataFile == NULL)
|
|
return ::ReportError(E_POINTER);
|
|
|
|
CContext cxt;
|
|
hr = cxt.Init( CContext::get_Server | CContext::get_Response );
|
|
// Do we have valid Server and Response objects?
|
|
if ( !FAILED( hr ) )
|
|
{
|
|
// Do we have valid CTipLists?
|
|
if ( (m_ptl != NULL) && (m_ptlUsed != NULL))
|
|
{
|
|
// Map bstrDataFile (a logical name such as /IISSamples/tips.txt) to
|
|
// a physical filesystem name such as d:\inetpub\IISSamples\tips.txt.
|
|
CComBSTR bstrPhysicalDataFile;
|
|
hr = cxt.Server()->MapPath(bstrDataFile, &bstrPhysicalDataFile);
|
|
|
|
// See note below about critical sections
|
|
CAutoLeaveCritSec alcs(&m_CS);
|
|
|
|
if (SUCCEEDED(hr))
|
|
hr = _ReadDataFile(bstrPhysicalDataFile, TRUE);
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
const CComVariant cvHR(OLESTR("\n<hr>\n\n"));
|
|
BOOL bFirstTip = TRUE;
|
|
|
|
for (CTip* pTip = m_ptl->m_pTipsListHead;
|
|
pTip != NULL;
|
|
pTip = pTip->m_pNext)
|
|
{
|
|
// Write the leading HR only on the first pass
|
|
|
|
if (bFirstTip == TRUE) {
|
|
cxt.Response()->Write(cvHR);
|
|
bFirstTip = FALSE;
|
|
}
|
|
|
|
// Write back to the user's browser, one tip at a time.
|
|
// This is more efficient than concatenating all of the
|
|
// tips into a potentially huge string and returning that.
|
|
|
|
CMBCSToWChar convStr;
|
|
BSTR pbstrTip;
|
|
|
|
// need to convert the string to Wide based on the UTF8 flag
|
|
|
|
if (hr = convStr.Init(pTip->m_ptsz, m_ptl->m_fUTF8 ? 65001 : CP_ACP)) {
|
|
break;
|
|
}
|
|
|
|
// make a proper BSTR out of the wide version
|
|
|
|
if (!(pbstrTip = ::SysAllocString(convStr.GetString()))) {
|
|
hr = ::ReportError( E_OUTOFMEMORY );
|
|
break;
|
|
}
|
|
|
|
cxt.Response()->Write(CComVariant(pbstrTip));
|
|
cxt.Response()->Write(cvHR);
|
|
|
|
::SysFreeString(pbstrTip);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
hr = ::ReportError(E_OUTOFMEMORY);
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
hr = ::ReportError(E_NOINTERFACE);
|
|
}
|
|
}
|
|
catch ( std::bad_alloc& )
|
|
{
|
|
hr = ::ReportError( E_OUTOFMEMORY );
|
|
}
|
|
catch ( ... )
|
|
{
|
|
hr = E_FAIL;
|
|
}
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CContentRotator Private Methods
|
|
|
|
//
|
|
// Do the work of ChooseContent, but with a real filename, not with a
|
|
// virtual filename
|
|
//
|
|
|
|
HRESULT
|
|
CContentRotator::_ChooseContent(
|
|
BSTR bstrPhysicalDataFile,
|
|
BSTR* pbstrRetVal)
|
|
{
|
|
ASSERT(bstrPhysicalDataFile != NULL && pbstrRetVal != NULL);
|
|
|
|
// The critical section ensures that the remaining code in this
|
|
// function is executing on only one thread at a time. This ensures
|
|
// that the cached contents of the tip list are consistent for the
|
|
// duration of a call.
|
|
|
|
// Actually, the critical section is not needed at all. Because we
|
|
// need to call Server.MapPath to map the virtual path of
|
|
// bstrDataFile to a physical filesystem path, the OnStartPage method
|
|
// must be called, as this is the only way that we can get access to
|
|
// the ScriptingContext object and thereby the Server object.
|
|
// However, the OnStartPage method is only called for page-level
|
|
// objects (object is created and destroyed in a single page) and for
|
|
// session-level objects. Page-level objects don't have to worry
|
|
// about protecting their data from multiple access (unless it's
|
|
// global data shared between several objects) and neither do
|
|
// session-level objects. Only application-level objects need worry
|
|
// about protecting their private data, but application-level objects
|
|
// don't give us any way to map the virtual path.
|
|
|
|
// The Content Rotator might be more useful it it were an
|
|
// application-level object. We would get better distribution of the
|
|
// tips (see below) and do a lot less rereading of the data file. The
|
|
// trivial changes necessary to accept a filesystem path, such as
|
|
// "D:\ContRot\tips.txt", instead of a virtual path, such as
|
|
// "/IISSamples/tips.txt",are left as an exercise for the reader.
|
|
|
|
CAutoLeaveCritSec alcs(&m_CS);
|
|
|
|
HRESULT hr = _ReadDataFile(bstrPhysicalDataFile, FALSE);
|
|
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
const UINT uRand = m_ptl->Rand();
|
|
UINT uCumulativeWeight = 0;
|
|
CTip* pTip = m_ptl->m_pTipsListHead;
|
|
LPCTSTR ptszTip = NULL;
|
|
|
|
for ( ; ; )
|
|
{
|
|
ASSERT_VALID(pTip);
|
|
|
|
ptszTip = pTip->m_ptsz;
|
|
uCumulativeWeight += pTip->m_uWeight;
|
|
|
|
if (uCumulativeWeight <= uRand)
|
|
pTip = pTip->m_pNext;
|
|
else
|
|
{
|
|
// Found the tip. Now we go through a bit of work to make
|
|
// sure that each tip is served up with the correct
|
|
// probability. If the tip has already been served up as
|
|
// many times as it's allowed (i.e., m_uWeight times), then
|
|
// it's moved to the Used List. Otherwise, it's (probably)
|
|
// moved to the end of the Tips List, to reduce the
|
|
// likelihood of it turning up too soon again and to
|
|
// randomize the order of the tips in the list. When all
|
|
// tips have been moved to the Used List, we start afresh.
|
|
|
|
// If the object is created, used, and destroyed in a
|
|
// single page (i.e., it's not a session-level object),
|
|
// then none of this does us any good. The list is in
|
|
// exactly the same order as it is in the data file and
|
|
// we just have to hope that Rand() does give us
|
|
// well-distributed random numbers.
|
|
|
|
// If you expect a single user to see more than one tip,
|
|
// you should use a session-level object, to benefit from
|
|
// the better distribution of tips. This would be the case
|
|
// if you're serving up tips from the same file on multiple
|
|
// pages, or if you have a page that automatically
|
|
// refreshes itself, such as the one included in the
|
|
// Samples directory.
|
|
|
|
if (--pTip->m_cServingsLeft > 0)
|
|
{
|
|
// Move it to the end of the list some of the time.
|
|
// If we move it there all of the time, then a heavily
|
|
// weighted tip is more likely to turn up a lot
|
|
// as the main list nears exhaustion.
|
|
if (rand() % 3 == 0)
|
|
{
|
|
// TRACE1("Move to End\n%s\n", ptszTip);
|
|
m_ptl->RemoveTip(pTip);
|
|
m_ptl->AppendTip(pTip);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TRACE1("Move to Used\n%s\n", ptszTip);
|
|
pTip->m_cServingsLeft = pTip->m_uWeight; // reset
|
|
m_ptl->RemoveTip(pTip);
|
|
m_ptlUsed->AppendTip(pTip);
|
|
|
|
if (m_ptl->m_cTips == 0)
|
|
{
|
|
TRACE0("List exhausted; swapping\n");
|
|
|
|
CTipList* const ptlTemp = m_ptl;
|
|
m_ptl = m_ptlUsed;
|
|
m_ptlUsed = ptlTemp;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// TRACE2("total weight = %u, rand = %u\n",
|
|
// m_ptl->m_uTotalWeight, uRand);
|
|
// TRACE1("tip = `%s'\n", ptszTip);
|
|
|
|
CMBCSToWChar convStr;
|
|
|
|
if (hr = convStr.Init(ptszTip, m_ptl->m_fUTF8 ? 65001 : CP_ACP));
|
|
|
|
else {
|
|
*pbstrRetVal = ::SysAllocString(convStr.GetString());
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
|
|
HRESULT
|
|
CContentRotator::_ReadDataFile(
|
|
BSTR bstrPhysicalDataFile,
|
|
BOOL fForceReread)
|
|
{
|
|
USES_CONVERSION; // needed for OLE2T
|
|
LPCTSTR ptszFilename = OLE2T(bstrPhysicalDataFile);
|
|
HRESULT hr = S_OK;
|
|
|
|
if (ptszFilename == NULL) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Have we cached this tips file already?
|
|
if (!fForceReread)
|
|
{
|
|
BOOL fIsSame;
|
|
HRESULT hr = m_ptl->SameAsCachedFile(ptszFilename, fIsSame);
|
|
|
|
TRACE(_T("%same file\n"), fIsSame ? _T("S") : _T("Not s"));
|
|
|
|
if (FAILED(hr) || fIsSame)
|
|
return hr;
|
|
}
|
|
|
|
// destroy any old tips
|
|
m_ptl->DeleteTips();
|
|
m_ptlUsed->DeleteTips();
|
|
|
|
hr = m_ptl->ReadDataFile(ptszFilename);
|
|
|
|
if (FAILED(hr)) {
|
|
m_ptl->DeleteTips();
|
|
m_ptlUsed->DeleteTips();
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// CTipList Public Methods
|
|
|
|
//
|
|
// Read a collection of tips from ptszDataFile
|
|
//
|
|
// The file format is zero or more copies of the following:
|
|
// One or more lines starting with "%%"
|
|
// Each %% line contains zero or more directives:
|
|
// #<weight> (positive integer, 1 <= weight <= MAX_WEIGHT)
|
|
// //<comment> (a comment that runs to the end of the line)
|
|
// The tip text follows, spread out over several lines
|
|
//
|
|
|
|
HRESULT
|
|
CTipList::ReadDataFile(
|
|
LPCTSTR ptszFilename)
|
|
{
|
|
TRACE1("ReadDataFile(%s)\n", ptszFilename);
|
|
|
|
UINT weightSum = 0;
|
|
|
|
if ( m_ptszFilename != NULL )
|
|
{
|
|
g_pMonitor->StopMonitoringFile( m_ptszFilename );
|
|
delete [] m_ptszFilename;
|
|
}
|
|
|
|
m_ptszFilename = TcsDup(ptszFilename);
|
|
|
|
if (m_ptszFilename == NULL)
|
|
return ::ReportError(E_OUTOFMEMORY);
|
|
|
|
// Open the file
|
|
CHFile hFile(m_ptszFilename);
|
|
|
|
if (!hFile)
|
|
return ::ReportError(::GetLastError());
|
|
|
|
// Get the last-write-time and the filesize
|
|
BY_HANDLE_FILE_INFORMATION bhfi;
|
|
|
|
if (!::GetFileInformationByHandle(hFile, &bhfi))
|
|
return ::ReportError(::GetLastError());
|
|
|
|
// If it's more than 4GB, let's not even think about it!
|
|
if (bhfi.nFileSizeHigh != 0)
|
|
return ::ReportError(E_OUTOFMEMORY);
|
|
|
|
// Calculate the number of TCHARs in the file
|
|
const DWORD cbFile = bhfi.nFileSizeLow;
|
|
const DWORD ctc = cbFile / sizeof(TCHAR);
|
|
|
|
// Allocate a buffer for the file's contents
|
|
m_ptszData = NULL;
|
|
ATLTRY(m_ptszData = new TCHAR [ctc + 2]);
|
|
if (m_ptszData == NULL)
|
|
return ::ReportError(E_OUTOFMEMORY);
|
|
|
|
// Read the file into the memory buffer. Let's be paranoid and
|
|
// not assume that ReadFile gives us the whole file in one chunk.
|
|
DWORD cbSeen = 0;
|
|
|
|
do
|
|
{
|
|
DWORD cbToRead = cbFile - cbSeen;
|
|
DWORD cbRead = 0;
|
|
|
|
if (!::ReadFile(hFile, ((LPBYTE) m_ptszData) + cbSeen,
|
|
cbToRead, &cbRead, NULL))
|
|
return ::ReportError(::GetLastError());
|
|
|
|
cbSeen += cbRead;
|
|
} while (cbSeen < cbFile);
|
|
|
|
m_ptszData[ctc] = _T('\0'); // Nul-terminate the string
|
|
|
|
LPTSTR ptsz = m_ptszData;
|
|
|
|
#ifdef _UNICODE
|
|
|
|
#error "This file should NOT be compiled with _UNICODE defined!!!
|
|
|
|
// Check for byte-order mark
|
|
if (*ptsz == 0xFFFE)
|
|
{
|
|
// Byte-reversed Unicode file. Swap the hi- and lo-bytes in each wchar
|
|
for ( ; ptsz < m_ptszData + ctc; ++ptsz)
|
|
{
|
|
BYTE* pb = (BYTE*) ptsz;
|
|
const BYTE bHi = pb[1];
|
|
pb[1] = pb[0];
|
|
pb[0] = bHi;
|
|
}
|
|
ptsz = m_ptszData;
|
|
}
|
|
|
|
if (*ptsz == 0xFEFF)
|
|
++ptsz; // skip the byte-order mark
|
|
#endif
|
|
|
|
// check for the UTF-8 BOM
|
|
if ((ctc > 3)
|
|
&& (ptsz[0] == (TCHAR)0xef)
|
|
&& (ptsz[1] == (TCHAR)0xbb)
|
|
&& (ptsz[2] == (TCHAR)0xbf)) {
|
|
|
|
// note its presence and advance the file pointer past it.
|
|
|
|
m_fUTF8 = true;
|
|
ptsz += 3;
|
|
}
|
|
|
|
// Finally, parse the file
|
|
while (ptsz < m_ptszData + ctc)
|
|
{
|
|
UINT uWeight = GetWeight(ptsz);
|
|
|
|
// a value of INVALID_WEIGHT for weight indicates that no weight was found,
|
|
// i.e. an invalid data file, or the value was not valid.
|
|
|
|
if (uWeight == INVALID_WEIGHT) {
|
|
return ::ReportError((DWORD)ERROR_INVALID_DATA);
|
|
}
|
|
|
|
weightSum += uWeight;
|
|
|
|
if (weightSum > MAX_WEIGHT) {
|
|
return ::ReportError((DWORD)ERROR_INVALID_DATA);
|
|
}
|
|
|
|
LPTSTR ptszTipText = GetTipText(ptsz);
|
|
|
|
if (!IsBlankString(ptszTipText) && uWeight > 0)
|
|
{
|
|
CTip* pTip = NULL;
|
|
ATLTRY(pTip = new CTip(ptszTipText, uWeight));
|
|
if (pTip == NULL)
|
|
return ::ReportError(E_OUTOFMEMORY);
|
|
AppendTip(pTip);
|
|
}
|
|
else if (ptsz < m_ptszData + ctc)
|
|
{
|
|
// not at a terminating "%%" line at the end of the data file
|
|
TRACE2("bad tip: tip = `%s', weight = %u\n", ptszTipText, uWeight);
|
|
}
|
|
}
|
|
|
|
g_pMonitor->MonitorFile( m_ptszFilename, m_pNotify );
|
|
|
|
if (m_uTotalWeight == 0 || m_cTips == 0)
|
|
return ::ReportError((DWORD)ERROR_INVALID_DATA);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Is ptszFilename the same file as m_ptszFilename in both its name
|
|
// and timestamp?
|
|
//
|
|
|
|
HRESULT
|
|
CTipList::SameAsCachedFile(
|
|
LPCTSTR ptszFilename,
|
|
BOOL& rfIsSame)
|
|
{
|
|
rfIsSame = FALSE;
|
|
|
|
// Have we cached a file at all?
|
|
if (m_ptszFilename == NULL)
|
|
return S_OK;
|
|
|
|
// Are the names the same?
|
|
if (_tcsicmp(ptszFilename, m_ptszFilename) != 0)
|
|
return S_OK;
|
|
|
|
#if 1
|
|
// FILETIME ftLastWriteTime;
|
|
// CHFile hFile(ptszFilename);
|
|
//
|
|
// if (!hFile)
|
|
// return ::ReportError(::GetLastError());
|
|
//
|
|
// if (!::GetFileTime(hFile, NULL, NULL, &ftLastWriteTime))
|
|
// return ::ReportError(::GetLastError());
|
|
//
|
|
// rfIsSame = (::CompareFileTime(&ftLastWriteTime, &m_ftLastWriteTime) == 0);
|
|
if ( !m_pNotify->IsNotified() )
|
|
{
|
|
rfIsSame = TRUE;
|
|
}
|
|
#else
|
|
// The following is more efficient, but it won't work on Win95 with
|
|
// Personal Web Server because GetFileAttributesEx is new to NT 4.0.
|
|
|
|
WIN32_FILE_ATTRIBUTE_DATA wfad;
|
|
|
|
if (!::GetFileAttributesEx(ptszFilename, GetFileExInfoStandard,
|
|
(LPVOID) &wfad))
|
|
return ::ReportError(::GetLastError());
|
|
|
|
rfIsSame = (::CompareFileTime(&wfad.ftLastWriteTime,
|
|
&m_ftLastWriteTime) == 0);
|
|
#endif
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Generate a random number in the range 0..m_uTotalWeight-1
|
|
//
|
|
|
|
UINT
|
|
CTipList::Rand() const
|
|
{
|
|
UINT u;
|
|
|
|
ASSERT(m_uTotalWeight > 0);
|
|
|
|
if (m_uTotalWeight == 1)
|
|
return 0;
|
|
else if (m_uTotalWeight <= RAND_MAX + 1)
|
|
u = rand() % m_uTotalWeight;
|
|
else
|
|
{
|
|
// RAND_MAX is only 32,767. This gives us a bigger range
|
|
// of random numbers if the weights are large.
|
|
u = ((rand() << 15) | rand()) % m_uTotalWeight;
|
|
}
|
|
|
|
ASSERT(0 <= u && u < m_uTotalWeight);
|
|
|
|
return u;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Append a tip to the list
|
|
//
|
|
|
|
void
|
|
CTipList::AppendTip(
|
|
CTip* pTip)
|
|
{
|
|
ASSERT_VALID(this);
|
|
|
|
pTip->m_pPrev = pTip->m_pNext = NULL;
|
|
ASSERT_VALID(pTip);
|
|
|
|
pTip->m_pPrev = m_pTipsListTail;
|
|
|
|
if (m_pTipsListTail == NULL)
|
|
m_pTipsListHead = pTip;
|
|
else
|
|
m_pTipsListTail->m_pNext = pTip;
|
|
|
|
m_pTipsListTail = pTip;
|
|
++m_cTips;
|
|
m_uTotalWeight += pTip->m_uWeight;
|
|
|
|
ASSERT_VALID(this);
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Remove a tip from somewhere in the list
|
|
//
|
|
|
|
void
|
|
CTipList::RemoveTip(
|
|
CTip* pTip)
|
|
{
|
|
ASSERT_VALID(this);
|
|
ASSERT_VALID(pTip);
|
|
|
|
ASSERT(m_cTips > 0);
|
|
|
|
if (m_cTips == 1)
|
|
{
|
|
ASSERT(m_pTipsListHead == pTip && pTip == m_pTipsListTail);
|
|
m_pTipsListHead = m_pTipsListTail = NULL;
|
|
}
|
|
else if (pTip == m_pTipsListHead)
|
|
{
|
|
ASSERT(m_pTipsListHead->m_pNext != NULL);
|
|
m_pTipsListHead = m_pTipsListHead->m_pNext;
|
|
m_pTipsListHead->m_pPrev = NULL;
|
|
}
|
|
else if (pTip == m_pTipsListTail)
|
|
{
|
|
ASSERT(m_pTipsListTail->m_pPrev != NULL);
|
|
m_pTipsListTail = m_pTipsListTail->m_pPrev;
|
|
m_pTipsListTail->m_pNext = NULL;
|
|
}
|
|
else
|
|
{
|
|
ASSERT(m_cTips >= 3);
|
|
pTip->m_pPrev->m_pNext = pTip->m_pNext;
|
|
pTip->m_pNext->m_pPrev = pTip->m_pPrev;
|
|
}
|
|
|
|
pTip->m_pPrev = pTip->m_pNext = NULL;
|
|
--m_cTips;
|
|
m_uTotalWeight -= pTip->m_uWeight;
|
|
|
|
ASSERT_VALID(this);
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Destroy the list of tips and reset all member variables
|
|
//
|
|
|
|
HRESULT
|
|
CTipList::DeleteTips()
|
|
{
|
|
ASSERT_VALID(this);
|
|
|
|
CTip* pTip = m_pTipsListHead;
|
|
|
|
for (UINT i = 0; i < m_cTips; ++i)
|
|
{
|
|
pTip = pTip->m_pNext;
|
|
delete m_pTipsListHead;
|
|
m_pTipsListHead = pTip;
|
|
}
|
|
|
|
ASSERT(pTip == NULL && m_pTipsListHead == NULL);
|
|
|
|
// check for both a valid Filename ptr as well as a valid Monitor ptr.
|
|
// If the ContRotModule::Unlock is called prior to this destructor,then
|
|
// the Monitor object has already been cleaned up and deleted.
|
|
|
|
if ( (m_ptszFilename != NULL) && (g_pMonitor != NULL) )
|
|
{
|
|
g_pMonitor->StopMonitoringFile( m_ptszFilename );
|
|
}
|
|
delete [] m_ptszFilename;
|
|
delete [] m_ptszData;
|
|
|
|
m_ptszFilename = m_ptszData = NULL;
|
|
// m_ftLastWriteTime.dwLowDateTime = m_ftLastWriteTime.dwHighDateTime = 0;
|
|
m_cTips = m_uTotalWeight = 0;
|
|
m_pTipsListHead = m_pTipsListTail = NULL;
|
|
|
|
ASSERT_VALID(this);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
|
|
|
|
#if DBG
|
|
|
|
// Paranoia: check that Tips and TipLists are internally consistent.
|
|
// Very useful in catching bugs.
|
|
|
|
void
|
|
CTip::AssertValid() const
|
|
{
|
|
ASSERT(m_ptsz != NULL && m_uWeight > 0);
|
|
ASSERT(0 < m_cServingsLeft && m_cServingsLeft <= m_uWeight);
|
|
ASSERT(m_pPrev == NULL || m_pPrev->m_pNext == this);
|
|
ASSERT(m_pNext == NULL || m_pNext->m_pPrev == this);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CTipList::AssertValid() const
|
|
{
|
|
if (m_cTips == 0)
|
|
{
|
|
ASSERT(m_pTipsListHead == NULL && m_pTipsListTail == NULL);
|
|
ASSERT(m_uTotalWeight == 0);
|
|
}
|
|
else
|
|
{
|
|
ASSERT(m_pTipsListHead != NULL && m_pTipsListTail != NULL);
|
|
ASSERT(m_pTipsListHead->m_pPrev == NULL);
|
|
ASSERT(m_pTipsListTail->m_pNext == NULL);
|
|
ASSERT(m_uTotalWeight > 0);
|
|
|
|
if (m_cTips == 1)
|
|
ASSERT(m_pTipsListHead == m_pTipsListTail);
|
|
else
|
|
ASSERT(m_pTipsListHead != m_pTipsListTail);
|
|
}
|
|
|
|
UINT uWeight = 0;
|
|
CTip* pTip = m_pTipsListHead;
|
|
UINT i;
|
|
|
|
for (i = 0; i < m_cTips; ++i)
|
|
{
|
|
ASSERT_VALID(pTip);
|
|
uWeight += pTip->m_uWeight;
|
|
|
|
if (i < m_cTips - 1)
|
|
pTip = pTip->m_pNext;
|
|
}
|
|
|
|
ASSERT(uWeight == m_uTotalWeight);
|
|
ASSERT(pTip == m_pTipsListTail);
|
|
}
|
|
|
|
#endif
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
// Utility functions
|
|
|
|
//
|
|
// Make a copy of a TSTR that can be deleted with operator delete[]
|
|
//
|
|
|
|
static LPTSTR
|
|
TcsDup(
|
|
LPCTSTR ptsz)
|
|
{
|
|
LPTSTR ptszNew = NULL;
|
|
ATLTRY(ptszNew = new TCHAR [_tcslen(ptsz) + 1]);
|
|
if (ptszNew != NULL)
|
|
_tcscpy(ptszNew, ptsz);
|
|
return ptszNew;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// reads a \n-terminated string from rptsz and modifies rptsz to
|
|
// point after the end of that string
|
|
//
|
|
|
|
static LPTSTR
|
|
GetLine(
|
|
LPTSTR& rptsz)
|
|
{
|
|
LPTSTR ptszOrig = rptsz;
|
|
LPTSTR ptszEol = _tcspbrk(rptsz, _T("\n"));
|
|
|
|
if (ptszEol != NULL)
|
|
{
|
|
// is it "\r\n"?
|
|
if (ptszEol > ptszOrig && ptszEol[-1] == _T('\r'))
|
|
ptszEol[-1] = _T('\0');
|
|
else
|
|
ptszEol[0] = _T('\0');
|
|
|
|
rptsz = ptszEol + 1;
|
|
}
|
|
else
|
|
{
|
|
// no newline, so point past the end of the string
|
|
rptsz += _tcslen(rptsz);
|
|
}
|
|
|
|
// TRACE1("GetLine: `%s'\n", ptszOrig);
|
|
return ptszOrig;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Is the string blank?
|
|
//
|
|
|
|
static BOOL
|
|
IsBlankString(
|
|
LPCTSTR ptsz)
|
|
{
|
|
if (ptsz == NULL)
|
|
return TRUE;
|
|
|
|
while (*ptsz != _T('\0'))
|
|
if (!_istspace(*ptsz))
|
|
return FALSE;
|
|
else
|
|
ptsz++;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Read a weight line from rptsz and update rptsz to point after the
|
|
// end of any %% lines.
|
|
//
|
|
|
|
static UINT
|
|
GetWeight(
|
|
LPTSTR& rptsz)
|
|
{
|
|
UINT u = INVALID_WEIGHT; // default to invalid weight
|
|
|
|
while (*rptsz == _T('%'))
|
|
{
|
|
LPTSTR ptsz = GetLine(rptsz);
|
|
|
|
if (ptsz[1] == _T('%'))
|
|
{
|
|
u = 1; // now that the format is correct, default to 1
|
|
|
|
ptsz +=2; // Skip "%%"
|
|
|
|
while (*ptsz != _T('\0'))
|
|
{
|
|
while (_istspace(*ptsz))
|
|
ptsz++;
|
|
|
|
if (*ptsz == _T('/') && ptsz[1] == _T('/'))
|
|
{
|
|
// TRACE1("// `%s'\n", ptsz+2);
|
|
break; // a comment: ignore the rest of the line
|
|
}
|
|
else if (*ptsz == _T('#'))
|
|
{
|
|
ptsz++;
|
|
|
|
if (_T('0') <= *ptsz && *ptsz <= _T('9'))
|
|
{
|
|
LPTSTR ptsz2;
|
|
u = _tcstoul(ptsz, &ptsz2, 10);
|
|
ptsz = ptsz2;
|
|
// TRACE1("#%u\n", u);
|
|
|
|
if (u > MAX_WEIGHT)
|
|
u = MAX_WEIGHT; // clamp
|
|
}
|
|
else // ignore word
|
|
{
|
|
while (*ptsz != _T('\0') && !_istspace(*ptsz))
|
|
ptsz++;
|
|
}
|
|
}
|
|
else // ignore word
|
|
{
|
|
while (*ptsz != _T('\0') && !_istspace(*ptsz))
|
|
ptsz++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return u;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Read the multiline tip text. Updates rptsz to point past the end of it.
|
|
//
|
|
|
|
static LPTSTR
|
|
GetTipText(
|
|
LPTSTR& rptsz)
|
|
{
|
|
LPTSTR ptszOrig = rptsz;
|
|
LPTSTR ptszEol = _tcsstr(rptsz, _T("\n%%"));
|
|
|
|
if (ptszEol != NULL)
|
|
{
|
|
// is it "\r\n"?
|
|
if (ptszEol > rptsz && ptszEol[-1] == _T('\r'))
|
|
ptszEol[-1] = _T('\0');
|
|
else
|
|
ptszEol[0] = _T('\0');
|
|
|
|
rptsz = ptszEol + 1;
|
|
}
|
|
else
|
|
{
|
|
// no "\n%%", so point past the end of the string
|
|
rptsz += _tcslen(rptsz);
|
|
}
|
|
|
|
// TRACE1("GetTipText: `%s'\n", ptszOrig);
|
|
return ptszOrig;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Set the Error Info. It's up to the calling application to
|
|
// decide what to do with it. By default, Denali/VBScript will
|
|
// print the error number (and message, if there is one) and
|
|
// abort the page.
|
|
//
|
|
|
|
static HRESULT
|
|
ReportError(
|
|
HRESULT hr,
|
|
DWORD dwErr)
|
|
{
|
|
HLOCAL pMsgBuf = NULL;
|
|
|
|
// If there's a message associated with this error, report that
|
|
if (::FormatMessage(
|
|
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
|
|
NULL, dwErr,
|
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
|
|
(LPTSTR) &pMsgBuf, 0, NULL)
|
|
> 0)
|
|
{
|
|
AtlReportError(CLSID_ContentRotator, (LPCTSTR) pMsgBuf,
|
|
IID_IContentRotator, hr);
|
|
}
|
|
|
|
// TODO: add some error messages to the string resources and
|
|
// return those, if FormatMessage doesn't return anything (not
|
|
// all system errors have associated error messages).
|
|
|
|
// Free the buffer, which was allocated by FormatMessage
|
|
if (pMsgBuf != NULL)
|
|
::LocalFree(pMsgBuf);
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Report a Win32 error code
|
|
//
|
|
|
|
static HRESULT
|
|
ReportError(
|
|
DWORD dwErr)
|
|
{
|
|
return ::ReportError(HRESULT_FROM_WIN32(dwErr), dwErr);
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Report an HRESULT error
|
|
//
|
|
|
|
static HRESULT
|
|
ReportError(
|
|
HRESULT hr)
|
|
{
|
|
return ::ReportError(hr, (DWORD) hr);
|
|
}
|