mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
The account page lists the account's reports as a table and the Client System Info page lists every report (or each account's newest), sortable and searchable by account name or video card; a row opens every field. Values that say little about the real system are marked, and every caveat is a tooltip on the value and column. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
8.9 KiB
C++
162 lines
8.9 KiB
C++
#include "ClientSysInfoView.h"
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
|
|
#include "ClientSysInfo.h"
|
|
|
|
namespace {
|
|
nlohmann::json Optional(const std::optional<int64_t>& value) {
|
|
return value ? nlohmann::json(*value) : nlohmann::json(nullptr);
|
|
}
|
|
|
|
// label -> count, most common first (then by label), the tail folded into "Other"
|
|
nlohmann::json Ranked(const std::map<std::string, uint64_t>& counts, size_t top) {
|
|
std::vector<std::pair<std::string, uint64_t>> sorted(counts.begin(), counts.end());
|
|
std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.second != b.second ? a.second > b.second : a.first < b.first; });
|
|
nlohmann::json out = nlohmann::json::array();
|
|
uint64_t other = 0;
|
|
for (size_t i = 0; i < sorted.size(); i++) {
|
|
if (i < top) out.push_back({ {"label", sorted[i].first}, {"count", sorted[i].second} });
|
|
else other += sorted[i].second;
|
|
}
|
|
if (other > 0) out.push_back({ {"label", "Other"}, {"count", other} });
|
|
return out;
|
|
}
|
|
|
|
// The adapter part of the video card text: the client adds " (HAL-<vertex processing>)" after it
|
|
std::string Adapter(const std::string& videoCard) {
|
|
const auto paren = videoCard.rfind(" (");
|
|
const auto adapter = paren == std::string::npos ? videoCard : videoCard.substr(0, paren);
|
|
return adapter.empty() ? "(empty)" : adapter;
|
|
}
|
|
|
|
std::string ClientOsName(uint32_t clientOs) {
|
|
switch (clientOs) {
|
|
case 1: return "Windows";
|
|
case 2: return "Mac";
|
|
default: return "Unknown (" + std::to_string(clientOs) + ")";
|
|
}
|
|
}
|
|
}
|
|
|
|
const nlohmann::json& ClientSysInfoView::Caveats() {
|
|
static const nlohmann::json caveats = {
|
|
{"ip", "The address the login came from. Only kept while log_login_addresses is on, and only shown with logs_audit."},
|
|
{"clientOs", "1 Windows, 2 Mac: which build of the client this is (from its own settings), not the operating system it runs on."},
|
|
{"memoryStats", "The client's memory text, as sent (the newest login's while the rest stays the same). Physical memory and the commit "
|
|
"limit (pfile) are the system's totals from GlobalMemoryStatusEx; under Wine they are the host's. The vmem figures are the 32-bit "
|
|
"client's own address space (2 or 4 GB), not the system's. p/v bytes are the client process's own use at login. Memory load and "
|
|
"free amounts change every login."},
|
|
{"videoCard", "From Direct3D 9: the adapter description the driver reports, then the device type (HAL, REF or SW) and vertex processing "
|
|
"mode. Usually the real graphics card; under Wine or DXVK it is what the translation layer reports (normally the real card, "
|
|
"sometimes a stand-in). Cut at 127 characters."},
|
|
{"numberOfProcessors", "GetSystemInfo: logical processors a 32-bit program sees (at most 32). Under Wine, the host's count."},
|
|
{"processorType", "GetSystemInfo's old processor type: 586 for every x86 processor seen from a 32-bit program. Says nothing about the CPU."},
|
|
{"processorLevel", "GetSystemInfo: the CPU family number (6 for most Intel CPUs; AMD uses other families, such as 23 or 25 for Ryzen)."},
|
|
{"processorRevision", "GetSystemInfo: model (high byte) and stepping (low byte) within the family."},
|
|
{"osVersionInfoSize", "The size of the structure the client asked GetVersionExW to fill: always 276. Not system info."},
|
|
{"osVersion", "GetVersionExW. The client has no compatibility manifest, so Windows 8.1, 10 and 11 all report 6.2 build 9200. "
|
|
"A compatibility mode reports the version it imitates (XP SP3: 5.1.2600), and Wine or Proton report the Windows version they "
|
|
"are set to. If the call failed the fields hold whatever was in memory."},
|
|
{"platformId", "GetVersionExW: 2 (Windows NT) on every Windows the client runs on."},
|
|
};
|
|
return caveats;
|
|
}
|
|
|
|
const nlohmann::json& ClientSysInfoView::Trust() {
|
|
static const nlohmann::json trust = {
|
|
{"clientOs", "unreliable"}, {"processorType", "unreliable"}, {"osVersionInfoSize", "unreliable"}, {"osVersion", "unreliable"},
|
|
{"platformId", "unreliable"}, {"memoryStats", "approximate"}, {"videoCard", "approximate"}, {"numberOfProcessors", "approximate"},
|
|
{"processorLevel", "approximate"}, {"processorRevision", "approximate"},
|
|
};
|
|
return trust;
|
|
}
|
|
|
|
std::string ClientSysInfoView::OsVersion(const IClientSysInfo::SysInfoRow& row) {
|
|
return std::to_string(row.majorVersion) + "." + std::to_string(row.minorVersion) + "." + std::to_string(row.buildNumber);
|
|
}
|
|
|
|
std::string ClientSysInfoView::OsLabel(const IClientSysInfo::SysInfoRow& row) {
|
|
const auto major = row.majorVersion, minor = row.minorVersion;
|
|
if (major == 5 && minor == 1) return "Windows XP (or XP compatibility mode)";
|
|
if (major == 5 && minor == 2) return "Windows XP x64 / Server 2003 (or compatibility mode)";
|
|
if (major == 6 && minor == 0) return "Windows Vista (or Vista compatibility mode)";
|
|
if (major == 6 && minor == 1) return "Windows 7 (or Windows 7 compatibility mode)";
|
|
if (major == 6 && minor == 2) return "Windows 8 or newer (8.1, 10 and 11 report 6.2 to this client)";
|
|
if (major == 6 && minor == 3) return "Windows 8.1";
|
|
if (major == 10 && minor == 0) return "Windows 10 or 11";
|
|
return "Unknown";
|
|
}
|
|
|
|
std::string ClientSysInfoView::MemoryBucket(const uint64_t totalKb) {
|
|
if (totalKb == 0) return "Not read";
|
|
constexpr uint64_t GB = 1024 * 1024;
|
|
// Totals are a little under the installed amount (memory the firmware keeps), so each bucket ends just above it
|
|
constexpr std::pair<uint64_t, const char*> BUCKETS[]{ { 2, "Under 2 GB" }, { 4, "2-4 GB" }, { 8, "4-8 GB" }, { 16, "8-16 GB" }, { 32, "16-32 GB" }, { 64, "32-64 GB" } };
|
|
for (const auto& [limit, label] : BUCKETS) {
|
|
if (totalKb <= limit * GB) return label;
|
|
}
|
|
return "Over 64 GB";
|
|
}
|
|
|
|
nlohmann::json ClientSysInfoView::RowJson(const IClientSysInfo::SysInfoRow& row, const bool showIp) {
|
|
const auto memory = ClientSysInfo::ParseMemoryStats(row.memoryStats);
|
|
nlohmann::json out = {
|
|
{"id", row.id}, {"account_id", row.accountId}, {"first_seen", row.firstSeen}, {"last_seen", row.lastSeen}, {"logins", row.logins},
|
|
{"client_os", row.clientOs}, {"client_os_name", ClientOsName(row.clientOs)},
|
|
{"memory_stats", row.memoryStats},
|
|
{"memory", {
|
|
{"complete", memory.complete},
|
|
{"working_set_bytes", Optional(memory.workingSetBytes)}, {"pagefile_usage_bytes", Optional(memory.pagefileUsageBytes)},
|
|
{"memory_load_percent", Optional(memory.memoryLoadPercent)},
|
|
{"total_phys_kb", Optional(memory.totalPhysKb)}, {"avail_phys_kb", Optional(memory.availPhysKb)},
|
|
{"total_pagefile_kb", Optional(memory.totalPageFileKb)}, {"avail_pagefile_kb", Optional(memory.availPageFileKb)},
|
|
{"total_virtual_kb", Optional(memory.totalVirtualKb)}, {"avail_virtual_kb", Optional(memory.availVirtualKb)},
|
|
{"peak_working_set_bytes", Optional(memory.peakWorkingSetBytes)}, {"peak_pagefile_usage_bytes", Optional(memory.peakPagefileUsageBytes)},
|
|
}},
|
|
{"memory_total_kb", row.memoryTotalKb}, {"memory_bucket", MemoryBucket(row.memoryTotalKb)},
|
|
{"video_card", row.videoCard},
|
|
{"number_of_processors", row.numberOfProcessors}, {"processor_type", row.processorType},
|
|
{"processor_level", row.processorLevel}, {"processor_revision", row.processorRevision},
|
|
{"processor_model", row.processorRevision >> 8}, {"processor_stepping", row.processorRevision & 0xFF},
|
|
{"os_version_info_size", row.osVersionInfoSize}, {"os_major_version", row.majorVersion}, {"os_minor_version", row.minorVersion},
|
|
{"os_build_number", row.buildNumber}, {"os_platform_id", row.platformId},
|
|
{"os_version", OsVersion(row)}, {"os_label", OsLabel(row)},
|
|
};
|
|
if (showIp) out["ip"] = row.ip;
|
|
if (!row.accountName.empty()) out["account_name"] = row.accountName;
|
|
return out;
|
|
}
|
|
|
|
nlohmann::json ClientSysInfoView::Spread(const std::vector<IClientSysInfo::SysInfoRow>& latest, const size_t top) {
|
|
std::map<std::string, uint64_t> os, video, memory, processors, clientOs;
|
|
for (const auto& row : latest) {
|
|
os[OsVersion(row) + " - " + OsLabel(row)]++;
|
|
video[Adapter(row.videoCard)]++;
|
|
memory[MemoryBucket(row.memoryTotalKb)]++;
|
|
processors[std::to_string(row.numberOfProcessors)]++;
|
|
clientOs[ClientOsName(row.clientOs)]++;
|
|
}
|
|
// Memory buckets and processor counts read best in their own order
|
|
auto ordered = [](const std::map<std::string, uint64_t>& counts, const std::vector<std::string>& order) {
|
|
nlohmann::json out = nlohmann::json::array();
|
|
for (const auto& label : order) {
|
|
const auto it = counts.find(label);
|
|
if (it != counts.end()) out.push_back({ {"label", label}, {"count", it->second} });
|
|
}
|
|
return out;
|
|
};
|
|
std::vector<std::string> cpuOrder;
|
|
for (const auto& [label, _] : processors) cpuOrder.push_back(label);
|
|
std::sort(cpuOrder.begin(), cpuOrder.end(), [](const std::string& a, const std::string& b) { return a.size() != b.size() ? a.size() < b.size() : a < b; });
|
|
return {
|
|
{"accounts", latest.size()},
|
|
{"os", Ranked(os, top)},
|
|
{"video", Ranked(video, top)},
|
|
{"memory", ordered(memory, { "Under 2 GB", "2-4 GB", "4-8 GB", "8-16 GB", "16-32 GB", "32-64 GB", "Over 64 GB", "Not read" })},
|
|
{"processors", ordered(processors, cpuOrder)},
|
|
{"clientOs", Ranked(clientOs, top)},
|
|
};
|
|
}
|