mirror of
https://github.com/reactos/reactos.git
synced 2024-12-26 17:14:41 +00:00
add ctm by Aleksey Bragin
svn path=/trunk/; revision=4488
This commit is contained in:
parent
82389bc005
commit
2941c583a4
5 changed files with 872 additions and 0 deletions
|
@ -21,6 +21,7 @@ APPS = calc \
|
||||||
mc \
|
mc \
|
||||||
notevil \
|
notevil \
|
||||||
sysutils \
|
sysutils \
|
||||||
|
sysutils$(SEP)ctm \
|
||||||
sysutils$(SEP)tlist \
|
sysutils$(SEP)tlist \
|
||||||
net$(SEP)finger \
|
net$(SEP)finger \
|
||||||
net$(SEP)ncftp \
|
net$(SEP)ncftp \
|
||||||
|
|
23
rosapps/sysutils/ctm/Makefile
Normal file
23
rosapps/sysutils/ctm/Makefile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#
|
||||||
|
# ReactOS Console TaskManager
|
||||||
|
#
|
||||||
|
# Makefile
|
||||||
|
#
|
||||||
|
|
||||||
|
PATH_TO_TOP = ../../../reactos
|
||||||
|
|
||||||
|
TARGET_TYPE = program
|
||||||
|
|
||||||
|
TARGET_APPTYPE = console
|
||||||
|
|
||||||
|
TARGET_NAME = ctm
|
||||||
|
|
||||||
|
TARGET_SDKLIBS = ntdll.a
|
||||||
|
|
||||||
|
TARGET_OBJECTS = $(TARGET_NAME).o
|
||||||
|
|
||||||
|
include $(PATH_TO_TOP)/rules.mak
|
||||||
|
|
||||||
|
include $(TOOLS_PATH)/helper.mk
|
||||||
|
|
||||||
|
# EOF
|
567
rosapps/sysutils/ctm/ctm.c
Normal file
567
rosapps/sysutils/ctm/ctm.c
Normal file
|
@ -0,0 +1,567 @@
|
||||||
|
/* Console Task Manager
|
||||||
|
|
||||||
|
ctm.c - main program file
|
||||||
|
|
||||||
|
Written by: Aleksey Bragin (aleksey@studiocerebral.com)
|
||||||
|
|
||||||
|
Most of the code dealing with getting system parameters is taken from
|
||||||
|
ReactOS Task Manager written by Brian Palmet (brianp@reactos.org)
|
||||||
|
|
||||||
|
History:
|
||||||
|
20 March 2003 - v0.03, works good under ReactOS, and allows process killing
|
||||||
|
18 March 2003 - Initial version 0.01, doesn't work under RectOS
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
|
||||||
|
|
||||||
|
|
||||||
|
//#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows //headers
|
||||||
|
#include <windows.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <malloc.h>
|
||||||
|
#include <memory.h>
|
||||||
|
#include <tchar.h>
|
||||||
|
#include <process.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "ctm.h"
|
||||||
|
|
||||||
|
#define MAX_PROC 17
|
||||||
|
//#define TIMES
|
||||||
|
|
||||||
|
HANDLE hStdin;
|
||||||
|
HANDLE hStdout;
|
||||||
|
|
||||||
|
DWORD inConMode;
|
||||||
|
DWORD outConMode;
|
||||||
|
|
||||||
|
//PROCNTQSI NtQuerySystemInformation= NULL;
|
||||||
|
|
||||||
|
const int ProcPerScreen = 17; // 17 processess are displayed on one page
|
||||||
|
ULONG ProcessCountOld = 0;
|
||||||
|
ULONG ProcessCount = 0;
|
||||||
|
|
||||||
|
double dbIdleTime;
|
||||||
|
double dbKernelTime;
|
||||||
|
double dbSystemTime;
|
||||||
|
LARGE_INTEGER liOldIdleTime = {{0,0}};
|
||||||
|
double OldKernelTime = 0;
|
||||||
|
LARGE_INTEGER liOldSystemTime = {{0,0}};
|
||||||
|
|
||||||
|
PPERFDATA pPerfDataOld = NULL; // Older perf data (saved to establish delta values)
|
||||||
|
PPERFDATA pPerfData = NULL; // Most recent copy of perf data
|
||||||
|
|
||||||
|
int selection=0;
|
||||||
|
int scrolled=0; // offset from which process start showing
|
||||||
|
|
||||||
|
#define NEW_CONSOLE
|
||||||
|
|
||||||
|
// Prototypes
|
||||||
|
unsigned int GetKeyPressed();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void GetInputOutputHandles()
|
||||||
|
{
|
||||||
|
#ifdef NEW_CONSOLE
|
||||||
|
HANDLE console = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
0, CONSOLE_TEXTMODE_BUFFER, 0);
|
||||||
|
|
||||||
|
if (SetConsoleActiveScreenBuffer(console) == FALSE)
|
||||||
|
{
|
||||||
|
hStdin = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hStdin = GetStdHandle(STD_INPUT_HANDLE);//console;
|
||||||
|
hStdout = console;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
hStdin = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestoreConsole()
|
||||||
|
{
|
||||||
|
SetConsoleMode(hStdin, inConMode);
|
||||||
|
SetConsoleMode(hStdout, outConMode);
|
||||||
|
|
||||||
|
#ifdef NEW_CONSOLE
|
||||||
|
SetConsoleActiveScreenBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayScreen()
|
||||||
|
{
|
||||||
|
COORD pos;
|
||||||
|
char lpStr[80];
|
||||||
|
int idx;
|
||||||
|
DWORD numChars;
|
||||||
|
int lines;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
pos.X = 2; pos.Y = 2;
|
||||||
|
strcpy(lpStr, "Console TaskManager v0.09 by Aleksey Bragin <aleksey@studiocerebral.com>");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
pos.X = 2; pos.Y = 3;
|
||||||
|
strcpy(lpStr, "+-------------------------------+-------+-----+-----------+-------------+");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
pos.X = 2; pos.Y = 4;
|
||||||
|
strcpy(lpStr, "| Image name | PID | CPU | Mem Usage | Page Faults |");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
pos.X = 2; pos.Y = 5;
|
||||||
|
strcpy(lpStr, "+-------------------------------+-------+-----+-----------+-------------+");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
pos.X = 2; pos.Y = 23;
|
||||||
|
strcpy(lpStr, "+-------------------------------+-------+-----+-----------+-------------+");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
// Menu
|
||||||
|
pos.X = 2; pos.Y = 24;
|
||||||
|
strcpy(lpStr, "Press: q - quit, k - kill process ");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &numChars);
|
||||||
|
|
||||||
|
// Processess
|
||||||
|
lines = ProcessCount;
|
||||||
|
if (lines > MAX_PROC)
|
||||||
|
lines = MAX_PROC;
|
||||||
|
for (idx=0; idx<lines; idx++)
|
||||||
|
{
|
||||||
|
int len;
|
||||||
|
char imgName[MAX_PATH];
|
||||||
|
char lpPid[8];
|
||||||
|
char lpCpu[6];
|
||||||
|
char lpMemUsg[12];
|
||||||
|
char lpPageFaults[15];
|
||||||
|
WORD wColor;
|
||||||
|
|
||||||
|
// data
|
||||||
|
// image name
|
||||||
|
pos.X = 3; pos.Y = 6+idx;
|
||||||
|
memset(imgName, 0, MAX_PATH);
|
||||||
|
WideCharToMultiByte(CP_ACP, 0, pPerfData[scrolled+idx].ImageName, -1,
|
||||||
|
imgName, MAX_PATH, NULL, NULL);
|
||||||
|
len = strlen(imgName);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, " ", 30, pos, &numChars);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, imgName, (len > 30) ? 30 : len, pos, &numChars);
|
||||||
|
|
||||||
|
// PID
|
||||||
|
pos.X = 35; pos.Y = 6+idx;
|
||||||
|
sprintf(lpPid, "%6ld", pPerfData[scrolled+idx].ProcessId);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpPid, strlen(lpPid), pos, &numChars);
|
||||||
|
|
||||||
|
// CPU
|
||||||
|
pos.X = 43; pos.Y = 6+idx;
|
||||||
|
sprintf(lpCpu, "%3d%%", pPerfData[scrolled+idx].CPUUsage);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpCpu, strlen(lpCpu), pos, &numChars);
|
||||||
|
|
||||||
|
// Mem usage
|
||||||
|
pos.X = 49; pos.Y = 6+idx;
|
||||||
|
sprintf(lpMemUsg, "%6ld", pPerfData[scrolled+idx].WorkingSetSizeBytes / 1024);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpMemUsg, strlen(lpMemUsg), pos, &numChars);
|
||||||
|
|
||||||
|
// Page Fault
|
||||||
|
pos.X = 61; pos.Y = 6+idx;
|
||||||
|
sprintf(lpPageFaults, "%12ld", pPerfData[scrolled+idx].PageFaultCount);
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpPageFaults, strlen(lpPageFaults), pos, &numChars);
|
||||||
|
|
||||||
|
// columns
|
||||||
|
pos.X = 2; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
pos.X = 34; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
pos.X = 42; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
pos.X = 48; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
pos.X = 60; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
pos.X = 74; pos.Y = 6+idx;
|
||||||
|
WriteConsoleOutputCharacter(hStdout, "|", 1, pos, &numChars);
|
||||||
|
|
||||||
|
|
||||||
|
// Attributes now...
|
||||||
|
pos.X = 3; pos.Y = 6+idx;
|
||||||
|
if (selection == idx)
|
||||||
|
{
|
||||||
|
wColor = BACKGROUND_GREEN |
|
||||||
|
FOREGROUND_RED |
|
||||||
|
FOREGROUND_GREEN |
|
||||||
|
FOREGROUND_BLUE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wColor = BACKGROUND_BLUE |
|
||||||
|
FOREGROUND_RED |
|
||||||
|
FOREGROUND_GREEN |
|
||||||
|
FOREGROUND_BLUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
FillConsoleOutputAttribute(
|
||||||
|
hStdout, // screen buffer handle
|
||||||
|
wColor, // color to fill with
|
||||||
|
31, // number of cells to fill
|
||||||
|
pos, // first cell to write to
|
||||||
|
&numChars); // actual number written
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns TRUE if exiting
|
||||||
|
int ProcessKeys(int numEvents)
|
||||||
|
{
|
||||||
|
unsigned char key = GetKeyPressed(numEvents);
|
||||||
|
if (key == VK_Q)
|
||||||
|
return TRUE;
|
||||||
|
else if (key == VK_K)
|
||||||
|
{
|
||||||
|
// user wants to kill some process, get his acknowledgement
|
||||||
|
DWORD pId;
|
||||||
|
COORD pos;
|
||||||
|
char lpStr[100];
|
||||||
|
|
||||||
|
pos.X = 2; pos.Y = 24;
|
||||||
|
strcpy(lpStr, "Are you sure you want to kill this process? (y/n)");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &pId);
|
||||||
|
|
||||||
|
do {
|
||||||
|
GetNumberOfConsoleInputEvents(hStdin, &pId);
|
||||||
|
key = GetKeyPressed(pId);
|
||||||
|
} while (key == 0);
|
||||||
|
|
||||||
|
if (key == VK_Y)
|
||||||
|
{
|
||||||
|
HANDLE hProcess;
|
||||||
|
pId = pPerfData[selection].ProcessId;
|
||||||
|
hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pId);
|
||||||
|
|
||||||
|
if (hProcess)
|
||||||
|
{
|
||||||
|
if (!TerminateProcess(hProcess, 0))
|
||||||
|
{
|
||||||
|
strcpy(lpStr, "Unable to terminate this proces... ");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &pId);
|
||||||
|
Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
strcpy(lpStr, "Unable to terminate this proces... ");
|
||||||
|
WriteConsoleOutputCharacter(hStdout, lpStr, strlen(lpStr), pos, &pId);
|
||||||
|
Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key == VK_UP)
|
||||||
|
{
|
||||||
|
if (selection > 0)
|
||||||
|
selection--;
|
||||||
|
else if ((selection == 0) && (scrolled > 0))
|
||||||
|
scrolled--;
|
||||||
|
}
|
||||||
|
else if (key == VK_DOWN)
|
||||||
|
{
|
||||||
|
if ((selection < MAX_PROC-1) && (selection < ProcessCount-1))
|
||||||
|
selection++;
|
||||||
|
else if ((selection == MAX_PROC-1) && (selection+scrolled < ProcessCount-1))
|
||||||
|
scrolled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PerfInit()
|
||||||
|
{
|
||||||
|
// NtQuerySystemInformation = //(PROCNTQSI)GetProcAddress(GetModuleHandle(_T("ntdll.dll")), //"NtQuerySystemInformation");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PerfDataRefresh()
|
||||||
|
{
|
||||||
|
LONG status;
|
||||||
|
ULONG ulSize;
|
||||||
|
LPBYTE pBuffer;
|
||||||
|
ULONG BufferSize;
|
||||||
|
ULONG Idx, Idx2;
|
||||||
|
HANDLE hProcess;
|
||||||
|
HANDLE hProcessToken;
|
||||||
|
PSYSTEM_PROCESS_INFORMATION pSPI;
|
||||||
|
PPERFDATA pPDOld;
|
||||||
|
TCHAR szTemp[MAX_PATH];
|
||||||
|
DWORD dwSize;
|
||||||
|
double CurrentKernelTime;
|
||||||
|
PSYSTEM_PROCESSORTIME_INFO SysProcessorTimeInfo;
|
||||||
|
SYSTEM_PERFORMANCE_INFORMATION SysPerfInfo;
|
||||||
|
SYSTEM_TIME_INFORMATION SysTimeInfo;
|
||||||
|
|
||||||
|
#ifdef TIMES
|
||||||
|
// Get new system time
|
||||||
|
status = NtQuerySystemInformation(SystemTimeInformation, &SysTimeInfo, sizeof(SysTimeInfo), 0);
|
||||||
|
if (status != NO_ERROR)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get new CPU's idle time
|
||||||
|
status = NtQuerySystemInformation(SystemPerformanceInformation, &SysPerfInfo, sizeof(SysPerfInfo), NULL);
|
||||||
|
if (status != NO_ERROR)
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
// Get processor information
|
||||||
|
SysProcessorTimeInfo = (PSYSTEM_PROCESSORTIME_INFO)malloc(sizeof(SYSTEM_PROCESSORTIME_INFO) * 1/*SystemBasicInfo.bKeNumberProcessors*/);
|
||||||
|
status = NtQuerySystemInformation(SystemProcessorTimeInformation, SysProcessorTimeInfo, sizeof(SYSTEM_PROCESSORTIME_INFO) * 1/*SystemBasicInfo.bKeNumberProcessors*/, &ulSize);
|
||||||
|
|
||||||
|
|
||||||
|
// Get process information
|
||||||
|
// We don't know how much data there is so just keep
|
||||||
|
// increasing the buffer size until the call succeeds
|
||||||
|
BufferSize = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
BufferSize += 0x10000;
|
||||||
|
//pBuffer = new BYTE[BufferSize];
|
||||||
|
pBuffer = (LPBYTE)malloc(BufferSize);
|
||||||
|
|
||||||
|
status = NtQuerySystemInformation(SystemProcessInformation, pBuffer, BufferSize, &ulSize);
|
||||||
|
|
||||||
|
if (status == 0xC0000004 /*STATUS_INFO_LENGTH_MISMATCH*/) {
|
||||||
|
//delete[] pBuffer;
|
||||||
|
free(pBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (status == 0xC0000004 /*STATUS_INFO_LENGTH_MISMATCH*/);
|
||||||
|
|
||||||
|
#ifdef TIMES
|
||||||
|
for (CurrentKernelTime=0, Idx=0; Idx<1/*SystemBasicInfo.bKeNumberProcessors*/; Idx++) {
|
||||||
|
CurrentKernelTime += Li2Double(SysProcessorTimeInfo[Idx].KernelTime);
|
||||||
|
CurrentKernelTime += Li2Double(SysProcessorTimeInfo[Idx].DpcTime);
|
||||||
|
CurrentKernelTime += Li2Double(SysProcessorTimeInfo[Idx].InterruptTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a first call - skip idle time calcs
|
||||||
|
if (liOldIdleTime.QuadPart != 0) {
|
||||||
|
// CurrentValue = NewValue - OldValue
|
||||||
|
dbIdleTime = Li2Double(SysPerfInfo.liIdleTime) - Li2Double(liOldIdleTime);
|
||||||
|
dbKernelTime = CurrentKernelTime - OldKernelTime;
|
||||||
|
dbSystemTime = Li2Double(SysTimeInfo.liKeSystemTime) - Li2Double(liOldSystemTime);
|
||||||
|
|
||||||
|
// CurrentCpuIdle = IdleTime / SystemTime
|
||||||
|
dbIdleTime = dbIdleTime / dbSystemTime;
|
||||||
|
dbKernelTime = dbKernelTime / dbSystemTime;
|
||||||
|
|
||||||
|
// CurrentCpuUsage% = 100 - (CurrentCpuIdle * 100) / NumberOfProcessors
|
||||||
|
dbIdleTime = 100.0 - dbIdleTime * 100.0; /* / (double)SystemBasicInfo.bKeNumberProcessors;// + 0.5; */
|
||||||
|
dbKernelTime = 100.0 - dbKernelTime * 100.0; /* / (double)SystemBasicInfo.bKeNumberProcessors;// + 0.5; */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store new CPU's idle and system time
|
||||||
|
liOldIdleTime = SysPerfInfo.liIdleTime;
|
||||||
|
liOldSystemTime = SysTimeInfo.liKeSystemTime;
|
||||||
|
OldKernelTime = CurrentKernelTime;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Determine the process count
|
||||||
|
// We loop through the data we got from NtQuerySystemInformation
|
||||||
|
// and count how many structures there are (until RelativeOffset is 0)
|
||||||
|
ProcessCountOld = ProcessCount;
|
||||||
|
ProcessCount = 0;
|
||||||
|
pSPI = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
|
||||||
|
while (pSPI) {
|
||||||
|
ProcessCount++;
|
||||||
|
if (pSPI->RelativeOffset == 0)
|
||||||
|
break;
|
||||||
|
pSPI = (PSYSTEM_PROCESS_INFORMATION)((LPBYTE)pSPI + pSPI->RelativeOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now alloc a new PERFDATA array and fill in the data
|
||||||
|
if (pPerfDataOld) {
|
||||||
|
//delete[] pPerfDataOld;
|
||||||
|
free(pPerfDataOld);
|
||||||
|
}
|
||||||
|
pPerfDataOld = pPerfData;
|
||||||
|
//pPerfData = new PERFDATA[ProcessCount];
|
||||||
|
pPerfData = (PPERFDATA)malloc(sizeof(PERFDATA) * ProcessCount);
|
||||||
|
pSPI = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
|
||||||
|
for (Idx=0; Idx<ProcessCount; Idx++) {
|
||||||
|
// Get the old perf data for this process (if any)
|
||||||
|
// so that we can establish delta values
|
||||||
|
pPDOld = NULL;
|
||||||
|
for (Idx2=0; Idx2<ProcessCountOld; Idx2++) {
|
||||||
|
if (pPerfDataOld[Idx2].ProcessId == pSPI->ProcessId) {
|
||||||
|
pPDOld = &pPerfDataOld[Idx2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear out process perf data structure
|
||||||
|
memset(&pPerfData[Idx], 0, sizeof(PERFDATA));
|
||||||
|
|
||||||
|
if (pSPI->Name.Buffer)
|
||||||
|
wcsncpy(pPerfData[Idx].ImageName, pSPI->Name.Buffer, pSPI->Name.MaximumLength);
|
||||||
|
else
|
||||||
|
wcscpy(pPerfData[Idx].ImageName, L"System Idle Process");
|
||||||
|
|
||||||
|
pPerfData[Idx].ProcessId = pSPI->ProcessId;
|
||||||
|
|
||||||
|
if (pPDOld) {
|
||||||
|
#ifdef TIMES
|
||||||
|
double CurTime = Li2Double(pSPI->KernelTime) + Li2Double(pSPI->UserTime);
|
||||||
|
double OldTime = Li2Double(pPDOld->KernelTime) + Li2Double(pPDOld->UserTime);
|
||||||
|
double CpuTime = (CurTime - OldTime) / dbSystemTime;
|
||||||
|
CpuTime = CpuTime * 100.0; /* / (double)SystemBasicInfo.bKeNumberProcessors;// + 0.5;*/
|
||||||
|
|
||||||
|
pPerfData[Idx].CPUUsage = (ULONG)CpuTime;
|
||||||
|
#else
|
||||||
|
pPerfData[Idx].CPUUsage = 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
pPerfData[Idx].CPUTime.QuadPart = pSPI->UserTime.QuadPart + pSPI->KernelTime.QuadPart;
|
||||||
|
pPerfData[Idx].WorkingSetSizeBytes = pSPI->TotalWorkingSetSizeBytes;
|
||||||
|
pPerfData[Idx].PeakWorkingSetSizeBytes = pSPI->PeakWorkingSetSizeBytes;
|
||||||
|
if (pPDOld)
|
||||||
|
pPerfData[Idx].WorkingSetSizeDelta = labs((LONG)pSPI->TotalWorkingSetSizeBytes - (LONG)pPDOld->WorkingSetSizeBytes);
|
||||||
|
else
|
||||||
|
pPerfData[Idx].WorkingSetSizeDelta = 0;
|
||||||
|
pPerfData[Idx].PageFaultCount = pSPI->PageFaultCount;
|
||||||
|
if (pPDOld)
|
||||||
|
pPerfData[Idx].PageFaultCountDelta = labs((LONG)pSPI->PageFaultCount - (LONG)pPDOld->PageFaultCount);
|
||||||
|
else
|
||||||
|
pPerfData[Idx].PageFaultCountDelta = 0;
|
||||||
|
pPerfData[Idx].VirtualMemorySizeBytes = pSPI->TotalVirtualSizeBytes;
|
||||||
|
pPerfData[Idx].PagedPoolUsagePages = pSPI->TotalPagedPoolUsagePages;
|
||||||
|
pPerfData[Idx].NonPagedPoolUsagePages = pSPI->TotalNonPagedPoolUsagePages;
|
||||||
|
pPerfData[Idx].BasePriority = pSPI->BasePriority;
|
||||||
|
pPerfData[Idx].HandleCount = pSPI->HandleCount;
|
||||||
|
pPerfData[Idx].ThreadCount = pSPI->ThreadCount;
|
||||||
|
pPerfData[Idx].SessionId = pSPI->SessionId;
|
||||||
|
|
||||||
|
#ifdef EXTRA_INFO
|
||||||
|
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pSPI->ProcessId);
|
||||||
|
if (hProcess) {
|
||||||
|
if (OpenProcessToken(hProcess, TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_IMPERSONATE, &hProcessToken)) {
|
||||||
|
ImpersonateLoggedOnUser(hProcessToken);
|
||||||
|
memset(szTemp, 0, sizeof(TCHAR[MAX_PATH]));
|
||||||
|
dwSize = MAX_PATH;
|
||||||
|
GetUserName(szTemp, &dwSize);
|
||||||
|
#ifndef UNICODE
|
||||||
|
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, szTemp, -1, pPerfData[Idx].UserName, MAX_PATH);
|
||||||
|
/*
|
||||||
|
int MultiByteToWideChar(
|
||||||
|
UINT CodePage, // code page
|
||||||
|
DWORD dwFlags, // character-type options
|
||||||
|
LPCSTR lpMultiByteStr, // string to map
|
||||||
|
int cbMultiByte, // number of bytes in string
|
||||||
|
LPWSTR lpWideCharStr, // wide-character buffer
|
||||||
|
int cchWideChar // size of buffer
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
#endif
|
||||||
|
RevertToSelf();
|
||||||
|
CloseHandle(hProcessToken);
|
||||||
|
}
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef TIMES
|
||||||
|
pPerfData[Idx].UserTime.QuadPart = pSPI->UserTime.QuadPart;
|
||||||
|
pPerfData[Idx].KernelTime.QuadPart = pSPI->KernelTime.QuadPart;
|
||||||
|
#endif
|
||||||
|
pSPI = (PSYSTEM_PROCESS_INFORMATION)((LPBYTE)pSPI + pSPI->RelativeOffset);
|
||||||
|
}
|
||||||
|
//delete[] pBuffer;
|
||||||
|
free(pBuffer);
|
||||||
|
|
||||||
|
free(SysProcessorTimeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code partly taken from slw32tty.c from mc/slang
|
||||||
|
unsigned int GetKeyPressed(int events)
|
||||||
|
{
|
||||||
|
long key, bytesRead;
|
||||||
|
INPUT_RECORD record;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
|
||||||
|
for (i=0; i<events; i++)
|
||||||
|
{
|
||||||
|
if (!ReadConsoleInput(hStdin, &record, 1, &bytesRead)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown)
|
||||||
|
return record.Event.KeyEvent.wVirtualKeyCode;//.uChar.AsciiChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(int *argc, char **argv)
|
||||||
|
{
|
||||||
|
GetInputOutputHandles();
|
||||||
|
|
||||||
|
if (hStdin == INVALID_HANDLE_VALUE || hStdout == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
printf("tmtm: can't use console.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetConsoleMode(hStdin, &inConMode) == 0)
|
||||||
|
{
|
||||||
|
printf("tmtm: can't GetConsoleMode() for input console.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GetConsoleMode(hStdout, &outConMode) == 0)
|
||||||
|
{
|
||||||
|
printf("tmtm: can't GetConsoleMode() for output console.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetConsoleMode(hStdin, 0); //FIXME: Should check for error!
|
||||||
|
SetConsoleMode(hStdout, 0); //FIXME: Should check for error!
|
||||||
|
|
||||||
|
PerfInit();
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
DWORD numEvents;
|
||||||
|
|
||||||
|
PerfDataRefresh();
|
||||||
|
DisplayScreen();
|
||||||
|
|
||||||
|
//WriteConsole(hStdin, " ", 1, &numEvents, NULL); // TODO: Make another way (this is ugly, I know)
|
||||||
|
GetNumberOfConsoleInputEvents(hStdin, &numEvents);
|
||||||
|
|
||||||
|
if (numEvents > 0)
|
||||||
|
{
|
||||||
|
if (ProcessKeys(numEvents) == TRUE)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Sleep(200); // TODO: Should be done more efficient (might be another thread handling input/etc)*/
|
||||||
|
}
|
||||||
|
|
||||||
|
RestoreConsole();
|
||||||
|
return 0;
|
||||||
|
}
|
244
rosapps/sysutils/ctm/ctm.h
Normal file
244
rosapps/sysutils/ctm/ctm.h
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
/* Console Task Manager
|
||||||
|
|
||||||
|
ctm.h - header file for main program
|
||||||
|
|
||||||
|
Written by: Aleksey Bragin (aleksey@studiocerebral.com)
|
||||||
|
|
||||||
|
Most of this file content is taken from
|
||||||
|
ReactOS Task Manager written by Brian Palmet (brianp@reactos.org)
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
|
||||||
|
|
||||||
|
#ifndef TMTM_H
|
||||||
|
#define TMTM_H
|
||||||
|
|
||||||
|
// keys
|
||||||
|
/*
|
||||||
|
#define VK_Q 0x51
|
||||||
|
#define VK_K 0x4B
|
||||||
|
#define VK_Y 0x59
|
||||||
|
#define VK_LEFT 0x25
|
||||||
|
#define VK_UP 0x26
|
||||||
|
#define VK_RIGHT 0x27
|
||||||
|
#define VK_DOWN 0x28
|
||||||
|
*/
|
||||||
|
|
||||||
|
//typedef ULARGE_INTEGER TIME, *PTIME;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
typedef struct _UNICODE_STRING {
|
||||||
|
USHORT Length;
|
||||||
|
USHORT MaximumLength;
|
||||||
|
PWSTR Buffer;
|
||||||
|
} UNICODE_STRING, *PUNICODE_STRING;
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define SystemBasicInformation 0
|
||||||
|
#define SystemPerformanceInformation 2
|
||||||
|
#define SystemTimeInformation 3
|
||||||
|
#define SystemProcessInformation 5
|
||||||
|
#define SystemProcessorTimeInformation 8
|
||||||
|
#define SystemHandleInformation 16
|
||||||
|
#define SystemPageFileInformation 18
|
||||||
|
#define SystemCacheInformation 21
|
||||||
|
|
||||||
|
#define Li2Double(x) ((double)((x).HighPart) * 4.294967296E9 + (double)((x).LowPart))
|
||||||
|
|
||||||
|
typedef struct _PERFDATA
|
||||||
|
{
|
||||||
|
WCHAR ImageName[MAX_PATH];
|
||||||
|
ULONG ProcessId;
|
||||||
|
WCHAR UserName[MAX_PATH];
|
||||||
|
ULONG SessionId;
|
||||||
|
ULONG CPUUsage;
|
||||||
|
TIME CPUTime;
|
||||||
|
ULONG WorkingSetSizeBytes;
|
||||||
|
ULONG PeakWorkingSetSizeBytes;
|
||||||
|
ULONG WorkingSetSizeDelta;
|
||||||
|
ULONG PageFaultCount;
|
||||||
|
ULONG PageFaultCountDelta;
|
||||||
|
ULONG VirtualMemorySizeBytes;
|
||||||
|
ULONG PagedPoolUsagePages;
|
||||||
|
ULONG NonPagedPoolUsagePages;
|
||||||
|
ULONG BasePriority;
|
||||||
|
ULONG HandleCount;
|
||||||
|
ULONG ThreadCount;
|
||||||
|
ULONG USERObjectCount;
|
||||||
|
ULONG GDIObjectCount;
|
||||||
|
//IO_COUNTERS IOCounters;
|
||||||
|
|
||||||
|
TIME UserTime;
|
||||||
|
TIME KernelTime;
|
||||||
|
} PERFDATA, *PPERFDATA;
|
||||||
|
|
||||||
|
typedef struct SYSTEM_PROCESS_INFORMATION
|
||||||
|
{
|
||||||
|
ULONG RelativeOffset;
|
||||||
|
ULONG ThreadCount;
|
||||||
|
ULONG Unused1 [6];
|
||||||
|
TIME CreateTime;
|
||||||
|
TIME UserTime;
|
||||||
|
TIME KernelTime;
|
||||||
|
UNICODE_STRING Name;
|
||||||
|
ULONG BasePriority;
|
||||||
|
ULONG ProcessId;
|
||||||
|
ULONG ParentProcessId;
|
||||||
|
ULONG HandleCount;
|
||||||
|
ULONG SessionId;
|
||||||
|
ULONG Unused2;
|
||||||
|
ULONG PeakVirtualSizeBytes;
|
||||||
|
ULONG TotalVirtualSizeBytes;
|
||||||
|
ULONG PageFaultCount;
|
||||||
|
ULONG PeakWorkingSetSizeBytes;
|
||||||
|
ULONG TotalWorkingSetSizeBytes;
|
||||||
|
ULONG PeakPagedPoolUsagePages;
|
||||||
|
ULONG TotalPagedPoolUsagePages;
|
||||||
|
ULONG PeakNonPagedPoolUsagePages;
|
||||||
|
ULONG TotalNonPagedPoolUsagePages;
|
||||||
|
ULONG TotalPageFileUsageBytes;
|
||||||
|
ULONG PeakPageFileUsageBytes;
|
||||||
|
ULONG TotalPrivateBytes;
|
||||||
|
//SYSTEM_THREAD_INFORMATION ThreadSysInfo [1];
|
||||||
|
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
|
||||||
|
|
||||||
|
/*
|
||||||
|
typedef
|
||||||
|
struct _SYSTEM_PROCESSORTIME_INFO
|
||||||
|
{
|
||||||
|
LARGE_INTEGER IdleTime;
|
||||||
|
LARGE_INTEGER KernelTime;
|
||||||
|
LARGE_INTEGER UserTime;
|
||||||
|
LARGE_INTEGER DpcTime;
|
||||||
|
LARGE_INTEGER InterruptTime;
|
||||||
|
ULONG InterruptCount;
|
||||||
|
ULONG Unused;
|
||||||
|
|
||||||
|
} SYSTEM_PROCESSORTIME_INFO, *PSYSTEM_PROCESSORTIME_INFO;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// SystemPerformanceInfo (2)
|
||||||
|
typedef struct SYSTEM_PERFORMANCE_INFORMATION
|
||||||
|
{
|
||||||
|
LARGE_INTEGER liIdleTime; //TotalProcessorTime
|
||||||
|
LARGE_INTEGER IoReadTransferCount;
|
||||||
|
LARGE_INTEGER IoWriteTransferCount;
|
||||||
|
LARGE_INTEGER IoOtherTransferCount;
|
||||||
|
ULONG IoReadOperationCount;
|
||||||
|
ULONG IoWriteOperationCount;
|
||||||
|
ULONG IoOtherOperationCount;
|
||||||
|
ULONG MmAvailablePages;
|
||||||
|
ULONG MmTotalCommitedPages;
|
||||||
|
ULONG MmTotalCommitLimit;
|
||||||
|
ULONG MmPeakLimit;
|
||||||
|
ULONG PageFaults;
|
||||||
|
ULONG WriteCopies;
|
||||||
|
ULONG TransitionFaults;
|
||||||
|
ULONG Unknown1;
|
||||||
|
ULONG DemandZeroFaults;
|
||||||
|
ULONG PagesInput;
|
||||||
|
ULONG PagesRead;
|
||||||
|
ULONG Unknown2;
|
||||||
|
ULONG Unknown3;
|
||||||
|
ULONG PagesOutput;
|
||||||
|
ULONG PageWrites;
|
||||||
|
ULONG Unknown4;
|
||||||
|
ULONG Unknown5;
|
||||||
|
ULONG PoolPagedBytes;
|
||||||
|
ULONG PoolNonPagedBytes;
|
||||||
|
ULONG Unknown6;
|
||||||
|
ULONG Unknown7;
|
||||||
|
ULONG Unknown8;
|
||||||
|
ULONG Unknown9;
|
||||||
|
ULONG MmTotalSystemFreePtes;
|
||||||
|
ULONG MmSystemCodepage;
|
||||||
|
ULONG MmTotalSystemDriverPages;
|
||||||
|
ULONG MmTotalSystemCodePages;
|
||||||
|
ULONG Unknown10;
|
||||||
|
ULONG Unknown11;
|
||||||
|
ULONG Unknown12;
|
||||||
|
ULONG MmSystemCachePage;
|
||||||
|
ULONG MmPagedPoolPage;
|
||||||
|
ULONG MmSystemDriverPage;
|
||||||
|
ULONG CcFastReadNoWait;
|
||||||
|
ULONG CcFastReadWait;
|
||||||
|
ULONG CcFastReadResourceMiss;
|
||||||
|
ULONG CcFastReadNotPossible;
|
||||||
|
ULONG CcFastMdlReadNoWait;
|
||||||
|
ULONG CcFastMdlReadWait;
|
||||||
|
ULONG CcFastMdlReadResourceMiss;
|
||||||
|
ULONG CcFastMdlReadNotPossible;
|
||||||
|
ULONG CcMapDataNoWait;
|
||||||
|
ULONG CcMapDataWait;
|
||||||
|
ULONG CcMapDataNoWaitMiss;
|
||||||
|
ULONG CcMapDataWaitMiss;
|
||||||
|
ULONG CcPinMappedDataCount;
|
||||||
|
ULONG CcPinReadNoWait;
|
||||||
|
ULONG CcPinReadWait;
|
||||||
|
ULONG CcPinReadNoWaitMiss;
|
||||||
|
ULONG CcPinReadWaitMiss;
|
||||||
|
ULONG CcCopyReadNoWait;
|
||||||
|
ULONG CcCopyReadWait;
|
||||||
|
ULONG CcCopyReadNoWaitMiss;
|
||||||
|
ULONG CcCopyReadWaitMiss;
|
||||||
|
ULONG CcMdlReadNoWait;
|
||||||
|
ULONG CcMdlReadWait;
|
||||||
|
ULONG CcMdlReadNoWaitMiss;
|
||||||
|
ULONG CcMdlReadWaitMiss;
|
||||||
|
ULONG CcReadaheadIos;
|
||||||
|
ULONG CcLazyWriteIos;
|
||||||
|
ULONG CcLazyWritePages;
|
||||||
|
ULONG CcDataFlushes;
|
||||||
|
ULONG CcDataPages;
|
||||||
|
ULONG ContextSwitches;
|
||||||
|
ULONG Unknown13;
|
||||||
|
ULONG Unknown14;
|
||||||
|
ULONG SystemCalls;
|
||||||
|
|
||||||
|
} SYSTEM_PERFORMANCE_INFORMATION, *PSYSTEM_PERFORMANCE_INFORMATION;
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
LARGE_INTEGER liKeBootTime;
|
||||||
|
LARGE_INTEGER liKeSystemTime;
|
||||||
|
LARGE_INTEGER liExpTimeZoneBias;
|
||||||
|
ULONG uCurrentTimeZoneId;
|
||||||
|
DWORD dwReserved;
|
||||||
|
} SYSTEM_TIME_INFORMATION;
|
||||||
|
|
||||||
|
|
||||||
|
// ntdll!NtQuerySystemInformation (NT specific!)
|
||||||
|
//
|
||||||
|
// The function copies the system information of the
|
||||||
|
// specified type into a buffer
|
||||||
|
//
|
||||||
|
// NTSYSAPI
|
||||||
|
// NTSTATUS
|
||||||
|
// NTAPI
|
||||||
|
// NtQuerySystemInformation(
|
||||||
|
// IN UINT SystemInformationClass, // information type
|
||||||
|
// OUT PVOID SystemInformation, // pointer to buffer
|
||||||
|
// IN ULONG SystemInformationLength, // buffer size in bytes
|
||||||
|
// OUT PULONG ReturnLength OPTIONAL // pointer to a 32-bit
|
||||||
|
// // variable that receives
|
||||||
|
// // the number of bytes
|
||||||
|
// // written to the buffer
|
||||||
|
// );
|
||||||
|
typedef LONG (WINAPI *PROCNTQSI)(UINT,PVOID,ULONG,PULONG);
|
||||||
|
|
||||||
|
#endif
|
37
rosapps/sysutils/ctm/ctm.rc
Normal file
37
rosapps/sysutils/ctm/ctm.rc
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#include <windows.h>
|
||||||
|
#include <reactos/resource.h>
|
||||||
|
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION RES_UINT_FV_MAJOR,RES_UINT_FV_MINOR,RES_UINT_FV_REVISION,RES_UINT_FV_BUILD
|
||||||
|
PRODUCTVERSION RES_UINT_PV_MAJOR,RES_UINT_PV_MINOR,RES_UINT_PV_REVISION,RES_UINT_PV_BUILD
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS 0x1L
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS 0x40004L
|
||||||
|
FILETYPE 0x2L
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", RES_STR_COMPANY_NAME
|
||||||
|
VALUE "FileDescription", "ReactOS Console Task Manager\0"
|
||||||
|
VALUE "FileVersion", RES_STR_FILE_VERSION
|
||||||
|
VALUE "InternalName", "tlist\0"
|
||||||
|
VALUE "LegalCopyright", "2003, Aleksey Bragin\0"
|
||||||
|
VALUE "OriginalFilename", "tlist.exe\0"
|
||||||
|
VALUE "ProductName", RES_STR_PRODUCT_NAME
|
||||||
|
VALUE "ProductVersion", RES_STR_PRODUCT_VERSION
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
Loading…
Reference in a new issue