Spool account sync events to disk
This commit is contained in:
+128
-3
@@ -1,18 +1,102 @@
|
|||||||
#include "AccountSync.hh"
|
#include "AccountSync.hh"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace AccountSync {
|
namespace AccountSync {
|
||||||
|
|
||||||
static std::mutex config_mutex;
|
static std::mutex config_mutex;
|
||||||
|
static std::mutex spool_mutex;
|
||||||
static Config current_config;
|
static Config current_config;
|
||||||
|
|
||||||
|
static uint64_t now_usecs() {
|
||||||
|
using namespace std::chrono;
|
||||||
|
return duration_cast<microseconds>(system_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
|
||||||
static Config get_config() {
|
static Config get_config() {
|
||||||
std::lock_guard<std::mutex> g(config_mutex);
|
std::lock_guard<std::mutex> g(config_mutex);
|
||||||
return current_config;
|
return current_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string json_escape(const std::string& s) {
|
||||||
|
std::string ret;
|
||||||
|
ret.reserve(s.size() + 8);
|
||||||
|
for (unsigned char ch : s) {
|
||||||
|
switch (ch) {
|
||||||
|
case '\\':
|
||||||
|
ret += "\\\\";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
ret += "\\\"";
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
ret += "\\b";
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
ret += "\\f";
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
ret += "\\n";
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
ret += "\\r";
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
ret += "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ch < 0x20) {
|
||||||
|
char buf[8];
|
||||||
|
std::snprintf(buf, sizeof(buf), "\\u%04X", ch);
|
||||||
|
ret += buf;
|
||||||
|
} else {
|
||||||
|
ret += static_cast<char>(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void append_spool_line(const Config& cfg, const std::string& line) {
|
||||||
|
if (cfg.spool_directory.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::lock_guard<std::mutex> g(spool_mutex);
|
||||||
|
std::filesystem::create_directories(cfg.spool_directory);
|
||||||
|
std::filesystem::path path = std::filesystem::path(cfg.spool_directory) / "events.jsonl";
|
||||||
|
|
||||||
|
std::ofstream f(path, std::ios::app);
|
||||||
|
if (!f) {
|
||||||
|
throw std::runtime_error("failed to open spool file");
|
||||||
|
}
|
||||||
|
f << line << '\n';
|
||||||
|
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[AccountSync] warning failed_to_write_spool directory=%s error=%s\n",
|
||||||
|
cfg.spool_directory.c_str(),
|
||||||
|
e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string base_event_json(const Config& cfg, const char* event, uint32_t account_id) {
|
||||||
|
return std::format(
|
||||||
|
"{{\"timestamp_usecs\":{},\"source\":\"newserv\",\"event\":\"{}\",\"region\":\"{}\",\"account_id\":{},\"account_id_str\":\"{:010}\"",
|
||||||
|
now_usecs(),
|
||||||
|
json_escape(event),
|
||||||
|
json_escape(cfg.region),
|
||||||
|
static_cast<unsigned int>(account_id),
|
||||||
|
static_cast<unsigned int>(account_id));
|
||||||
|
}
|
||||||
|
|
||||||
void configure(const Config& cfg) {
|
void configure(const Config& cfg) {
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> g(config_mutex);
|
std::lock_guard<std::mutex> g(config_mutex);
|
||||||
@@ -21,9 +105,11 @@ void configure(const Config& cfg) {
|
|||||||
|
|
||||||
if (cfg.enabled) {
|
if (cfg.enabled) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] config enabled region=%s coordinator_url=%s login_locks=%s\n",
|
"[AccountSync] config enabled region=%s coordinator_url=%s notify_bb_sessions=%s spool_directory=%s login_locks=%s\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
cfg.coordinator_url.c_str(),
|
cfg.coordinator_url.c_str(),
|
||||||
|
cfg.notify_bb_sessions ? "true" : "false",
|
||||||
|
cfg.spool_directory.c_str(),
|
||||||
cfg.enable_login_locks ? "true" : "false");
|
cfg.enable_login_locks ? "true" : "false");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +126,12 @@ void configure_from_json(const phosg::JSON& json) {
|
|||||||
cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true);
|
cfg.notify_player_saves = json.get_bool("NotifyPlayerSaves", true);
|
||||||
cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true);
|
cfg.notify_backup_saves = json.get_bool("NotifyBackupSaves", true);
|
||||||
cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false);
|
cfg.enable_login_locks = json.get_bool("EnableLoginLocks", false);
|
||||||
|
|
||||||
|
// Backward-compatible during transition: old test configs with EnableLoginLocks=true
|
||||||
|
// still emit BB session events, but this does not enforce locks.
|
||||||
|
cfg.notify_bb_sessions = json.get_bool("NotifyBBSessions", cfg.enable_login_locks);
|
||||||
|
|
||||||
|
cfg.spool_directory = json.get_string("SpoolDirectory", "system/account-sync-spool");
|
||||||
configure(cfg);
|
configure(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +140,16 @@ void notify_account_saved(uint32_t account_id, const std::string& filename) {
|
|||||||
if (!cfg.enabled || !cfg.notify_account_saves) {
|
if (!cfg.enabled || !cfg.notify_account_saves) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n",
|
"[AccountSync] event=account_saved region=%s account_id=%010u filename=%s\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
static_cast<unsigned int>(account_id),
|
static_cast<unsigned int>(account_id),
|
||||||
filename.c_str());
|
filename.c_str());
|
||||||
|
|
||||||
|
auto line = base_event_json(cfg, "account_saved", account_id) +
|
||||||
|
std::format(",\"filename\":\"{}\"}}", json_escape(filename));
|
||||||
|
append_spool_line(cfg, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) {
|
void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& filename) {
|
||||||
@@ -60,12 +157,17 @@ void notify_backup_saved(uint32_t account_id, size_t slot, const std::string& fi
|
|||||||
if (!cfg.enabled || !cfg.notify_backup_saves) {
|
if (!cfg.enabled || !cfg.notify_backup_saves) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n",
|
"[AccountSync] event=backup_saved region=%s account_id=%010u slot=%zu filename=%s\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
static_cast<unsigned int>(account_id),
|
static_cast<unsigned int>(account_id),
|
||||||
slot,
|
slot,
|
||||||
filename.c_str());
|
filename.c_str());
|
||||||
|
|
||||||
|
auto line = base_event_json(cfg, "backup_saved", account_id) +
|
||||||
|
std::format(",\"slot\":{},\"filename\":\"{}\"}}", slot, json_escape(filename));
|
||||||
|
append_spool_line(cfg, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void notify_player_state_saved(
|
void notify_player_state_saved(
|
||||||
@@ -77,6 +179,7 @@ void notify_player_state_saved(
|
|||||||
if (!cfg.enabled || !cfg.notify_player_saves) {
|
if (!cfg.enabled || !cfg.notify_player_saves) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n",
|
"[AccountSync] event=player_state_saved region=%s reason=%s account_id=%010u bb_username=%s filename=%s\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
@@ -84,6 +187,13 @@ void notify_player_state_saved(
|
|||||||
static_cast<unsigned int>(account_id),
|
static_cast<unsigned int>(account_id),
|
||||||
bb_username.c_str(),
|
bb_username.c_str(),
|
||||||
filename.c_str());
|
filename.c_str());
|
||||||
|
|
||||||
|
auto line = base_event_json(cfg, "player_state_saved", account_id) +
|
||||||
|
std::format(",\"reason\":\"{}\",\"bb_username\":\"{}\",\"filename\":\"{}\"}}",
|
||||||
|
json_escape(reason),
|
||||||
|
json_escape(bb_username),
|
||||||
|
json_escape(filename));
|
||||||
|
append_spool_line(cfg, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void notify_bb_login_start(
|
void notify_bb_login_start(
|
||||||
@@ -92,9 +202,10 @@ void notify_bb_login_start(
|
|||||||
int64_t character_slot,
|
int64_t character_slot,
|
||||||
uint8_t connection_phase) {
|
uint8_t connection_phase) {
|
||||||
auto cfg = get_config();
|
auto cfg = get_config();
|
||||||
if (!cfg.enabled || !cfg.enable_login_locks) {
|
if (!cfg.enabled || !cfg.notify_bb_sessions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n",
|
"[AccountSync] event=bb_login_start region=%s account_id=%010u bb_username=%s character_slot=%lld connection_phase=%u\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
@@ -102,6 +213,13 @@ void notify_bb_login_start(
|
|||||||
bb_username.c_str(),
|
bb_username.c_str(),
|
||||||
static_cast<long long>(character_slot),
|
static_cast<long long>(character_slot),
|
||||||
static_cast<unsigned int>(connection_phase));
|
static_cast<unsigned int>(connection_phase));
|
||||||
|
|
||||||
|
auto line = base_event_json(cfg, "bb_login_start", account_id) +
|
||||||
|
std::format(",\"bb_username\":\"{}\",\"character_slot\":{},\"connection_phase\":{}}}",
|
||||||
|
json_escape(bb_username),
|
||||||
|
static_cast<long long>(character_slot),
|
||||||
|
static_cast<unsigned int>(connection_phase));
|
||||||
|
append_spool_line(cfg, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
void notify_bb_login_end(
|
void notify_bb_login_end(
|
||||||
@@ -109,15 +227,22 @@ void notify_bb_login_end(
|
|||||||
const std::string& bb_username,
|
const std::string& bb_username,
|
||||||
int64_t character_slot) {
|
int64_t character_slot) {
|
||||||
auto cfg = get_config();
|
auto cfg = get_config();
|
||||||
if (!cfg.enabled || !cfg.enable_login_locks) {
|
if (!cfg.enabled || !cfg.notify_bb_sessions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n",
|
"[AccountSync] event=bb_login_end region=%s account_id=%010u bb_username=%s character_slot=%lld\n",
|
||||||
cfg.region.c_str(),
|
cfg.region.c_str(),
|
||||||
static_cast<unsigned int>(account_id),
|
static_cast<unsigned int>(account_id),
|
||||||
bb_username.c_str(),
|
bb_username.c_str(),
|
||||||
static_cast<long long>(character_slot));
|
static_cast<long long>(character_slot));
|
||||||
|
|
||||||
|
auto line = base_event_json(cfg, "bb_login_end", account_id) +
|
||||||
|
std::format(",\"bb_username\":\"{}\",\"character_slot\":{}}}",
|
||||||
|
json_escape(bb_username),
|
||||||
|
static_cast<long long>(character_slot));
|
||||||
|
append_spool_line(cfg, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace AccountSync
|
} // namespace AccountSync
|
||||||
|
|||||||
+3
-1
@@ -18,7 +18,9 @@ struct Config {
|
|||||||
bool notify_account_saves = true;
|
bool notify_account_saves = true;
|
||||||
bool notify_player_saves = true;
|
bool notify_player_saves = true;
|
||||||
bool notify_backup_saves = true;
|
bool notify_backup_saves = true;
|
||||||
bool enable_login_locks = false;
|
bool notify_bb_sessions = false;
|
||||||
|
bool enable_login_locks = false; // Reserved for future blocking lock behavior
|
||||||
|
std::string spool_directory = "system/account-sync-spool";
|
||||||
};
|
};
|
||||||
|
|
||||||
void configure(const Config& cfg);
|
void configure(const Config& cfg);
|
||||||
|
|||||||
Reference in New Issue
Block a user