blob: 854658453e0338ce042411c48196ee79599e2b62 [file]
#include "MctpUtil.hpp"
#include "absl/strings/match.h"
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/container/flat_map.hpp>
#include <phosphor-logging/lg2.hpp>
#include <sdbusplus/bus/match.hpp>
#include <chrono>
#include <cstdint>
#include <filesystem>
std::map<std::string, SensorData> mctpEndpointConfigMap;
enum class State : std::uint8_t
{
None,
Add,
Remove
};
struct EndpointState
{
State state = State::None;
bool querying = false;
std::string emConfigPath;
std::chrono::steady_clock::time_point lastQueryTime =
std::chrono::steady_clock::time_point::min();
};
static std::map<std::string, EndpointState> endpointStates;
static uint8_t filterMsgType = 0;
static std::map<MctpCallbackToken, MctpEndpointCallback> mctpEndpointCallbacks;
static MctpCallbackToken nextCallbackToken = 1;
static std::unique_ptr<sdbusplus::bus::match_t> associationMatch = nullptr;
static std::unique_ptr<sdbusplus::bus::match_t> associationRemoveMatch =
nullptr;
static void performEmConfigQuery(
const std::shared_ptr<sdbusplus::asio::connection>& conn,
const std::string& endpointPath, const std::string& emConfigPath,
bool bypassCooldown, const std::function<void()>& cleanup,
const boost::asio::yield_context& yield)
{
auto& state = endpointStates[endpointPath];
auto now = std::chrono::steady_clock::now();
static constexpr auto cooldown = std::chrono::seconds(5);
state.querying = true; // Protect the window!
// Apply rate-limiting cooldown only if this endpoint has been queried
// before (to avoid subtracting time_point::min() which causes overflow), if
// we are not explicitly bypassing it (e.g. for post-priming queries), and
// if we are within the 5-second cooldown window.
if (state.lastQueryTime != std::chrono::steady_clock::time_point::min() &&
!bypassCooldown && (now - state.lastQueryTime < cooldown))
{
auto delay = cooldown - (now - state.lastQueryTime);
boost::asio::steady_timer timer(conn->get_io_context());
timer.expires_after(delay);
boost::system::error_code ec;
timer.async_wait(yield[ec]);
if (ec)
{
if (cleanup)
{
cleanup();
}
else
{
state.querying = false;
}
return;
}
}
// Step 1: Query SupportedMessageTypes directly from mctpd
boost::system::error_code ec;
std::variant<std::vector<uint8_t>> value;
try
{
value = conn->yield_method_call<std::variant<std::vector<uint8_t>>>(
yield, ec, "au.com.codeconstruct.MCTP1", endpointPath,
"org.freedesktop.DBus.Properties", "Get",
"xyz.openbmc_project.MCTP.Endpoint", "SupportedMessageTypes");
}
catch (const std::exception& e)
{
lg2::error(
"Exception getting SupportedMessageTypes for {PATH}: {ERROR}",
"PATH", endpointPath, "ERROR", e.what());
if (cleanup)
{
cleanup();
}
else
{
state.querying = false;
}
return;
}
if (ec)
{
lg2::error("Failed to get SupportedMessageTypes for {PATH}: {ERROR}",
"PATH", endpointPath, "ERROR", ec.message());
if (cleanup)
{
cleanup();
}
else
{
state.querying = false;
}
return;
}
const auto* typesPtr = std::get_if<std::vector<uint8_t>>(&value);
if (typesPtr == nullptr)
{
lg2::error("Invalid SupportedMessageTypes type for {PATH}", "PATH",
endpointPath);
if (cleanup)
{
cleanup();
}
else
{
state.querying = false;
}
return;
}
// Check if it contains the requested message type
if (std::find(typesPtr->begin(), typesPtr->end(), filterMsgType) ==
typesPtr->end())
{
lg2::info(
"Endpoint {PATH} does not support message type {TYPE}, ignoring",
"PATH", endpointPath, "TYPE", filterMsgType);
if (cleanup)
{
cleanup();
}
else
{
state.querying = false; // Clear protection!
}
return;
}
// Step 2: Proceed to EM query
ManagedObjectType objects;
try
{
objects = conn->yield_method_call<ManagedObjectType>(
yield, ec, "xyz.openbmc_project.EntityManager",
"/xyz/openbmc_project/inventory",
"org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
}
catch (const std::exception& e)
{
lg2::error("Exception getting managed objects: {ERROR}", "ERROR",
e.what());
if (cleanup)
{
cleanup();
}
return;
}
state.querying = false;
state.lastQueryTime = std::chrono::steady_clock::now(); // Update time!
State lastState = state.state;
state.state = State::None; // Reset for next query
if (lastState == State::Remove)
{
lg2::info("Ignoring query reply for removed endpoint {ENDPOINT}",
"ENDPOINT", endpointPath);
auto it = mctpEndpointConfigMap.find(endpointPath);
if (it != mctpEndpointConfigMap.end())
{
mctpEndpointConfigMap.erase(it);
}
if (cleanup)
{
cleanup();
}
return;
}
if (ec)
{
lg2::error("Failed to get managed objects for {PATH}: {ERROR}", "PATH",
emConfigPath, "ERROR", ec.message());
if (cleanup)
{
cleanup();
}
return;
}
auto objIt = objects.find(sdbusplus::message::object_path(emConfigPath));
if (objIt == objects.end())
{
lg2::warning("EM config path {PATH} not found in managed objects",
"PATH", emConfigPath);
if (cleanup)
{
cleanup();
}
return;
}
if (!objIt->second.empty())
{
lg2::info("Recorded MCTP endpoint {ENDPOINT} with config from {PATH}",
"ENDPOINT", endpointPath, "PATH", emConfigPath);
mctpEndpointConfigMap[endpointPath] = objIt->second;
lg2::info("DEBUG: Map populated, new size: {SIZE}", "SIZE",
mctpEndpointConfigMap.size());
for (const auto& [token, cb] : mctpEndpointCallbacks)
{
cb(endpointPath, objIt->second, false);
}
}
if (lastState == State::Add)
{
lg2::info("DEBUG: Triggering deferred query for endpoint {ENDPOINT}",
"ENDPOINT", endpointPath);
performEmConfigQuery(conn, endpointPath, emConfigPath, false, nullptr,
yield);
}
if (cleanup)
{
cleanup();
}
}
static void triggerDeferredQueries(
const std::shared_ptr<sdbusplus::asio::connection>& conn,
bool isPriming = false)
{
for (auto& [path, state] : endpointStates)
{
if (path == "__global__")
{
continue;
}
if (state.state == State::Add)
{
state.state = State::None;
boost::asio::spawn(
conn->get_io_context(),
[conn, path = path, emConfigPath = state.emConfigPath,
isPriming](const boost::asio::yield_context& yield) {
performEmConfigQuery(conn, path, emConfigPath, isPriming,
nullptr, yield);
});
}
else if (state.state == State::Remove)
{
state.state = State::None;
auto it = mctpEndpointConfigMap.find(path);
if (it != mctpEndpointConfigMap.end())
{
mctpEndpointConfigMap.erase(it);
}
}
}
}
void setupMctpEndpointListener(
const std::shared_ptr<sdbusplus::asio::connection>& conn,
MctpMessageType msgType)
{
filterMsgType = static_cast<uint8_t>(msgType);
// Match 1: Listen for Associations from mctp-reactor
const std::string associationMatchSpec =
"type='signal',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesAdded',arg0path='/au/com/codeconstruct/mctp1/'";
associationMatch = std::make_unique<sdbusplus::bus::match_t>(
static_cast<sdbusplus::bus_t&>(*conn), associationMatchSpec,
[conn](sdbusplus::message_t& msg) {
sdbusplus::message::object_path path;
boost::container::flat_map<
std::string,
boost::container::flat_map<
std::string,
std::variant<std::string, std::vector<std::string>>>>
interfaces;
msg.read(path, interfaces);
auto it = interfaces.find("xyz.openbmc_project.Association");
if (it == interfaces.end())
{
return;
}
auto propIt = it->second.find("endpoints");
if (propIt == it->second.end())
{
return;
}
const auto* endpointsPtr =
std::get_if<std::vector<std::string>>(&propIt->second);
if (!endpointsPtr)
{
return;
}
std::string emConfigPath = endpointsPtr->front();
std::string endpointPath = path.str;
constexpr std::string_view suffix = "/configured_by";
if (endpointPath.ends_with("/configured_by"))
{
endpointPath =
endpointPath.substr(0, endpointPath.size() - suffix.length());
}
auto& state = endpointStates[endpointPath];
state.emConfigPath = emConfigPath;
bool wasQuerying = endpointStates["__global__"].querying ||
state.querying;
if (wasQuerying)
{
lg2::info("Deferring Add for endpoint {PATH}", "PATH",
endpointPath);
state.state = State::Add;
}
else
{
boost::asio::spawn(conn->get_io_context(),
[conn, endpointPath, emConfigPath](
const boost::asio::yield_context& yield) {
performEmConfigQuery(conn, endpointPath, emConfigPath, false,
nullptr, yield);
});
}
});
const std::string associationRemoveMatchSpec =
"type='signal',interface='org.freedesktop.DBus.ObjectManager',member='InterfacesRemoved',arg0path='/au/com/codeconstruct/mctp1/'";
associationRemoveMatch = std::make_unique<sdbusplus::bus::match_t>(
static_cast<sdbusplus::bus_t&>(*conn), associationRemoveMatchSpec,
[](sdbusplus::message_t& msg) {
sdbusplus::message::object_path path;
std::vector<std::string> interfaces;
msg.read(path, interfaces);
std::string endpointPath = path.str;
constexpr std::string_view suffix = "/configured_by";
if (endpointPath.ends_with("/configured_by"))
{
endpointPath =
endpointPath.substr(0, endpointPath.size() - suffix.length());
}
auto& state = endpointStates[endpointPath];
if (endpointStates["__global__"].querying || state.querying)
{
state.state = State::Remove;
}
else
{
auto it = mctpEndpointConfigMap.find(endpointPath);
if (it != mctpEndpointConfigMap.end())
{
lg2::info("Removing MCTP endpoint {ENDPOINT} from map",
"ENDPOINT", endpointPath);
mctpEndpointConfigMap.erase(it);
for (const auto& [token, cb] : mctpEndpointCallbacks)
{
cb(endpointPath, {}, true);
}
}
}
});
// Prime the cache: Query existing Associations
endpointStates["__global__"].querying = true;
auto barrier = std::make_shared<int>(0);
#ifdef UNIT_TEST
// Inject 5 second delay ONLY for tests to hit the race condition
auto timer =
std::make_shared<boost::asio::steady_timer>(conn->get_io_context());
timer->expires_after(std::chrono::seconds(5));
timer->async_wait([conn, barrier](const boost::system::error_code& ec) {
if (ec)
return;
#endif
constexpr auto associationInterfaces =
std::to_array({"xyz.openbmc_project.Association"});
conn->async_method_call(
[conn, barrier](const boost::system::error_code& ec,
const GetSubTreeType& subtree) {
if (ec)
{
lg2::error(
"Failed to get associations from Object Mapper: {ERROR}",
"ERROR", ec.message());
endpointStates["__global__"].querying = false;
return;
}
if (subtree.empty())
{
endpointStates["__global__"].querying = false;
return;
}
for (const auto& [path, services] : subtree)
{
if (!path.ends_with("/configured_by"))
{
continue;
}
std::string endpointPath = path;
constexpr std::string_view suffix = "/configured_by";
endpointPath = endpointPath.substr(0, endpointPath.size() -
suffix.length());
(*barrier)++; // Increment barrier!
conn->async_method_call(
[conn, barrier, endpointPath](
const boost::system::error_code& ec,
const std::variant<std::vector<std::string>>& value) {
std::weak_ptr<sdbusplus::asio::connection> weakConn = conn;
auto cleanup = [barrier, weakConn]() {
auto conn = weakConn.lock();
if (!conn)
{
return;
}
(*barrier)--;
if (*barrier == 0)
{
endpointStates["__global__"].querying = false;
triggerDeferredQueries(conn, true);
}
};
if (ec)
{
lg2::error(
"Failed to get endpoints for {PATH}: {ERROR}",
"PATH", endpointPath, "ERROR", ec.message());
cleanup();
return;
}
const auto* endpoints =
std::get_if<std::vector<std::string>>(&value);
if (!endpoints || endpoints->empty())
{
cleanup();
return;
}
std::string emConfigPath = endpoints->front();
auto& state = endpointStates[endpointPath];
state.emConfigPath = emConfigPath;
boost::asio::spawn(
conn->get_io_context(),
[conn, endpointPath, emConfigPath,
cleanup = std::move(cleanup)](
const boost::asio::yield_context& yield) {
performEmConfigQuery(conn, endpointPath, emConfigPath,
true, std::move(cleanup), yield);
});
},
"xyz.openbmc_project.ObjectMapper", path,
"org.freedesktop.DBus.Properties", "Get",
"xyz.openbmc_project.Association", "endpoints");
}
},
"xyz.openbmc_project.ObjectMapper",
"/xyz/openbmc_project/object_mapper",
"xyz.openbmc_project.ObjectMapper", "GetSubTree",
"/au/com/codeconstruct/mctp1", 0, associationInterfaces);
#ifdef UNIT_TEST
});
#endif
}
BusInfo extractBusInfo(const SensorData& configData)
{
BusInfo busInfo;
auto extractProp = [](const auto& properties, BusInfo& info,
const std::string& key) {
auto it = properties.find(key);
if (it != properties.end())
{
info[key] = std::visit(VariantToStringVisitor(), it->second);
}
};
for (const auto& [intf, props] : configData)
{
if (absl::StrContains(intf,
"xyz.openbmc_project.Configuration.BusInfo"))
{
std::string busType;
auto it = props.find("BusType");
if (it != props.end())
{
busType = std::visit(VariantToStringVisitor(), it->second);
}
if (busType == "USB")
{
extractProp(props, busInfo, "RootHubPath");
extractProp(props, busInfo, "Port");
extractProp(props, busInfo, "Configuration");
extractProp(props, busInfo, "InterfaceNum");
busInfo["BusType"] = busType;
}
else if (busType == "I2C")
{
extractProp(props, busInfo, "Bus");
extractProp(props, busInfo, "Address");
busInfo["BusType"] = busType;
}
else if (!busType.empty())
{
lg2::warning("Unsupported BusType {TYPE}", "TYPE", busType);
}
}
}
return busInfo;
}
void cleanupMctpEndpointListener()
{
associationMatch.reset();
associationRemoveMatch.reset();
endpointStates.clear(); // Prevent state leaking between unit tests
}
MctpCallbackToken registerMctpEndpointCallback(MctpEndpointCallback&& cb)
{
MctpCallbackToken token = nextCallbackToken++;
mctpEndpointCallbacks[token] = std::move(cb);
return token;
}
void unregisterMctpEndpointCallback(MctpCallbackToken token)
{
mctpEndpointCallbacks.erase(token);
}
void unregisterAllMctpEndpointCallbacks()
{
mctpEndpointCallbacks.clear();
}