├── test ├── assets │ └── test.txt ├── unit │ ├── main.cpp │ ├── EventTests.cpp │ ├── CMakeLists.txt │ └── NotifierBuilderTests.cpp └── CMakeLists.txt ├── .github ├── FUNDING.yml └── workflows │ ├── example.yml │ └── build_and_test.yml ├── src ├── inotify-cppConfig.cmake ├── Notification.cpp ├── FileSystemEvent.cpp ├── include │ └── inotify-cpp │ │ ├── Notification.h │ │ ├── FileSystemEvent.h │ │ ├── FileSystemAdapter.h │ │ ├── Event.h │ │ ├── NotifierBuilder.h │ │ └── Inotify.h ├── Event.cpp ├── CMakeLists.txt ├── NotifierBuilder.cpp └── Inotify.cpp ├── .gitignore ├── .clang-format ├── example ├── CMakeLists.txt └── main.cpp ├── LICENSE ├── CMakeLists.txt └── README.md /test/assets/test.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: erikzenker 2 | -------------------------------------------------------------------------------- /test/unit/main.cpp: -------------------------------------------------------------------------------- 1 | #define BOOST_TEST_DYN_LINK 2 | #define BOOST_TEST_MODULE "Inotify-cpp unit tests" 3 | #include 4 | 5 | -------------------------------------------------------------------------------- /src/inotify-cppConfig.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeFindDependencyMacro) 2 | find_dependency(Boost 1.54.0) 3 | include("${CMAKE_CURRENT_LIST_DIR}/inotify-cppTargets.cmake") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeCache.txt 2 | **/**/CMakeFiles 3 | **/**/Makefile 4 | example/inotify_example 5 | source/libinotify-cpp.a 6 | test/unit/inotify_unit_test 7 | Testing/ 8 | test/unit/testDirectory/ 9 | install_manifest.txt 10 | .idea/ 11 | **/build/ 12 | cmake-build-debug/ 13 | .vscode -------------------------------------------------------------------------------- /src/Notification.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace inotify { 4 | 5 | Notification::Notification( 6 | const Event& event, 7 | const inotifypp::filesystem::path& path, 8 | std::chrono::steady_clock::time_point time) 9 | : event(event) 10 | , path(path) 11 | , time(time) 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | Language: Cpp 3 | NamespaceIndentation: None 4 | DerivePointerAlignment: false 5 | PointerAlignment: Left 6 | ColumnLimit: 100 7 | Standard: Cpp11 8 | AccessModifierOffset: -2 9 | AllowShortFunctionsOnASingleLine: false 10 | BinPackArguments: false 11 | BinPackParameters: false 12 | AlignAfterOpenBracket: AlwaysBreak -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(inotify-cppTests) 3 | 4 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -Wpedantic -g3 -O0") 5 | 6 | if(${INOTIFY_CLANG_COVERAGE}) 7 | set(CMAKE_CXX_FLAGS "-fprofile-instr-generate -fcoverage-mapping") 8 | endif() 9 | 10 | if(${INOTIFY_GCC_COVERAGE}) 11 | set(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") 12 | endif() 13 | 14 | add_subdirectory(unit) 15 | -------------------------------------------------------------------------------- /src/FileSystemEvent.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace inotify { 6 | FileSystemEvent::FileSystemEvent( 7 | const int wd, 8 | uint32_t mask, 9 | const inotifypp::filesystem::path& path, 10 | const std::chrono::steady_clock::time_point& eventTime) 11 | : wd(wd) 12 | , mask(mask) 13 | , path(path) 14 | , eventTime(eventTime) 15 | { 16 | } 17 | 18 | FileSystemEvent::~FileSystemEvent() 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/include/inotify-cpp/Notification.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace inotify { 9 | 10 | class Notification { 11 | public: 12 | Notification( 13 | const Event& event, 14 | const inotifypp::filesystem::path& path, 15 | std::chrono::steady_clock::time_point time); 16 | 17 | public: 18 | const Event event; 19 | const inotifypp::filesystem::path path; 20 | const std::chrono::steady_clock::time_point time; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/include/inotify-cpp/FileSystemEvent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace inotify { 8 | class FileSystemEvent { 9 | public: 10 | FileSystemEvent( 11 | int wd, 12 | uint32_t mask, 13 | const inotifypp::filesystem::path& path, 14 | const std::chrono::steady_clock::time_point& eventTime); 15 | 16 | ~FileSystemEvent(); 17 | 18 | public: 19 | int wd; 20 | uint32_t mask; 21 | inotifypp::filesystem::path path; 22 | std::chrono::steady_clock::time_point eventTime; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /test/unit/EventTests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | 7 | using namespace inotify; 8 | 9 | BOOST_AUTO_TEST_CASE(shouldCalculateEventsCorrectly) 10 | { 11 | BOOST_CHECK_EQUAL(8, static_cast(Event::close_write)); 12 | BOOST_CHECK_EQUAL(16, static_cast(Event::close_nowrite)); 13 | BOOST_CHECK_EQUAL(24, static_cast(Event::close)); 14 | BOOST_CHECK_EQUAL(Event::close, Event::close_nowrite | Event::close_write); 15 | } 16 | 17 | BOOST_AUTO_TEST_CASE(shouldOutputEventsCorrectly) 18 | { 19 | std::stringstream eventSStream; 20 | eventSStream << Event::close; 21 | BOOST_CHECK_EQUAL("close (close_write | close_nowrite) ", eventSStream.str()); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | name: Example 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install Dependencies 17 | run: sudo apt install cmake libboost-all-dev gcc 18 | - name: Print System Information 19 | run: | 20 | cmake --version 21 | g++ --version 22 | cat /usr/include/boost/version.hpp 23 | - name: Build 24 | env: 25 | CXX: g++ 26 | CC: gcc 27 | run: | 28 | mkdir build && cd build 29 | cmake .. -DBUILD_TEST=OFF 30 | cmake --build . --target inotify_example 31 | - name: Run example 32 | run: | 33 | ./build/example/inotify_example 34 | -------------------------------------------------------------------------------- /src/include/inotify-cpp/FileSystemAdapter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if __cplusplus >= 201703L && !defined(USE_BOOST_FILESYSTEM) 4 | 5 | #include 6 | #include 7 | 8 | namespace inotifypp 9 | { 10 | namespace filesystem = std::filesystem; 11 | 12 | using error_code = std::error_code; 13 | 14 | template 15 | using optional = std::optional; 16 | 17 | inline constexpr auto nullopt() { return std::nullopt; } 18 | 19 | } 20 | 21 | #else 22 | 23 | #include 24 | #include 25 | 26 | namespace inotifypp 27 | { 28 | namespace filesystem = boost::filesystem; 29 | 30 | using error_code = boost::system::error_code; 31 | 32 | template 33 | using optional = boost::optional; 34 | 35 | inline constexpr boost::none_t nullopt() { return boost::none; }; 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(inotify-cppExample) 3 | 4 | ############################################################################### 5 | # INOTIFY-CPP 6 | ############################################################################### 7 | if(NOT TARGET inotify-cpp::inotify-cpp) 8 | find_package(inotify-cpp CONFIG REQUIRED) 9 | endif() 10 | 11 | ############################################################################### 12 | # Thread 13 | ############################################################################### 14 | find_package(Threads) 15 | 16 | ############################################################################### 17 | # Target 18 | ############################################################################### 19 | add_executable(inotify_example main.cpp) 20 | target_link_libraries(inotify_example 21 | PRIVATE 22 | inotify-cpp::inotify-cpp 23 | ${CMAKE_THREAD_LIBS_INIT}) 24 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install Dependencies 17 | run: sudo apt install cmake libboost-all-dev libboost-test-dev gcc 18 | - name: Print System Information 19 | run: | 20 | cmake --version 21 | g++ --version 22 | cat /usr/include/boost/version.hpp 23 | - name: Build 24 | env: 25 | CXX: g++ 26 | CC: gcc 27 | run: | 28 | mkdir build && cd build 29 | cmake .. -DINOTIFY_GCC_COVERAGE=1 30 | cmake --build . 31 | - name: Test 32 | run: | 33 | cd build 34 | ctest -VV 35 | - name: Generate Coverage Report 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | run: | 39 | cd build 40 | bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' 41 | -------------------------------------------------------------------------------- /test/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(inotify-cppUnitTests) 3 | 4 | ############################################################################### 5 | # INOTIFY-CPP 6 | ############################################################################### 7 | if(NOT TARGET inotify-cpp::inotify-cpp) 8 | find_package(inotify-cpp CONFIG REQUIRED) 9 | endif() 10 | 11 | ############################################################################### 12 | # Thread 13 | ############################################################################### 14 | find_package(Threads) 15 | 16 | ############################################################################### 17 | # Test 18 | ############################################################################### 19 | add_executable(inotify_unit_test main.cpp NotifierBuilderTests.cpp EventTests.cpp) 20 | target_link_libraries(inotify_unit_test 21 | PRIVATE 22 | inotify-cpp::inotify-cpp 23 | Boost::unit_test_framework 24 | ${CMAKE_THREAD_LIBS_INIT}) 25 | 26 | add_test(NAME inotify_unit_test COMMAND inotify_unit_test) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Erik Zenker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/include/inotify-cpp/Event.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | namespace inotify { 8 | 9 | enum class Event : std::uint32_t { 10 | access = IN_ACCESS, 11 | attrib = IN_ATTRIB, 12 | close_write = IN_CLOSE_WRITE, 13 | close_nowrite = IN_CLOSE_NOWRITE, 14 | close = IN_CLOSE, 15 | create = IN_CREATE, 16 | remove = IN_DELETE, 17 | remove_self = IN_DELETE_SELF, 18 | modify = IN_MODIFY, 19 | move_self = IN_MOVE_SELF, 20 | moved_from = IN_MOVED_FROM, 21 | moved_to = IN_MOVED_TO, 22 | move = IN_MOVE, 23 | open = IN_OPEN, 24 | is_dir = IN_ISDIR, 25 | unmount = IN_UNMOUNT, 26 | q_overflow = IN_Q_OVERFLOW, 27 | ignored = IN_IGNORED, 28 | oneshot = IN_ONESHOT, 29 | all = IN_ALL_EVENTS 30 | }; 31 | constexpr Event operator&(Event lhs, Event rhs) 32 | { 33 | return static_cast( 34 | static_cast::type>(lhs) 35 | & static_cast::type>(rhs)); 36 | } 37 | 38 | constexpr Event operator|(Event lhs, Event rhs) 39 | { 40 | return static_cast( 41 | static_cast::type>(lhs) 42 | | static_cast::type>(rhs)); 43 | } 44 | 45 | std::ostream& operator<<(std::ostream& stream, const Event& event); 46 | bool containsEvent(const Event& allEvents, const Event& event); 47 | } -------------------------------------------------------------------------------- /src/include/inotify-cpp/NotifierBuilder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace inotify { 11 | 12 | using EventObserver = std::function; 13 | 14 | class NotifierBuilder { 15 | public: 16 | NotifierBuilder(); 17 | 18 | auto run() -> void; 19 | auto runOnce() -> void; 20 | auto stop() -> void; 21 | auto watchPathRecursively(inotifypp::filesystem::path path) -> NotifierBuilder&; 22 | auto watchFile(inotifypp::filesystem::path file) -> NotifierBuilder&; 23 | auto unwatchFile(inotifypp::filesystem::path file) -> NotifierBuilder&; 24 | auto ignoreFileOnce(inotifypp::filesystem::path file) -> NotifierBuilder&; 25 | auto ignoreFile(inotifypp::filesystem::path file) -> NotifierBuilder&; 26 | auto onEvent(Event event, EventObserver) -> NotifierBuilder&; 27 | auto onEvents(std::vector event, EventObserver) -> NotifierBuilder&; 28 | auto onUnexpectedEvent(EventObserver) -> NotifierBuilder&; 29 | auto setEventTimeout(std::chrono::milliseconds timeout, EventObserver eventObserver) 30 | -> NotifierBuilder&; 31 | 32 | private: 33 | std::shared_ptr mInotify; 34 | std::map mEventObserver; 35 | EventObserver mUnexpectedEventObserver; 36 | }; 37 | 38 | NotifierBuilder BuildNotifier(); 39 | } 40 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | project(inotify-cpp) 3 | 4 | if(NOT CMAKE_CXX_STANDARD) 5 | set(CMAKE_CXX_STANDARD 17) 6 | endif() 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | 9 | option(BUILD_EXAMPLE "Build inotify-cpp example program" ON) 10 | option(BUILD_TEST "Build inotify-cpp unittest program" ON) 11 | option(BUILD_SHARED_LIBS "Build inotify-cpp as a shared library" ON) 12 | option(BUILD_STATIC_LIBS "Build inotify-cpp as a static library" OFF) 13 | option(USE_BOOST_FILESYSTEM "Build with boost::filesystem" OFF) 14 | 15 | if(USE_BOOST_FILESYSTEM) 16 | list(APPEND USED_BOOST_LIBS filesystem) 17 | endif() 18 | if(BUILD_TEST) 19 | list(APPEND USED_BOOST_LIBS unit_test_framework) 20 | endif() 21 | 22 | find_package(Boost 1.54.0 COMPONENTS ${USED_BOOST_LIBS} REQUIRED) 23 | 24 | add_library(inotify-filesystem-adapter INTERFACE) 25 | if(CMAKE_CXX_STANDARD LESS 17 OR USE_BOOST_FILESYSTEM) 26 | add_definitions(-DUSE_BOOST_FILESYSTEM) 27 | target_link_libraries(inotify-filesystem-adapter INTERFACE Boost::filesystem) 28 | else() 29 | target_link_libraries(inotify-filesystem-adapter INTERFACE stdc++fs) 30 | endif() 31 | 32 | add_subdirectory(src) 33 | 34 | if(BUILD_EXAMPLE) 35 | add_subdirectory(example) 36 | endif() 37 | 38 | if(BUILD_TEST) 39 | enable_testing() 40 | add_subdirectory(test) 41 | endif() 42 | 43 | message(STATUS "") 44 | message(STATUS "") 45 | message(STATUS "${PROJECT_NAME} configuration summary:") 46 | message(STATUS "") 47 | message(STATUS " CMake build type ................ : ${CMAKE_BUILD_TYPE}") 48 | message(STATUS " Build shared libs .............. : ${BUILD_SHARED_LIBS}") 49 | message(STATUS " Build static libs .............. : ${BUILD_STATIC_LIBS}") 50 | message(STATUS " Build example .................. : ${BUILD_EXAMPLE}") 51 | message(STATUS " Build test ...................... : ${BUILD_TEST}") 52 | message(STATUS " Build c++ standard .............. : ${CMAKE_CXX_STANDARD}") 53 | message(STATUS " Build with boost::filesystem .... : ${USE_BOOST_FILESYSTEM}") 54 | message(STATUS "") 55 | message(STATUS " Dependencies:") 56 | message(STATUS " Boost version.................... : ${Boost_VERSION}") 57 | message(STATUS " Boost root....................... : ${Boost_DIR}") 58 | message(STATUS "") 59 | -------------------------------------------------------------------------------- /example/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace inotify; 9 | 10 | int main(int argc, char** argv) 11 | { 12 | if (argc <= 1) { 13 | std::cout << "Usage: ./inotify_example /path/to/dir" << std::endl; 14 | exit(0); 15 | } 16 | 17 | // Parse the directory to watch 18 | inotifypp::filesystem::path path(argv[1]); 19 | 20 | // Set the event handler which will be used to process particular events 21 | auto handleNotification = [&](Notification notification) { 22 | std::cout << "Event " << notification.event << " on " << notification.path << " at " 23 | << notification.time.time_since_epoch().count() << " was triggered." << std::endl; 24 | }; 25 | 26 | // Set the a separate unexpected event handler for all other events. An exception is thrown by 27 | // default. 28 | auto handleUnexpectedNotification = [](Notification notification) { 29 | std::cout << "Event " << notification.event << " on " << notification.path << " at " 30 | << notification.time.time_since_epoch().count() 31 | << " was triggered, but was not expected" << std::endl; 32 | }; 33 | 34 | // Set the events to be notified for 35 | auto events = { Event::open | Event::is_dir, // some events occur in combinations 36 | Event::access, 37 | Event::create, 38 | Event::modify, 39 | Event::remove, 40 | Event::move }; 41 | 42 | // The notifier is configured to watch the parsed path for the defined events. Particular files 43 | // or paths can be ignored(once). 44 | auto notifier = BuildNotifier() 45 | .watchPathRecursively(path) 46 | .ignoreFileOnce("fileIgnoredOnce") 47 | .ignoreFile("fileIgnored") 48 | .onEvents(events, handleNotification) 49 | .onUnexpectedEvent(handleUnexpectedNotification); 50 | 51 | // The event loop is started in a separate thread context. 52 | std::thread thread([&](){ notifier.run(); }); 53 | 54 | // Terminate the event loop after 60 seconds 55 | std::this_thread::sleep_for(std::chrono::seconds(60)); 56 | notifier.stop(); 57 | thread.join(); 58 | return 0; 59 | } 60 | -------------------------------------------------------------------------------- /src/Event.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace inotify { 8 | 9 | bool containsEvent(const Event& allEvents, const Event& event) 10 | { 11 | if (static_cast(event & allEvents)) { 12 | return true; 13 | } else { 14 | return false; 15 | } 16 | } 17 | 18 | std::ostream& operator<<(std::ostream& stream, const Event& event) 19 | { 20 | std::string maskString = ""; 21 | 22 | // Close is an combined helper event so we need to check its parts separatetly 23 | if (containsEvent(event, Event::close_write) && containsEvent(event, Event::close_nowrite)) { 24 | maskString.append("close (close_write | close_nowrite) "); 25 | } else { 26 | if (containsEvent(event, Event::close_write)) 27 | maskString.append("close_write "); 28 | if (containsEvent(event, Event::close_nowrite)) 29 | maskString.append("close_nowrite "); 30 | } 31 | 32 | // Move is an combined helper event so we need to check its parts separatetly 33 | if (containsEvent(event, Event::moved_from) && containsEvent(event, Event::moved_to)) { 34 | maskString.append("move (moved_from | moved_to) "); 35 | } else { 36 | if (containsEvent(event, Event::moved_from)) 37 | maskString.append("moved_from "); 38 | if (containsEvent(event, Event::moved_to)) 39 | maskString.append("moved_to "); 40 | } 41 | 42 | if (containsEvent(event, Event::access)) 43 | maskString.append("access "); 44 | if (containsEvent(event, Event::attrib)) 45 | maskString.append("attrib "); 46 | if (containsEvent(event, Event::create)) 47 | maskString.append("create "); 48 | if (containsEvent(event, Event::remove)) 49 | maskString.append("remove "); 50 | if (containsEvent(event, Event::remove_self)) 51 | maskString.append("remove_self "); 52 | if (containsEvent(event, Event::modify)) 53 | maskString.append("modify "); 54 | if (containsEvent(event, Event::move_self)) 55 | maskString.append("move_self "); 56 | if (containsEvent(event, Event::open)) 57 | maskString.append("open "); 58 | if (containsEvent(event, Event::is_dir)) 59 | maskString.append("is_dir "); 60 | if (containsEvent(event, Event::unmount)) 61 | maskString.append("unmount "); 62 | if (containsEvent(event, Event::q_overflow)) 63 | maskString.append("q_overflow "); 64 | if (containsEvent(event, Event::ignored)) 65 | maskString.append("ignored "); 66 | if (containsEvent(event, Event::oneshot)) 67 | maskString.append("oneshot "); 68 | 69 | stream << maskString; 70 | return stream; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(LIB_NAME inotify-cpp) 2 | set(LIB_COMPANY inotify-cpp) 3 | set(LIB_SRCS NotifierBuilder.cpp Event.cpp FileSystemEvent.cpp Inotify.cpp Notification.cpp) 4 | set(LIB_HEADER 5 | include/inotify-cpp/NotifierBuilder.h 6 | include/inotify-cpp/Event.h 7 | include/inotify-cpp/FileSystemEvent.h 8 | include/inotify-cpp/Inotify.h 9 | include/inotify-cpp/Notification.h) 10 | 11 | cmake_minimum_required(VERSION 3.8) 12 | project(${LIB_NAME} VERSION 0.2.0) 13 | include(GNUInstallDirs) 14 | 15 | macro(configure_target target) 16 | set_target_properties(${target} PROPERTIES 17 | LINKER_LANGUAGE CXX 18 | OUTPUT_NAME ${LIB_NAME} 19 | SOVERSION ${PROJECT_VERSION} 20 | ) 21 | 22 | install(TARGETS ${target} EXPORT ${target}Targets 23 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 24 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 25 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 26 | INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 27 | install(EXPORT ${target}Targets 28 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB_NAME} 29 | FILE ${target}Targets.cmake 30 | NAMESPACE ${LIB_COMPANY}::) 31 | 32 | target_compile_features(${target} PUBLIC cxx_std_11) 33 | target_include_directories(${target} PUBLIC 34 | $ 35 | $ 36 | ${Boost_INCLUDE_DIR}) 37 | target_link_libraries(${target} PUBLIC inotify-filesystem-adapter) 38 | 39 | endmacro(configure_target target) 40 | 41 | # add export for adapter 42 | install(TARGETS inotify-filesystem-adapter EXPORT inotify-filesystem-adapterExport) 43 | install(EXPORT inotify-filesystem-adapterExport DESTINATION ${CMAKE_INSTALL_LIBDIR}) 44 | 45 | # library definition 46 | if(BUILD_SHARED_LIBS) 47 | add_library(${LIB_NAME}-shared SHARED ${LIB_SRCS} ${LIB_HEADER}) 48 | configure_target(${LIB_NAME}-shared) 49 | if(NOT BUILD_STATIC_LIBS) 50 | add_library(${LIB_COMPANY}::${LIB_NAME} ALIAS ${LIB_NAME}-shared) 51 | endif() 52 | endif() 53 | 54 | if(BUILD_STATIC_LIBS) 55 | add_library(${LIB_NAME}-static STATIC ${LIB_SRCS} ${LIB_HEADER}) 56 | configure_target(${LIB_NAME}-static) 57 | if(NOT BUILD_SHARED_LIBS) 58 | add_library(${LIB_COMPANY}::${LIB_NAME} ALIAS ${LIB_NAME}-static) 59 | endif() 60 | endif() 61 | 62 | if(BUILD_SHARED_LIBS AND BUILD_STATIC_LIBS) 63 | add_library(${LIB_COMPANY}::${LIB_NAME} ALIAS ${LIB_NAME}-shared) 64 | endif() 65 | 66 | install(DIRECTORY ${CMAKE_INSTALL_INCLUDEDIR}/${LIB_NAME} DESTINATION include) 67 | 68 | include(CMakePackageConfigHelpers) 69 | write_basic_package_version_file(${LIB_NAME}ConfigVersion.cmake 70 | COMPATIBILITY SameMajorVersion) 71 | install(FILES ${LIB_NAME}Config.cmake ${CMAKE_CURRENT_BINARY_DIR}/${LIB_NAME}ConfigVersion.cmake 72 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${LIB_NAME}) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inotify-cpp # 2 | ![Build and Test](https://github.com/erikzenker/inotify-cpp/workflows/Build%20and%20Test/badge.svg) [![codecov](https://codecov.io/gh/erikzenker/inotify-cpp/branch/master/graph/badge.svg)](https://codecov.io/gh/erikzenker/inotify-cpp) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/erikzenker) 3 | =========== 4 | 5 | __Inotify-cpp__ is a C++ wrapper for linux inotify. It lets you watch for 6 | filesystem events on your filesystem tree. The following usage example shows 7 | the implementation of a simple filesystem event watcher for the commandline. 8 | 9 | ## Usage ## 10 | 11 | ```c++ 12 | #include 13 | 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | using namespace inotify; 21 | 22 | int main(int argc, char** argv) 23 | { 24 | if (argc <= 1) { 25 | std::cout << "Usage: ./inotify_example /path/to/dir" << std::endl; 26 | exit(0); 27 | } 28 | 29 | // Parse the directory to watch 30 | std::filesystem::path path(argv[1]); 31 | 32 | // Set the event handler which will be used to process particular events 33 | auto handleNotification = [&](Notification notification) { 34 | std::cout << "Event " << notification.event << " on " << notification.path << " at " 35 | << notification.time.time_since_epoch().count() << " was triggered." << std::endl; 36 | }; 37 | 38 | // Set the a separate unexpected event handler for all other events. An exception is thrown by 39 | // default. 40 | auto handleUnexpectedNotification = [](Notification notification) { 41 | std::cout << "Event " << notification.event << " on " << notification.path << " at " 42 | << notification.time.time_since_epoch().count() 43 | << " was triggered, but was not expected" << std::endl; 44 | }; 45 | 46 | // Set the events to be notified for 47 | auto events = { Event::open | Event::is_dir, // some events occur in combinations 48 | Event::access, 49 | Event::create, 50 | Event::modify, 51 | Event::remove, 52 | Event::move }; 53 | 54 | // The notifier is configured to watch the parsed path for the defined events. Particular files 55 | // or paths can be ignored(once). 56 | auto notifier = BuildNotifier() 57 | .watchPathRecursively(path) 58 | .ignoreFileOnce("fileIgnoredOnce") 59 | .ignoreFile("fileIgnored") 60 | .onEvents(events, handleNotification) 61 | .onUnexpectedEvent(handleUnexpectedNotification); 62 | 63 | // The event loop is started in a separate thread context. 64 | std::thread thread([&](){ notifier.run(); }); 65 | 66 | // Terminate the event loop after 60 seconds 67 | std::this_thread::sleep_for(std::chrono::seconds(60)); 68 | notifier.stop(); 69 | thread.join(); 70 | return 0; 71 | } 72 | ``` 73 | 74 | ## Build and Install Library ## 75 | ```bash 76 | mkdir build; cd build 77 | cmake -DCMAKE_INSTALL_PREFIX=/usr .. 78 | cmake --build . 79 | 80 | # run tests 81 | ctest -VV 82 | 83 | # install the library 84 | cmake --build . --target install 85 | ``` 86 | 87 | ## Build Example ## 88 | ```bash 89 | mkdir build; cd build 90 | cmake .. 91 | cmake --build . --target inotify_example 92 | ./example/inotify_example 93 | ``` 94 | 95 | ## Install from Packet ## 96 | * Arch Linux: `yaourt -S inotify-cpp-git` 97 | 98 | ## Dependencies ## 99 | + lib 100 | + boost 1.54.0 101 | + c++17 102 | + linux 2.6.13 103 | + build 104 | + cmake 3.8 105 | 106 | ## Licence 107 | MIT 108 | 109 | ## Author ## 110 | Written by Erik Zenker (erikzenker@hotmail.com) 111 | 112 | ## Thanks for Contribution ## 113 | + [spelcaster](https://github.com/spelcaster) 114 | -------------------------------------------------------------------------------- /src/NotifierBuilder.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | namespace inotify { 5 | 6 | NotifierBuilder::NotifierBuilder() 7 | : mInotify(std::make_shared()) 8 | { 9 | } 10 | 11 | NotifierBuilder BuildNotifier() 12 | { 13 | return {}; 14 | } 15 | 16 | auto NotifierBuilder::watchPathRecursively(inotifypp::filesystem::path path) -> NotifierBuilder& 17 | { 18 | mInotify->watchDirectoryRecursively(path); 19 | return *this; 20 | } 21 | 22 | auto NotifierBuilder::watchFile(inotifypp::filesystem::path file) -> NotifierBuilder& 23 | { 24 | mInotify->watchFile(file); 25 | return *this; 26 | } 27 | 28 | auto NotifierBuilder::unwatchFile(inotifypp::filesystem::path file) -> NotifierBuilder& 29 | { 30 | mInotify->unwatchFile(file); 31 | return *this; 32 | } 33 | 34 | auto NotifierBuilder::ignoreFileOnce(inotifypp::filesystem::path file) -> NotifierBuilder& 35 | { 36 | mInotify->ignoreFileOnce(file.string()); 37 | return *this; 38 | } 39 | 40 | auto NotifierBuilder::ignoreFile(inotifypp::filesystem::path file) -> NotifierBuilder& 41 | { 42 | mInotify->ignoreFile(file.string()); 43 | return *this; 44 | } 45 | 46 | auto NotifierBuilder::onEvent(Event event, EventObserver eventObserver) -> NotifierBuilder& 47 | { 48 | mInotify->setEventMask(mInotify->getEventMask() | static_cast(event)); 49 | mEventObserver[event] = eventObserver; 50 | return *this; 51 | } 52 | 53 | auto NotifierBuilder::onEvents(std::vector events, EventObserver eventObserver) 54 | -> NotifierBuilder& 55 | { 56 | for (auto event : events) { 57 | mInotify->setEventMask(mInotify->getEventMask() | static_cast(event)); 58 | mEventObserver[event] = eventObserver; 59 | } 60 | 61 | return *this; 62 | } 63 | 64 | auto NotifierBuilder::onUnexpectedEvent(EventObserver eventObserver) -> NotifierBuilder& 65 | { 66 | mUnexpectedEventObserver = eventObserver; 67 | return *this; 68 | } 69 | /** 70 | * Sets the time between two successive events. Events occurring in between 71 | * will be ignored and the event observer will be called. 72 | * 73 | * @param timeout 74 | * @param eventObserver 75 | * @return 76 | */ 77 | auto NotifierBuilder::setEventTimeout( 78 | std::chrono::milliseconds timeout, EventObserver eventObserver) -> NotifierBuilder& 79 | { 80 | auto onEventTimeout = [eventObserver](FileSystemEvent fileSystemEvent) { 81 | Notification notification { static_cast(fileSystemEvent.mask), 82 | fileSystemEvent.path, 83 | fileSystemEvent.eventTime }; 84 | eventObserver(notification); 85 | }; 86 | 87 | mInotify->setEventTimeout(timeout, onEventTimeout); 88 | return *this; 89 | } 90 | 91 | auto NotifierBuilder::runOnce() -> void 92 | { 93 | auto fileSystemEvent = mInotify->getNextEvent(); 94 | if (!fileSystemEvent) { 95 | return; 96 | } 97 | 98 | Event currentEvent = static_cast(fileSystemEvent->mask); 99 | 100 | Notification notification { currentEvent, fileSystemEvent->path, fileSystemEvent->eventTime }; 101 | 102 | for (auto& eventAndEventObserver : mEventObserver) { 103 | auto& event = eventAndEventObserver.first; 104 | auto& eventObserver = eventAndEventObserver.second; 105 | 106 | if (event == Event::all) { 107 | eventObserver(notification); 108 | return; 109 | } 110 | 111 | if (event == currentEvent) { 112 | eventObserver(notification); 113 | return; 114 | } 115 | } 116 | 117 | if (mUnexpectedEventObserver) { 118 | mUnexpectedEventObserver(notification); 119 | } 120 | } 121 | 122 | auto NotifierBuilder::run() -> void 123 | { 124 | while (true) { 125 | if (mInotify->hasStopped()) { 126 | break; 127 | } 128 | 129 | runOnce(); 130 | } 131 | } 132 | 133 | auto NotifierBuilder::stop() -> void 134 | { 135 | mInotify->stop(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/include/inotify-cpp/Inotify.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Inotify.h 3 | * @author Erik Zenker 4 | * @date 20.11.2017 5 | * @copyright MIT 6 | **/ 7 | #pragma once 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | 28 | #define MAX_EVENTS 4096 29 | /** 30 | * MAX_EPOLL_EVENTS is set to 1 since there exists 31 | * only one eventbuffer. The value can be increased 32 | * when readEventsIntoBuffer can handle multiple 33 | * epoll events. 34 | */ 35 | #define MAX_EPOLL_EVENTS 1 36 | #define EVENT_SIZE (sizeof (inotify_event)) 37 | 38 | /** 39 | * @brief C++ wrapper for linux inotify interface 40 | * @class Inotify 41 | * Inotify.h 42 | * "include/Inotify.h" 43 | * 44 | * folders will be watched by watchFolderRecursively or 45 | * files by watchFile. If there are changes inside this 46 | * folder or files events will be raised. This events 47 | * can be get by getNextEvent. 48 | * 49 | * @eventMask 50 | * 51 | * IN_ACCESS File was accessed (read) (*). 52 | * IN_ATTRIB Metadata changed—for example, permissions, 53 | * timestamps, extended attributes, link count 54 | * (since Linux 2.6.25), UID, or GID. (*). 55 | * IN_CLOSE_WRITE File opened for writing was closed (*). 56 | * IN_CLOSE_NOWRITE File not opened for writing was closed (*). 57 | * IN_CREATE File/directory created in watched directory(*). 58 | * IN_DELETE File/directory deleted from watched directory(*). 59 | * IN_DELETE_SELF Watched file/directory was itself deleted. 60 | * IN_MODIFY File was modified (*). 61 | * IN_MOVE_SELF Watched file/directory was itself moved. 62 | * IN_MOVED_FROM Generated for the directory containing the old 63 | * filename when a file is renamed (*). 64 | * IN_MOVED_TO Generated for the directory containing the new 65 | * filename when a file is renamed (*). 66 | * IN_OPEN File was opened (*). 67 | * IN_ALL_EVENTS macro is defined as a bit mask of all of the above 68 | * events 69 | * IN_MOVE IN_MOVED_FROM|IN_MOVED_TO 70 | * IN_CLOSE IN_CLOSE_WRITE | IN_CLOSE_NOWRITE 71 | * 72 | * See inotify manpage for more event details 73 | * 74 | */ 75 | namespace inotify { 76 | 77 | class Inotify { 78 | public: 79 | Inotify(); 80 | ~Inotify(); 81 | void watchDirectoryRecursively(inotifypp::filesystem::path path); 82 | void watchFile(inotifypp::filesystem::path file); 83 | void unwatchFile(inotifypp::filesystem::path file); 84 | void ignoreFileOnce(inotifypp::filesystem::path file); 85 | void ignoreFile(inotifypp::filesystem::path file); 86 | void setEventMask(uint32_t eventMask); 87 | uint32_t getEventMask(); 88 | void setEventTimeout(std::chrono::milliseconds eventTimeout, std::function onEventTimeout); 89 | inotifypp::optional getNextEvent(); 90 | void stop(); 91 | bool hasStopped(); 92 | 93 | private: 94 | inotifypp::filesystem::path wdToPath(int wd); 95 | bool isIgnored(std::string file); 96 | bool isOnTimeout(const std::chrono::steady_clock::time_point &eventTime); 97 | void removeWatch(int wd); 98 | ssize_t readEventsIntoBuffer(std::vector& eventBuffer); 99 | void readEventsFromBuffer(uint8_t* buffer, int length, std::vector &events); 100 | void filterEvents(std::vector& events, std::queue& eventQueue); 101 | void sendStopSignal(); 102 | 103 | private: 104 | int mError; 105 | std::chrono::milliseconds mEventTimeout; 106 | std::chrono::steady_clock::time_point mLastEventTime; 107 | uint32_t mEventMask; 108 | uint32_t mThreadSleep; 109 | std::vector mIgnoredDirectories; 110 | std::vector mOnceIgnoredDirectories; 111 | std::queue mEventQueue; 112 | boost::bimap mDirectorieMap; 113 | int mInotifyFd; 114 | std::atomic mStopped; 115 | int mEpollFd; 116 | epoll_event mInotifyEpollEvent; 117 | epoll_event mStopPipeEpollEvent; 118 | epoll_event mEpollEvents[MAX_EPOLL_EVENTS]; 119 | 120 | std::function mOnEventTimeout; 121 | std::vector mEventBuffer; 122 | 123 | int mStopPipeFd[2]; 124 | const int mPipeReadIdx; 125 | const int mPipeWriteIdx; 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /src/Inotify.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace fs = inotifypp::filesystem; 13 | 14 | namespace inotify { 15 | 16 | Inotify::Inotify() 17 | : mError(0) 18 | , mEventTimeout(0) 19 | , mLastEventTime(std::chrono::steady_clock::now()) 20 | , mEventMask(IN_ALL_EVENTS) 21 | , mThreadSleep(250) 22 | , mIgnoredDirectories(std::vector()) 23 | , mInotifyFd(0) 24 | , mOnEventTimeout([](FileSystemEvent) {}) 25 | , mEventBuffer(MAX_EVENTS * (EVENT_SIZE + 16), 0) 26 | , mPipeReadIdx(0) 27 | , mPipeWriteIdx(1) 28 | { 29 | mStopped = false; 30 | 31 | if (pipe2(mStopPipeFd, O_NONBLOCK) == -1) { 32 | mError = errno; 33 | std::stringstream errorStream; 34 | errorStream << "Can't initialize stop pipe ! " << strerror(mError) << "."; 35 | throw std::runtime_error(errorStream.str()); 36 | } 37 | 38 | mInotifyFd = inotify_init1(IN_NONBLOCK); 39 | if (mInotifyFd == -1) { 40 | mError = errno; 41 | std::stringstream errorStream; 42 | errorStream << "Can't initialize inotify ! " << strerror(mError) << "."; 43 | throw std::runtime_error(errorStream.str()); 44 | } 45 | 46 | mEpollFd = epoll_create1(0); 47 | if (mEpollFd == -1) { 48 | mError = errno; 49 | std::stringstream errorStream; 50 | errorStream << "Can't initialize epoll ! " << strerror(mError) << "."; 51 | throw std::runtime_error(errorStream.str()); 52 | } 53 | 54 | mInotifyEpollEvent.events = EPOLLIN | EPOLLET; 55 | mInotifyEpollEvent.data.fd = mInotifyFd; 56 | if (epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mInotifyFd, &mInotifyEpollEvent) == -1) { 57 | mError = errno; 58 | std::stringstream errorStream; 59 | errorStream << "Can't add inotify filedescriptor to epoll ! " << strerror(mError) << "."; 60 | throw std::runtime_error(errorStream.str()); 61 | } 62 | 63 | mStopPipeEpollEvent.events = EPOLLIN | EPOLLET; 64 | mStopPipeEpollEvent.data.fd = mStopPipeFd[mPipeReadIdx]; 65 | if (epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mStopPipeFd[mPipeReadIdx], &mStopPipeEpollEvent) == -1) { 66 | mError = errno; 67 | std::stringstream errorStream; 68 | errorStream << "Can't add pipe filedescriptor to epoll ! " << strerror(mError) << "."; 69 | throw std::runtime_error(errorStream.str()); 70 | } 71 | } 72 | 73 | Inotify::~Inotify() 74 | { 75 | epoll_ctl(mEpollFd, EPOLL_CTL_DEL, mInotifyFd, 0); 76 | epoll_ctl(mEpollFd, EPOLL_CTL_DEL, mStopPipeFd[mPipeReadIdx], 0); 77 | 78 | if (!close(mInotifyFd)) { 79 | mError = errno; 80 | } 81 | 82 | if (!close(mEpollFd)) { 83 | mError = errno; 84 | } 85 | 86 | close(mStopPipeFd[mPipeReadIdx]); 87 | close(mStopPipeFd[mPipeWriteIdx]); 88 | } 89 | 90 | /** 91 | * @brief Adds the given path and all files and subdirectories 92 | * to the set of watched files/directories. 93 | * Symlinks will be followed! 94 | * 95 | * @param path that will be watched recursively 96 | * 97 | */ 98 | void Inotify::watchDirectoryRecursively(fs::path path) 99 | { 100 | std::vector paths; 101 | 102 | if (fs::exists(path)) { 103 | paths.push_back(path); 104 | 105 | if (fs::is_directory(path)) { 106 | inotifypp::error_code ec; 107 | fs::recursive_directory_iterator it( 108 | path, fs::directory_options::follow_directory_symlink, ec); 109 | fs::recursive_directory_iterator end; 110 | 111 | for (; it != end; it.increment(ec)) { 112 | fs::path currentPath = *it; 113 | 114 | if (!fs::is_directory(currentPath) && !fs::is_symlink(currentPath)) { 115 | continue; 116 | } 117 | 118 | paths.push_back(currentPath); 119 | } 120 | } 121 | } else { 122 | throw std::invalid_argument( 123 | "Can´t watch Path! Path does not exist. Path: " + path.string()); 124 | } 125 | 126 | for (auto& path : paths) { 127 | watchFile(path); 128 | } 129 | } 130 | 131 | /** 132 | * @brief Adds a single file/directorie to the list of 133 | * watches. Path and corresponding watchdescriptor 134 | * will be stored in the directorieMap. This is done 135 | * because events on watches just return this 136 | * watchdescriptor. 137 | * 138 | * @param path that will be watched 139 | * 140 | */ 141 | void Inotify::watchFile(fs::path filePath) 142 | { 143 | if (fs::exists(filePath)) { 144 | mError = 0; 145 | int wd = 0; 146 | if (!isIgnored(filePath.string())) { 147 | wd = inotify_add_watch(mInotifyFd, filePath.string().c_str(), mEventMask); 148 | } 149 | 150 | if (wd == -1) { 151 | mError = errno; 152 | std::stringstream errorStream; 153 | if (mError == 28) { 154 | errorStream << "Failed to watch! " << strerror(mError) 155 | << ". Please increase number of watches in " 156 | "\"/proc/sys/fs/inotify/max_user_watches\"."; 157 | throw std::runtime_error(errorStream.str()); 158 | } 159 | 160 | errorStream << "Failed to watch! " << strerror(mError) 161 | << ". Path: " << filePath.string(); 162 | throw std::runtime_error(errorStream.str()); 163 | } 164 | mDirectorieMap.left.insert({wd, filePath}); 165 | } else { 166 | throw std::invalid_argument( 167 | "Can´t watch Path! Path does not exist. Path: " + filePath.string()); 168 | } 169 | } 170 | 171 | void Inotify::ignoreFileOnce(fs::path file) 172 | { 173 | mOnceIgnoredDirectories.push_back(file.string()); 174 | } 175 | 176 | void Inotify::ignoreFile(fs::path file) 177 | { 178 | mIgnoredDirectories.push_back(file.string()); 179 | } 180 | 181 | 182 | void Inotify::unwatchFile(fs::path file) 183 | { 184 | removeWatch(mDirectorieMap.right.at(file)); 185 | } 186 | 187 | /** 188 | * @brief Removes watch from set of watches. This 189 | * is not done recursively! 190 | * 191 | * @param wd watchdescriptor 192 | * 193 | */ 194 | void Inotify::removeWatch(int wd) 195 | { 196 | int result = inotify_rm_watch(mInotifyFd, wd); 197 | if (result == -1) { 198 | mError = errno; 199 | std::stringstream errorStream; 200 | errorStream << "Failed to remove watch! " << strerror(mError) << "."; 201 | throw std::runtime_error(errorStream.str()); 202 | } 203 | } 204 | 205 | fs::path Inotify::wdToPath(int wd) 206 | { 207 | return mDirectorieMap.left.at(wd); 208 | } 209 | 210 | void Inotify::setEventMask(uint32_t eventMask) 211 | { 212 | mEventMask = eventMask; 213 | } 214 | 215 | uint32_t Inotify::getEventMask() 216 | { 217 | return mEventMask; 218 | } 219 | 220 | void Inotify::setEventTimeout( 221 | std::chrono::milliseconds eventTimeout, std::function onEventTimeout) 222 | { 223 | mLastEventTime -= eventTimeout; 224 | mEventTimeout = eventTimeout; 225 | mOnEventTimeout = onEventTimeout; 226 | } 227 | 228 | /** 229 | * @brief Blocking wait on new events of watched files/directories 230 | * specified on the eventmask. FileSystemEvents 231 | * will be returned one by one. Thus this 232 | * function can be called in some while(true) 233 | * loop. 234 | * 235 | * @return A new FileSystemEvent 236 | * 237 | */ 238 | inotifypp::optional Inotify::getNextEvent() 239 | { 240 | std::vector newEvents; 241 | 242 | while (mEventQueue.empty() && !mStopped) { 243 | auto length = readEventsIntoBuffer(mEventBuffer); 244 | readEventsFromBuffer(mEventBuffer.data(), length, newEvents); 245 | filterEvents(newEvents, mEventQueue); 246 | } 247 | 248 | if (mStopped) { 249 | return inotifypp::nullopt(); 250 | } 251 | 252 | auto event = mEventQueue.front(); 253 | mEventQueue.pop(); 254 | return event; 255 | } 256 | 257 | void Inotify::stop() 258 | { 259 | mStopped = true; 260 | sendStopSignal(); 261 | } 262 | 263 | void Inotify::sendStopSignal() 264 | { 265 | std::vector buf(1,0); 266 | write(mStopPipeFd[mPipeWriteIdx], buf.data(), buf.size()); 267 | } 268 | 269 | bool Inotify::hasStopped() 270 | { 271 | return mStopped; 272 | } 273 | 274 | bool Inotify::isIgnored(std::string file) 275 | { 276 | for (unsigned i = 0; i < mOnceIgnoredDirectories.size(); ++i) { 277 | size_t pos = file.find(mOnceIgnoredDirectories[i]); 278 | if (pos != std::string::npos) { 279 | mOnceIgnoredDirectories.erase(mOnceIgnoredDirectories.begin() + i); 280 | return true; 281 | } 282 | } 283 | 284 | for (unsigned i = 0; i < mIgnoredDirectories.size(); ++i) { 285 | size_t pos = file.find(mIgnoredDirectories[i]); 286 | if (pos != std::string::npos) { 287 | return true; 288 | } 289 | } 290 | 291 | return false; 292 | } 293 | 294 | bool Inotify::isOnTimeout(const std::chrono::steady_clock::time_point &eventTime) 295 | { 296 | return std::chrono::duration_cast(eventTime - mLastEventTime) < mEventTimeout; 297 | } 298 | 299 | ssize_t Inotify::readEventsIntoBuffer(std::vector& eventBuffer) 300 | { 301 | ssize_t length = 0; 302 | length = 0; 303 | auto timeout = -1; 304 | auto nFdsReady = epoll_wait(mEpollFd, mEpollEvents, MAX_EPOLL_EVENTS, timeout); 305 | 306 | if (nFdsReady == -1) { 307 | return length; 308 | } 309 | 310 | for (auto n = 0; n < nFdsReady; ++n) { 311 | if (mEpollEvents[n].data.fd == mStopPipeFd[mPipeReadIdx]) { 312 | break; 313 | } 314 | 315 | length = read(mEpollEvents[n].data.fd, eventBuffer.data(), eventBuffer.size()); 316 | if (length == -1) { 317 | mError = errno; 318 | if(mError == EINTR){ 319 | break; 320 | } 321 | } 322 | } 323 | 324 | return length; 325 | } 326 | 327 | void Inotify::readEventsFromBuffer( 328 | uint8_t* buffer, int length, std::vector& events) 329 | { 330 | int i = 0; 331 | while (i < length) { 332 | inotify_event* event = ((struct inotify_event*)&buffer[i]); 333 | 334 | if(event->mask & IN_IGNORED){ 335 | i += EVENT_SIZE + event->len; 336 | mDirectorieMap.left.erase(event->wd); 337 | continue; 338 | } 339 | 340 | auto path = wdToPath(event->wd); 341 | 342 | if (fs::is_directory(path)) { 343 | path = path / std::string(event->name); 344 | } 345 | 346 | if (fs::is_directory(path)) { 347 | event->mask |= IN_ISDIR; 348 | } 349 | FileSystemEvent fsEvent(event->wd, event->mask, path, std::chrono::steady_clock::now()); 350 | 351 | if (!fsEvent.path.empty()) { 352 | events.push_back(fsEvent); 353 | 354 | } else { 355 | // Event is not complete --> ignore 356 | } 357 | 358 | i += EVENT_SIZE + event->len; 359 | } 360 | } 361 | 362 | void Inotify::filterEvents( 363 | std::vector& events, std::queue& eventQueue) 364 | { 365 | for (auto eventIt = events.begin(); eventIt < events.end();) { 366 | FileSystemEvent currentEvent = *eventIt; 367 | if (isOnTimeout(currentEvent.eventTime)) { 368 | eventIt = events.erase(eventIt); 369 | mOnEventTimeout(currentEvent); 370 | } else if (isIgnored(currentEvent.path.string())) { 371 | eventIt = events.erase(eventIt); 372 | } else { 373 | mLastEventTime = currentEvent.eventTime; 374 | eventQueue.push(currentEvent); 375 | eventIt++; 376 | } 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /test/unit/NotifierBuilderTests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace inotify; 12 | 13 | void openFile(const inotifypp::filesystem::path& file) 14 | { 15 | std::ifstream stream; 16 | stream.open(file.string(), std::ifstream::in); 17 | BOOST_CHECK(stream.is_open()); 18 | stream.close(); 19 | } 20 | 21 | void createFile(const inotifypp::filesystem::path& file) 22 | { 23 | std::ofstream stream(file.string()); 24 | } 25 | 26 | struct NotifierBuilderTests { 27 | NotifierBuilderTests() 28 | : testDirectory_("testDirectory") 29 | , recursiveTestDirectory_(testDirectory_ / "recursiveTestDirectory") 30 | , testFile_(testDirectory_ / "test.txt") 31 | , recursiveTestFile_(recursiveTestDirectory_ / "recursiveTest.txt") 32 | , createdFile_(testDirectory_ / "created.txt") 33 | , timeout_(1) 34 | { 35 | inotifypp::filesystem::create_directories(testDirectory_); 36 | inotifypp::filesystem::create_directories(recursiveTestDirectory_); 37 | 38 | createFile(testFile_); 39 | createFile(recursiveTestFile_); 40 | } 41 | 42 | ~NotifierBuilderTests() 43 | { 44 | inotifypp::filesystem::remove_all(testDirectory_); 45 | } 46 | 47 | inotifypp::filesystem::path testDirectory_; 48 | inotifypp::filesystem::path recursiveTestDirectory_; 49 | inotifypp::filesystem::path testFile_; 50 | inotifypp::filesystem::path recursiveTestFile_; 51 | inotifypp::filesystem::path createdFile_; 52 | 53 | std::chrono::seconds timeout_; 54 | 55 | // Events 56 | std::promise promisedOpen_; 57 | std::promise promisedCreate_; 58 | std::promise promisedCloseNoWrite_; 59 | std::promise promisedCombinedEvent_; 60 | }; 61 | 62 | BOOST_FIXTURE_TEST_CASE(shouldNotAcceptNotExistingPaths, NotifierBuilderTests) 63 | { 64 | BOOST_CHECK_THROW( 65 | BuildNotifier().watchPathRecursively("/not/existing/path/"), std::invalid_argument); 66 | BOOST_CHECK_THROW(BuildNotifier().watchFile("/not/existing/file"), std::invalid_argument); 67 | } 68 | 69 | BOOST_FIXTURE_TEST_CASE(shouldStopNotifierLoop, NotifierBuilderTests) 70 | { 71 | auto notifier = BuildNotifier().watchFile(testFile_); 72 | 73 | std::thread thread([¬ifier]() { notifier.run(); }); 74 | 75 | std::this_thread::sleep_for(std::chrono::milliseconds{100}); 76 | 77 | notifier.stop(); 78 | thread.join(); 79 | } 80 | 81 | BOOST_FIXTURE_TEST_CASE(shouldNotifyOnOpenEvent, NotifierBuilderTests) 82 | { 83 | auto notifier = BuildNotifier().watchFile(testFile_).onEvent( 84 | Event::open, [&](Notification notification) { promisedOpen_.set_value(notification); }); 85 | 86 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 87 | 88 | openFile(testFile_); 89 | 90 | auto futureOpenEvent = promisedOpen_.get_future(); 91 | BOOST_CHECK(futureOpenEvent.wait_for(timeout_) == std::future_status::ready); 92 | BOOST_CHECK(futureOpenEvent.get().event == Event::open); 93 | thread.join(); 94 | } 95 | 96 | BOOST_FIXTURE_TEST_CASE(shouldNotifyOnAllEvents, NotifierBuilderTests) 97 | { 98 | auto notifier 99 | = BuildNotifier() 100 | .watchFile(testFile_) 101 | .onEvent( 102 | Event::all, 103 | [&](Notification notification) { 104 | switch (notification.event) { 105 | default: 106 | BOOST_ASSERT_MSG(false, "All events should be handled"); 107 | case Event::open: 108 | promisedOpen_.set_value(notification); 109 | break; 110 | case Event::close_nowrite: 111 | promisedCombinedEvent_.set_value(notification); 112 | break; 113 | }; 114 | }) 115 | .onUnexpectedEvent([](Notification) { 116 | BOOST_ASSERT_MSG(false, "All events should be catched by event observer"); 117 | }); 118 | 119 | std::thread thread([¬ifier]() { notifier.run(); }); 120 | 121 | openFile(testFile_); 122 | 123 | auto futureOpenEvent = promisedOpen_.get_future(); 124 | auto futureCombinedEvent = promisedCombinedEvent_.get_future(); 125 | BOOST_CHECK(futureOpenEvent.wait_for(timeout_) == std::future_status::ready); 126 | BOOST_CHECK(futureCombinedEvent.wait_for(timeout_) == std::future_status::ready); 127 | 128 | notifier.stop(); 129 | thread.join(); 130 | } 131 | 132 | BOOST_FIXTURE_TEST_CASE(shouldNotifyOnCombinedEvent, NotifierBuilderTests) 133 | { 134 | auto notifier = BuildNotifier() 135 | .watchPathRecursively(testDirectory_) 136 | .onEvent(Event::create | Event::is_dir, [&](Notification notification) { 137 | promisedCombinedEvent_.set_value(notification); 138 | }); 139 | 140 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 141 | 142 | inotifypp::filesystem::create_directories(testDirectory_ / "test"); 143 | 144 | auto futureCombinedEvent = promisedCombinedEvent_.get_future(); 145 | BOOST_CHECK(futureCombinedEvent.wait_for(timeout_) == std::future_status::ready); 146 | 147 | notifier.stop(); 148 | thread.join(); 149 | } 150 | 151 | BOOST_FIXTURE_TEST_CASE(shouldNotifyOnMultipleEvents, NotifierBuilderTests) 152 | { 153 | auto notifier = BuildNotifier().watchFile(testFile_).onEvents( 154 | { Event::open, Event::close_nowrite }, [&](Notification notification) { 155 | switch (notification.event) { 156 | default: 157 | break; 158 | case Event::open: 159 | promisedOpen_.set_value(notification); 160 | break; 161 | case Event::close_nowrite: 162 | promisedCloseNoWrite_.set_value(notification); 163 | break; 164 | } 165 | }); 166 | 167 | std::thread thread([¬ifier]() { 168 | notifier.runOnce(); 169 | notifier.runOnce(); 170 | }); 171 | 172 | openFile(testFile_); 173 | 174 | auto futureOpen = promisedOpen_.get_future(); 175 | auto futureCloseNoWrite = promisedCloseNoWrite_.get_future(); 176 | BOOST_CHECK(futureOpen.wait_for(timeout_) == std::future_status::ready); 177 | BOOST_CHECK(futureOpen.get().event == Event::open); 178 | BOOST_CHECK(futureCloseNoWrite.wait_for(timeout_) == std::future_status::ready); 179 | BOOST_CHECK(futureCloseNoWrite.get().event == Event::close_nowrite); 180 | thread.join(); 181 | } 182 | 183 | BOOST_FIXTURE_TEST_CASE(shouldStopRunOnce, NotifierBuilderTests) 184 | { 185 | auto notifier = BuildNotifier().watchFile(testFile_); 186 | 187 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 188 | 189 | notifier.stop(); 190 | 191 | thread.join(); 192 | } 193 | 194 | BOOST_FIXTURE_TEST_CASE(shouldStopRun, NotifierBuilderTests) 195 | { 196 | auto notifier = BuildNotifier().watchFile(testFile_); 197 | 198 | std::thread thread([¬ifier]() { notifier.run(); }); 199 | 200 | notifier.stop(); 201 | 202 | thread.join(); 203 | } 204 | 205 | BOOST_FIXTURE_TEST_CASE(shouldIgnoreFileOnce, NotifierBuilderTests) 206 | { 207 | auto notifier = BuildNotifier().watchFile(testFile_).ignoreFileOnce(testFile_).onEvent( 208 | Event::open, [&](Notification notification) { promisedOpen_.set_value(notification); }); 209 | 210 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 211 | 212 | openFile(testFile_); 213 | 214 | auto futureOpen = promisedOpen_.get_future(); 215 | BOOST_CHECK(futureOpen.wait_for(timeout_) != std::future_status::ready); 216 | 217 | notifier.stop(); 218 | thread.join(); 219 | } 220 | 221 | BOOST_FIXTURE_TEST_CASE(shouldIgnoreFile, NotifierBuilderTests) 222 | { 223 | auto notifier = BuildNotifier().watchFile(testFile_).ignoreFile(testFile_).onEvent( 224 | Event::open, [&](Notification notification) { promisedOpen_.set_value(notification); }); 225 | 226 | std::thread thread([¬ifier]() { notifier.run(); }); 227 | 228 | openFile(testFile_); 229 | openFile(testFile_); 230 | 231 | auto futureOpen = promisedOpen_.get_future(); 232 | BOOST_CHECK(futureOpen.wait_for(timeout_) != std::future_status::ready); 233 | 234 | notifier.stop(); 235 | thread.join(); 236 | } 237 | 238 | // This test might fail on dev machines when editors trigger events by indexing the build directory 239 | BOOST_FIXTURE_TEST_CASE(shouldWatchPathRecursively, NotifierBuilderTests) 240 | { 241 | 242 | auto notifier = BuildNotifier() 243 | .watchPathRecursively(testDirectory_) 244 | .onEvent(Event::open, [&](Notification notification) { 245 | switch (notification.event) { 246 | default: 247 | break; 248 | case Event::open: 249 | if (notification.path == recursiveTestFile_) { 250 | promisedOpen_.set_value(notification); 251 | } 252 | break; 253 | } 254 | }); 255 | 256 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 257 | 258 | openFile(recursiveTestFile_); 259 | 260 | auto futureOpen = promisedOpen_.get_future(); 261 | BOOST_CHECK(futureOpen.wait_for(timeout_) == std::future_status::ready); 262 | 263 | notifier.stop(); 264 | thread.join(); 265 | } 266 | 267 | // This test might fail on dev machines when editors trigger events by indexing the build directory 268 | BOOST_FIXTURE_TEST_CASE(shouldNotTriggerEventsOnWatchRecursively, NotifierBuilderTests) 269 | { 270 | auto notifier 271 | = BuildNotifier() 272 | .watchPathRecursively(testDirectory_) 273 | .onEvent(Event::all, [&](Notification notification) { 274 | std::cout << "event:" << notification.path << " " << notification.event 275 | << std::endl; 276 | BOOST_ASSERT_MSG( 277 | false, 278 | "Events should not be triggered when watching a directory recursively."); 279 | }); 280 | 281 | std::thread thread([¬ifier]() { notifier.run(); }); 282 | 283 | std::this_thread::sleep_for(std::chrono::milliseconds { 1000 }); 284 | 285 | notifier.stop(); 286 | thread.join(); 287 | } 288 | 289 | BOOST_FIXTURE_TEST_CASE(shouldWatchCreatedFile, NotifierBuilderTests) 290 | { 291 | 292 | auto notifier = BuildNotifier().watchPathRecursively(testDirectory_); 293 | 294 | notifier 295 | .onEvent( 296 | Event::create, 297 | [&](Notification notification) { 298 | notifier.watchFile(notification.path); 299 | promisedCreate_.set_value(notification); 300 | }) 301 | .onEvent(Event::close_nowrite, [&](Notification notification) { 302 | promisedCloseNoWrite_.set_value(notification); 303 | }); 304 | 305 | std::thread thread([¬ifier]() { notifier.run(); }); 306 | 307 | createFile(createdFile_); 308 | openFile(createdFile_); 309 | 310 | auto futureCreate = promisedCreate_.get_future(); 311 | BOOST_CHECK(futureCreate.wait_for(timeout_) == std::future_status::ready); 312 | auto futureCloseNoWrite = promisedCloseNoWrite_.get_future(); 313 | BOOST_CHECK(futureCloseNoWrite.wait_for(timeout_) == std::future_status::ready); 314 | 315 | notifier.stop(); 316 | thread.join(); 317 | } 318 | 319 | BOOST_FIXTURE_TEST_CASE(shouldUnwatchPath, NotifierBuilderTests) 320 | { 321 | std::promise timeoutObserved; 322 | 323 | auto notifier = BuildNotifier().watchFile(testFile_).unwatchFile(testFile_); 324 | 325 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 326 | 327 | openFile(testFile_); 328 | BOOST_CHECK(promisedOpen_.get_future().wait_for(timeout_) != std::future_status::ready); 329 | notifier.stop(); 330 | thread.join(); 331 | } 332 | 333 | BOOST_FIXTURE_TEST_CASE(shouldCallUserDefinedUnexpectedExceptionObserver, NotifierBuilderTests) 334 | { 335 | std::promise observerCalled; 336 | 337 | auto notifier = BuildNotifier().watchFile(testFile_).onUnexpectedEvent( 338 | [&](Notification) { observerCalled.set_value(); }); 339 | 340 | std::thread thread([¬ifier]() { notifier.runOnce(); }); 341 | 342 | openFile(testFile_); 343 | 344 | BOOST_CHECK(observerCalled.get_future().wait_for(timeout_) == std::future_status::ready); 345 | thread.join(); 346 | } 347 | 348 | BOOST_FIXTURE_TEST_CASE(shouldSetEventTimeout, NotifierBuilderTests) 349 | { 350 | std::promise timeoutObserved; 351 | std::chrono::milliseconds timeout(100); 352 | 353 | auto notifier 354 | = BuildNotifier() 355 | .watchFile(testFile_) 356 | .onEvent( 357 | Event::open, 358 | [&](Notification notification) { 359 | promisedOpen_.set_value(notification); }) 360 | .setEventTimeout(timeout, [&](Notification notification) { 361 | timeoutObserved.set_value(notification); 362 | }); 363 | 364 | std::thread thread([¬ifier]() { 365 | notifier.run(); // open 366 | }); 367 | 368 | openFile(testFile_); 369 | 370 | BOOST_CHECK(promisedOpen_.get_future().wait_for(timeout_) == std::future_status::ready); 371 | BOOST_CHECK(timeoutObserved.get_future().wait_for(timeout_) == std::future_status::ready); 372 | 373 | notifier.stop(); 374 | thread.join(); 375 | } 376 | 377 | BOOST_FIXTURE_TEST_CASE(shouldNotAppendSlashOnWatchingFiles, NotifierBuilderTests) 378 | { 379 | std::chrono::milliseconds timeout(100); 380 | 381 | auto notifier = BuildNotifier().watchFile(testFile_).onEvent( 382 | Event::open, [&](Notification notification) { promisedOpen_.set_value(notification); }); 383 | 384 | std::thread thread([¬ifier]() { 385 | notifier.run(); // open 386 | }); 387 | 388 | openFile(testFile_); 389 | 390 | auto future = promisedOpen_.get_future(); 391 | 392 | BOOST_CHECK(future.wait_for(timeout_) == std::future_status::ready); 393 | auto notification = future.get(); 394 | BOOST_CHECK(notification.path.string().back() != '/'); 395 | 396 | notifier.stop(); 397 | thread.join(); 398 | } 399 | --------------------------------------------------------------------------------