| #include "associations.hpp" |
| #include "handler.hpp" |
| #include "processing.hpp" |
| #include "types.hpp" |
| |
| #include <tinyxml2.h> |
| |
| #include <boost/asio/io_context.hpp> |
| #include <boost/asio/signal_set.hpp> |
| #include <boost/container/flat_map.hpp> |
| #include <sdbusplus/asio/connection.hpp> |
| #include <sdbusplus/asio/object_server.hpp> |
| #include <xyz/openbmc_project/Common/error.hpp> |
| |
| #include <atomic> |
| #include <chrono> |
| #include <exception> |
| #include <iomanip> |
| #include <iostream> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| AssociationMaps associationMaps; |
| |
| void updateOwners(sdbusplus::asio::connection* conn, |
| boost::container::flat_map<std::string, std::string>& owners, |
| const std::string& newObject) |
| { |
| if (newObject.starts_with(":")) |
| { |
| return; |
| } |
| conn->async_method_call( |
| [&, newObject](const boost::system::error_code ec, |
| const std::string& nameOwner) { |
| if (ec) |
| { |
| std::cerr << "Error getting owner of " << newObject << " : " |
| << ec << "\n"; |
| return; |
| } |
| owners[nameOwner] = newObject; |
| }, |
| "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetNameOwner", |
| newObject); |
| } |
| |
| void sendIntrospectionCompleteSignal(sdbusplus::asio::connection* systemBus, |
| const std::string& processName) |
| { |
| // TODO(ed) This signal doesn't get exposed properly in the |
| // introspect right now. Find out how to register signals in |
| // sdbusplus |
| sdbusplus::message_t m = systemBus->new_signal( |
| "/xyz/openbmc_project/object_mapper", |
| "xyz.openbmc_project.ObjectMapper.Private", "IntrospectionComplete"); |
| m.append(processName); |
| m.signal_send(); |
| } |
| |
| struct InProgressIntrospect |
| { |
| InProgressIntrospect() = delete; |
| InProgressIntrospect(const InProgressIntrospect&) = delete; |
| InProgressIntrospect(InProgressIntrospect&&) = delete; |
| InProgressIntrospect& operator=(const InProgressIntrospect&) = delete; |
| InProgressIntrospect& operator=(InProgressIntrospect&&) = delete; |
| InProgressIntrospect( |
| sdbusplus::asio::connection* systemBus, boost::asio::io_context& io, |
| const std::string& processName, AssociationMaps& am |
| #ifdef MAPPER_ENABLE_DEBUG |
| , |
| std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>> |
| globalStartTime |
| #endif |
| ) : |
| systemBus(systemBus), io(io), processName(processName), assocMaps(am) |
| #ifdef MAPPER_ENABLE_DEBUG |
| , |
| globalStartTime(std::move(globalStartTime)), |
| processStartTime(std::chrono::steady_clock::now()) |
| #endif |
| {} |
| ~InProgressIntrospect() |
| { |
| try |
| { |
| sendIntrospectionCompleteSignal(systemBus, processName); |
| #ifdef MAPPER_ENABLE_DEBUG |
| std::chrono::duration<float> diff = |
| std::chrono::steady_clock::now() - processStartTime; |
| std::cout << std::setw(50) << processName << " scan took " |
| << diff.count() << " seconds\n"; |
| |
| // If we're the last outstanding caller globally, calculate the |
| // time it took |
| if (globalStartTime != nullptr && globalStartTime.use_count() == 1) |
| { |
| diff = std::chrono::steady_clock::now() - *globalStartTime; |
| std::cout << "Total scan took " << diff.count() |
| << " seconds to complete\n"; |
| } |
| #endif |
| } |
| catch (const std::exception& e) |
| { |
| std::cerr |
| << "Terminating, unhandled exception while introspecting: " |
| << e.what() << "\n"; |
| std::terminate(); |
| } |
| catch (...) |
| { |
| std::cerr |
| << "Terminating, unhandled exception while introspecting\n"; |
| std::terminate(); |
| } |
| } |
| sdbusplus::asio::connection* systemBus; |
| boost::asio::io_context& io; |
| std::string processName; |
| AssociationMaps& assocMaps; |
| #ifdef MAPPER_ENABLE_DEBUG |
| std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>> |
| globalStartTime; |
| std::chrono::time_point<std::chrono::steady_clock> processStartTime; |
| #endif |
| }; |
| |
| void doAssociations(boost::asio::io_context& io, |
| sdbusplus::asio::connection* systemBus, |
| InterfaceMapType& interfaceMap, |
| sdbusplus::asio::object_server& objectServer, |
| const std::string& processName, const std::string& path, |
| int timeoutRetries = 0) |
| { |
| constexpr int maxTimeoutRetries = 3; |
| systemBus->async_method_call( |
| [&io, &objectServer, path, processName, &interfaceMap, systemBus, |
| timeoutRetries]( |
| const boost::system::error_code ec, |
| const std::variant<std::vector<Association>>& variantAssociations) { |
| if (ec) |
| { |
| if (ec.value() == boost::system::errc::timed_out && |
| timeoutRetries < maxTimeoutRetries) |
| { |
| doAssociations(io, systemBus, interfaceMap, objectServer, |
| processName, path, timeoutRetries + 1); |
| return; |
| } |
| std::cerr << "Error getting associations from " << path << "\n"; |
| } |
| std::vector<Association> associations = |
| std::get<std::vector<Association>>(variantAssociations); |
| associationChanged(io, objectServer, associations, path, |
| processName, interfaceMap, associationMaps); |
| }, |
| processName, path, "org.freedesktop.DBus.Properties", "Get", |
| assocDefsInterface, assocDefsProperty); |
| } |
| |
| void doIntrospect(boost::asio::io_context& io, |
| sdbusplus::asio::connection* systemBus, |
| const std::shared_ptr<InProgressIntrospect>& transaction, |
| InterfaceMapType& interfaceMap, |
| sdbusplus::asio::object_server& objectServer, |
| const std::string& path, int timeoutRetries = 0) |
| { |
| constexpr int maxTimeoutRetries = 3; |
| systemBus->async_method_call( |
| [&io, &interfaceMap, &objectServer, transaction, path, systemBus, |
| timeoutRetries](const boost::system::error_code ec, |
| const std::string& introspectXml) { |
| if (ec) |
| { |
| if (ec.value() == boost::system::errc::timed_out && |
| timeoutRetries < maxTimeoutRetries) |
| { |
| doIntrospect(io, systemBus, transaction, interfaceMap, |
| objectServer, path, timeoutRetries + 1); |
| return; |
| } |
| std::cerr << "Introspect call failed with error: " << ec << ", " |
| << ec.message() |
| << " on process: " << transaction->processName |
| << " path: " << path << "\n"; |
| return; |
| } |
| |
| tinyxml2::XMLDocument doc; |
| |
| tinyxml2::XMLError e = doc.Parse(introspectXml.c_str()); |
| if (e != tinyxml2::XMLError::XML_SUCCESS) |
| { |
| std::cerr << "XML parsing failed\n"; |
| return; |
| } |
| |
| tinyxml2::XMLNode* pRoot = doc.FirstChildElement("node"); |
| if (pRoot == nullptr) |
| { |
| std::cerr << "XML document did not contain any data\n"; |
| return; |
| } |
| auto& thisPathMap = interfaceMap[path]; |
| tinyxml2::XMLElement* pElement = |
| pRoot->FirstChildElement("interface"); |
| while (pElement != nullptr) |
| { |
| const char* ifaceName = pElement->Attribute("name"); |
| if (ifaceName == nullptr) |
| { |
| continue; |
| } |
| |
| thisPathMap[transaction->processName].emplace(ifaceName); |
| |
| if (std::strcmp(ifaceName, assocDefsInterface) == 0) |
| { |
| doAssociations(io, systemBus, interfaceMap, objectServer, |
| transaction->processName, path); |
| } |
| |
| pElement = pElement->NextSiblingElement("interface"); |
| } |
| |
| // Check if this new path has a pending association that can |
| // now be completed. |
| checkIfPendingAssociation(io, path, interfaceMap, |
| transaction->assocMaps, objectServer); |
| |
| pElement = pRoot->FirstChildElement("node"); |
| while (pElement != nullptr) |
| { |
| const char* childPath = pElement->Attribute("name"); |
| if (childPath != nullptr) |
| { |
| std::string parentPath(path); |
| if (parentPath == "/") |
| { |
| parentPath.clear(); |
| } |
| |
| doIntrospect(io, systemBus, transaction, interfaceMap, |
| objectServer, parentPath + "/" + childPath); |
| } |
| pElement = pElement->NextSiblingElement("node"); |
| } |
| }, |
| transaction->processName, path, "org.freedesktop.DBus.Introspectable", |
| "Introspect"); |
| } |
| |
| void startNewIntrospect( |
| sdbusplus::asio::connection* systemBus, boost::asio::io_context& io, |
| InterfaceMapType& interfaceMap, const std::string& processName, |
| AssociationMaps& assocMaps, |
| #ifdef MAPPER_ENABLE_DEBUG |
| std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>> |
| globalStartTime, |
| #endif |
| sdbusplus::asio::object_server& objectServer) |
| { |
| if (needToIntrospect(processName)) |
| { |
| std::shared_ptr<InProgressIntrospect> transaction = |
| std::make_shared<InProgressIntrospect>( |
| systemBus, io, processName, assocMaps |
| #ifdef MAPPER_ENABLE_DEBUG |
| , |
| globalStartTime |
| #endif |
| ); |
| |
| doIntrospect(io, systemBus, transaction, interfaceMap, objectServer, |
| "/"); |
| } |
| } |
| |
| void doListNames( |
| boost::asio::io_context& io, InterfaceMapType& interfaceMap, |
| sdbusplus::asio::connection* systemBus, |
| boost::container::flat_map<std::string, std::string>& nameOwners, |
| AssociationMaps& assocMaps, sdbusplus::asio::object_server& objectServer) |
| { |
| systemBus->async_method_call( |
| [&io, &interfaceMap, &nameOwners, &objectServer, systemBus, |
| &assocMaps](const boost::system::error_code ec, |
| std::vector<std::string> processNames) { |
| if (ec) |
| { |
| std::cerr << "Error getting names: " << ec << "\n"; |
| std::exit(EXIT_FAILURE); |
| return; |
| } |
| // Try to make startup consistent |
| std::sort(processNames.begin(), processNames.end()); |
| #ifdef MAPPER_ENABLE_DEBUG |
| std::shared_ptr<std::chrono::time_point<std::chrono::steady_clock>> |
| globalStartTime = std::make_shared< |
| std::chrono::time_point<std::chrono::steady_clock>>( |
| std::chrono::steady_clock::now()); |
| #endif |
| for (const std::string& processName : processNames) |
| { |
| if (needToIntrospect(processName)) |
| { |
| startNewIntrospect(systemBus, io, interfaceMap, processName, |
| assocMaps, |
| #ifdef MAPPER_ENABLE_DEBUG |
| globalStartTime, |
| #endif |
| objectServer); |
| updateOwners(systemBus, nameOwners, processName); |
| } |
| } |
| }, |
| "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", |
| "ListNames"); |
| } |
| |
| // Remove parents of the passed in path that: |
| // 1) Only have the 3 default interfaces on them |
| // - Means D-Bus created these, not application code, |
| // with the Properties, Introspectable, and Peer ifaces |
| // 2) Have no other child for this owner |
| void removeUnneededParents(const std::string& objectPath, |
| const std::string& owner, |
| InterfaceMapType& interfaceMap) |
| { |
| auto parent = objectPath; |
| |
| while (true) |
| { |
| auto pos = parent.find_last_of('/'); |
| if ((pos == std::string::npos) || (pos == 0)) |
| { |
| break; |
| } |
| parent = parent.substr(0, pos); |
| |
| auto parentIt = interfaceMap.find(parent); |
| if (parentIt == interfaceMap.end()) |
| { |
| break; |
| } |
| |
| auto ifacesIt = parentIt->second.find(owner); |
| if (ifacesIt == parentIt->second.end()) |
| { |
| break; |
| } |
| |
| if (ifacesIt->second.size() != 3) |
| { |
| break; |
| } |
| |
| auto childPath = parent + '/'; |
| |
| // Remove this parent if there isn't a remaining child on this owner |
| auto child = std::find_if( |
| interfaceMap.begin(), interfaceMap.end(), |
| [&owner, &childPath](const auto& entry) { |
| return entry.first.starts_with(childPath) && |
| (entry.second.find(owner) != entry.second.end()); |
| }); |
| |
| if (child == interfaceMap.end()) |
| { |
| parentIt->second.erase(ifacesIt); |
| if (parentIt->second.empty()) |
| { |
| interfaceMap.erase(parentIt); |
| } |
| } |
| else |
| { |
| break; |
| } |
| } |
| } |
| |
| int main() |
| { |
| boost::asio::io_context io; |
| std::shared_ptr<sdbusplus::asio::connection> systemBus = |
| std::make_shared<sdbusplus::asio::connection>(io); |
| |
| sdbusplus::asio::object_server server(systemBus); |
| |
| // Construct a signal set registered for process termination. |
| boost::asio::signal_set signals(io, SIGINT, SIGTERM); |
| signals.async_wait([&io](const boost::system::error_code&, int) { |
| io.stop(); |
| }); |
| |
| InterfaceMapType interfaceMap; |
| boost::container::flat_map<std::string, std::string> nameOwners; |
| |
| auto nameChangeHandler = [&interfaceMap, &io, &nameOwners, &server, |
| systemBus](sdbusplus::message_t& message) { |
| std::string name; // well-known |
| std::string oldOwner; // unique-name |
| std::string newOwner; // unique-name |
| |
| message.read(name, oldOwner, newOwner); |
| |
| if (name.starts_with(':')) |
| { |
| // We should do nothing with unique-name connections. |
| return; |
| } |
| |
| if (!oldOwner.empty()) |
| { |
| processNameChangeDelete(io, nameOwners, name, oldOwner, |
| interfaceMap, associationMaps, server); |
| } |
| |
| if (!newOwner.empty()) |
| { |
| #ifdef MAPPER_ENABLE_DEBUG |
| auto transaction = std::make_shared< |
| std::chrono::time_point<std::chrono::steady_clock>>( |
| std::chrono::steady_clock::now()); |
| #endif |
| // New daemon added |
| if (needToIntrospect(name)) |
| { |
| nameOwners[newOwner] = name; |
| startNewIntrospect(systemBus.get(), io, interfaceMap, name, |
| associationMaps, |
| #ifdef MAPPER_ENABLE_DEBUG |
| transaction, |
| #endif |
| server); |
| } |
| } |
| }; |
| |
| sdbusplus::bus::match_t nameOwnerChanged( |
| static_cast<sdbusplus::bus_t&>(*systemBus), |
| sdbusplus::bus::match::rules::nameOwnerChanged(), |
| std::move(nameChangeHandler)); |
| |
| auto interfacesAddedHandler = [&io, &interfaceMap, &nameOwners, |
| &server](sdbusplus::message_t& message) { |
| sdbusplus::message::object_path objPath; |
| InterfacesAdded interfacesAdded; |
| message.read(objPath, interfacesAdded); |
| std::string wellKnown; |
| if (!getWellKnown(nameOwners, message.get_sender(), wellKnown)) |
| { |
| return; // only introspect well-known |
| } |
| if (needToIntrospect(wellKnown)) |
| { |
| processInterfaceAdded(io, interfaceMap, objPath, interfacesAdded, |
| wellKnown, associationMaps, server); |
| } |
| }; |
| |
| sdbusplus::bus::match_t interfacesAdded( |
| static_cast<sdbusplus::bus_t&>(*systemBus), |
| sdbusplus::bus::match::rules::interfacesAdded(), |
| std::move(interfacesAddedHandler)); |
| |
| auto interfacesRemovedHandler = [&io, &interfaceMap, &nameOwners, |
| &server](sdbusplus::message_t& message) { |
| sdbusplus::message::object_path objPath; |
| std::vector<std::string> interfacesRemoved; |
| message.read(objPath, interfacesRemoved); |
| auto connectionMap = interfaceMap.find(objPath.str); |
| if (connectionMap == interfaceMap.end()) |
| { |
| return; |
| } |
| |
| std::string sender; |
| if (!getWellKnown(nameOwners, message.get_sender(), sender)) |
| { |
| return; |
| } |
| for (const std::string& interface : interfacesRemoved) |
| { |
| auto interfaceSet = connectionMap->second.find(sender); |
| if (interfaceSet == connectionMap->second.end()) |
| { |
| continue; |
| } |
| |
| if (interface == assocDefsInterface) |
| { |
| removeAssociation(io, objPath.str, sender, server, |
| associationMaps); |
| } |
| |
| interfaceSet->second.erase(interface); |
| |
| if (interfaceSet->second.empty()) |
| { |
| // If this was the last interface on this connection, |
| // erase the connection |
| connectionMap->second.erase(interfaceSet); |
| |
| // Instead of checking if every single path is the endpoint |
| // of an association that needs to be moved to pending, |
| // only check when the only remaining owner of this path is |
| // ourself, which would be because we still own the |
| // association path. |
| if ((connectionMap->second.size() == 1) && |
| (connectionMap->second.begin()->first == |
| "xyz.openbmc_project.ObjectMapper")) |
| { |
| // Remove the 2 association D-Bus paths and move the |
| // association to pending. |
| moveAssociationToPending(io, objPath.str, associationMaps, |
| server); |
| } |
| } |
| } |
| // If this was the last connection on this object path, |
| // erase the object path |
| if (connectionMap->second.empty()) |
| { |
| interfaceMap.erase(connectionMap); |
| } |
| |
| removeUnneededParents(objPath.str, sender, interfaceMap); |
| }; |
| |
| sdbusplus::bus::match_t interfacesRemoved( |
| static_cast<sdbusplus::bus_t&>(*systemBus), |
| sdbusplus::bus::match::rules::interfacesRemoved(), |
| std::move(interfacesRemovedHandler)); |
| |
| auto associationChangedHandler = [&io, &server, &nameOwners, &interfaceMap]( |
| sdbusplus::message_t& message) { |
| std::string objectName; |
| boost::container::flat_map<std::string, |
| std::variant<std::vector<Association>>> |
| values; |
| message.read(objectName, values); |
| auto prop = values.find(assocDefsProperty); |
| if (prop != values.end()) |
| { |
| std::vector<Association> associations = |
| std::get<std::vector<Association>>(prop->second); |
| |
| std::string wellKnown; |
| if (!getWellKnown(nameOwners, message.get_sender(), wellKnown)) |
| { |
| return; |
| } |
| associationChanged(io, server, associations, message.get_path(), |
| wellKnown, interfaceMap, associationMaps); |
| } |
| }; |
| sdbusplus::bus::match_t assocChangedMatch( |
| static_cast<sdbusplus::bus_t&>(*systemBus), |
| sdbusplus::bus::match::rules::interface( |
| "org.freedesktop.DBus.Properties") + |
| sdbusplus::bus::match::rules::member("PropertiesChanged") + |
| sdbusplus::bus::match::rules::argN(0, assocDefsInterface), |
| std::move(associationChangedHandler)); |
| |
| std::shared_ptr<sdbusplus::asio::dbus_interface> iface = |
| server.add_interface("/xyz/openbmc_project/object_mapper", |
| "xyz.openbmc_project.ObjectMapper"); |
| |
| iface->register_method( |
| "GetAncestors", [&interfaceMap](std::string& reqPath, |
| std::vector<std::string>& interfaces) { |
| return getAncestors(interfaceMap, reqPath, interfaces); |
| }); |
| |
| iface->register_method( |
| "GetObject", [&interfaceMap](const std::string& path, |
| std::vector<std::string>& interfaces) { |
| return getObject(interfaceMap, path, interfaces); |
| }); |
| |
| iface->register_method( |
| "GetSubTree", [&interfaceMap](std::string& reqPath, int32_t depth, |
| std::vector<std::string>& interfaces) { |
| return getSubTree(interfaceMap, reqPath, depth, interfaces); |
| }); |
| |
| iface->register_method( |
| "GetSubTreePaths", |
| [&interfaceMap](std::string& reqPath, int32_t depth, |
| std::vector<std::string>& interfaces) { |
| return getSubTreePaths(interfaceMap, reqPath, depth, interfaces); |
| }); |
| |
| iface->register_method( |
| "GetAssociatedSubTree", |
| [&interfaceMap](const sdbusplus::message::object_path& associationPath, |
| const sdbusplus::message::object_path& reqPath, |
| int32_t depth, std::vector<std::string>& interfaces) { |
| return getAssociatedSubTree(interfaceMap, associationMaps, |
| associationPath, reqPath, depth, |
| interfaces); |
| }); |
| |
| iface->register_method( |
| "GetAssociatedSubTreePaths", |
| [&interfaceMap](const sdbusplus::message::object_path& associationPath, |
| const sdbusplus::message::object_path& reqPath, |
| int32_t depth, std::vector<std::string>& interfaces) { |
| return getAssociatedSubTreePaths(interfaceMap, associationMaps, |
| associationPath, reqPath, depth, |
| interfaces); |
| }); |
| |
| iface->initialize(); |
| |
| io.post([&]() { |
| doListNames(io, interfaceMap, systemBus.get(), nameOwners, |
| associationMaps, server); |
| }); |
| |
| systemBus->request_name("xyz.openbmc_project.ObjectMapper"); |
| |
| io.run(); |
| } |