You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
12 KiB
406 lines
12 KiB
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Digital Voice Modem - Converged FNE Software
|
|
* GPLv2 Open Source. Use is subject to license terms.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* Copyright (C) 2026 DVMProject Authors
|
|
*
|
|
*/
|
|
#include "fne/PatchStatusRegistry.h"
|
|
|
|
#include "common/Log.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cctype>
|
|
#include <limits>
|
|
#include <sstream>
|
|
|
|
constexpr uint32_t PatchStatusRegistry::DEFAULT_TTL_SECONDS;
|
|
constexpr uint32_t PatchStatusRegistry::MIN_TTL_SECONDS;
|
|
constexpr uint32_t PatchStatusRegistry::MAX_TTL_SECONDS;
|
|
constexpr uint32_t PatchStatusRegistry::MAX_WAIT_MS;
|
|
|
|
PatchStatusRegistry::PatchStatusRegistry() :
|
|
m_mutex(),
|
|
m_revisionChanged(),
|
|
m_peerPatches(),
|
|
m_revision(0U),
|
|
m_defaultTtlSeconds(DEFAULT_TTL_SECONDS),
|
|
m_minTtlSeconds(MIN_TTL_SECONDS),
|
|
m_maxTtlSeconds(MAX_TTL_SECONDS)
|
|
{
|
|
/* stub */
|
|
}
|
|
|
|
void PatchStatusRegistry::configure(uint32_t defaultTtlSeconds, uint32_t minTtlSeconds, uint32_t maxTtlSeconds)
|
|
{
|
|
if (minTtlSeconds == 0U)
|
|
minTtlSeconds = MIN_TTL_SECONDS;
|
|
if (maxTtlSeconds < minTtlSeconds)
|
|
maxTtlSeconds = minTtlSeconds;
|
|
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
m_minTtlSeconds = minTtlSeconds;
|
|
m_maxTtlSeconds = maxTtlSeconds;
|
|
m_defaultTtlSeconds = std::max(m_minTtlSeconds, std::min(defaultTtlSeconds, m_maxTtlSeconds));
|
|
}
|
|
|
|
bool PatchStatusRegistry::publish(json::object& request, json::object& response, std::string& errorMessage)
|
|
{
|
|
if (!request["peerId"].is<uint32_t>()) {
|
|
errorMessage = "peerId was not a valid integer";
|
|
return false;
|
|
}
|
|
|
|
if (!request["patches"].is<json::array>()) {
|
|
errorMessage = "patches was not a valid array";
|
|
return false;
|
|
}
|
|
|
|
PeerPatchSnapshot incoming;
|
|
incoming.peerId = request["peerId"].get<uint32_t>();
|
|
if (incoming.peerId == 0U) {
|
|
errorMessage = "peerId cannot be zero";
|
|
return false;
|
|
}
|
|
|
|
if (request["peerName"].is<std::string>())
|
|
incoming.peerName = request["peerName"].get<std::string>();
|
|
|
|
if (request["sequence"].is<uint32_t>())
|
|
incoming.sequence = request["sequence"].get<uint32_t>();
|
|
|
|
uint32_t ttlSeconds = defaultTtlSeconds();
|
|
if (request["ttlSeconds"].is<uint32_t>())
|
|
ttlSeconds = request["ttlSeconds"].get<uint32_t>();
|
|
ttlSeconds = clampTtl(ttlSeconds);
|
|
|
|
incoming.updatedAt = nowMs();
|
|
incoming.expiresAt = incoming.updatedAt + (static_cast<uint64_t>(ttlSeconds) * 1000U);
|
|
|
|
json::array patches = request["patches"].get<json::array>();
|
|
for (json::value& value : patches) {
|
|
if (!value.is<json::object>()) {
|
|
errorMessage = "patches contained a non-object entry";
|
|
return false;
|
|
}
|
|
|
|
json::object patchObj = value.get<json::object>();
|
|
PatchRecord patch;
|
|
if (!parsePatch(patchObj, patch, errorMessage))
|
|
return false;
|
|
|
|
incoming.patches.push_back(patch);
|
|
}
|
|
|
|
cleanupExpired();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
if (incoming.patches.empty())
|
|
m_peerPatches.erase(incoming.peerId);
|
|
else
|
|
m_peerPatches[incoming.peerId] = incoming;
|
|
bumpRevisionLocked();
|
|
|
|
response = snapshotLocked();
|
|
response["acceptedPeerId"].set<uint32_t>(incoming.peerId);
|
|
response["ttlSeconds"].set<uint32_t>(ttlSeconds);
|
|
}
|
|
|
|
m_revisionChanged.notify_all();
|
|
return true;
|
|
}
|
|
|
|
bool PatchStatusRegistry::removePeer(uint32_t peerId)
|
|
{
|
|
if (peerId == 0U)
|
|
return false;
|
|
|
|
bool removed = false;
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
removed = m_peerPatches.erase(peerId) > 0U;
|
|
if (removed)
|
|
bumpRevisionLocked();
|
|
}
|
|
|
|
if (removed)
|
|
m_revisionChanged.notify_all();
|
|
|
|
return removed;
|
|
}
|
|
|
|
uint32_t PatchStatusRegistry::cleanupExpired()
|
|
{
|
|
uint32_t removed = 0U;
|
|
bool changed = false;
|
|
uint64_t now = nowMs();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
for (auto it = m_peerPatches.begin(); it != m_peerPatches.end();) {
|
|
if (it->second.expiresAt <= now) {
|
|
it = m_peerPatches.erase(it);
|
|
removed++;
|
|
changed = true;
|
|
}
|
|
else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
bumpRevisionLocked();
|
|
}
|
|
|
|
if (changed)
|
|
m_revisionChanged.notify_all();
|
|
|
|
return removed;
|
|
}
|
|
|
|
json::object PatchStatusRegistry::snapshot()
|
|
{
|
|
cleanupExpired();
|
|
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return snapshotLocked();
|
|
}
|
|
|
|
json::object PatchStatusRegistry::waitForChanges(uint64_t sinceRevision, uint32_t waitMs)
|
|
{
|
|
waitMs = std::min(waitMs, MAX_WAIT_MS);
|
|
cleanupExpired();
|
|
|
|
std::unique_lock<std::mutex> lock(m_mutex);
|
|
if (waitMs > 0U && sinceRevision >= m_revision) {
|
|
m_revisionChanged.wait_for(lock, std::chrono::milliseconds(waitMs), [&]() {
|
|
return m_revision > sinceRevision;
|
|
});
|
|
}
|
|
|
|
return snapshotLocked();
|
|
}
|
|
|
|
uint64_t PatchStatusRegistry::revision() const
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return m_revision;
|
|
}
|
|
|
|
uint32_t PatchStatusRegistry::defaultTtlSeconds() const
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return m_defaultTtlSeconds;
|
|
}
|
|
|
|
uint32_t PatchStatusRegistry::minTtlSeconds() const
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return m_minTtlSeconds;
|
|
}
|
|
|
|
uint32_t PatchStatusRegistry::maxTtlSeconds() const
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return m_maxTtlSeconds;
|
|
}
|
|
|
|
uint64_t PatchStatusRegistry::nowMs()
|
|
{
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
std::string PatchStatusRegistry::normalizeMode(const std::string& mode)
|
|
{
|
|
std::string normalized = mode;
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) {
|
|
return static_cast<char>(std::tolower(c));
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
std::string PatchStatusRegistry::buildTalkgroupKey(const PatchMember& member)
|
|
{
|
|
std::ostringstream ss;
|
|
ss << normalizeMode(member.mode) << ':' << member.tgid << ':' << static_cast<uint32_t>(member.slot);
|
|
return ss.str();
|
|
}
|
|
|
|
json::object PatchStatusRegistry::memberToJson(const PatchMember& member)
|
|
{
|
|
json::object obj = json::object();
|
|
obj["system"].set<std::string>(member.system);
|
|
obj["mode"].set<std::string>(member.mode);
|
|
obj["tgid"].set<uint32_t>(member.tgid);
|
|
obj["slot"].set<uint8_t>(member.slot);
|
|
obj["key"].set<std::string>(buildTalkgroupKey(member));
|
|
return obj;
|
|
}
|
|
|
|
json::object PatchStatusRegistry::patchToJson(const PatchRecord& patch)
|
|
{
|
|
json::object obj = json::object();
|
|
obj["patchId"].set<std::string>(patch.patchId);
|
|
obj["active"].set<bool>(patch.active);
|
|
obj["oneWay"].set<bool>(patch.oneWay);
|
|
|
|
json::array members = json::array();
|
|
for (const PatchMember& member : patch.members)
|
|
members.push_back(json::value(memberToJson(member)));
|
|
obj["members"].set<json::array>(members);
|
|
|
|
return obj;
|
|
}
|
|
|
|
json::object PatchStatusRegistry::peerSnapshotToJson(const PeerPatchSnapshot& peer)
|
|
{
|
|
json::object obj = json::object();
|
|
obj["peerId"].set<uint32_t>(peer.peerId);
|
|
obj["peerName"].set<std::string>(peer.peerName);
|
|
obj["sequence"].set<uint32_t>(peer.sequence);
|
|
obj["updatedAt"].set<uint64_t>(peer.updatedAt);
|
|
obj["expiresAt"].set<uint64_t>(peer.expiresAt);
|
|
|
|
json::array patches = json::array();
|
|
for (const PatchRecord& patch : peer.patches)
|
|
patches.push_back(json::value(patchToJson(patch)));
|
|
obj["patches"].set<json::array>(patches);
|
|
|
|
return obj;
|
|
}
|
|
|
|
bool PatchStatusRegistry::parsePatch(json::object& obj, PatchRecord& patch, std::string& errorMessage) const
|
|
{
|
|
if (obj["patchId"].is<std::string>())
|
|
patch.patchId = obj["patchId"].get<std::string>();
|
|
|
|
if (obj["active"].is<bool>())
|
|
patch.active = obj["active"].get<bool>();
|
|
|
|
if (obj["oneWay"].is<bool>())
|
|
patch.oneWay = obj["oneWay"].get<bool>();
|
|
|
|
if (!obj["members"].is<json::array>()) {
|
|
errorMessage = "patch members was not a valid array";
|
|
return false;
|
|
}
|
|
|
|
json::array members = obj["members"].get<json::array>();
|
|
for (json::value& value : members) {
|
|
if (!value.is<json::object>()) {
|
|
errorMessage = "patch members contained a non-object entry";
|
|
return false;
|
|
}
|
|
|
|
json::object memberObj = value.get<json::object>();
|
|
PatchMember member;
|
|
if (!parseMember(memberObj, member, errorMessage))
|
|
return false;
|
|
|
|
patch.members.push_back(member);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PatchStatusRegistry::parseMember(json::object& obj, PatchMember& member, std::string& errorMessage) const
|
|
{
|
|
if (obj["system"].is<std::string>())
|
|
member.system = obj["system"].get<std::string>();
|
|
|
|
if (obj["mode"].is<std::string>())
|
|
member.mode = normalizeMode(obj["mode"].get<std::string>());
|
|
else
|
|
member.mode = "unknown";
|
|
|
|
if (!obj["tgid"].is<uint32_t>()) {
|
|
errorMessage = "patch member tgid was not a valid integer";
|
|
return false;
|
|
}
|
|
|
|
member.tgid = obj["tgid"].get<uint32_t>();
|
|
if (member.tgid == 0U) {
|
|
errorMessage = "patch member tgid cannot be zero";
|
|
return false;
|
|
}
|
|
|
|
if (obj["slot"].is<uint8_t>())
|
|
member.slot = obj["slot"].get<uint8_t>();
|
|
else if (obj["slot"].is<uint32_t>()) {
|
|
uint32_t slot = obj["slot"].get<uint32_t>();
|
|
if (slot > std::numeric_limits<uint8_t>::max()) {
|
|
errorMessage = "patch member slot was out of range";
|
|
return false;
|
|
}
|
|
member.slot = static_cast<uint8_t>(slot);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32_t PatchStatusRegistry::clampTtl(uint32_t ttlSeconds) const
|
|
{
|
|
std::lock_guard<std::mutex> guard(m_mutex);
|
|
return std::max(m_minTtlSeconds, std::min(ttlSeconds, m_maxTtlSeconds));
|
|
}
|
|
|
|
json::object PatchStatusRegistry::snapshotLocked() const
|
|
{
|
|
json::object response = json::object();
|
|
response["revision"].set<uint64_t>(m_revision);
|
|
|
|
json::array peers = json::array();
|
|
json::array patches = json::array();
|
|
json::object byTalkgroup = json::object();
|
|
|
|
for (const auto& entry : m_peerPatches) {
|
|
const PeerPatchSnapshot& peer = entry.second;
|
|
peers.push_back(json::value(peerSnapshotToJson(peer)));
|
|
|
|
for (const PatchRecord& patch : peer.patches) {
|
|
json::object patchObj = patchToJson(patch);
|
|
patchObj["peerId"].set<uint32_t>(peer.peerId);
|
|
patchObj["peerName"].set<std::string>(peer.peerName);
|
|
patchObj["updatedAt"].set<uint64_t>(peer.updatedAt);
|
|
patchObj["expiresAt"].set<uint64_t>(peer.expiresAt);
|
|
patches.push_back(json::value(patchObj));
|
|
|
|
for (const PatchMember& member : patch.members) {
|
|
std::string key = buildTalkgroupKey(member);
|
|
json::array entries = json::array();
|
|
if (byTalkgroup[key].is<json::array>())
|
|
entries = byTalkgroup[key].get<json::array>();
|
|
|
|
json::object tgPatch = json::object();
|
|
tgPatch["peerId"].set<uint32_t>(peer.peerId);
|
|
tgPatch["peerName"].set<std::string>(peer.peerName);
|
|
tgPatch["patchId"].set<std::string>(patch.patchId);
|
|
tgPatch["active"].set<bool>(patch.active);
|
|
tgPatch["oneWay"].set<bool>(patch.oneWay);
|
|
tgPatch["updatedAt"].set<uint64_t>(peer.updatedAt);
|
|
tgPatch["expiresAt"].set<uint64_t>(peer.expiresAt);
|
|
tgPatch["member"].set<json::object>(memberToJson(member));
|
|
entries.push_back(json::value(tgPatch));
|
|
byTalkgroup[key].set<json::array>(entries);
|
|
}
|
|
}
|
|
}
|
|
|
|
response["peers"].set<json::array>(peers);
|
|
response["patches"].set<json::array>(patches);
|
|
response["byTalkgroup"].set<json::object>(byTalkgroup);
|
|
return response;
|
|
}
|
|
|
|
void PatchStatusRegistry::bumpRevisionLocked()
|
|
{
|
|
m_revision++;
|
|
if (m_revision == 0U)
|
|
m_revision = 1U;
|
|
}
|