Skip to content

Commit

Permalink
File monitoring (inotify) support (#361)
Browse files Browse the repository at this point in the history
* add inotify

* add file lock

* complete implementation

* fix type

* fix

* format

* add atomic

* make explicit

* use io error

* reduce nest

* rename

* remove lock

* test monitor

* fix pre-commit

* link stdc++fs

* fix blocking issue

* fix sonar issues

* increase wait time

* use userPtr in tests

* fix clang-tidy issues

* more clang-tidy issues

* remove move

---------

Co-authored-by: Ege Çetin <ege.cetin@pavotek.com.tr>
  • Loading branch information
egecetin and Ege Çetin authored Dec 28, 2024
1 parent a2c67d1 commit dc26aa7
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
67 changes: 67 additions & 0 deletions include/utils/FileHelpers.hpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#pragma once

#include <atomic>
#include <filesystem>
#include <fstream>
#include <regex>
#include <string>
#include <sys/inotify.h>
#include <thread>
#include <vector>

/**
Expand Down Expand Up @@ -51,3 +55,66 @@ inline std::vector<std::string> findFromFile(const std::string &filePath, const
std::string lastWord;
return findFromFile(filePath, pattern, lastWord);
}

/// Callback function for file notifications
using FNotifyCallback = std::function<void(const void *)>;

/**
* 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<std::thread> _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();
};
125 changes: 125 additions & 0 deletions src/utils/FileHelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#include "utils/FileHelpers.hpp"

#include "utils/ErrorHelpers.hpp"

#include <fcntl.h>
#include <iostream>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <spdlog/spdlog.h>

/// 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<char>(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<inotify_event *>(&buffer[static_cast<size_t>(idx)]);

// Check if file notify type matches
if ((event->mask & _notifyEvents) != 0)
{
_notifyCallback(_userPtr);
break;
}

idx += static_cast<ssize_t>(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<std::thread>(&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;
}
}
21 changes: 21 additions & 0 deletions tests/unittests/Utils_UnitTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const int *>(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)
Expand Down

0 comments on commit dc26aa7

Please sign in to comment.