compat/fsmonitor/fsm-listen-darwin: implement FSEvent listener on MacOS

Implement file system event listener on MacOS using FSEvent, CoreFoundation, and CoreServices. Co-authored-by: Kevin Willford <Kevin.Willford@microsoft.com> Co-authored-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff Hostetler committed Mar 25, 2022 at 18:03 UTC 65723b305ad431f0cc1161c1e98346a2ec3152f2
1 file changed +383
compat/fsmonitor/fsm-listen-darwin.c
+383
@@ -25,20 +25,403 @@
25 #include "cache.h"
26 #include "fsmonitor.h"
27 #include "fsm-listen.h"
28 +#include "fsmonitor--daemon.h"
29 +
30 +struct fsmonitor_daemon_backend_data
31 +{
32 + CFStringRef cfsr_worktree_path;
33 + CFStringRef cfsr_gitdir_path;
34 +
35 + CFArrayRef cfar_paths_to_watch;
36 + int nr_paths_watching;
37 +
38 + FSEventStreamRef stream;
39 +
40 + CFRunLoopRef rl;
41 +
42 + enum shutdown_style {
43 + SHUTDOWN_EVENT = 0,
44 + FORCE_SHUTDOWN,
45 + FORCE_ERROR_STOP,
46 + } shutdown_style;
47 +
48 + unsigned int stream_scheduled:1;
49 + unsigned int stream_started:1;
50 +};
51 +
52 +static void log_flags_set(const char *path, const FSEventStreamEventFlags flag)
53 +{
54 + struct strbuf msg = STRBUF_INIT;
55 +
56 + if (flag & kFSEventStreamEventFlagMustScanSubDirs)
57 + strbuf_addstr(&msg, "MustScanSubDirs|");
58 + if (flag & kFSEventStreamEventFlagUserDropped)
59 + strbuf_addstr(&msg, "UserDropped|");
60 + if (flag & kFSEventStreamEventFlagKernelDropped)
61 + strbuf_addstr(&msg, "KernelDropped|");
62 + if (flag & kFSEventStreamEventFlagEventIdsWrapped)
63 + strbuf_addstr(&msg, "EventIdsWrapped|");
64 + if (flag & kFSEventStreamEventFlagHistoryDone)
65 + strbuf_addstr(&msg, "HistoryDone|");
66 + if (flag & kFSEventStreamEventFlagRootChanged)
67 + strbuf_addstr(&msg, "RootChanged|");
68 + if (flag & kFSEventStreamEventFlagMount)
69 + strbuf_addstr(&msg, "Mount|");
70 + if (flag & kFSEventStreamEventFlagUnmount)
71 + strbuf_addstr(&msg, "Unmount|");
72 + if (flag & kFSEventStreamEventFlagItemChangeOwner)
73 + strbuf_addstr(&msg, "ItemChangeOwner|");
74 + if (flag & kFSEventStreamEventFlagItemCreated)
75 + strbuf_addstr(&msg, "ItemCreated|");
76 + if (flag & kFSEventStreamEventFlagItemFinderInfoMod)
77 + strbuf_addstr(&msg, "ItemFinderInfoMod|");
78 + if (flag & kFSEventStreamEventFlagItemInodeMetaMod)
79 + strbuf_addstr(&msg, "ItemInodeMetaMod|");
80 + if (flag & kFSEventStreamEventFlagItemIsDir)
81 + strbuf_addstr(&msg, "ItemIsDir|");
82 + if (flag & kFSEventStreamEventFlagItemIsFile)
83 + strbuf_addstr(&msg, "ItemIsFile|");
84 + if (flag & kFSEventStreamEventFlagItemIsHardlink)
85 + strbuf_addstr(&msg, "ItemIsHardlink|");
86 + if (flag & kFSEventStreamEventFlagItemIsLastHardlink)
87 + strbuf_addstr(&msg, "ItemIsLastHardlink|");
88 + if (flag & kFSEventStreamEventFlagItemIsSymlink)
89 + strbuf_addstr(&msg, "ItemIsSymlink|");
90 + if (flag & kFSEventStreamEventFlagItemModified)
91 + strbuf_addstr(&msg, "ItemModified|");
92 + if (flag & kFSEventStreamEventFlagItemRemoved)
93 + strbuf_addstr(&msg, "ItemRemoved|");
94 + if (flag & kFSEventStreamEventFlagItemRenamed)
95 + strbuf_addstr(&msg, "ItemRenamed|");
96 + if (flag & kFSEventStreamEventFlagItemXattrMod)
97 + strbuf_addstr(&msg, "ItemXattrMod|");
98 + if (flag & kFSEventStreamEventFlagOwnEvent)
99 + strbuf_addstr(&msg, "OwnEvent|");
100 + if (flag & kFSEventStreamEventFlagItemCloned)
101 + strbuf_addstr(&msg, "ItemCloned|");
102 +
103 + trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=%u %s",
104 + path, flag, msg.buf);
105 +
106 + strbuf_release(&msg);
107 +}
108 +
109 +static int ef_is_root_delete(const FSEventStreamEventFlags ef)
110 +{
111 + return (ef & kFSEventStreamEventFlagItemIsDir &&
112 + ef & kFSEventStreamEventFlagItemRemoved);
113 +}
114 +
115 +static int ef_is_root_renamed(const FSEventStreamEventFlags ef)
116 +{
117 + return (ef & kFSEventStreamEventFlagItemIsDir &&
118 + ef & kFSEventStreamEventFlagItemRenamed);
119 +}
120 +
121 +static int ef_is_dropped(const FSEventStreamEventFlags ef)
122 +{
123 + return (ef & kFSEventStreamEventFlagMustScanSubDirs ||
124 + ef & kFSEventStreamEventFlagKernelDropped ||
125 + ef & kFSEventStreamEventFlagUserDropped);
126 +}
127 +
128 +static void fsevent_callback(ConstFSEventStreamRef streamRef,
129 + void *ctx,
130 + size_t num_of_events,
131 + void *event_paths,
132 + const FSEventStreamEventFlags event_flags[],
133 + const FSEventStreamEventId event_ids[])
134 +{
135 + struct fsmonitor_daemon_state *state = ctx;
136 + struct fsmonitor_daemon_backend_data *data = state->backend_data;
137 + char **paths = (char **)event_paths;
138 + struct fsmonitor_batch *batch = NULL;
139 + struct string_list cookie_list = STRING_LIST_INIT_DUP;
140 + const char *path_k;
141 + const char *slash;
142 + int k;
143 + struct strbuf tmp = STRBUF_INIT;
144 +
145 + /*
146 + * Build a list of all filesystem changes into a private/local
147 + * list and without holding any locks.
148 + */
149 + for (k = 0; k < num_of_events; k++) {
150 + /*
151 + * On Mac, we receive an array of absolute paths.
152 + */
153 + path_k = paths[k];
154 +
155 + /*
156 + * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR.
157 + * Please don't log them to Trace2.
158 + *
159 + * trace_printf_key(&trace_fsmonitor, "Path: '%s'", path_k);
160 + */
161 +
162 + /*
163 + * If event[k] is marked as dropped, we assume that we have
164 + * lost sync with the filesystem and should flush our cached
165 + * data. We need to:
166 + *
167 + * [1] Abort/wake any client threads waiting for a cookie and
168 + * flush the cached state data (the current token), and
169 + * create a new token.
170 + *
171 + * [2] Discard the batch that we were locally building (since
172 + * they are conceptually relative to the just flushed
173 + * token).
174 + */
175 + if (ef_is_dropped(event_flags[k])) {
176 + if (trace_pass_fl(&trace_fsmonitor))
177 + log_flags_set(path_k, event_flags[k]);
178 +
179 + fsmonitor_force_resync(state);
180 + fsmonitor_batch__free_list(batch);
181 + string_list_clear(&cookie_list, 0);
182 +
183 + /*
184 + * We assume that any events that we received
185 + * in this callback after this dropped event
186 + * may still be valid, so we continue rather
187 + * than break. (And just in case there is a
188 + * delete of ".git" hiding in there.)
189 + */
190 + continue;
191 + }
192 +
193 + switch (fsmonitor_classify_path_absolute(state, path_k)) {
194 +
195 + case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
196 + case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
197 + /* special case cookie files within .git or gitdir */
198 +
199 + /* Use just the filename of the cookie file. */
200 + slash = find_last_dir_sep(path_k);
201 + string_list_append(&cookie_list,
202 + slash ? slash + 1 : path_k);
203 + break;
204 +
205 + case IS_INSIDE_DOT_GIT:
206 + case IS_INSIDE_GITDIR:
207 + /* ignore all other paths inside of .git or gitdir */
208 + break;
209 +
210 + case IS_DOT_GIT:
211 + case IS_GITDIR:
212 + /*
213 + * If .git directory is deleted or renamed away,
214 + * we have to quit.
215 + */
216 + if (ef_is_root_delete(event_flags[k])) {
217 + trace_printf_key(&trace_fsmonitor,
218 + "event: gitdir removed");
219 + goto force_shutdown;
220 + }
221 + if (ef_is_root_renamed(event_flags[k])) {
222 + trace_printf_key(&trace_fsmonitor,
223 + "event: gitdir renamed");
224 + goto force_shutdown;
225 + }
226 + break;
227 +
228 + case IS_WORKDIR_PATH:
229 + /* try to queue normal pathnames */
230 +
231 + if (trace_pass_fl(&trace_fsmonitor))
232 + log_flags_set(path_k, event_flags[k]);
233 +
234 + /*
235 + * Because of the implicit "binning" (the
236 + * kernel calls us at a given frequency) and
237 + * de-duping (the kernel is free to combine
238 + * multiple events for a given pathname), an
239 + * individual fsevent could be marked as both
240 + * a file and directory. Add it to the queue
241 + * with both spellings so that the client will
242 + * know how much to invalidate/refresh.
243 + */
244 +
245 + if (event_flags[k] & kFSEventStreamEventFlagItemIsFile) {
246 + const char *rel = path_k +
247 + state->path_worktree_watch.len + 1;
248 +
249 + if (!batch)
250 + batch = fsmonitor_batch__new();
251 + fsmonitor_batch__add_path(batch, rel);
252 + }
253 +
254 + if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) {
255 + const char *rel = path_k +
256 + state->path_worktree_watch.len + 1;
257 +
258 + strbuf_reset(&tmp);
259 + strbuf_addstr(&tmp, rel);
260 + strbuf_addch(&tmp, '/');
261 +
262 + if (!batch)
263 + batch = fsmonitor_batch__new();
264 + fsmonitor_batch__add_path(batch, tmp.buf);
265 + }
266 +
267 + break;
268 +
269 + case IS_OUTSIDE_CONE:
270 + default:
271 + trace_printf_key(&trace_fsmonitor,
272 + "ignoring '%s'", path_k);
273 + break;
274 + }
275 + }
276 +
277 + fsmonitor_publish(state, batch, &cookie_list);
278 + string_list_clear(&cookie_list, 0);
279 + strbuf_release(&tmp);
280 + return;
281 +
282 +force_shutdown:
283 + fsmonitor_batch__free_list(batch);
284 + string_list_clear(&cookie_list, 0);
285 +
286 + data->shutdown_style = FORCE_SHUTDOWN;
287 + CFRunLoopStop(data->rl);
288 + strbuf_release(&tmp);
289 + return;
290 +}
291 +
292 +/*
293 + * In the call to `FSEventStreamCreate()` to setup our watch, the
294 + * `latency` argument determines the frequency of calls to our callback
295 + * with new FS events. Too slow and events get dropped; too fast and
296 + * we burn CPU unnecessarily. Since it is rather obscure, I don't
297 + * think this needs to be a config setting. I've done extensive
298 + * testing on my systems and chosen the value below. It gives good
299 + * results and I've not seen any dropped events.
300 + *
301 + * With a latency of 0.1, I was seeing lots of dropped events during
302 + * the "touch 100000" files test within t/perf/p7519, but with a
303 + * latency of 0.001 I did not see any dropped events. So I'm going
304 + * to assume that this is the "correct" value.
305 + *
306 + * https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
307 + */
308
309 int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
310 {
311 + FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer |
312 + kFSEventStreamCreateFlagWatchRoot |
313 + kFSEventStreamCreateFlagFileEvents;
314 + FSEventStreamContext ctx = {
315 + 0,
316 + state,
317 + NULL,
318 + NULL,
319 + NULL
320 + };
321 + struct fsmonitor_daemon_backend_data *data;
322 + const void *dir_array[2];
323 +
324 + CALLOC_ARRAY(data, 1);
325 + state->backend_data = data;
326 +
327 + data->cfsr_worktree_path = CFStringCreateWithCString(
328 + NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8);
329 + dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path;
330 +
331 + if (state->nr_paths_watching > 1) {
332 + data->cfsr_gitdir_path = CFStringCreateWithCString(
333 + NULL, state->path_gitdir_watch.buf,
334 + kCFStringEncodingUTF8);
335 + dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path;
336 + }
337 +
338 + data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array,
339 + data->nr_paths_watching,
340 + NULL);
341 + data->stream = FSEventStreamCreate(NULL, fsevent_callback, &ctx,
342 + data->cfar_paths_to_watch,
343 + kFSEventStreamEventIdSinceNow,
344 + 0.001, flags);
345 + if (data->stream == NULL)
346 + goto failed;
347 +
348 + /*
349 + * `data->rl` needs to be set inside the listener thread.
350 + */
351 +
352 + return 0;
353 +
354 +failed:
355 + error(_("Unable to create FSEventStream."));
356 +
357 + FREE_AND_NULL(state->backend_data);
358 return -1;
359 }
360
361 void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
362 {
363 + struct fsmonitor_daemon_backend_data *data;
364 +
365 + if (!state || !state->backend_data)
366 + return;
367 +
368 + data = state->backend_data;
369 +
370 + if (data->stream) {
371 + if (data->stream_started)
372 + FSEventStreamStop(data->stream);
373 + if (data->stream_scheduled)
374 + FSEventStreamInvalidate(data->stream);
375 + FSEventStreamRelease(data->stream);
376 + }
377 +
378 + FREE_AND_NULL(state->backend_data);
379 }
380
381 void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
382 {
383 + struct fsmonitor_daemon_backend_data *data;
384 +
385 + data = state->backend_data;
386 + data->shutdown_style = SHUTDOWN_EVENT;
387 +
388 + CFRunLoopStop(data->rl);
389 }
390
391 void fsm_listen__loop(struct fsmonitor_daemon_state *state)
392 {
393 + struct fsmonitor_daemon_backend_data *data;
394 +
395 + data = state->backend_data;
396 +
397 + data->rl = CFRunLoopGetCurrent();
398 +
399 + FSEventStreamScheduleWithRunLoop(data->stream, data->rl, kCFRunLoopDefaultMode);
400 + data->stream_scheduled = 1;
401 +
402 + if (!FSEventStreamStart(data->stream)) {
403 + error(_("Failed to start the FSEventStream"));
404 + goto force_error_stop_without_loop;
405 + }
406 + data->stream_started = 1;
407 +
408 + CFRunLoopRun();
409 +
410 + switch (data->shutdown_style) {
411 + case FORCE_ERROR_STOP:
412 + state->error_code = -1;
413 + /* fall thru */
414 + case FORCE_SHUTDOWN:
415 + ipc_server_stop_async(state->ipc_server_data);
416 + /* fall thru */
417 + case SHUTDOWN_EVENT:
418 + default:
419 + break;
420 + }
421 + return;
422 +
423 +force_error_stop_without_loop:
424 + state->error_code = -1;
425 + ipc_server_stop_async(state->ipc_server_data);
426 + return;
427 }