From d56d6393f67cd587e0d3e8c18a57a4f49a701c2c Mon Sep 17 00:00:00 2001 From: Jan Olszak Date: Mon, 19 Oct 2015 14:49:45 +0200 Subject: [PATCH] common: Inotify implementation [Feature] Asynchronous file system events [Cause] N/A [Solution] N/A [Verification] Build, install and run tests Change-Id: I53b1d75026550cf3563dce7fb1ea50eede4fb5cb --- common/utils/eventfd.hpp | 2 +- common/utils/fs.cpp | 53 +++++++++--- common/utils/fs.hpp | 5 ++ common/utils/inotify.cpp | 151 ++++++++++++++++++++++++++++++++++ common/utils/inotify.hpp | 88 ++++++++++++++++++++ common/utils/paths.hpp | 3 +- common/utils/signalfd.hpp | 4 +- tests/unit_tests/utils/ut-inotify.cpp | 150 +++++++++++++++++++++++++++++++++ 8 files changed, 439 insertions(+), 17 deletions(-) create mode 100644 common/utils/inotify.cpp create mode 100644 common/utils/inotify.hpp create mode 100644 tests/unit_tests/utils/ut-inotify.cpp diff --git a/common/utils/eventfd.hpp b/common/utils/eventfd.hpp index f49bbff..698847d 100644 --- a/common/utils/eventfd.hpp +++ b/common/utils/eventfd.hpp @@ -31,7 +31,7 @@ class EventFD { public: EventFD(); - ~EventFD(); + virtual ~EventFD(); EventFD(const EventFD& eventfd) = delete; EventFD& operator=(const EventFD&) = delete; diff --git a/common/utils/fs.cpp b/common/utils/fs.cpp index a7a979a..62e6c37 100644 --- a/common/utils/fs.cpp +++ b/common/utils/fs.cpp @@ -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) { diff --git a/common/utils/fs.hpp b/common/utils/fs.hpp index bff2e70..2237450 100644 --- a/common/utils/fs.hpp +++ b/common/utils/fs.hpp @@ -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& files); diff --git a/common/utils/inotify.cpp b/common/utils/inotify.cpp new file mode 100644 index 0000000..a94d95b --- /dev/null +++ b/common/utils/inotify.cpp @@ -0,0 +1,151 @@ +/* +* Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +* +* Contact: Jan Olszak +* +* 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 + +#include + + +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 buffer(bufferSize); + utils::read(mFD, buffer.data(), bufferSize); + + // Handle all events + unsigned int offset = 0; + while (offset < bufferSize) { + struct ::inotify_event *event = reinterpret_cast(&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 index 0000000..15c90b8 --- /dev/null +++ b/common/utils/inotify.hpp @@ -0,0 +1,88 @@ +/* +* Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved +* +* Contact: Jan Olszak +* +* 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 +#include +#include + +#include + + +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 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 Lock; + std::recursive_mutex mMutex; + + int mFD; + ipc::epoll::EventPoll& mEventPoll; + std::vector mHandlers; + + void handleInternal(); + void removeHandlerInternal(const std::string& path); +}; + +} // namespace utils + +#endif // COMMON_UTILS_INOTIFY_HPP diff --git a/common/utils/paths.hpp b/common/utils/paths.hpp index 26ebde6..2757130 100644 --- a/common/utils/paths.hpp +++ b/common/utils/paths.hpp @@ -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 diff --git a/common/utils/signalfd.hpp b/common/utils/signalfd.hpp index cc72ed9..ea62e70 100644 --- a/common/utils/signalfd.hpp +++ b/common/utils/signalfd.hpp @@ -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 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 index 0000000..31b9e54 --- /dev/null +++ b/tests/unit_tests/utils/ut-inotify.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2015 Samsung Electronics Co., Ltd All Rights Reserved + * + * Contact: Jan Olszak + * + * 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 + +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 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 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 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 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 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 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() -- 2.7.4