diff --git a/CMakeLists.txt b/CMakeLists.txt index 4caecbc9..a38a410f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,7 @@ file( ${PROJECT_SOURCE_DIR}/src/utils/BaseServerStats.cpp ${PROJECT_SOURCE_DIR}/src/utils/ConfigParser.cpp ${PROJECT_SOURCE_DIR}/src/utils/ErrorHelpers.cpp + ${PROJECT_SOURCE_DIR}/src/utils/FileHelpers.cpp ${PROJECT_SOURCE_DIR}/src/utils/Tracer.cpp ${PROJECT_SOURCE_DIR}/src/zeromq/ZeroMQ.cpp ${PROJECT_SOURCE_DIR}/src/zeromq/ZeroMQMonitor.cpp @@ -135,7 +136,7 @@ target_compile_options(${PROJECT_NAME}-lib PRIVATE -Wall -Wextra -g -Wl,--build- target_include_directories(${PROJECT_NAME}-lib PRIVATE ${PROJECT_BINARY_DIR}) target_link_libraries( ${PROJECT_NAME}-lib - PUBLIC cppzmq crashpad::client CURL::libcurl prometheus-cpp::pull sentry::sentry spdlog::spdlog + PUBLIC cppzmq crashpad::client CURL::libcurl prometheus-cpp::pull sentry::sentry spdlog::spdlog stdc++fs ) enable_security_flags_for_target(${PROJECT_NAME}-lib) diff --git a/include/utils/FileHelpers.hpp b/include/utils/FileHelpers.hpp index 9a55bff9..2fb7e5aa 100644 --- a/include/utils/FileHelpers.hpp +++ b/include/utils/FileHelpers.hpp @@ -1,8 +1,12 @@ #pragma once +#include +#include #include #include #include +#include +#include #include /** @@ -51,3 +55,66 @@ inline std::vector findFromFile(const std::string &filePath, const std::string lastWord; return findFromFile(filePath, pattern, lastWord); } + +/// Callback function for file notifications +using FNotifyCallback = std::function; + +/** + * Invokes functions for a file for given notify events + */ +class FileMonitor { + private: + /// File descriptor + int _fDescriptor{-1}; + /// Watch descriptor + int _wDescriptor{-1}; + /// File path + std::filesystem::path _filePath; + /// Callback function + FNotifyCallback _notifyCallback; + /// Notify types + uint32_t _notifyEvents; + /// User pointer + const void *_userPtr = nullptr; + + /// Thread + std::unique_ptr _thread; + /// Flag to stop monitoring + std::atomic_flag _shouldStop{false}; + + void threadFunc() const noexcept; + + public: + /** + * Constructor + * @param[in] filePath Path to the file + * @param[in] notifyEvents Events to notify + */ + explicit FileMonitor(std::filesystem::path filePath, uint32_t notifyEvents = IN_MODIFY); + + /// @brief Copy constructor + FileMonitor(const FileMonitor & /*unused*/) = delete; + + /// @brief Move constructor + FileMonitor(FileMonitor && /*unused*/) = delete; + + /// @brief Copy assignment operator + FileMonitor &operator=(FileMonitor /*unused*/) = delete; + + /// @brief Move assignment operator + FileMonitor &operator=(FileMonitor && /*unused*/) = delete; + + [[nodiscard]] FNotifyCallback notifyCallback() const { return _notifyCallback; } + void notifyCallback(FNotifyCallback func) { _notifyCallback = std::move(func); } + + /** + * Sets user pointer + * @param[in] ptr User pointer + */ + void userPtr(const void *ptr) { _userPtr = ptr; } + + /** + * Destructor + */ + ~FileMonitor(); +}; diff --git a/src/utils/FileHelpers.cpp b/src/utils/FileHelpers.cpp new file mode 100644 index 00000000..bc2cb039 --- /dev/null +++ b/src/utils/FileHelpers.cpp @@ -0,0 +1,125 @@ +#include "utils/FileHelpers.hpp" + +#include "utils/ErrorHelpers.hpp" + +#include +#include +#include +#include +#include + +#include + +/// Sleep interval for the file monitor +constexpr int SLEEP_INTERVAL_MS = 50; + +void FileMonitor::threadFunc() const noexcept +{ + while (!_shouldStop._M_i) + { + // Buffer for reading events + unsigned int nBytes = 0; + if (ioctl(_fDescriptor, FIONREAD, &nBytes) < 0) + { + spdlog::error("Failed to get available events for file monitoring: {}", getErrnoString(errno)); + } + + auto buffer = std::vector(nBytes + 1, '\0'); + auto nRead = read(_fDescriptor, buffer.data(), nBytes); + if (nRead < 0) + { + spdlog::error("Failed to read events for file monitoring: {}", getErrnoString(errno)); + } + else if (nRead == 0) + { + spdlog::debug("No events read for file monitoring"); + } + + ssize_t idx = 0; + while (_notifyCallback && idx < nRead) + { + const auto *event = reinterpret_cast(&buffer[static_cast(idx)]); + + // Check if file notify type matches + if ((event->mask & _notifyEvents) != 0) + { + _notifyCallback(_userPtr); + break; + } + + idx += static_cast(sizeof(inotify_event) + event->len); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(SLEEP_INTERVAL_MS)); + } +} + +FileMonitor::FileMonitor(std::filesystem::path filePath, uint32_t notifyEvents) + : _fDescriptor(inotify_init()), _filePath(std::move(filePath)), _notifyEvents(notifyEvents) +{ + if (_fDescriptor < 0) + { + throw std::ios_base::failure("Failed to initialize inotify"); + } + + _wDescriptor = inotify_add_watch(_fDescriptor, _filePath.c_str(), notifyEvents); + if (_wDescriptor < 0) + { + close(_fDescriptor); + throw std::ios_base::failure("Failed to add watch descriptor"); + } + + if (fcntl(_fDescriptor, F_SETFL, fcntl(_fDescriptor, F_GETFL) | O_NONBLOCK) < 0) + { + close(_fDescriptor); + throw std::ios_base::failure("Failed to set file descriptor to non-blocking mode"); + } + + _thread = std::make_unique(&FileMonitor::threadFunc, this); +} + +FileMonitor::~FileMonitor() +{ + _shouldStop.test_and_set(); + if (_thread && _thread->joinable()) + { + _thread->join(); + _thread.reset(); + } + + // Remove watch descriptor first + if (_wDescriptor >= 0) + { + if (inotify_rm_watch(_fDescriptor, _wDescriptor) < 0) + { + try + { + spdlog::error("Failed to remove watch descriptor: {}", getErrnoString(errno)); + } + catch (const std::exception &e) + { + std::cerr << "Failed to remove watch descriptor and also logger thrown an exception: " + << getErrnoString(errno) << " " << e.what() << '\n'; + } + } + _wDescriptor = -1; + } + + // Then close the file descriptor + if (_fDescriptor >= 0) + { + if (close(_fDescriptor) < 0) + { + try + { + spdlog::error("Failed to close file descriptor: {}", getErrnoString(errno)); + } + catch (const std::exception &e) + { + std::cerr << "Failed to close file descriptor and also logger thrown an exception: " + << getErrnoString(errno) << " " << e.what() << '\n'; + } + } + _fDescriptor = -1; + } +} diff --git a/tests/unittests/Utils_UnitTests.cpp b/tests/unittests/Utils_UnitTests.cpp index 65a10c10..12d0454e 100644 --- a/tests/unittests/Utils_UnitTests.cpp +++ b/tests/unittests/Utils_UnitTests.cpp @@ -71,6 +71,27 @@ TEST(Utils_Tests, FileHelpersUnitTests) readLines = findFromFile(TEST_DATA_READ_PATH, "^dummy", word); ASSERT_TRUE(readLines.empty()); + + FileMonitor monitor(TEST_DATA_READ_PATH, IN_OPEN); + int val = 0xFF; + monitor.userPtr(&val); + + bool isInvoked = false; + bool isAccessed = false; + monitor.notifyCallback([&isInvoked, &isAccessed](const void *ptr) { + isInvoked = true; + if (ptr) + { + isAccessed = *(static_cast(ptr)) == 0xFF; + } + }); + + std::ifstream file(TEST_DATA_READ_PATH); + ASSERT_TRUE(file.is_open()); + + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + ASSERT_TRUE(isInvoked); + ASSERT_TRUE(isAccessed); } TEST(Utils_Tests, InputParserUnitTests)