LibCore: Improve support for the macOS file watcher with actual files

When asked to monitor a file (not a directory), we often need to instead
monitor the parent directory to receive FS events. For example, when a
symbolic link is deleted/created, we don't receive any events unless we
are watching the parent.
This commit is contained in:
Timothy Flynn 2024-08-23 14:23:57 -04:00 committed by Andreas Kling
parent 574b4be433
commit 9f496a9c65
Notes: github-actions[bot] 2024-08-25 07:48:58 +00:00
2 changed files with 74 additions and 6 deletions

View file

@ -4,9 +4,13 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/FileWatcher.h>
#include <LibCore/System.h>
#include <LibCore/Timer.h>
#include <LibFileSystem/FileSystem.h>
#include <LibTest/TestCase.h>
#include <fcntl.h>
#include <unistd.h>
@ -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();
}

View file

@ -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<ino_t> inode_id_from_path(StringView path)
return stat.st_ino;
}
static ErrorOr<bool> 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<bool> 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<bool> 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)