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
}