LibCore: Implement FileWatcher for Linux

This implements FileWatcher using inotify filesystem events. Serenity's
InodeWatcher is remarkably similar to inotify, so this is almost an
identical implementation.

The existing TestLibCoreFileWatcher test is added to Lagom (currently
just for Linux).

This does not implement BlockingFileWatcher as that is currently not
used anywhere but on Serenity.
This commit is contained in:
Timothy Flynn 2023-01-17 14:43:36 -05:00 committed by Tim Flynn
parent 91cbdc67de
commit 8ca528217c
4 changed files with 184 additions and 3 deletions

View file

@ -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})

View file

@ -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);
}

View file

@ -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()

View file

@ -0,0 +1,175 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "FileWatcher.h"
#include <AK/Debug.h>
#include <AK/DeprecatedString.h>
#include <AK/LexicalPath.h>
#include <Kernel/API/InodeWatcherFlags.h>
#include <LibCore/Notifier.h>
#include <errno.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <unistd.h>
#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<FileWatcherEvent> get_event_from_fd(int fd, HashMap<unsigned, DeprecatedString> 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<u8, max_event_size> 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<inotify_event const*>(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<NonnullRefPtr<FileWatcher>> 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> 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<bool> 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<bool> 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;
}
}