1511 lines
39 KiB
C
1511 lines
39 KiB
C
//============================================================================
|
|
// Copyright (c) 1995, Microsoft Corporation
|
|
//
|
|
// File: enum.c
|
|
//
|
|
// History:
|
|
// V Raman June-25-1997 Created.
|
|
//
|
|
// Enumeration functions exported to IP Router Manager.
|
|
//============================================================================
|
|
|
|
|
|
#include "pchmgm.h"
|
|
#pragma hdrstop
|
|
|
|
|
|
DWORD
|
|
GetGroupMfes(
|
|
IN PGROUP_ENTRY pge,
|
|
IN DWORD dwStartSource,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN DWORD dwBufferSize,
|
|
IN OUT PDWORD pdwSize,
|
|
IN OUT PDWORD pdwNumEntries,
|
|
IN BOOL bIncludeFirst,
|
|
IN DWORD dwFlags
|
|
);
|
|
|
|
|
|
VOID
|
|
CopyMfe(
|
|
IN PGROUP_ENTRY pge,
|
|
IN PSOURCE_ENTRY pse,
|
|
IN OUT PBYTE pb,
|
|
IN DWORD dwFlags
|
|
);
|
|
|
|
|
|
|
|
//
|
|
// MFE enumeration
|
|
//
|
|
|
|
//----------------------------------------------------------------------------
|
|
// GetNextMfe
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
DWORD
|
|
GetMfe(
|
|
IN PMIB_IPMCAST_MFE pmimm,
|
|
IN OUT PDWORD pdwBufferSize,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN DWORD dwFlags
|
|
)
|
|
{
|
|
|
|
BOOL bGrpLock = FALSE, bGrpEntryLock = FALSE;
|
|
|
|
DWORD dwErr = NO_ERROR, dwGrpBucket, dwSrcBucket, dwSizeReqd,
|
|
dwInd;
|
|
|
|
PGROUP_ENTRY pge;
|
|
|
|
PSOURCE_ENTRY pse;
|
|
|
|
POUT_IF_ENTRY poie;
|
|
|
|
PLIST_ENTRY ple, pleHead;
|
|
|
|
|
|
|
|
TRACEENUM3(
|
|
ENUM, "ENTERED GetMfe : %x, %x, Stats : %x", pmimm-> dwGroup,
|
|
pmimm-> dwSource, dwFlags
|
|
);
|
|
|
|
do
|
|
{
|
|
//
|
|
// Find group entry
|
|
//
|
|
|
|
dwGrpBucket = GROUP_TABLE_HASH( pmimm-> dwGroup, 0 );
|
|
|
|
ACQUIRE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
bGrpLock = TRUE;
|
|
|
|
pleHead = GROUP_BUCKET_HEAD( dwGrpBucket );
|
|
|
|
pge = GetGroupEntry( pleHead, pmimm-> dwGroup, 0 );
|
|
|
|
if ( pge == NULL )
|
|
{
|
|
//
|
|
// group entry not found, quit
|
|
//
|
|
|
|
dwErr = ERROR_NOT_FOUND;
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// acquire group entry lock and release group bucket lock
|
|
//
|
|
|
|
ACQUIRE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
bGrpEntryLock = TRUE;
|
|
|
|
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
bGrpLock = FALSE;
|
|
|
|
|
|
//
|
|
// Find Source entry
|
|
//
|
|
|
|
dwSrcBucket = SOURCE_TABLE_HASH( pmimm-> dwSource, pmimm-> dwSrcMask );
|
|
|
|
pleHead = SOURCE_BUCKET_HEAD( pge, dwSrcBucket );
|
|
|
|
pse = GetSourceEntry( pleHead, pmimm-> dwSource, pmimm-> dwSrcMask );
|
|
|
|
if ( pse == NULL )
|
|
{
|
|
//
|
|
// Source entry not found, quit
|
|
//
|
|
|
|
dwErr = ERROR_NOT_FOUND;
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// check buffersize requirements
|
|
//
|
|
|
|
dwSizeReqd = ( dwFlags ) ?
|
|
( (dwFlags == MGM_MFE_STATS_0) ?
|
|
SIZEOF_MIB_MFE_STATS( pse-> dwMfeIfCount ) :
|
|
SIZEOF_MIB_MFE_STATS_EX(
|
|
pse-> dwMfeIfCount ) ) :
|
|
SIZEOF_MIB_MFE( pse-> dwMfeIfCount );
|
|
|
|
if ( *pdwBufferSize < dwSizeReqd )
|
|
{
|
|
//
|
|
// buffer supplied is too small to fit the MFE
|
|
//
|
|
|
|
*pdwBufferSize = dwSizeReqd;
|
|
|
|
dwErr = ERROR_INSUFFICIENT_BUFFER;
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// if mfe statistics have been requested and
|
|
// mfe is in the kernel
|
|
// get it
|
|
//
|
|
|
|
if ( dwFlags && pse-> bInForwarder )
|
|
{
|
|
GetMfeFromForwarder( pge, pse );
|
|
}
|
|
|
|
#if 1
|
|
CopyMfe( pge, pse, pbBuffer, dwFlags );
|
|
#else
|
|
//
|
|
// copy base MFE into user supplied buffer
|
|
//
|
|
|
|
pmimms = ( PMIB_IPMCAST_MFE_STATS ) pbBuffer;
|
|
|
|
pmimms-> dwGroup = pge-> dwGroupAddr;
|
|
pmimms-> dwSource = pse-> dwSourceAddr;
|
|
pmimms-> dwSrcMask = pse-> dwSourceMask;
|
|
|
|
pmimms-> dwInIfIndex = pse-> dwInIfIndex;
|
|
pmimms-> dwUpStrmNgbr = pse-> dwUpstreamNeighbor;
|
|
pmimms-> dwInIfProtocol = pse-> dwInProtocolId;
|
|
|
|
pmimms-> dwRouteProtocol = pse-> dwRouteProtocol;
|
|
pmimms-> dwRouteNetwork = pse-> dwRouteNetwork;
|
|
pmimms-> dwRouteMask = pse-> dwRouteMask;
|
|
|
|
pmimms-> ulNumOutIf = pse-> imsStatistics.ulNumOutIf;
|
|
pmimms-> ulInPkts = pse-> imsStatistics.ulInPkts;
|
|
pmimms-> ulInOctets = pse-> imsStatistics.ulInOctets;
|
|
pmimms-> ulPktsDifferentIf = pse-> imsStatistics.ulPktsDifferentIf;
|
|
pmimms-> ulQueueOverflow = pse-> imsStatistics.ulQueueOverflow;
|
|
|
|
|
|
MgmElapsedSecs( &pse-> liCreationTime, &pmimms-> ulUpTime );
|
|
|
|
pmimms-> ulExpiryTime = pse-> dwTimeOut - pmimms-> ulUpTime;
|
|
|
|
|
|
//
|
|
// copy all the OIL entries
|
|
//
|
|
|
|
pleHead = &pse-> leMfeIfList;
|
|
|
|
for ( ple = pleHead-> Flink, dwInd = 0;
|
|
ple != pleHead;
|
|
ple = ple-> Flink, dwInd++ )
|
|
{
|
|
poie = CONTAINING_RECORD( ple, OUT_IF_ENTRY, leIfList );
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].dwOutIfIndex =
|
|
poie-> imosIfStats.dwOutIfIndex;
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].dwNextHopAddr =
|
|
poie-> imosIfStats.dwNextHopAddr;
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].ulTtlTooLow =
|
|
poie-> imosIfStats.ulTtlTooLow;
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].ulFragNeeded =
|
|
poie-> imosIfStats.ulFragNeeded;
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].ulOutPackets =
|
|
poie-> imosIfStats.ulOutPackets;
|
|
|
|
pmimms-> rgmiosOutStats[ dwInd ].ulOutDiscards =
|
|
poie-> imosIfStats.ulOutDiscards;
|
|
}
|
|
#endif
|
|
|
|
} while ( FALSE );
|
|
|
|
|
|
//
|
|
// release locks are appropriate
|
|
//
|
|
|
|
if ( bGrpEntryLock )
|
|
{
|
|
RELEASE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
}
|
|
|
|
if ( bGrpLock )
|
|
{
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
}
|
|
|
|
|
|
TRACEENUM1( ENUM, "LEAVING GetMfe :: %x", dwErr );
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
// GetNextMfe
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
DWORD
|
|
GetNextMfe(
|
|
IN PMIB_IPMCAST_MFE pmimmStart,
|
|
IN OUT PDWORD pdwBufferSize,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN OUT PDWORD pdwNumEntries,
|
|
IN BOOL bIncludeFirst,
|
|
IN DWORD dwFlags
|
|
)
|
|
{
|
|
|
|
BOOL bFound, bgeLock = FALSE;
|
|
|
|
DWORD dwGrpBucket, dwErr = NO_ERROR, dwBufferLeft,
|
|
dwStartSource, dwSize;
|
|
|
|
PBYTE pbStart;
|
|
|
|
PGROUP_ENTRY pge;
|
|
|
|
PLIST_ENTRY ple, pleMasterHead, pleGrpBucket;
|
|
|
|
|
|
|
|
TRACEENUM2(
|
|
ENUM, "ENTERED GetNextMfe (G, S) = (%x, %x)", pmimmStart-> dwGroup,
|
|
pmimmStart-> dwSource
|
|
);
|
|
|
|
|
|
do
|
|
{
|
|
//
|
|
// 1. Lock group hash bucket.
|
|
//
|
|
|
|
dwGrpBucket = GROUP_TABLE_HASH( pmimmStart-> dwGroup, 0 );
|
|
|
|
ACQUIRE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
|
|
//
|
|
// 2. merge temp and master lists
|
|
// - Lock temp list
|
|
// - merge temp with master list
|
|
// - unlock temp list
|
|
//
|
|
|
|
ACQUIRE_TEMP_GROUP_LOCK_EXCLUSIVE();
|
|
|
|
MergeTempAndMasterGroupLists( TEMP_GROUP_LIST_HEAD() );
|
|
|
|
ACQUIRE_MASTER_GROUP_LOCK_SHARED();
|
|
|
|
RELEASE_TEMP_GROUP_LOCK_EXCLUSIVE();
|
|
|
|
|
|
pleMasterHead = MASTER_GROUP_LIST_HEAD();
|
|
|
|
ple = pleMasterHead-> Flink;
|
|
|
|
//
|
|
// To retrieve the next set of group entries in lexicographic order,
|
|
// given a group entry (in this case specified by pmimmStart-> dwGroup)
|
|
// the master group list must be walked from the head until either
|
|
// the group entry specified is found or the next "higher" group entry
|
|
// is found. This is expensive.
|
|
//
|
|
// As an optimization the group specified (pmimmStart-> dwGroup) is
|
|
// looked up in the group hash table. If an entry is found, then the
|
|
// group entry contains links into the master (lexicographic) group
|
|
// list. These links can the used to determine the next entries in
|
|
// the group list. This way we can quickly find an group entry in
|
|
// the master list rather than walk the master group list from the
|
|
// beginning.
|
|
//
|
|
// It should be noted that in case the group entry specified in not
|
|
// present in the group hash table, it will be necessary to walk the
|
|
// master group list from the start.
|
|
//
|
|
// Each group entry is present in two lists, the hash bucket list
|
|
// and either temp group list or the master group list.
|
|
//
|
|
// For this optimization to "work", it must be ensured that an entry
|
|
// present in the hash table is also present in the master
|
|
// group list. To ensure this the temp group list is merged into
|
|
// the master group list before searching the group hash table for
|
|
// the specified entry.
|
|
//
|
|
|
|
|
|
//
|
|
// At this point the group under consideration (pmimmStart-> dwGroup),
|
|
// cannot be added to either the hash bucket or master group list
|
|
// if it is not already present because both the group hash bucket lock
|
|
// and the master list lock have been acquired.
|
|
//
|
|
|
|
//
|
|
// 3. find group entry in the hash list
|
|
//
|
|
|
|
pleGrpBucket = GROUP_BUCKET_HEAD( dwGrpBucket );
|
|
|
|
pge = GetGroupEntry( pleGrpBucket, pmimmStart-> dwGroup, 0 );
|
|
|
|
if ( pge != NULL )
|
|
{
|
|
//
|
|
// group entry for pmimmStart-> dwGroup is present. lock the entry.
|
|
//
|
|
|
|
ACQUIRE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
bgeLock = TRUE;
|
|
|
|
//
|
|
// release group hash bucket lock
|
|
//
|
|
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
}
|
|
|
|
else
|
|
{
|
|
|
|
//
|
|
// group entry is not present in the hash table, which implies
|
|
// that the group entry is not present at all.
|
|
//
|
|
|
|
//
|
|
// release group hash bucket lock
|
|
//
|
|
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
//
|
|
// 3.1 Walk master list from the start to determine the next
|
|
// highest group entry.
|
|
//
|
|
|
|
bFound = FindGroupEntry(
|
|
pleMasterHead, pmimmStart-> dwGroup, 0,
|
|
&pge, FALSE
|
|
);
|
|
|
|
if ( !bFound && pge == NULL )
|
|
{
|
|
//
|
|
// No more group entries left to enumerate
|
|
//
|
|
|
|
dwErr = ERROR_NO_MORE_ITEMS;
|
|
|
|
RELEASE_MASTER_GROUP_LOCK_SHARED();
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// Next group entry found. lock it
|
|
//
|
|
|
|
ACQUIRE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
bgeLock = TRUE;
|
|
|
|
bIncludeFirst = TRUE;
|
|
}
|
|
|
|
|
|
//
|
|
// At this point we have the group entry we want which is
|
|
// either the one for pmimmStart-> dwGroup OR the next higher
|
|
// one (if there is no group entry for pmimmStart-> Group).
|
|
//
|
|
|
|
//
|
|
// 4. Now get as many source entries as will fit into
|
|
// the buffer provided.
|
|
//
|
|
|
|
dwBufferLeft = *pdwBufferSize;
|
|
|
|
pbStart = pbBuffer;
|
|
|
|
*pdwNumEntries = 0;
|
|
|
|
dwStartSource = ( bIncludeFirst ) ? 0 : pmimmStart-> dwSource;
|
|
|
|
dwSize = 0;
|
|
|
|
|
|
while ( ( dwErr = GetGroupMfes( pge, dwStartSource, pbStart,
|
|
dwBufferLeft, &dwSize, pdwNumEntries,
|
|
bIncludeFirst, dwFlags ) )
|
|
== ERROR_MORE_DATA )
|
|
{
|
|
//
|
|
// more data items will fit into this buffer, but no more
|
|
// source entries available in this group entry
|
|
//
|
|
// 4.1 Move forward to next group entry.
|
|
//
|
|
|
|
pbStart += dwSize;
|
|
|
|
dwBufferLeft -= dwSize;
|
|
|
|
dwSize = 0;
|
|
|
|
dwStartSource = 0;
|
|
|
|
|
|
//
|
|
// 4.1.1 Release this group entry lock
|
|
//
|
|
|
|
RELEASE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
|
|
|
|
//
|
|
// 4.1.2 get next entry lock
|
|
//
|
|
|
|
ple = pge-> leGrpList.Flink;
|
|
|
|
if ( ple == pleMasterHead )
|
|
{
|
|
//
|
|
// No more group entries in the master group list.
|
|
// All MFEs have been exhausted. So quit.
|
|
//
|
|
|
|
dwErr = ERROR_NO_MORE_ITEMS;
|
|
|
|
bgeLock = FALSE;
|
|
|
|
break;
|
|
}
|
|
|
|
pge = CONTAINING_RECORD( ple, GROUP_ENTRY, leGrpList );
|
|
|
|
ACQUIRE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
|
|
dwStartSource = 0;
|
|
|
|
bIncludeFirst = TRUE;
|
|
}
|
|
|
|
|
|
//
|
|
// 5. you have packed as much as possible into the buffer
|
|
//
|
|
// Clean up and return the correct error code.
|
|
//
|
|
|
|
if ( bgeLock )
|
|
{
|
|
RELEASE_GROUP_ENTRY_LOCK_EXCLUSIVE( pge );
|
|
}
|
|
|
|
|
|
if ( dwErr == ERROR_INSUFFICIENT_BUFFER )
|
|
{
|
|
//
|
|
// ran out of buffer. If there is at least one Mfe
|
|
// packed into the buffer provided then it is ok.
|
|
//
|
|
|
|
if ( *pdwNumEntries != 0 )
|
|
{
|
|
dwErr = ERROR_MORE_DATA;
|
|
}
|
|
|
|
else
|
|
{
|
|
//
|
|
// not even one entry could be packed into the buffer
|
|
// return the size required for this so that an
|
|
// appropriately sized buffer can be allocated for the
|
|
// next call.
|
|
//
|
|
|
|
*pdwBufferSize = dwSize;
|
|
}
|
|
}
|
|
|
|
RELEASE_MASTER_GROUP_LOCK_SHARED();
|
|
|
|
} while ( FALSE );
|
|
|
|
|
|
TRACEENUM1( ENUM, "LEAVING GetNextMfe : %x", dwErr );
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
//
|
|
// GetGroupMfes
|
|
//
|
|
// Retrieves as many MFEs for a group starting at the specified source.
|
|
// Assumes that the group entry is locked.
|
|
//----------------------------------------------------------------------------
|
|
|
|
DWORD
|
|
GetGroupMfes(
|
|
IN PGROUP_ENTRY pge,
|
|
IN DWORD dwStartSource,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN DWORD dwBufferSize,
|
|
IN OUT PDWORD pdwSize,
|
|
IN OUT PDWORD pdwNumEntries,
|
|
IN BOOL bIncludeFirst,
|
|
IN DWORD dwFlags
|
|
)
|
|
{
|
|
|
|
BOOL bFound;
|
|
|
|
DWORD dwErr = ERROR_MORE_DATA, dwSrcBucket,
|
|
dwSizeReqd, dwInd;
|
|
|
|
PSOURCE_ENTRY pse = NULL;
|
|
|
|
PLIST_ENTRY pleMasterHead, pleSrcBucket, ple = NULL,
|
|
pleSrc;
|
|
|
|
POUT_IF_ENTRY poie = NULL;
|
|
|
|
|
|
TRACEENUM2(
|
|
ENUM, "ENTERED GetGroupMfes : %x, %x",
|
|
pge-> dwGroupAddr, dwStartSource
|
|
);
|
|
|
|
do
|
|
{
|
|
//
|
|
// merge temp and group source lists
|
|
//
|
|
|
|
MergeTempAndMasterSourceLists( pge );
|
|
|
|
|
|
//
|
|
// similar to the group lookup, optimize the source lookup
|
|
// by first trying to find the source entry in the source
|
|
// hash table.
|
|
//
|
|
// If found in the hash table then use the entry's links
|
|
// the into master source list to find next entry.
|
|
//
|
|
// if not found in the hash table walk the master list from
|
|
// the beginning to determine the next entry.
|
|
//
|
|
|
|
pleMasterHead = MASTER_SOURCE_LIST_HEAD( pge );
|
|
|
|
dwSrcBucket = SOURCE_TABLE_HASH( dwStartSource, 0 );
|
|
|
|
pleSrcBucket = SOURCE_BUCKET_HEAD( pge, dwSrcBucket );
|
|
|
|
|
|
bFound = FindSourceEntry( pleSrcBucket, dwStartSource, 0, &pse, TRUE );
|
|
|
|
if ( !bFound )
|
|
{
|
|
//
|
|
// source entry is not present in the hash table
|
|
// Walk the master source list from the start.
|
|
//
|
|
|
|
pse = NULL;
|
|
|
|
FindSourceEntry( pleMasterHead, 0, 0, &pse, FALSE );
|
|
|
|
|
|
//
|
|
// No next entry found in the master list. Implies
|
|
// no more sources in the master source list for this group.
|
|
//
|
|
|
|
if ( pse == NULL )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
//
|
|
// Entry for starting source found in hash table.
|
|
// Use its links into the master list to get next entry.
|
|
//
|
|
|
|
if ( !bIncludeFirst )
|
|
{
|
|
ple = pse-> leSrcList.Flink;
|
|
|
|
pse = CONTAINING_RECORD( ple, SOURCE_ENTRY, leSrcList );
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// At this point the entry pointed to by pse is the first entry
|
|
// the needs to be packed into the buffer supplied. Starting
|
|
// with this source entry keep packing MFEs into the
|
|
// buffer till there are no more MFEs for this group.
|
|
//
|
|
|
|
pleSrc = &pse-> leSrcList;
|
|
|
|
//
|
|
// while there are source entries for this group entry
|
|
//
|
|
|
|
while ( pleSrc != pleMasterHead )
|
|
{
|
|
pse = CONTAINING_RECORD( pleSrc, SOURCE_ENTRY, leSrcList );
|
|
|
|
|
|
//
|
|
// Is this source entry an MFE
|
|
//
|
|
|
|
if ( !IS_VALID_INTERFACE( pse-> dwInIfIndex,
|
|
pse-> dwInIfNextHopAddr ) )
|
|
{
|
|
pleSrc = pleSrc-> Flink;
|
|
|
|
continue;
|
|
}
|
|
|
|
|
|
//
|
|
// This source entry is an MFE also.
|
|
//
|
|
|
|
//
|
|
// Check if enough space left in the buffer to fit this MFE.
|
|
//
|
|
// If not and not a single MFE is present in the buffer then
|
|
// return the size required to fit this MFE.
|
|
//
|
|
|
|
dwSizeReqd = ( dwFlags ) ?
|
|
( ( dwFlags == MGM_MFE_STATS_0 ) ?
|
|
SIZEOF_MIB_MFE_STATS( pse-> dwMfeIfCount ) :
|
|
SIZEOF_MIB_MFE_STATS_EX(
|
|
pse-> dwMfeIfCount
|
|
) ) :
|
|
SIZEOF_MIB_MFE( pse-> dwMfeIfCount );
|
|
|
|
if ( dwBufferSize < dwSizeReqd )
|
|
{
|
|
dwErr = ERROR_INSUFFICIENT_BUFFER;
|
|
|
|
if ( *pdwNumEntries == 0 )
|
|
{
|
|
*pdwSize = dwSizeReqd;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// If MFE stats have been requested and
|
|
// MFE is present in the forwarder
|
|
// get them.
|
|
//
|
|
|
|
if ( dwFlags && pse-> bInForwarder )
|
|
{
|
|
//
|
|
// MFE is currently in the forwarder. Query it and update
|
|
// stats user mode.
|
|
//
|
|
|
|
GetMfeFromForwarder( pge, pse );
|
|
}
|
|
|
|
|
|
//
|
|
// copy base MFE into user supplied buffer
|
|
//
|
|
|
|
|
|
CopyMfe( pge, pse, pbBuffer, dwFlags );
|
|
|
|
pbBuffer += dwSizeReqd;
|
|
|
|
dwBufferSize -= dwSizeReqd;
|
|
|
|
*pdwSize += dwSizeReqd;
|
|
|
|
(*pdwNumEntries)++;
|
|
|
|
pleSrc = pleSrc-> Flink;
|
|
}
|
|
|
|
} while ( FALSE );
|
|
|
|
|
|
TRACEENUM2( ENUM, "LEAVING GetGroupsMfes : %d %d", *pdwNumEntries, dwErr );
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
|
|
//============================================================================
|
|
// Group Enumeration
|
|
//
|
|
//============================================================================
|
|
|
|
|
|
PGROUP_ENUMERATOR
|
|
VerifyEnumeratorHandle(
|
|
IN HANDLE hEnum
|
|
)
|
|
{
|
|
|
|
DWORD dwErr;
|
|
|
|
PGROUP_ENUMERATOR pgeEnum;
|
|
|
|
|
|
|
|
pgeEnum = (PGROUP_ENUMERATOR)
|
|
( ( (ULONG_PTR) hEnum )
|
|
^ (ULONG_PTR) MGM_ENUM_HANDLE_TAG );
|
|
|
|
try
|
|
{
|
|
if ( pgeEnum-> dwSignature != MGM_ENUM_SIGNATURE )
|
|
{
|
|
dwErr = ERROR_INVALID_PARAMETER;
|
|
|
|
TRACE0( ANY, "Invalid Enumeration handle" );
|
|
|
|
pgeEnum = NULL;
|
|
}
|
|
}
|
|
|
|
except ( GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ?
|
|
EXCEPTION_EXECUTE_HANDLER :
|
|
EXCEPTION_CONTINUE_SEARCH )
|
|
{
|
|
dwErr = ERROR_INVALID_PARAMETER;
|
|
|
|
TRACE0( ANY, "Invalid enumeration handle" );
|
|
|
|
pgeEnum = NULL;
|
|
}
|
|
|
|
return pgeEnum;
|
|
}
|
|
|
|
|
|
//
|
|
// Get Memberships for buckets
|
|
//
|
|
|
|
DWORD
|
|
GetNextGroupMemberships(
|
|
IN PGROUP_ENUMERATOR pgeEnum,
|
|
IN OUT PDWORD pdwBufferSize,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN OUT PDWORD pdwNumEntries
|
|
)
|
|
{
|
|
|
|
BOOL bIncludeFirst = TRUE, bFound;
|
|
|
|
DWORD dwMaxEntries, dwGrpBucket, dwErr = ERROR_NO_MORE_ITEMS;
|
|
|
|
PGROUP_ENTRY pge = NULL;
|
|
|
|
PSOURCE_GROUP_ENTRY psge;
|
|
|
|
PLIST_ENTRY ple, pleGrpHead;
|
|
|
|
|
|
|
|
do
|
|
{
|
|
//
|
|
// Compute the number of entries that will fit into the buffer
|
|
//
|
|
|
|
dwMaxEntries = (*pdwBufferSize) / sizeof( SOURCE_GROUP_ENTRY );
|
|
|
|
|
|
//
|
|
// STEP I :
|
|
//
|
|
|
|
//
|
|
// position the start of the GetNext to the group entry that was
|
|
// the last enumerated by the previous GetNext operation
|
|
//
|
|
|
|
//
|
|
// Find the last group entry retrieved by the previous get operation.
|
|
//
|
|
|
|
dwGrpBucket = GROUP_TABLE_HASH(
|
|
pgeEnum-> dwLastGroup, pgeEnum-> dwLastGroupMask
|
|
);
|
|
|
|
ACQUIRE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
pleGrpHead = GROUP_BUCKET_HEAD( dwGrpBucket );
|
|
|
|
bFound = FindGroupEntry(
|
|
pleGrpHead, pgeEnum-> dwLastGroup,
|
|
pgeEnum-> dwLastGroupMask, &pge, TRUE
|
|
);
|
|
|
|
if ( bFound )
|
|
{
|
|
//
|
|
// group entry found
|
|
//
|
|
|
|
bIncludeFirst = !pgeEnum-> bEnumBegun;
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// last group entry retrieved by previous getnext is no
|
|
// longer present
|
|
//
|
|
|
|
//
|
|
// check if there are any more group entries present in
|
|
// the same bucket
|
|
//
|
|
|
|
else if ( pge != NULL )
|
|
{
|
|
//
|
|
// Next group entry in the same group bucket.
|
|
// For a new group start from the first source bucket,
|
|
// first source entry.
|
|
//
|
|
|
|
pgeEnum-> dwLastSource = 0;
|
|
pgeEnum-> dwLastSourceMask = 0;
|
|
}
|
|
|
|
|
|
else // ( pge == NULL )
|
|
{
|
|
//
|
|
// no more entries in this group bucket, move to next
|
|
// non-empty group bucket entry.
|
|
//
|
|
|
|
//
|
|
// skip empty buckets in the group hash table
|
|
//
|
|
|
|
do
|
|
{
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
dwGrpBucket++;
|
|
|
|
if ( dwGrpBucket >= GROUP_TABLE_SIZE )
|
|
{
|
|
//
|
|
// Entire hash table has been traversed, quit
|
|
//
|
|
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Move to next group bucket
|
|
//
|
|
|
|
ACQUIRE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
pleGrpHead = GROUP_BUCKET_HEAD( dwGrpBucket );
|
|
|
|
|
|
//
|
|
// Check if any group entries present
|
|
//
|
|
|
|
if ( !IsListEmpty( pleGrpHead ) )
|
|
{
|
|
//
|
|
// group bucket has at least on group entry
|
|
//
|
|
|
|
pge = CONTAINING_RECORD(
|
|
pleGrpHead-> Flink, GROUP_ENTRY, leGrpHashList
|
|
);
|
|
|
|
//
|
|
// For a new group start from the first source bucket,
|
|
// first source entry.
|
|
//
|
|
|
|
pgeEnum-> dwLastSource = 0;
|
|
pgeEnum-> dwLastSourceMask = 0;
|
|
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Empty group bucket, move to next one
|
|
//
|
|
|
|
} while ( TRUE );
|
|
}
|
|
|
|
|
|
//
|
|
// if all hash buckets have been traversed, quit.
|
|
//
|
|
|
|
if ( dwGrpBucket >= GROUP_TABLE_SIZE )
|
|
{
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// STEP II:
|
|
//
|
|
|
|
//
|
|
// start retrieving group membership entries
|
|
//
|
|
|
|
ple = &pge-> leGrpHashList;
|
|
|
|
|
|
//
|
|
// Walk each hash bucket starting from dwGrpBucket to GROUP_TABLE_SIZE
|
|
//
|
|
|
|
while ( dwGrpBucket < GROUP_TABLE_SIZE )
|
|
{
|
|
//
|
|
// For each group hash table bucket
|
|
//
|
|
|
|
while ( ple != pleGrpHead )
|
|
{
|
|
//
|
|
// For each group entry in the bucket
|
|
//
|
|
|
|
pge = CONTAINING_RECORD( ple, GROUP_ENTRY, leGrpHashList );
|
|
|
|
ACQUIRE_GROUP_ENTRY_LOCK_SHARED( pge );
|
|
|
|
dwErr = GetNextMembershipsForThisGroup(
|
|
pge, pgeEnum, bIncludeFirst, pbBuffer,
|
|
pdwNumEntries, dwMaxEntries
|
|
);
|
|
|
|
RELEASE_GROUP_ENTRY_LOCK_SHARED( pge );
|
|
|
|
if ( dwErr == ERROR_MORE_DATA )
|
|
{
|
|
//
|
|
// User supplied buffer is full.
|
|
//
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// Move to next entry
|
|
//
|
|
|
|
ple = ple-> Flink;
|
|
|
|
//
|
|
// Next group entry in the same group bucket.
|
|
// For a new group start from the first source bucket,
|
|
// first source entry.
|
|
//
|
|
|
|
pgeEnum-> dwLastSource = 0;
|
|
|
|
pgeEnum-> dwLastSourceMask = 0;
|
|
|
|
bIncludeFirst = TRUE;
|
|
}
|
|
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
if ( dwErr == ERROR_MORE_DATA )
|
|
{
|
|
break;
|
|
}
|
|
|
|
|
|
//
|
|
// Move to next group bucket
|
|
//
|
|
|
|
dwGrpBucket++;
|
|
|
|
//
|
|
// skip empty group hash buckets
|
|
//
|
|
|
|
while ( dwGrpBucket < GROUP_TABLE_SIZE )
|
|
{
|
|
ACQUIRE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
pleGrpHead = GROUP_BUCKET_HEAD( dwGrpBucket );
|
|
|
|
if ( !IsListEmpty( pleGrpHead ) )
|
|
{
|
|
break;
|
|
}
|
|
|
|
RELEASE_GROUP_LOCK_SHARED( dwGrpBucket );
|
|
|
|
dwGrpBucket++;
|
|
}
|
|
|
|
|
|
if ( dwGrpBucket >= GROUP_TABLE_SIZE )
|
|
{
|
|
//
|
|
// All group buckets have traversed. End of enumeration
|
|
//
|
|
|
|
dwErr = ERROR_NO_MORE_ITEMS;
|
|
}
|
|
|
|
else
|
|
{
|
|
//
|
|
// New group hash bucket, start from source entry 0.
|
|
//
|
|
|
|
ple = pleGrpHead-> Flink;
|
|
|
|
pgeEnum-> dwLastSource = 0;
|
|
pgeEnum-> dwLastSourceMask = 0;
|
|
bIncludeFirst = TRUE;
|
|
}
|
|
}
|
|
|
|
} while ( FALSE );
|
|
|
|
pgeEnum-> bEnumBegun = TRUE;
|
|
|
|
|
|
//
|
|
// Store the position where the enumeration ended
|
|
//
|
|
|
|
psge = (PSOURCE_GROUP_ENTRY) pbBuffer;
|
|
|
|
if ( *pdwNumEntries )
|
|
{
|
|
pgeEnum-> dwLastSource = psge[ *pdwNumEntries - 1 ].dwSourceAddr;
|
|
|
|
pgeEnum-> dwLastSourceMask = psge[ *pdwNumEntries - 1 ].dwSourceMask;
|
|
|
|
pgeEnum-> dwLastGroup = psge[ *pdwNumEntries - 1 ].dwGroupAddr;
|
|
|
|
pgeEnum-> dwLastGroupMask = psge[ *pdwNumEntries - 1 ].dwGroupMask;
|
|
}
|
|
|
|
else
|
|
{
|
|
pgeEnum-> dwLastSource = 0xFFFFFFFF;
|
|
|
|
pgeEnum-> dwLastSourceMask = 0xFFFFFFFF;
|
|
|
|
pgeEnum-> dwLastGroup = 0xFFFFFFFF;
|
|
|
|
pgeEnum-> dwLastGroupMask = 0xFFFFFFFF;
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
// GetMemberships for Group
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
DWORD
|
|
GetNextMembershipsForThisGroup(
|
|
IN PGROUP_ENTRY pge,
|
|
IN OUT PGROUP_ENUMERATOR pgeEnum,
|
|
IN BOOL bIncludeFirst,
|
|
IN OUT PBYTE pbBuffer,
|
|
IN OUT PDWORD pdwNumEntries,
|
|
IN DWORD dwMaxEntries
|
|
)
|
|
{
|
|
|
|
BOOL bFound;
|
|
|
|
DWORD dwErr = ERROR_NO_MORE_ITEMS, dwSrcBucket;
|
|
|
|
PSOURCE_GROUP_ENTRY psgBuffer;
|
|
|
|
PSOURCE_ENTRY pse = NULL;
|
|
|
|
PLIST_ENTRY pleSrcHead, ple;
|
|
|
|
|
|
|
|
do
|
|
{
|
|
|
|
if ( *pdwNumEntries >= dwMaxEntries )
|
|
{
|
|
//
|
|
// quit here.
|
|
//
|
|
|
|
dwErr = ERROR_MORE_DATA;
|
|
|
|
break;
|
|
}
|
|
|
|
|
|
psgBuffer = (PSOURCE_GROUP_ENTRY) pbBuffer;
|
|
|
|
|
|
//
|
|
// STEP I:
|
|
// Position start of enumeration
|
|
//
|
|
|
|
dwSrcBucket = SOURCE_TABLE_HASH(
|
|
pgeEnum-> dwLastSource, pgeEnum-> dwLastSourceMask
|
|
);
|
|
|
|
pleSrcHead = SOURCE_BUCKET_HEAD( pge, dwSrcBucket );
|
|
|
|
bFound = FindSourceEntry(
|
|
pleSrcHead, pgeEnum-> dwLastSource,
|
|
pgeEnum-> dwLastSourceMask, &pse, TRUE
|
|
);
|
|
|
|
if ( bFound )
|
|
{
|
|
if ( ( bIncludeFirst ) && !IsListEmpty( &pse-> leOutIfList ) )
|
|
{
|
|
//
|
|
// the first group membership found.
|
|
//
|
|
|
|
psgBuffer[ *pdwNumEntries ].dwSourceAddr = pse-> dwSourceAddr;
|
|
|
|
psgBuffer[ *pdwNumEntries ].dwSourceMask = pse-> dwSourceMask;
|
|
|
|
psgBuffer[ *pdwNumEntries ].dwGroupAddr = pge-> dwGroupAddr;
|
|
|
|
psgBuffer[ (*pdwNumEntries)++ ].dwGroupMask = pge-> dwGroupMask;
|
|
|
|
if ( *pdwNumEntries >= dwMaxEntries )
|
|
{
|
|
//
|
|
// buffer full. quit here.
|
|
//
|
|
|
|
dwErr = ERROR_MORE_DATA;
|
|
|
|
break;
|
|
}
|
|
|
|
//
|
|
// move to next source
|
|
//
|
|
|
|
ple = pse-> leSrcHashList.Flink;
|
|
}
|
|
|
|
else
|
|
{
|
|
ple = pse-> leSrcHashList.Flink;
|
|
}
|
|
}
|
|
|
|
else if ( pse != NULL )
|
|
{
|
|
ple = &pse-> leSrcHashList;
|
|
}
|
|
|
|
else
|
|
{
|
|
ple = pleSrcHead-> Flink;
|
|
}
|
|
|
|
|
|
//
|
|
// STEP II:
|
|
//
|
|
// enumerate group memberships
|
|
//
|
|
|
|
while ( *pdwNumEntries < dwMaxEntries )
|
|
{
|
|
//
|
|
// for each source bucket
|
|
//
|
|
|
|
while ( ( ple != pleSrcHead ) &&
|
|
( *pdwNumEntries < dwMaxEntries ) )
|
|
{
|
|
//
|
|
// for each source entry in the bucket
|
|
//
|
|
|
|
//
|
|
// if group membership exists for this source
|
|
//
|
|
|
|
pse = CONTAINING_RECORD( ple, SOURCE_ENTRY, leSrcHashList );
|
|
|
|
if ( !IsListEmpty( &pse-> leOutIfList ) )
|
|
{
|
|
psgBuffer[ *pdwNumEntries ].dwSourceAddr =
|
|
pse-> dwSourceAddr;
|
|
|
|
psgBuffer[ *pdwNumEntries ].dwSourceMask =
|
|
pse-> dwSourceMask;
|
|
|
|
psgBuffer[ *pdwNumEntries ].dwGroupAddr =
|
|
pge-> dwGroupAddr;
|
|
|
|
psgBuffer[ (*pdwNumEntries)++ ].dwGroupMask =
|
|
pge-> dwGroupMask;
|
|
|
|
if ( *pdwNumEntries >= dwMaxEntries )
|
|
{
|
|
dwErr = ERROR_MORE_DATA;
|
|
}
|
|
}
|
|
|
|
ple = ple-> Flink;
|
|
}
|
|
|
|
dwSrcBucket++;
|
|
|
|
if ( dwSrcBucket < SOURCE_TABLE_SIZE )
|
|
{
|
|
pleSrcHead = SOURCE_BUCKET_HEAD( pge, dwSrcBucket );
|
|
|
|
ple = pleSrcHead-> Flink;
|
|
}
|
|
|
|
else
|
|
{
|
|
//
|
|
// all source buckets for this group have been
|
|
// traversed. quit this group entry
|
|
//
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
} while ( FALSE );
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
// Copy the MFE (optionally with stats)
|
|
//
|
|
//----------------------------------------------------------------------------
|
|
|
|
VOID
|
|
CopyMfe(
|
|
IN PGROUP_ENTRY pge,
|
|
IN PSOURCE_ENTRY pse,
|
|
IN OUT PBYTE pb,
|
|
IN DWORD dwFlags
|
|
)
|
|
{
|
|
DWORD dwInd;
|
|
|
|
PLIST_ENTRY ple, pleHead;
|
|
|
|
POUT_IF_ENTRY poie;
|
|
|
|
PMIB_IPMCAST_MFE pmimm = NULL;
|
|
|
|
PMIB_IPMCAST_MFE_STATS pmimms = NULL;
|
|
|
|
PMIB_IPMCAST_OIF_STATS pmimos = NULL;
|
|
|
|
//
|
|
// copy base MFE into user supplied buffer
|
|
//
|
|
|
|
if ( dwFlags )
|
|
{
|
|
//
|
|
// Need to base MFE
|
|
//
|
|
|
|
pmimms = ( PMIB_IPMCAST_MFE_STATS ) pb;
|
|
|
|
pmimms-> dwGroup = pge-> dwGroupAddr;
|
|
pmimms-> dwSource = pse-> dwSourceAddr;
|
|
pmimms-> dwSrcMask = pse-> dwSourceMask;
|
|
|
|
pmimms-> dwInIfIndex = pse-> dwInIfIndex;
|
|
pmimms-> dwUpStrmNgbr = pse-> dwUpstreamNeighbor;
|
|
pmimms-> dwInIfProtocol = pse-> dwInProtocolId;
|
|
|
|
pmimms-> dwRouteProtocol = pse-> dwRouteProtocol;
|
|
pmimms-> dwRouteNetwork = pse-> dwRouteNetwork;
|
|
pmimms-> dwRouteMask = pse-> dwRouteMask;
|
|
|
|
MgmElapsedSecs( &pse-> liCreationTime, &pmimms-> ulUpTime );
|
|
|
|
pmimms-> ulExpiryTime = pse-> dwTimeOut - pmimms-> ulUpTime;
|
|
|
|
|
|
//
|
|
// Copy incoming stats
|
|
//
|
|
|
|
pmimms-> ulNumOutIf = pse-> dwMfeIfCount;
|
|
pmimms-> ulInPkts = pse-> imsStatistics.ulInPkts;
|
|
pmimms-> ulInOctets = pse-> imsStatistics.ulInOctets;
|
|
pmimms-> ulPktsDifferentIf = pse-> imsStatistics.ulPktsDifferentIf;
|
|
pmimms-> ulQueueOverflow = pse-> imsStatistics.ulQueueOverflow;
|
|
|
|
if ( dwFlags & MGM_MFE_STATS_1 )
|
|
{
|
|
PMIB_IPMCAST_MFE_STATS_EX pmimmsex =
|
|
( PMIB_IPMCAST_MFE_STATS_EX ) pb;
|
|
|
|
pmimmsex-> ulUninitMfe = pse-> imsStatistics.ulUninitMfe;
|
|
pmimmsex-> ulNegativeMfe = pse-> imsStatistics.ulNegativeMfe;
|
|
pmimmsex-> ulInDiscards = pse-> imsStatistics.ulInDiscards;
|
|
pmimmsex-> ulInHdrErrors = pse-> imsStatistics.ulInHdrErrors;
|
|
pmimmsex-> ulTotalOutPackets= pse-> imsStatistics.ulTotalOutPackets;
|
|
|
|
pmimos = pmimmsex-> rgmiosOutStats;
|
|
}
|
|
|
|
else
|
|
{
|
|
pmimos = pmimms-> rgmiosOutStats;
|
|
}
|
|
|
|
//
|
|
// copy all the OIL entries
|
|
//
|
|
|
|
pleHead = &pse-> leMfeIfList;
|
|
|
|
for ( ple = pleHead-> Flink, dwInd = 0;
|
|
ple != pleHead;
|
|
ple = ple-> Flink, dwInd++ )
|
|
{
|
|
poie = CONTAINING_RECORD( ple, OUT_IF_ENTRY, leIfList );
|
|
|
|
pmimos[ dwInd ].dwOutIfIndex = poie-> dwIfIndex;
|
|
pmimos[ dwInd ].dwNextHopAddr = poie-> dwIfNextHopAddr;
|
|
|
|
//
|
|
// Copy outgoing stats
|
|
//
|
|
|
|
pmimos[ dwInd ].ulTtlTooLow = poie-> imosIfStats.ulTtlTooLow;
|
|
pmimos[ dwInd ].ulFragNeeded = poie-> imosIfStats.ulFragNeeded;
|
|
pmimos[ dwInd ].ulOutPackets = poie-> imosIfStats.ulOutPackets;
|
|
pmimos[ dwInd ].ulOutDiscards = poie-> imosIfStats.ulOutDiscards;
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
//
|
|
// Need to copy non-stats MFE structure only
|
|
//
|
|
|
|
pmimm = (PMIB_IPMCAST_MFE) pb;
|
|
|
|
pmimm-> dwGroup = pge-> dwGroupAddr;
|
|
pmimm-> dwSource = pse-> dwSourceAddr;
|
|
pmimm-> dwSrcMask = pse-> dwSourceMask;
|
|
|
|
pmimm-> dwInIfIndex = pse-> dwInIfIndex;
|
|
pmimm-> dwUpStrmNgbr = pse-> dwUpstreamNeighbor;
|
|
pmimm-> dwInIfProtocol = pse-> dwInProtocolId;
|
|
|
|
pmimm-> dwRouteProtocol = pse-> dwRouteProtocol;
|
|
pmimm-> dwRouteNetwork = pse-> dwRouteNetwork;
|
|
pmimm-> dwRouteMask = pse-> dwRouteMask;
|
|
|
|
pmimm-> ulNumOutIf = pse-> dwMfeIfCount;
|
|
|
|
MgmElapsedSecs( &pse-> liCreationTime, &pmimm-> ulUpTime );
|
|
|
|
pmimm-> ulExpiryTime = pse-> dwTimeOut - pmimm-> ulUpTime;
|
|
|
|
|
|
//
|
|
// copy all the OIL entries minus the stats
|
|
//
|
|
|
|
pleHead = &pse-> leMfeIfList;
|
|
|
|
for ( ple = pleHead-> Flink, dwInd = 0;
|
|
ple != pleHead;
|
|
ple = ple-> Flink, dwInd++ )
|
|
{
|
|
poie = CONTAINING_RECORD( ple, OUT_IF_ENTRY, leIfList );
|
|
|
|
pmimm-> rgmioOutInfo[ dwInd ].dwOutIfIndex =
|
|
poie-> dwIfIndex;
|
|
|
|
pmimm-> rgmioOutInfo[ dwInd ].dwNextHopAddr =
|
|
poie-> dwIfNextHopAddr;
|
|
}
|
|
}
|
|
}
|