bmc/general_systemd: Support systemd status reporting

We should be able to figure out the service status without using a file
since systemd exposes all of this information on DBus

Tested:
    Ran on a system and created services that trigger different types of
    behavior during verification to simulate running for a couple
    seconds and succeeding and failure. Also tested failing as quickly
    as possible to ensure the signal capturing is working correctly.

Change-Id: I26358dae7e908a93b710587121fa104dd40ea661
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/bmc/general_systemd.cpp b/bmc/general_systemd.cpp
index 5b9fb87..bcdda0d 100644
--- a/bmc/general_systemd.cpp
+++ b/bmc/general_systemd.cpp
@@ -27,6 +27,132 @@
 namespace ipmi_flash
 {
 
+static constexpr auto systemdService = "org.freedesktop.systemd1";
+static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
+static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
+
+bool SystemdNoFile::trigger()
+{
+    if (job)
+    {
+        std::fprintf(stderr, "Job alreading running %s: %s\n",
+                     triggerService.c_str(), job->c_str());
+        return false;
+    }
+
+    try
+    {
+        jobMonitor.emplace(
+            bus,
+            "type='signal',"
+            "sender='org.freedesktop.systemd1',"
+            "path='/org/freedesktop/systemd1',"
+            "interface='org.freedesktop.systemd1.Manager',"
+            "member='JobRemoved',",
+            [&](sdbusplus::message::message& m) { this->match(m); });
+
+        auto method = bus.new_method_call(systemdService, systemdRoot,
+                                          systemdInterface, "StartUnit");
+        method.append(triggerService);
+        method.append(mode);
+
+        sdbusplus::message::object_path obj_path;
+        bus.call(method).read(obj_path);
+        job = std::move(obj_path);
+        std::fprintf(stderr, "Triggered %s mode %s: %s\n",
+                     triggerService.c_str(), mode.c_str(), job->c_str());
+        currentStatus = ActionStatus::running;
+        return true;
+    }
+    catch (const std::exception& e)
+    {
+        job = std::nullopt;
+        jobMonitor = std::nullopt;
+        currentStatus = ActionStatus::failed;
+        std::fprintf(stderr, "Failed to trigger %s mode %s: %s\n",
+                     triggerService.c_str(), mode.c_str(), e.what());
+        return false;
+    }
+}
+
+void SystemdNoFile::abort()
+{
+    if (!job)
+    {
+        std::fprintf(stderr, "No running job %s\n", triggerService.c_str());
+        return;
+    }
+
+    // Cancel the job
+    auto cancel_req = bus.new_method_call(systemdService, job->c_str(),
+                                          systemdInterface, "Cancel");
+    try
+    {
+        bus.call_noreply(cancel_req);
+        std::fprintf(stderr, "Canceled %s: %s\n", triggerService.c_str(),
+                     job->c_str());
+    }
+    catch (const sdbusplus::exception::SdBusError& ex)
+    {
+        std::fprintf(stderr, "Failed to cancel job %s %s: %s\n",
+                     triggerService.c_str(), job->c_str(), ex.what());
+    }
+}
+
+ActionStatus SystemdNoFile::status()
+{
+    return currentStatus;
+}
+
+const std::string& SystemdNoFile::getMode() const
+{
+    return mode;
+}
+
+void SystemdNoFile::match(sdbusplus::message::message& m)
+{
+    if (!job)
+    {
+        std::fprintf(stderr, "No running job %s\n", triggerService.c_str());
+        return;
+    }
+
+    uint32_t job_id;
+    sdbusplus::message::object_path job_path;
+    std::string unit;
+    std::string result;
+    try
+    {
+        m.read(job_id, job_path, unit, result);
+    }
+    catch (const sdbusplus::exception::SdBusError& e)
+    {
+        std::fprintf(stderr, "Bad JobRemoved signal %s: %s\n",
+                     triggerService.c_str(), e.what());
+        return;
+    }
+
+    if (*job != job_path.str)
+    {
+        return;
+    }
+
+    std::fprintf(stderr, "Job Finished %s %s: %s\n", triggerService.c_str(),
+                 job->c_str(), result.c_str());
+    jobMonitor = std::nullopt;
+    job = std::nullopt;
+    currentStatus =
+        result == "done" ? ActionStatus::success : ActionStatus::failed;
+}
+
+std::unique_ptr<TriggerableActionInterface>
+    SystemdNoFile::CreateSystemdNoFile(sdbusplus::bus::bus&& bus,
+                                       const std::string& service,
+                                       const std::string& mode)
+{
+    return std::make_unique<SystemdNoFile>(std::move(bus), service, mode);
+}
+
 std::unique_ptr<TriggerableActionInterface>
     SystemdWithStatusFile::CreateSystemdWithStatusFile(
         sdbusplus::bus::bus&& bus, const std::string& path,
@@ -36,40 +162,12 @@
                                                    service, mode);
 }
 
-bool SystemdWithStatusFile::trigger()
-{
-    static constexpr auto systemdService = "org.freedesktop.systemd1";
-    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
-    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
-
-    auto method = bus.new_method_call(systemdService, systemdRoot,
-                                      systemdInterface, "StartUnit");
-    method.append(triggerService);
-    method.append(mode);
-
-    try
-    {
-        bus.call_noreply(method);
-    }
-    catch (const sdbusplus::exception::SdBusError& ex)
-    {
-        /* TODO: Once logging supports unit-tests, add a log message to test
-         * this failure.
-         */
-        return false;
-    }
-
-    return true;
-}
-
-void SystemdWithStatusFile::abort()
-{
-    /* TODO: Implement this. */
-}
-
 ActionStatus SystemdWithStatusFile::status()
 {
-    ActionStatus result = ActionStatus::unknown;
+    // Assume a status based on job execution if there is no file
+    ActionStatus result = SystemdNoFile::status() == ActionStatus::running
+                              ? ActionStatus::running
+                              : ActionStatus::failed;
 
     std::ifstream ifs;
     ifs.open(checkPath);
@@ -98,59 +196,4 @@
     return result;
 }
 
-const std::string SystemdWithStatusFile::getMode() const
-{
-    return mode;
-}
-
-std::unique_ptr<TriggerableActionInterface>
-    SystemdNoFile::CreateSystemdNoFile(sdbusplus::bus::bus&& bus,
-                                       const std::string& service,
-                                       const std::string& mode)
-{
-    return std::make_unique<SystemdNoFile>(std::move(bus), service, mode);
-}
-
-bool SystemdNoFile::trigger()
-{
-    static constexpr auto systemdService = "org.freedesktop.systemd1";
-    static constexpr auto systemdRoot = "/org/freedesktop/systemd1";
-    static constexpr auto systemdInterface = "org.freedesktop.systemd1.Manager";
-
-    auto method = bus.new_method_call(systemdService, systemdRoot,
-                                      systemdInterface, "StartUnit");
-    method.append(triggerService);
-    method.append(mode);
-
-    try
-    {
-        bus.call_noreply(method);
-        state = ActionStatus::running;
-        return true;
-    }
-    catch (const sdbusplus::exception::SdBusError& ex)
-    {
-        /* TODO: Once logging supports unit-tests, add a log message to test
-         * this failure.
-         */
-        state = ActionStatus::failed;
-        return false;
-    }
-}
-
-void SystemdNoFile::abort()
-{
-    return;
-}
-
-ActionStatus SystemdNoFile::status()
-{
-    return state;
-}
-
-const std::string SystemdNoFile::getMode() const
-{
-    return mode;
-}
-
 } // namespace ipmi_flash
diff --git a/bmc/general_systemd.hpp b/bmc/general_systemd.hpp
index c7fe4cb..a8d9a88 100644
--- a/bmc/general_systemd.hpp
+++ b/bmc/general_systemd.hpp
@@ -4,16 +4,56 @@
 
 #include <memory>
 #include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
 #include <string>
 
 namespace ipmi_flash
 {
 
+class SystemdNoFile : public TriggerableActionInterface
+{
+  public:
+    static std::unique_ptr<TriggerableActionInterface>
+        CreateSystemdNoFile(sdbusplus::bus::bus&& bus,
+                            const std::string& service,
+                            const std::string& mode);
+
+    SystemdNoFile(sdbusplus::bus::bus&& bus, const std::string& service,
+                  const std::string& mode) :
+        bus(std::move(bus)),
+        triggerService(service), mode(mode)
+    {
+    }
+
+    SystemdNoFile(const SystemdNoFile&) = delete;
+    SystemdNoFile& operator=(const SystemdNoFile&) = delete;
+    // sdbusplus match requires us to be pinned
+    SystemdNoFile(SystemdNoFile&&) = delete;
+    SystemdNoFile& operator=(SystemdNoFile&&) = delete;
+
+    bool trigger() override;
+    void abort() override;
+    ActionStatus status() override;
+
+    const std::string& getMode() const;
+
+  private:
+    sdbusplus::bus::bus bus;
+    const std::string triggerService;
+    const std::string mode;
+
+    std::optional<sdbusplus::bus::match::match> jobMonitor;
+    std::optional<std::string> job;
+    ActionStatus currentStatus = ActionStatus::unknown;
+
+    void match(sdbusplus::message::message& m);
+};
+
 /**
  * Representation of what is used for triggering an action with systemd and
  * checking the result by reading a file.
  */
-class SystemdWithStatusFile : public TriggerableActionInterface
+class SystemdWithStatusFile : public SystemdNoFile
 {
   public:
     /**
@@ -34,62 +74,15 @@
 
     SystemdWithStatusFile(sdbusplus::bus::bus&& bus, const std::string& path,
                           const std::string& service, const std::string& mode) :
-        bus(std::move(bus)),
-        checkPath(path), triggerService(service), mode(mode)
+        SystemdNoFile(std::move(bus), service, mode),
+        checkPath(path)
     {
     }
 
-    ~SystemdWithStatusFile() = default;
-    SystemdWithStatusFile(const SystemdWithStatusFile&) = delete;
-    SystemdWithStatusFile& operator=(const SystemdWithStatusFile&) = delete;
-    SystemdWithStatusFile(SystemdWithStatusFile&&) = default;
-    SystemdWithStatusFile& operator=(SystemdWithStatusFile&&) = default;
-
-    bool trigger() override;
-    void abort() override;
     ActionStatus status() override;
 
-    const std::string getMode() const;
-
   private:
-    sdbusplus::bus::bus bus;
     const std::string checkPath;
-    const std::string triggerService;
-    const std::string mode;
-};
-
-class SystemdNoFile : public TriggerableActionInterface
-{
-  public:
-    static std::unique_ptr<TriggerableActionInterface>
-        CreateSystemdNoFile(sdbusplus::bus::bus&& bus,
-                            const std::string& service,
-                            const std::string& mode);
-
-    SystemdNoFile(sdbusplus::bus::bus&& bus, const std::string& service,
-                  const std::string& mode) :
-        bus(std::move(bus)),
-        triggerService(service), mode(mode)
-    {
-    }
-
-    ~SystemdNoFile() = default;
-    SystemdNoFile(const SystemdNoFile&) = delete;
-    SystemdNoFile& operator=(const SystemdNoFile&) = delete;
-    SystemdNoFile(SystemdNoFile&&) = default;
-    SystemdNoFile& operator=(SystemdNoFile&&) = default;
-
-    bool trigger() override;
-    void abort() override;
-    ActionStatus status() override;
-
-    const std::string getMode() const;
-
-  private:
-    sdbusplus::bus::bus bus;
-    const std::string triggerService;
-    const std::string mode;
-    ActionStatus state = ActionStatus::unknown;
 };
 
 } // namespace ipmi_flash