From b67e0ba9bac5f69ea10f36010026a5db476d7d32 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 18:40:22 -0500 Subject: [PATCH] feat(common): read the memory text the client sends at login The login request's memoryStats is a run of short parts with no separators (process working set and private bytes, memory load, physical memory, commit limit, the 32-bit client's own address space, peaks). ParseMemoryStats splits it into numbers; a text cut short keeps the parts before the cut. Co-Authored-By: Claude Opus 5.5 --- dCommon/CMakeLists.txt | 1 + dCommon/ClientSysInfo.cpp | 61 +++++++++++++++++++++++ dCommon/ClientSysInfo.h | 39 +++++++++++++++ tests/dCommonTests/CMakeLists.txt | 1 + tests/dCommonTests/ClientSysInfoTests.cpp | 57 +++++++++++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 dCommon/ClientSysInfo.cpp create mode 100644 dCommon/ClientSysInfo.h create mode 100644 tests/dCommonTests/ClientSysInfoTests.cpp diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index 6139d03f0..1080d3143 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -1,5 +1,6 @@ set(DCOMMON_SOURCES "AMFDeserialize.cpp" + "ClientSysInfo.cpp" "AmfSerialize.cpp" "BinaryIO.cpp" "dConfig.cpp" diff --git a/dCommon/ClientSysInfo.cpp b/dCommon/ClientSysInfo.cpp new file mode 100644 index 000000000..36a9ad51f --- /dev/null +++ b/dCommon/ClientSysInfo.cpp @@ -0,0 +1,61 @@ +#include "ClientSysInfo.h" + +#include +#include +#include + +namespace { + struct Reader { + std::string_view text; + size_t pos{}; + + void SkipSpace() { + while (pos < text.size() && std::isspace(static_cast(text[pos]))) pos++; + } + + bool Literal(std::string_view literal) { + SkipSpace(); + if (text.substr(pos, literal.size()) != literal) return false; + pos += literal.size(); + return true; + } + + std::optional Number() { + SkipSpace(); + int64_t value{}; + const auto* begin = text.data() + pos; + const auto [end, error] = std::from_chars(begin, text.data() + text.size(), value); + if (error != std::errc{} || end == begin) return std::nullopt; + pos += static_cast(end - begin); + return value; + } + }; +} + +ClientSysInfo::MemoryStats ClientSysInfo::ParseMemoryStats(const std::string_view text) { + MemoryStats stats; + // Each part: text before the number, text after it, and where the number goes + struct Part { std::string_view prefix; std::string_view suffix; std::optional MemoryStats::* field; }; + const std::array parts{ { + { "", "p,", &MemoryStats::workingSetBytes }, + { "", "vbytes.", &MemoryStats::pagefileUsageBytes }, + { "", "n-use.", &MemoryStats::memoryLoadPercent }, + { "", "TKb-pmem.", &MemoryStats::totalPhysKb }, + { "", "FKb pmem.", &MemoryStats::availPhysKb }, + { "", "TKb pfile.", &MemoryStats::totalPageFileKb }, + { "", "FKb pfile.", &MemoryStats::availPageFileKb }, + { "", "TKbytes vmem.", &MemoryStats::totalVirtualKb }, + { "", "FKb vmem.", &MemoryStats::availVirtualKb }, + { "P", "p,", &MemoryStats::peakWorkingSetBytes }, + { "", "v.", &MemoryStats::peakPagefileUsageBytes }, + } }; + Reader reader{ text }; + for (const auto& part : parts) { + if (!part.prefix.empty() && !reader.Literal(part.prefix)) return stats; + const auto value = reader.Number(); + if (!value || !reader.Literal(part.suffix)) return stats; + stats.*part.field = *value; + } + stats.complete = true; + return stats; +} diff --git a/dCommon/ClientSysInfo.h b/dCommon/ClientSysInfo.h new file mode 100644 index 000000000..1a67d01b9 --- /dev/null +++ b/dCommon/ClientSysInfo.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +/** + * The system description the client sends in its login request, as reported by the client. It comes from old Windows + * calls and describes what Windows (or Wine) tells a 32-bit program without a compatibility manifest, which is not + * necessarily the player's real hardware. docs/Dashboard.md (Client system info) says what each field means. + */ +namespace ClientSysInfo { + /** + * The login request's memoryStats text, split into numbers. The client writes these parts one after another with no + * separator (numbers are right-aligned to 7 characters): + * " p, vbytes." " n-use." " TKb-pmem." " FKb pmem." " TKb pfile." " FKb pfile." + * " TKbytes vmem. \n" " FKb vmem." "P p, v." + * The client cuts the text at 255 characters, so the last parts can be missing; a part that doesn't match stops the + * parse and leaves it and everything after it empty. + */ + struct MemoryStats { + std::optional workingSetBytes; // the client process's working set at login (bytes) + std::optional pagefileUsageBytes; // the client process's private (commit) bytes at login + std::optional memoryLoadPercent; // how much of the system's memory Windows says is in use + std::optional totalPhysKb; // physical memory Windows reports (KB) + std::optional availPhysKb; // free physical memory at login (KB) + std::optional totalPageFileKb; // commit limit: physical memory plus page files (KB) + std::optional availPageFileKb; // free commit at login (KB) + std::optional totalVirtualKb; // the 32-bit client's own address space, not the system's (KB) + std::optional availVirtualKb; // free address space in the client (KB) + std::optional peakWorkingSetBytes; // the client process's peak working set (bytes) + std::optional peakPagefileUsageBytes; // the client process's peak private bytes + + // Every part was read + bool complete{}; + }; + + MemoryStats ParseMemoryStats(std::string_view text); +} diff --git a/tests/dCommonTests/CMakeLists.txt b/tests/dCommonTests/CMakeLists.txt index db44c3db9..1abaee7b9 100644 --- a/tests/dCommonTests/CMakeLists.txt +++ b/tests/dCommonTests/CMakeLists.txt @@ -1,5 +1,6 @@ set(DCOMMONTEST_SOURCES "AMFDeserializeTests.cpp" + "ClientSysInfoTests.cpp" "Amf3Tests.cpp" "ToUnderlyingTests.cpp" "InstanceMigrationTests.cpp" diff --git a/tests/dCommonTests/ClientSysInfoTests.cpp b/tests/dCommonTests/ClientSysInfoTests.cpp new file mode 100644 index 000000000..12884f104 --- /dev/null +++ b/tests/dCommonTests/ClientSysInfoTests.cpp @@ -0,0 +1,57 @@ +#include + +#include "ClientSysInfo.h" + +using ClientSysInfo::ParseMemoryStats; + +namespace { + // As the client writes it: each number right-aligned to 7 characters, no separators, " \n" after the vmem total + constexpr std::string_view FULL = "123456789 p,234567890 vbytes.45 n-use.16717048 TKb-pmem.8123456 FKb pmem.33434096 TKb pfile." + "20000000 FKb pfile.4194176 TKbytes vmem. \n3800000 FKb vmem.P 130000000 p,240000000 v."; +} + +TEST(ClientSysInfoTests, ReadsEveryPart) { + const auto s = ParseMemoryStats(FULL); + EXPECT_TRUE(s.complete); + EXPECT_EQ(s.workingSetBytes, 123456789); + EXPECT_EQ(s.pagefileUsageBytes, 234567890); + EXPECT_EQ(s.memoryLoadPercent, 45); + EXPECT_EQ(s.totalPhysKb, 16717048); + EXPECT_EQ(s.availPhysKb, 8123456); + EXPECT_EQ(s.totalPageFileKb, 33434096); + EXPECT_EQ(s.availPageFileKb, 20000000); + EXPECT_EQ(s.totalVirtualKb, 4194176); + EXPECT_EQ(s.availVirtualKb, 3800000); + EXPECT_EQ(s.peakWorkingSetBytes, 130000000); + EXPECT_EQ(s.peakPagefileUsageBytes, 240000000); +} + +TEST(ClientSysInfoTests, PaddedNumbers) { + const auto s = ParseMemoryStats(" 12345 p, 67890 vbytes.7 n-use.1048576 TKb-pmem. 52428 FKb pmem."); + EXPECT_FALSE(s.complete); + EXPECT_EQ(s.workingSetBytes, 12345); + EXPECT_EQ(s.pagefileUsageBytes, 67890); + EXPECT_EQ(s.memoryLoadPercent, 7); + EXPECT_EQ(s.totalPhysKb, 1048576); + EXPECT_EQ(s.availPhysKb, 52428); + EXPECT_FALSE(s.totalPageFileKb); +} + +TEST(ClientSysInfoTests, CutTextKeepsWhatWasRead) { + // The client stops at 255 characters: a part cut in half is left out, with everything after it + const std::string cut(FULL.substr(0, FULL.find("FKb vmem.") + 3)); + const auto s = ParseMemoryStats(cut); + EXPECT_FALSE(s.complete); + EXPECT_EQ(s.totalVirtualKb, 4194176); + EXPECT_FALSE(s.availVirtualKb); + EXPECT_FALSE(s.peakWorkingSetBytes); +} + +TEST(ClientSysInfoTests, OtherTextReadsNothing) { + for (const auto* text : { "", "garbage", "12 q,34 vbytes.", "p, vbytes." }) { + const auto s = ParseMemoryStats(text); + EXPECT_FALSE(s.complete) << text; + EXPECT_FALSE(s.workingSetBytes) << text; + EXPECT_FALSE(s.totalPhysKb) << text; + } +}