Add safe bank failure diagnostics for T5 and T6
Address #14 with request correlation, bounded parser reasons, startup version reporting, and rate-limited storage and settlement logs. Preserve accounting and API identity contracts, extend portable tests, and update release versions and rollout guidance.
This commit is contained in:
@@ -109,3 +109,23 @@ decisions and strict response validation for the new bank script. Build and
|
||||
install the DLL, then restart the full process. No upstream pins or external
|
||||
dependencies changed. See [bank setup and recovery](../../README.md#player-bank-t5-and-t6).
|
||||
Portable checks: `python3 tools/test_plugin_bank.py`.
|
||||
|
||||
## Bank diagnostics (TWS 1.2.0)
|
||||
|
||||
`bankVersion()` returns the loaded DLL's TWS component version, independently of
|
||||
upstream pins and the native loader's unchanged `plugin_version()` value.
|
||||
`bankResponse()` now also returns `reason`: `ok` on success, otherwise one of
|
||||
`empty_body`, `body_limit`, `invalid_json`, `schema`, `field_limit`, `state`,
|
||||
`amount_range`, `ok_state`, `missing_id`, or `zero_transfer`. Invalid responses
|
||||
clear all parsed fields. Existing acknowledgment acceptance rules are unchanged.
|
||||
The returned `code` on valid responses remains API data; bank scripts allowlist
|
||||
it before logging. Callers must never log arbitrary response fields.
|
||||
|
||||
Native journal logs name load/write/remove, limit identical failures to one
|
||||
per 30 seconds, and log recovery with suppressed counts. Exception text, journal
|
||||
contents and credentials are excluded. The new scripts require this DLL even
|
||||
when disabled by dvar because `bankVersion()` must link. No new third-party
|
||||
dependencies or upstream pin changes are required. Rebuild/install the DLL and
|
||||
restart the full server process; see the [diagnostic rollout](../../README.md#bank-diagnostics--tws-14).
|
||||
Portable checks: `python3 tools/test_plugin_bank.py` executes the real parser,
|
||||
storage and native registration adapter with sanitizers and engine shims.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "gsc.hpp"
|
||||
#include "game/scripting/array.hpp"
|
||||
#include <utils/bank.hpp>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
|
||||
namespace bank
|
||||
{
|
||||
@@ -13,11 +15,33 @@ namespace bank
|
||||
return {"tws-bank/t6", server, endpoint};
|
||||
}
|
||||
|
||||
void storage_error()
|
||||
// Only fixed operation names are printed, never exception text or paths
|
||||
// from private configuration. Per-operation state has bounded memory.
|
||||
void storage_result(size_t operation, bool failed)
|
||||
{
|
||||
// Do not echo parse exceptions: their text can contain player data.
|
||||
printf("^1[tws_bank] Outbox operation failed; check tws-bank/t6 files, disk space and permissions. Existing reports retained.\n");
|
||||
struct status
|
||||
{
|
||||
bool failed = false;
|
||||
std::chrono::steady_clock::time_point logged{};
|
||||
unsigned suppressed = 0;
|
||||
};
|
||||
static std::array<status, 3> states;
|
||||
constexpr const char* names[] = {"load", "write", "remove"};
|
||||
auto& state = states[operation];
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (failed && state.failed && now - state.logged < std::chrono::seconds(30))
|
||||
{
|
||||
++state.suppressed;
|
||||
return;
|
||||
}
|
||||
if (failed || state.failed)
|
||||
printf("[tws_bank] native_storage operation=%s outcome=%s suppressed=%u\n",
|
||||
names[operation], failed ? "storage_failure" : "recovered", state.suppressed);
|
||||
state.failed = failed;
|
||||
state.logged = now;
|
||||
state.suppressed = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class component final : public component_interface
|
||||
@@ -25,6 +49,7 @@ namespace bank
|
||||
public:
|
||||
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
|
||||
{
|
||||
gsc::function::add("bankversion", []() { return std::string("1.2.0"); });
|
||||
gsc::function::add("bankload", [](const std::string& server, const std::string& endpoint) -> scripting::script_value
|
||||
{
|
||||
try
|
||||
@@ -38,25 +63,27 @@ namespace bank
|
||||
entry["body"] = value.body;
|
||||
entries.emplace_back(entry);
|
||||
}
|
||||
storage_result(0, false);
|
||||
return entries;
|
||||
}
|
||||
catch (...) { storage_error(); return {}; }
|
||||
catch (...) { storage_result(0, true); return {}; }
|
||||
});
|
||||
gsc::function::add("bankstore", [](const std::string& server, const std::string& endpoint, const std::string& body)
|
||||
{
|
||||
try { spool(server, endpoint).store(body); return true; }
|
||||
catch (...) { storage_error(); return false; }
|
||||
try { spool(server, endpoint).store(body); storage_result(1, false); return true; }
|
||||
catch (...) { storage_result(1, true); return false; }
|
||||
});
|
||||
gsc::function::add("bankremove", [](const std::string& server, const std::string& endpoint, const std::string& id)
|
||||
{
|
||||
try { spool(server, endpoint).erase(id); return true; }
|
||||
catch (...) { storage_error(); return false; }
|
||||
try { spool(server, endpoint).erase(id); storage_result(2, false); return true; }
|
||||
catch (...) { storage_result(2, true); return false; }
|
||||
});
|
||||
gsc::function::add("bankresponse", [](const std::string& response)
|
||||
{
|
||||
const auto ack = utils::bank::parse_acknowledgement(response);
|
||||
scripting::array result;
|
||||
result["valid"] = ack.valid;
|
||||
result["reason"] = ack.reason;
|
||||
result["state"] = ack.state;
|
||||
result["code"] = ack.code;
|
||||
result["request_id"] = ack.id;
|
||||
|
||||
@@ -29,30 +29,42 @@ namespace utils::bank
|
||||
|
||||
acknowledgement parse_acknowledgement(const std::string& response)
|
||||
{
|
||||
acknowledgement result;
|
||||
if (response.empty() || response.size() >= max_body_size) return result;
|
||||
auto invalid = [](const char* reason)
|
||||
{
|
||||
acknowledgement value;
|
||||
value.reason = reason;
|
||||
return value;
|
||||
};
|
||||
if (response.empty()) return invalid("empty_body");
|
||||
if (response.size() >= max_body_size) return invalid("body_limit");
|
||||
const auto data = nlohmann::json::parse(response, nullptr, false);
|
||||
if (!data.is_object()) return result;
|
||||
if (data.is_discarded()) return invalid("invalid_json");
|
||||
if (!data.is_object()) return invalid("schema");
|
||||
try
|
||||
{
|
||||
if (!data.at("ok").is_boolean()) return result;
|
||||
acknowledgement result;
|
||||
if (!data.at("ok").is_boolean()) return invalid("schema");
|
||||
result.state = data.at("state").get<std::string>();
|
||||
result.code = data.at("code").get<std::string>();
|
||||
result.id = data.at("request_id").get<std::string>();
|
||||
if (result.id.size() > 64 || result.code.size() > 64 ||
|
||||
(result.state != "balance" && result.state != "prepared" && result.state != "committed" &&
|
||||
result.state != "cancelled" && result.state != "rejected")) return {};
|
||||
if (result.id.size() > 64 || result.code.size() > 64)
|
||||
return invalid("field_limit");
|
||||
if (result.state != "balance" && result.state != "prepared" && result.state != "committed" &&
|
||||
result.state != "cancelled" && result.state != "rejected") return invalid("state");
|
||||
for (const auto* key : {"amount", "balance"})
|
||||
if (!data.at(key).is_number_integer() || data.at(key) < 0 || data.at(key) > 1000000) return {};
|
||||
if (!data.at(key).is_number_integer() || data.at(key) < 0 || data.at(key) > 1000000)
|
||||
return invalid("amount_range");
|
||||
result.amount = data.at("amount").get<int>();
|
||||
result.balance = data.at("balance").get<int>();
|
||||
if (data.at("ok").get<bool>() != (result.state != "rejected") ||
|
||||
(result.state != "balance" && result.id.empty()) ||
|
||||
((result.state == "prepared" || result.state == "committed") && result.amount == 0)) return {};
|
||||
if (data.at("ok").get<bool>() != (result.state != "rejected")) return invalid("ok_state");
|
||||
if (result.state != "balance" && result.id.empty()) return invalid("missing_id");
|
||||
if ((result.state == "prepared" || result.state == "committed") && result.amount == 0)
|
||||
return invalid("zero_transfer");
|
||||
result.valid = true;
|
||||
result.reason = "ok";
|
||||
return result;
|
||||
}
|
||||
catch (...) { return {}; }
|
||||
return result;
|
||||
catch (...) { return invalid("schema"); }
|
||||
}
|
||||
|
||||
outbox::outbox(std::filesystem::path root, std::string server, std::string endpoint)
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace utils::bank
|
||||
{
|
||||
bool valid = false;
|
||||
std::string state, code, id;
|
||||
// Fixed diagnostic vocabulary; never includes response text.
|
||||
std::string reason = "schema";
|
||||
int amount = 0, balance = 0;
|
||||
};
|
||||
acknowledgement parse_acknowledgement(const std::string& response);
|
||||
|
||||
Reference in New Issue
Block a user