frudevice: add support for eeprom sysfs when found

If there is an eeprom sysfs entry for a device at the address, use that
instead of raw i2c accesses.

Tested: Verified that if there is an eeprom device on the bus it is
properly added to dbus, while other devices continue to be raw detected.

Tested: Verified that if the eeprom device is found after other devices
are raw scanned, the eeprom driver can fail.  Therefore, moved the code
from a per address pre-check to a per bus pre-check.

Tested: Verified in a mixed environment it found both eeprom-driver
based FRUs and the probed FRUs.

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: Id71c3bf07e5fbaf8d01540ecd694f8177b81685c
diff --git a/src/FruDevice.cpp b/src/FruDevice.cpp
index 1a389e5..a7fe02b 100644
--- a/src/FruDevice.cpp
+++ b/src/FruDevice.cpp
@@ -27,11 +27,14 @@
 #include <filesystem>
 #include <fstream>
 #include <future>
+#include <iomanip>
 #include <iostream>
 #include <nlohmann/json.hpp>
 #include <regex>
 #include <sdbusplus/asio/connection.hpp>
 #include <sdbusplus/asio/object_server.hpp>
+#include <set>
+#include <sstream>
 #include <string>
 #include <thread>
 #include <variant>
@@ -66,6 +69,42 @@
 
 static BusMap busMap;
 
+// Given a bus/address, produce the path in sysfs for an eeprom.
+static std::string getEepromPath(size_t bus, size_t address)
+{
+    std::stringstream output;
+    output << "/sys/bus/i2c/devices/" << bus << "-" << std::right
+           << std::setfill('0') << std::setw(4) << std::hex << address
+           << "/eeprom";
+    return output.str();
+}
+
+static bool hasEepromFile(size_t bus, size_t address)
+{
+    auto path = getEepromPath(bus, address);
+    try
+    {
+        return fs::exists(path);
+    }
+    catch (...)
+    {
+        return false;
+    }
+}
+
+static ssize_t readFromEeprom(int fd, uint16_t offset, uint8_t len,
+                              uint8_t* buf)
+{
+    auto result = lseek(fd, offset, SEEK_SET);
+    if (result < 0)
+    {
+        std::cerr << "failed to seek\n";
+        return -1;
+    }
+
+    return read(fd, buf, len);
+}
+
 static bool isMuxBus(size_t bus)
 {
     return is_symlink(std::filesystem::path(
@@ -165,6 +204,154 @@
     return true;
 }
 
+// TODO: This code is very similar to the non-eeprom version and can be merged
+// with some tweaks.
+static std::vector<char> processEeprom(int bus, int address)
+{
+    std::vector<char> device;
+    std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> block_data;
+
+    auto path = getEepromPath(bus, address);
+
+    int file = open(path.c_str(), O_RDONLY);
+    if (file < 0)
+    {
+        std::cerr << "Unable to open eeprom file: " << path << "\n";
+        return device;
+    }
+
+    ssize_t readBytes = readFromEeprom(file, 0, 0x8, block_data.data());
+    if (readBytes < 0)
+    {
+        std::cerr << "failed to read eeprom at " << bus << " address "
+                  << address << "\n";
+        close(file);
+        return device;
+    }
+
+    if (!validateHeader(block_data))
+    {
+        if (DEBUG)
+        {
+            std::cerr << "Illegal header at bus " << bus << " address "
+                      << address << "\n";
+        }
+
+        close(file);
+        return device;
+    }
+
+    device.insert(device.end(), block_data.begin(), block_data.begin() + 8);
+
+    for (size_t jj = 1; jj <= FRU_AREAS.size(); jj++)
+    {
+        auto area_offset = device[jj];
+        if (area_offset == 0)
+        {
+            continue;
+        }
+
+        area_offset = static_cast<char>(area_offset * 8);
+        if (readFromEeprom(file, area_offset, 0x8, block_data.data()) < 0)
+        {
+            std::cerr << "failed to read bus " << bus << " address " << address
+                      << "\n";
+            device.clear();
+            close(file);
+            return device;
+        }
+
+        int length = block_data[1] * 8;
+        device.insert(device.end(), block_data.begin(), block_data.begin() + 8);
+        length -= 8;
+        area_offset = static_cast<char>(area_offset + 8);
+
+        while (length > 0)
+        {
+            auto to_get = std::min(0x20, length);
+
+            if (readFromEeprom(file, area_offset, static_cast<uint8_t>(to_get),
+                               block_data.data()) < 0)
+            {
+                std::cerr << "failed to read bus " << bus << " address "
+                          << address << "\n";
+                device.clear();
+                close(file);
+                return device;
+            }
+
+            device.insert(device.end(), block_data.begin(),
+                          block_data.begin() + to_get);
+            area_offset = static_cast<char>(area_offset + to_get);
+            length -= to_get;
+        }
+    }
+
+    close(file);
+    return device;
+}
+
+std::set<int> findI2CEeproms(int i2cBus, std::shared_ptr<DeviceMap> devices)
+{
+    std::set<int> foundList;
+
+    std::string path = "/sys/bus/i2c/devices/i2c-" + std::to_string(i2cBus);
+
+    // For each file listed under the i2c device
+    // NOTE: This should be faster than just checking for each possible address
+    // path.
+    for (const auto& p : fs::directory_iterator(path))
+    {
+        const std::string node = p.path().string();
+        std::smatch m;
+        bool found =
+            std::regex_match(node, m, std::regex(".+\\d+-([0-9abcdef]+$)"));
+
+        if (!found)
+        {
+            continue;
+        }
+        if (m.size() != 2)
+        {
+            std::cerr << "regex didn't capture\n";
+            continue;
+        }
+
+        std::ssub_match subMatch = m[1];
+        std::string addressString = subMatch.str();
+
+        std::size_t ignored;
+        const int hexBase = 16;
+        int address = std::stoi(addressString, &ignored, hexBase);
+
+        const std::string eeprom = node + "/eeprom";
+
+        try
+        {
+            if (!fs::exists(eeprom))
+            {
+                continue;
+            }
+        }
+        catch (...)
+        {
+            continue;
+        }
+
+        // There is an eeprom file at this address, it may have invalid
+        // contents, but we found it.
+        foundList.insert(address);
+
+        std::vector<char> device = processEeprom(i2cBus, address);
+        if (!device.empty())
+        {
+            devices->emplace(address, device);
+        }
+    }
+
+    return foundList;
+}
+
 int get_bus_frus(int file, int first, int last, int bus,
                  std::shared_ptr<DeviceMap> devices)
 {
@@ -172,8 +359,24 @@
     std::future<int> future = std::async(std::launch::async, [&]() {
         std::array<uint8_t, I2C_SMBUS_BLOCK_MAX> block_data;
 
+        // NOTE: When reading the devices raw on the bus, it can interfere with
+        // the driver's ability to operate, therefore read eeproms first before
+        // scanning for devices without drivers. Several experiments were run
+        // and it was determined that if there were any devices on the bus
+        // before the eeprom was hit and read, the eeprom driver wouldn't open
+        // while the bus device was open. An experiment was not performed to see
+        // if this issue was resolved if the i2c bus device was closed, but
+        // hexdumps of the eeprom later were successful.
+
+        // Scan for i2c eeproms loaded on this bus.
+        std::set<int> skipList = findI2CEeproms(bus, devices);
+
         for (int ii = first; ii <= last; ii++)
         {
+            if (skipList.find(ii) != skipList.end())
+            {
+                continue;
+            }
 
             // Set slave address
             if (ioctl(file, I2C_SLAVE_FORCE, ii) < 0)
@@ -755,6 +958,30 @@
     }
     else
     {
+        if (hasEepromFile(bus, address))
+        {
+            auto path = getEepromPath(bus, address);
+            int eeprom = open(path.c_str(), O_RDWR | O_CLOEXEC);
+            if (eeprom < 0)
+            {
+                std::cerr << "unable to open i2c device " << path << "\n";
+                throw DBusInternalError();
+                return false;
+            }
+
+            ssize_t writtenBytes = write(eeprom, fru.data(), fru.size());
+            if (writtenBytes < 0)
+            {
+                std::cerr << "unable to write to i2c device " << path << "\n";
+                close(eeprom);
+                throw DBusInternalError();
+                return false;
+            }
+
+            close(eeprom);
+            return true;
+        }
+
         std::string i2cBus = "/dev/i2c-" + std::to_string(bus);
 
         int file = open(i2cBus.c_str(), O_RDWR | O_CLOEXEC);