diff --git a/Tests/LibCore/TestLibCoreFileWatcher.cpp b/Tests/LibCore/TestLibCoreFileWatcher.cpp index 8f871ceb0ae..98f623c6e2a 100644 --- a/Tests/LibCore/TestLibCoreFileWatcher.cpp +++ b/Tests/LibCore/TestLibCoreFileWatcher.cpp @@ -4,9 +4,13 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include #include +#include #include +#include #include #include #include @@ -61,3 +65,45 @@ TEST_CASE(file_watcher_child_events) event_loop.exec(); } + +TEST_CASE(contents_changed) +{ + auto event_loop = Core::EventLoop(); + + auto temp_path = MUST(FileSystem::real_path("/tmp"sv)); + auto test_path = LexicalPath::join(temp_path, "testfile"sv); + + auto write_file = [&](auto contents) { + auto file = MUST(Core::File::open(test_path.string(), Core::File::OpenMode::Write)); + MUST(file->write_until_depleted(contents)); + }; + + write_file("line1\n"sv); + + auto file_watcher = MUST(Core::FileWatcher::create()); + MUST(file_watcher->add_watch(test_path.string(), Core::FileWatcherEvent::Type::ContentModified)); + + int event_count = 0; + file_watcher->on_change = [&](Core::FileWatcherEvent const& event) { + EXPECT_EQ(event.event_path, test_path.string()); + EXPECT(has_flag(event.type, Core::FileWatcherEvent::Type::ContentModified)); + + if (++event_count == 2) { + MUST(Core::System::unlink(test_path.string())); + event_loop.quit(0); + } + }; + + auto timer1 = Core::Timer::create_single_shot(500, [&] { write_file("line2\n"sv); }); + timer1->start(); + + auto timer2 = Core::Timer::create_single_shot(1000, [&] { write_file("line3\n"sv); }); + timer2->start(); + + auto catchall_timer = Core::Timer::create_single_shot(2000, [&] { + VERIFY_NOT_REACHED(); + }); + catchall_timer->start(); + + event_loop.exec(); +} diff --git a/Userland/Libraries/LibCore/FileWatcherMacOS.mm b/Userland/Libraries/LibCore/FileWatcherMacOS.mm index a4cbe50cb27..37c0a20d4a6 100644 --- a/Userland/Libraries/LibCore/FileWatcherMacOS.mm +++ b/Userland/Libraries/LibCore/FileWatcherMacOS.mm @@ -26,6 +26,7 @@ namespace Core { struct MonitoredPath { ByteString path; FileWatcherEvent::Type event_mask { FileWatcherEvent::Type::Invalid }; + bool is_directory { false }; }; static void on_file_system_event(ConstFSEventStreamRef, void*, size_t, void*, FSEventStreamEventFlags const[], FSEventStreamEventId const[]); @@ -36,6 +37,13 @@ static ErrorOr inode_id_from_path(StringView path) return stat.st_ino; } +static ErrorOr is_directory(StringView path) +{ + // We cannot use FileSystem::is_directory as LibFileSystem depends on LibCore. + auto stat = TRY(System::stat(path)); + return S_ISDIR(stat.st_mode); +} + class FileWatcherMacOS final : public FileWatcher { AK_MAKE_NONCOPYABLE(FileWatcherMacOS); @@ -64,6 +72,10 @@ public: ErrorOr add_watch(ByteString path, FileWatcherEvent::Type event_mask) { + auto path_is_directory = TRY(is_directory(path)); + if (!path_is_directory) + path = LexicalPath { move(path) }.parent().string(); + if (m_path_to_inode_id.contains(path)) { dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path); return false; @@ -71,7 +83,7 @@ public: auto inode_id = TRY(inode_id_from_path(path)); TRY(m_path_to_inode_id.try_set(path, inode_id)); - TRY(m_inode_id_to_path.try_set(inode_id, { path, event_mask })); + TRY(m_inode_id_to_path.try_set(inode_id, { path, event_mask, path_is_directory })); TRY(refresh_monitored_paths()); @@ -81,6 +93,9 @@ public: ErrorOr remove_watch(ByteString path) { + if (!TRY(is_directory(path))) + path = LexicalPath { move(path) }.parent().string(); + auto it = m_path_to_inode_id.find(path); if (it == m_path_to_inode_id.end()) { dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path); @@ -109,7 +124,8 @@ public: return MonitoredPath { LexicalPath::join(it->value.path, lexical_path.basename()).string(), - it->value.event_mask + it->value.event_mask, + it->value.is_directory }; } @@ -225,10 +241,16 @@ void on_file_system_event(ConstFSEventStreamRef, void* user_data, size_t event_s event.event_path = move(monitored_path.path); auto flags = event_flags[i]; - if ((flags & kFSEventStreamEventFlagItemCreated) != 0) - event.type |= FileWatcherEvent::Type::ChildCreated; - if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) - event.type |= FileWatcherEvent::Type::ChildDeleted; + if ((flags & kFSEventStreamEventFlagItemCreated) != 0) { + if (monitored_path.is_directory) + event.type |= FileWatcherEvent::Type::ChildCreated; + } + if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) { + if (monitored_path.is_directory) + event.type |= FileWatcherEvent::Type::ChildDeleted; + else + event.type |= FileWatcherEvent::Type::Deleted; + } if ((flags & kFSEventStreamEventFlagItemModified) != 0) event.type |= FileWatcherEvent::Type::ContentModified; if ((flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0)