diff --git a/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp index ea51eef868..64d9367cf7 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 d71c7cdae4..ee52c1ae0e 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 f8cd15bf43..c80b8a28a4 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 19aa40ef5f..3533180ae8 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 3cae18b05b..c8de77ffa4 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 c47e136293..675856fc25 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 e4b120a9c8..b23df06e38 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 a023434931..7b7f64da37 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; } - return {}; + + dbgln_if(FILE_WATCHER_DEBUG, "get_event_from_fd: got event from wd {} on '{}' type {}", fd, result.event_path, result.type); + return result; } -BlockingFileWatcher::BlockingFileWatcher(const String& path) - : m_path(path) +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; + } + + 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(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 {}; + dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher::wait_for_event()"); - 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 {}; + auto maybe_event = get_event_from_fd(m_watcher_fd, m_wd_to_path); + if (!maybe_event.has_value()) + return maybe_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 {}; - - 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; + 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); - 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; + 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 9e8a680397..c2a341afd7 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 f6c7cea890..8ba68483f5 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) { + 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); - // 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 (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 6b7c7544aa..4ad589c82b 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 ddf2472597..db542fc02f 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);