Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File monitoring (inotify) support #361

Merged
merged 23 commits into from
Dec 28, 2024
Merged
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
Loading