Serenity Operating System
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}