FileWatcherMacOS.mm 9.6 KB

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