diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index a143b030c1..0567b9882c 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -573,6 +573,10 @@ if (BUILD_LAGOM) # LibCore lagom_test(../../Tests/LibCore/TestLibCoreIODevice.cpp WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../Tests/LibCore) + if (LINUX AND NOT EMSCRIPTEN) + lagom_test(../../Tests/LibCore/TestLibCoreFileWatcher.cpp) + endif() + # Crypto file(GLOB LIBCRYPTO_TESTS CONFIGURE_DEPENDS "../../Tests/LibCrypto/*.cpp") foreach(source ${LIBCRYPTO_TESTS}) diff --git a/Tests/LibCore/TestLibCoreFileWatcher.cpp b/Tests/LibCore/TestLibCoreFileWatcher.cpp index 6e4b499634..2d384f1cb8 100644 --- a/Tests/LibCore/TestLibCoreFileWatcher.cpp +++ b/Tests/LibCore/TestLibCoreFileWatcher.cpp @@ -28,10 +28,10 @@ TEST_CASE(file_watcher_child_events) file_watcher->on_change = [&](Core::FileWatcherEvent const& event) { if (event_count == 0) { EXPECT_EQ(event.event_path, "/tmp/testfile"); - EXPECT_EQ(event.type, Core::FileWatcherEvent::Type::ChildCreated); + EXPECT(has_flag(event.type, Core::FileWatcherEvent::Type::ChildCreated)); } else if (event_count == 1) { EXPECT_EQ(event.event_path, "/tmp/testfile"); - EXPECT_EQ(event.type, Core::FileWatcherEvent::Type::ChildDeleted); + EXPECT(has_flag(event.type, Core::FileWatcherEvent::Type::ChildDeleted)); event_loop.quit(0); } diff --git a/Userland/Libraries/LibCore/CMakeLists.txt b/Userland/Libraries/LibCore/CMakeLists.txt index 9465e4b3b6..4deb6fed09 100644 --- a/Userland/Libraries/LibCore/CMakeLists.txt +++ b/Userland/Libraries/LibCore/CMakeLists.txt @@ -44,9 +44,11 @@ if (NOT ANDROID AND NOT WIN32 AND NOT EMSCRIPTEN) ) endif() -# FIXME: Implement Core::FileWatcher for Linux, macOS, *BSD, and Windows. +# FIXME: Implement Core::FileWatcher for macOS, *BSD, and Windows. if (SERENITYOS) list(APPEND SOURCES FileWatcherSerenity.cpp) +elseif (LINUX AND NOT EMSCRIPTEN) + list(APPEND SOURCES FileWatcherLinux.cpp) else() list(APPEND SOURCES FileWatcherUnimplemented.cpp) endif() diff --git a/Userland/Libraries/LibCore/FileWatcherLinux.cpp b/Userland/Libraries/LibCore/FileWatcherLinux.cpp new file mode 100644 index 0000000000..48d3998672 --- /dev/null +++ b/Userland/Libraries/LibCore/FileWatcherLinux.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "FileWatcher.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(AK_OS_LINUX) +static_assert(false, "This file must only be used for Linux"); +#endif + +namespace Core { + +static constexpr unsigned inode_watcher_flags_to_inotify_flags(InodeWatcherFlags flags) +{ + unsigned result = 0; + + if ((flags & InodeWatcherFlags::Nonblock) != InodeWatcherFlags::None) + result |= IN_NONBLOCK; + if ((flags & InodeWatcherFlags::CloseOnExec) != InodeWatcherFlags::None) + result |= IN_CLOEXEC; + + return result; +} + +static Optional get_event_from_fd(int fd, HashMap const& wd_to_path) +{ + static constexpr auto max_event_size = sizeof(inotify_event) + NAME_MAX + 1; + + // Note from INOTIFY(7) man page: + // + // Some systems cannot read integer variables if they are not properly aligned. On other + // systems, incorrect alignment may decrease performance. Hence, the buffer used for reading + // from the inotify file descriptor should have the same alignment as inotify_event. + alignas(alignof(inotify_event)) Array buffer; + ssize_t rc = ::read(fd, buffer.data(), buffer.size()); + + if (rc == 0) { + return {}; + } else if (rc < 0) { + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Reading from wd {} failed: {}", fd, strerror(errno)); + return {}; + } + + auto const* event = reinterpret_cast(buffer.data()); + FileWatcherEvent result; + + auto it = wd_to_path.find(event->wd); + if (it == wd_to_path.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Got an event for a non-existent wd {}?!", event->wd); + return {}; + } + + auto const& path = it->value; + + if ((event->mask & IN_CREATE) != 0) + result.type |= FileWatcherEvent::Type::ChildCreated; + if ((event->mask & IN_DELETE) != 0) + result.type |= FileWatcherEvent::Type::ChildDeleted; + if ((event->mask & IN_DELETE_SELF) != 0) + result.type |= FileWatcherEvent::Type::Deleted; + if ((event->mask & IN_MODIFY) != 0) + result.type |= FileWatcherEvent::Type::ContentModified; + if ((event->mask & IN_ATTRIB) != 0) + result.type |= FileWatcherEvent::Type::MetadataModified; + + if (result.type == FileWatcherEvent::Type::Invalid) { + warnln("Unknown event type {:x} returned by the watch_file descriptor for {}", event->mask, path); + return {}; + } + + if (event->len > 0) { + StringView child_name { event->name, strlen(event->name) }; + result.event_path = LexicalPath::join(path, child_name).string(); + } else { + result.event_path = path; + } + + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: got event from wd {} on '{}' type {}", fd, result.event_path, result.type); + return result; +} + +ErrorOr> FileWatcher::create(InodeWatcherFlags flags) +{ + auto watcher_fd = ::inotify_init1(inode_watcher_flags_to_inotify_flags(flags | InodeWatcherFlags::CloseOnExec)); + if (watcher_fd < 0) + return Error::from_errno(errno); + + auto notifier = TRY(Notifier::try_create(watcher_fd, Notifier::Event::Read)); + return adopt_nonnull_ref_or_enomem(new (nothrow) FileWatcher(watcher_fd, move(notifier))); +} + +FileWatcher::FileWatcher(int watcher_fd, NonnullRefPtr notifier) + : FileWatcherBase(watcher_fd) + , m_notifier(move(notifier)) +{ + m_notifier->on_ready_to_read = [this] { + auto maybe_event = get_event_from_fd(m_notifier->fd(), m_wd_to_path); + if (maybe_event.has_value()) { + auto event = maybe_event.value(); + on_change(event); + + if (has_flag(event.type, FileWatcherEvent::Type::Deleted)) { + auto result = remove_watch(event.event_path); + if (result.is_error()) { + dbgln_if(FILE_WATCHER_DEBUG, "on_ready_to_read: {}", result.error()); + } + } + } + }; +} + +FileWatcher::~FileWatcher() = default; + +ErrorOr FileWatcherBase::add_watch(DeprecatedString path, FileWatcherEvent::Type event_mask) +{ + if (m_path_to_wd.find(path) != m_path_to_wd.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path); + return false; + } + + unsigned inotify_mask = 0; + + if (has_flag(event_mask, FileWatcherEvent::Type::ChildCreated)) + inotify_mask |= IN_CREATE; + if (has_flag(event_mask, FileWatcherEvent::Type::ChildDeleted)) + inotify_mask |= IN_DELETE; + if (has_flag(event_mask, FileWatcherEvent::Type::Deleted)) + inotify_mask |= IN_DELETE_SELF; + if (has_flag(event_mask, FileWatcherEvent::Type::ContentModified)) + inotify_mask |= IN_MODIFY; + if (has_flag(event_mask, FileWatcherEvent::Type::MetadataModified)) + inotify_mask |= IN_ATTRIB; + + int watch_descriptor = ::inotify_add_watch(m_watcher_fd, path.characters(), inotify_mask); + if (watch_descriptor < 0) + return Error::from_errno(errno); + + m_path_to_wd.set(path, watch_descriptor); + m_wd_to_path.set(watch_descriptor, path); + + dbgln_if(FILE_WATCHER_DEBUG, "add_watch: watching path '{}' on InodeWatcher {} wd {}", path, m_watcher_fd, watch_descriptor); + return true; +} + +ErrorOr FileWatcherBase::remove_watch(DeprecatedString path) +{ + auto it = m_path_to_wd.find(path); + if (it == m_path_to_wd.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path); + return false; + } + + if (::inotify_rm_watch(m_watcher_fd, it->value) < 0) + return Error::from_errno(errno); + + m_path_to_wd.remove(it); + m_wd_to_path.remove(it->value); + + dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: stopped watching path '{}' on InodeWatcher {}", path, m_watcher_fd); + return true; +} + +}