| #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(); |
| } |