sensor-mon: Start timers and shutdown

When the threshold alarm is tripped, start a timer for the configured
amount of time.  If the alarm clears before that time, stop the timer.
If it doesn't, power off the system.

Note that the alarm for the SoftShutdown threshold still does a hard
shutdown.  That's because in the family of systems this was written for,
the hypervisor also gets an alert when the threshold trips and is the
one that drives the power off.  The shutdown that this code does would
only occur when the hypervisor doesn't shut down first.

This could easily be wrapped in some sort of compile flags where the
default behavior would be to do a soft shutdown, but since at this point
there is no user of that the default will remain the way it is.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I3bc8f82562594dcd61bfcbc471ffe1a2d9451595
diff --git a/configure.ac b/configure.ac
index d89fa3e..ae01878 100644
--- a/configure.ac
+++ b/configure.ac
@@ -233,6 +233,24 @@
 
 AS_IF([test "x$enable_sensor_monitor" == "xyes"], [
 
+    #Default hard shutdown delay is 23 seconds
+    AC_ARG_VAR(SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS,
+            [Milliseconds to delay the alarm hard shutdown])
+    AS_IF([test "x$SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS" == "x"],
+        [SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS=23000])
+    AC_DEFINE_UNQUOTED([SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS],
+                       [$SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS],
+                       [Milliseconds to delay the alarm hard shutdown])
+
+    #Default soft shutdown delay is 15 minutes
+    AC_ARG_VAR(SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS,
+               [Milliseconds to delay the alarm soft shutdown])
+    AS_IF([test "x$SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS" == "x"],
+        [SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS=900000])
+    AC_DEFINE_UNQUOTED([SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS],
+                       [$SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS],
+                       [Milliseconds to delay the alarm soft shutdown])
+
     AC_CONFIG_FILES([sensor-monitor/Makefile])
 ])
 
diff --git a/sensor-monitor/shutdown_alarm_monitor.cpp b/sensor-monitor/shutdown_alarm_monitor.cpp
index b5b5843..6023f88 100644
--- a/sensor-monitor/shutdown_alarm_monitor.cpp
+++ b/sensor-monitor/shutdown_alarm_monitor.cpp
@@ -40,6 +40,18 @@
      {{AlarmType::low, "SoftShutdownAlarmLow"},
       {AlarmType::high, "SoftShutdownAlarmHigh"}}}};
 
+const std::map<ShutdownType, std::chrono::milliseconds> shutdownDelays{
+    {ShutdownType::hard,
+     std::chrono::milliseconds{SHUTDOWN_ALARM_HARD_SHUTDOWN_DELAY_MS}},
+    {ShutdownType::soft,
+     std::chrono::milliseconds{SHUTDOWN_ALARM_SOFT_SHUTDOWN_DELAY_MS}}};
+
+constexpr auto systemdService = "org.freedesktop.systemd1";
+constexpr auto systemdPath = "/org/freedesktop/systemd1";
+constexpr auto systemdMgrIface = "org.freedesktop.systemd1.Manager";
+constexpr auto valueInterface = "xyz.openbmc_project.Sensor.Value";
+constexpr auto valueProperty = "Value";
+
 using namespace sdbusplus::bus::match;
 
 ShutdownAlarmMonitor::ShutdownAlarmMonitor(sdbusplus::bus::bus& bus,
@@ -164,7 +176,108 @@
 
 void ShutdownAlarmMonitor::checkAlarm(bool value, const AlarmKey& alarmKey)
 {
-    // start or stop a timer possibly
+    auto alarm = alarms.find(alarmKey);
+    if (alarm == alarms.end())
+    {
+        return;
+    }
+
+    // Start or stop the timer if necessary.
+    auto& timer = alarm->second;
+    if (value)
+    {
+        if (!timer)
+        {
+            startTimer(alarmKey);
+        }
+    }
+    else
+    {
+        if (timer)
+        {
+            stopTimer(alarmKey);
+        }
+    }
+}
+
+void ShutdownAlarmMonitor::startTimer(const AlarmKey& alarmKey)
+{
+    const auto& [sensorPath, shutdownType, alarmType] = alarmKey;
+    const auto& propertyName = alarmProperties.at(shutdownType).at(alarmType);
+    std::chrono::milliseconds shutdownDelay{shutdownDelays.at(shutdownType)};
+    std::optional<double> value;
+
+    auto alarm = alarms.find(alarmKey);
+    if (alarm == alarms.end())
+    {
+        throw std::runtime_error("Couldn't find alarm inside startTimer");
+    }
+
+    try
+    {
+        value = SDBusPlus::getProperty<double>(bus, sensorPath, valueInterface,
+                                               valueProperty);
+    }
+    catch (const DBusServiceError& e)
+    {
+        // If the sensor was just added, the Value interface for it may
+        // not be in the mapper yet.  This could only happen if the sensor
+        // application was started with power up and the value exceeded the
+        // threshold immediately.
+    }
+
+    log<level::INFO>(
+        fmt::format("Starting {}ms {} shutdown timer due to sensor {} value {}",
+                    shutdownDelay.count(), propertyName, sensorPath, *value)
+            .c_str());
+
+    auto& timer = alarm->second;
+
+    timer = std::make_unique<
+        sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>>(
+        event, std::bind(&ShutdownAlarmMonitor::timerExpired, this, alarmKey));
+
+    timer->restartOnce(shutdownDelay);
+}
+
+void ShutdownAlarmMonitor::stopTimer(const AlarmKey& alarmKey)
+{
+    const auto& [sensorPath, shutdownType, alarmType] = alarmKey;
+    const auto& propertyName = alarmProperties.at(shutdownType).at(alarmType);
+
+    auto value = SDBusPlus::getProperty<double>(bus, sensorPath, valueInterface,
+                                                valueProperty);
+
+    auto alarm = alarms.find(alarmKey);
+    if (alarm == alarms.end())
+    {
+        throw std::runtime_error("Couldn't find alarm inside stopTimer");
+    }
+
+    log<level::INFO>(
+        fmt::format("Stopping {} shutdown timer due to sensor {} value {}",
+                    propertyName, sensorPath, value)
+            .c_str());
+
+    auto& timer = alarm->second;
+    timer->setEnabled(false);
+    timer.reset();
+}
+
+void ShutdownAlarmMonitor::timerExpired(const AlarmKey& alarmKey)
+{
+    const auto& [sensorPath, shutdownType, alarmType] = alarmKey;
+    const auto& propertyName = alarmProperties.at(shutdownType).at(alarmType);
+
+    log<level::ERR>(
+        fmt::format(
+            "The {} shutdown timer expired for sensor {}, shutting down",
+            propertyName, sensorPath)
+            .c_str());
+
+    SDBusPlus::callMethod(systemdService, systemdPath, systemdMgrIface,
+                          "StartUnit", "obmc-chassis-hard-poweroff@0.target",
+                          "replace");
 }
 
 void ShutdownAlarmMonitor::powerStateChanged(bool powerStateOn)
@@ -176,6 +289,14 @@
     else
     {
         // Cancel and delete all timers
+        std::for_each(alarms.begin(), alarms.end(), [](auto& alarm) {
+            auto& timer = alarm.second;
+            if (timer)
+            {
+                timer->setEnabled(false);
+                timer.reset();
+            }
+        });
     }
 }
 
diff --git a/sensor-monitor/shutdown_alarm_monitor.hpp b/sensor-monitor/shutdown_alarm_monitor.hpp
index 65f905d..de1c49e 100644
--- a/sensor-monitor/shutdown_alarm_monitor.hpp
+++ b/sensor-monitor/shutdown_alarm_monitor.hpp
@@ -23,6 +23,9 @@
 #include <sdeventplus/event.hpp>
 #include <sdeventplus/utility/timer.hpp>
 
+#include <chrono>
+#include <optional>
+
 namespace sensor::monitor
 {
 
@@ -107,6 +110,27 @@
     void findAlarms();
 
     /**
+     * @brief Starts a shutdown timer.
+     *
+     * @param[in] alarmKey - The alarm key
+     */
+    void startTimer(const AlarmKey& alarmKey);
+
+    /**
+     * @brief Stops a shutdown timer.
+     *
+     * @param[in] alarmKey - The alarm key
+     */
+    void stopTimer(const AlarmKey& alarmKey);
+
+    /**
+     * @brief The function called when the timer expires.
+     *
+     * @param[in] alarmKey - The alarm key
+     */
+    void timerExpired(const AlarmKey& alarmKey);
+
+    /**
      * @brief The power state changed handler.
      *
      * Checks alarms when power is turned on, and clears