common: Inotify implementation 30/49730/7
authorJan Olszak <j.olszak@samsung.com>
Mon, 19 Oct 2015 12:49:45 +0000 (14:49 +0200)
committerJan Olszak <j.olszak@samsung.com>
Wed, 21 Oct 2015 14:58:17 +0000 (16:58 +0200)
[Feature]       Asynchronous file system events
[Cause]         N/A
[Solution]      N/A
[Verification]  Build, install and run tests

Change-Id: I53b1d75026550cf3563dce7fb1ea50eede4fb5cb

common/utils/eventfd.hpp
common/utils/fs.cpp
common/utils/fs.hpp
common/utils/inotify.cpp [new file with mode: 0644]
common/utils/inotify.hpp [new file with mode: 0644]
common/utils/paths.hpp
common/utils/signalfd.hpp
tests/unit_tests/utils/ut-inotify.cpp [new file with mode: 0644]

index f49bbff..698847d 100644 (file)
@@ -31,7 +31,7 @@ class EventFD {
 public:
 
     EventFD();
-    ~EventFD();
+    virtual ~EventFD();
 
     EventFD(const EventFD& eventfd) = delete;
     EventFD& operator=(const EventFD&) = delete;
index a7a979a..62e6c37 100644 (file)
@@ -135,6 +135,35 @@ bool isCharDevice(const std::string& path)
     return ::stat(path.c_str(), &s) == 0 && S_IFCHR == (s.st_mode & S_IFMT);
 }
 
+void assertIsDir(const std::string& path)
+{
+    if (path.empty()) {
+        const std::string msg = "Empty path";
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    struct stat s;
+    if (::stat(path.c_str(), &s)) {
+        const std::string msg = "Error in stat() " + path + ": " + getSystemErrorMessage();
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    if(!(s.st_mode & S_IFDIR)) {
+        const std::string msg = "Not a directory";
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    if(::access(path.c_str(), X_OK) < 0) {
+        const std::string msg = "Not a traversable directory";
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+}
+
+
 namespace {
 // NOTE: Should be the same as in systemd/src/core/mount-setup.c
 const std::string RUN_MOUNT_POINT_OPTIONS = "mode=755,smackfstransmute=System::Run";
@@ -270,8 +299,8 @@ bool copyDirContentsRec(const boost::filesystem::path& src, const boost::filesys
 {
     try {
         for (fs::directory_iterator file(src);
-             file != fs::directory_iterator();
-             ++file) {
+                file != fs::directory_iterator();
+                ++file) {
             fs::path current(file->path());
             fs::path destination = dst / current.filename();
 
@@ -400,7 +429,7 @@ bool createDirs(const std::string& path, mode_t mode)
                     fs::remove(*iter, errorCode);
                     if (errorCode) {
                         LOGE("Error during cleaning: dir: " << *iter
-                            << ", msg: " << errorCode.message());
+                             << ", msg: " << errorCode.message());
                     }
                 }
                 return false;
@@ -455,12 +484,12 @@ bool createFile(const std::string& path, int flags, mode_t mode)
 
 bool createFifo(const std::string& path, mode_t mode)
 {
-   int ret = ::mkfifo(path.c_str(), mode);
-   if (ret < 0) {
-       LOGE("Failed to make fifo: path=host:" << path);
-       return false;
-   }
-   return true;
+    int ret = ::mkfifo(path.c_str(), mode);
+    if (ret < 0) {
+        LOGE("Failed to make fifo: path=host:" << path);
+        return false;
+    }
+    return true;
 }
 
 bool copyFile(const std::string& src, const std::string& dest)
@@ -510,9 +539,9 @@ bool createLink(const std::string& src, const std::string& dest)
     bool retSmack = copySmackLabel(src, dest);
     if (!retSmack) {
         LOGE("Failed to copy smack label: path=host:"
-              << src
-              << ", path=host:"
-              << dest);
+             << src
+             << ", path=host:"
+             << dest);
         boost::system::error_code ec;
         fs::remove(dest, ec);
         if (!ec) {
index bff2e70..2237450 100644 (file)
@@ -65,6 +65,11 @@ bool removeFile(const std::string& path);
 bool isCharDevice(const std::string& path);
 
 /**
+ * Checks if a path exists and points to a directory
+ */
+void assertIsDir(const std::string& path);
+
+/**
  * List all (including '.' and '..' entries) dir entries
  */
 bool listDir(const std::string& path, std::vector<std::string>& files);
diff --git a/common/utils/inotify.cpp b/common/utils/inotify.cpp
new file mode 100644 (file)
index 0000000..a94d95b
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+*  Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved
+*
+*  Contact: Jan Olszak <j.olszak@samsung.com>
+*
+*  Licensed under the Apache License, Version 2.0 (the "License");
+*  you may not use this file except in compliance with the License.
+*  You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+*  Unless required by applicable law or agreed to in writing, software
+*  distributed under the License is distributed on an "AS IS" BASIS,
+*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*  See the License for the specific language governing permissions and
+*  limitations under the License
+*/
+
+/**
+ * @file
+ * @author  Jan Olszak (j.olszak@samsung.com)
+ * @brief   Inotify wrapper
+ */
+
+#include "utils/inotify.hpp"
+#include "utils/paths.hpp"
+#include "utils/fs.hpp"
+#include "utils/fd-utils.hpp"
+#include "utils/exception.hpp"
+
+#include "logger/logger.hpp"
+
+#include <sys/ioctl.h>
+
+#include <functional>
+
+
+namespace utils {
+
+Inotify::Inotify(ipc::epoll::EventPoll& eventPoll)
+    :mEventPoll(eventPoll)
+{
+    mFD = ::inotify_init1(IN_CLOEXEC);
+    if (mFD == -1) {
+        const std::string msg = "Error in inotify_init1: " + getSystemErrorMessage();
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    mEventPoll.addFD(mFD, EPOLLIN, std::bind(&Inotify::handleInternal, this));
+}
+
+Inotify::~Inotify()
+{
+    Lock lock(mMutex);
+
+    for(const auto& handler: mHandlers) {
+        if (-1 == ::inotify_rm_watch(mFD, handler.watchID)) {
+            LOGE("Error in inotify_rm_watch: " + getSystemErrorMessage());
+        }
+    }
+    mEventPoll.removeFD(mFD);
+}
+
+int Inotify::getFD() const
+{
+    return mFD;
+}
+
+void Inotify::setHandler(const std::string& path,
+                         const uint32_t eventMask,
+                         const Callback&& callback)
+{
+    Lock lock(mMutex);
+
+    removeHandlerInternal(path);
+
+    int watchID = ::inotify_add_watch(mFD, path.c_str(), eventMask);
+    if (-1 == watchID) {
+        const std::string msg = "Error in inotify_add_watch: " + getSystemErrorMessage();
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    mHandlers.push_back({path, watchID, callback});
+}
+
+void Inotify::removeHandlerInternal(const std::string& path)
+{
+    // Find the corresponding handler's data
+    auto it = std::find_if(mHandlers.begin(), mHandlers.end(), [&path](const Handler& h) {
+        return path == h.path;
+    });
+
+    if (it == mHandlers.end()) {
+        return;
+    }
+
+    // Unwatch the path
+    if (-1 == ::inotify_rm_watch(mFD, it->watchID)) {
+        const std::string msg = "Error in inotify_rm_watch: " + getSystemErrorMessage();
+        LOGE(msg);
+        throw UtilsException(msg);
+    }
+
+    mHandlers.erase(it);
+}
+
+void Inotify::removeHandler(const std::string& path)
+{
+    Lock lock(mMutex);
+    removeHandlerInternal(path);
+}
+
+void Inotify::handleInternal()
+{
+    Lock lock(mMutex);
+
+    // Get how much data is awaiting
+    unsigned int bufferSize;
+    utils::ioctl(mFD, FIONREAD, &bufferSize);
+
+    // Read all events into a buffer
+    std::vector<char> buffer(bufferSize);
+    utils::read(mFD, buffer.data(), bufferSize);
+
+    // Handle all events
+    unsigned int offset = 0;
+    while (offset < bufferSize) {
+        struct ::inotify_event *event = reinterpret_cast<struct ::inotify_event*>(&buffer[offset]);
+        offset = offset + sizeof(inotify_event) + event->len;
+
+        if(event->mask & IN_IGNORED) {
+            // Watch was removed - ignore
+            continue;
+        }
+
+        auto it = std::find_if(mHandlers.begin(), mHandlers.end(), [event](const Handler& h) {
+            return event->wd == h.watchID;
+        });
+        if (it == mHandlers.end()) {
+            // Meantime the callback was deleted by another callback
+            LOGE("No callback for file: " << event->name);
+            continue;
+        }
+
+        it->call(event->name, event->mask);
+    }
+}
+
+} // namespace utils
diff --git a/common/utils/inotify.hpp b/common/utils/inotify.hpp
new file mode 100644 (file)
index 0000000..15c90b8
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+*  Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved
+*
+*  Contact: Jan Olszak <j.olszak@samsung.com>
+*
+*  Licensed under the Apache License, Version 2.0 (the "License");
+*  you may not use this file except in compliance with the License.
+*  You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+*  Unless required by applicable law or agreed to in writing, software
+*  distributed under the License is distributed on an "AS IS" BASIS,
+*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*  See the License for the specific language governing permissions and
+*  limitations under the License
+*/
+
+/**
+ * @file
+ * @author  Jan Olszak (j.olszak@samsung.com)
+ * @brief   Inotify wrapper
+ */
+
+#ifndef COMMON_UTILS_INOTIFY_HPP
+#define COMMON_UTILS_INOTIFY_HPP
+
+#include "ipc/epoll/event-poll.hpp"
+
+#include <functional>
+#include <mutex>
+#include <vector>
+
+#include <sys/inotify.h>
+
+
+namespace utils {
+
+/**
+ * Inotify monitors a directory and when a specified file or folder
+ * is created or deleted it calls a corresponding handler.
+ */
+class Inotify {
+public:
+    typedef std::function<void(const std::string&, const uint32_t)> Callback;
+
+    Inotify(ipc::epoll::EventPoll& eventPoll);
+    virtual ~Inotify();
+
+    Inotify(const Inotify&) = delete;
+    Inotify& operator=(const Inotify&) = delete;
+
+    /**
+     * Add a callback for a specified path
+     */
+    void setHandler(const std::string& path, const uint32_t eventMask, const Callback&& callback);
+
+    /**
+     * Stop watching the path
+     */
+    void removeHandler(const std::string& path);
+
+    /**
+     * @return inotify file descriptor
+     */
+    int getFD() const;
+
+private:
+    struct Handler {
+        std::string path;
+        int watchID;
+        Callback call;
+    };
+
+    typedef std::lock_guard<std::recursive_mutex> Lock;
+    std::recursive_mutex mMutex;
+
+    int mFD;
+    ipc::epoll::EventPoll& mEventPoll;
+    std::vector<Handler> mHandlers;
+
+    void handleInternal();
+    void removeHandlerInternal(const std::string& path);
+};
+
+} // namespace utils
+
+#endif // COMMON_UTILS_INOTIFY_HPP
index 26ebde6..2757130 100644 (file)
@@ -87,7 +87,7 @@ inline void removeTrailingSlash(std::string& path)
     }
 }
 
-} // anonymous namespace
+} // namespace
 
 /*
  * Gets the dir name of a file path, analogous to dirname(1)
@@ -118,7 +118,6 @@ inline std::string getAbsolutePath(const std::string& path, const std::string& b
     }
 }
 
-
 } // namespace utils
 
 
index cc72ed9..ea62e70 100644 (file)
@@ -19,7 +19,7 @@
 /**
  * @file
  * @author  Jan Olszak (j.olszak@samsung.com)
- * @brief   Eventfd wrapper
+ * @brief   Signalfd wrapper
  */
 
 #ifndef COMMON_UTILS_SIGNALFD_HPP
@@ -49,7 +49,7 @@ public:
     typedef std::function<void(const int sigNum)> Callback;
 
     SignalFD(ipc::epoll::EventPoll& eventPoll);
-    ~SignalFD();
+    virtual ~SignalFD();
 
     SignalFD(const SignalFD& signalfd) = delete;
     SignalFD& operator=(const SignalFD&) = delete;
diff --git a/tests/unit_tests/utils/ut-inotify.cpp b/tests/unit_tests/utils/ut-inotify.cpp
new file mode 100644 (file)
index 0000000..31b9e54
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ *  Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Contact: Jan Olszak <j.olszak@samsung.com>
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+
+/**
+ * @file
+ * @author  Jan Olszak (j.olszak@samsung.com)
+ * @brief   Unit tests of Inotify
+ */
+
+#include "config.hpp"
+#include "ut.hpp"
+
+#include "utils/inotify.hpp"
+#include "utils/fs.hpp"
+#include "utils/scoped-dir.hpp"
+#include "utils/value-latch.hpp"
+
+#include "logger/logger.hpp"
+
+#include "ipc/epoll/event-poll.hpp"
+#include "ipc/epoll/thread-dispatcher.hpp"
+
+#include <boost/filesystem.hpp>
+
+using namespace utils;
+namespace fs = boost::filesystem;
+
+namespace {
+
+const std::string TEST_DIR  = "/tmp/ut-inotify/";
+const std::string DIR_NAME  = "dir";
+const std::string FILE_NAME = "file.txt";
+
+const std::string DIR_PATH  = TEST_DIR + DIR_NAME;
+const std::string FILE_PATH = TEST_DIR + FILE_NAME;
+
+
+struct Fixture {
+    utils::ScopedDir mTestDir;
+
+    Fixture()
+        :mTestDir(TEST_DIR)
+    {}
+};
+
+} // namespace
+
+
+BOOST_FIXTURE_TEST_SUITE(InotifySuite, Fixture)
+
+BOOST_AUTO_TEST_CASE(ConstructorDesctructor)
+{
+    ipc::epoll::EventPoll poll;
+    Inotify i(poll);
+}
+
+BOOST_AUTO_TEST_CASE(CreateDeleteFileHandler)
+{
+    ipc::epoll::ThreadDispatcher dispatcher;
+    Inotify i(dispatcher.getPoll());
+
+    // Callback on creation
+    ValueLatch<std::string> createResult;
+    i.setHandler(TEST_DIR, IN_CREATE, [&](const std::string& name, uint32_t) {
+        createResult.set(name);
+    });
+    utils::createFile(FILE_PATH, O_WRONLY | O_CREAT, 0666);
+    BOOST_REQUIRE_EQUAL(createResult.get(), FILE_NAME);
+
+    // Redefine the callback for delete
+    ValueLatch<std::string> deleteResult;
+    i.setHandler(TEST_DIR, IN_DELETE, [&](const std::string& name, uint32_t) {
+        deleteResult.set(name);
+    });
+    fs::remove(FILE_PATH);
+    BOOST_REQUIRE_EQUAL(deleteResult.get(), FILE_NAME);
+}
+
+BOOST_AUTO_TEST_CASE(CreateDeleteDirHandler)
+{
+    ipc::epoll::ThreadDispatcher dispatcher;
+    Inotify i(dispatcher.getPoll());
+
+    // Callback on creation
+    ValueLatch<std::string> createResult;
+    i.setHandler(TEST_DIR, IN_CREATE, [&](const std::string& name, uint32_t) {
+        createResult.set(name);
+    });
+    utils::createEmptyDir(DIR_PATH);
+    BOOST_REQUIRE_EQUAL(createResult.get(), DIR_NAME);
+
+
+    // Redefine the callback for delete
+    ValueLatch<std::string> deleteResult;
+    i.setHandler(TEST_DIR, IN_DELETE, [&](const std::string& name, uint32_t) {
+        deleteResult.set(name);
+    });
+    fs::remove_all(DIR_PATH);
+    BOOST_REQUIRE_EQUAL(deleteResult.get(), DIR_NAME);
+}
+
+BOOST_AUTO_TEST_CASE(NoFalseEventHandler)
+{
+    ipc::epoll::ThreadDispatcher dispatcher;
+    Inotify i(dispatcher.getPoll());
+
+    utils::createFile(FILE_PATH, O_WRONLY | O_CREAT, 0666);
+
+    // Callback on creation shouldn't be called
+    ValueLatch<std::string> createResult;
+    i.setHandler(TEST_DIR, IN_CREATE, [&](const std::string& name, uint32_t) {
+        createResult.set(name);
+    });
+    fs::remove(FILE_PATH);
+    BOOST_REQUIRE_THROW(createResult.get(10), UtilsException);
+}
+
+BOOST_AUTO_TEST_CASE(RemoveHandler)
+{
+    ipc::epoll::ThreadDispatcher dispatcher;
+    Inotify i(dispatcher.getPoll());
+
+    // Callback on creation
+    ValueLatch<std::string> createResult;
+    i.setHandler(TEST_DIR, IN_CREATE, [&](const std::string& name, uint32_t) {
+        createResult.set(name);
+    });
+    i.removeHandler(TEST_DIR);
+    utils::createFile(FILE_PATH, O_WRONLY | O_CREAT, 0666);
+    fs::remove(FILE_PATH);
+    BOOST_REQUIRE_THROW(createResult.get(10), UtilsException);
+}
+
+BOOST_AUTO_TEST_SUITE_END()