mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-22 04:25:13 +00:00
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:
parent
91cbdc67de
commit
8ca528217c
Notes:
sideshowbarker
2024-07-17 01:46:00 +09:00
Author: https://github.com/trflynn89 Commit: https://github.com/SerenityOS/serenity/commit/8ca528217c Pull-request: https://github.com/SerenityOS/serenity/pull/17063 Reviewed-by: https://github.com/ADKaster ✅ Reviewed-by: https://github.com/gmta ✅
4 changed files with 184 additions and 3 deletions
|
@ -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})
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
175
Userland/Libraries/LibCore/FileWatcherLinux.cpp
Normal file
175
Userland/Libraries/LibCore/FileWatcherLinux.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue