mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-10 10:09:14 +00:00
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:
parent
574b4be433
commit
9f496a9c65
Notes:
github-actions[bot]
2024-08-25 07:48:58 +00:00
Author: https://github.com/trflynn89
Commit: 9f496a9c65
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/1182
2 changed files with 74 additions and 6 deletions
|
@ -4,9 +4,13 @@
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <AK/LexicalPath.h>
|
||||||
#include <LibCore/EventLoop.h>
|
#include <LibCore/EventLoop.h>
|
||||||
|
#include <LibCore/File.h>
|
||||||
#include <LibCore/FileWatcher.h>
|
#include <LibCore/FileWatcher.h>
|
||||||
|
#include <LibCore/System.h>
|
||||||
#include <LibCore/Timer.h>
|
#include <LibCore/Timer.h>
|
||||||
|
#include <LibFileSystem/FileSystem.h>
|
||||||
#include <LibTest/TestCase.h>
|
#include <LibTest/TestCase.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
@ -61,3 +65,45 @@ TEST_CASE(file_watcher_child_events)
|
||||||
|
|
||||||
event_loop.exec();
|
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();
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ namespace Core {
|
||||||
struct MonitoredPath {
|
struct MonitoredPath {
|
||||||
ByteString path;
|
ByteString path;
|
||||||
FileWatcherEvent::Type event_mask { FileWatcherEvent::Type::Invalid };
|
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[]);
|
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;
|
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 {
|
class FileWatcherMacOS final : public FileWatcher {
|
||||||
AK_MAKE_NONCOPYABLE(FileWatcherMacOS);
|
AK_MAKE_NONCOPYABLE(FileWatcherMacOS);
|
||||||
|
|
||||||
|
@ -64,6 +72,10 @@ public:
|
||||||
|
|
||||||
ErrorOr<bool> add_watch(ByteString path, FileWatcherEvent::Type event_mask)
|
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)) {
|
if (m_path_to_inode_id.contains(path)) {
|
||||||
dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path);
|
dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path);
|
||||||
return false;
|
return false;
|
||||||
|
@ -71,7 +83,7 @@ public:
|
||||||
|
|
||||||
auto inode_id = TRY(inode_id_from_path(path));
|
auto inode_id = TRY(inode_id_from_path(path));
|
||||||
TRY(m_path_to_inode_id.try_set(path, inode_id));
|
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());
|
TRY(refresh_monitored_paths());
|
||||||
|
|
||||||
|
@ -81,6 +93,9 @@ public:
|
||||||
|
|
||||||
ErrorOr<bool> remove_watch(ByteString path)
|
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);
|
auto it = m_path_to_inode_id.find(path);
|
||||||
if (it == m_path_to_inode_id.end()) {
|
if (it == m_path_to_inode_id.end()) {
|
||||||
dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path);
|
dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path);
|
||||||
|
@ -109,7 +124,8 @@ public:
|
||||||
|
|
||||||
return MonitoredPath {
|
return MonitoredPath {
|
||||||
LexicalPath::join(it->value.path, lexical_path.basename()).string(),
|
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);
|
event.event_path = move(monitored_path.path);
|
||||||
|
|
||||||
auto flags = event_flags[i];
|
auto flags = event_flags[i];
|
||||||
if ((flags & kFSEventStreamEventFlagItemCreated) != 0)
|
if ((flags & kFSEventStreamEventFlagItemCreated) != 0) {
|
||||||
event.type |= FileWatcherEvent::Type::ChildCreated;
|
if (monitored_path.is_directory)
|
||||||
if ((flags & kFSEventStreamEventFlagItemRemoved) != 0)
|
event.type |= FileWatcherEvent::Type::ChildCreated;
|
||||||
event.type |= FileWatcherEvent::Type::ChildDeleted;
|
}
|
||||||
|
if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) {
|
||||||
|
if (monitored_path.is_directory)
|
||||||
|
event.type |= FileWatcherEvent::Type::ChildDeleted;
|
||||||
|
else
|
||||||
|
event.type |= FileWatcherEvent::Type::Deleted;
|
||||||
|
}
|
||||||
if ((flags & kFSEventStreamEventFlagItemModified) != 0)
|
if ((flags & kFSEventStreamEventFlagItemModified) != 0)
|
||||||
event.type |= FileWatcherEvent::Type::ContentModified;
|
event.type |= FileWatcherEvent::Type::ContentModified;
|
||||||
if ((flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0)
|
if ((flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue