sched-host-tran: handle with BMC time changing

Handle with different processes when BMC time is changed after scheduled time
is set.

Tested:
Case1: BMC time is changed to be later than current time but still earlier than
scheduled time
1. Get current time
 # date
   Tue Feb 25 07:07:44 UTC 2020
 # date +%s
   1582614271
2. Schedule time, do host transition after at 07:20:00 around
 # busctl get-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime
   t 1582615256
3. Change BMC time to 07:19:00 around
 # busctl set-property xyz.openbmc_project.Time.Manager \
   /xyz/openbmc_project/time/bmc xyz.openbmc_project.Time.EpochTime Elapsed \
   t 1582615136000000
 # date
   Tue Feb 25 07:19:20 UTC 2020
 # date +%s
   1582615187
4. Host transition is done after 1 minute around,
   instead of waiting 13 mins around.

Case2: BMC time is changed after scheduled time is reached
Following Case1, the scheduled time is reached already,
1. Change BMC time to 07:10:00 around
 # busctl set-property xyz.openbmc_project.Time.Manager \
   /xyz/openbmc_project/time/bmc xyz.openbmc_project.Time.EpochTime Elapsed \
   t 1582614600000000
2. APP shows "The function Scheduled Host Transition is disabled", because
   the scheduled time is reached already and the scheduled time has been set
   to 0 after host transition is triggered.

Case3: BMC time is changed to be bigger than scheduled time before scheduled
time is reached
1. Set scheduled time 07:08:00 around
 # busctl set-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime t 1582787314
2. Change BMC time to 07:10:00 around
 # busctl set-property xyz.openbmc_project.Time.Manager \
   /xyz/openbmc_project/time/bmc xyz.openbmc_project.Time.EpochTime Elapsed \
   t 1582787434000000
3. It will do host transition as requested.

Case4: BMC time is changed to be earlier than current time
1. Set scheduled time 07:10:00 around
 # busctl set-property xyz.openbmc_project.State.ScheduledHostTransition \
   /xyz/openbmc_project/state/host0 \
   xyz.openbmc_project.State.ScheduledHostTransition ScheduledTime t 1582787434
2. Change BMC time to 07:08:00 around
 # busctl set-property xyz.openbmc_project.Time.Manager \
   /xyz/openbmc_project/time/bmc xyz.openbmc_project.Time.EpochTime Elapsed \
   t 1582787314000000
3. App will wait 2 minutes more to do host transition.

Change-Id: I23228be944d1b2f71161317228c8b16d7f5ca4eb
Signed-off-by: Carol Wang <wangkair@cn.ibm.com>
diff --git a/scheduled_host_transition.cpp b/scheduled_host_transition.cpp
index b65d097..9fdd32b 100644
--- a/scheduled_host_transition.cpp
+++ b/scheduled_host_transition.cpp
@@ -5,6 +5,17 @@
 #include <phosphor-logging/log.hpp>
 #include <xyz/openbmc_project/ScheduledTime/error.hpp>
 #include <chrono>
+#include <sys/timerfd.h>
+#include <unistd.h>
+
+// Need to do this since its not exported outside of the kernel.
+// Refer : https://gist.github.com/lethean/446cea944b7441228298
+#ifndef TFD_TIMER_CANCEL_ON_SET
+#define TFD_TIMER_CANCEL_ON_SET (1 << 1)
+#endif
+
+// Needed to make sure timerfd does not misfire even though we set CANCEL_ON_SET
+#define TIME_T_MAX (time_t)((1UL << ((sizeof(time_t) << 3) - 1)) - 1)
 
 namespace phosphor
 {
@@ -39,7 +50,8 @@
             timer.setEnabled(false);
         }
 
-        log<level::INFO>("The function Scheduled Host Transition is disabled.");
+        log<level::INFO>("scheduledTime: The function Scheduled Host "
+                         "Transition is disabled.");
     }
     else
     {
@@ -141,6 +153,114 @@
     HostTransition::scheduledTime(0);
 }
 
+void ScheduledHostTransition::initialize()
+{
+    // Subscribe time change event
+    // Choose the MAX time that is possible to avoid mis fires.
+    constexpr itimerspec maxTime = {
+        {0, 0},          // it_interval
+        {TIME_T_MAX, 0}, // it_value
+    };
+
+    // Create and operate on a timer that delivers timer expiration
+    // notifications via a file descriptor.
+    timeFd = timerfd_create(CLOCK_REALTIME, 0);
+    if (timeFd == -1)
+    {
+        auto eno = errno;
+        log<level::ERR>("Failed to create timerfd", entry("ERRNO=%d", eno),
+                        entry("RC=%d", timeFd));
+        throw std::system_error(eno, std::system_category());
+    }
+
+    // Starts the timer referred to by the file descriptor fd.
+    // If TFD_TIMER_CANCEL_ON_SET is specified along with TFD_TIMER_ABSTIME
+    // and the clock for this timer is CLOCK_REALTIME, then mark this timer
+    // as cancelable if the real-time clock undergoes a discontinuous change.
+    // In this way, we can monitor whether BMC time is changed or not.
+    auto r = timerfd_settime(
+        timeFd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &maxTime, nullptr);
+    if (r != 0)
+    {
+        auto eno = errno;
+        log<level::ERR>("Failed to set timerfd", entry("ERRNO=%d", eno),
+                        entry("RC=%d", r));
+        throw std::system_error(eno, std::system_category());
+    }
+
+    sd_event_source* es;
+    // Add a new I/O event source to an event loop. onTimeChange will be called
+    // when the event source is triggered.
+    r = sd_event_add_io(event.get(), &es, timeFd, EPOLLIN, onTimeChange, this);
+    if (r < 0)
+    {
+        auto eno = errno;
+        log<level::ERR>("Failed to add event", entry("ERRNO=%d", eno),
+                        entry("RC=%d", r));
+        throw std::system_error(eno, std::system_category());
+    }
+    timeChangeEventSource.reset(es);
+}
+
+ScheduledHostTransition::~ScheduledHostTransition()
+{
+    close(timeFd);
+}
+
+void ScheduledHostTransition::handleTimeUpdates()
+{
+    if (!timer.isEnabled())
+    {
+        return;
+    }
+    // Stop the timer if it's running
+    timer.setEnabled(false);
+
+    // Get scheduled time
+    auto schedTime = HostTransition::scheduledTime();
+
+    if (schedTime == 0)
+    {
+        log<level::INFO>("handleTimeUpdates: The function Scheduled Host "
+                         "Transition is disabled.");
+        return;
+    }
+
+    auto deltaTime = seconds(schedTime) - getTime();
+    if (deltaTime <= seconds(0))
+    {
+        // When BMC time is changed to be later than scheduled time, check the
+        // state of host transition to decide whether need to do host transition
+        hostTransition();
+        // Set scheduledTime to 0 to disable host transition
+        HostTransition::scheduledTime(0);
+    }
+    else
+    {
+        // Start a timer to do host transition at scheduled time
+        timer.restart(deltaTime);
+    }
+}
+
+int ScheduledHostTransition::onTimeChange(sd_event_source* /* es */, int fd,
+                                          uint32_t /* revents */,
+                                          void* userdata)
+{
+    auto schedHostTran = static_cast<ScheduledHostTransition*>(userdata);
+
+    std::array<char, 64> time{};
+
+    // We are not interested in the data here.
+    // So read until there is no new data here in the FD
+    while (read(fd, time.data(), time.max_size()) > 0)
+        ;
+
+    log<level::INFO>("BMC system time is changed");
+    schedHostTran->handleTimeUpdates();
+
+    return 0;
+}
+
 } // namespace manager
 } // namespace state
 } // namespace phosphor
diff --git a/scheduled_host_transition.hpp b/scheduled_host_transition.hpp
index 1cb2591..a17ca3b 100644
--- a/scheduled_host_transition.hpp
+++ b/scheduled_host_transition.hpp
@@ -32,12 +32,13 @@
     ScheduledHostTransition(sdbusplus::bus::bus& bus, const char* objPath,
                             const sdeventplus::Event& event) :
         ScheduledHostTransitionInherit(bus, objPath),
-        bus(bus),
+        bus(bus), event(event),
         timer(event, std::bind(&ScheduledHostTransition::callback, this))
     {
+        initialize();
     }
 
-    ~ScheduledHostTransition() = default;
+    ~ScheduledHostTransition();
 
     /**
      * @brief Handle with scheduled time
@@ -55,9 +56,15 @@
     /** @brief sdbusplus bus client connection */
     sdbusplus::bus::bus& bus;
 
+    /** @brief sdbusplus event */
+    const sdeventplus::Event& event;
+
     /** @brief Timer used for host transition with seconds */
     sdeventplus::utility::Timer<sdeventplus::ClockId::RealTime> timer;
 
+    /** @brief The fd for time change event */
+    int timeFd = -1;
+
     /** @brief Get current time
      *
      *  @return - return current epoch time
@@ -73,6 +80,37 @@
 
     /** @brief Used by the timer to do host transition */
     void callback();
+
+    /** @brief Initialize timerFd related resource */
+    void initialize();
+
+    /** @brief The callback function on system time change
+     *
+     * @param[in] es - Source of the event
+     * @param[in] fd - File descriptor of the timer
+     * @param[in] revents - Not used
+     * @param[in] userdata - User data pointer
+     */
+    static int onTimeChange(sd_event_source* es, int fd, uint32_t revents,
+                            void* userdata);
+
+    /** @brief The deleter of sd_event_source */
+    std::function<void(sd_event_source*)> sdEventSourceDeleter =
+        [](sd_event_source* p) {
+            if (p)
+            {
+                sd_event_source_unref(p);
+            }
+        };
+
+    using SdEventSource =
+        std::unique_ptr<sd_event_source, decltype(sdEventSourceDeleter)>;
+
+    /** @brief The event source on system time change */
+    SdEventSource timeChangeEventSource{nullptr, sdEventSourceDeleter};
+
+    /** @brief Handle with the process when bmc time is changed*/
+    void handleTimeUpdates();
 };
 } // namespace manager
 } // namespace state
diff --git a/test/test_scheduled_host_transition.cpp b/test/test_scheduled_host_transition.cpp
index 58e7aea..f148b59 100644
--- a/test/test_scheduled_host_transition.cpp
+++ b/test/test_scheduled_host_transition.cpp
@@ -17,6 +17,8 @@
 using namespace std::chrono;
 using InvalidTimeError =
     sdbusplus::xyz::openbmc_project::ScheduledTime::Error::InvalidTime;
+using HostTransition =
+    sdbusplus::xyz::openbmc_project::State::server::ScheduledHostTransition;
 
 class TestScheduledHostTransition : public testing::Test
 {
@@ -42,6 +44,11 @@
     {
         return scheduledHostTransition.timer.isEnabled();
     }
+
+    void bmcTimeChange()
+    {
+        scheduledHostTransition.handleTimeUpdates();
+    }
 };
 
 TEST_F(TestScheduledHostTransition, disableHostTransition)
@@ -78,6 +85,32 @@
     EXPECT_EQ(scheduledHostTransition.scheduledTransition(), Transition::Off);
 }
 
+TEST_F(TestScheduledHostTransition, bmcTimeChangeWithDisabledHostTransition)
+{
+    // Disable host transition
+    scheduledHostTransition.scheduledTime(0);
+    bmcTimeChange();
+    // Check timer
+    EXPECT_FALSE(isTimerEnabled());
+    // Check scheduled time
+    EXPECT_EQ(scheduledHostTransition.HostTransition::scheduledTime(), 0);
+}
+
+TEST_F(TestScheduledHostTransition, bmcTimeChangeBackward)
+{
+    // Current time is earlier than scheduled time due to BMC time changing
+    uint64_t schTime =
+        static_cast<uint64_t>((getCurrentTime() + seconds(60)).count());
+    // Set scheduled time, which is the same as bmc time is changed.
+    // But can't use this method to write another case like
+    // bmcTimeChangeForward, because set a scheduled time earlier than current
+    // time will throw an error.
+    scheduledHostTransition.scheduledTime(schTime);
+    bmcTimeChange();
+    // Check timer
+    EXPECT_TRUE(isTimerEnabled());
+}
+
 } // namespace manager
 } // namespace state
 } // namespace phosphor