mirror of https://github.com/lianthony/NT4.0
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.
1858 lines
58 KiB
1858 lines
58 KiB
//+-------------------------------------------------------------------------
|
|
//
|
|
// Microsoft Windows
|
|
// Copyright (C) Microsoft Corporation, 1992 - 1993.
|
|
//
|
|
// File: CallMain.cxx (32 bit target)
|
|
//
|
|
// Contents: Contains the CallMainControl interface
|
|
//
|
|
// Functions:
|
|
//
|
|
// History: 23-Dec-93 Johann Posch (johannp) Created
|
|
//
|
|
// CODEWORK: nuke many pCI parameters and use _pCICur instead to reduce
|
|
// code and stack usage
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
#include <ole2int.h>
|
|
#include <userapis.h>
|
|
#include <tls.h>
|
|
#include <thkreg.h>
|
|
#include "callcont.hxx"
|
|
#include "callmain.hxx"
|
|
#include "chancont.hxx"
|
|
|
|
#define WM_SYSTIMER 0x0118
|
|
#define SYS_ALTDOWN 0x2000
|
|
#define WM_NCMOUSEFIRST WM_NCMOUSEMOVE
|
|
#define WM_NCMOUSELAST WM_NCMBUTTONDBLCLK
|
|
#define DebWarn(x)
|
|
|
|
|
|
// the callmaincontrol pointer for multithreaded case
|
|
CCallMainControl *sgpCMCMultiThread = NULL;
|
|
extern COleStaticMutexSem sgmxs;
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: GetSlowTimeFactor
|
|
//
|
|
// Synopsis: Get the time slowing factor for Wow apps
|
|
//
|
|
// Returns: The factor by which we need to slow time down.
|
|
//
|
|
// Algorithm: If there is a factor in the registry, we open and read the
|
|
// registry. Otherwise we just set it to the default.
|
|
//
|
|
// History: 22-Jul-94 Ricksa Created
|
|
// 09-Jun-95 Susia ANSI Chicago optimization
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
#ifdef _CHICAGO_
|
|
#undef RegOpenKeyEx
|
|
#define RegOpenKeyEx RegOpenKeyExA
|
|
#undef RegQueryValueEx
|
|
#define RegQueryValueEx RegQueryValueExA
|
|
#endif
|
|
|
|
DWORD GetSlowTimeFactor(void)
|
|
{
|
|
// Default slowing time so we can just exit if there is no key which
|
|
// is assumed to be the common case.
|
|
DWORD dwSlowTimeFactor = OLETHK_DEFAULT_SLOWRPCTIME;
|
|
|
|
// Key for reading the value from the registry
|
|
HKEY hkeyOleThk;
|
|
|
|
// Get the Ole Thunk special value key
|
|
|
|
LONG lStatus = RegOpenKeyEx(HKEY_CLASSES_ROOT, OLETHK_KEY, 0, KEY_READ, &hkeyOleThk);
|
|
|
|
|
|
if (lStatus == ERROR_SUCCESS)
|
|
{
|
|
DWORD dwType;
|
|
DWORD dwSizeData = sizeof(dwSlowTimeFactor);
|
|
|
|
lStatus = RegQueryValueEx (hkeyOleThk, OLETHK_SLOWRPCTIME_VALUE, NULL,
|
|
&dwType, (LPBYTE) &dwSlowTimeFactor, &dwSizeData);
|
|
|
|
if ((lStatus != ERROR_SUCCESS) || dwType != REG_DWORD)
|
|
{
|
|
// Guarantee that value is reasonable if something went wrong.
|
|
dwSlowTimeFactor = OLETHK_DEFAULT_SLOWRPCTIME;
|
|
}
|
|
|
|
// Close the key since we are done with it.
|
|
RegCloseKey(hkeyOleThk);
|
|
}
|
|
|
|
return dwSlowTimeFactor;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallInfo::GetElapsedTime
|
|
//
|
|
// Synopsis: Get the elapsed time for an RPC call
|
|
//
|
|
// Returns: Elapsed time of current call
|
|
//
|
|
// Algorithm: This checks whether we have the slow time factor. If not,
|
|
// and we are in WOW we read this from the registry. Otherwise,
|
|
// this is just set to one. Then we calculate the time of the
|
|
// RPC and divide it by the slow time factor.
|
|
//
|
|
// History: 22-Jul-94 Ricksa Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(DWORD) CCallInfo::GetElapsedTime()
|
|
{
|
|
// Define slow time factor to something invalid
|
|
static dwSlowTimeFactor = 0;
|
|
|
|
if (dwSlowTimeFactor == 0)
|
|
{
|
|
if (IsWOWProcess())
|
|
{
|
|
// Get time factor from registry otherwise set to the default
|
|
dwSlowTimeFactor = GetSlowTimeFactor();
|
|
}
|
|
else
|
|
{
|
|
// Time is unmodified for 32 bit apps
|
|
dwSlowTimeFactor = 1;
|
|
}
|
|
}
|
|
|
|
DWORD dwTickCount = GetTickCount();
|
|
DWORD dwElapsedTime = dwTickCount - _dwTimeOfCall;
|
|
if (dwTickCount < _dwTimeOfCall)
|
|
{
|
|
// the timer wrapped
|
|
dwElapsedTime = 0xffffffff - _dwTimeOfCall + dwTickCount;
|
|
}
|
|
|
|
return (dwElapsedTime / dwSlowTimeFactor);
|
|
}
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Function: GetCallMainControlForThread
|
|
//
|
|
// Synopsis: retrieves the callmaincontrol for the current thread
|
|
//
|
|
// Arguments: none
|
|
//
|
|
// Returns: CallMainControl
|
|
//
|
|
// History: Jan-94 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
CCallMainControl *GetCallMainControlForThread()
|
|
{
|
|
if (FreeThreading)
|
|
{
|
|
// there is only one CMC per process for a FreeThreading app.
|
|
if (!sgpCMCMultiThread)
|
|
{
|
|
sgpCMCMultiThread = new CCallMainControl();
|
|
}
|
|
|
|
return sgpCMCMultiThread;
|
|
}
|
|
|
|
// there is a CMC per thread for non freethreading apps
|
|
CCallMainControl *pcmc = (CCallMainControl *)TLSGetCallControl();
|
|
|
|
if (pcmc == NULL)
|
|
{
|
|
pcmc = new CCallMainControl();
|
|
TLSSetCallControl(pcmc);
|
|
}
|
|
|
|
return pcmc;
|
|
}
|
|
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Function: SetCallMainControlForThread
|
|
//
|
|
// Synopsis: installs a new callmaincontrol
|
|
//
|
|
// Arguments: [pcmc] -- callmaincontrol to install
|
|
//
|
|
// Returns: TRUE on success
|
|
//
|
|
// History: Jan-94 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
BOOL SetCallMainControlForThread(CCallMainControl *pcmc)
|
|
{
|
|
if (FreeThreading)
|
|
{
|
|
// only need one CMC for this whole process
|
|
sgpCMCMultiThread = pcmc;
|
|
return TRUE;
|
|
}
|
|
|
|
// need one CMC per apartment
|
|
return TLSSetCallControl(pcmc);
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// CallMainControl implementation
|
|
//
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Method: CCallMainControl::CCallMainControl
|
|
//
|
|
// Synopsis: Constructor
|
|
//
|
|
// Arguments: (none)
|
|
//
|
|
// Returns:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
CCallMainControl::CCallMainControl()
|
|
{
|
|
_cRef = 0;
|
|
_pMF = NULL;
|
|
|
|
// initialize the call info table
|
|
_pCICur = NULL;
|
|
|
|
_cCur = CALLDATAID_INVALID;
|
|
// CODEWORK: nuke this & use single linked list
|
|
_cCallInfoMac = CALLINFOMAX;
|
|
memset(_CallInfoTable, 0, sizeof(_CallInfoTable));
|
|
|
|
_cODs = 0;
|
|
_CallType = CALLTYPE_NOCALL; // 0 is no call at all
|
|
_CallCat = CALLCAT_NOCALL;
|
|
_fInMessageFilter = FALSE;
|
|
|
|
_fMultiThreaded = FALSE;
|
|
}
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Method: CCallMainControl::~CCallMainControl
|
|
//
|
|
// Synopsis: Destructor
|
|
//
|
|
// Arguments: (none)
|
|
//
|
|
// Returns:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
CCallMainControl::~CCallMainControl()
|
|
{
|
|
if (_pMF)
|
|
{
|
|
_pMF->Release();
|
|
}
|
|
|
|
SetCallMainControlForThread(NULL);
|
|
}
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Method: CCallMainControl::Register
|
|
//
|
|
// Synopsis: registers a new callcontrol on the current callmaincontrol
|
|
//
|
|
// Arguments: [pOrigindata] -- origindata of callcontrol
|
|
//
|
|
// Returns:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
INTERNAL_(BOOL) CCallMainControl::Register (PORIGINDATA pOrigindata)
|
|
{
|
|
BOOL fRet = TRUE;
|
|
|
|
// single thread access
|
|
CLock lck(_mxs);
|
|
|
|
if ( (pOrigindata && pOrigindata->CallOrigin > 0 && pOrigindata->CallOrigin < CALLORIGIN_LAST)
|
|
&& _cODs < ODMAX)
|
|
{
|
|
for (UINT i = 0; i < _cODs; i++)
|
|
{
|
|
// check if all call origins are valid
|
|
Win4Assert(_rgpOrigindata[i] && "CallMainControl: invalid origin state");
|
|
|
|
if (_rgpOrigindata[i]->CallOrigin == pOrigindata->CallOrigin)
|
|
{
|
|
// already registered
|
|
CairoleDebugOut((DEB_ERROR, "CallMainControl: Callorigin already registered"));
|
|
fRet = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fRet)
|
|
{
|
|
// determine if this is multithreaded
|
|
_fMultiThreaded = FreeThreading;
|
|
|
|
// add origin to the next empty spot
|
|
_rgpOrigindata[_cODs] = pOrigindata;
|
|
_cODs++;
|
|
_cRef++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fRet = FALSE;
|
|
}
|
|
|
|
return fRet;
|
|
}
|
|
|
|
//+---------------------------------------------------------------------------
|
|
//
|
|
// Method: CCallMainControl::Unregister
|
|
//
|
|
// Synopsis: unregister a callcontrol on the callmaincontrol
|
|
//
|
|
// Arguments: [pOrigindata] -- origindata of callcontrol
|
|
//
|
|
// Returns:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
INTERNAL_(BOOL) CCallMainControl::Unregister (PORIGINDATA pOrigindata)
|
|
{
|
|
BOOL fRet = FALSE;
|
|
|
|
// always single thread access
|
|
{
|
|
CLock lck(_mxs);
|
|
|
|
if ( (pOrigindata && pOrigindata->CallOrigin > 0 && pOrigindata->CallOrigin < CALLORIGIN_LAST)
|
|
&& (_cODs < ODMAX) )
|
|
{
|
|
for (UINT i = 0; i < _cODs; i++)
|
|
{
|
|
if (_rgpOrigindata[i] == pOrigindata)
|
|
{
|
|
// copy the last one in the freed spot
|
|
_cODs--;
|
|
_rgpOrigindata[i] = _rgpOrigindata[_cODs];
|
|
|
|
// PeekOriginAndDDE needs this to be NULL
|
|
_rgpOrigindata[_cODs] = NULL;
|
|
_cRef--;
|
|
|
|
if (pOrigindata->CallOrigin == CALLORIGIN_RPC32_MULTITHREAD)
|
|
{
|
|
_fMultiThreaded = FALSE;
|
|
}
|
|
|
|
fRet = TRUE;
|
|
break; // break out of the loop
|
|
}
|
|
}
|
|
|
|
// fRet should be TRUE by now - otherwise callorigin was not found
|
|
Win4Assert(fRet && "CallMainControl::Unregister Callorigin not found in list.");
|
|
}
|
|
|
|
// CLock leaves scope here
|
|
}
|
|
|
|
if (FreeThreading)
|
|
{
|
|
// take the global lock that protects sgpCMCMultiThread
|
|
COleStaticLock lck(sgmxs);
|
|
|
|
if (_cRef == 0)
|
|
{
|
|
// we are about to delete ourselves so NULL the global
|
|
// pointer to this thing.
|
|
sgpCMCMultiThread = NULL;
|
|
}
|
|
}
|
|
|
|
if (_cRef == 0)
|
|
{
|
|
delete this;
|
|
}
|
|
|
|
return fRet;
|
|
}
|
|
|
|
//
|
|
// Hook up the msgFilter info with a new new message filter
|
|
//
|
|
INTERNAL_(PMESSAGEFILTER32) CCallMainControl::SetMessageFilter(PMESSAGEFILTER32 pMF)
|
|
{
|
|
BeginCriticalSection();
|
|
|
|
// save the old one to return
|
|
PMESSAGEFILTER32 pMFOld = _pMF;
|
|
|
|
// hook up the new one
|
|
_pMF = pMF;
|
|
if (_pMF)
|
|
{
|
|
_pMF->AddRef();
|
|
}
|
|
|
|
EndCriticalSection();
|
|
|
|
return pMFOld;
|
|
}
|
|
|
|
|
|
INTERNAL_(ULONG) CCallMainControl::AddRef()
|
|
{
|
|
InterlockedIncrement((long *)&_cRef);
|
|
return _cRef;
|
|
}
|
|
|
|
INTERNAL_(ULONG) CCallMainControl::Release()
|
|
{
|
|
ULONG cRef = _cRef - 1;
|
|
|
|
if (InterlockedDecrement((long *)&_cRef) == 0)
|
|
{
|
|
delete this;
|
|
return 0;
|
|
}
|
|
|
|
return cRef;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::CanHandleIncomingCall
|
|
//
|
|
// Synopsis: called whenever an incoming call arrives to ask the apps
|
|
// message filter (if there is one) whether it wants to handle
|
|
// the call or not.
|
|
//
|
|
// Arguments: none
|
|
//
|
|
// Returns: nothing
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL CCallMainControl::CanHandleIncomingCall(DWORD TIDCaller,
|
|
REFLID lid,
|
|
PINTERFACEINFO32 pIfInfo)
|
|
{
|
|
// default: all calls are accepted
|
|
HRESULT hr = S_OK;
|
|
|
|
// single thread retrieval of the previous call info and
|
|
// the message filter, since other threads can change them.
|
|
|
|
BeginCriticalSection();
|
|
|
|
PCALLINFO pCI = GetPrevCallInfo(lid);
|
|
|
|
#if DBG==1
|
|
LID lidPrev;
|
|
if (pCI)
|
|
lidPrev = pCI->GetLID();
|
|
else
|
|
lidPrev = GUID_NULL;
|
|
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"CanHandleIncomingCall: TIDCaller:%x CallType:%x lid:%x pCI:%x prevLid:%x\n",
|
|
TIDCaller, _CallType, lid.Data1, pCI, lidPrev.Data1));
|
|
#endif
|
|
|
|
CALLTYPE CallType = SetCallTypeOfCall(pCI, pIfInfo->callcat);
|
|
PMESSAGEFILTER32 pMF = GetMessageFilter();
|
|
|
|
EndCriticalSection();
|
|
|
|
|
|
if (pMF)
|
|
{
|
|
// the app has installed a message filter. call it.
|
|
|
|
DWORD dwElapsedTime = (pCI) ? pCI->GetElapsedTime() : 0;
|
|
|
|
// ensure that we dont allow the App to make an outgoing call
|
|
// from within the message filter code.
|
|
_fInMessageFilter = TRUE;
|
|
|
|
// The DDE layer doesn't provide any interface information. This
|
|
// was true on the 16-bit implementation, and has also been
|
|
// brought forward into this implementation to insure
|
|
// compatibility. However, the callcat of the pIfInfo is still
|
|
// provided.
|
|
//
|
|
// Therefore, if pIfInfo has its pUnk member set to NULL, then
|
|
// we are going to send a NULL pIfInfo to the message filter.
|
|
|
|
DWORD dwRet = pMF->HandleInComingCall((DWORD) CallType,
|
|
TIDCaller,
|
|
dwElapsedTime,
|
|
pIfInfo->pUnk?pIfInfo:NULL);
|
|
_fInMessageFilter = FALSE;
|
|
|
|
ReleaseMessageFilter(pMF);
|
|
|
|
// strict checking of app return code for win32
|
|
Win4Assert(dwRet == SERVERCALLEX_ISHANDLED ||
|
|
dwRet == SERVERCALLEX_REJECTED ||
|
|
dwRet == SERVERCALLEX_RETRYLATER ||
|
|
IsWOWThread() && "Invalid Return code from App IMessageFilter");
|
|
|
|
|
|
if (dwRet != SERVERCALLEX_ISHANDLED)
|
|
{
|
|
if (pIfInfo->callcat == CALLCAT_ASYNC ||
|
|
pIfInfo->callcat == CALLCAT_INPUTSYNC)
|
|
{
|
|
// Note: input-sync and async calls can not be rejected
|
|
// Even though they can not be rejected, we still have to
|
|
// call the MF above to maintain 16bit compatability.
|
|
|
|
hr = S_OK;
|
|
}
|
|
else if (dwRet == SERVERCALLEX_REJECTED)
|
|
{
|
|
hr = RPC_E_SERVERCALL_REJECTED;
|
|
}
|
|
else if (dwRet == SERVERCALLEX_RETRYLATER)
|
|
{
|
|
hr = RPC_E_SERVERCALL_RETRYLATER;
|
|
}
|
|
else
|
|
{
|
|
// 16bit OLE let bogus return codes go through and of course
|
|
// apps rely on that behaviour so we let them through too, but
|
|
// we are more strict on 32bit.
|
|
hr = (IsWOWThread()) ? S_OK : RPC_E_UNEXPECTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::SetCallTypeOfCall
|
|
//
|
|
// Synopsis: called when an incoming call arrives to maintain the state
|
|
// of the thread.
|
|
//
|
|
// Arguments: [pCI] - callinfo
|
|
// [callcat] - call category of incoming call
|
|
//
|
|
// Returns: the new calltype
|
|
//
|
|
// Algorithm: complicated state machine
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes:
|
|
//
|
|
// CALLTYPE_TOPLEVEL = 1, // toplevel call - no outgoing call
|
|
// CALLTYPE_NESTED = 2, // callback on behalf of previous outgoing call
|
|
// CALLTYPE_ASYNC = 3, // aysnchronous call - can NOT be rejected
|
|
// CALLTYPE_TOPLEVEL_CALLPENDING = 4, // new toplevel call with new ***LID
|
|
// CALLTYPE_ASYNC_CALLPENDING = 5 // async call - can NOT be rejected
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(CALLTYPE) CCallMainControl::SetCallTypeOfCall (PCALLINFO pCI, CALLCATEGORY CallCat)
|
|
{
|
|
CALLTYPE ctNow = (CallCat == CALLCAT_ASYNC)
|
|
? CALLTYPE_ASYNC : CALLTYPE_TOPLEVEL;
|
|
|
|
switch (_CallType)
|
|
{
|
|
case CALLTYPE_NOCALL: // no incomming call - no call to dispatch
|
|
if (_cCur == CALLDATAID_INVALID)
|
|
{
|
|
_CallType = ctNow;
|
|
break;
|
|
}
|
|
|
|
// otherwise fallthru the toplevel case
|
|
|
|
case CALLTYPE_TOPLEVEL: // dispatching or making a toplevel call
|
|
case CALLTYPE_NESTED: // nested call
|
|
if (pCI)
|
|
{
|
|
// same locigal thread
|
|
_CallType = (ctNow == CALLTYPE_TOPLEVEL)
|
|
? CALLTYPE_NESTED : CALLTYPE_ASYNC;
|
|
}
|
|
else
|
|
{
|
|
// Note: the new incoming call has a different LID!
|
|
_CallType = (ctNow == CALLTYPE_TOPLEVEL)
|
|
? CALLTYPE_TOPLEVEL_CALLPENDING : CALLTYPE_ASYNC_CALLPENDING;
|
|
}
|
|
break;
|
|
|
|
case CALLTYPE_ASYNC: // dispatching or making an async call
|
|
// Note: we do not allow to call out on async calls
|
|
// -> all new incoming calls have to be on a new LID
|
|
if (pCI)
|
|
{
|
|
_CallType = (ctNow == CALLTYPE_TOPLEVEL)
|
|
? CALLTYPE_TOPLEVEL_CALLPENDING : CALLTYPE_ASYNC_CALLPENDING;
|
|
}
|
|
break;
|
|
|
|
case CALLTYPE_TOPLEVEL_CALLPENDING:
|
|
_CallType = CALLTYPE_NESTED;
|
|
break;
|
|
|
|
case CALLTYPE_ASYNC_CALLPENDING:
|
|
default:
|
|
// no state change
|
|
break;
|
|
}
|
|
|
|
CairoleDebugOut((DEB_CALLCONT,"SetCallTypeOfCall return:%x\n", _CallType));
|
|
return _CallType;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::CanMakeOutCall
|
|
//
|
|
// Synopsis: called when the app wants to make an outgoing call to
|
|
// determine if it is OK to do it now or not.
|
|
//
|
|
// Arguments: [callcat] - call category of call the app wants to make
|
|
// [iid] - interface the call is being made on
|
|
//
|
|
// Returns: S_OK - ok to make the call
|
|
// RPC_E_CANTCALLOUT_INEXTERNALCALL - inside IMessageFilter
|
|
// RPC_E_CANTCALLOUT_INASYNCCALL - inside async call
|
|
// RPC_E_CANTCALLOUT_ININPUTSYNCCALL - inside input sync or SendMsg
|
|
//
|
|
// Algorithm: * NO outgoing call while dispatching an async call
|
|
// * Only input sync calls can be made while
|
|
// dispatching an input-sync call
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
// Notes: CODEWORK: this can be heavily optimized.
|
|
// Might be better to call this from GetBuffer instead of
|
|
// after all the parameters have been marshalled.
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL CCallMainControl::CanMakeOutCall (CALLCATEGORY callcat, REFIID iid)
|
|
{
|
|
HRESULT hr = NOERROR;
|
|
|
|
Win4Assert(callcat >= CALLCAT_SYNCHRONOUS
|
|
&& callcat <= CALLCAT_INTERNALINPUTSYNC
|
|
&& "CallMainControl::CanMakeOutCall invalid call category.");
|
|
|
|
if (!_fMultiThreaded)
|
|
{
|
|
// this lets RotRegister go through
|
|
if (_fInMessageFilter && callcat != CALLCAT_INTERNALINPUTSYNC)
|
|
{
|
|
CairoleDebugOut((DEB_ERROR, "App trying to call out from within IMessageFilter\n"));
|
|
hr = RPC_E_CANTCALLOUT_INEXTERNALCALL;
|
|
}
|
|
// let pass async and internal calls if dispatching an async call
|
|
else if ((_CallType == CALLTYPE_ASYNC
|
|
|| _CallType == CALLTYPE_ASYNC_CALLPENDING)
|
|
&& callcat != CALLCAT_ASYNC
|
|
&& callcat != CALLCAT_INTERNALSYNC
|
|
&& callcat != CALLCAT_INTERNALINPUTSYNC)
|
|
{
|
|
hr = RPC_E_CANTCALLOUT_INASYNCCALL;
|
|
}
|
|
// * do not allow a non-async and non-inputsync outgoing calls
|
|
// if dispatching an inputsync call, an internal input sync call
|
|
// or if we are in the process of handling a send message.
|
|
//
|
|
// If we are in WOW, then we are going to allow outgoing calls
|
|
// while InSendMessage(). They used to be allowed, and there
|
|
// are cases such as Publishers Cue Cards that do this
|
|
//
|
|
else if (((_CallCat == CALLCAT_INPUTSYNC)
|
|
|| (_CallCat == CALLCAT_INTERNALINPUTSYNC)
|
|
|| InSendMessage())
|
|
&& (callcat != CALLCAT_ASYNC)
|
|
&& (callcat != CALLCAT_INPUTSYNC)
|
|
&& (callcat != CALLCAT_INTERNALSYNC)
|
|
&& (callcat != CALLCAT_INTERNALINPUTSYNC))
|
|
{
|
|
#if DBG == 1
|
|
if ((_CallCat != CALLCAT_INPUTSYNC)
|
|
&& (_CallCat != CALLCAT_INTERNALINPUTSYNC))
|
|
{
|
|
CairoleDebugOut((DEB_WARN,
|
|
"CCallMainControl::CanMakeOutCall Failing Call because InSendMessage\n"));
|
|
}
|
|
#endif // DBG == 1
|
|
hr = RPC_E_CANTCALLOUT_ININPUTSYNCCALL;
|
|
}
|
|
}
|
|
|
|
CairoleDebugOut(((hr == S_OK) ? DEB_CALLCONT : DEB_ERROR,
|
|
"CanMakeOutCall: _CallType:%x _CallCat:%x callcat:%x Return:%x\n",
|
|
_CallType, _CallCat, callcat, hr));
|
|
|
|
return hr;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::GetPrevCallInfo
|
|
//
|
|
// Synopsis: When an incoming call arrives this is used to find any
|
|
// previous call for the same logical thread, ignoring
|
|
// INTERNAL calls.
|
|
//
|
|
// Arguments: [lid] - logical threadid of incoming call
|
|
//
|
|
// Returns: pCI - if previous callinfo found for this lid
|
|
// NULL - if not
|
|
//
|
|
// Algorithm: see synopsis
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(PCALLINFO) CCallMainControl::GetPrevCallInfo(REFLID lid)
|
|
{
|
|
for (UINT i = _cCur; i != CALLDATAID_INVALID; i--)
|
|
{
|
|
PCALLINFO pCI = GetCIfromCallID(i);
|
|
if ( pCI
|
|
&& pCI->GetLID() == lid
|
|
&& pCI->GetCallCat() < CALLCAT_INTERNALSYNC
|
|
&& pCI->GetCallCat() > CALLCAT_NOCALL)
|
|
{
|
|
return pCI;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::HandleRejectedCall
|
|
//
|
|
// Synopsis: called when the response to a remote call is rejected or
|
|
// retry later.
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
//
|
|
// Returns: nothing - CallState of pCI modified appropriately
|
|
//
|
|
// Algorithm: Calls the app's message filter (if there is one) to
|
|
// determine whether the call should be failed, retried
|
|
// immediately, or retried at some later time. If there is
|
|
// no message filter then the call is rejected.
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(void) CCallMainControl::HandleRejectedCall (PCALLINFO pCI)
|
|
{
|
|
// we should not end up here if there is no outstanding call
|
|
Win4Assert(_cCur != CALLDATAID_UNUSED && "HandleRejectedCall:: not call waiting.");
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"RetryRejectedCall pCI:%x TaskId:%x ElapsedTime:%x CallState:%x\n",
|
|
pCI, pCI->GetTaskIdServer(), pCI->GetElapsedTime(), pCI->GetCallState()));
|
|
|
|
// default return value - rejected
|
|
DWORD dwRet = 0xffffffff;
|
|
|
|
// single thread access to getting the message filter
|
|
BeginCriticalSection();
|
|
PMESSAGEFILTER32 pMF = GetMessageFilter();
|
|
EndCriticalSection();
|
|
|
|
// in case caller has no MessageFilter - return MSG_REJECTED
|
|
if (pMF)
|
|
{
|
|
// ensure that we dont allow the App to make an outgoing call
|
|
// from within the message filter code.
|
|
_fInMessageFilter = TRUE;
|
|
|
|
dwRet = pMF->RetryRejectedCall(pCI->GetTaskIdServer(),
|
|
pCI->GetElapsedTime(),
|
|
pCI->GetCallState());
|
|
_fInMessageFilter = FALSE;
|
|
ReleaseMessageFilter(pMF);
|
|
}
|
|
|
|
if (dwRet == 0xffffffff)
|
|
{
|
|
// Really rejected. Mark is as such incase it was actually
|
|
// Call_RetryLater, also ensures that IsWaiting returns FALSE
|
|
pCI->SetCallState(Call_Rejected, RPC_E_SERVERCALL_REJECTED);
|
|
}
|
|
else if (dwRet >= 100)
|
|
{
|
|
// Retrt Later. Start the timer. This ensures that IsTimerAtZero
|
|
// returns FALSE and IsWaiting returns TRUE
|
|
pCI->StartTimer(dwRet);
|
|
}
|
|
else
|
|
{
|
|
// Retry Immediately. Set the state so that IsTimerAtZero
|
|
// returns TRUE and IsWaiting returns TRUE
|
|
pCI->SetCallState(Call_WaitOnCall, S_OK);
|
|
}
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::HandlePendingMessage
|
|
//
|
|
// Synopsis: this function is called for system messages and other
|
|
// pending messages
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(DWORD) CCallMainControl::HandlePendingMessage(PCALLINFO pCI)
|
|
{
|
|
TRACECALL(TRACE_CALLCONT, "CallMainControl::HandlePendingMessage called");
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"MessagePending pCI:%x TaskId:%x ElapsedTime:%x CallState:%x\n",
|
|
pCI, pCI->GetTaskIdServer(), pCI->GetElapsedTime(),
|
|
(_cCur >= 1) ? PENDINGTYPE_NESTED : PENDINGTYPE_TOPLEVEL));
|
|
|
|
DWORD dwRet = PENDINGMSG_WAITDEFPROCESS;
|
|
|
|
// single thread access to getting the message filter
|
|
BeginCriticalSection();
|
|
PMESSAGEFILTER32 pMF = GetMessageFilter();
|
|
EndCriticalSection();
|
|
|
|
if (pMF)
|
|
{
|
|
// call the message filter
|
|
DWORD dwPendingType = (_cCur == 0) ? PENDINGTYPE_TOPLEVEL
|
|
: PENDINGTYPE_NESTED;
|
|
|
|
// ensure that we dont allow the App to make an outgoing call
|
|
// from within the message filter code.
|
|
_fInMessageFilter = TRUE;
|
|
|
|
dwRet = pMF->MessagePending(pCI->GetTaskIdServer(),
|
|
pCI->GetElapsedTime(),
|
|
dwPendingType);
|
|
_fInMessageFilter = FALSE;
|
|
ReleaseMessageFilter(pMF);
|
|
}
|
|
|
|
switch (dwRet)
|
|
{
|
|
case PENDINGMSG_CANCELCALL :
|
|
|
|
pCI->SetCallState(Call_Canceled, RPC_E_CALL_CANCELED);
|
|
break;
|
|
|
|
default :
|
|
Win4Assert(FALSE && "Invalid return value from HandleIncomingCall" );
|
|
// do default processing
|
|
|
|
case PENDINGMSG_WAITDEFPROCESS:
|
|
// For Win32, default and wait no process are the same.
|
|
|
|
case PENDINGMSG_WAITNOPROCESS :
|
|
{
|
|
// wait for the return and don't dispatch the message
|
|
// handle only the important system stuff
|
|
MSG msg;
|
|
|
|
// perform default action on system message
|
|
// only dispatch special system messages
|
|
BOOL fSys = FALSE;
|
|
if ((fSys = MyPeekMessage(pCI, &msg, 0, WM_SYSCOMMAND, WM_SYSCOMMAND, PM_REMOVE | PM_NOYIELD) )
|
|
|| (MyPeekMessage(pCI, &msg, 0, WM_SYSKEYDOWN, WM_SYSKEYDOWN, PM_NOREMOVE | PM_NOYIELD) )
|
|
)
|
|
{
|
|
// Note: message: SYSOCMMAND SC_TASKLIST is generated by system and posted to the active window;
|
|
// we let this message by default thru
|
|
if (fSys)
|
|
{
|
|
// only dispatch some syscommands
|
|
switch (msg.wParam)
|
|
{
|
|
case SC_HOTKEY:
|
|
case SC_TASKLIST:
|
|
CairoleDebugOut((DEB_CALLCONT,">>>> Dispatching SYSCOMMAND message: %x; wParm: %x \r\n",msg.message, msg.wParam));
|
|
DispatchMessage(&msg);
|
|
default:
|
|
// we have to take out all syscommand messages
|
|
CairoleDebugOut((DEB_CALLCONT,">>>> Received/discarded SYSCOMMAND message: %x; wParm: %x \r\n",msg.message, msg.wParam));
|
|
MessageBeep(0);
|
|
break;
|
|
}
|
|
}
|
|
else if (DispatchSystemMessage(msg, FALSE))
|
|
{
|
|
// take care of the sys key messages
|
|
// dispatch the message
|
|
CairoleDebugOut((DEB_CALLCONT, "==> Dispatched system message: %x \r\n",msg.message));
|
|
MyPeekMessage(pCI, &msg, 0, WM_SYSKEYDOWN, WM_SYSKEYDOWN, PM_REMOVE | PM_NOYIELD);
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&msg);
|
|
}
|
|
}
|
|
else if ( MyPeekMessage(pCI, &msg, 0, WM_ACTIVATE, WM_ACTIVATE, PM_REMOVE | PM_NOYIELD)
|
|
|| MyPeekMessage(pCI, &msg, 0, WM_ACTIVATEAPP, WM_ACTIVATEAPP, PM_REMOVE | PM_NOYIELD)
|
|
|| MyPeekMessage(pCI, &msg, 0, WM_NCACTIVATE, WM_NCACTIVATE, PM_REMOVE | PM_NOYIELD) )
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, ">>> Dispatched ACTIVATE message: Hwnd: >%x< message: %x \r\n",msg.hwnd, msg.message));
|
|
DispatchMessage(&msg);
|
|
}
|
|
else
|
|
{
|
|
// no message peeked
|
|
//CairoleDebugOut((DEB_CALLCONT, "==> No system message peeked: %x \r\n",msg.message));
|
|
}
|
|
}
|
|
break;
|
|
|
|
} // end switch
|
|
|
|
return dwRet;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::DispatchSystemMessages
|
|
//
|
|
// Synopsis: called when....
|
|
//
|
|
// Arguments: [msg] -
|
|
// [fBeep] - TRUE means beep - CODEWORK: nobody sets to TRUE
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm: Dispatach mouse and keyboard message.
|
|
// The folling keysstroke should get handled:
|
|
//
|
|
// alt-escape - enunerate tasks by window
|
|
// alt-tab - enumerate tasks by icons
|
|
// alt-shift-escape - enunerate tasks by window
|
|
// alt-shift-tab - enumerate tasks by icons
|
|
// ctrl-escape - switch to task list
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(BOOL) CCallMainControl::DispatchSystemMessage(MSG msg, BOOL fBeep)
|
|
{
|
|
BOOL fDispatch = FALSE;
|
|
|
|
WORD wMsg = msg.message;
|
|
WORD wKeyCode = msg.wParam;
|
|
DWORD dwKeyData = msg.lParam;
|
|
|
|
CairoleDebugOut((DEB_CALLCONT, "Command: %x; KeyCode %x, KeyData %08x \r\n",wMsg, wKeyCode, dwKeyData));
|
|
|
|
switch (wMsg)
|
|
{
|
|
case WM_SYSKEYDOWN:
|
|
// user hold ALT key was pressed another key
|
|
// no window has the focus and we are the window which is active
|
|
if ( dwKeyData & SYS_ALTDOWN)
|
|
{
|
|
// alt key is pressed
|
|
switch (wKeyCode)
|
|
{
|
|
case VK_MENU: // ALT KEY
|
|
case VK_SHIFT:
|
|
// don't beep
|
|
break;
|
|
case VK_TAB:
|
|
case VK_ESCAPE:
|
|
// Alt-Esc, Alt-Tab - let it thru to the DefWinProc
|
|
fDispatch = TRUE;
|
|
break;
|
|
default:
|
|
// beep on all other keystrokes
|
|
if (fBeep)
|
|
MessageBeep(0);
|
|
break;
|
|
|
|
}
|
|
}
|
|
break;
|
|
|
|
case WM_KEYDOWN:
|
|
if ( wKeyCode != VK_CONTROL
|
|
&& wKeyCode != VK_SHIFT)
|
|
MessageBeep(0);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return fDispatch;
|
|
}
|
|
|
|
//
|
|
// insert the callinfo in a free spot
|
|
//
|
|
INTERNAL_(UINT) CCallMainControl::InsertCI (PCALLINFO pCI)
|
|
{
|
|
// CODEWORK: Assert that we are inside critical section in MT case
|
|
// CODEWORK: Use a linked list for the callinfos, then InsertCI,
|
|
// SetupModalLoop and ResetModalLoop can mostly vanish,
|
|
// we save 800 bytes of data per apartment, and we loose
|
|
// the limitation of _cCallInfoMac concurrent calls.
|
|
|
|
// find empty spot
|
|
for (UINT i = 0; i < _cCallInfoMac && _CallInfoTable[i]; i++)
|
|
;
|
|
|
|
if (i < _cCallInfoMac)
|
|
{
|
|
_CallInfoTable[i] = pCI;
|
|
pCI->SetId(i);
|
|
return i;
|
|
}
|
|
|
|
// table is full
|
|
return CALLDATAID_INVALID;
|
|
}
|
|
|
|
//
|
|
// Set up the a new call info - prepare the modal loop for a new outgoing call
|
|
// Note: Must be called before calling RunModalLoop
|
|
INTERNAL_(UINT) CCallMainControl::SetupModalLoop (PCALLINFO pCI)
|
|
{
|
|
// insert the new callinfo in the table
|
|
// single thread access to _cCur and the CallInfo table
|
|
CLock lck(_mxs);
|
|
|
|
UINT cNew = InsertCI(pCI);
|
|
|
|
if (cNew != CALLDATAID_INVALID)
|
|
{
|
|
// set the topmost pointer
|
|
if ( _cCur == CALLDATAID_UNUSED
|
|
|| _cCur < cNew)
|
|
_cCur = cNew;
|
|
}
|
|
else
|
|
{
|
|
Win4Assert(!"CallInfo table is full");
|
|
CairoleDebugOut((DEB_ERROR,"CallInfo table is full\n\r"));
|
|
}
|
|
|
|
return cNew;
|
|
}
|
|
//
|
|
// Restore the call info - with the previous call info - previous call on the stack
|
|
// Note: Must be called after RunModalLoop
|
|
//
|
|
INTERNAL_(VOID) CCallMainControl::ResetModalLoop (UINT id)
|
|
{
|
|
Win4Assert(id < _cCallInfoMac && "ResetModalLoop: restoring wrong callinfo.");
|
|
|
|
// reset the old call state
|
|
// remove the call info from the table
|
|
{
|
|
CLock lck(_mxs);
|
|
|
|
FreeCallID(id);
|
|
// reset the current values
|
|
if (id == _cCur)
|
|
{
|
|
// reset it back to the first one in use
|
|
while( --_cCur != CALLDATAID_INVALID && !GetCIfromCallID(_cCur) )
|
|
;
|
|
}
|
|
// lock leaves scope
|
|
}
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::CallOnEvent
|
|
//
|
|
// Synopsis: this function is called when an event occurs signalling
|
|
// the completion of the call
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(void) CCallMainControl::CallOnEvent(PCALLINFO pCI)
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, "CallMainControl::OnEvent called pCI:%x hEvent:%x\n",
|
|
pCI, pCI->GetEvent()));
|
|
|
|
#if DBG==1
|
|
if (!_fMultiThreaded)
|
|
{
|
|
// if we are not MT, then the pCI MUST be for the current thread
|
|
Win4Assert(pCI->GetTID() == GetCurrentThreadId() &&
|
|
"OnEvent: thread id wrong.");
|
|
}
|
|
#endif
|
|
|
|
// in MT case only one thread should call OnEvent
|
|
if (!_fMultiThreaded || (pCI->GetTID() == GetCurrentThreadId()))
|
|
{
|
|
pCI->OnEvent();
|
|
}
|
|
|
|
CairoleDebugOut((DEB_CALLCONT, "CallMainControl::OnEvent returned\n"));
|
|
return;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::PeekOriginAndDDEMessage
|
|
//
|
|
// Synopsis: Called when a windows message arrives to look for incoming
|
|
// Rpc messages which might be the reply to an outstanding call
|
|
// or may be new incoming messages requests.
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
// [pMsg] - ptr to message
|
|
// [fDDEMsg] - TRUE -> search also for DDE messages
|
|
//
|
|
// Returns: nothing
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(void) CCallMainControl::PeekOriginAndDDEMessage(PCALLINFO pCI,
|
|
BOOL fDDEMsg)
|
|
{
|
|
// loop over all call origins looking for incoming Rpc messages. Note that
|
|
// it is possible for a dispatch here to cause one of the call origins to
|
|
// be deregistered or another to be registered, so our loop has to account
|
|
// for that. The outer loop is OK because it always references the current
|
|
// count of ODs (_cODs). The while loop is OK because it always ensures
|
|
// _rgpOrigindata is non NULL.
|
|
|
|
|
|
CairoleDebugOut((DEB_CALLCONT, "PeekOriginAndDDEMessage: fDDEMsg:%d\n", fDDEMsg));
|
|
|
|
MSG Msg;
|
|
|
|
for (UINT i = 0; i < _cODs; i++)
|
|
{
|
|
ORIGINDATA *pOD;
|
|
|
|
while ( pCI->IsWaiting() // waiting for current call to complete
|
|
&& ((pOD = _rgpOrigindata[i]) != NULL) // origin data is valid
|
|
&& (MyPeekMessage(pCI, &Msg, pOD->hwnd,
|
|
pOD->wFirstMsg, pOD->wLastMsg,PM_REMOVE | PM_NOYIELD)))
|
|
{
|
|
// dispatch all messages
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"Origin Msg to dispatch: hwnd:%d, msg:%d time:%ld\n",
|
|
Msg.hwnd, Msg.message, Msg.time));
|
|
DispatchMessage(&Msg);
|
|
}
|
|
}
|
|
|
|
if (fDDEMsg)
|
|
{
|
|
while ( pCI->IsWaiting()
|
|
&& MyPeekMessage(pCI, &Msg, 0,WM_DDE_FIRST, WM_DDE_LAST,
|
|
PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"DDE message to dispatch: hwnd: %x, message %x time: %ld\n",
|
|
Msg.hwnd,Msg.message, Msg.time));
|
|
DispatchMessage(&Msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::MyPeekMessage
|
|
//
|
|
// Synopsis: This function is called whenever we want to do a PeekMessage.
|
|
// It has special handling for WM_QUIT messages.
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
// [pMsg] - message structure
|
|
// [hWnd] - window to peek on
|
|
// [min/max] - min and max message numbers
|
|
// [wFlag] - peek flags
|
|
//
|
|
// Returns: TRUE - a message is available
|
|
// FALSE - no messages available
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(BOOL) CCallMainControl::MyPeekMessage(PCALLINFO pCI,
|
|
MSG *pMsg, HWND hwnd, UINT min, UINT max, WORD wFlag)
|
|
{
|
|
BOOL fRet = PeekMessage(pMsg, hwnd, min, max, wFlag);
|
|
if (fRet)
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, "MyPeekMessage: hwnd:%x, msg:%x time:%x\n", pMsg->hwnd, pMsg->message, pMsg->time));
|
|
if (pMsg->message == WM_QUIT)
|
|
{
|
|
// just remember that we saw a QUIT message. we will ignore it for
|
|
// now and repost it after our call has completed.
|
|
|
|
CairoleDebugOut((DEB_CALLCONT, "WM_QUIT received while waiting on remote call\n"));
|
|
pCI->SetQuitCode(pMsg->wParam);
|
|
|
|
if (wFlag & PM_NOREMOVE)
|
|
{
|
|
// quit message is still on queue so pull it off
|
|
PeekMessage(pMsg, hwnd, WM_QUIT, WM_QUIT, PM_REMOVE | PM_NOYIELD);
|
|
}
|
|
|
|
// peek again to see if there is another message
|
|
fRet = PeekMessage(pMsg, hwnd, min, max, wFlag);
|
|
}
|
|
}
|
|
|
|
return fRet;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::FindMessage
|
|
//
|
|
// Synopsis: Called by HandleWakeForMsg when a message arrives on the
|
|
// windows msg queue. Determines if there is something of
|
|
// interest to us, and pulls timer msgs. Dispatches RPC, DDE,
|
|
// and RPC timer messages.
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
// [dwStatus] - current Queue status (from GetQueueStatus)
|
|
//
|
|
// Returns: TRUE - there is a message to process
|
|
// FALSE - no messages to process
|
|
//
|
|
// Algorithm: Find the next message in the queue by using the following
|
|
// priority list:
|
|
//
|
|
// 1. RPC and DDE messages
|
|
// 2. mouse and keyboard messages
|
|
// 3. other messages
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(BOOL) CCallMainControl::FindMessage(PCALLINFO pCI, DWORD dwStatus)
|
|
{
|
|
WORD wOld = HIWORD(dwStatus);
|
|
WORD wNew = (WORD) dwStatus;
|
|
|
|
if (!wNew)
|
|
{
|
|
if (!(wOld & QS_POSTMESSAGE))
|
|
{
|
|
// there are no message to take care of
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
wNew |= QS_POSTMESSAGE;
|
|
}
|
|
}
|
|
|
|
MSG Msg;
|
|
|
|
// Priority 1: look for CALLORIGIN and DDE messages
|
|
if (wNew & (QS_POSTMESSAGE | QS_SENDMESSAGE | QS_TIMER))
|
|
{
|
|
PeekOriginAndDDEMessage(pCI, TRUE);
|
|
|
|
// check if we got the acknowledge
|
|
if (!pCI->IsWaiting())
|
|
return FALSE;
|
|
}
|
|
|
|
if (wNew & QS_TIMER)
|
|
{
|
|
// throw the system timer messages away
|
|
while (MyPeekMessage(pCI, &Msg, 0, WM_SYSTIMER, WM_SYSTIMER, PM_REMOVE | PM_NOYIELD))
|
|
;
|
|
}
|
|
|
|
// Priority 2: messages from the hardware queue
|
|
if (wNew & (QS_KEY | QS_MOUSEMOVE | QS_MOUSEBUTTON))
|
|
{
|
|
// this messages are always removed
|
|
return TRUE;
|
|
}
|
|
else if (wNew & QS_TIMER)
|
|
{
|
|
if (MyPeekMessage(pCI, &Msg, 0, WM_TIMER, WM_TIMER, PM_NOREMOVE | PM_NOYIELD) )
|
|
return TRUE;
|
|
}
|
|
else if (wNew & QS_PAINT)
|
|
{
|
|
// this message might not get removed
|
|
return TRUE;
|
|
}
|
|
else if (wNew & (QS_POSTMESSAGE | QS_SENDMESSAGE))
|
|
{
|
|
if (MyPeekMessage(pCI, &Msg, 0, 0, 0, PM_NOREMOVE))
|
|
{
|
|
// Priority 3: all other messages
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::HandleWakeForMsg (private)
|
|
//
|
|
// Synopsis: Handle wake for the arrival of some kind of message
|
|
//
|
|
// Arguments: [dwInput] - input type for call to wake up on
|
|
// [pCI] - call information structure
|
|
// [fClearedQueueInPast] - whether we ever decided to clear queue
|
|
//
|
|
// Returns: nothing
|
|
// pCI->fClearedQueue set if appropriate
|
|
//
|
|
// Algorithm: If this is called to wake up for a posted message, we
|
|
// check the queue status. If the message queue status indicates
|
|
// that there is some kind of a modal loop going on, then we
|
|
// clear all the keyboard and mouse messages in our queue. Then
|
|
// if we wake up for all input, we check the message queue to
|
|
// see whether we need to notify the application that a message
|
|
// has arrived. Then, we dispatch any messages that have to do
|
|
// with the ORPC system. Finally we yield just in case we need
|
|
// to dispatch a send message in the VDM. For an input sync
|
|
// RPC, all we do is a call that will yield to get the pending
|
|
// send message dispatched.
|
|
//
|
|
// History: 13-Aug-94 Ricksa Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL_(void) CCallMainControl::HandleWakeForMsg(DWORD dwInput,
|
|
PCALLINFO pCI)
|
|
{
|
|
// Use for various peeks.
|
|
MSG msg;
|
|
|
|
// Is this an input sync call?
|
|
if (dwInput != QS_SENDMESSAGE)
|
|
{
|
|
// No, so we have to worry about the state of the message queue.
|
|
// We have to be careful that we aren't holding the input focus
|
|
// on an input synchronized queue.
|
|
|
|
// So what is the state of the queue? - note we or QS_TRANSFER because
|
|
// this an undocumented flag which tells us the the input focus has
|
|
// changed to us.
|
|
DWORD dwQueueFlags =
|
|
GetQueueStatus(QS_TRANSFER | QS_ALLINPUT);
|
|
|
|
CairoleDebugOut((DEB_CALLCONT, "Queue Status %lx\n", dwQueueFlags));
|
|
|
|
// Call through to the application if we are going to. We do this here
|
|
// so that the application gets a chance to process any
|
|
// messages that it wants to and also allows the call control to
|
|
// dispatch certain messages that it knows how to, thus making the
|
|
// queue more empty.
|
|
if (((dwInput & QS_ALLINPUT) == QS_ALLINPUT)
|
|
&& FindMessage(pCI, dwQueueFlags))
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, "HandlePendingMessage calling\n"));
|
|
// pending message in the queue
|
|
HandlePendingMessage(pCI);
|
|
}
|
|
|
|
// Did the input focus change to us?
|
|
if (((LOWORD(dwQueueFlags) & QS_TRANSFER)) || pCI->GetClearedQueue())
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, "Message Queue is being cleared\n"));
|
|
pCI->SetClearedQueue();
|
|
|
|
// Try to clear the queue as best we can of any messages that
|
|
// might be holding off some other modal loop from executing.
|
|
// So we eat all mouse and key events.
|
|
if (HIWORD(dwQueueFlags) & QS_KEY)
|
|
{
|
|
while (MyPeekMessage(pCI, &msg, NULL, WM_KEYFIRST, WM_KEYLAST,
|
|
PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
;
|
|
}
|
|
}
|
|
|
|
// Clear mouse releated messages if there are any
|
|
if (HIWORD(dwQueueFlags) & QS_MOUSE)
|
|
{
|
|
while (MyPeekMessage(pCI, &msg, NULL, WM_MOUSEFIRST, WM_MOUSELAST,
|
|
PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
;
|
|
}
|
|
|
|
while (MyPeekMessage(pCI, &msg, NULL, WM_NCMOUSEFIRST,
|
|
WM_NCMOUSELAST, PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
;
|
|
}
|
|
|
|
while (MyPeekMessage(pCI, &msg, NULL, WM_QUEUESYNC, WM_QUEUESYNC,
|
|
PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
;
|
|
}
|
|
}
|
|
|
|
// Get rid of paint message if we can as well -- this makes
|
|
// the screen look so much better.
|
|
if (HIWORD(dwQueueFlags) & QS_PAINT)
|
|
{
|
|
if (MyPeekMessage(pCI, &msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE | PM_NOYIELD))
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT, "Dispatch paint\n"));
|
|
DispatchMessage(&msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We process posted messages so make sure DDE and
|
|
// RPC have all their messages handled.
|
|
PeekOriginAndDDEMessage(pCI, TRUE);
|
|
|
|
if (IsWOWThreadCallable())
|
|
{
|
|
// In WOW, a genuine yield is the only thing to guarantee
|
|
// that SendMessage will get through
|
|
g_pOleThunkWOW->YieldTask16();
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
// We need to give user control so that the send message
|
|
// can get dispatched. Thus the following is simply a no-op
|
|
// which gets into user to let it dispatch the message.
|
|
if (!IsWOWThreadCallable())
|
|
{
|
|
PeekMessage(&msg, 0, WM_NULL, WM_NULL, PM_NOREMOVE);
|
|
}
|
|
else
|
|
{
|
|
// In WOW, a genuine yield is the only thing to guarantee
|
|
// that SendMessage will get through
|
|
g_pOleThunkWOW->YieldTask16();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::BlockFn (private)
|
|
//
|
|
// Synopsis: implements the modal loop part of a call. This block function
|
|
// is called either by the transmit code above, or by the Rpc
|
|
// runtime during a call.
|
|
//
|
|
// Arguments: none
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL CCallMainControl::BlockFn(void)
|
|
{
|
|
COleTls tls;
|
|
PCALLINFO pCICur = (PCALLINFO) tls->pCALLINFO;
|
|
Win4Assert(pCICur && "Blocked but no call in progress!");
|
|
|
|
DWORD dwStatus = 0;
|
|
CALLCATEGORY CallCatOut = pCICur->GetCallCat();
|
|
DWORD dwInput = pCICur->GetMsgQInputFlag();
|
|
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"CallMainControl::BlockFn CallCat:%x, dwInput:%x\n",
|
|
CallCatOut, dwInput));
|
|
|
|
|
|
// wait for an incomming message or for the call to complete.
|
|
DWORD dwWakeReason = WAIT_TIMEOUT;
|
|
EVENT rgEvents[1];
|
|
DWORD cEvents = 0;
|
|
|
|
rgEvents[0] = pCICur->GetEvent();
|
|
if (rgEvents[0] != NULL)
|
|
{
|
|
// first check if the event is already signalled. This ensures
|
|
// that when we return from nested calls and the upper calls
|
|
// have already been acknowledged, that no windows messages
|
|
// can come in.
|
|
|
|
cEvents = 1;
|
|
CairoleDebugOut((DEB_CALLCONT, "WaitForSingleObject hEvent:%x\n", rgEvents[1]));
|
|
dwWakeReason = WaitForSingleObject(rgEvents[0], 0);
|
|
}
|
|
|
|
if (dwWakeReason == WAIT_TIMEOUT)
|
|
{
|
|
// event (if any) is not signalled yet, wait for more work, either
|
|
// the call to complete, a SCM call to come in that MUST get through,
|
|
// or a message arrives (except if we are making an outgoing SCM call)
|
|
|
|
// in Wow, do a directed yield because MsgWaitForMultiple does
|
|
// not yield. If the call is handled during that time, a msg
|
|
// should have been posted and MsgWait will wake up immediately.
|
|
|
|
DWORD WaitTime;
|
|
|
|
pCICur->DoDirectedYieldIfNeeded();
|
|
|
|
// Even if we do a directed yield, there is no particular reason
|
|
// not to wait the maximum amount of time since we have to wait
|
|
// some time if we want to be sure to get a QS_TRANSFER indication.
|
|
WaitTime = pCICur->TicksToWait();
|
|
|
|
// If we want to wake up for a posted message, we need to make
|
|
// sure that we haven't missed any because of the queue status
|
|
// being affected by prior PeekMessages. We don't worry about
|
|
// QS_SENDMESSAGE because if PeekMessage got called, the pending
|
|
// send got dispatched. Further, if we are in an input sync call,
|
|
// we don't want to start dispatching regular RPC calls here by
|
|
// accident.
|
|
if (dwInput & QS_POSTMESSAGE)
|
|
{
|
|
dwStatus = GetQueueStatus(dwInput);
|
|
|
|
// We care about any message on the queue not just new messages
|
|
// because PeekMessage affects the queue state. It resets the
|
|
// state so even if a message is not processed, the queue state
|
|
// represents this as an old message even though no one has
|
|
// ever looked at it. So even though the message queue tells us
|
|
// there are no new messages in the queue. A new message we are
|
|
// interested in could be in the queue.
|
|
WORD wNew = (WORD) dwStatus | HIWORD(dwStatus);
|
|
|
|
// Note that we look for send as well as post because our
|
|
// queue status could have reset the state of the send message
|
|
// bit and therefore, MsgWaitForMultipleObject below will not
|
|
// wake up to dispatch the send message.
|
|
if (wNew & (QS_POSTMESSAGE | QS_SENDMESSAGE))
|
|
{
|
|
// the acknowledge message might be already in the queue
|
|
PeekOriginAndDDEMessage(pCICur, TRUE);
|
|
|
|
// check if we got the acknowledge
|
|
if (!pCICur->IsWaiting())
|
|
{
|
|
return RPC_S_OK;
|
|
}
|
|
}
|
|
|
|
#ifdef _CHICAGO_
|
|
//Note:POSTPPC
|
|
WORD wOld = HIWORD(dwStatus);
|
|
|
|
if (wOld & (QS_POSTMESSAGE))
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT | DEB_IWARN, "Set timeout time to 100\n"));
|
|
WaitTime = 100;
|
|
}
|
|
#endif //_CHICAGO_
|
|
}
|
|
|
|
CairoleDebugOut((DEB_CALLCONT,
|
|
"MsgWaitForMultiple cEvents:%x hEvent[0]:%x WaitTime:%x dwInput:%x\n",
|
|
cEvents, rgEvents[0], WaitTime, dwInput));
|
|
|
|
dwWakeReason = MsgWaitForMultipleObjects(cEvents, rgEvents, FALSE,
|
|
WaitTime, dwInput);
|
|
|
|
CairoleDebugOut((DEB_CALLCONT,"MsgWaitForMultipleObject returned:%ld\n",
|
|
dwWakeReason));
|
|
}
|
|
|
|
// OK, we've done whatever blocking we were going to do and now we have
|
|
// been woken up, so figure out why we woke up and handle it.
|
|
|
|
if (dwWakeReason == (WAIT_OBJECT_0 + cEvents))
|
|
{
|
|
HandleWakeForMsg(dwInput, pCICur);
|
|
}
|
|
else if (dwWakeReason == WAIT_TIMEOUT)
|
|
{
|
|
// retrytimer is at zero - exit and retransmit the call
|
|
CairoleDebugOut((DEB_CALLCONT, "Timer at zero!\n"));
|
|
|
|
#ifdef _CHICAGO_
|
|
//Note:POSTPPC
|
|
WORD wOld = HIWORD(dwStatus);
|
|
//
|
|
// we need to call message pending here since there might be a message the app has not seen yet
|
|
//
|
|
if (wOld & (QS_POSTMESSAGE))
|
|
{
|
|
CairoleDebugOut((DEB_CALLCONT | DEB_IWARN, "Timer at zero - Calling HandleWakeForMsg\n"));
|
|
HandleWakeForMsg(dwInput, pCICur);
|
|
CairoleDebugOut((DEB_CALLCONT | DEB_IWARN, "Timer at zero - Calling HandleWakeForMsg done\n"));
|
|
}
|
|
#endif // _CHICAGO_
|
|
}
|
|
else
|
|
{
|
|
// an event was signaled
|
|
Win4Assert(dwWakeReason >= WAIT_OBJECT_0 && dwWakeReason < WAIT_OBJECT_0 + cEvents
|
|
&& "Bad return from (Msg)WaitFor(Single/Multiple)Objects");
|
|
|
|
// call completion event was signalled
|
|
CairoleDebugOut((DEB_CALLCONT, "CallComplete Event signaled\n"));
|
|
CallOnEvent(pCICur);
|
|
}
|
|
|
|
|
|
if (pCICur->GetCallState() == Call_Canceled)
|
|
return RPC_S_CALL_CANCELLED;
|
|
else
|
|
{
|
|
// return call fail in case of timout
|
|
return (dwWakeReason == WAIT_TIMEOUT) ? RPC_S_CALL_IN_PROGRESS : RPC_S_OK;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Member: CCallMainControl::TransmitAndRunModalLoop (private)
|
|
//
|
|
// Synopsis: transmits an outgoing call, then enters a modal loop until
|
|
// the reply comes in.
|
|
//
|
|
// Arguments: [pCI] - call information structure
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL CCallMainControl::TransmitAndRunModalLoop (PCALLINFO pCICur)
|
|
{
|
|
TRACECALL(TRACE_CALLCONT, "CCallMainControl::TrasmitAndRunModalLoop");
|
|
|
|
// set up the call info
|
|
// CODEWORK: nuke this and use a linked list on the stack
|
|
|
|
UINT id = SetupModalLoop(pCICur);
|
|
if (id == CALLDATAID_INVALID)
|
|
{
|
|
Win4Assert(id && "RunModalLoop: could not set up callinfo.\n");
|
|
return RPC_E_INVALID_CALLDATA;
|
|
}
|
|
|
|
COleTls tls;
|
|
PCALLINFO pCIPrev = (PCALLINFO) tls->pCALLINFO;
|
|
tls->pCALLINFO = (void *)pCICur;
|
|
|
|
do
|
|
{
|
|
// Transmit the call.
|
|
CairoleDebugOut((DEB_CALLCONT, "TransmitCall pCI:%x pCIPrev:%x\n", pCICur, pCIPrev));
|
|
TransmitCall(pCICur);
|
|
|
|
// In the MSWMSG protocol, TransmitCall is a blocking call. The Rpc
|
|
// transport will transmit the call asynchronously then call us back
|
|
// in BlockFn which is our real modal loop. When this call returns,
|
|
// IsWaiting will be FALSE.
|
|
//
|
|
// For all other protocols, including DDE, TransmitCall is non
|
|
// blocking so we have to call the BlockFn ourselves. In these cases,
|
|
// IsWaiting will be TRUE until the call completes.
|
|
//
|
|
// For all protocols, input synchronous calls and failed calls will
|
|
// return with IsWaiting FALSE.
|
|
|
|
while (pCICur->IsWaiting())
|
|
{
|
|
BlockFn();
|
|
}
|
|
|
|
// By this point the call has completed. Now check if it was rejected
|
|
// and if so, whether we need to retry immediately, later, or never.
|
|
// Handling of Rejected calls must occur here, not in the BlockFn, due
|
|
// to the fact that some calls and some protocols are synchronous, and
|
|
// other calls and protocols are asynchronous.
|
|
|
|
if (pCICur->IsRejected())
|
|
{
|
|
// this function decides on 1 of 3 different courses of action
|
|
// 1. fail the call - sets the state to Call_Rejected
|
|
// 2. retry immediately - starts timer but set to zero time
|
|
// 3. retry later - starts the timer, set to > 100
|
|
|
|
HandleRejectedCall(pCICur);
|
|
|
|
// have to go into modal loop if there is a timer installed to
|
|
// retry the call later. if the call is cancelled while in this
|
|
// loop, the loop will be exited.
|
|
|
|
while (!pCICur->IsTimerAtZero())
|
|
{
|
|
BlockFn();
|
|
}
|
|
|
|
// Either it is time to retransmit the call, or the call was
|
|
// cancelled or rejected. Clear the timer just in case.
|
|
|
|
pCICur->ClearTimer();
|
|
}
|
|
|
|
// the only way we could be waiting now is if the call is about to
|
|
// be retried after being rejected.
|
|
|
|
} while (pCICur->IsWaiting());
|
|
|
|
Win4Assert(pCICur->GetCallState() == Call_Ok ||
|
|
pCICur->GetCallState() == Call_Error ||
|
|
pCICur->GetCallState() == Call_Canceled ||
|
|
pCICur->GetCallState() == Call_Rejected);
|
|
|
|
HRESULT hr = pCICur->GetHresultOfCall();
|
|
|
|
// CODEWORK: do this in the dtor
|
|
// restore the current callinfo
|
|
tls->pCALLINFO = (void *)pCIPrev;
|
|
|
|
|
|
// reset the call info - we are done with this call
|
|
// CODEWORK: nuke this
|
|
ResetModalLoop(id);
|
|
|
|
// only the lowest modal loop should repost the Quit message
|
|
// CODEWORK: move this into dtor of pCI
|
|
pCICur->HandleQuitCode();
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Method: CCallMainControl::TransmitCall
|
|
//
|
|
// Synopsis: sets our call state and transmits the call to the server.
|
|
//
|
|
// Arguments: [pCI] - call info
|
|
//
|
|
// Returns: result of the call
|
|
//
|
|
// Algorithm:
|
|
//
|
|
// History: Dec-93 JohannP (Johann Posch) Created
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
INTERNAL CCallMainControl::TransmitCall(PCALLINFO pCI)
|
|
{
|
|
TRACECALL(TRACE_CALLCONT, "CCallMainControl::TransmitCall called");
|
|
Win4Assert(pCI && "CCallMainControl::TransmitCall Invalid CallInfo");
|
|
|
|
|
|
HRESULT hr;
|
|
// set our internal state to indicate we are making a call
|
|
pCI->SetCallState(Call_WaitOnCall, S_OK);
|
|
|
|
// Transmit may issue a callback to BlockFn.
|
|
hr = pCI->Transmit();
|
|
|
|
if (FAILED(hr))
|
|
{
|
|
if (hr == RPC_E_SERVERCALL_RETRYLATER)
|
|
{
|
|
pCI->SetCallState(Call_RetryLater, RPC_E_SERVERCALL_RETRYLATER);
|
|
}
|
|
else if (hr == RPC_E_SERVERCALL_REJECTED)
|
|
{
|
|
pCI->SetCallState(Call_Rejected, RPC_E_SERVERCALL_REJECTED);
|
|
}
|
|
else if (hr == RPC_E_CALL_CANCELED)
|
|
{
|
|
pCI->SetCallState(Call_Canceled, hr);
|
|
}
|
|
else
|
|
{
|
|
// the call failed, set the state to error. This also ensures
|
|
// IsWaiting returns FALSE.
|
|
pCI->SetCallState(Call_Error, hr);
|
|
}
|
|
}
|
|
|
|
return hr;
|
|
}
|