Add inotify watch support

Added code to register for inotify events on the needed
path and the user callback on events

Change-Id: I90529f8e96fcbecfe0a1b943c3c435dab79222c3
Signed-off-by: Vishwanatha Subbanna <vishwa@linux.vnet.ibm.com>
diff --git a/Makefile.am b/Makefile.am
index e2f2fcf..3b91845 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -23,7 +23,8 @@
 		vlan_interface.hpp \
 		rtnetlink_server.hpp \
 		timer.hpp \
-		dns_updater.hpp
+		dns_updater.hpp \
+		watch.hpp
 
 phosphor_network_manager_SOURCES = \
 		ethernet_interface.cpp \
@@ -41,7 +42,8 @@
         vlan_interface.cpp \
 		rtnetlink_server.cpp \
 		timer.cpp \
-		dns_updater.cpp
+		dns_updater.cpp \
+		watch.cpp
 
 CLEANFILES = \
 		xyz/openbmc_project/Network/VLAN/Create/server.cpp \
diff --git a/test/Makefile.am b/test/Makefile.am
index 5c70fbe..5a1e712 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -2,7 +2,7 @@
 
 TESTS = $(check_PROGRAMS)
 
-check_PROGRAMS = test test_dns_updater
+check_PROGRAMS = test test_dns_updater test_watch
 
 test_SOURCES = \
 	test_util.cpp \
@@ -13,6 +13,7 @@
 	test_vlan_interface.cpp
 
 test_dns_updater_SOURCES = test_dns_updater.cpp
+test_watch_SOURCES = test_watch.cpp
 
 generic_cpp_flags = -Igtest $(GTEST_CPPFLAGS) $(AM_CPPFLAGS)
 
@@ -37,6 +38,10 @@
 test_dns_updater_CXXFLAGS = ${generic_cxx_flags}
 test_dns_updater_LDFLAGS  = ${generic_ld_flags}
 
+test_watch_CPPFLAGS = ${generic_cpp_flags}
+test_watch_CXXFLAGS = ${generic_cxx_flags}
+test_watch_LDFLAGS  = ${generic_ld_flags}
+
 test_LDADD = $(top_builddir)/ethernet_interface.o \
 			$(top_builddir)/network_manager.o \
 			$(top_builddir)/network_config.o \
@@ -52,3 +57,4 @@
 			$(top_builddir)/xyz/openbmc_project/Network/IP/Create/phosphor_network_manager-server.o
 
 test_dns_updater_LDADD = $(top_builddir)/dns_updater.o
+test_watch_LDADD = $(top_builddir)/watch.o
diff --git a/test/test_watch.cpp b/test/test_watch.cpp
new file mode 100644
index 0000000..dac43b3
--- /dev/null
+++ b/test/test_watch.cpp
@@ -0,0 +1,90 @@
+#include "watch.hpp"
+#include "types.hpp"
+
+#include <gtest/gtest.h>
+
+#include <fstream>
+#include <experimental/filesystem>
+
+static constexpr auto TRIGGER_FILE = "/tmp/netif_state";
+
+namespace fs = std::experimental::filesystem;
+
+class WatchTest : public ::testing::Test
+{
+    public:
+        // systemd event handler
+        sd_event* events;
+
+        // Need this so that events can be initialized.
+        int rc;
+
+        // Gets called as part of each TEST_F construction
+        WatchTest()
+            : rc(sd_event_default(&events)),
+              eventPtr(events)
+        {
+            // Create a file containing DNS entries like in netif/state
+            std::ofstream file(TRIGGER_FILE);
+            file << "";
+
+            // Check for successful creation of
+            // event handler
+            EXPECT_GE(rc, 0);
+        }
+
+        // Gets called as part of each TEST_F destruction
+        ~WatchTest()
+        {
+            if (fs::exists(TRIGGER_FILE))
+            {
+                fs::remove(TRIGGER_FILE);
+            }
+        }
+
+        // unique_ptr for sd_event
+        phosphor::network::EventPtr eventPtr;
+
+        // Count of callback invocation
+        int count = 0;
+
+        // This is supposed to get hit twice
+        // Once at the beginning to see if there is anything
+        // and the second time when the data is fired.
+        void callBackHandler(const fs::path& file)
+        {
+            count++;
+
+            // Expect that the file is what we wanted
+            EXPECT_EQ(file, TRIGGER_FILE);
+        }
+};
+
+/** @brief Makes sure that the inotify event is fired
+ */
+TEST_F(WatchTest, validateEventNotification)
+{
+    // Create a watch object and register the handler
+    phosphor::network::inotify::Watch watch(eventPtr, TRIGGER_FILE,
+            std::bind(&WatchTest::callBackHandler, this,
+                std::placeholders::_1));
+
+    // Reading the event post subscription
+    callBackHandler(TRIGGER_FILE);
+
+    // Callback function must have hit by now
+    EXPECT_EQ(1, count);
+
+    // Make a run and see that no changes
+    sd_event_run(eventPtr.get(), 10);
+    EXPECT_EQ(1, count);
+
+    // Pump the data and get notification
+    {
+        std::ofstream file(TRIGGER_FILE);
+        file <<"DNS=1.2.3.4\n";
+    }
+
+    sd_event_run(eventPtr.get(), 10);
+    EXPECT_EQ(2, count);
+}
diff --git a/watch.cpp b/watch.cpp
new file mode 100644
index 0000000..3cacefd
--- /dev/null
+++ b/watch.cpp
@@ -0,0 +1,139 @@
+#include "watch.hpp"
+
+#include <xyz/openbmc_project/Common/error.hpp>
+#include <phosphor-logging/log.hpp>
+#include <phosphor-logging/elog-errors.hpp>
+
+#include <sys/inotify.h>
+#include <errno.h>
+
+namespace phosphor
+{
+namespace network
+{
+namespace inotify
+{
+
+using namespace phosphor::logging;
+using namespace sdbusplus::xyz::openbmc_project::Common::Error;
+
+Watch::Watch(phosphor::network::EventPtr& eventPtr,
+             fs::path path,
+             UserCallBack userFunc,
+             int flags,
+             uint32_t mask,
+             uint32_t events) :
+    path(path),
+    userFunc(userFunc),
+    flags(flags),
+    mask(mask),
+    events(events),
+    fd(inotifyInit())
+{
+    // Check if watch file exists
+    // This is supposed to be there always
+    if (!fs::is_regular_file(path))
+    {
+        log<level::ERR>("Watch file doesn't exist",
+                        entry("FILE=%s", path.c_str()));
+        elog<InternalFailure>();
+    }
+
+    auto dirPath = path.parent_path();
+    wd = inotify_add_watch(fd(), dirPath.c_str(), mask);
+    if (wd == -1)
+    {
+        log<level::ERR>("Error from inotify_add_watch",
+                        entry("ERRNO=%d", errno));
+        elog<InternalFailure>();
+    }
+
+    // Register the fd with sd_event infrastructure and setup a
+    // callback handler to be invoked on events
+    auto rc = sd_event_add_io(eventPtr.get(),
+                              nullptr,
+                              fd(),
+                              events,
+                              Watch::processEvents,
+                              this);
+    if (rc < 0)
+    {
+        // Failed to add to event loop
+        log<level::ERR>("Error registering with sd_event_add_io",
+                        entry("RC=%d", rc));
+        elog<InternalFailure>();
+    }
+}
+
+int Watch::inotifyInit()
+{
+    auto fd = inotify_init1(flags);
+    if (fd < 0)
+    {
+        log<level::ERR>("Error from inotify_init1",
+                        entry("ERRNO=%d", errno));
+        elog<InternalFailure>();
+    }
+    return fd;
+}
+
+int Watch::processEvents(sd_event_source* eventSource,
+                         int fd,
+                         uint32_t retEvents,
+                         void* userData)
+{
+    auto watch = static_cast<Watch*>(userData);
+
+    // Not the ones we are interested in
+    if (!(retEvents & watch->events))
+    {
+        return 0;
+    }
+
+    // Buffer size to be used while reading events.
+    // per inotify(7), below number should be fine for reading
+    // at-least one event
+    constexpr auto maxBytes = sizeof(struct inotify_event) + NAME_MAX + 1;
+    uint8_t eventData[maxBytes]{};
+
+    auto bytes = read(fd, eventData, maxBytes);
+    if (bytes <= 0)
+    {
+        // Failed to read inotify event data
+        // Report error and return
+        log<level::ERR>("Error reading inotify event",
+                        entry("ERRNO=%d", errno));
+        report<InternalFailure>();
+        return 0;
+    }
+
+    auto offset = 0;
+    auto stateFile = watch->path.filename();
+    while (offset < bytes)
+    {
+        auto event = reinterpret_cast<inotify_event*>(&eventData[offset]);
+
+        // Filter the interesting ones
+        auto mask = event->mask & watch->mask;
+        if (mask)
+        {
+            if((event->len > 0) &&
+               (strstr(event->name, stateFile.string().c_str())))
+            {
+                if (watch->userFunc)
+                {
+                    watch->userFunc(watch->path);
+                }
+                // Found the event of interest
+                break;
+            }
+        }
+        // Move past this entry
+        offset += offsetof(inotify_event, name) + event->len;
+    }
+    return 0;
+}
+
+} // namespace inotify
+} // namespace network
+} // namespace phosphor
diff --git a/watch.hpp b/watch.hpp
new file mode 100644
index 0000000..0e20420
--- /dev/null
+++ b/watch.hpp
@@ -0,0 +1,114 @@
+#pragma once
+
+#include "util.hpp"
+#include "types.hpp"
+#include "dns_updater.hpp"
+
+#include <systemd/sd-event.h>
+
+#include <sys/inotify.h>
+#include <map>
+#include <functional>
+#include <experimental/filesystem>
+
+namespace phosphor
+{
+namespace network
+{
+namespace inotify
+{
+
+namespace fs = std::experimental::filesystem;
+
+// Auxiliary callback to be invoked on inotify events
+using UserCallBack = std::function<void(const std::string&)>;
+
+/** @class Watch
+ *
+ *  @brief Adds inotify watch on directory
+ *
+ *  @details Calls back user function on matching events
+ */
+class Watch
+{
+    public:
+        Watch() = delete;
+        Watch(const Watch&) = delete;
+        Watch& operator=(const Watch&) = delete;
+        Watch(Watch&&) = delete;
+        Watch& operator=(Watch&&) = delete;
+
+        /** @brief Hooks inotify watch with sd-event
+         *
+         *  @param[in] eventPtr - Reference to sd_event wrapped in unique_ptr
+         *  @param[in] path     - File path to be watched
+         *  @param[in] userFunc - User specific callback function on events
+         *  @param[in] flags    - Flags to be supplied to inotify
+         *  @param[in] mask     - Mask of events to be supplied to inotify
+         *  @param[in] events   - Events to be watched
+         */
+        Watch(phosphor::network::EventPtr& eventPtr,
+              const fs::path path,
+              UserCallBack userFunc,
+              int flags = IN_NONBLOCK,
+              uint32_t mask = IN_CLOSE_WRITE,
+              uint32_t events = EPOLLIN);
+
+        /** @brief Remove inotify watch and close fd's */
+        ~Watch()
+        {
+            if ((fd() >= 0) && (wd >= 0))
+            {
+                inotify_rm_watch(fd(), wd);
+            }
+        }
+
+    private:
+        /** @brief Callback invoked when inotify event fires
+         *
+         *  @details On a matching event, calls back into user supplied
+         *           function if there is one registered
+         *
+         *  @param[in] eventSource - Event source
+         *  @param[in] fd          - Inotify fd
+         *  @param[in] retEvents   - Events that matched for fd
+         *  @param[in] userData    - Pointer to Watch object
+         *
+         *  @returns 0 on success, -1 on fail
+         */
+        static int processEvents(sd_event_source* eventSource,
+                                 int fd,
+                                 uint32_t retEvents,
+                                 void* userData);
+
+        /** @brief Initializes an inotify instance
+         *
+         *  @return Descriptor on success, -1 on failure
+         */
+        int inotifyInit();
+
+        /** @brief File path to be watched */
+        const fs::path path;
+
+        /** @brief User callback function */
+        UserCallBack userFunc;
+
+        /** @brief Inotify flags */
+        int flags;
+
+        /** @brief Mask of events */
+        uint32_t mask;
+
+        /** @brief Events to be watched */
+        uint32_t events;
+
+        /** @brief Watch descriptor */
+        int wd = -1;
+
+        /** @brief File descriptor manager */
+        phosphor::Descriptor fd;
+};
+
+} // namespace inotify
+} // namespace network
+} // namespace phosphor