Serenity Operating System
at master 277 lines 9.8 kB view raw
1/* 2 * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "FileWatcher.h" 8#include <AK/Debug.h> 9#include <AK/LexicalPath.h> 10#include <AK/OwnPtr.h> 11#include <LibCore/EventLoop.h> 12#include <LibCore/Notifier.h> 13#include <LibCore/System.h> 14#include <errno.h> 15#include <limits.h> 16 17#if !defined(AK_OS_MACOS) 18static_assert(false, "This file must only be used for macOS"); 19#endif 20 21#define FixedPoint FixedPointMacOS // AK::FixedPoint conflicts with FixedPoint from MacTypes.h. 22#include <CoreServices/CoreServices.h> 23#include <dispatch/dispatch.h> 24#undef FixedPoint 25 26namespace Core { 27 28struct MonitoredPath { 29 DeprecatedString path; 30 FileWatcherEvent::Type event_mask { FileWatcherEvent::Type::Invalid }; 31}; 32 33static void on_file_system_event(ConstFSEventStreamRef, void*, size_t, void*, FSEventStreamEventFlags const[], FSEventStreamEventId const[]); 34 35static ErrorOr<ino_t> inode_id_from_path(StringView path) 36{ 37 auto stat = TRY(System::stat(path)); 38 return stat.st_ino; 39} 40 41class FileWatcherMacOS final : public FileWatcher { 42 AK_MAKE_NONCOPYABLE(FileWatcherMacOS); 43 44public: 45 virtual ~FileWatcherMacOS() override 46 { 47 close_event_stream(); 48 dispatch_release(m_dispatch_queue); 49 } 50 51 static ErrorOr<NonnullRefPtr<FileWatcherMacOS>> create(FileWatcherFlags) 52 { 53 auto context = TRY(try_make<FSEventStreamContext>()); 54 55 auto queue_name = DeprecatedString::formatted("Serenity.FileWatcher.{:p}", context.ptr()); 56 auto dispatch_queue = dispatch_queue_create(queue_name.characters(), DISPATCH_QUEUE_SERIAL); 57 if (dispatch_queue == nullptr) 58 return Error::from_errno(errno); 59 60 // NOTE: This isn't actually used on macOS, but is needed for FileWatcherBase. 61 // Creating it with an FD of -1 will effectively disable the notifier. 62 auto notifier = TRY(Notifier::try_create(-1, Notifier::Event::None)); 63 64 return adopt_nonnull_ref_or_enomem(new (nothrow) FileWatcherMacOS(move(context), dispatch_queue, move(notifier))); 65 } 66 67 ErrorOr<bool> add_watch(DeprecatedString path, FileWatcherEvent::Type event_mask) 68 { 69 if (m_path_to_inode_id.contains(path)) { 70 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: path '{}' is already being watched", path); 71 return false; 72 } 73 74 auto inode_id = TRY(inode_id_from_path(path)); 75 TRY(m_path_to_inode_id.try_set(path, inode_id)); 76 TRY(m_inode_id_to_path.try_set(inode_id, { path, event_mask })); 77 78 TRY(refresh_monitored_paths()); 79 80 dbgln_if(FILE_WATCHER_DEBUG, "add_watch: watching path '{}' inode {}", path, inode_id); 81 return true; 82 } 83 84 ErrorOr<bool> remove_watch(DeprecatedString path) 85 { 86 auto it = m_path_to_inode_id.find(path); 87 if (it == m_path_to_inode_id.end()) { 88 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: path '{}' is not being watched", path); 89 return false; 90 } 91 92 m_inode_id_to_path.remove(it->value); 93 m_path_to_inode_id.remove(it); 94 95 TRY(refresh_monitored_paths()); 96 97 dbgln_if(FILE_WATCHER_DEBUG, "remove_watch: stopped watching path '{}'", path); 98 return true; 99 } 100 101 ErrorOr<MonitoredPath> canonicalize_path(DeprecatedString path) 102 { 103 LexicalPath lexical_path { move(path) }; 104 auto parent_path = lexical_path.parent(); 105 106 auto inode_id = TRY(inode_id_from_path(parent_path.string())); 107 108 auto it = m_inode_id_to_path.find(inode_id); 109 if (it == m_inode_id_to_path.end()) 110 return Error::from_string_literal("Got an event for a non-existent inode ID"); 111 112 return MonitoredPath { 113 LexicalPath::join(it->value.path, lexical_path.basename()).string(), 114 it->value.event_mask 115 }; 116 } 117 118 void handle_event(FileWatcherEvent event) 119 { 120 NonnullRefPtr strong_this { *this }; 121 122 m_main_event_loop.deferred_invoke( 123 [strong_this = move(strong_this), event = move(event)]() { 124 strong_this->on_change(event); 125 }); 126 } 127 128private: 129 FileWatcherMacOS(NonnullOwnPtr<FSEventStreamContext> context, dispatch_queue_t dispatch_queue, NonnullRefPtr<Notifier> notifier) 130 : FileWatcher(-1, move(notifier)) 131 , m_main_event_loop(EventLoop::current()) 132 , m_context(move(context)) 133 , m_dispatch_queue(dispatch_queue) 134 { 135 m_context->info = this; 136 } 137 138 void close_event_stream() 139 { 140 if (!m_stream) 141 return; 142 143 dispatch_sync(m_dispatch_queue, ^{ 144 FSEventStreamStop(m_stream); 145 FSEventStreamInvalidate(m_stream); 146 FSEventStreamRelease(m_stream); 147 m_stream = nullptr; 148 }); 149 } 150 151 ErrorOr<void> refresh_monitored_paths() 152 { 153 static constexpr FSEventStreamCreateFlags stream_flags = kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagUseExtendedData; 154 static constexpr CFAbsoluteTime stream_latency = 0.25; 155 156 close_event_stream(); 157 158 if (m_path_to_inode_id.is_empty()) 159 return {}; 160 161 auto monitored_paths = CFArrayCreateMutable(kCFAllocatorDefault, m_path_to_inode_id.size(), &kCFTypeArrayCallBacks); 162 if (monitored_paths == nullptr) 163 return Error::from_errno(ENOMEM); 164 165 for (auto it : m_path_to_inode_id) { 166 auto path = CFStringCreateWithCString(kCFAllocatorDefault, it.key.characters(), kCFStringEncodingUTF8); 167 if (path == nullptr) 168 return Error::from_errno(ENOMEM); 169 170 CFArrayAppendValue(monitored_paths, static_cast<void const*>(path)); 171 } 172 173 dispatch_sync(m_dispatch_queue, ^{ 174 m_stream = FSEventStreamCreate( 175 kCFAllocatorDefault, 176 &on_file_system_event, 177 m_context.ptr(), 178 monitored_paths, 179 kFSEventStreamEventIdSinceNow, 180 stream_latency, 181 stream_flags); 182 183 if (m_stream) { 184 FSEventStreamSetDispatchQueue(m_stream, m_dispatch_queue); 185 FSEventStreamStart(m_stream); 186 } 187 }); 188 189 if (!m_stream) 190 return Error::from_string_literal("Could not create an FSEventStream"); 191 return {}; 192 } 193 194 EventLoop& m_main_event_loop; 195 196 NonnullOwnPtr<FSEventStreamContext> m_context; 197 dispatch_queue_t m_dispatch_queue { nullptr }; 198 FSEventStreamRef m_stream { nullptr }; 199 200 HashMap<DeprecatedString, ino_t> m_path_to_inode_id; 201 HashMap<ino_t, MonitoredPath> m_inode_id_to_path; 202}; 203 204void on_file_system_event(ConstFSEventStreamRef, void* user_data, size_t event_size, void* event_paths, FSEventStreamEventFlags const event_flags[], FSEventStreamEventId const[]) 205{ 206 auto& file_watcher = *reinterpret_cast<FileWatcherMacOS*>(user_data); 207 auto paths = reinterpret_cast<CFArrayRef>(event_paths); 208 209 for (size_t i = 0; i < event_size; ++i) { 210 auto path_dictionary = static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(paths, static_cast<CFIndex>(i))); 211 auto path = static_cast<CFStringRef>(CFDictionaryGetValue(path_dictionary, kFSEventStreamEventExtendedDataPathKey)); 212 213 char file_path_buffer[PATH_MAX] {}; 214 if (!CFStringGetFileSystemRepresentation(path, file_path_buffer, sizeof(file_path_buffer))) { 215 dbgln_if(FILE_WATCHER_DEBUG, "Could not convert event to a file path"); 216 continue; 217 } 218 219 auto maybe_monitored_path = file_watcher.canonicalize_path(DeprecatedString { file_path_buffer }); 220 if (maybe_monitored_path.is_error()) { 221 dbgln_if(FILE_WATCHER_DEBUG, "Could not canonicalize path {}: {}", file_path_buffer, maybe_monitored_path.error()); 222 continue; 223 } 224 auto monitored_path = maybe_monitored_path.release_value(); 225 226 FileWatcherEvent event; 227 event.event_path = move(monitored_path.path); 228 229 auto flags = event_flags[i]; 230 if ((flags & kFSEventStreamEventFlagItemCreated) != 0) 231 event.type |= FileWatcherEvent::Type::ChildCreated; 232 if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) 233 event.type |= FileWatcherEvent::Type::ChildDeleted; 234 if ((flags & kFSEventStreamEventFlagItemModified) != 0) 235 event.type |= FileWatcherEvent::Type::ContentModified; 236 if ((flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0) 237 event.type |= FileWatcherEvent::Type::MetadataModified; 238 239 if (event.type == FileWatcherEvent::Type::Invalid) { 240 dbgln_if(FILE_WATCHER_DEBUG, "Unknown event type {:x} returned by the FS event for {}", flags, path); 241 continue; 242 } 243 if ((event.type & monitored_path.event_mask) == FileWatcherEvent::Type::Invalid) { 244 dbgln_if(FILE_WATCHER_DEBUG, "Dropping unwanted FS event {} for {}", flags, path); 245 continue; 246 } 247 248 file_watcher.handle_event(move(event)); 249 } 250} 251 252ErrorOr<NonnullRefPtr<FileWatcher>> FileWatcher::create(FileWatcherFlags flags) 253{ 254 return TRY(FileWatcherMacOS::create(flags)); 255} 256 257FileWatcher::FileWatcher(int watcher_fd, NonnullRefPtr<Notifier> notifier) 258 : FileWatcherBase(watcher_fd) 259 , m_notifier(move(notifier)) 260{ 261} 262 263FileWatcher::~FileWatcher() = default; 264 265ErrorOr<bool> FileWatcherBase::add_watch(DeprecatedString path, FileWatcherEvent::Type event_mask) 266{ 267 auto& file_watcher = verify_cast<FileWatcherMacOS>(*this); 268 return file_watcher.add_watch(move(path), event_mask); 269} 270 271ErrorOr<bool> FileWatcherBase::remove_watch(DeprecatedString path) 272{ 273 auto& file_watcher = verify_cast<FileWatcherMacOS>(*this); 274 return file_watcher.remove_watch(move(path)); 275} 276 277}