feat(db): client system info as reported by the client

client_sysinfo (migrations mysql 102, sqlite 85) keeps the system description
each account's client sent at login, exactly as sent, plus the physical memory
read from it. While nothing but the memory in use changes, the account's newest
row gets the new time and one more login; otherwise a new row starts. Log
pruning deletes rows not seen for a while (eLog::CLIENT_SYSINFO) and deleting
an account deletes its rows.

Tests on SQLite alone (dDatabaseSqliteTests) and in the MySQL parity tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 18:40:22 -05:00
parent b67e0ba9ba
commit 7bc41f2246
18 changed files with 371 additions and 7 deletions

View File

@@ -41,6 +41,7 @@
#include "IServerHealth.h"
#include "IChatLog.h"
#include "IChatFlags.h"
#include "IClientSysInfo.h"
#include "IRelatedData.h"
#include "IServerOperations.h"
#include "IPlayerPositions.h"
@@ -70,7 +71,7 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IChatFlags, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants, public IGuilds {
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IChatFlags, public IClientSysInfo, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic, public IApiKeys, public IUgcLookup, public IPermissionGrants, public IGuilds {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.

View File

@@ -0,0 +1,58 @@
#ifndef __ICLIENTSYSINFO__H__
#define __ICLIENTSYSINFO__H__
#include <cstdint>
#include <string>
#include <vector>
/**
* The system description each client sent with its login request, as reported by the client (see ClientSysInfo.h:
* compatibility values from old Windows calls, not necessarily the real hardware). The auth server keeps one row per
* account while the description stays the same, and starts a new row when it changes.
*/
class IClientSysInfo {
public:
struct SysInfoRow {
uint64_t id{};
uint32_t accountId{};
int64_t firstSeen{};
int64_t lastSeen{};
uint32_t logins{ 1 };
std::string ip; // empty while log_login_addresses is off
uint32_t clientOs{}; // the request's clientOS (1 Windows, 2 Mac)
std::string memoryStats; // the raw text of the newest login in this row
uint64_t memoryTotalKb{}; // physical memory from memoryStats (0: not read)
std::string videoCard;
uint32_t numberOfProcessors{};
uint32_t processorType{};
uint16_t processorLevel{};
uint16_t processorRevision{};
uint32_t osVersionInfoSize{};
uint32_t majorVersion{};
uint32_t minorVersion{};
uint32_t buildNumber{};
uint32_t platformId{};
// Whether two logins describe the same client (memory in use and the time are left out: they change each login)
bool SameAs(const SysInfoRow& other) const {
return ip == other.ip && clientOs == other.clientOs && memoryTotalKb == other.memoryTotalKb && videoCard == other.videoCard &&
numberOfProcessors == other.numberOfProcessors && processorType == other.processorType && processorLevel == other.processorLevel &&
processorRevision == other.processorRevision && osVersionInfoSize == other.osVersionInfoSize && majorVersion == other.majorVersion &&
minorVersion == other.minorVersion && buildNumber == other.buildNumber && platformId == other.platformId;
}
};
/**
* A successful login's description. If it is the same as the account's newest row, that row gets the new time, one
* more login and this login's memoryStats text; otherwise a new row is added.
*/
virtual void RecordClientSysInfo(const SysInfoRow& info) = 0;
// An account's rows, newest first
virtual std::vector<SysInfoRow> GetClientSysInfo(uint32_t accountId, uint32_t limit) = 0;
// Each account's newest row, for the spread across players
virtual std::vector<SysInfoRow> GetLatestClientSysInfo(uint32_t limit) = 0;
};
#endif //!__ICLIENTSYSINFO__H__

View File

@@ -100,7 +100,7 @@ public:
virtual uint32_t CompactEconomy(uint32_t cutoffDay, uint32_t mapCutoffDay) = 0;
virtual uint32_t PruneTransfers(int64_t beforeTime) = 0;
enum class eLog : uint8_t { ACTIVITY, COMMAND, AUDIT, CHEAT_DETECTION, CHAT, LOGIN_ADDRESS };
enum class eLog : uint8_t { ACTIVITY, COMMAND, AUDIT, CHEAT_DETECTION, CHAT, LOGIN_ADDRESS, CLIENT_SYSINFO };
// Delete log rows older than a unix time; rows without a time (0) are kept. Returns rows deleted.
virtual uint32_t PruneLog(eLog log, int64_t beforeTime) = 0;

View File

@@ -347,6 +347,9 @@ public:
void InsertChatFlagEvent(const ChatFlagEvent& event) override;
std::vector<ChatFlagEvent> GetChatFlagEvents(uint64_t flagId) override;
std::vector<std::pair<uint64_t, uint64_t>> GetFlaggedMessages(const std::vector<uint64_t>& messageIds) override;
void RecordClientSysInfo(const SysInfoRow& info) override;
std::vector<SysInfoRow> GetClientSysInfo(uint32_t accountId, uint32_t limit) override;
std::vector<SysInfoRow> GetLatestClientSysInfo(uint32_t limit) override;
void InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) override;
nlohmann::json GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) override;
Totp GetTotp(uint32_t accountId) override;

View File

@@ -201,7 +201,7 @@ void MySQLDatabase::DeleteAccount(const uint32_t accountId) {
ExecuteDelete("DELETE FROM character_snapshots WHERE character_id = ?;", characterId);
}
// Rows about the account itself. Audit log, chat log and reports stay as the record of what happened.
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses",
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses", "client_sysinfo",
"dashboard_preferences", "accounts_rewardcodes", "player_cheat_detections" }) {
ExecuteDelete(std::string("DELETE FROM ") + table + " WHERE account_id = ?;", accountId);
}

View File

@@ -297,6 +297,7 @@ uint32_t MySQLDatabase::PruneLog(eLog log, int64_t beforeTime) {
case eLog::CHEAT_DETECTION: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM player_cheat_detections WHERE violation_time < FROM_UNIXTIME(?);", beforeTime));
case eLog::CHAT: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM chat_log WHERE time > 0 AND time < ?;", beforeTime));
case eLog::LOGIN_ADDRESS: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM account_login_addresses WHERE last_seen < ?;", beforeTime));
case eLog::CLIENT_SYSINFO: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM client_sysinfo WHERE last_seen < ?;", beforeTime));
}
return 0;
}

View File

@@ -29,6 +29,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"MessageCaptures.cpp"
"ChatLog.cpp"
"ChatFlags.cpp"
"ClientSysInfo.cpp"
"RelatedData.cpp"
"ActivityLog.cpp"
"Behaviors.cpp"

View File

@@ -0,0 +1,58 @@
#include "MySQLDatabase.h"
namespace {
IClientSysInfo::SysInfoRow Row(sql::ResultSet& r) {
IClientSysInfo::SysInfoRow s;
s.id = r.getUInt64("id");
s.accountId = r.getUInt("account_id");
s.firstSeen = r.getInt64("first_seen");
s.lastSeen = r.getInt64("last_seen");
s.logins = r.getUInt("logins");
s.ip = r.getString("ip").c_str();
s.clientOs = r.getUInt("client_os");
s.memoryStats = r.getString("memory_stats").c_str();
s.memoryTotalKb = r.getUInt64("memory_total_kb");
s.videoCard = r.getString("video_card").c_str();
s.numberOfProcessors = r.getUInt("number_of_processors");
s.processorType = r.getUInt("processor_type");
s.processorLevel = static_cast<uint16_t>(r.getUInt("processor_level"));
s.processorRevision = static_cast<uint16_t>(r.getUInt("processor_revision"));
s.osVersionInfoSize = r.getUInt("os_version_info_size");
s.majorVersion = r.getUInt("os_major_version");
s.minorVersion = r.getUInt("os_minor_version");
s.buildNumber = r.getUInt("os_build_number");
s.platformId = r.getUInt("os_platform_id");
return s;
}
}
void MySQLDatabase::RecordClientSysInfo(const SysInfoRow& s) {
auto newest = ExecuteSelect("SELECT * FROM client_sysinfo WHERE account_id = ? ORDER BY id DESC LIMIT 1;", s.accountId);
if (newest->next()) {
const auto previous = Row(*newest.m_resultSet);
if (previous.SameAs(s)) {
ExecuteUpdate("UPDATE client_sysinfo SET last_seen = ?, logins = logins + 1, memory_stats = ? WHERE id = ?;", s.lastSeen, s.memoryStats, previous.id);
return;
}
}
ExecuteInsert("INSERT INTO client_sysinfo (account_id, first_seen, last_seen, logins, ip, client_os, memory_stats, memory_total_kb, video_card, "
"number_of_processors, processor_type, processor_level, processor_revision, os_version_info_size, os_major_version, os_minor_version, "
"os_build_number, os_platform_id) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
s.accountId, s.firstSeen, s.lastSeen, s.ip, s.clientOs, s.memoryStats, s.memoryTotalKb, s.videoCard, s.numberOfProcessors, s.processorType,
s.processorLevel, s.processorRevision, s.osVersionInfoSize, s.majorVersion, s.minorVersion, s.buildNumber, s.platformId);
}
std::vector<IClientSysInfo::SysInfoRow> MySQLDatabase::GetClientSysInfo(const uint32_t accountId, const uint32_t limit) {
std::vector<SysInfoRow> rows;
auto result = ExecuteSelect("SELECT * FROM client_sysinfo WHERE account_id = ? ORDER BY id DESC LIMIT ?;", accountId, limit);
while (result->next()) rows.push_back(Row(*result.m_resultSet));
return rows;
}
std::vector<IClientSysInfo::SysInfoRow> MySQLDatabase::GetLatestClientSysInfo(const uint32_t limit) {
std::vector<SysInfoRow> rows;
auto result = ExecuteSelect("SELECT c.* FROM client_sysinfo c JOIN (SELECT MAX(id) AS id FROM client_sysinfo GROUP BY account_id) n ON n.id = c.id "
"ORDER BY c.id DESC LIMIT ?;", limit);
while (result->next()) rows.push_back(Row(*result.m_resultSet));
return rows;
}

View File

@@ -331,6 +331,9 @@ public:
void InsertChatFlagEvent(const ChatFlagEvent& event) override;
std::vector<ChatFlagEvent> GetChatFlagEvents(uint64_t flagId) override;
std::vector<std::pair<uint64_t, uint64_t>> GetFlaggedMessages(const std::vector<uint64_t>& messageIds) override;
void RecordClientSysInfo(const SysInfoRow& info) override;
std::vector<SysInfoRow> GetClientSysInfo(uint32_t accountId, uint32_t limit) override;
std::vector<SysInfoRow> GetLatestClientSysInfo(uint32_t limit) override;
void InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) override;
nlohmann::json GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) override;
Totp GetTotp(uint32_t accountId) override;

View File

@@ -206,7 +206,7 @@ void SQLiteDatabase::DeleteAccount(const uint32_t accountId) {
ExecuteDelete("DELETE FROM character_snapshots WHERE character_id = ?;", characterId);
}
// Rows about the account itself. Audit log, chat log and reports stay as the record of what happened.
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses",
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses", "client_sysinfo",
"dashboard_preferences", "accounts_rewardcodes", "player_cheat_detections" }) {
ExecuteDelete(std::string("DELETE FROM ") + table + " WHERE account_id = ?;", accountId);
}

View File

@@ -300,6 +300,7 @@ uint32_t SQLiteDatabase::PruneLog(eLog log, int64_t beforeTime) {
case eLog::CHEAT_DETECTION: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM player_cheat_detections WHERE violation_time < datetime(?, 'unixepoch');", beforeTime));
case eLog::CHAT: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM chat_log WHERE time > 0 AND time < ?;", beforeTime));
case eLog::LOGIN_ADDRESS: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM account_login_addresses WHERE last_seen < ?;", beforeTime));
case eLog::CLIENT_SYSINFO: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM client_sysinfo WHERE last_seen < ?;", beforeTime));
}
return 0;
}

View File

@@ -29,6 +29,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"MessageCaptures.cpp"
"ChatLog.cpp"
"ChatFlags.cpp"
"ClientSysInfo.cpp"
"RelatedData.cpp"
"ActivityLog.cpp"
"Behaviors.cpp"

View File

@@ -0,0 +1,60 @@
#include "SQLiteDatabase.h"
namespace {
IClientSysInfo::SysInfoRow Row(CppSQLite3Query& r) {
IClientSysInfo::SysInfoRow s;
s.id = static_cast<uint64_t>(r.getInt64Field("id"));
s.accountId = static_cast<uint32_t>(r.getInt64Field("account_id"));
s.firstSeen = r.getInt64Field("first_seen");
s.lastSeen = r.getInt64Field("last_seen");
s.logins = static_cast<uint32_t>(r.getInt64Field("logins"));
s.ip = r.getStringField("ip");
s.clientOs = static_cast<uint32_t>(r.getInt64Field("client_os"));
s.memoryStats = r.getStringField("memory_stats");
s.memoryTotalKb = static_cast<uint64_t>(r.getInt64Field("memory_total_kb"));
s.videoCard = r.getStringField("video_card");
s.numberOfProcessors = static_cast<uint32_t>(r.getInt64Field("number_of_processors"));
s.processorType = static_cast<uint32_t>(r.getInt64Field("processor_type"));
s.processorLevel = static_cast<uint16_t>(r.getInt64Field("processor_level"));
s.processorRevision = static_cast<uint16_t>(r.getInt64Field("processor_revision"));
s.osVersionInfoSize = static_cast<uint32_t>(r.getInt64Field("os_version_info_size"));
s.majorVersion = static_cast<uint32_t>(r.getInt64Field("os_major_version"));
s.minorVersion = static_cast<uint32_t>(r.getInt64Field("os_minor_version"));
s.buildNumber = static_cast<uint32_t>(r.getInt64Field("os_build_number"));
s.platformId = static_cast<uint32_t>(r.getInt64Field("os_platform_id"));
return s;
}
}
void SQLiteDatabase::RecordClientSysInfo(const SysInfoRow& s) {
{
auto [_, newest] = ExecuteSelect("SELECT * FROM client_sysinfo WHERE account_id = ? ORDER BY id DESC LIMIT 1;", s.accountId);
if (!newest.eof()) {
const auto previous = Row(newest);
if (previous.SameAs(s)) {
ExecuteUpdate("UPDATE client_sysinfo SET last_seen = ?, logins = logins + 1, memory_stats = ? WHERE id = ?;", s.lastSeen, s.memoryStats, previous.id);
return;
}
}
}
ExecuteInsert("INSERT INTO client_sysinfo (account_id, first_seen, last_seen, logins, ip, client_os, memory_stats, memory_total_kb, video_card, "
"number_of_processors, processor_type, processor_level, processor_revision, os_version_info_size, os_major_version, os_minor_version, "
"os_build_number, os_platform_id) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
s.accountId, s.firstSeen, s.lastSeen, s.ip, s.clientOs, s.memoryStats, s.memoryTotalKb, s.videoCard, s.numberOfProcessors, s.processorType,
s.processorLevel, s.processorRevision, s.osVersionInfoSize, s.majorVersion, s.minorVersion, s.buildNumber, s.platformId);
}
std::vector<IClientSysInfo::SysInfoRow> SQLiteDatabase::GetClientSysInfo(const uint32_t accountId, const uint32_t limit) {
std::vector<SysInfoRow> rows;
auto [_, result] = ExecuteSelect("SELECT * FROM client_sysinfo WHERE account_id = ? ORDER BY id DESC LIMIT ?;", accountId, limit);
for (; !result.eof(); result.nextRow()) rows.push_back(Row(result));
return rows;
}
std::vector<IClientSysInfo::SysInfoRow> SQLiteDatabase::GetLatestClientSysInfo(const uint32_t limit) {
std::vector<SysInfoRow> rows;
auto [_, result] = ExecuteSelect("SELECT c.* FROM client_sysinfo c JOIN (SELECT MAX(id) AS id FROM client_sysinfo GROUP BY account_id) n ON n.id = c.id "
"ORDER BY c.id DESC LIMIT ?;", limit);
for (; !result.eof(); result.nextRow()) rows.push_back(Row(result));
return rows;
}

View File

@@ -381,6 +381,9 @@ class TestSQLDatabase : public GameDatabase {
void InsertChatFlagEvent(const ChatFlagEvent& event) override {}
std::vector<ChatFlagEvent> GetChatFlagEvents(uint64_t flagId) override { return {}; }
std::vector<std::pair<uint64_t, uint64_t>> GetFlaggedMessages(const std::vector<uint64_t>& messageIds) override { return {}; }
void RecordClientSysInfo(const SysInfoRow& info) override {}
std::vector<SysInfoRow> GetClientSysInfo(uint32_t accountId, uint32_t limit) override { return {}; }
std::vector<SysInfoRow> GetLatestClientSysInfo(uint32_t limit) override { return {}; }
void InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) override {}
nlohmann::json GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) override { return nlohmann::json::array(); }
Totp GetTotp(uint32_t accountId) override { return {}; }

View File

@@ -0,0 +1,28 @@
/* The system description clients send with their login request, exactly as the client reported it (old Windows calls:
compatibility values, not necessarily the player's real hardware). Written by the auth server at each successful
login: the account's newest row gets last_seen, logins + 1 and the new memory_stats text while everything else is the
same; otherwise a new row starts. ip is empty while log_login_addresses is off. memory_total_kb is the physical memory
read from memory_stats (0: not read). Rows not seen for log_client_sysinfo_days are deleted by the log pruning task. */
CREATE TABLE IF NOT EXISTS client_sysinfo (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
account_id INT UNSIGNED NOT NULL,
first_seen BIGINT NOT NULL,
last_seen BIGINT NOT NULL,
logins INT UNSIGNED NOT NULL DEFAULT 1,
ip VARCHAR(64) NOT NULL DEFAULT '',
client_os INT UNSIGNED NOT NULL DEFAULT 0,
memory_stats VARCHAR(512) NOT NULL DEFAULT '',
memory_total_kb BIGINT UNSIGNED NOT NULL DEFAULT 0,
video_card VARCHAR(256) NOT NULL DEFAULT '',
number_of_processors INT UNSIGNED NOT NULL DEFAULT 0,
processor_type INT UNSIGNED NOT NULL DEFAULT 0,
processor_level SMALLINT UNSIGNED NOT NULL DEFAULT 0,
processor_revision SMALLINT UNSIGNED NOT NULL DEFAULT 0,
os_version_info_size INT UNSIGNED NOT NULL DEFAULT 0,
os_major_version INT UNSIGNED NOT NULL DEFAULT 0,
os_minor_version INT UNSIGNED NOT NULL DEFAULT 0,
os_build_number INT UNSIGNED NOT NULL DEFAULT 0,
os_platform_id INT UNSIGNED NOT NULL DEFAULT 0,
INDEX client_sysinfo_account (account_id, id),
INDEX client_sysinfo_last_seen (last_seen)
);

View File

@@ -0,0 +1,24 @@
/* The system description clients send with their login request, as reported by the client. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS client_sysinfo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
first_seen BIGINT NOT NULL,
last_seen BIGINT NOT NULL,
logins INTEGER NOT NULL DEFAULT 1,
ip TEXT NOT NULL DEFAULT '',
client_os INTEGER NOT NULL DEFAULT 0,
memory_stats TEXT NOT NULL DEFAULT '',
memory_total_kb BIGINT NOT NULL DEFAULT 0,
video_card TEXT NOT NULL DEFAULT '',
number_of_processors INTEGER NOT NULL DEFAULT 0,
processor_type INTEGER NOT NULL DEFAULT 0,
processor_level INTEGER NOT NULL DEFAULT 0,
processor_revision INTEGER NOT NULL DEFAULT 0,
os_version_info_size INTEGER NOT NULL DEFAULT 0,
os_major_version INTEGER NOT NULL DEFAULT 0,
os_minor_version INTEGER NOT NULL DEFAULT 0,
os_build_number INTEGER NOT NULL DEFAULT 0,
os_platform_id INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS client_sysinfo_account ON client_sysinfo (account_id, id);
CREATE INDEX IF NOT EXISTS client_sysinfo_last_seen ON client_sysinfo (last_seen);

View File

@@ -1,13 +1,14 @@
/**
* The chat history and chat flag tables on SQLite, with no other database needed: a fresh SQLite file gets every
* migration (MigrationRunner::RunMigrations, as the servers run them), then the IChatLog and IChatFlags methods are
* checked against what was written. DatabaseParityTests compares the same methods with MySQL when one is set up.
* migration (MigrationRunner::RunMigrations, as the servers run them), then the IChatLog, IChatFlags and IClientSysInfo
* methods are checked against what was written. DatabaseParityTests compares the same methods with MySQL when one is set up.
*/
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdlib>
#include <filesystem>
#include <map>
#include <unistd.h>
#include "Database.h"
@@ -307,3 +308,84 @@ TEST_F(ChatHistorySqlite, PruningOldChatKeepsFlags) {
EXPECT_EQ(g_Db->CountChatMessages(all), before - 4);
EXPECT_EQ(g_Db->CountChatFlags({ .includePrivate = true, .includeWhispers = true }), flags);
}
// The client system info table (IClientSysInfo) shares this SQLite file: its rows don't touch the chat tables
namespace {
IClientSysInfo::SysInfoRow SysInfo(uint32_t account, int64_t time) {
IClientSysInfo::SysInfoRow s;
s.accountId = account;
s.firstSeen = s.lastSeen = time;
s.ip = "10.0.0.1";
s.clientOs = 1;
s.memoryStats = " 12345 p, 67890 vbytes.40 n-use.16717048 TKb-pmem.";
s.memoryTotalKb = 16717048;
s.videoCard = "NVIDIA GeForce GTX 1080 (HAL-hw vp)";
s.numberOfProcessors = 16;
s.processorType = 586;
s.processorLevel = 6;
s.processorRevision = 0x9e0a;
s.osVersionInfoSize = 276;
s.majorVersion = 6;
s.minorVersion = 2;
s.buildNumber = 9200;
s.platformId = 2;
return s;
}
}
TEST(ClientSysInfoSqlite, SameClientKeepsOneRow) {
auto s = SysInfo(900, 5000);
g_Db->RecordClientSysInfo(s);
s.lastSeen = 5100;
s.memoryStats = " 99999 p, 67890 vbytes.55 n-use.16717048 TKb-pmem."; // memory in use changes every login
g_Db->RecordClientSysInfo(s);
auto rows = g_Db->GetClientSysInfo(900, 10);
ASSERT_EQ(rows.size(), 1u);
EXPECT_EQ(rows[0].firstSeen, 5000);
EXPECT_EQ(rows[0].lastSeen, 5100);
EXPECT_EQ(rows[0].logins, 2u);
EXPECT_EQ(rows[0].memoryStats, s.memoryStats); // the newest login's text, as sent
EXPECT_EQ(rows[0].videoCard, s.videoCard);
EXPECT_EQ(rows[0].processorRevision, 0x9e0a);
EXPECT_EQ(rows[0].memoryTotalKb, 16717048u);
// Something else changes: a new row, newest first; going back to the old one starts another
s.firstSeen = s.lastSeen = 5200;
s.ip = "10.0.0.2";
g_Db->RecordClientSysInfo(s);
s.firstSeen = s.lastSeen = 5300;
s.ip = "10.0.0.1";
g_Db->RecordClientSysInfo(s);
rows = g_Db->GetClientSysInfo(900, 10);
ASSERT_EQ(rows.size(), 3u);
EXPECT_EQ(rows[0].ip, "10.0.0.1");
EXPECT_EQ(rows[0].firstSeen, 5300);
EXPECT_EQ(rows[1].ip, "10.0.0.2");
EXPECT_EQ(g_Db->GetClientSysInfo(900, 1).size(), 1u);
}
TEST(ClientSysInfoSqlite, LatestIsEachAccountsNewestRow) {
auto a = SysInfo(901, 6000);
g_Db->RecordClientSysInfo(a);
a.lastSeen = 6100;
a.buildNumber = 2600;
g_Db->RecordClientSysInfo(a);
g_Db->RecordClientSysInfo(SysInfo(902, 6050));
std::map<uint32_t, uint32_t> builds;
for (const auto& row : g_Db->GetLatestClientSysInfo(1000)) {
EXPECT_FALSE(builds.contains(row.accountId)) << row.accountId;
builds[row.accountId] = row.buildNumber;
}
EXPECT_EQ(builds[901], 2600u);
EXPECT_EQ(builds[902], 9200u);
}
TEST(ClientSysInfoSqlite, PruningDropsRowsNotSeenSince) {
g_Db->RecordClientSysInfo(SysInfo(903, 100));
auto kept = SysInfo(904, 100);
kept.lastSeen = 9000;
g_Db->RecordClientSysInfo(kept);
EXPECT_GE(g_Db->PruneLog(IDashboardAdmin::eLog::CLIENT_SYSINFO, 200), 1u);
EXPECT_TRUE(g_Db->GetClientSysInfo(903, 10).empty());
EXPECT_EQ(g_Db->GetClientSysInfo(904, 10).size(), 1u);
}

View File

@@ -898,7 +898,7 @@ TEST_F(ParitySeeded, LogsAndAudit) {
Both("PruneLog", [](GameDatabase& db) {
json out = json::array();
for (const auto log : { IDashboardAdmin::eLog::ACTIVITY, IDashboardAdmin::eLog::COMMAND, IDashboardAdmin::eLog::AUDIT,
IDashboardAdmin::eLog::CHEAT_DETECTION, IDashboardAdmin::eLog::CHAT, IDashboardAdmin::eLog::LOGIN_ADDRESS }) {
IDashboardAdmin::eLog::CHEAT_DETECTION, IDashboardAdmin::eLog::CHAT, IDashboardAdmin::eLog::LOGIN_ADDRESS, IDashboardAdmin::eLog::CLIENT_SYSINFO }) {
out.push_back(db.PruneLog(log, 1000)); // nothing is that old
}
return out;
@@ -1446,6 +1446,46 @@ TEST_F(ParitySeeded, Moderation) {
Both("GetLinkedAccounts", [](GameDatabase& db) { return json{ db.GetLinkedAccounts(1), db.GetLinkedAccounts(2), db.GetLinkedAccounts(3) }; });
Both("PruneLog login addresses", [](GameDatabase& db) { return db.PruneLog(IDashboardAdmin::eLog::LOGIN_ADDRESS, 1700000350); });
Both("CountLoginAddresses after prune", [](GameDatabase& db) { return json{ db.CountLoginAddresses(1), db.CountLoginAddresses(2) }; });
Both("RecordClientSysInfo", [](GameDatabase& db) {
IClientSysInfo::SysInfoRow s;
s.accountId = 1;
s.firstSeen = s.lastSeen = 1700000000;
s.ip = "10.0.0.1";
s.clientOs = 1;
s.memoryStats = "1 p,2 vbytes.3 n-use.4 TKb-pmem.";
s.memoryTotalKb = 4;
s.videoCard = "Card (HAL-hw vp)";
s.numberOfProcessors = 8;
s.processorType = 586;
s.processorLevel = 6;
s.processorRevision = 0x9e0a;
s.osVersionInfoSize = 276;
s.majorVersion = 6;
s.minorVersion = 2;
s.buildNumber = 9200;
s.platformId = 2;
db.RecordClientSysInfo(s);
s.lastSeen = 1700000100;
s.memoryStats = "9 p,2 vbytes.3 n-use.4 TKb-pmem.";
db.RecordClientSysInfo(s); // same client: the row is kept
s.lastSeen = 1700000200;
s.buildNumber = 2600;
db.RecordClientSysInfo(s); // changed: a new row
s.accountId = 2;
db.RecordClientSysInfo(s);
});
Both("GetClientSysInfo", [](GameDatabase& db) {
json out = json::array();
for (const auto& rows : { db.GetClientSysInfo(1, 10), db.GetLatestClientSysInfo(10) }) {
for (const auto& r : rows) {
out.push_back({ r.accountId, r.firstSeen, r.lastSeen, r.logins, r.ip, r.clientOs, r.memoryStats, r.memoryTotalKb, r.videoCard,
r.numberOfProcessors, r.processorType, r.processorLevel, r.processorRevision, r.osVersionInfoSize, r.majorVersion,
r.minorVersion, r.buildNumber, r.platformId });
}
}
return out;
});
Both("PruneLog client sysinfo", [](GameDatabase& db) { return db.PruneLog(IDashboardAdmin::eLog::CLIENT_SYSINFO, 1700000150); });
Both("InsertModerationDecision", [](GameDatabase& db) {
db.InsertModerationDecision("name", CHAR_ALICE2, "AliceRenamed", false, "Not allowed", 1700000000);
db.InsertModerationDecision("name", CHAR_ALICE2, "AliceOther", true, "", 1700000100);