Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/improve exceptions #164

Merged
merged 23 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6fdd796
doc: Add a hint to OnDvarInit
SnowyWhite Nov 2, 2024
e1b61b1
feat: Add UI_GetGameTypeDisplayName
SnowyWhite Nov 2, 2024
d17fd51
feat: Add new dvar to save the server version
SnowyWhite Nov 2, 2024
28cf8b0
feat: Add dvar for the client version
SnowyWhite Nov 2, 2024
0859f4e
feat: Add method to get the Windows version
SnowyWhite Nov 2, 2024
5d546d0
feat: Include Windows Controls library
SnowyWhite Nov 2, 2024
ccadb19
feat: Limit Windows to the minimum supported version
SnowyWhite Nov 2, 2024
a9da13f
fix: Actually set sv_version value
SnowyWhite Nov 2, 2024
11f89ba
feat: Implement improved exception message
SnowyWhite Nov 2, 2024
3e34e46
feat: Apply formatting before copying the message to clipboard
SnowyWhite Nov 3, 2024
d99ff56
fix: Display MessageBox in case of a stack overflow
SnowyWhite Nov 3, 2024
c3602ad
Merge branch 'develop' into feat/improve-exceptions
Rackover Nov 4, 2024
4665b5a
Merge branch 'develop' of https://github.com/iw4x/iw4x-client into fe…
SnowyWhite Nov 5, 2024
d0d799a
feat: Add method to get system architecture
SnowyWhite Nov 5, 2024
96d22ec
fix: Use macros for duplicate strings
SnowyWhite Nov 5, 2024
d2e6ef2
Merge branch 'feat/improve-exceptions' of https://github.com/SnowyWhi…
SnowyWhite Nov 5, 2024
955b562
fix: Replace macro with constexpr
SnowyWhite Nov 5, 2024
e9c4ae2
fix: Implement requested changes
SnowyWhite Nov 7, 2024
4f9220f
fix: Use proper check for private match
SnowyWhite Nov 7, 2024
3858c31
fix: Rename com_sv_running to sv_running
SnowyWhite Nov 7, 2024
148e29a
feat: Add Mod Name to message
SnowyWhite Nov 12, 2024
62c3175
Merge branch 'develop' into feat/improve-exceptions
SnowyWhite Dec 3, 2024
144381d
Merge branch 'develop' into feat/improve-exceptions
Rackover Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Components/Modules/Events.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ namespace Components
static void OnSVInit(const std::function<void()>& callback);

// Client & Server (triggered once)
// Required for String Dvars (game will crash if the dvar subsystem wasn't initialised)
static void OnDvarInit(const std::function<void()>& callback);

// Client & Server (triggered once)
Expand Down
169 changes: 161 additions & 8 deletions src/Components/Modules/Exception.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
#include "Console.hpp"
#include "Exception.hpp"
#include "Window.hpp"
#include "Party.hpp"
#include "TextRenderer.hpp"

#include <cwctype>
#include <version.hpp>

namespace Components
{
constexpr auto CLIPBOARD_MSG = "Do you want to copy this message to the clipboard?";
constexpr auto DISCORD_LINK = "https://discord.gg/2ETE8engZM";
Rackover marked this conversation as resolved.
Show resolved Hide resolved

Utils::Hook Exception::SetFilterHook;
int Exception::MiniDumpType;

Expand Down Expand Up @@ -53,6 +59,148 @@ namespace Components
Game::Sys_SuspendOtherThreads();
}

std::wstring Exception::GetErrorMessage()
{
const auto clientVersion = (*Game::shortversion)->current.string;
const auto osVersion = Utils::IsWineEnvironment() ? "Wine" : Utils::GetWindowsVersion();
const auto launchParams = Utils::String::Convert(Utils::GetLaunchParameters());

std::string clientInfo = std::format(R"(
Client Info:
IW4x Version: {}
OS Version: {}
Parameters: {})",
clientVersion, osVersion, launchParams);

if (!Game::CL_IsCgameInitialized())
{
std::string msg = std::format("{}\n\n{}", clientInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}

const auto* gameType = (*Game::sv_gametype)->current.string;
const auto* mapName = (*Game::sv_mapname)->current.string;

// Get info for a private match
{
if (std::strcmp(Game::cls->servername, "localhost") == 0)
SnowyWhite marked this conversation as resolved.
Show resolved Hide resolved
{
std::string privateMatchInfo = std::format(R"(
Host Info:
Type: Private Match
Gametype: {}
Map Name: {})",
gameType, mapName);

std::string msg = std::format("{}\n{}\n\n{}", clientInfo, privateMatchInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}
}

// Get info for a dedicated server
{
const auto serverVersion = Dvar::Var("sv_version").get<std::string>();
const auto ipAddress = Network::Address(*Game::connectedHost).getString();

char serverName[256]{ 0 };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-constant bound is dangerous

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to accompany your comment with a snippet or a suggestion for something that the offending code should be replaced with

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None that come to mind right now without more context. I believe the first step would be to look into StripColors and StripAllTextIcons and see how we can handle it better; perhaps leave a TODO for now or a FIXME;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened issue #165 for this.

TextRenderer::StripColors(Party::GetHostName().data(), serverName, sizeof(serverName));
TextRenderer::StripAllTextIcons(serverName, serverName, sizeof(serverName));

std::string serverInfo = std::format(R"(
Server Info:
Type: Dedicated Server
IW4x Version: {}
Server Name: {}
IP Address: {}
Gametype: {}
Map Name: {})",
serverVersion, serverName, ipAddress, gameType, mapName);

std::string msg = std::format("{}\n{}\n\n{}", clientInfo, serverInfo, CLIPBOARD_MSG);
std::wstring message(msg.begin(), msg.end());
return message;
}
}

std::string Exception::FormatMessageForClipboard(const std::wstring& message)
{
std::wstringstream ss(message);
std::wstring line;
std::wostringstream result;

// Trim all the whitespaces from the message
auto trim = [](std::wstring& s)
{
s.erase(s.begin(), std::find_if_not(s.begin(), s.end(), [](wchar_t ch) { return std::iswspace(ch); })); // trim left
s.erase(std::find_if_not(s.rbegin(), s.rend(), [](wchar_t ch) { return std::iswspace(ch); }).base()); // trim right
};

// Construct a corrected version and get rid of the last line
while (std::getline(ss, line))
{
trim(line);

if (line != Utils::String::Convert(CLIPBOARD_MSG))
{
if (!line.empty())
{
result << line << L'\n';
}
}
}

// Enough wide strings for today
return Utils::String::Convert(result.str());
}

void Exception::DisplayErrorMessage(const std::wstring& title, const std::wstring& message)
{
const std::wstring footerText = std::format(
L"Join the official <a href=\"{}\">Discord Server</a> for additional support.",
Utils::String::Convert(DISCORD_LINK));

TASKDIALOGCONFIG taskDialogConfig = { 0 };
taskDialogConfig.cbSize = sizeof(taskDialogConfig);
taskDialogConfig.hInstance = GetModuleHandleA(nullptr);
taskDialogConfig.hwndParent = Window::GetWindow();
taskDialogConfig.pszWindowTitle = L"Unhandled Exception";
taskDialogConfig.pszMainIcon = MAKEINTRESOURCEW(-7); // Red bar with a shield icon
taskDialogConfig.pszMainInstruction = title.c_str();
taskDialogConfig.pszContent = message.c_str();
taskDialogConfig.dwCommonButtons = TDCBF_YES_BUTTON | TDCBF_NO_BUTTON;
taskDialogConfig.nDefaultButton = IDYES;
taskDialogConfig.pszFooterIcon = TD_INFORMATION_ICON;
taskDialogConfig.pszFooter = footerText.c_str();
taskDialogConfig.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_POSITION_RELATIVE_TO_WINDOW | TDF_SIZE_TO_CONTENT;
taskDialogConfig.lpCallbackData = reinterpret_cast<LONG_PTR>(&message);
taskDialogConfig.pfCallback = Exception::TaskDialogCallbackProc;

::TaskDialogIndirect(&taskDialogConfig, nullptr, nullptr, nullptr);
}

HRESULT CALLBACK Exception::TaskDialogCallbackProc(HWND, UINT notification, WPARAM clickedButton, LPARAM, LONG_PTR data)
{
const auto* msg = reinterpret_cast<const std::wstring*>(data);

if (notification == TDN_HYPERLINK_CLICKED)
{
Utils::OpenUrl(DISCORD_LINK);
}

if (notification == TDN_BUTTON_CLICKED)
{
if (clickedButton == IDYES)
{
std::string formattedMessage = Exception::FormatMessageForClipboard(*msg);
Exception::CopyMessageToClipboard(formattedMessage.c_str());
}
}

return S_OK;
}

void Exception::CopyMessageToClipboard(const char* error)
{
const auto hWndNewOwner = GetDesktopWindow();
Expand Down Expand Up @@ -98,20 +246,25 @@ namespace Components
return EXCEPTION_CONTINUE_EXECUTION;
}

const char* error;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_STACK_OVERFLOW)
{
error = "Termination because of a stack overflow.\nCopy exception address to clipboard?";
const auto error = std::format("Termination because of a stack overflow.\n{}", CLIPBOARD_MSG);

// Message should be copied to the clipboard if no button is pressed
if (MessageBoxA(nullptr, error.c_str(), nullptr, MB_YESNO | MB_ICONERROR) == IDYES)
{
CopyMessageToClipboard(Utils::String::VA("0x%08X", ExceptionInfo->ExceptionRecord->ExceptionAddress));
}
}
else
{
error = Utils::String::VA("Fatal error (0x%08X) at 0x%08X.\nCopy exception address to clipboard?", ExceptionInfo->ExceptionRecord->ExceptionCode, ExceptionInfo->ExceptionRecord->ExceptionAddress);
}
const auto code = ExceptionInfo->ExceptionRecord->ExceptionCode;
const auto address = reinterpret_cast<std::uintptr_t>(ExceptionInfo->ExceptionRecord->ExceptionAddress);

// Message should be copied to the keyboard if no button is pressed
if (MessageBoxA(nullptr, error, nullptr, MB_YESNO | MB_ICONERROR) == IDYES)
{
CopyMessageToClipboard(Utils::String::VA("0x%08X", ExceptionInfo->ExceptionRecord->ExceptionAddress));
std::wstring title = Utils::String::Convert(std::format("Fatal error (0x{:X}) at 0x{:X}", code, address));
std::wstring message = Exception::GetErrorMessage();

Exception::DisplayErrorMessage(title, message);
}

if (Flags::HasFlag("bigminidumps"))
Expand Down
5 changes: 5 additions & 0 deletions src/Components/Modules/Exception.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ namespace Components
static LONG WINAPI ExceptionFilter(LPEXCEPTION_POINTERS ExceptionInfo);
static __declspec(noreturn) void LongJmp_Internal_Stub(jmp_buf env, int status);

static std::wstring GetErrorMessage();
static std::string FormatMessageForClipboard(const std::wstring& message);

static void DisplayErrorMessage(const std::wstring& title, const std::wstring& message);
static void CopyMessageToClipboard(const char* error);

static LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter_Stub(LPTOP_LEVEL_EXCEPTION_FILTER);
static HRESULT CALLBACK TaskDialogCallbackProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData);

static int MiniDumpType;
static Utils::Hook SetFilterHook;
Expand Down
14 changes: 11 additions & 3 deletions src/Components/Modules/Party.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "Stats.hpp"
#include "TextRenderer.hpp"
#include "Voice.hpp"
#include "Events.hpp"

#include <version.hpp>

Expand Down Expand Up @@ -40,6 +41,7 @@ namespace Components
std::map<std::uint64_t, Network::Address> Party::LobbyMap;

Dvar::Var Party::PartyEnable;
Dvar::Var Party::ServerVersion;

SteamID Party::GenerateLobbyId()
{
Expand Down Expand Up @@ -208,8 +210,12 @@ namespace Components
return;
}

PartyEnable = Dvar::Register<bool>("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system");
Dvar::Register<bool>("xblive_privatematch", true, Game::DVAR_INIT, "");
Events::OnDvarInit([]
{
ServerVersion = Dvar::Register<const char*>("sv_version", "", Game::DVAR_SERVERINFO | Game::DVAR_INIT, "Server version");
PartyEnable = Dvar::Register<bool>("party_enable", Dedicated::IsEnabled(), Game::DVAR_NONE, "Enable party system");
Dvar::Register<bool>("xblive_privatematch", true, Game::DVAR_INIT, "");
});

// Kill the party migrate handler - it's not necessary and has apparently been used in the past for trickery?
Utils::Hook(0x46AB70, PartyMigrate_HandlePacket, HOOK_JUMP).install()->quick();
Expand Down Expand Up @@ -563,11 +569,13 @@ namespace Components
{
int clients;
int maxClients;
std::string version;

try
{
clients = std::stoi(Container.info.get("clients"));
maxClients = std::stoi(Container.info.get("sv_maxclients"));
version = Container.info.get("version");
}
catch ([[maybe_unused]] const std::exception& ex)
{
Expand All @@ -582,7 +590,7 @@ namespace Components
else
{
Dvar::Var("xblive_privateserver").set(true);

ServerVersion.set(version.c_str());
SnowyWhite marked this conversation as resolved.
Show resolved Hide resolved
Game::Menus_CloseAll(Game::uiContext);

Game::_XSESSION_INFO hostInfo;
Expand Down
1 change: 1 addition & 0 deletions src/Components/Modules/Party.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace Components
static std::map<std::uint64_t, Network::Address> LobbyMap;

static Dvar::Var PartyEnable;
static Dvar::Var ServerVersion;

static SteamID GenerateLobbyId();

Expand Down
6 changes: 3 additions & 3 deletions src/Components/Modules/QuickPatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,9 @@ namespace Components
// disable bind protection
Utils::Hook::Set<BYTE>(0x4DACA2, 0xEB);

// require Windows 5
Utils::Hook::Set<BYTE>(0x467ADF, 5);
Utils::Hook::Set<char>(0x6DF5D6, '5');
// require Windows 6 (Vista)
Utils::Hook::Set<BYTE>(0x467ADF, 6);
Utils::Hook::Set<char>(0x6DF5D6, '6');

// disable 'ignoring asset' notices
Utils::Hook::Nop(0x5BB902, 5);
Expand Down
1 change: 1 addition & 0 deletions src/Game/Dvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ namespace Game
const dvar_t** cg_scoreboardWidth = reinterpret_cast<const dvar_t**>(0x9FD0AC);

const dvar_t** version = reinterpret_cast<const dvar_t**>(0x1AD7930);
const dvar_t** shortversion = reinterpret_cast<const dvar_t**>(0x01AD79D0);

const dvar_t** viewposNow = reinterpret_cast<const dvar_t**>(0x9FD30C);

Expand Down
4 changes: 4 additions & 0 deletions src/Game/Dvars.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ namespace Game
extern const dvar_t** cg_scoreboardWidth;

extern const dvar_t** version;
/// <summary>

This comment was marked as resolved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why so? care to explain? :P

This comment was marked as resolved.

Copy link
Contributor

@Rackover Rackover Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea but as a result of it being microsoft-specific, this comment is visible in Intellisense when you type the name of the function or variable in Visual Studio.

If you type Game::version it will print in a tooltip the content of the <summary> placed just above, making it convenient to use. I do not have the same behaviour with just /// (although maybe there are other ways to achieve it)

If you have a practical case where the XML Documentation actually poses a problem, one that I can verify, in comparison to slash code documentation (or other methods) tell me! I do not know one, but I also work predominantly with M$ tooling

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

If what you mean by " 👎 " is that Visual Studio doesn't handle XML comments in Intellisense, I can provide examples where it does.
If what you mean by " 👎 " is that you know no practical downsides to using XML documentation then i'm happy to mark this conversation as resolved.

If you meant to express anything else, please use your words, I'm not fluent in "👎 " 😄

This comment was marked as resolved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question is: Is there anything that XML documentation breaks in comparison to ///? For instance, is there a widely used code editor that simply won't accept or display XML comments but would display /// comments?

If this is not the case then XML comments are the way to go - a large portion of people who work with this project are using or will use Visual Studio predominantly, where having Intellisense enabled by default means XML comments do have an advantage over /// comments. But if XML comments breaks other widely used IDEs, I don't mind considering leaving them out of the codebase.

I'm looking for a practical example of XML docs presenting a problem, in a real environment - i understand the theory behind having things standard or not but it's just not something that strikes me as particularly important in that project or in that ecosystem, so practical cases where something actually breaks take priority.

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the XML style comments on purpose for a couple of reasons:

  1. As mentioned by louve, one just needs to type /// and it will automatically create the <summary> tag and others for return types and parameters etc.

  2. Being able to distinguish them from other comments might come in handy in the future (perhaps the wiki will get a section explaining the codebase, and XML comments would make it easy for automated export and formatting).

  3. I am aware that this is a Visual Studio specific feature, but on the other hand most people will be working on the project with this IDE anyway and will therefore benefit from it, while users of other IDEs will not have much of a disadvantage. Moreover, the XML style is just the default option in Visual Studio, there are other, more standardized options, switching to another that works for everyone would be no problem:
    docs

  4. Having an eye-catching doc style will hopefully remind and encourage others to document their code too :D

/// Client Revision
/// </summary>
extern const dvar_t** shortversion;

extern const dvar_t** viewposNow;

Expand Down
1 change: 1 addition & 0 deletions src/Game/Functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ namespace Game
UI_ReplaceConversions_t UI_ReplaceConversions = UI_ReplaceConversions_t(0x4E9740);
UI_ParseInfos_t UI_ParseInfos = UI_ParseInfos_t(0x4027A0);
UI_GetMapDisplayName_t UI_GetMapDisplayName = UI_GetMapDisplayName_t(0x420700);
UI_GetGameTypeDisplayName_t UI_GetGameTypeDisplayName = UI_GetGameTypeDisplayName_t(0x4EB0B0);
ParseConfigStringToStruct_t ParseConfigStringToStruct = ParseConfigStringToStruct_t(0x403B60);

Win_GetLanguage_t Win_GetLanguage = Win_GetLanguage_t(0x45CBA0);
Expand Down
3 changes: 3 additions & 0 deletions src/Game/Functions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ namespace Game
typedef const char*(*UI_GetMapDisplayName_t)(const char* pszMap);
extern UI_GetMapDisplayName_t UI_GetMapDisplayName;

typedef const char* (*UI_GetGameTypeDisplayName_t)(const char* pszGameType);
extern UI_GetGameTypeDisplayName_t UI_GetGameTypeDisplayName;

typedef int(*ParseConfigStringToStruct_t)(void* pStruct, const Game::cspField_t* pFieldList, const int iNumFields, const char* pszBuffer, const int iMaxFieldTypes, int(__cdecl* parseSpecialFieldType)(char*, const char*, const int), void(__cdecl* parseStrcpy)(char*, char*));
extern ParseConfigStringToStruct_t ParseConfigStringToStruct;

Expand Down
2 changes: 2 additions & 0 deletions src/STDInclude.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
#include <hidsdi.h>
#pragma comment(lib, "Hid.lib")

#include <CommCtrl.h>
#pragma comment(lib, "Comctl32.lib")

// Ignore the warnings
#pragma warning(push)
Expand Down
45 changes: 45 additions & 0 deletions src/Utils/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,51 @@ namespace Utils
return (GetProcAddress(hntdll, "wine_get_version") != nullptr);
}

std::string GetWindowsVersion()
{
const auto ntdll = Utils::Library("ntdll.dll");
const auto rtlGetVersion = ntdll.getProc<LONG(WINAPI*)(PRTL_OSVERSIONINFOW)>("RtlGetVersion");

if (rtlGetVersion)
{
LONG STATUS_SUCCESS = 0;
RTL_OSVERSIONINFOW versionInfo = {};
versionInfo.dwOSVersionInfoSize = sizeof(versionInfo);

if (rtlGetVersion(&versionInfo) == STATUS_SUCCESS)
SnowyWhite marked this conversation as resolved.
Show resolved Hide resolved
{
const auto major = versionInfo.dwMajorVersion;
const auto minor = versionInfo.dwMinorVersion;
const auto build = versionInfo.dwBuildNumber;
const auto arch = Utils::GetWindowsArchitecture();

if (major == 10 && build >= 22000) return std::format("Windows 11 (Build {}) {}", build, arch);
if (major == 10) return std::format("Windows 10 (Build {}) {}", build, arch);
if (major == 6 && minor == 3) return std::format("Windows 8.1 (Build {}) {}", build, arch);
if (major == 6 && minor == 2) return std::format("Windows 8.0 (Build {}) {}", build, arch);
if (major == 6 && minor == 1) return std::format("Windows 7 (Build {}) {}", build, arch);
if (major == 6 && minor == 0) return std::format("Windows Vista (Build {}) {}", build, arch);
if (major == 5 && minor == 1) return std::format("Windows XP (Build {}) {}", build, arch);
}
}

return "Unknown Version";
}

std::string GetWindowsArchitecture()
{
SYSTEM_INFO sysInfo;
::GetNativeSystemInfo(&sysInfo);

switch (sysInfo.wProcessorArchitecture)
{
case PROCESSOR_ARCHITECTURE_AMD64: return "64 Bit";
case PROCESSOR_ARCHITECTURE_INTEL: return "32 Bit";
case PROCESSOR_ARCHITECTURE_ARM: return "ARM";
default: return "Unknown Architecture";
}
}

unsigned long GetParentProcessId()
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Expand Down
Loading