698 lines
16 KiB
C
698 lines
16 KiB
C
|
|
/*************************************************************************
|
|
*
|
|
* timer.c
|
|
*
|
|
* This module contains the ICA timer routines.
|
|
*
|
|
* Copyright 1998, Microsoft.
|
|
*
|
|
*************************************************************************/
|
|
|
|
/*
|
|
* Includes
|
|
*/
|
|
#include <precomp.h>
|
|
#pragma hdrstop
|
|
#include <ntddkbd.h>
|
|
#include <ntddmou.h>
|
|
|
|
|
|
/*
|
|
* Local structures
|
|
*/
|
|
typedef VOID (*PICATIMERFUNC)( PVOID, PVOID );
|
|
|
|
typedef struct _ICA_WORK_ITEM {
|
|
LIST_ENTRY Links;
|
|
WORK_QUEUE_ITEM WorkItem;
|
|
PICATIMERFUNC pFunc;
|
|
PVOID pParam;
|
|
PSDLINK pSdLink;
|
|
ULONG LockFlags;
|
|
ULONG fCanceled: 1;
|
|
} ICA_WORK_ITEM, *PICA_WORK_ITEM;
|
|
|
|
/*
|
|
* Timer structure
|
|
*/
|
|
typedef struct _ICA_TIMER {
|
|
LONG RefCount;
|
|
KTIMER kTimer;
|
|
KDPC TimerDpc;
|
|
PSDLINK pSdLink;
|
|
LIST_ENTRY WorkItemListHead;
|
|
} ICA_TIMER, * PICA_TIMER;
|
|
|
|
|
|
/*
|
|
* Local procedure prototypes
|
|
*/
|
|
VOID
|
|
_IcaTimerDpc(
|
|
IN struct _KDPC *Dpc,
|
|
IN PVOID DeferredContext,
|
|
IN PVOID SystemArgument1,
|
|
IN PVOID SystemArgument2
|
|
);
|
|
|
|
VOID
|
|
_IcaDelayedWorker(
|
|
IN PVOID WorkerContext
|
|
);
|
|
|
|
BOOLEAN
|
|
_IcaCancelTimer(
|
|
PICA_TIMER pTimer,
|
|
PICA_WORK_ITEM *ppWorkItem
|
|
);
|
|
|
|
VOID
|
|
_IcaReferenceTimer(
|
|
PICA_TIMER pTimer
|
|
);
|
|
|
|
VOID
|
|
_IcaDereferenceTimer(
|
|
PICA_TIMER pTimer
|
|
);
|
|
|
|
NTSTATUS
|
|
IcaExceptionFilter(
|
|
IN PWSTR OutputString,
|
|
IN PEXCEPTION_POINTERS pexi
|
|
);
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaTimerCreate
|
|
*
|
|
* Create a timer
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* pContext (input)
|
|
* Pointer to SDCONTEXT of caller
|
|
* phTimer (output)
|
|
* address to return timer handle
|
|
*
|
|
* EXIT:
|
|
* STATUS_SUCCESS - no error
|
|
*
|
|
******************************************************************************/
|
|
|
|
NTSTATUS
|
|
IcaTimerCreate(
|
|
IN PSDCONTEXT pContext,
|
|
OUT PVOID * phTimer
|
|
)
|
|
{
|
|
PICA_TIMER pTimer;
|
|
NTSTATUS Status;
|
|
|
|
/*
|
|
* Allocate timer object and initialize it
|
|
*/
|
|
pTimer = ICA_ALLOCATE_POOL( NonPagedPool, sizeof(ICA_TIMER) );
|
|
if ( pTimer == NULL )
|
|
return( STATUS_NO_MEMORY );
|
|
|
|
RtlZeroMemory( pTimer, sizeof(ICA_TIMER) );
|
|
pTimer->RefCount = 1;
|
|
KeInitializeTimer( &pTimer->kTimer );
|
|
KeInitializeDpc( &pTimer->TimerDpc, _IcaTimerDpc, pTimer );
|
|
pTimer->pSdLink = CONTAINING_RECORD( pContext, SDLINK, SdContext );
|
|
InitializeListHead( &pTimer->WorkItemListHead );
|
|
|
|
TRACESTACK(( pTimer->pSdLink->pStack, TC_ICADD, TT_API3, "ICADD: TimerCreate: %08x\n", pTimer ));
|
|
|
|
*phTimer = (PVOID) pTimer;
|
|
return( STATUS_SUCCESS );
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaTimerStart
|
|
*
|
|
* Start a timer
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* TimerHandle (input)
|
|
* timer handle
|
|
* pFunc (input)
|
|
* address of procedure to call when timer expires
|
|
* pParam (input)
|
|
* parameter to pass to procedure
|
|
* TimeLeft (input)
|
|
* relative time until timer expires (1/1000 seconds)
|
|
* LockFlags (input)
|
|
* Bit flags to specify which (if any) stack locks to obtain
|
|
*
|
|
* EXIT:
|
|
* TRUE : timer was already armed and had to be canceled
|
|
* FALSE : timer was not armed
|
|
*
|
|
******************************************************************************/
|
|
|
|
BOOLEAN
|
|
IcaTimerStart(
|
|
IN PVOID TimerHandle,
|
|
IN PVOID pFunc,
|
|
IN PVOID pParam,
|
|
IN ULONG TimeLeft,
|
|
IN ULONG LockFlags )
|
|
{
|
|
PICA_TIMER pTimer = (PICA_TIMER)TimerHandle;
|
|
KIRQL oldIrql;
|
|
PICA_WORK_ITEM pWorkItem;
|
|
LARGE_INTEGER DueTime;
|
|
BOOLEAN bCanceled, bSet;
|
|
|
|
TRACESTACK(( pTimer->pSdLink->pStack, TC_ICADD, TT_API3,
|
|
"ICADD: TimerStart: %08x, Time %08x, pFunc %08x (%08x)\n",
|
|
TimerHandle, TimeLeft, pFunc, pParam ));
|
|
|
|
ASSERT( ExIsResourceAcquiredExclusiveLite( &pTimer->pSdLink->pStack->Resource ) );
|
|
|
|
/*
|
|
* Cancel the timer if it currently armed,
|
|
* and get the current workitem and reuse it if there was one.
|
|
*/
|
|
bCanceled = _IcaCancelTimer( pTimer, &pWorkItem );
|
|
|
|
/*
|
|
* Initialize the ICA work item (allocate one first if there isn't one).
|
|
*/
|
|
if ( pWorkItem == NULL ) {
|
|
pWorkItem = ICA_ALLOCATE_POOL( NonPagedPool, sizeof(ICA_WORK_ITEM) );
|
|
if ( pWorkItem == NULL ) {
|
|
return( FALSE );
|
|
}
|
|
}
|
|
|
|
pWorkItem->pFunc = pFunc;
|
|
pWorkItem->pParam = pParam;
|
|
pWorkItem->pSdLink = pTimer->pSdLink;
|
|
pWorkItem->LockFlags = LockFlags;
|
|
pWorkItem->fCanceled = FALSE;
|
|
ExInitializeWorkItem( &pWorkItem->WorkItem, _IcaDelayedWorker, pWorkItem );
|
|
|
|
/*
|
|
* If the timer was NOT canceled above (we are setting it for
|
|
* the first time), then reference the SDLINK object on behalf
|
|
* of the timer thread.
|
|
*/
|
|
if ( !bCanceled )
|
|
IcaReferenceSdLink( pTimer->pSdLink );
|
|
|
|
/*
|
|
* If timer should run immediately, then just queue the
|
|
* workitem to an ExWorker thread now.
|
|
*/
|
|
if ( TimeLeft == 0 ) {
|
|
|
|
ExQueueWorkItem( &pWorkItem->WorkItem, CriticalWorkQueue );
|
|
|
|
} else {
|
|
|
|
/*
|
|
* Convert timer time from milliseconds to system relative time
|
|
*/
|
|
DueTime = RtlEnlargedIntegerMultiply( TimeLeft, -10000 );
|
|
|
|
/*
|
|
* Increment the timer reference count,
|
|
* insert the workitem onto the workitem list,
|
|
* and arm the timer.
|
|
*/
|
|
_IcaReferenceTimer( pTimer );
|
|
IcaAcquireSpinLock( &IcaSpinLock, &oldIrql );
|
|
InsertTailList( &pTimer->WorkItemListHead, &pWorkItem->Links );
|
|
IcaReleaseSpinLock( &IcaSpinLock, oldIrql );
|
|
bSet = KeSetTimer( &pTimer->kTimer, DueTime, &pTimer->TimerDpc );
|
|
ASSERT( !bSet );
|
|
}
|
|
|
|
return( bCanceled );
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaTimerCancel
|
|
*
|
|
* cancel the specified timer
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* TimerHandle (input)
|
|
* timer handle
|
|
*
|
|
* EXIT:
|
|
* TRUE : timer was actually canceled
|
|
* FALSE : timer was not armed
|
|
*
|
|
******************************************************************************/
|
|
|
|
BOOLEAN
|
|
IcaTimerCancel( IN PVOID TimerHandle )
|
|
{
|
|
PICA_TIMER pTimer = (PICA_TIMER)TimerHandle;
|
|
BOOLEAN bCanceled;
|
|
|
|
TRACESTACK(( pTimer->pSdLink->pStack, TC_ICADD, TT_API3,
|
|
"ICADD: TimerCancel: %08x\n", pTimer ));
|
|
|
|
ASSERT( ExIsResourceAcquiredExclusiveLite( &pTimer->pSdLink->pStack->Resource ) );
|
|
|
|
/*
|
|
* Cancel timer if it is enabled
|
|
*/
|
|
bCanceled = _IcaCancelTimer( pTimer, NULL );
|
|
if ( bCanceled )
|
|
IcaDereferenceSdLink( pTimer->pSdLink );
|
|
|
|
return( bCanceled );
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaTimerClose
|
|
*
|
|
* cancel the specified timer
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* TimerHandle (input)
|
|
* timer handle
|
|
*
|
|
* EXIT:
|
|
* TRUE : timer was actually canceled
|
|
* FALSE : timer was not armed
|
|
*
|
|
******************************************************************************/
|
|
|
|
BOOLEAN
|
|
IcaTimerClose( IN PVOID TimerHandle )
|
|
{
|
|
PICA_TIMER pTimer = (PICA_TIMER)TimerHandle;
|
|
BOOLEAN bCanceled;
|
|
|
|
TRACESTACK(( pTimer->pSdLink->pStack, TC_ICADD, TT_API3,
|
|
"ICADD: TimerClose: %08x\n", pTimer ));
|
|
|
|
ASSERT( ExIsResourceAcquiredExclusiveLite( &pTimer->pSdLink->pStack->Resource ) );
|
|
|
|
/*
|
|
* Cancel timer if it is enabled
|
|
*/
|
|
bCanceled = IcaTimerCancel( TimerHandle );
|
|
|
|
/*
|
|
* Decrement timer reference
|
|
* (the last reference will free the object)
|
|
*/
|
|
//ASSERT( pTimer->RefCount == 1 );
|
|
//ASSERT( IsListEmpty( &pTimer->WorkItemListHead ) );
|
|
_IcaDereferenceTimer( pTimer );
|
|
|
|
return( bCanceled );
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaQueueWorkItemEx, IcaQueueWorkItem.
|
|
*
|
|
* Queue a work item for async execution
|
|
*
|
|
* REM: IcaQueueWorkItemEx is the new API. It allows the caller to preallocate
|
|
* the ICA_WORK_ITEM. IcaQueueWorkItem is left there for lecacy drivers that have not
|
|
* been compiled with the new library not to crash the system.
|
|
*
|
|
* ENTRY:
|
|
* pContext (input)
|
|
* Pointer to SDCONTEXT of caller
|
|
* pFunc (input)
|
|
* address of procedure to call when timer expires
|
|
* pParam (input)
|
|
* parameter to pass to procedure
|
|
* LockFlags (input)
|
|
* Bit flags to specify which (if any) stack locks to obtain
|
|
*
|
|
* EXIT:
|
|
* STATUS_SUCCESS - no error
|
|
*
|
|
******************************************************************************/
|
|
|
|
|
|
|
|
NTSTATUS
|
|
IcaQueueWorkItem(
|
|
IN PSDCONTEXT pContext,
|
|
IN PVOID pFunc,
|
|
IN PVOID pParam,
|
|
IN ULONG LockFlags )
|
|
{
|
|
PSDLINK pSdLink;
|
|
PICA_WORK_ITEM pWorkItem;
|
|
|
|
NTSTATUS Status;
|
|
|
|
Status = IcaQueueWorkItemEx( pContext, pFunc, pParam, LockFlags, NULL );
|
|
return Status;
|
|
}
|
|
|
|
|
|
NTSTATUS
|
|
IcaQueueWorkItemEx(
|
|
IN PSDCONTEXT pContext,
|
|
IN PVOID pFunc,
|
|
IN PVOID pParam,
|
|
IN ULONG LockFlags,
|
|
IN PVOID pIcaWorkItem )
|
|
{
|
|
PSDLINK pSdLink;
|
|
PICA_WORK_ITEM pWorkItem = (PICA_WORK_ITEM) pIcaWorkItem;
|
|
|
|
pSdLink = CONTAINING_RECORD( pContext, SDLINK, SdContext );
|
|
|
|
/*
|
|
* Allocate the ICA work item if not yet allocated and initialize it.
|
|
*/
|
|
if (pWorkItem == NULL) {
|
|
pWorkItem = ICA_ALLOCATE_POOL( NonPagedPool, sizeof(ICA_WORK_ITEM) );
|
|
if ( pWorkItem == NULL )
|
|
return( STATUS_NO_MEMORY );
|
|
}
|
|
|
|
pWorkItem->pFunc = pFunc;
|
|
pWorkItem->pParam = pParam;
|
|
pWorkItem->pSdLink = pSdLink;
|
|
pWorkItem->LockFlags = LockFlags;
|
|
ExInitializeWorkItem( &pWorkItem->WorkItem, _IcaDelayedWorker, pWorkItem );
|
|
|
|
/*
|
|
* Reference the SDLINK object on behalf of the delayed worker routine.
|
|
*/
|
|
IcaReferenceSdLink( pSdLink );
|
|
|
|
/*
|
|
* Queue work item to an ExWorker thread.
|
|
*/
|
|
ExQueueWorkItem( &pWorkItem->WorkItem, CriticalWorkQueue );
|
|
|
|
return( STATUS_SUCCESS );
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* IcaAllocateWorkItem.
|
|
*
|
|
* Allocate ICA_WORK_ITEM structure to queue a workitem.
|
|
*
|
|
* REM: The main reason to allocate this in termdd (instead of doing it
|
|
* in the caller is to keep ICA_WORK_ITEM an internal termdd structure that is
|
|
* opaque for protocol drivers. There is no need for an IcaFreeWorkItem() API in
|
|
* termdd since the deallocation is transparently done in termdd once the workitem
|
|
* has been delivered.
|
|
*
|
|
* ENTRY:
|
|
* pParam (output) : pointer to return allocated workitem
|
|
*
|
|
* EXIT:
|
|
* STATUS_SUCCESS - no error
|
|
*
|
|
******************************************************************************/
|
|
|
|
NTSTATUS
|
|
IcaAllocateWorkItem(
|
|
OUT PVOID *pParam )
|
|
{
|
|
PICA_WORK_ITEM pWorkItem;
|
|
|
|
*pParam = ICA_ALLOCATE_POOL( NonPagedPool, sizeof(ICA_WORK_ITEM) );
|
|
if ( *pParam == NULL ){
|
|
return( STATUS_NO_MEMORY );
|
|
}
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* _IcaTimerDpc
|
|
*
|
|
* Ica timer DPC routine.
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* Dpc (input)
|
|
* Unused
|
|
*
|
|
* DeferredContext (input)
|
|
* Pointer to ICA_TIMER object.
|
|
*
|
|
* SystemArgument1 (input)
|
|
* Unused
|
|
*
|
|
* SystemArgument2 (input)
|
|
* Unused
|
|
*
|
|
* EXIT:
|
|
* nothing
|
|
*
|
|
******************************************************************************/
|
|
|
|
VOID
|
|
_IcaTimerDpc(
|
|
IN struct _KDPC *Dpc,
|
|
IN PVOID DeferredContext,
|
|
IN PVOID SystemArgument1,
|
|
IN PVOID SystemArgument2
|
|
)
|
|
{
|
|
PICA_TIMER pTimer = (PICA_TIMER)DeferredContext;
|
|
KIRQL oldIrql;
|
|
PLIST_ENTRY Head;
|
|
PICA_WORK_ITEM pWorkItem;
|
|
|
|
/*
|
|
* Acquire spinlock and remove the first workitem from the list
|
|
*/
|
|
IcaAcquireSpinLock( &IcaSpinLock, &oldIrql );
|
|
|
|
Head = RemoveHeadList( &pTimer->WorkItemListHead );
|
|
pWorkItem = CONTAINING_RECORD( Head, ICA_WORK_ITEM, Links );
|
|
|
|
IcaReleaseSpinLock( &IcaSpinLock, oldIrql );
|
|
|
|
/*
|
|
* If workitem has been canceled, just free the memory now.
|
|
*/
|
|
if ( pWorkItem->fCanceled ) {
|
|
|
|
ICA_FREE_POOL( pWorkItem );
|
|
|
|
/*
|
|
* Otherwise, queue workitem to an ExWorker thread.
|
|
*/
|
|
} else {
|
|
|
|
ExQueueWorkItem( &pWorkItem->WorkItem, CriticalWorkQueue );
|
|
}
|
|
|
|
_IcaDereferenceTimer( pTimer );
|
|
}
|
|
|
|
|
|
/*******************************************************************************
|
|
*
|
|
* _IcaDelayedWorker
|
|
*
|
|
* Ica delayed worker routine.
|
|
*
|
|
*
|
|
* ENTRY:
|
|
* WorkerContext (input)
|
|
* Pointer to ICA_WORK_ITEM object.
|
|
*
|
|
* EXIT:
|
|
* nothing
|
|
*
|
|
******************************************************************************/
|
|
|
|
VOID
|
|
_IcaDelayedWorker(
|
|
IN PVOID WorkerContext
|
|
)
|
|
{
|
|
PICA_CONNECTION pConnect;
|
|
PICA_WORK_ITEM pWorkItem = (PICA_WORK_ITEM)WorkerContext;
|
|
PICA_STACK pStack = pWorkItem->pSdLink->pStack;
|
|
NTSTATUS Status;
|
|
|
|
/*
|
|
* Obtain any required locks before calling the worker routine.
|
|
*/
|
|
if ( pWorkItem->LockFlags & ICALOCK_IO ) {
|
|
pConnect = IcaLockConnectionForStack( pStack );
|
|
}
|
|
if ( pWorkItem->LockFlags & ICALOCK_DRIVER ) {
|
|
IcaLockStack( pStack );
|
|
}
|
|
|
|
/*
|
|
* Call the worker routine.
|
|
*/
|
|
try {
|
|
(*pWorkItem->pFunc)( pWorkItem->pSdLink->SdContext.pContext,
|
|
pWorkItem->pParam );
|
|
} except( IcaExceptionFilter( L"_IcaDelayedWorker TRAPPED!!",
|
|
GetExceptionInformation() ) ) {
|
|
Status = GetExceptionCode();
|
|
}
|
|
|
|
/*
|
|
* Release any locks acquired above.
|
|
*/
|
|
if ( pWorkItem->LockFlags & ICALOCK_DRIVER ) {
|
|
IcaUnlockStack( pStack );
|
|
}
|
|
if ( pWorkItem->LockFlags & ICALOCK_IO ) {
|
|
IcaUnlockConnection( pConnect );
|
|
}
|
|
|
|
/*
|
|
* Dereference the SDLINK object now.
|
|
* This undoes the reference that was made on our behalf in the
|
|
* IcaTimerStart or IcaQueueWorkItem routine.
|
|
*/
|
|
IcaDereferenceSdLink( pWorkItem->pSdLink );
|
|
|
|
/*
|
|
* Free the ICA_WORK_ITEM memory block.
|
|
*/
|
|
ICA_FREE_POOL( pWorkItem );
|
|
}
|
|
|
|
|
|
BOOLEAN
|
|
_IcaCancelTimer(
|
|
PICA_TIMER pTimer,
|
|
PICA_WORK_ITEM *ppWorkItem
|
|
)
|
|
{
|
|
KIRQL oldIrql;
|
|
PLIST_ENTRY Tail;
|
|
PICA_WORK_ITEM pWorkItem;
|
|
BOOLEAN bCanceled;
|
|
|
|
/*
|
|
* Get IcaSpinLock to in order to cancel any previous timer
|
|
*/
|
|
IcaAcquireSpinLock( &IcaSpinLock, &oldIrql );
|
|
|
|
/*
|
|
* See if the timer is currently armed.
|
|
* The timer is armed if the workitem list is non-empty and
|
|
* the tail entry is not marked canceled.
|
|
*/
|
|
if ( !IsListEmpty( &pTimer->WorkItemListHead ) &&
|
|
(Tail = pTimer->WorkItemListHead.Blink) &&
|
|
(pWorkItem = CONTAINING_RECORD( Tail, ICA_WORK_ITEM, Links )) &&
|
|
!pWorkItem->fCanceled ) {
|
|
|
|
/*
|
|
* If the timer can be canceled, remove the workitem from the list
|
|
* and decrement the reference count for the timer.
|
|
*/
|
|
if ( KeCancelTimer( &pTimer->kTimer ) ) {
|
|
RemoveEntryList( &pWorkItem->Links );
|
|
pTimer->RefCount--;
|
|
ASSERT( pTimer->RefCount > 0 );
|
|
|
|
|
|
/*
|
|
* The timer was armed but could not be canceled.
|
|
* On a MP system, its possible for this to happen and the timer
|
|
* DPC can be executing on another CPU in parallel with this code.
|
|
*
|
|
* Mark the workitem as canceled,
|
|
* but leave it for the timer DPC routine to cleanup.
|
|
*/
|
|
} else {
|
|
pWorkItem->fCanceled = TRUE;
|
|
pWorkItem = NULL;
|
|
}
|
|
|
|
/*
|
|
* Indicate we (effectively) canceled the timer
|
|
*/
|
|
bCanceled = TRUE;
|
|
|
|
/*
|
|
* No timer is armed
|
|
*/
|
|
} else {
|
|
pWorkItem = NULL;
|
|
bCanceled = FALSE;
|
|
}
|
|
|
|
/*
|
|
* Release IcaSpinLock now
|
|
*/
|
|
IcaReleaseSpinLock( &IcaSpinLock, oldIrql );
|
|
|
|
if ( ppWorkItem ) {
|
|
*ppWorkItem = pWorkItem;
|
|
} else if ( pWorkItem ) {
|
|
ICA_FREE_POOL( pWorkItem );
|
|
}
|
|
|
|
return( bCanceled );
|
|
}
|
|
|
|
|
|
VOID
|
|
_IcaReferenceTimer(
|
|
PICA_TIMER pTimer
|
|
)
|
|
{
|
|
|
|
ASSERT( pTimer->RefCount >= 0 );
|
|
|
|
/*
|
|
* Increment the reference count
|
|
*/
|
|
if ( InterlockedIncrement( &pTimer->RefCount) <= 0 ) {
|
|
ASSERT( FALSE );
|
|
}
|
|
}
|
|
|
|
|
|
VOID
|
|
_IcaDereferenceTimer(
|
|
PICA_TIMER pTimer
|
|
)
|
|
{
|
|
|
|
ASSERT( pTimer->RefCount > 0 );
|
|
|
|
/*
|
|
* Decrement the reference count
|
|
* If it is 0 then free the timer now.
|
|
*/
|
|
if ( InterlockedDecrement( &pTimer->RefCount) == 0 ) {
|
|
ASSERT( IsListEmpty( &pTimer->WorkItemListHead ) );
|
|
ICA_FREE_POOL( pTimer );
|
|
}
|
|
}
|
|
|
|
|