Add device-management infrastructure

We're moving toward having dbus-sensors take over sensor device
lifecycle management from entity-manager; this code (loosely based on
entity-manager's include/devices.hpp and src/overlay.cpp) provides some
general-purpose utilities for instantiating and destroying devices.

This also includes a variant of setupPropertiesChangedMatches() taking
an I2CDeviceTypeMap instead of a span of C strings, so that we can
support both existing sensor daemons and ones that get converted to use
that instead of the simple string arrays they currently employ.

Signed-off-by: Zev Weiss <zev@bewilderbeest.net>
Change-Id: I39fcd0de2c70cd58b4d132cdcae5f82319732dbb
diff --git a/include/DeviceMgmt.hpp b/include/DeviceMgmt.hpp
new file mode 100644
index 0000000..daad381
--- /dev/null
+++ b/include/DeviceMgmt.hpp
@@ -0,0 +1,41 @@
+#pragma once
+#include <Utils.hpp>
+#include <boost/container/flat_map.hpp>
+
+#include <functional>
+#include <optional>
+#include <string_view>
+
+struct I2CDeviceType
+{
+    const char* name;
+    bool createsHWMon;
+};
+
+using I2CDeviceTypeMap =
+    boost::container::flat_map<std::string, I2CDeviceType, std::less<>>;
+
+struct I2CDevice
+{
+    I2CDevice(const I2CDeviceType& type, uint64_t bus, uint64_t address) :
+        type(&type), bus(bus), address(address){};
+
+    const I2CDeviceType* type;
+    uint64_t bus;
+    uint64_t address;
+
+    bool present(void) const;
+    int create(void) const;
+    int destroy(void) const;
+};
+
+std::optional<I2CDevice> getI2CDevice(const I2CDeviceTypeMap& dtmap,
+                                      const SensorBaseConfigMap& cfg);
+
+// HACK: this declaration "should" live in Utils.hpp, but that leads to a
+// tangle of header-dependency hell because each header needs types declared
+// in the other.
+std::vector<std::unique_ptr<sdbusplus::bus::match_t>>
+    setupPropertiesChangedMatches(
+        sdbusplus::asio::connection& bus, const I2CDeviceTypeMap& typeMap,
+        const std::function<void(sdbusplus::message_t&)>& handler);
diff --git a/meson.build b/meson.build
index 8b29e64..1b355d5 100644
--- a/meson.build
+++ b/meson.build
@@ -118,6 +118,21 @@
     dependencies: [ sdbusplus ],
 )
 
+devicemgmt_a = static_library(
+    'devicemgmt_a',
+    [
+        'src/DeviceMgmt.cpp',
+    ],
+    dependencies: default_deps,
+    implicit_include_directories: false,
+    include_directories: 'include',
+)
+
+devicemgmt_dep = declare_dependency(
+    link_with: [ devicemgmt_a ],
+    dependencies: default_deps,
+)
+
 pwmsensor_a = static_library(
     'pwmsensor_a',
     'src/PwmSensor.cpp',
diff --git a/src/DeviceMgmt.cpp b/src/DeviceMgmt.cpp
new file mode 100644
index 0000000..ca52a3a
--- /dev/null
+++ b/src/DeviceMgmt.cpp
@@ -0,0 +1,124 @@
+#include <DeviceMgmt.hpp>
+
+#include <filesystem>
+#include <fstream>
+
+namespace fs = std::filesystem;
+
+std::optional<I2CDevice> getI2CDevice(const I2CDeviceTypeMap& dtmap,
+                                      const SensorBaseConfigMap& cfg)
+{
+    auto findType = cfg.find("Type");
+    auto findBus = cfg.find("Bus");
+    auto findAddr = cfg.find("Address");
+
+    if (findType == cfg.end() || findBus == cfg.end() || findAddr == cfg.end())
+    {
+        return std::nullopt;
+    }
+
+    const std::string* type = std::get_if<std::string>(&findType->second);
+    const uint64_t* bus = std::get_if<uint64_t>(&findBus->second);
+    const uint64_t* addr = std::get_if<uint64_t>(&findAddr->second);
+
+    if (type == nullptr || bus == nullptr || addr == nullptr)
+    {
+        return std::nullopt;
+    }
+
+    auto findDevType = dtmap.find(type->c_str());
+    if (findDevType == dtmap.end())
+    {
+        return std::nullopt;
+    }
+
+    return I2CDevice(findDevType->second, *bus, *addr);
+}
+
+static fs::path i2cBusPath(uint64_t bus)
+{
+    return {"/sys/bus/i2c/devices/i2c-" + std::to_string(bus)};
+}
+
+static std::string deviceDirName(uint64_t bus, uint64_t address)
+{
+    std::ostringstream name;
+    name << bus << "-" << std::hex << std::setw(4) << std::setfill('0')
+         << address;
+    return name.str();
+}
+
+bool I2CDevice::present(void) const
+{
+    fs::path path = i2cBusPath(bus) / deviceDirName(bus, address);
+
+    if (type->createsHWMon)
+    {
+        path /= "hwmon";
+    }
+
+    // Ignore errors; anything but a clean 'true' is fine as 'false'
+    std::error_code ec;
+    return fs::exists(path, ec);
+}
+
+int I2CDevice::create(void) const
+{
+    // If it's already instantiated, there's nothing we need to do.
+    if (present())
+    {
+        return 0;
+    }
+
+    // Try to create it: 'echo $devtype $addr > .../i2c-$bus/new_device'
+    fs::path ctorPath = i2cBusPath(bus) / "new_device";
+    std::ofstream ctor(ctorPath);
+    if (!ctor.good())
+    {
+        std::cerr << "Failed to open " << ctorPath << "\n";
+        return -1;
+    }
+
+    ctor << type->name << " " << address << "\n";
+    ctor.flush();
+    if (!ctor.good())
+    {
+        std::cerr << "Failed to write to " << ctorPath << "\n";
+        return -1;
+    }
+
+    // Check if that created the requisite sysfs directory
+    if (!present())
+    {
+        destroy();
+        return -1;
+    }
+
+    return 0;
+}
+
+int I2CDevice::destroy(void) const
+{
+    // No present() check on this like in create(), since it might be used to
+    // clean up after a device instantiation that was only partially
+    // successful (i.e. when present() would return false but there's still a
+    // dummy i2c client device to remove)
+
+    fs::path dtorPath = i2cBusPath(bus) / "delete_device";
+    std::ofstream dtor(dtorPath);
+    if (!dtor.good())
+    {
+        std::cerr << "Failed to open " << dtorPath << "\n";
+        return -1;
+    }
+
+    dtor << address << "\n";
+    dtor.flush();
+    if (!dtor.good())
+    {
+        std::cerr << "Failed to write to " << dtorPath << "\n";
+        return -1;
+    }
+
+    return 0;
+}
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 7b33cb7..700b336 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -16,6 +16,7 @@
 
 #include "dbus-sensor_config.h"
 
+#include <DeviceMgmt.hpp>
 #include <Utils.hpp>
 #include <boost/container/flat_map.hpp>
 #include <sdbusplus/asio/connection.hpp>
@@ -713,3 +714,17 @@
     }
     return matches;
 }
+
+std::vector<std::unique_ptr<sdbusplus::bus::match_t>>
+    setupPropertiesChangedMatches(
+        sdbusplus::asio::connection& bus, const I2CDeviceTypeMap& typeMap,
+        const std::function<void(sdbusplus::message_t&)>& handler)
+{
+    std::vector<const char*> types;
+    types.reserve(typeMap.size());
+    for (const auto& [type, dt] : typeMap)
+    {
+        types.push_back(type.data());
+    }
+    return setupPropertiesChangedMatches(bus, {types}, handler);
+}