mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-01 01:17:13 +00:00
feat(dashboard): client system info, as reported by the client
Account pages get a Client system info (as reported) card (client_sysinfo): every description the account's client sent at login, newest first, with the raw values, the memory text split into numbers and each field's caveat. The address is only shown with logs_audit. The Client System Info page (Logs & Health) shows the spread across players from each account's newest report: Windows version, video card, memory buckets, processor count and client build, marked as client-reported and approximate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,8 @@ set(DASHBOARDROUTES_SOURCES
|
||||
"ChatRoutes.cpp"
|
||||
"ChatHistory.cpp"
|
||||
"ChatFlagRoutes.cpp"
|
||||
"ClientSysInfoRoutes.cpp"
|
||||
"ClientSysInfoView.cpp"
|
||||
"Strikes.cpp"
|
||||
"Inspector.cpp"
|
||||
"CDClientBrowser.cpp"
|
||||
|
||||
40
dDashboardServer/routes/ClientSysInfoRoutes.cpp
Normal file
40
dDashboardServer/routes/ClientSysInfoRoutes.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "ClientSysInfoRoutes.h"
|
||||
|
||||
#include "ClientSysInfoView.h"
|
||||
#include "Database.h"
|
||||
#include "RouteUtils.h"
|
||||
#include "eHTTPMethod.h"
|
||||
|
||||
using namespace RouteUtils;
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t HISTORY_ROWS = 200;
|
||||
constexpr uint32_t SPREAD_ACCOUNTS = 100000;
|
||||
}
|
||||
|
||||
void ClientSysInfoRoutes::RegisterRoutes() {
|
||||
Route(eHTTPMethod::GET, "/client_sysinfo", Perm("client_sysinfo"), "Client system info: the spread across players, as reported by the client",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
RenderPage(reply, context, "client_sysinfo.jinja2", "client_sysinfo");
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/accounts/:id/client_sysinfo", Perm("client_sysinfo"),
|
||||
"The system info an account's client sent at login, as reported by the client (compatibility values from old Windows calls, not "
|
||||
"necessarily the real hardware): {rows: [...] newest first (at most 200), caveats: {field: text}, showsIp}. ip only with logs_audit",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto accountId = PathId<uint32_t>(context.path, 2);
|
||||
if (!accountId) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid account");
|
||||
const bool showIp = Can(context, "logs_audit");
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
for (const auto& row : Database::Get()->GetClientSysInfo(*accountId, HISTORY_ROWS)) rows.push_back(ClientSysInfoView::RowJson(row, showIp));
|
||||
JsonSuccess(reply, { {"rows", rows}, {"caveats", ClientSysInfoView::Caveats()}, {"showsIp", showIp} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/client_sysinfo/spread", Perm("client_sysinfo"),
|
||||
"How the system info clients report is spread across players, from each account's newest row (approximate: as reported by the "
|
||||
"client): {spread: {accounts, os, video, memory, processors, clientOs: [{label, count}]}, caveats}",
|
||||
[](HTTPReply& reply, const HTTPContext&) {
|
||||
JsonSuccess(reply, { {"spread", ClientSysInfoView::Spread(Database::Get()->GetLatestClientSysInfo(SPREAD_ACCOUNTS))},
|
||||
{"caveats", ClientSysInfoView::Caveats()} });
|
||||
});
|
||||
}
|
||||
10
dDashboardServer/routes/ClientSysInfoRoutes.h
Normal file
10
dDashboardServer/routes/ClientSysInfoRoutes.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Client system info (client_sysinfo): what each account's game client reported about its system at login, on the
|
||||
* account page, and the spread across players on its own page. Values are as reported by the client and often
|
||||
* compatibility defaults, not the real hardware (ClientSysInfoView::Caveats). Addresses only with logs_audit.
|
||||
*/
|
||||
namespace ClientSysInfoRoutes {
|
||||
void RegisterRoutes();
|
||||
}
|
||||
151
dDashboardServer/routes/ClientSysInfoView.cpp
Normal file
151
dDashboardServer/routes/ClientSysInfoView.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
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;
|
||||
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)},
|
||||
};
|
||||
}
|
||||
37
dDashboardServer/routes/ClientSysInfoView.h
Normal file
37
dDashboardServer/routes/ClientSysInfoView.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "IClientSysInfo.h"
|
||||
#include "json.hpp"
|
||||
|
||||
/**
|
||||
* How the dashboard shows the system info clients send at login (client_sysinfo), without the database or web server.
|
||||
* Every value is as reported by the client: old Windows calls in a 32-bit program without a compatibility manifest,
|
||||
* so several fields are compatibility values rather than the player's real hardware. Caveats() says what each field
|
||||
* is worth; the routes are in ClientSysInfoRoutes.cpp.
|
||||
*/
|
||||
namespace ClientSysInfoView {
|
||||
// field -> what it is and how far to trust it, for the page and the API
|
||||
const nlohmann::json& Caveats();
|
||||
|
||||
// One row as the dashboard sends it: the raw values, plus labels and the memory text split into numbers. The
|
||||
// address is only included when showIp (logs_audit).
|
||||
nlohmann::json RowJson(const IClientSysInfo::SysInfoRow& row, bool showIp);
|
||||
|
||||
// "6.2.9200", and what Windows version that is reported as
|
||||
std::string OsVersion(const IClientSysInfo::SysInfoRow& row);
|
||||
std::string OsLabel(const IClientSysInfo::SysInfoRow& row);
|
||||
|
||||
// A physical memory bucket by the reported total: "Under 2 GB", "2-4 GB", ... "64 GB or more", "Not read"
|
||||
std::string MemoryBucket(uint64_t totalKb);
|
||||
|
||||
/**
|
||||
* The spread across players from each account's newest row: {accounts, os: [{label, count}], video: [...],
|
||||
* memory: [...], processors: [...], clientOs: [...]}, most common first, at most top entries each (the rest are
|
||||
* one "Other" entry).
|
||||
*/
|
||||
nlohmann::json Spread(const std::vector<IClientSysInfo::SysInfoRow>& latest, size_t top = 20);
|
||||
}
|
||||
Reference in New Issue
Block a user