Get PSU version from vendor specific tool

The code was getting the version from PSU inventory object.
This commit changes the behavior to use a vendor-specific tool to get
the version directly, where the tool is expected to be configured during
build time.

Add an example get_version app that shows the expected behavior of the
tool:
* It accepts an argument of PSU inventory object;
* It outputs the version to stdout.

Tested: 1. Put and configure to use the example get_version on witherspoon,
        verify that PSU software objects are created with the version
        returned by the exmaple tool.
        2. With the Witherspoon specific tool in
        https://gerrit.openbmc-project.xyz/c/openbmc/witherspoon-pfault-analysis/+/24811,
        verify the version is correctly got from the PSU inventory path
        and the software objects are created.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I5195cb6fc8998a76b09abcfe0b107364cb180c01
diff --git a/src/utils.cpp b/src/utils.cpp
index 5e92f4a..7a968b7 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -6,6 +6,7 @@
 
 #include <fstream>
 #include <phosphor-logging/log.hpp>
+#include <sstream>
 
 using namespace phosphor::logging;
 
@@ -19,6 +20,39 @@
 constexpr auto MAPPER_INTERFACE = "xyz.openbmc_project.ObjectMapper";
 } // namespace
 
+namespace internal
+{
+template <typename... Ts>
+std::string concat_string(Ts const&... ts)
+{
+    std::stringstream s;
+    ((s << ts << " "), ...) << std::endl;
+    return s.str();
+}
+
+// Helper function to run command
+// Returns return code and the stdout
+template <typename... Ts>
+std::pair<int, std::string> exec(Ts const&... ts)
+{
+    std::array<char, 512> buffer;
+    std::string cmd = concat_string(ts...);
+    std::stringstream result;
+    int rc;
+    FILE* pipe = popen(cmd.c_str(), "r");
+    if (!pipe)
+    {
+        throw std::runtime_error("popen() failed!");
+    }
+    while (fgets(buffer.data(), buffer.size(), pipe) != nullptr)
+    {
+        result << buffer.data();
+    }
+    rc = pclose(pipe);
+    return {rc, result.str()};
+}
+
+} // namespace internal
 const UtilsInterface& getUtils()
 {
     static Utils utils;
@@ -98,6 +132,15 @@
     return (hexId.substr(0, 8));
 }
 
+std::string Utils::getVersion(const std::string& inventoryPath) const
+{
+    // Invoke vendor-specify tool to get the version string, e.g.
+    //   psutils get-version
+    //   /xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply0
+    auto [rc, r] = internal::exec(PSU_VERSION_UTIL, inventoryPath);
+    return (rc == 0) ? r : "";
+}
+
 any Utils::getPropertyImpl(sdbusplus::bus::bus& bus, const char* service,
                            const char* path, const char* interface,
                            const char* propertyName) const