/* * Copyright (c) 2020, Itamar S. * Copyright (c) 2021, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include "FileWatcher.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Core { // Only supported in serenity mode because we use InodeWatcher syscalls #ifdef __serenity__ static Optional get_event_from_fd(int fd, HashMap const& wd_to_path) { 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 {}; } InodeWatcherEvent* event = reinterpret_cast(buffer); FileWatcherEvent result; 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 }; 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; } static String canonicalize_path(String path) { if (!path.is_empty() && path[0] == '/') return LexicalPath::canonicalized_path(move(path)); char* cwd = getcwd(nullptr, 0); VERIFY(cwd); return LexicalPath::join(cwd, move(path)).string(); } Result FileWatcherBase::add_watch(String path, FileWatcherEvent::Type event_mask) { String canonical_path = canonicalize_path(move(path)); 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) { String canonical_path = canonicalize_path(move(path)); 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))) { VERIFY(m_watcher_fd != -1); dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher created with InodeWatcher {}", m_watcher_fd); } BlockingFileWatcher::~BlockingFileWatcher() { close(m_watcher_fd); } Optional BlockingFileWatcher::wait_for_event() { dbgln_if(FILE_WATCHER_DEBUG, "BlockingFileWatcher::wait_for_event()"); auto maybe_event = get_event_from_fd(m_watcher_fd, m_wd_to_path); if (!maybe_event.has_value()) return maybe_event; 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 event; } Result, String> FileWatcher::create(InodeWatcherFlags flags) { 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)); } auto notifier = Notifier::construct(watcher_fd, Notifier::Event::Read); return adopt_ref(*new 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 (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() { m_notifier->on_ready_to_read = nullptr; close(m_notifier->fd()); dbgln_if(FILE_WATCHER_DEBUG, "Stopped watcher at fd {}", m_notifier->fd()); } #endif }