FRU: Invalidate cache when frus change

This creates a match for FruDevice changes and
update the cache when one changes.

Tested:
ipmitool fru list immediately after rescan
had correct values. fru list was faster

Change-Id: I2f332814bd1a0ef38a87dc31b8cd3c16fa6b8d76
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/src/storagecommands.cpp b/src/storagecommands.cpp
index 1887581..c2c1236 100644
--- a/src/storagecommands.cpp
+++ b/src/storagecommands.cpp
@@ -93,20 +93,16 @@
 
 constexpr static const size_t maxMessageSize = 64;
 constexpr static const size_t maxFruSdrNameSize = 16;
-using ManagedObjectType = boost::container::flat_map<
-    sdbusplus::message::object_path,
-    boost::container::flat_map<
-        std::string, boost::container::flat_map<std::string, DbusVariant>>>;
-using ManagedEntry = std::pair<
-    sdbusplus::message::object_path,
-    boost::container::flat_map<
-        std::string, boost::container::flat_map<std::string, DbusVariant>>>;
+using ObjectType = boost::container::flat_map<
+    std::string, boost::container::flat_map<std::string, DbusVariant>>;
+using ManagedObjectType =
+    boost::container::flat_map<sdbusplus::message::object_path, ObjectType>;
+using ManagedEntry = std::pair<sdbusplus::message::object_path, ObjectType>;
 
 constexpr static const char* fruDeviceServiceName =
     "xyz.openbmc_project.FruDevice";
 constexpr static const char* entityManagerServiceName =
     "xyz.openbmc_project.EntityManager";
-constexpr static const size_t cacheTimeoutSeconds = 30;
 constexpr static const size_t writeTimeoutSeconds = 10;
 constexpr static const char* chassisTypeRackMount = "23";
 
@@ -116,12 +112,13 @@
 static std::vector<uint8_t> fruCache;
 static uint8_t cacheBus = 0xFF;
 static uint8_t cacheAddr = 0XFF;
+static uint8_t lastDevId = 0xFF;
 
 static uint8_t writeBus = 0xFF;
 static uint8_t writeAddr = 0XFF;
 
 std::unique_ptr<phosphor::Timer> writeTimer = nullptr;
-std::unique_ptr<phosphor::Timer> cacheTimer = nullptr;
+static std::vector<sdbusplus::bus::match::match> fruMatches;
 
 ManagedObjectType frus;
 
@@ -160,45 +157,13 @@
 
 void createTimers()
 {
-
     writeTimer = std::make_unique<phosphor::Timer>(writeFru);
-    cacheTimer = std::make_unique<phosphor::Timer>([]() { return; });
 }
 
-ipmi::Cc replaceCacheFru(ipmi::Context::ptr ctx, uint8_t devId)
+void recalculateHashes()
 {
-    static uint8_t lastDevId = 0xFF;
-
-    bool timerRunning = (cacheTimer != nullptr) && !cacheTimer->isExpired();
-    if (lastDevId == devId && timerRunning)
-    {
-        return IPMI_CC_OK; // cache already up to date
-    }
-    // if timer is running, stop it and writeFru manually
-    else if (timerRunning)
-    {
-        cacheTimer->stop();
-    }
-
-    cacheTimer->start(std::chrono::duration_cast<std::chrono::microseconds>(
-        std::chrono::seconds(cacheTimeoutSeconds)));
-
-    boost::system::error_code ec;
-
-    frus = ctx->bus->yield_method_call<ManagedObjectType>(
-        ctx->yield, ec, fruDeviceServiceName, "/",
-        "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
-    if (ec)
-    {
-        phosphor::logging::log<phosphor::logging::level::ERR>(
-            "GetMangagedObjects for getSensorMap failed",
-            phosphor::logging::entry("ERROR=%s", ec.message().c_str()));
-
-        return ipmi::ccResponseError;
-    }
 
     deviceHashes.clear();
-
     // hash the object paths to create unique device id's. increment on
     // collision
     std::hash<std::string> hasher;
@@ -259,6 +224,35 @@
             }
         }
     }
+}
+
+void replaceCacheFru(const std::shared_ptr<sdbusplus::asio::connection>& bus,
+                     boost::asio::yield_context& yield,
+                     const std::optional<std::string>& path = std::nullopt)
+{
+    boost::system::error_code ec;
+
+    frus = bus->yield_method_call<ManagedObjectType>(
+        yield, ec, fruDeviceServiceName, "/",
+        "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
+    if (ec)
+    {
+        phosphor::logging::log<phosphor::logging::level::ERR>(
+            "GetMangagedObjects for getSensorMap failed",
+            phosphor::logging::entry("ERROR=%s", ec.message().c_str()));
+
+        return;
+    }
+    recalculateHashes();
+}
+
+ipmi::Cc getFru(ipmi::Context::ptr ctx, uint8_t devId)
+{
+    if (lastDevId == devId && devId != 0xFF)
+    {
+        return ipmi::ccSuccess;
+    }
+
     auto deviceFind = deviceHashes.find(devId);
     if (deviceFind == deviceHashes.end())
     {
@@ -266,11 +260,12 @@
     }
 
     fruCache.clear();
-    ec.clear();
 
     cacheBus = deviceFind->second.first;
     cacheAddr = deviceFind->second.second;
 
+    boost::system::error_code ec;
+
     fruCache = ctx->bus->yield_method_call<std::vector<uint8_t>>(
         ctx->yield, ec, fruDeviceServiceName, "/xyz/openbmc_project/FruDevice",
         "xyz.openbmc_project.FruDeviceManager", "GetRawFru", cacheBus,
@@ -281,7 +276,6 @@
             "Couldn't get raw fru",
             phosphor::logging::entry("ERROR=%s", ec.message().c_str()));
 
-        lastDevId = 0xFF;
         cacheBus = 0xFF;
         cacheAddr = 0xFF;
         return ipmi::ccResponseError;
@@ -291,6 +285,84 @@
     return ipmi::ccSuccess;
 }
 
+void writeFruIfRunning()
+{
+    if (!writeTimer->isRunning())
+    {
+        return;
+    }
+    writeTimer->stop();
+    writeFru();
+}
+
+void startMatch(void)
+{
+    if (fruMatches.size())
+    {
+        return;
+    }
+
+    fruMatches.reserve(2);
+
+    auto bus = getSdBus();
+    fruMatches.emplace_back(*bus,
+                            "type='signal',arg0path='/xyz/openbmc_project/"
+                            "FruDevice/',member='InterfacesAdded'",
+                            [](sdbusplus::message::message& message) {
+                                sdbusplus::message::object_path path;
+                                ObjectType object;
+                                try
+                                {
+                                    message.read(path, object);
+                                }
+                                catch (sdbusplus::exception_t&)
+                                {
+                                    return;
+                                }
+                                auto findType = object.find(
+                                    "xyz.openbmc_project.FruDevice");
+                                if (findType == object.end())
+                                {
+                                    return;
+                                }
+                                writeFruIfRunning();
+                                frus[path] = object;
+                                recalculateHashes();
+                                lastDevId = 0xFF;
+                            });
+
+    fruMatches.emplace_back(*bus,
+                            "type='signal',arg0path='/xyz/openbmc_project/"
+                            "FruDevice/',member='InterfacesRemoved'",
+                            [](sdbusplus::message::message& message) {
+                                sdbusplus::message::object_path path;
+                                std::set<std::string> interfaces;
+                                try
+                                {
+                                    message.read(path, interfaces);
+                                }
+                                catch (sdbusplus::exception_t&)
+                                {
+                                    return;
+                                }
+                                auto findType = interfaces.find(
+                                    "xyz.openbmc_project.FruDevice");
+                                if (findType == interfaces.end())
+                                {
+                                    return;
+                                }
+                                writeFruIfRunning();
+                                frus.erase(path);
+                                recalculateHashes();
+                                lastDevId = 0xFF;
+                            });
+
+    // call once to populate
+    boost::asio::spawn(*getIoContext(), [](boost::asio::yield_context yield) {
+        replaceCacheFru(getSdBus(), yield);
+    });
+}
+
 /** @brief implements the read FRU data command
  *  @param fruDeviceId        - FRU Device ID
  *  @param fruInventoryOffset - FRU Inventory Offset to write
@@ -310,7 +382,7 @@
         return ipmi::responseInvalidFieldRequest();
     }
 
-    ipmi::Cc status = replaceCacheFru(ctx, fruDeviceId);
+    ipmi::Cc status = getFru(ctx, fruDeviceId);
 
     if (status != ipmi::ccSuccess)
     {
@@ -361,7 +433,7 @@
 
     size_t writeLen = dataToWrite.size();
 
-    ipmi::Cc status = replaceCacheFru(ctx, fruDeviceId);
+    ipmi::Cc status = getFru(ctx, fruDeviceId);
     if (status != ipmi::ccSuccess)
     {
         return ipmi::response(status);
@@ -466,12 +538,7 @@
         return ipmi::responseInvalidFieldRequest();
     }
 
-    ipmi::Cc status = replaceCacheFru(ctx, fruDeviceId);
-
-    if (status != IPMI_CC_OK)
-    {
-        return ipmi::response(status);
-    }
+    getFru(ctx, fruDeviceId);
 
     constexpr uint8_t accessType =
         static_cast<uint8_t>(GetFRUAreaAccessType::byte);
@@ -481,11 +548,6 @@
 
 ipmi_ret_t getFruSdrCount(ipmi::Context::ptr ctx, size_t& count)
 {
-    ipmi_ret_t ret = replaceCacheFru(ctx, 0);
-    if (ret != IPMI_CC_OK)
-    {
-        return ret;
-    }
     count = deviceHashes.size();
     return IPMI_CC_OK;
 }
@@ -493,11 +555,6 @@
 ipmi_ret_t getFruSdrs(ipmi::Context::ptr ctx, size_t index,
                       get_sdr::SensorDataFruRecord& resp)
 {
-    ipmi_ret_t ret = replaceCacheFru(ctx, 0); // this will update the hash list
-    if (ret != IPMI_CC_OK)
-    {
-        return ret;
-    }
     if (deviceHashes.size() < index)
     {
         return IPMI_CC_INVALID_FIELD_REQUEST;
@@ -1244,6 +1301,8 @@
 void registerStorageFunctions()
 {
     createTimers();
+    startMatch();
+
     // <Get FRU Inventory Area Info>
     ipmi::registerHandler(ipmi::prioOemBase, ipmi::netFnStorage,
                           ipmi::storage::cmdGetFruInventoryAreaInfo,