497 lines
15 KiB
C
497 lines
15 KiB
C
|
/***************************************************************************\
|
||
|
* data.c
|
||
|
*
|
||
|
* Microsoft Confidential
|
||
|
* Copyright (c) 1991 Microsoft Corporation
|
||
|
*
|
||
|
* Module to query system data for WINMETER application. . . builds database
|
||
|
*
|
||
|
* History:
|
||
|
* Written by Hadi Partovi (t-hadip) summer 1991
|
||
|
*
|
||
|
* Re-written and adapted for NT by Fran Borda (v-franb) Nov.1991
|
||
|
* for Newman Consulting
|
||
|
* Took out all WIN-specific and bargraph code. Added 3 new
|
||
|
* linegraphs (Mem/Paging, Process/Threads/Handles, IO), and
|
||
|
* tailored info to that available under NT.
|
||
|
\***************************************************************************/
|
||
|
|
||
|
#include "winmeter.h"
|
||
|
|
||
|
SYSTEM_PERFORMANCE_INFORMATION PerfInfo,PreviousPerfInfo;
|
||
|
OBJECT_BASIC_INFORMATION ObjectInfo;
|
||
|
ULONG A_PageCount,C_PageCount,F_PageCount;
|
||
|
ULONG ProcessCount,ThreadCount,FileHandleCount;
|
||
|
|
||
|
HANDLE NullDeviceHandle = NULL;
|
||
|
|
||
|
// main global information structures
|
||
|
extern GLOBAL g;
|
||
|
|
||
|
// global variables for this module
|
||
|
static BOOL fFirstTime=TRUE; // set to FALSE after first query
|
||
|
|
||
|
int do_procs = FALSE;
|
||
|
int do_mem = FALSE;
|
||
|
int win_on_top = FALSE;
|
||
|
int do_io = FALSE;
|
||
|
|
||
|
// internal functions in this module
|
||
|
void FreeLGS(void);
|
||
|
// Frees memory for linegraphs
|
||
|
void InitializeLineGraph(void);
|
||
|
// Initializes g.plgList - the three default linegraphs
|
||
|
void QueryThreadData(void);
|
||
|
|
||
|
// functions for linegraph: return values to be plotted
|
||
|
// these simply return plottable values from the database
|
||
|
VALUE valGiveCPUUsage(void);
|
||
|
#if 1
|
||
|
VALUE valGiveProcesses(void);
|
||
|
VALUE valGiveThreads(void);
|
||
|
VALUE valGiveFiles(void);
|
||
|
VALUE valGiveMemoryA(void);
|
||
|
VALUE valGiveMemoryC(void);
|
||
|
VALUE valGiveMemoryF(void);
|
||
|
VALUE valGiveIORead(void);
|
||
|
VALUE valGiveIOWrite(void);
|
||
|
VALUE valGiveIOOther(void);
|
||
|
#endif
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* AllocLGValues()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Allocates memory for the stored values of a linegraph.
|
||
|
* If the linegraph already had memory allocated, the function
|
||
|
* reallocates the memory, clearing stored values
|
||
|
* Finally, sets various indices in the lg structure
|
||
|
\***************************************************************************/
|
||
|
void AllocLGValues(void)
|
||
|
{
|
||
|
PLGDATA plgd; // pointer to linegraph data
|
||
|
|
||
|
for (plgd=g.plg->plgd; plgd; plgd=plgd->plgdNext)
|
||
|
{
|
||
|
if (plgd->pValues)
|
||
|
// clear values
|
||
|
MemFree(plgd->pValues);
|
||
|
|
||
|
plgd->pValues = MemAlloc(sizeof(VALUE)*g.plg->nMaxValues);
|
||
|
}
|
||
|
|
||
|
// reset the line graph indices without redrawing it
|
||
|
ClearLineGraph();
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* DoUpdate()
|
||
|
*
|
||
|
* Entry: Three values: the delta value, the old queried value and the new value
|
||
|
* Exit: Updates the old value and the delta
|
||
|
* This is a macro for updating delta counters and cOld counters,
|
||
|
* but should eventually be changed so that both are part of one
|
||
|
* COUNTER structure, or something
|
||
|
\***************************************************************************/
|
||
|
#define DoUpdate(dwDelta, dwOld, dwNew) { \
|
||
|
dwDelta = dwNew - dwOld; \
|
||
|
dwOld = dwNew; }
|
||
|
// if dwNew < dwOldV, counter has rolled over, dwDelta should really
|
||
|
// evaluate to (dwNew + ULONG_MAX-dwOld).
|
||
|
// However, the two's complement method of subtraction ensures that
|
||
|
// -dwOld= ULONG_MAX-dwOld. Ergo, all is well, as long as the delta
|
||
|
// is stored in a DWORD
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* FreeDatabaseMemory()
|
||
|
*
|
||
|
* Entry: none
|
||
|
* Exit: frees all memory used for database (before exiting)
|
||
|
\***************************************************************************/
|
||
|
void FreeDatabaseMemory(void)
|
||
|
{
|
||
|
// Free up linegraph memory
|
||
|
FreeLGS();
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* FreeLGValues()
|
||
|
*
|
||
|
* Entry: Flag, set if should free entire PLGDATA structure
|
||
|
* Exit: Frees the LGDATA linked list of the current linegraph
|
||
|
* This is used if the History option is off (g.fRemember==FALSE),
|
||
|
* to free one linegraph's memory when switching to displaying another
|
||
|
\***************************************************************************/
|
||
|
void FreeLGValues(
|
||
|
BOOL fFreeAll) // flag specifying whether to free everything
|
||
|
{
|
||
|
PLGDATA plgd1, plgd2; // pointers into linked list
|
||
|
|
||
|
for (plgd1=g.plg->plgd; plgd1; plgd1=plgd2)
|
||
|
{
|
||
|
plgd2 = plgd1->plgdNext;
|
||
|
if (plgd1->pValues)
|
||
|
{
|
||
|
MemFree(plgd1->pValues);
|
||
|
plgd1->pValues = NULL; // set to null so it isn't "freed" again
|
||
|
}
|
||
|
if (fFreeAll)
|
||
|
{
|
||
|
MemFree(plgd1->lpszDescription);
|
||
|
MemFree(plgd1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* FreeLGS()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Frees up all the memory held by all the linegraphs in memory
|
||
|
\***************************************************************************/
|
||
|
void FreeLGS(void)
|
||
|
{
|
||
|
PLGRAPH plgNext; // temporary pointer to linegraph
|
||
|
|
||
|
// go through all linegraphs in list
|
||
|
for (g.plg = g.plgList; g.plg; g.plg=plgNext)
|
||
|
{
|
||
|
FreeLGValues(TRUE);
|
||
|
plgNext = g.plg->plgNext;
|
||
|
MemFree(g.plg->lpszTitle);
|
||
|
MemFree(g.plg);
|
||
|
}
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* InitializeDatabase()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Initializes thread and process database for querying
|
||
|
* Allocates memory asnd sets up the line graph linked list
|
||
|
\***************************************************************************/
|
||
|
void InitializeDatabase(void)
|
||
|
{
|
||
|
|
||
|
InitializeLineGraph();
|
||
|
QueryThreadData();
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* InitializeLineGraph()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Initializes the global linegraph linked list
|
||
|
* Creates a linked list pointed to by g.plgList, and sets g.plg
|
||
|
* to the starting current linegraph.
|
||
|
\***************************************************************************/
|
||
|
void InitializeLineGraph(void)
|
||
|
{
|
||
|
g.plgCPU = g.plgList = MemAlloc(sizeof(LGRAPH));
|
||
|
g.plgProcs = g.plgCPU->plgNext = MemAlloc(sizeof(LGRAPH));
|
||
|
g.plgMemory = g.plgProcs->plgNext = MemAlloc(sizeof(LGRAPH));
|
||
|
g.plgIO = g.plgMemory->plgNext = MemAlloc(sizeof(LGRAPH));
|
||
|
g.plgIO->plgNext = NULL;
|
||
|
|
||
|
g.plgCPU->nLines = 1;
|
||
|
g.plgMemory->nLines = g.plgIO->nLines = g.plgProcs->nLines = 3;
|
||
|
|
||
|
// INITIALIZE CPU USAGE LINEGRAPH
|
||
|
g.plg = g.plgCPU;
|
||
|
g.plg->nDisplayValues = NO_VALUES_YET;
|
||
|
|
||
|
// load title
|
||
|
MyLoadString(IDS_CPU_USAGE);
|
||
|
g.plg->lpszTitle = MemAlloc(1 + lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->lpszTitle, g.szBuf);
|
||
|
LoadLineGraphSettings();
|
||
|
|
||
|
// do line
|
||
|
g.plg->plgd = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_CPU);
|
||
|
g.plg->plgd->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->iColor = BLUE_INDEX;
|
||
|
g.plg->plgd->valNext = &valGiveCPUUsage;
|
||
|
g.plg->plgd->plgdNext = NULL;
|
||
|
|
||
|
AllocLGValues();
|
||
|
|
||
|
// INITIALIZE ProcThreadFile USAGE LINEGRAPH
|
||
|
g.plg = g.plgProcs;
|
||
|
g.plg->nDisplayValues = NO_VALUES_YET;
|
||
|
|
||
|
// load title
|
||
|
MyLoadString(IDS_PROC_INFO);
|
||
|
g.plg->lpszTitle = MemAlloc(1 + lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->lpszTitle, g.szBuf);
|
||
|
LoadLineGraphSettings();
|
||
|
|
||
|
// first line
|
||
|
g.plg->plgd = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_PROCESSES);
|
||
|
g.plg->plgd->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->iColor = RED_INDEX;
|
||
|
g.plg->plgd->valNext = &valGiveProcesses;
|
||
|
|
||
|
// second line
|
||
|
g.plg->plgd->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_THREADS);
|
||
|
g.plg->plgd->plgdNext->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->iColor = GREEN_INDEX;
|
||
|
g.plg->plgd->plgdNext->valNext = &valGiveThreads;
|
||
|
|
||
|
// third line
|
||
|
g.plg->plgd->plgdNext->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_FILES);
|
||
|
g.plg->plgd->plgdNext->plgdNext->lpszDescription =
|
||
|
MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->plgdNext->iColor = BLUE_INDEX;
|
||
|
g.plg->plgd->plgdNext->plgdNext->valNext = &valGiveFiles;
|
||
|
g.plg->plgd->plgdNext->plgdNext->plgdNext = NULL;
|
||
|
|
||
|
AllocLGValues();
|
||
|
|
||
|
|
||
|
// INITIALIZE MEMORY USAGE LINEGRAPH
|
||
|
g.plg = g.plgMemory;
|
||
|
g.plg->nDisplayValues = NO_VALUES_YET;
|
||
|
|
||
|
// load title
|
||
|
MyLoadString(IDS_MEMORY_USAGE);
|
||
|
g.plg->lpszTitle = MemAlloc(1 + lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->lpszTitle, g.szBuf);
|
||
|
LoadLineGraphSettings();
|
||
|
|
||
|
// first line
|
||
|
g.plg->plgd = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_AVAILPAGES);
|
||
|
g.plg->plgd->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->iColor = RED_INDEX;
|
||
|
g.plg->plgd->valNext = &valGiveMemoryA;
|
||
|
|
||
|
// second line
|
||
|
g.plg->plgd->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_COMMITPAGES);
|
||
|
g.plg->plgd->plgdNext->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->iColor = GREEN_INDEX;
|
||
|
g.plg->plgd->plgdNext->valNext = &valGiveMemoryC;
|
||
|
|
||
|
// third line
|
||
|
g.plg->plgd->plgdNext->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_PAGEFAULTS);
|
||
|
g.plg->plgd->plgdNext->plgdNext->lpszDescription =
|
||
|
MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->plgdNext->iColor = BLUE_INDEX;
|
||
|
g.plg->plgd->plgdNext->plgdNext->valNext = &valGiveMemoryF;
|
||
|
g.plg->plgd->plgdNext->plgdNext->plgdNext = NULL;
|
||
|
|
||
|
AllocLGValues();
|
||
|
|
||
|
// INITIALIZE IO USAGE LINEGRAPH
|
||
|
g.plg = g.plgIO;
|
||
|
g.plg->nDisplayValues = NO_VALUES_YET;
|
||
|
|
||
|
// load title
|
||
|
MyLoadString(IDS_IO_USAGE);
|
||
|
g.plg->lpszTitle = MemAlloc(1 + lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->lpszTitle, g.szBuf);
|
||
|
LoadLineGraphSettings();
|
||
|
|
||
|
// first line
|
||
|
g.plg->plgd = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_IO_READS);
|
||
|
g.plg->plgd->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->iColor = RED_INDEX;
|
||
|
g.plg->plgd->valNext = &valGiveIORead;
|
||
|
|
||
|
// second line
|
||
|
g.plg->plgd->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_IO_WRITES);
|
||
|
g.plg->plgd->plgdNext->lpszDescription = MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->iColor = GREEN_INDEX;
|
||
|
g.plg->plgd->plgdNext->valNext = &valGiveIOWrite;
|
||
|
|
||
|
// third line
|
||
|
g.plg->plgd->plgdNext->plgdNext = MemAlloc(sizeof(LGDATA));
|
||
|
MyLoadString(IDS_IO_OTHER);
|
||
|
g.plg->plgd->plgdNext->plgdNext->lpszDescription =
|
||
|
MemAlloc(1+lstrlen(g.szBuf));
|
||
|
lstrcpy(g.plg->plgd->plgdNext->plgdNext->lpszDescription, g.szBuf);
|
||
|
g.plg->plgd->plgdNext->plgdNext->iColor = BLUE_INDEX;
|
||
|
g.plg->plgd->plgdNext->plgdNext->valNext = &valGiveIOOther;
|
||
|
g.plg->plgd->plgdNext->plgdNext->plgdNext = NULL;
|
||
|
|
||
|
AllocLGValues();
|
||
|
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* QueryGlobalData()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Queries globabl data
|
||
|
\***************************************************************************/
|
||
|
void QueryGlobalData(void)
|
||
|
{
|
||
|
// Query System for global info
|
||
|
NtQuerySystemInformation(SystemPerformanceInformation,
|
||
|
&PerfInfo,
|
||
|
sizeof(PerfInfo),
|
||
|
NULL
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* QueryThreadData()
|
||
|
*
|
||
|
* Entry: None
|
||
|
* Exit: Queries thread data
|
||
|
*
|
||
|
\***************************************************************************/
|
||
|
void QueryThreadData(void)
|
||
|
{
|
||
|
|
||
|
// in case this was the first query, (from InitializeDatabase()),
|
||
|
// clear fFirstTime flag to signify that initialization query is over
|
||
|
|
||
|
fFirstTime = FALSE;
|
||
|
|
||
|
NtQueryObject(NtCurrentProcess(),ObjectBasicInformation,
|
||
|
&ObjectInfo,sizeof(ObjectInfo),NULL);
|
||
|
ProcessCount = ObjectInfo.NumberOfObjects;
|
||
|
|
||
|
NtQueryObject(NtCurrentThread(),ObjectBasicInformation,
|
||
|
&ObjectInfo,sizeof(ObjectInfo),NULL);
|
||
|
ThreadCount = ObjectInfo.NumberOfObjects;
|
||
|
|
||
|
NtQueryObject(NullDeviceHandle,ObjectBasicInformation,
|
||
|
&ObjectInfo,sizeof(ObjectInfo),NULL);
|
||
|
#if 0
|
||
|
// Currently, this always matches the ThreadCount, which seems odd,
|
||
|
// so we'll display the TotalHandleCount for now.
|
||
|
FileHandleCount = ObjectInfo.NumberOfObjects;
|
||
|
#else
|
||
|
FileHandleCount = ObjectInfo.TotalHandleCount;
|
||
|
#endif
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************\
|
||
|
* valGiveCPUUsage()
|
||
|
*
|
||
|
* Entry: none
|
||
|
* Exit: Gives % value of CPU Tics that are not idle - linegraph fxn
|
||
|
\***************************************************************************/
|
||
|
VALUE valGiveCPUUsage(void)
|
||
|
{
|
||
|
LARGE_INTEGER EndTime,
|
||
|
BeginTime,
|
||
|
ElapsedTime,
|
||
|
PercentIdle,
|
||
|
DelayTimeTicks;
|
||
|
|
||
|
DelayTimeTicks = RtlExtendedIntegerMultiply(
|
||
|
RtlConvertUlongToLargeInteger(g.nTimerInterval),
|
||
|
(ULONG)10000);
|
||
|
|
||
|
EndTime = *(PLARGE_INTEGER)&PerfInfo.IdleProcessTime;
|
||
|
BeginTime = *(PLARGE_INTEGER)&PreviousPerfInfo.IdleProcessTime;
|
||
|
|
||
|
ElapsedTime = RtlLargeIntegerSubtract(EndTime,BeginTime);
|
||
|
|
||
|
PercentIdle = RtlLargeIntegerDivide(
|
||
|
// Multiply the elapsed time by 100 to retain some
|
||
|
// precision.
|
||
|
RtlExtendedIntegerMultiply(ElapsedTime,(ULONG)100),
|
||
|
// Then divide by the delay and ignore any remainder.
|
||
|
DelayTimeTicks, NULL);
|
||
|
if (PercentIdle.LowPart > 100)
|
||
|
PercentIdle.LowPart = 0;
|
||
|
|
||
|
return (100 - PercentIdle.LowPart) ;
|
||
|
}
|
||
|
|
||
|
VALUE valGiveProcesses(void)
|
||
|
{
|
||
|
return (ProcessCount);
|
||
|
}
|
||
|
|
||
|
VALUE valGiveThreads(void)
|
||
|
{
|
||
|
return(ThreadCount);
|
||
|
|
||
|
}
|
||
|
|
||
|
VALUE valGiveFiles(void)
|
||
|
{
|
||
|
return(FileHandleCount);
|
||
|
}
|
||
|
|
||
|
VALUE valGiveMemoryA(void)
|
||
|
{
|
||
|
return((PerfInfo.AvailablePages/10) > M_DEFAULT_DVAL_AXISHEIGHT ?
|
||
|
M_DEFAULT_DVAL_AXISHEIGHT : (PerfInfo.AvailablePages/10));
|
||
|
}
|
||
|
|
||
|
VALUE valGiveMemoryC(void)
|
||
|
{
|
||
|
return((PerfInfo.CommittedPages/10) > M_DEFAULT_DVAL_AXISHEIGHT ?
|
||
|
M_DEFAULT_DVAL_AXISHEIGHT : (PerfInfo.CommittedPages/10));
|
||
|
}
|
||
|
|
||
|
VALUE valGiveMemoryF(void)
|
||
|
{
|
||
|
return((PerfInfo.PageFaultCount - PreviousPerfInfo.PageFaultCount)
|
||
|
> M_DEFAULT_DVAL_AXISHEIGHT ? M_DEFAULT_DVAL_AXISHEIGHT :
|
||
|
(PerfInfo.PageFaultCount - PreviousPerfInfo.PageFaultCount));
|
||
|
}
|
||
|
|
||
|
VALUE valGiveIORead(void)
|
||
|
{
|
||
|
return((PerfInfo.IoReadOperationCount - PreviousPerfInfo.IoReadOperationCount)
|
||
|
> I_DEFAULT_DVAL_AXISHEIGHT ? I_DEFAULT_DVAL_AXISHEIGHT :
|
||
|
(PerfInfo.IoReadOperationCount - PreviousPerfInfo.IoReadOperationCount));
|
||
|
}
|
||
|
|
||
|
VALUE valGiveIOWrite(void)
|
||
|
{
|
||
|
return((PerfInfo.IoWriteOperationCount - PreviousPerfInfo.IoWriteOperationCount)
|
||
|
> I_DEFAULT_DVAL_AXISHEIGHT ? I_DEFAULT_DVAL_AXISHEIGHT :
|
||
|
(PerfInfo.IoWriteOperationCount - PreviousPerfInfo.IoWriteOperationCount));
|
||
|
}
|
||
|
|
||
|
VALUE valGiveIOOther(void)
|
||
|
{
|
||
|
return((PerfInfo.IoOtherOperationCount - PreviousPerfInfo.IoOtherOperationCount)
|
||
|
> I_DEFAULT_DVAL_AXISHEIGHT ? I_DEFAULT_DVAL_AXISHEIGHT :
|
||
|
(PerfInfo.IoOtherOperationCount - PreviousPerfInfo.IoOtherOperationCount));
|
||
|
}
|