//---------------------------------------------------------------------------- // // Microsoft Windows // Copyright (C) Microsoft Corporation, 1994 - 2000 // // File: rownotfy.cxx // // Contents: Rowset notification connection points // // Classes: CRowsetNotification // CRowsetAsynchNotification // // History: 16 Feb 1998 AlanW Created from conpt.cxx // //---------------------------------------------------------------------------- #include "pch.cxx" #pragma hdrstop #include #include #include "tabledbg.hxx" ///////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////// //////////////// CRowsetAsynchNotification methods /////////////// ///////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////// //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::CRowsetAsynchNotification, public // // Synopsis: Constructor for connection point container class. // // Arguments: [query] -- query object with notify info // [pRowset] -- Rowset pointer // [ErrorObject] -- OLE-DB error object // [fWatch] -- TRUE if watch notification to be done // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- CRowsetAsynchNotification::CRowsetAsynchNotification( PQuery & query, ULONG hCursor, IRowset * pRowset, CCIOleDBError & ErrorObject, BOOL fWatch) : _query(query), _hCursor(hCursor), CRowsetNotification( ), _AsynchConnectionPoint ( IID_IDBAsynchNotify ), _WatchConnectionPoint ( IID_IRowsetWatchNotify ), _fDoWatch (fWatch), _fPopulationComplete (FALSE), _cAdvise( 0 ), _pRowset(pRowset), _threadNotify(0), _threadNotifyId( 0 ) { _AsynchConnectionPoint.SetContrUnk( (IUnknown *)this ); if (_fDoWatch) { _WatchConnectionPoint.SetContrUnk( (IUnknown *)this ); } } //CRowsetAsynchNotification //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::~CRowsetAsynchNotification, public // // Synopsis: Destructor for rowset watch notification class // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- CRowsetAsynchNotification::~CRowsetAsynchNotification() { Win4Assert( _cRefs == 0 && _pContainer == 0 ); Win4Assert( _cAdvise == 0 ); if ( 0 != _pContainer ) StopNotifications(); } //~CRowsetAsynchNotification //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::AddRef, public // // Synopsis: Increments aggregated object ref. count. // // Returns: ULONG // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- STDMETHODIMP_(ULONG) CRowsetAsynchNotification::AddRef() { return InterlockedIncrement( (long *) &_cRefs ); } //AddRef //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::Release, public // // Synopsis: Decrements aggregated obj. ref. count, deletes on final release. // // Returns: ULONG // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- STDMETHODIMP_(ULONG) CRowsetAsynchNotification::Release() { long cRefs = InterlockedDecrement((long *) &_cRefs); tbDebugOut(( DEB_NOTIFY, "conpt: release, new crefs: %lx\n", _cRefs )); // If no references, make sure container doesn't know about me anymore if ( 0 == cRefs ) { Win4Assert( 0 == _pContainer ); if ( 0 != _pContainer ) { // must have gotten here through excess client release StopNotifications(); } delete this; } return cRefs; } //Release //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::StopNotifications // // Synopsis: Shuts down the notification thread (if any) // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- void CRowsetAsynchNotification::StopNotifications() { if ( GetCurrentThreadId() == _threadNotifyId ) { Win4Assert( !"Notification thread used illegally" ); return; } _EndNotifyThread(); Disconnect(); _AsynchConnectionPoint.Disconnect( ); if (_fDoWatch) _WatchConnectionPoint.Disconnect( ); } //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_EndNotifyThread // // Synopsis: Shuts down the notification thread (if any) // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- void CRowsetAsynchNotification::_EndNotifyThread() { // // Is there a thread out there to close? // if (0 != _threadNotify) { // Signal the thread to die and wait for it to do so, or just kill // the thread if it looks like it might be lost in client code. tbDebugOut(( DEB_NOTIFY, "set notify thread die event %lx\n", _evtEndNotifyThread.GetHandle() )); _evtEndNotifyThread.Set(); DWORD dw = WaitForSingleObject( _threadNotify, INFINITE ); BOOL fCloseWorked = CloseHandle( _threadNotify ); Win4Assert( fCloseWorked && "CloseHandle of notify thread failed" ); _threadNotify = 0; tbDebugOut(( DEB_NOTIFY, "notify thread is history \n")); } } //_EndNotifyThread //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_StartNotifyThread // // Synopsis: Starts the notification thread // // History: 17 Mar 1998 AlanW // //-------------------------------------------------------------------------- inline void CRowsetAsynchNotification::_StartNotifyThread() { tbDebugOut(( DEB_NOTIFY, "starting rowset notify thread\n" )); // First advise, create the thread Win4Assert(0 == _threadNotify); _threadNotify = CreateThread( 0, 65536, (LPTHREAD_START_ROUTINE) _NotifyThread, this, 0, & _threadNotifyId ); if (0 == _threadNotify) THROW( CException() ); } //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_NotifyThread, private // // Synopsis: Entry point for notification thread // // Arguments: [self] -- a container to call to do the work // // Returns: Thread exit code // // Notes: this function is "static" // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- DWORD CRowsetAsynchNotification::_NotifyThread( CRowsetAsynchNotification *self) { TRANSLATE_EXCEPTIONS; DWORD dw = self->_DoNotifications(); UNTRANSLATE_EXCEPTIONS; return dw; } //_NotifyThread //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_DoNotifications, private // // Synopsis: Collects notifications and passes them out to clients. // Loops sending notifications until event to end thread // arrives. // // Returns: Thread exit code // // History: 07-Oct-1994 dlee // //-------------------------------------------------------------------------- DWORD CRowsetAsynchNotification::_DoNotifications() { BOOL fContinue = TRUE; do { TRY { if (fContinue && _AsynchConnectionPoint.GetAdviseCount() ) fContinue = _DoAsynchNotification(); if (fContinue && _fDoWatch && _WatchConnectionPoint.GetAdviseCount() ) fContinue = _DoWatchNotification(); if (_fPopulationComplete && !_fDoWatch) fContinue = FALSE; } CATCH( CException, e ) { // don't want to 'break' out of a catch block, use variable fContinue = FALSE; } END_CATCH; // Sleep for a bit, but wake up if the thread is to go away, ULONG x = _evtEndNotifyThread.Wait( defNotificationSleepDuration, FALSE ); if ( STATUS_WAIT_0 == x ) fContinue = FALSE; } while ( fContinue ); return 0; } //_DoNotifications //+------------------------------------------------------------------------- // // Function: IsLowResources // // Synopsis: Returns TRUE if it looks like the error is resource related. // // Returns: BOOL - TRUE if low on resources // // History: 9 May 1999 dlee // //-------------------------------------------------------------------------- BOOL IsLowResources( SCODE sc ) { return E_OUTOFMEMORY == sc || STATUS_NO_MEMORY == sc || STATUS_COMMITMENT_LIMIT == sc || STATUS_INSUFFICIENT_RESOURCES == sc || HRESULT_FROM_WIN32( ERROR_COMMITMENT_LIMIT ) == sc || HRESULT_FROM_WIN32( ERROR_NO_SYSTEM_RESOURCES ) == sc || STG_E_INSUFFICIENTMEMORY == sc; } //IsLowResources //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_DoAsynchNotification, private // // Synopsis: Collects asynch notifications and passes them out to clients. // Tracks whether rowset population is completed, after which // no more notifications are given (except for completion // notifications for new advises). // // Returns: BOOL - TRUE if notification thread should continue // // History: 18 Mar 1998 AlanW // //-------------------------------------------------------------------------- inline DWORD MapAsynchPhaseAndOp( ULONG op, ULONG phase ) { Win4Assert( op == 0 && phase < 4 ); return (((op+1) << 4) | (1 << phase)); } #define ASYNC_PHASE_MASK 0x0F #define ASYNC_OP_MASK 0xF0 BOOL CRowsetAsynchNotification::_DoAsynchNotification() { DBCOUNTITEM ulDenominator = 0, ulNumerator = 0; DBCOUNTITEM cRows = 0; BOOL fNewRows = FALSE; SCODE sc = S_OK; TRY { CNotificationSync Sync( _evtEndNotifyThread.GetHandle() ); sc = _query.RatioFinished( Sync, _hCursor, ulDenominator, ulNumerator, cRows, fNewRows ); // Did the main thread tell this thread to go away? if ( SUCCEEDED( sc ) ) { Win4Assert( ulDenominator > 0 && ulNumerator <= ulDenominator ); if (fNewRows) OnRowChange( _pRowset, 0, 0, DBREASON_ROW_ASYNCHINSERT, DBEVENTPHASE_DIDEVENT, TRUE ); if (!_fPopulationComplete && (ulNumerator == ulDenominator)) { OnRowsetChange( _pRowset, DBREASON_ROWSET_POPULATIONCOMPLETE, DBEVENTPHASE_DIDEVENT, TRUE ); _fPopulationComplete = TRUE; } } } CATCH( CException, e ) { sc = e.GetErrorCode(); } END_CATCH; if ( STATUS_CANCELLED == sc ) return FALSE; BOOL fLowResources = IsLowResources( sc ); if ( !fLowResources && !SUCCEEDED( sc ) ) return FALSE; // Give notice to all active advises. // Need to be careful here to avoid deadlock. The enumerator // grabs the CPC's mutex. This limits what the client can do. ULONG ulAsynchPhase = DBASYNCHPHASE_POPULATION; if (_fPopulationComplete) ulAsynchPhase = DBASYNCHPHASE_COMPLETE; ULONG dwOpMask = MapAsynchPhaseAndOp( DBASYNCHOP_OPEN, ulAsynchPhase ); CEnumConnectionsLite Enum( _AsynchConnectionPoint ); CConnectionPointBase::CConnectionContext *pConnCtx = Enum.First(); while ( 0 != pConnCtx ) { IDBAsynchNotify *pNotifyAsynch = (IDBAsynchNotify *)(pConnCtx->_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging asynch client %x\n", pNotifyAsynch )); SCODE sc = S_OK; if ( fLowResources ) { // // Let the client know we're really wedged and they might not // get more notifications consistently. // sc = pNotifyAsynch->OnLowResource( 0 ); } else { if ( 0 == (pConnCtx->_dwSpare & dwOpMask) ) { sc = pNotifyAsynch->OnProgress( DB_NULL_HCHAPTER, DBASYNCHOP_OPEN, ulNumerator, ulDenominator, ulAsynchPhase, 0); } BOOL fOnStop = FALSE; if ( _fPopulationComplete && S_OK == sc ) { sc = DB_S_UNWANTEDPHASE; fOnStop = TRUE; } if (DB_S_UNWANTEDPHASE == sc) pConnCtx->_dwSpare |= (dwOpMask & ASYNC_PHASE_MASK); else if (DB_S_UNWANTEDOPERATION == sc) pConnCtx->_dwSpare |= (dwOpMask & ASYNC_OP_MASK); else if (E_NOTIMPL == sc) pConnCtx->_dwSpare |= (ASYNC_PHASE_MASK | ASYNC_OP_MASK); if ( fOnStop ) pNotifyAsynch->OnStop( DB_NULL_HCHAPTER, DBASYNCHOP_OPEN, S_OK, 0 ); } tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging asynch client\n" )); pConnCtx = Enum.Next(); } // Keep trying to get notifications even if fLowResources is TRUE return TRUE; } //_DoAsynchNotification //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::_DoWatchNotification, private // // Synopsis: Collects watch notifications and passes them out to clients. // // Returns: BOOL - TRUE if notification thread should continue // // History: 18 Mar 1998 AlanW // //-------------------------------------------------------------------------- BOOL CRowsetAsynchNotification::_DoWatchNotification() { // get notification information from the cursor CNotificationSync Sync( _evtEndNotifyThread.GetHandle() ); DBWATCHNOTIFY changeType; SCODE sc = _query.GetNotifications( Sync, changeType ); // Did the main thread tell this thread to go away? if (STATUS_CANCELLED == sc) return FALSE; if (!SUCCEEDED(sc)) return FALSE; // got some, give them to all active advises // Need to be careful here to avoid deadlock. The enumerator // grabs the CPC's mutex. We need to break out of the loop // if the notify thread needs to go away... tbDebugOut(( DEB_NOTIFY, "rownotfy: watch type %d\n", changeType )); CEnumConnectionsLite Enum( _WatchConnectionPoint ); CConnectionPointBase::CConnectionContext *pConnCtx = Enum.First(); while ( pConnCtx ) { IRowsetWatchNotify *pNotifyWatch = (IRowsetWatchNotify *)(pConnCtx->_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging watch client %x\n", pNotifyWatch )); pNotifyWatch->OnChange( _pRowset, changeType); tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging watch client\n" )); pConnCtx = Enum.Next(); } return TRUE; } //_DoWatchNotification //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::AdviseHelper, private static // // Synopsis: Starts the notification thread if this is the first advise. // // Arguments: [pHelperContext] - "this" pointer // [pConnPt] - the connection point, either Async or Watch CP // [pConnCtx] - pointer to connection context // // Notes: CPC critical section is assumed to be held. // // History: 17 Mar 1998 AlanW // //-------------------------------------------------------------------------- void CRowsetAsynchNotification::AdviseHelper( PVOID pHelperContext, CConnectionPointBase * pConnPt, CConnectionContext * pConnCtx ) { CRowsetAsynchNotification * pSelf = (CRowsetAsynchNotification *) pHelperContext; pSelf->_cAdvise++; if (1 == pSelf->_cAdvise) { // First advise, create the notification thread pSelf->_StartNotifyThread(); Win4Assert(0 != pSelf->_threadNotify); } } //+------------------------------------------------------------------------- // // Method: CRowsetAsynchNotification::UnadviseHelper, private static // // Synopsis: Stops the notification thread if this is the last active advise. // // Arguments: [pHelperContext] -- "this" pointer // [pConnPt] -- the connection point, either Async or // Watch CP // [pConnCtx] -- pointer to connection context // [lock] -- CPC lock // // Notes: CPC critical section is assumed to be held. // // History: 17 Mar 1998 AlanW // //-------------------------------------------------------------------------- void CRowsetAsynchNotification::UnadviseHelper( PVOID pHelperContext, CConnectionPointBase * pConnPt, CConnectionContext * pConnCtx, CReleasableLock & lock ) { CRowsetAsynchNotification * pSelf = (CRowsetAsynchNotification *) pHelperContext; Win4Assert( pSelf->_cAdvise > 0 ); if ( --pSelf->_cAdvise == 0 ) { // Release the lock so the notify thread has a change to end // when we tell it to end. lock.Release(); pSelf->_EndNotifyThread(); } } //+------------------------------------------------------------------------- // // Function: MapPhaseAndReason, inline local // // Synopsis: Encode IRowsetNotify phase and reason into a bit mask // for supported phases and reasons. Luckily, most supported // reasons have only a single phase. Multi-phase reasons take // 5 bits. // // Returns: DWORD - bit mask for the reason/phase combination // // History: 18 Mar 1998 AlanW // //-------------------------------------------------------------------------- const maxSinglePhaseReasons = 8; const maxMultiPhaseReasons = 4; const maxPhases = 5; const bitDidNotifyVote = 0x80000000; inline DWORD MapPhaseAndReason( ULONG phase, ULONG reason ) { Win4Assert( reason <= 21 && phase <= 4 ); BOOL fSinglePhase = TRUE; DWORD dwMappedReason = 0; switch (reason) { // single-phase reasons case DBREASON_ROW_ASYNCHINSERT: dwMappedReason = 0; break; case DBREASON_ROWSET_RELEASE: dwMappedReason = 1; break; case DBREASON_ROW_ACTIVATE: dwMappedReason = 2; break; case DBREASON_ROW_RELEASE: dwMappedReason = 3; break; case DBREASON_ROWSET_POPULATIONCOMPLETE: dwMappedReason = 4; break; case DBREASON_ROWSET_POPULATIONSTOPPED: dwMappedReason = 5; break; // multi-phase reasons case DBREASON_ROWSET_FETCHPOSITIONCHANGE: dwMappedReason = 0; fSinglePhase = FALSE; break; default: tbDebugOut(( DEB_ERROR, "MapPhaseAndReason - reason: %d phase: %d\n", reason, phase )); Win4Assert( !"MapPhaseAndReason: unhandled reason" ); } Win4Assert( FALSE == fSinglePhase || phase == DBEVENTPHASE_DIDEVENT ); if (fSinglePhase) { Win4Assert( dwMappedReason < maxSinglePhaseReasons ); dwMappedReason = (1 << dwMappedReason); } else { dwMappedReason = dwMappedReason*maxPhases + phase + maxSinglePhaseReasons; Win4Assert( dwMappedReason <= 30 ); dwMappedReason = (1 << dwMappedReason); } return dwMappedReason; } //+------------------------------------------------------------------------- // // Function: MapAllPhases, inline local // // Synopsis: Return a bit mask that includes all bits for all supported // phases of the encoded reason. // // Arguments: [dwMappedReasonAndPhase] - a bit mask returned by // MapPhaseAndReason. // // Returns: DWORD - bit mask for all phases // // History: 18 Mar 1998 AlanW // //-------------------------------------------------------------------------- inline DWORD MapAllPhases( DWORD dwMappedReasonAndPhase ) { Win4Assert( dwMappedReasonAndPhase != 0 ); if (dwMappedReasonAndPhase < (1<_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging notify client %x\n", pNotify )); SCODE sc1 = pNotify->OnFieldChange( pRowset, hRow, cColumns, rgColumns, eReason, ePhase, fCantDeny ); tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging notify client\n" )); pConnCtx = Enum.Next(); } return sc; } SCODE CRowsetNotification::OnRowChange ( IRowset * pRowset, DBCOUNTITEM cRows, const HROW rghRows[], DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny ) { // Note: we don't generate any row notifications which can be denied. Win4Assert( fCantDeny && DBEVENTPHASE_DIDEVENT == ePhase ); DWORD dwMask = MapPhaseAndReason( ePhase, eReason ); CEnumConnectionsLite Enum( *this ); CConnectionPointBase::CConnectionContext *pConnCtx = Enum.First(); while ( pConnCtx ) { if (0 == (pConnCtx->_dwSpare & dwMask)) { IRowsetNotify *pNotify = (IRowsetNotify *)(pConnCtx->_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging notify client %x\n", pNotify )); SCODE sc1 = pNotify->OnRowChange( pRowset, cRows, rghRows, eReason, ePhase, fCantDeny ); tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging notify client\n" )); if (DB_S_UNWANTEDPHASE == sc1) pConnCtx->_dwSpare |= dwMask; else if (DB_S_UNWANTEDREASON == sc1) pConnCtx->_dwSpare |= MapAllPhases(dwMask); } pConnCtx = Enum.Next(); } return S_OK; } //+------------------------------------------------------------------------- // // Method: CRowsetNotification::DoRowsetChangeCallout, private // // Synopsis: Issues an OnRowsetChange notification to all active // advises. If [fCantDeny] is FALSE, tracks which advises // were successfully notified and checks the votes. // // Arguments: [Enum] - a connection context enumeration // [pRowset] - the rowset issuing the notification // [eReason] - the notification reason // [ePhase] - the notification phase // [fCantDeny] - if TRUE, notification can't be denied // // Notes: CPC critical section is assumed to be held. // // History: 08 Apr 1998 AlanW // //-------------------------------------------------------------------------- BOOL CRowsetNotification::DoRowsetChangeCallout ( CEnumConnectionsLite & Enum, IRowset * pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny ) { DWORD dwMask = MapPhaseAndReason( ePhase, eReason ); CConnectionPointBase::CConnectionContext *pConnCtx; BOOL fVeto = FALSE; for ( pConnCtx = Enum.First(); pConnCtx; pConnCtx = Enum.Next() ) { if ( 0 == (pConnCtx->_dwSpare & dwMask) ) { IRowsetNotify *pNotify = (IRowsetNotify *)(pConnCtx->_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging notify client %x\n", pNotify )); SCODE sc1 = pNotify->OnRowsetChange( pRowset, eReason, ePhase, fCantDeny ); if ( !fCantDeny ) { if (S_FALSE == sc1) { tbDebugOut(( DEB_NOTIFY, "rownotfy: notify client veto'ed\n" )); fVeto = TRUE; break; } pConnCtx->_dwSpare |= bitDidNotifyVote; } if (DB_S_UNWANTEDPHASE == sc1) pConnCtx->_dwSpare |= dwMask; else if (DB_S_UNWANTEDREASON == sc1) pConnCtx->_dwSpare |= MapAllPhases(dwMask); tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging notify client\n" )); } } return fVeto; } SCODE CRowsetNotification::OnRowsetChange ( IRowset * pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny ) { // Note: we don't use SYNCHAFTER, and ABOUTTODO is done here after voting Win4Assert( DBEVENTPHASE_ABOUTTODO != ePhase && DBEVENTPHASE_SYNCHAFTER != ePhase ); BOOL fVeto = FALSE; CEnumConnectionsLite Enum( *this ); if ( DBEVENTPHASE_OKTODO == ePhase ) { Win4Assert( !fCantDeny ); fVeto = DoRowsetChangeCallout( Enum, pRowset, eReason, DBEVENTPHASE_OKTODO, FALSE ); if ( ! fVeto ) fVeto = DoRowsetChangeCallout( Enum, pRowset, eReason, DBEVENTPHASE_ABOUTTODO, FALSE ); DWORD dwMask = MapPhaseAndReason( DBEVENTPHASE_FAILEDTODO, eReason ); CConnectionPointBase::CConnectionContext *pConnCtx; for ( pConnCtx = Enum.First(); pConnCtx; pConnCtx = Enum.Next() ) { if (fVeto && 0 == (pConnCtx->_dwSpare & dwMask) && 0 != (pConnCtx->_dwSpare & bitDidNotifyVote) ) { IRowsetNotify *pNotify = (IRowsetNotify *)(pConnCtx->_pIUnk); tbDebugOut(( DEB_NOTIFY, "rownotfy: pinging notify client %x for FAIL\n", pNotify )); SCODE sc1 = pNotify->OnRowsetChange( pRowset, eReason, DBEVENTPHASE_FAILEDTODO, TRUE ); tbDebugOut(( DEB_NOTIFY, "rownotfy:(end) pinging notify client for FAIL\n" )); if (DB_S_UNWANTEDPHASE == sc1) pConnCtx->_dwSpare |= dwMask; else if (DB_S_UNWANTEDREASON == sc1) pConnCtx->_dwSpare |= MapAllPhases(dwMask); } pConnCtx->_dwSpare &= ~bitDidNotifyVote; } } else { // a non-vetoable notification; just send the notifies Win4Assert( fCantDeny ); DoRowsetChangeCallout( Enum, pRowset, eReason, ePhase, TRUE ); } return fVeto ? S_FALSE : S_OK; }