From 2159f90e00cdca66eedbbb0807b731c74695e5d8 Mon Sep 17 00:00:00 2001 From: sin-ack Date: Sun, 9 May 2021 17:09:30 +0000 Subject: [PATCH] Userland+LibCore: Update FileWatcher + its users for InodeWatcher 2.0 With the new InodeWatcher API, the old style of creating a watcher per inode will no longer work. Therefore the FileWatcher API has been updated to support multiple watches, and its users have also been refactored to the new style. At the moment, all operations done on a (Blocking)FileWatcher return Result objects, however, this may be changed in the future if it becomes too obnoxious. :^) Co-authored-by: Gunnar Beutner --- .../Dialogs/ProjectTemplatesModel.cpp | 13 +- .../DevTools/HackStudio/HackStudioWidget.cpp | 40 ++- .../DevTools/HackStudio/HackStudioWidget.h | 4 +- .../DevTools/UserspaceEmulator/Emulator.h | 5 +- .../UserspaceEmulator/Emulator_syscalls.cpp | 25 +- Userland/Libraries/LibC/fcntl.cpp | 18 +- Userland/Libraries/LibC/fcntl.h | 5 +- Userland/Libraries/LibCore/FileWatcher.cpp | 242 ++++++++++++------ Userland/Libraries/LibCore/FileWatcher.h | 94 +++++-- Userland/Libraries/LibGUI/FileSystemModel.cpp | 82 ++++-- Userland/Libraries/LibGUI/FileSystemModel.h | 6 +- Userland/Services/CrashDaemon/main.cpp | 13 +- 12 files changed, 405 insertions(+), 142 deletions(-) diff --git a/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp index ea51eef8685252b58fa1f50e8ad28bb4b0544f7f..64d9367cf798aae5cbc5f5d1356961c95fd4e3c9 100644 --- a/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp +++ b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Nick Vella + * Copyright (c) 2021, sin-ack * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include #include +#include #include #include #include @@ -21,12 +23,21 @@ ProjectTemplatesModel::ProjectTemplatesModel() : m_templates() , m_mapping() { - auto watcher_or_error = Core::FileWatcher::watch(ProjectTemplate::templates_path()); + auto watcher_or_error = Core::FileWatcher::create(); if (!watcher_or_error.is_error()) { m_file_watcher = watcher_or_error.release_value(); m_file_watcher->on_change = [&](auto) { update(); }; + + auto watch_result = m_file_watcher->add_watch( + ProjectTemplate::templates_path(), + Core::FileWatcherEvent::Type::ChildCreated + | Core::FileWatcherEvent::Type::ChildDeleted); + + if (watch_result.is_error()) { + warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watch_result.error()); + } } else { warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watcher_or_error.error()); } diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.cpp b/Userland/DevTools/HackStudio/HackStudioWidget.cpp index d71c7cdae47de862415de709a18400a3cc10fd96..ee52c1ae0e56ace54d74ce61ed8cdf06f92fee58 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.cpp +++ b/Userland/DevTools/HackStudio/HackStudioWidget.cpp @@ -1,7 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2020, Itamar S. - * Copyright (c) 2020, the SerenityOS developers. + * Copyright (c) 2020-2021, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ @@ -24,6 +24,7 @@ #include "TerminalWrapper.h" #include #include +#include #include #include #include @@ -131,6 +132,24 @@ HackStudioWidget::HackStudioWidget(const String& path_to_project) initialize_debugger(); create_toolbar(toolbar_container); + + auto maybe_watcher = Core::FileWatcher::create(); + if (maybe_watcher.is_error()) { + warnln("Couldn't create a file watcher, deleted files won't be noticed! Error: {}", maybe_watcher.error()); + } else { + m_file_watcher = maybe_watcher.release_value(); + m_file_watcher->on_change = [this](Core::FileWatcherEvent const& event) { + if (event.type != Core::FileWatcherEvent::Type::Deleted) + return; + + if (event.event_path.starts_with(project().root_path())) { + String relative_path = LexicalPath::relative_path(event.event_path, project().root_path()); + handle_external_file_deletion(relative_path); + } else { + handle_external_file_deletion(event.event_path); + } + }; + } } void HackStudioWidget::update_actions() @@ -224,18 +243,12 @@ bool HackStudioWidget::open_file(const String& full_filename) new_project_file = m_project->get_file(filename); m_open_files.set(filename, *new_project_file); m_open_files_vector.append(filename); - auto watcher_or_error = Core::FileWatcher::watch(filename); - if (!watcher_or_error.is_error()) { - auto& watcher = watcher_or_error.value(); - watcher->on_change = [this, filename]() { - struct stat st; - if (lstat(filename.characters(), &st) < 0) { - if (errno == ENOENT) { - handle_external_file_deletion(filename); - } - } - }; - m_file_watchers.set(filename, watcher_or_error.release_value()); + + if (!m_file_watcher.is_null()) { + auto watch_result = m_file_watcher->add_watch(filename, Core::FileWatcherEvent::Type::Deleted); + if (watch_result.is_error()) { + warnln("Couldn't watch '{}'", filename); + } } m_open_files_view->model()->update(); @@ -1030,7 +1043,6 @@ void HackStudioWidget::handle_external_file_deletion(const String& filepath) } } - m_file_watchers.remove(filepath); m_open_files_view->model()->update(); } diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.h b/Userland/DevTools/HackStudio/HackStudioWidget.h index f8cd15bf43c1917709dfed3826140bd57bc3023a..c80b8a28a43d4a090a9683724b283035b50c7947 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.h +++ b/Userland/DevTools/HackStudio/HackStudioWidget.h @@ -1,7 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2020, Itamar S. - * Copyright (c) 2020, the SerenityOS developers. + * Copyright (c) 2020-2021, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ @@ -122,7 +122,7 @@ private: RefPtr m_current_editor_wrapper; HashMap> m_open_files; - HashMap> m_file_watchers; + RefPtr m_file_watcher; Vector m_open_files_vector; // NOTE: This contains the keys from m_open_files and m_file_watchers OwnPtr m_project; diff --git a/Userland/DevTools/UserspaceEmulator/Emulator.h b/Userland/DevTools/UserspaceEmulator/Emulator.h index 19aa40ef5fb3e0d1bb11b441c5ad21491f418a12..3533180ae89a82705e87feab761f14560d5ee98f 100644 --- a/Userland/DevTools/UserspaceEmulator/Emulator.h +++ b/Userland/DevTools/UserspaceEmulator/Emulator.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020-2021, Andreas Kling + * Copyright (c) 2021, sin-ack * * SPDX-License-Identifier: BSD-2-Clause */ @@ -158,7 +159,9 @@ private: int virt$sched_getparam(pid_t, FlatPtr); int virt$set_thread_name(pid_t, FlatPtr, size_t); pid_t virt$setsid(); - int virt$watch_file(FlatPtr, size_t); + int virt$create_inode_watcher(unsigned); + int virt$inode_watcher_add_watch(FlatPtr); + int virt$inode_watcher_remove_watch(int, int); int virt$readlink(FlatPtr); u32 virt$allocate_tls(FlatPtr, size_t); int virt$ptsname(int fd, FlatPtr buffer, size_t buffer_size); diff --git a/Userland/DevTools/UserspaceEmulator/Emulator_syscalls.cpp b/Userland/DevTools/UserspaceEmulator/Emulator_syscalls.cpp index 3cae18b05b7a803d3bd411d546633a33cd0f5aef..c8de77ffa45e04ed952fd53b8a1739618bf54197 100644 --- a/Userland/DevTools/UserspaceEmulator/Emulator_syscalls.cpp +++ b/Userland/DevTools/UserspaceEmulator/Emulator_syscalls.cpp @@ -222,8 +222,12 @@ u32 Emulator::virt_syscall(u32 function, u32 arg1, u32 arg2, u32 arg3) return virt$set_thread_name(arg1, arg2, arg3); case SC_setsid: return virt$setsid(); - case SC_watch_file: - return virt$watch_file(arg1, arg2); + case SC_create_inode_watcher: + return virt$create_inode_watcher(arg1); + case SC_inode_watcher_add_watch: + return virt$inode_watcher_add_watch(arg1); + case SC_inode_watcher_remove_watch: + return virt$inode_watcher_remove_watch(arg1, arg2); case SC_clock_nanosleep: return virt$clock_nanosleep(arg1); case SC_readlink: @@ -1386,10 +1390,21 @@ pid_t Emulator::virt$setsid() return syscall(SC_setsid); } -int Emulator::virt$watch_file(FlatPtr user_path_addr, size_t path_length) +int Emulator::virt$create_inode_watcher(unsigned flags) { - auto user_path = mmu().copy_buffer_from_vm(user_path_addr, path_length); - return syscall(SC_watch_file, user_path.data(), user_path.size()); + return syscall(SC_create_inode_watcher, flags); +} + +int Emulator::virt$inode_watcher_add_watch(FlatPtr params_addr) +{ + Syscall::SC_inode_watcher_add_watch_params params; + mmu().copy_from_vm(¶ms, params_addr, sizeof(params)); + return syscall(SC_inode_watcher_add_watch, ¶ms); +} + +int Emulator::virt$inode_watcher_remove_watch(int fd, int wd) +{ + return syscall(SC_inode_watcher_add_watch, fd, wd); } int Emulator::virt$clock_nanosleep(FlatPtr params_addr) diff --git a/Userland/Libraries/LibC/fcntl.cpp b/Userland/Libraries/LibC/fcntl.cpp index c47e136293864c744e2387658e2252a9d06514ae..675856fc2546d6b70467b7f9a3562a2d8689c951 100644 --- a/Userland/Libraries/LibC/fcntl.cpp +++ b/Userland/Libraries/LibC/fcntl.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack * * SPDX-License-Identifier: BSD-2-Clause */ @@ -22,9 +23,22 @@ int fcntl(int fd, int cmd, ...) __RETURN_WITH_ERRNO(rc, rc, -1); } -int watch_file(const char* path, size_t path_length) +int create_inode_watcher(unsigned flags) { - int rc = syscall(SC_watch_file, path, path_length); + int rc = syscall(SC_create_inode_watcher, flags); + __RETURN_WITH_ERRNO(rc, rc, -1); +} + +int inode_watcher_add_watch(int fd, const char* path, size_t path_length, unsigned event_mask) +{ + Syscall::SC_inode_watcher_add_watch_params params { fd, { path, path_length }, event_mask }; + int rc = syscall(SC_inode_watcher_add_watch, ¶ms); + __RETURN_WITH_ERRNO(rc, rc, -1); +} + +int inode_watcher_remove_watch(int fd, int wd) +{ + int rc = syscall(SC_inode_watcher_remove_watch, fd, wd); __RETURN_WITH_ERRNO(rc, rc, -1); } diff --git a/Userland/Libraries/LibC/fcntl.h b/Userland/Libraries/LibC/fcntl.h index e4b120a9c87fff2a92459278659605642cb6578c..b23df06e38a948ef3527aa4d01cfd6d3737693b3 100644 --- a/Userland/Libraries/LibC/fcntl.h +++ b/Userland/Libraries/LibC/fcntl.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack * * SPDX-License-Identifier: BSD-2-Clause */ @@ -42,7 +43,9 @@ int open(const char* path, int options, ...); int openat(int dirfd, const char* path, int options, ...); int fcntl(int fd, int cmd, ...); -int watch_file(const char* path, size_t path_length); +int create_inode_watcher(unsigned flags); +int inode_watcher_add_watch(int fd, const char* path, size_t path_length, unsigned event_mask); +int inode_watcher_remove_watch(int fd, int wd); #define F_RDLCK 0 #define F_WRLCK 1 diff --git a/Userland/Libraries/LibCore/FileWatcher.cpp b/Userland/Libraries/LibCore/FileWatcher.cpp index a023434931b4b69ec3c4b7a777009b8ba4fd16a1..7b7f64da378af882954cdd22ed635648f16d256f 100644 --- a/Userland/Libraries/LibCore/FileWatcher.cpp +++ b/Userland/Libraries/LibCore/FileWatcher.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -24,36 +25,154 @@ namespace Core { -// Only supported in serenity mode because we use `watch_file` +// Only supported in serenity mode because we use InodeWatcher syscalls #ifdef __serenity__ -static String get_child_path_from_inode_index(const String& path, unsigned child_inode_index) +static Optional get_event_from_fd(int fd, HashMap const& wd_to_path) { - DirIterator iterator(path, Core::DirIterator::SkipDots); - if (iterator.has_error()) { + u8 buffer[MAXIMUM_EVENT_SIZE]; + int rc = read(fd, &buffer, MAXIMUM_EVENT_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 {}; } - while (iterator.has_next()) { - auto child_full_path = iterator.next_full_path(); + InodeWatcherEvent* event = reinterpret_cast(buffer); + FileWatcherEvent result; - struct stat st = {}; - if (lstat(child_full_path.characters(), &st) < 0) { + auto it = wd_to_path.find(event->watch_descriptor); + if (it == wd_to_path.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Got an event for a non-existent wd {}?!", event->watch_descriptor); + return {}; + } + String const& path = it->value; + + switch (event->type) { + case InodeWatcherEvent::Type::ChildCreated: + result.type = FileWatcherEvent::Type::ChildCreated; + break; + case InodeWatcherEvent::Type::ChildDeleted: + result.type = FileWatcherEvent::Type::ChildDeleted; + break; + case InodeWatcherEvent::Type::Deleted: + result.type = FileWatcherEvent::Type::Deleted; + break; + case InodeWatcherEvent::Type::ContentModified: + result.type = FileWatcherEvent::Type::ContentModified; + break; + case InodeWatcherEvent::Type::MetadataModified: + result.type = FileWatcherEvent::Type::MetadataModified; + break; + default: + warnln("Unknown event type {} returned by the watch_file descriptor for {}", static_cast(event->type), path); + return {}; + } + + // We trust that the kernel only sends the name when appropriate. + if (event->name_length > 0) { + String child_name { event->name, event->name_length - 1 }; + auto lexical_path = LexicalPath::join(path, child_name); + if (!lexical_path.is_valid()) { + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: Reading from wd {}: Invalid child name '{}'", fd, child_name); return {}; } - if (st.st_ino == child_inode_index) { - return child_full_path; - } + result.event_path = lexical_path.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; +} + +Result FileWatcherBase::add_watch(String path, FileWatcherEvent::Type event_mask) +{ + LexicalPath lexical_path; + if (path.length() > 0 && path[0] == '/') { + lexical_path = LexicalPath { path }; + } else { + char* buf = getcwd(nullptr, 0); + lexical_path = LexicalPath::join(String(buf), path); + free(buf); + } + + if (!lexical_path.is_valid()) { + dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' invalid", path); + return false; + } + + auto const& canonical_path = lexical_path.string(); + if (m_path_to_wd.find(canonical_path) != m_path_to_wd.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", canonical_path); + return false; + } + + auto kernel_mask = InodeWatcherEvent::Type::Invalid; + if (has_flag(event_mask, FileWatcherEvent::Type::ChildCreated)) + kernel_mask |= InodeWatcherEvent::Type::ChildCreated; + if (has_flag(event_mask, FileWatcherEvent::Type::ChildDeleted)) + kernel_mask |= InodeWatcherEvent::Type::ChildDeleted; + if (has_flag(event_mask, FileWatcherEvent::Type::Deleted)) + kernel_mask |= InodeWatcherEvent::Type::Deleted; + if (has_flag(event_mask, FileWatcherEvent::Type::ContentModified)) + kernel_mask |= InodeWatcherEvent::Type::ContentModified; + if (has_flag(event_mask, FileWatcherEvent::Type::MetadataModified)) + kernel_mask |= InodeWatcherEvent::Type::MetadataModified; + + int wd = inode_watcher_add_watch(m_watcher_fd, canonical_path.characters(), canonical_path.length(), static_cast(kernel_mask)); + if (wd < 0) + return String::formatted("Could not watch file '{}' : {}", canonical_path, strerror(errno)); + + m_path_to_wd.set(canonical_path, wd); + m_wd_to_path.set(wd, canonical_path); + + dbgln_if(FILE_WATCHER_DEBUG, "add_watch: watching path '{}' on InodeWatcher {} wd {}", canonical_path, m_watcher_fd, wd); + return true; +} + +Result FileWatcherBase::remove_watch(String path) +{ + LexicalPath lexical_path; + if (path.length() > 0 && path[0] == '/') { + lexical_path = LexicalPath { path }; + } else { + char* buf = getcwd(nullptr, 0); + lexical_path = LexicalPath::join(String(buf), path); + free(buf); + } + + if (!lexical_path.is_valid()) { + dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' invalid", path); + return false; } - return {}; + + auto const& canonical_path = lexical_path.string(); + auto it = m_path_to_wd.find(canonical_path); + if (it == m_path_to_wd.end()) { + dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", canonical_path); + return false; + } + + int rc = inode_watcher_remove_watch(m_watcher_fd, it->value); + if (rc < 0) { + return String::formatted("Could not stop watching file '{}' : {}", path, strerror(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 {}", canonical_path, m_watcher_fd); + return true; } -BlockingFileWatcher::BlockingFileWatcher(const String& path) - : m_path(path) +BlockingFileWatcher::BlockingFileWatcher(InodeWatcherFlags flags) + : FileWatcherBase(create_inode_watcher(static_cast(flags))) { - m_watcher_fd = watch_file(path.characters(), path.length()); VERIFY(m_watcher_fd != -1); + dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher created with InodeWatcher {}", m_watcher_fd); } BlockingFileWatcher::~BlockingFileWatcher() @@ -63,80 +182,51 @@ BlockingFileWatcher::~BlockingFileWatcher() Optional BlockingFileWatcher::wait_for_event() { - InodeWatcherEvent event {}; - int rc = read(m_watcher_fd, &event, sizeof(event)); - if (rc <= 0) - return {}; - - FileWatcherEvent result; - if (event.type == InodeWatcherEvent::Type::ChildAdded) - result.type = FileWatcherEvent::Type::ChildAdded; - else if (event.type == InodeWatcherEvent::Type::ChildRemoved) - result.type = FileWatcherEvent::Type::ChildRemoved; - else if (event.type == InodeWatcherEvent::Type::Modified) - result.type = FileWatcherEvent::Type::Modified; - else - return {}; + dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher::wait_for_event()"); - if (result.type == FileWatcherEvent::Type::ChildAdded || result.type == FileWatcherEvent::Type::ChildRemoved) { - auto child_path = get_child_path_from_inode_index(m_path, event.inode_index); - if (!LexicalPath(child_path).is_valid()) - return {}; + auto maybe_event = get_event_from_fd(m_watcher_fd, m_wd_to_path); + if (!maybe_event.has_value()) + return maybe_event; - result.child_path = child_path; + auto event = maybe_event.release_value(); + if (event.type == FileWatcherEvent::Type::Deleted) { + auto result = remove_watch(event.event_path); + if (result.is_error()) { + dbgln_if(FILE_WATCHER_DEBUG, "wait_for_event: {}", result.error()); + } } - return result; + return event; } -Result, String> FileWatcher::watch(const String& path) +Result, String> FileWatcher::create(InodeWatcherFlags flags) { - auto watch_fd = watch_file(path.characters(), path.length()); - if (watch_fd < 0) { - return String::formatted("Could not watch file '{}' : {}", path.characters(), strerror(errno)); + auto watcher_fd = create_inode_watcher(static_cast(flags | InodeWatcherFlags::CloseOnExec)); + if (watcher_fd < 0) { + return String::formatted("FileWatcher: Could not create InodeWatcher: {}", strerror(errno)); } - fcntl(watch_fd, F_SETFD, FD_CLOEXEC); - if (watch_fd < 0) { - return String::formatted("Could not watch file '{}' : {}", path.characters(), strerror(errno)); - } - - dbgln_if(FILE_WATCHER_DEBUG, "Started watcher for file '{}'", path.characters()); - auto notifier = Notifier::construct(watch_fd, Notifier::Event::Read); - return adopt_ref(*new FileWatcher(move(notifier), move(path))); + auto notifier = Notifier::construct(watcher_fd, Notifier::Event::Read); + return adopt_ref(*new FileWatcher(watcher_fd, move(notifier))); } -FileWatcher::FileWatcher(NonnullRefPtr notifier, const String& path) - : m_notifier(move(notifier)) - , m_path(path) +FileWatcher::FileWatcher(int watcher_fd, NonnullRefPtr notifier) + : FileWatcherBase(watcher_fd) + , m_notifier(move(notifier)) { m_notifier->on_ready_to_read = [this] { - InodeWatcherEvent event {}; - int rc = read(m_notifier->fd(), &event, sizeof(event)); - if (rc <= 0) - return; - - FileWatcherEvent result; - if (event.type == InodeWatcherEvent::Type::ChildAdded) { - result.type = FileWatcherEvent::Type::ChildAdded; - } else if (event.type == InodeWatcherEvent::Type::ChildRemoved) { - result.type = FileWatcherEvent::Type::ChildRemoved; - } else if (event.type == InodeWatcherEvent::Type::Modified) { - result.type = FileWatcherEvent::Type::Modified; - } else { - warnln("Unknown event type {} returned by the watch_file descriptor for {}", (unsigned)event.type, m_path.characters()); - return; + 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 (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()); + } + } } - - if (result.type == FileWatcherEvent::Type::ChildAdded || result.type == FileWatcherEvent::Type::ChildRemoved) { - auto child_path = get_child_path_from_inode_index(m_path, event.inode_index); - if (!LexicalPath(child_path).is_valid()) - return; - - result.child_path = child_path; - } - - on_change(result); }; } @@ -144,7 +234,7 @@ FileWatcher::~FileWatcher() { m_notifier->on_ready_to_read = nullptr; close(m_notifier->fd()); - dbgln_if(FILE_WATCHER_DEBUG, "Ended watcher for file '{}'", m_path.characters()); + dbgln_if(FILE_WATCHER_DEBUG, "Stopped watcher at fd {}", m_notifier->fd()); } #endif diff --git a/Userland/Libraries/LibCore/FileWatcher.h b/Userland/Libraries/LibCore/FileWatcher.h index 9e8a680397b20266f7b14d32c493d152b41483c5..c2a341afd76e64469fcfa232a1a2f5db89e5967a 100644 --- a/Userland/Libraries/LibCore/FileWatcher.h +++ b/Userland/Libraries/LibCore/FileWatcher.h @@ -7,54 +7,118 @@ #pragma once +#include #include #include #include #include #include #include +#include +#include #include namespace Core { struct FileWatcherEvent { enum class Type { - Modified, - ChildAdded, - ChildRemoved, + Invalid = 0, + MetadataModified = 1 << 0, + ContentModified = 1 << 1, + Deleted = 1 << 2, + ChildCreated = 1 << 3, + ChildDeleted = 1 << 4, }; Type type; - String child_path; + String event_path; }; -class BlockingFileWatcher { +AK_ENUM_BITWISE_OPERATORS(FileWatcherEvent::Type); + +class FileWatcherBase { +public: + virtual ~FileWatcherBase() { } + + Result add_watch(String path, FileWatcherEvent::Type event_mask); + Result remove_watch(String path); + bool is_watching(String const& path) const { return m_path_to_wd.find(path) != m_path_to_wd.end(); } + +protected: + FileWatcherBase(int watcher_fd) + : m_watcher_fd(watcher_fd) + { + } + + int m_watcher_fd { -1 }; + HashMap m_path_to_wd; + HashMap m_wd_to_path; +}; + +class BlockingFileWatcher final : public FileWatcherBase { AK_MAKE_NONCOPYABLE(BlockingFileWatcher); public: - explicit BlockingFileWatcher(const String& path); + explicit BlockingFileWatcher(InodeWatcherFlags = InodeWatcherFlags::None); ~BlockingFileWatcher(); Optional wait_for_event(); - -private: - String m_path; - int m_watcher_fd { -1 }; }; -class FileWatcher : public RefCounted { +class FileWatcher final : public FileWatcherBase + , public RefCounted { AK_MAKE_NONCOPYABLE(FileWatcher); public: - static Result, String> watch(const String& path); + static Result, String> create(InodeWatcherFlags = InodeWatcherFlags::None); ~FileWatcher(); - Function on_change; + Function on_change; private: - FileWatcher(NonnullRefPtr, const String& path); + FileWatcher(int watcher_fd, NonnullRefPtr); NonnullRefPtr m_notifier; - String m_path; +}; + +} + +namespace AK { + +template<> +struct Formatter : Formatter { + void format(FormatBuilder& builder, const Core::FileWatcherEvent& value) + { + Formatter::format(builder, "FileWatcherEvent(\"{}\", {})", value.event_path, value.type); + } +}; + +template<> +struct Formatter : Formatter { + void format(FormatBuilder& builder, const Core::FileWatcherEvent::Type& value) + { + char const* type; + switch (value) { + case Core::FileWatcherEvent::Type::ChildCreated: + type = "ChildCreated"; + break; + case Core::FileWatcherEvent::Type::ChildDeleted: + type = "ChildDeleted"; + break; + case Core::FileWatcherEvent::Type::Deleted: + type = "Deleted"; + break; + case Core::FileWatcherEvent::Type::ContentModified: + type = "ContentModified"; + break; + case Core::FileWatcherEvent::Type::MetadataModified: + type = "MetadataModified"; + break; + default: + VERIFY_NOT_REACHED(); + } + + builder.put_string(type); + } }; } diff --git a/Userland/Libraries/LibGUI/FileSystemModel.cpp b/Userland/Libraries/LibGUI/FileSystemModel.cpp index f6c7cea890f465813b6e4fe343da1af1838a37ad..8ba68483f59ffa2565153564a8dad445597c4294 100644 --- a/Userland/Libraries/LibGUI/FileSystemModel.cpp +++ b/Userland/Libraries/LibGUI/FileSystemModel.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2021, sin-ack * * SPDX-License-Identifier: BSD-2-Clause */ @@ -125,21 +126,18 @@ void FileSystemModel::Node::traverse_if_needed() children.append(move(directory_children)); children.append(move(file_children)); - if (!m_file_watcher) { - - // We are not already watching this file, create a new watcher - auto watcher_or_error = Core::FileWatcher::watch(full_path); - - // Note : the watcher may not be created (e.g. we do not have access rights.) This is expected, just don't watch if that's the case. - if (!watcher_or_error.is_error()) { - m_file_watcher = watcher_or_error.release_value(); - m_file_watcher->on_change = [this](auto) { - has_traversed = false; - mode = 0; - children.clear(); - reify_if_needed(); - m_model.did_update(); - }; + if (!m_model.m_file_watcher->is_watching(full_path)) { + // We are not already watching this file, watch it + auto result = m_model.m_file_watcher->add_watch(full_path, + Core::FileWatcherEvent::Type::MetadataModified + | Core::FileWatcherEvent::Type::ChildCreated + | Core::FileWatcherEvent::Type::ChildDeleted + | Core::FileWatcherEvent::Type::Deleted); + + if (result.is_error()) { + dbgln("Couldn't watch '{}': {}", full_path, result.error()); + } else if (result.value() == false) { + dbgln("Couldn't watch '{}', probably already watching", full_path); } } } @@ -181,10 +179,29 @@ String FileSystemModel::Node::full_path() const ModelIndex FileSystemModel::index(String path, int column) const { - LexicalPath lexical_path(move(path)); + Node const* node = node_for_path(move(path)); + if (node != nullptr) { + return node->index(column); + } + + return {}; +} + +FileSystemModel::Node const* FileSystemModel::node_for_path(String const& path) const +{ + LexicalPath lexical_path; + if (path == m_root_path) { + lexical_path = LexicalPath { "/" }; + } else if (!m_root_path.is_empty() && path.starts_with(m_root_path)) { + lexical_path = LexicalPath { LexicalPath::relative_path(path, m_root_path) }; + } else { + lexical_path = LexicalPath { move(path) }; + } + const Node* node = m_root->m_parent_of_root ? &m_root->children.first() : m_root; if (lexical_path.string() == "/") - return node->index(column); + return node; + for (size_t i = 0; i < lexical_path.parts().size(); ++i) { auto& part = lexical_path.parts()[i]; bool found = false; @@ -194,14 +211,14 @@ ModelIndex FileSystemModel::index(String path, int column) const node = &child; found = true; if (i == lexical_path.parts().size() - 1) - return child.index(column); + return node; break; } } if (!found) - return {}; + return nullptr; } - return {}; + return nullptr; } String FileSystemModel::full_path(const ModelIndex& index) const @@ -225,6 +242,31 @@ FileSystemModel::FileSystemModel(String root_path, Mode mode) m_group_names.set(group->gr_gid, group->gr_name); endgrent(); + auto result = Core::FileWatcher::create(); + if (result.is_error()) { + dbgln("{}", result.error()); + VERIFY_NOT_REACHED(); + } + + m_file_watcher = result.release_value(); + m_file_watcher->on_change = [this](Core::FileWatcherEvent const& event) { + Node const* maybe_node = node_for_path(event.event_path); + if (maybe_node == nullptr) { + dbgln("Received event at \"{}\" but we don't have that node", event.event_path); + return; + } + auto& node = *const_cast(maybe_node); + + dbgln("Event at \"{}\" on Node {}: {}", node.full_path(), &node, event); + + // FIXME: Your time is coming, un-granular updates. + node.has_traversed = false; + node.mode = 0; + node.children.clear(); + node.reify_if_needed(); + did_update(); + }; + update(); } diff --git a/Userland/Libraries/LibGUI/FileSystemModel.h b/Userland/Libraries/LibGUI/FileSystemModel.h index 6b7c7544aa4ec1fecaee6a663fe78c3f88e6d728..4ad589c82bfd3c6b43a143ce3f4227429f87bc3d 100644 --- a/Userland/Libraries/LibGUI/FileSystemModel.h +++ b/Userland/Libraries/LibGUI/FileSystemModel.h @@ -87,8 +87,6 @@ public: bool m_selected { false }; - RefPtr m_file_watcher; - int m_error { 0 }; bool m_parent_of_root { false }; @@ -148,6 +146,8 @@ private: String name_for_uid(uid_t) const; String name_for_gid(gid_t) const; + Node const* node_for_path(String const&) const; + HashMap m_user_names; HashMap m_group_names; @@ -162,6 +162,8 @@ private: unsigned m_thumbnail_progress_total { 0 }; bool m_should_show_dotfiles { false }; + + RefPtr m_file_watcher; }; } diff --git a/Userland/Services/CrashDaemon/main.cpp b/Userland/Services/CrashDaemon/main.cpp index ddf247259718a93b18914c5db721eee78adbb8a5..db542fc02f8cdad8a167b53140cebdf7c5ac5932 100644 --- a/Userland/Services/CrashDaemon/main.cpp +++ b/Userland/Services/CrashDaemon/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -99,13 +100,19 @@ int main() return 1; } - Core::BlockingFileWatcher watcher { "/tmp/coredump" }; + Core::BlockingFileWatcher watcher; + auto watch_result = watcher.add_watch("/tmp/coredump", Core::FileWatcherEvent::Type::ChildCreated); + if (watch_result.is_error()) { + warnln("Failed to watch the coredump directory: {}", watch_result.error()); + VERIFY_NOT_REACHED(); + } + while (true) { auto event = watcher.wait_for_event(); VERIFY(event.has_value()); - if (event.value().type != Core::FileWatcherEvent::Type::ChildAdded) + if (event.value().type != Core::FileWatcherEvent::Type::ChildCreated) continue; - auto coredump_path = event.value().child_path; + auto& coredump_path = event.value().event_path; if (coredump_path.ends_with(".gz")) continue; // stops compress_coredump from accidentally triggering us dbgln("New coredump file: {}", coredump_path);