1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3
4 #include "entity_manager.hpp"
5
6 #include "../utils.hpp"
7 #include "../variant_visitors.hpp"
8 #include "configuration.hpp"
9 #include "dbus_interface.hpp"
10 #include "log_device_inventory.hpp"
11 #include "overlay.hpp"
12 #include "perform_scan.hpp"
13 #include "topology.hpp"
14 #include "utils.hpp"
15
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/post.hpp>
18 #include <boost/asio/steady_timer.hpp>
19 #include <boost/container/flat_set.hpp>
20 #include <boost/range/iterator_range.hpp>
21 #include <nlohmann/json.hpp>
22 #include <phosphor-logging/lg2.hpp>
23 #include <sdbusplus/asio/connection.hpp>
24 #include <sdbusplus/asio/object_server.hpp>
25 #include <xyz/openbmc_project/Association/Definitions/common.hpp>
26 #include <xyz/openbmc_project/Inventory/Item/Bmc/common.hpp>
27 #include <xyz/openbmc_project/Inventory/Item/System/common.hpp>
28 #include <xyz/openbmc_project/Inventory/Item/common.hpp>
29
30 #include <filesystem>
31 #include <flat_map>
32 #include <fstream>
33 #include <functional>
34 #include <map>
35 #include <regex>
36 constexpr const char* tempConfigDir = "/tmp/configuration/";
37 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
38
39 static constexpr std::array<const char*, 6> settableInterfaces = {
40 "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
41
42 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
43 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
44
getPermission(const std::string & interface)45 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
46 {
47 return std::find(settableInterfaces.begin(), settableInterfaces.end(),
48 interface) != settableInterfaces.end()
49 ? sdbusplus::asio::PropertyPermission::readWrite
50 : sdbusplus::asio::PropertyPermission::readOnly;
51 }
52
EntityManager(std::shared_ptr<sdbusplus::asio::connection> & systemBus,boost::asio::io_context & io,const std::vector<std::filesystem::path> & configurationDirectories,const std::filesystem::path & schemaDirectory)53 EntityManager::EntityManager(
54 std::shared_ptr<sdbusplus::asio::connection>& systemBus,
55 boost::asio::io_context& io,
56 const std::vector<std::filesystem::path>& configurationDirectories,
57 const std::filesystem::path& schemaDirectory) :
58 systemBus(systemBus),
59 objServer(sdbusplus::asio::object_server(systemBus, /*skipManager=*/true)),
60 configuration(configurationDirectories, schemaDirectory),
61 lastJson(nlohmann::json::object()),
62 systemConfiguration(nlohmann::json::object()), io(io),
63 dbus_interface(io, objServer, schemaDirectory), powerStatus(*systemBus),
64 propertiesChangedTimer(io)
65 {
66 // All other objects that EntityManager currently support are under the
67 // inventory subtree.
68 // See the discussion at
69 // https://discord.com/channels/775381525260664832/1018929092009144380
70 objServer.add_manager("/xyz/openbmc_project/inventory");
71
72 entityIface = objServer.add_interface("/xyz/openbmc_project/EntityManager",
73 "xyz.openbmc_project.EntityManager");
74 entityIface->register_method("ReScan", [this]() {
75 propertiesChangedCallback();
76 });
77 dbus_interface::tryIfaceInitialize(entityIface);
78
79 initFilters(configuration.probeInterfaces);
80 }
81
postToDbus(const nlohmann::json & newConfiguration)82 void EntityManager::postToDbus(const nlohmann::json& newConfiguration)
83 {
84 std::map<std::string, std::string> newBoards; // path -> name
85
86 // iterate through boards
87 for (const auto& [boardId, boardConfig] : newConfiguration.items())
88 {
89 const nlohmann::json::object_t* boardConfigPtr =
90 boardConfig.get_ptr<const nlohmann::json::object_t*>();
91 if (boardConfigPtr == nullptr)
92 {
93 lg2::error("boardConfig for {BOARD} was not an object", "BOARD",
94 boardId);
95 continue;
96 }
97 postBoardToDBus(boardId, *boardConfigPtr, newBoards);
98 }
99
100 for (const auto& [assocPath, assocPropValue] :
101 topology.getAssocs(std::views::keys(newBoards)))
102 {
103 auto findBoard = newBoards.find(assocPath);
104 if (findBoard == newBoards.end())
105 {
106 continue;
107 }
108
109 auto ifacePtr = dbus_interface.createInterface(
110 assocPath,
111 sdbusplus::common::xyz::openbmc_project::association::Definitions::
112 interface,
113 findBoard->second);
114
115 ifacePtr->register_property("Associations", assocPropValue);
116 dbus_interface::tryIfaceInitialize(ifacePtr);
117 }
118 }
119
postBoardToDBus(const std::string & boardId,const nlohmann::json::object_t & boardConfig,std::map<std::string,std::string> & newBoards)120 void EntityManager::postBoardToDBus(
121 const std::string& boardId, const nlohmann::json::object_t& boardConfig,
122 std::map<std::string, std::string>& newBoards)
123 {
124 auto boardNameIt = boardConfig.find("Name");
125 if (boardNameIt == boardConfig.end())
126 {
127 lg2::error("Unable to find name for {BOARD}", "BOARD", boardId);
128 return;
129 }
130 const std::string* boardNamePtr =
131 boardNameIt->second.get_ptr<const std::string*>();
132 if (boardNamePtr == nullptr)
133 {
134 lg2::error("Name for {BOARD} was not a string", "BOARD", boardId);
135 return;
136 }
137 std::string boardName = *boardNamePtr;
138 std::string boardNameOrig = *boardNamePtr;
139 std::string jsonPointerPath = "/" + boardId;
140 // loop through newConfiguration, but use values from system
141 // configuration to be able to modify via dbus later
142 auto boardValues = systemConfiguration[boardId];
143 auto findBoardType = boardValues.find("Type");
144 std::string boardType;
145 if (findBoardType != boardValues.end() &&
146 findBoardType->type() == nlohmann::json::value_t::string)
147 {
148 boardType = findBoardType->get<std::string>();
149 std::regex_replace(boardType.begin(), boardType.begin(),
150 boardType.end(), illegalDbusMemberRegex, "_");
151 }
152 else
153 {
154 lg2::error("Unable to find type for {BOARD} reverting to Chassis.",
155 "BOARD", boardName);
156 boardType = "Chassis";
157 }
158
159 lg2::debug("post {TYPE} '{NAME}' to DBus", "TYPE", boardType, "NAME",
160 boardName);
161
162 const std::string boardPath =
163 em_utils::buildInventorySystemPath(boardName, boardType);
164
165 std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
166 dbus_interface.createInterface(
167 boardPath,
168 sdbusplus::common::xyz::openbmc_project::inventory::Item::interface,
169 boardName);
170
171 const std::string invItemIntf = std::format(
172 "{}.{}",
173 sdbusplus::common::xyz::openbmc_project::inventory::Item::interface,
174 boardType);
175
176 std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
177 dbus_interface.createInterface(boardPath, invItemIntf, boardNameOrig);
178
179 dbus_interface.createAddObjectMethod(jsonPointerPath, boardPath,
180 systemConfiguration, boardNameOrig);
181
182 dbus_interface.populateInterfaceFromJson(
183 systemConfiguration, jsonPointerPath, boardIface, boardValues);
184 jsonPointerPath += "/";
185 // iterate through board properties
186 for (const auto& [propName, propValue] : boardValues.items())
187 {
188 if (propValue.type() == nlohmann::json::value_t::object)
189 {
190 std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
191 dbus_interface.createInterface(boardPath, propName,
192 boardNameOrig);
193
194 dbus_interface.populateInterfaceFromJson(
195 systemConfiguration, jsonPointerPath + propName, iface,
196 propValue);
197 }
198 }
199
200 nlohmann::json::iterator exposes = boardValues.find("Exposes");
201 if (exposes == boardValues.end())
202 {
203 return;
204 }
205 // iterate through exposes
206 jsonPointerPath += "Exposes/";
207
208 // store the board level pointer so we can modify it on the way down
209 std::string jsonPointerPathBoard = jsonPointerPath;
210 size_t exposesIndex = -1;
211 for (nlohmann::json& item : *exposes)
212 {
213 postExposesRecordsToDBus(item, exposesIndex, boardNameOrig,
214 jsonPointerPath, jsonPointerPathBoard,
215 boardPath, boardType);
216 }
217
218 newBoards.emplace(boardPath, boardNameOrig);
219 }
220
postExposesRecordsToDBus(nlohmann::json & item,size_t & exposesIndex,const std::string & boardNameOrig,std::string jsonPointerPath,const std::string & jsonPointerPathBoard,const std::string & boardPath,const std::string & boardType)221 void EntityManager::postExposesRecordsToDBus(
222 nlohmann::json& item, size_t& exposesIndex,
223 const std::string& boardNameOrig, std::string jsonPointerPath,
224 const std::string& jsonPointerPathBoard, const std::string& boardPath,
225 const std::string& boardType)
226 {
227 exposesIndex++;
228 jsonPointerPath = jsonPointerPathBoard;
229 jsonPointerPath += std::to_string(exposesIndex);
230
231 auto findName = item.find("Name");
232 if (findName == item.end())
233 {
234 lg2::error("cannot find name in field {ITEM}", "ITEM", item);
235 return;
236 }
237 auto findStatus = item.find("Status");
238 // if status is not found it is assumed to be status = 'okay'
239 if (findStatus != item.end())
240 {
241 if (*findStatus == "disabled")
242 {
243 return;
244 }
245 }
246 auto findType = item.find("Type");
247 std::string itemType;
248 if (findType != item.end())
249 {
250 itemType = findType->get<std::string>();
251 std::regex_replace(itemType.begin(), itemType.begin(), itemType.end(),
252 illegalDbusPathRegex, "_");
253 }
254 else
255 {
256 itemType = "unknown";
257 }
258 std::string itemName = findName->get<std::string>();
259 std::regex_replace(itemName.begin(), itemName.begin(), itemName.end(),
260 illegalDbusMemberRegex, "_");
261 std::string ifacePath = boardPath;
262 ifacePath += "/";
263 ifacePath += itemName;
264
265 if (itemType == "BMC")
266 {
267 std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
268 dbus_interface.createInterface(
269 ifacePath,
270 sdbusplus::common::xyz::openbmc_project::inventory::item::Bmc::
271 interface,
272 boardNameOrig);
273 dbus_interface.populateInterfaceFromJson(
274 systemConfiguration, jsonPointerPath, bmcIface, item,
275 getPermission(itemType));
276 }
277 else if (itemType == "System")
278 {
279 std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
280 dbus_interface.createInterface(
281 ifacePath,
282 sdbusplus::common::xyz::openbmc_project::inventory::item::
283 System::interface,
284 boardNameOrig);
285 dbus_interface.populateInterfaceFromJson(
286 systemConfiguration, jsonPointerPath, systemIface, item,
287 getPermission(itemType));
288 }
289
290 for (const auto& [name, config] : item.items())
291 {
292 jsonPointerPath = jsonPointerPathBoard;
293 jsonPointerPath.append(std::to_string(exposesIndex))
294 .append("/")
295 .append(name);
296
297 if (!postConfigurationRecord(name, config, boardNameOrig, itemType,
298 jsonPointerPath, ifacePath))
299 {
300 break;
301 }
302 }
303
304 std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
305 dbus_interface.createInterface(
306 ifacePath, "xyz.openbmc_project.Configuration." + itemType,
307 boardNameOrig);
308
309 dbus_interface.populateInterfaceFromJson(
310 systemConfiguration, jsonPointerPath, itemIface, item,
311 getPermission(itemType));
312
313 topology.addBoard(boardPath, boardType, boardNameOrig, item);
314 }
315
postConfigurationRecord(const std::string & name,nlohmann::json & config,const std::string & boardNameOrig,const std::string & itemType,const std::string & jsonPointerPath,const std::string & ifacePath)316 bool EntityManager::postConfigurationRecord(
317 const std::string& name, nlohmann::json& config,
318 const std::string& boardNameOrig, const std::string& itemType,
319 const std::string& jsonPointerPath, const std::string& ifacePath)
320 {
321 if (config.type() == nlohmann::json::value_t::object)
322 {
323 std::string ifaceName = "xyz.openbmc_project.Configuration.";
324 ifaceName.append(itemType).append(".").append(name);
325
326 std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
327 dbus_interface.createInterface(ifacePath, ifaceName, boardNameOrig);
328
329 dbus_interface.populateInterfaceFromJson(
330 systemConfiguration, jsonPointerPath, objectIface, config,
331 getPermission(name));
332 }
333 else if (config.type() == nlohmann::json::value_t::array)
334 {
335 size_t index = 0;
336 if (config.empty())
337 {
338 return true;
339 }
340 bool isLegal = true;
341 auto type = config[0].type();
342 if (type != nlohmann::json::value_t::object)
343 {
344 return true;
345 }
346
347 // verify legal json
348 for (const auto& arrayItem : config)
349 {
350 if (arrayItem.type() != type)
351 {
352 isLegal = false;
353 break;
354 }
355 }
356 if (!isLegal)
357 {
358 lg2::error("dbus format error {JSON}", "JSON", config);
359 return false;
360 }
361
362 for (auto& arrayItem : config)
363 {
364 std::string ifaceName = "xyz.openbmc_project.Configuration.";
365 ifaceName.append(itemType).append(".").append(name);
366 ifaceName.append(std::to_string(index));
367
368 std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
369 dbus_interface.createInterface(ifacePath, ifaceName,
370 boardNameOrig);
371
372 dbus_interface.populateInterfaceFromJson(
373 systemConfiguration,
374 jsonPointerPath + "/" + std::to_string(index), objectIface,
375 arrayItem, getPermission(name));
376 index++;
377 }
378 }
379
380 return true;
381 }
382
deviceRequiresPowerOn(const nlohmann::json & entity)383 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
384 {
385 auto powerState = entity.find("PowerState");
386 if (powerState == entity.end())
387 {
388 return false;
389 }
390
391 const auto* ptr = powerState->get_ptr<const std::string*>();
392 if (ptr == nullptr)
393 {
394 return false;
395 }
396
397 return *ptr == "On" || *ptr == "BiosPost";
398 }
399
pruneDevice(const nlohmann::json & systemConfiguration,const bool powerOff,const bool scannedPowerOff,const std::string & name,const nlohmann::json & device)400 static void pruneDevice(const nlohmann::json& systemConfiguration,
401 const bool powerOff, const bool scannedPowerOff,
402 const std::string& name, const nlohmann::json& device)
403 {
404 if (systemConfiguration.contains(name))
405 {
406 return;
407 }
408
409 if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
410 {
411 return;
412 }
413
414 logDeviceRemoved(device);
415 }
416
startRemovedTimer(boost::asio::steady_timer & timer)417 void EntityManager::startRemovedTimer(boost::asio::steady_timer& timer)
418 {
419 if (systemConfiguration.empty() || lastJson.empty())
420 {
421 return; // not ready yet
422 }
423 if (scannedPowerOn)
424 {
425 return;
426 }
427
428 if (!powerStatus.isPowerOn() && scannedPowerOff)
429 {
430 return;
431 }
432
433 timer.expires_after(std::chrono::seconds(10));
434 timer.async_wait([this](const boost::system::error_code& ec) {
435 if (ec == boost::asio::error::operation_aborted)
436 {
437 return;
438 }
439
440 bool powerOff = !powerStatus.isPowerOn();
441 for (const auto& [name, device] : lastJson.items())
442 {
443 pruneDevice(systemConfiguration, powerOff, scannedPowerOff, name,
444 device);
445 }
446
447 scannedPowerOff = true;
448 if (!powerOff)
449 {
450 scannedPowerOn = true;
451 }
452 });
453 }
454
pruneConfiguration(bool powerOff,const std::string & name,const nlohmann::json & device)455 void EntityManager::pruneConfiguration(bool powerOff, const std::string& name,
456 const nlohmann::json& device)
457 {
458 lg2::debug("pruning configuration");
459
460 if (powerOff && deviceRequiresPowerOn(device))
461 {
462 // power not on yet, don't know if it's there or not
463 return;
464 }
465
466 auto& ifaces = dbus_interface.getDeviceInterfaces(device);
467 for (auto& iface : ifaces)
468 {
469 auto sharedPtr = iface.lock();
470 if (!!sharedPtr)
471 {
472 objServer.remove_interface(sharedPtr);
473 }
474 }
475
476 ifaces.clear();
477 systemConfiguration.erase(name);
478 topology.remove(device["Name"].get<std::string>());
479 logDeviceRemoved(device);
480 }
481
publishNewConfiguration(const size_t & instance,const size_t count,boost::asio::steady_timer & timer,const nlohmann::json newConfiguration)482 void EntityManager::publishNewConfiguration(
483 const size_t& instance, const size_t count,
484 boost::asio::steady_timer& timer, // Gerrit discussion:
485 // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
486 //
487 // Discord discussion:
488 // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
489 //
490 // NOLINTNEXTLINE(performance-unnecessary-value-param)
491 const nlohmann::json newConfiguration)
492 {
493 loadOverlays(newConfiguration, io);
494
495 boost::asio::post(io, [this]() {
496 if (!writeJsonFiles(systemConfiguration))
497 {
498 lg2::error("Error writing json files");
499 }
500 });
501
502 boost::asio::post(io, [this, &instance, count, &timer, newConfiguration]() {
503 postToDbus(newConfiguration);
504 if (count == instance)
505 {
506 startRemovedTimer(timer);
507 }
508 });
509 }
510
511 // main properties changed entry
propertiesChangedCallback()512 void EntityManager::propertiesChangedCallback()
513 {
514 lg2::debug("properties changed callback");
515 propertiesChangedInstance++;
516 size_t count = propertiesChangedInstance;
517
518 propertiesChangedTimer.expires_after(std::chrono::milliseconds(500));
519
520 // setup an async wait as we normally get flooded with new requests
521 propertiesChangedTimer.async_wait(
522 [this, count](const boost::system::error_code& ec) {
523 lg2::debug("properties changed callback timer expired");
524 if (ec == boost::asio::error::operation_aborted)
525 {
526 // we were cancelled
527 return;
528 }
529 if (ec)
530 {
531 lg2::error("async wait error {ERR}", "ERR", ec.message());
532 return;
533 }
534
535 if (propertiesChangedInProgress)
536 {
537 propertiesChangedCallback();
538 return;
539 }
540 propertiesChangedInProgress = true;
541
542 lg2::debug("properties changed callback in progress");
543
544 nlohmann::json oldConfiguration = systemConfiguration;
545 auto missingConfigurations = std::make_shared<nlohmann::json>();
546 *missingConfigurations = systemConfiguration;
547
548 auto perfScan = std::make_shared<scan::PerformScan>(
549 *this, *missingConfigurations, configuration.configurations, io,
550 [this, count, oldConfiguration, missingConfigurations]() {
551 // this is something that since ac has been applied to the
552 // bmc we saw, and we no longer see it
553 bool powerOff = !powerStatus.isPowerOn();
554 for (const auto& [name, device] :
555 missingConfigurations->items())
556 {
557 pruneConfiguration(powerOff, name, device);
558 }
559 nlohmann::json newConfiguration = systemConfiguration;
560
561 deriveNewConfiguration(oldConfiguration, newConfiguration);
562
563 for (const auto& [_, device] : newConfiguration.items())
564 {
565 logDeviceAdded(device);
566 }
567
568 propertiesChangedInProgress = false;
569
570 boost::asio::post(io, [this, newConfiguration, count] {
571 publishNewConfiguration(
572 std::ref(propertiesChangedInstance), count,
573 std::ref(propertiesChangedTimer), newConfiguration);
574 });
575 });
576 perfScan->run();
577 });
578 }
579
580 // Check if InterfacesAdded payload contains an iface that needs probing.
iaContainsProbeInterface(sdbusplus::message_t & msg,const std::unordered_set<std::string> & probeInterfaces)581 static bool iaContainsProbeInterface(
582 sdbusplus::message_t& msg,
583 const std::unordered_set<std::string>& probeInterfaces)
584 {
585 sdbusplus::message::object_path path;
586 DBusObject interfaces;
587 msg.read(path, interfaces);
588 return std::ranges::any_of(interfaces | std::views::keys,
589 [&probeInterfaces](const auto& ifaceName) {
590 return probeInterfaces.contains(ifaceName);
591 });
592 }
593
594 // Check if InterfacesRemoved payload contains an iface that needs probing.
irContainsProbeInterface(const std::vector<std::string> & interfaces,const std::unordered_set<std::string> & probeInterfaces)595 static bool irContainsProbeInterface(
596 const std::vector<std::string>& interfaces,
597 const std::unordered_set<std::string>& probeInterfaces)
598 {
599 return std::ranges::any_of(interfaces,
600 [&probeInterfaces](const auto& ifaceName) {
601 return probeInterfaces.contains(ifaceName);
602 });
603 }
604
handleCurrentConfigurationJson()605 void EntityManager::handleCurrentConfigurationJson()
606 {
607 if (EM_CACHE_CONFIGURATION && em_utils::fwVersionIsSame())
608 {
609 if (std::filesystem::is_regular_file(currentConfiguration))
610 {
611 // this file could just be deleted, but it's nice for debug
612 std::filesystem::create_directory(tempConfigDir);
613 std::filesystem::remove(lastConfiguration);
614 std::filesystem::copy(currentConfiguration, lastConfiguration);
615 std::filesystem::remove(currentConfiguration);
616
617 std::ifstream jsonStream(lastConfiguration);
618 if (jsonStream.good())
619 {
620 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
621 if (data.is_discarded())
622 {
623 lg2::error("syntax error in {PATH}", "PATH",
624 lastConfiguration);
625 }
626 else
627 {
628 lastJson = std::move(data);
629 }
630 }
631 else
632 {
633 lg2::error("unable to open {PATH}", "PATH", lastConfiguration);
634 }
635 }
636 }
637 else
638 {
639 // not an error, just logging at this level to make it in the journal
640 std::error_code ec;
641 lg2::error("Clearing previous configuration");
642 std::filesystem::remove(currentConfiguration, ec);
643 }
644 }
645
registerCallback(const std::string & path)646 void EntityManager::registerCallback(const std::string& path)
647 {
648 if (dbusMatches.contains(path))
649 {
650 return;
651 }
652
653 lg2::debug("creating PropertiesChanged match on {PATH}", "PATH", path);
654
655 std::function<void(sdbusplus::message_t & message)> eventHandler =
656 [&](sdbusplus::message_t&) { propertiesChangedCallback(); };
657
658 sdbusplus::bus::match_t match(
659 static_cast<sdbusplus::bus_t&>(*systemBus),
660 "type='signal',member='PropertiesChanged',path='" + path + "'",
661 eventHandler);
662 dbusMatches.emplace(path, std::move(match));
663 }
664
665 // We need a poke from DBus for static providers that create all their
666 // objects prior to claiming a well-known name, and thus don't emit any
667 // org.freedesktop.DBus.Properties signals. Similarly if a process exits
668 // for any reason, expected or otherwise, we'll need a poke to remove
669 // entities from DBus.
initFilters(const std::unordered_set<std::string> & probeInterfaces)670 void EntityManager::initFilters(
671 const std::unordered_set<std::string>& probeInterfaces)
672 {
673 nameOwnerChangedMatch = std::make_unique<sdbusplus::bus::match_t>(
674 static_cast<sdbusplus::bus_t&>(*systemBus),
675 sdbusplus::bus::match::rules::nameOwnerChanged(),
676 [this](sdbusplus::message_t& m) {
677 auto [name, oldOwner,
678 newOwner] = m.unpack<std::string, std::string, std::string>();
679
680 if (name.starts_with(':'))
681 {
682 // We should do nothing with unique-name connections.
683 return;
684 }
685
686 propertiesChangedCallback();
687 });
688
689 // We also need a poke from DBus when new interfaces are created or
690 // destroyed.
691 interfacesAddedMatch = std::make_unique<sdbusplus::bus::match_t>(
692 static_cast<sdbusplus::bus_t&>(*systemBus),
693 sdbusplus::bus::match::rules::interfacesAdded(),
694 [this, probeInterfaces](sdbusplus::message_t& msg) {
695 if (iaContainsProbeInterface(msg, probeInterfaces))
696 {
697 propertiesChangedCallback();
698 }
699 });
700
701 interfacesRemovedMatch = std::make_unique<sdbusplus::bus::match_t>(
702 static_cast<sdbusplus::bus_t&>(*systemBus),
703 sdbusplus::bus::match::rules::interfacesRemoved(),
704 [this, probeInterfaces](sdbusplus::message_t& msg) {
705 auto [path, interfaces] =
706 msg.unpack<sdbusplus::message::object_path,
707 std::vector<std::string>>();
708
709 if (irContainsProbeInterface(interfaces, probeInterfaces))
710 {
711 // Clean up match on probe interface removal to avoid leaks
712 dbusMatches.erase(path.str);
713 propertiesChangedCallback();
714 }
715 });
716 }
717