]> git.ipfire.org Git - thirdparty/git.git/blame - compat/fsmonitor/fsm-listen-darwin.c
fsmonitor--daemon: prepare for adding health thread
[thirdparty/git.git] / compat / fsmonitor / fsm-listen-darwin.c
CommitLineData
5ff01b1f
JH
1#ifndef __clang__
2#include "fsm-darwin-gcc.h"
3#else
4#include <CoreFoundation/CoreFoundation.h>
5#include <CoreServices/CoreServices.h>
6
7#ifndef AVAILABLE_MAC_OS_X_VERSION_10_13_AND_LATER
8/*
9 * This enum value was added in 10.13 to:
10 *
11 * /Applications/Xcode.app/Contents/Developer/Platforms/ \
12 * MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/ \
13 * Library/Frameworks/CoreServices.framework/Frameworks/ \
14 * FSEvents.framework/Versions/Current/Headers/FSEvents.h
15 *
16 * If we're compiling against an older SDK, this symbol won't be
17 * present. Silently define it here so that we don't have to ifdef
18 * the logging or masking below. This should be harmless since older
19 * versions of macOS won't ever emit this FS event anyway.
20 */
21#define kFSEventStreamEventFlagItemCloned 0x00400000
22#endif
23#endif
24
f67df255
JH
25#include "cache.h"
26#include "fsmonitor.h"
27#include "fsm-listen.h"
65723b30
JH
28#include "fsmonitor--daemon.h"
29
30struct 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
52static 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
8e8f4b81 103 trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=0x%x %s",
65723b30
JH
104 path, flag, msg.buf);
105
106 strbuf_release(&msg);
107}
108
109static int ef_is_root_delete(const FSEventStreamEventFlags ef)
110{
111 return (ef & kFSEventStreamEventFlagItemIsDir &&
112 ef & kFSEventStreamEventFlagItemRemoved);
113}
114
115static int ef_is_root_renamed(const FSEventStreamEventFlags ef)
116{
117 return (ef & kFSEventStreamEventFlagItemIsDir &&
118 ef & kFSEventStreamEventFlagItemRenamed);
119}
120
121static int ef_is_dropped(const FSEventStreamEventFlags ef)
122{
123 return (ef & kFSEventStreamEventFlagMustScanSubDirs ||
124 ef & kFSEventStreamEventFlagKernelDropped ||
125 ef & kFSEventStreamEventFlagUserDropped);
126}
127
8e8f4b81
JH
128/*
129 * If an `xattr` change is the only reason we received this event,
130 * then silently ignore it. Git doesn't care about xattr's. We
131 * have to be careful here because the kernel can combine multiple
132 * events for a single path. And because events always have certain
133 * bits set, such as `ItemIsFile` or `ItemIsDir`.
134 *
135 * Return 1 if we should ignore it.
136 */
137static int ef_ignore_xattr(const FSEventStreamEventFlags ef)
138{
139 static const FSEventStreamEventFlags mask =
140 kFSEventStreamEventFlagItemChangeOwner |
141 kFSEventStreamEventFlagItemCreated |
142 kFSEventStreamEventFlagItemFinderInfoMod |
143 kFSEventStreamEventFlagItemInodeMetaMod |
144 kFSEventStreamEventFlagItemModified |
145 kFSEventStreamEventFlagItemRemoved |
146 kFSEventStreamEventFlagItemRenamed |
147 kFSEventStreamEventFlagItemXattrMod |
148 kFSEventStreamEventFlagItemCloned;
149
150 return ((ef & mask) == kFSEventStreamEventFlagItemXattrMod);
151}
152
65723b30
JH
153static void fsevent_callback(ConstFSEventStreamRef streamRef,
154 void *ctx,
155 size_t num_of_events,
156 void *event_paths,
157 const FSEventStreamEventFlags event_flags[],
158 const FSEventStreamEventId event_ids[])
159{
160 struct fsmonitor_daemon_state *state = ctx;
161 struct fsmonitor_daemon_backend_data *data = state->backend_data;
162 char **paths = (char **)event_paths;
163 struct fsmonitor_batch *batch = NULL;
164 struct string_list cookie_list = STRING_LIST_INIT_DUP;
165 const char *path_k;
166 const char *slash;
167 int k;
168 struct strbuf tmp = STRBUF_INIT;
169
170 /*
171 * Build a list of all filesystem changes into a private/local
172 * list and without holding any locks.
173 */
174 for (k = 0; k < num_of_events; k++) {
175 /*
176 * On Mac, we receive an array of absolute paths.
177 */
178 path_k = paths[k];
179
180 /*
181 * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR.
182 * Please don't log them to Trace2.
183 *
184 * trace_printf_key(&trace_fsmonitor, "Path: '%s'", path_k);
185 */
186
187 /*
188 * If event[k] is marked as dropped, we assume that we have
189 * lost sync with the filesystem and should flush our cached
190 * data. We need to:
191 *
192 * [1] Abort/wake any client threads waiting for a cookie and
193 * flush the cached state data (the current token), and
194 * create a new token.
195 *
196 * [2] Discard the batch that we were locally building (since
197 * they are conceptually relative to the just flushed
198 * token).
199 */
200 if (ef_is_dropped(event_flags[k])) {
201 if (trace_pass_fl(&trace_fsmonitor))
202 log_flags_set(path_k, event_flags[k]);
203
204 fsmonitor_force_resync(state);
205 fsmonitor_batch__free_list(batch);
206 string_list_clear(&cookie_list, 0);
207
208 /*
209 * We assume that any events that we received
210 * in this callback after this dropped event
211 * may still be valid, so we continue rather
212 * than break. (And just in case there is a
213 * delete of ".git" hiding in there.)
214 */
215 continue;
216 }
217
8e8f4b81
JH
218 if (ef_ignore_xattr(event_flags[k])) {
219 trace_printf_key(&trace_fsmonitor,
220 "ignore-xattr: '%s', flags=0x%x",
221 path_k, event_flags[k]);
222 continue;
223 }
224
65723b30
JH
225 switch (fsmonitor_classify_path_absolute(state, path_k)) {
226
227 case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
228 case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
229 /* special case cookie files within .git or gitdir */
230
231 /* Use just the filename of the cookie file. */
232 slash = find_last_dir_sep(path_k);
233 string_list_append(&cookie_list,
234 slash ? slash + 1 : path_k);
235 break;
236
237 case IS_INSIDE_DOT_GIT:
238 case IS_INSIDE_GITDIR:
239 /* ignore all other paths inside of .git or gitdir */
240 break;
241
242 case IS_DOT_GIT:
243 case IS_GITDIR:
244 /*
245 * If .git directory is deleted or renamed away,
246 * we have to quit.
247 */
248 if (ef_is_root_delete(event_flags[k])) {
249 trace_printf_key(&trace_fsmonitor,
250 "event: gitdir removed");
251 goto force_shutdown;
252 }
253 if (ef_is_root_renamed(event_flags[k])) {
254 trace_printf_key(&trace_fsmonitor,
255 "event: gitdir renamed");
256 goto force_shutdown;
257 }
258 break;
259
260 case IS_WORKDIR_PATH:
261 /* try to queue normal pathnames */
262
263 if (trace_pass_fl(&trace_fsmonitor))
264 log_flags_set(path_k, event_flags[k]);
265
266 /*
267 * Because of the implicit "binning" (the
268 * kernel calls us at a given frequency) and
269 * de-duping (the kernel is free to combine
270 * multiple events for a given pathname), an
271 * individual fsevent could be marked as both
272 * a file and directory. Add it to the queue
273 * with both spellings so that the client will
274 * know how much to invalidate/refresh.
275 */
276
277 if (event_flags[k] & kFSEventStreamEventFlagItemIsFile) {
278 const char *rel = path_k +
279 state->path_worktree_watch.len + 1;
280
281 if (!batch)
282 batch = fsmonitor_batch__new();
283 fsmonitor_batch__add_path(batch, rel);
284 }
285
286 if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) {
287 const char *rel = path_k +
288 state->path_worktree_watch.len + 1;
289
290 strbuf_reset(&tmp);
291 strbuf_addstr(&tmp, rel);
292 strbuf_addch(&tmp, '/');
293
294 if (!batch)
295 batch = fsmonitor_batch__new();
296 fsmonitor_batch__add_path(batch, tmp.buf);
297 }
298
299 break;
300
301 case IS_OUTSIDE_CONE:
302 default:
303 trace_printf_key(&trace_fsmonitor,
304 "ignoring '%s'", path_k);
305 break;
306 }
307 }
308
309 fsmonitor_publish(state, batch, &cookie_list);
310 string_list_clear(&cookie_list, 0);
311 strbuf_release(&tmp);
312 return;
313
314force_shutdown:
315 fsmonitor_batch__free_list(batch);
316 string_list_clear(&cookie_list, 0);
317
318 data->shutdown_style = FORCE_SHUTDOWN;
319 CFRunLoopStop(data->rl);
320 strbuf_release(&tmp);
321 return;
322}
323
324/*
325 * In the call to `FSEventStreamCreate()` to setup our watch, the
326 * `latency` argument determines the frequency of calls to our callback
327 * with new FS events. Too slow and events get dropped; too fast and
328 * we burn CPU unnecessarily. Since it is rather obscure, I don't
329 * think this needs to be a config setting. I've done extensive
330 * testing on my systems and chosen the value below. It gives good
331 * results and I've not seen any dropped events.
332 *
333 * With a latency of 0.1, I was seeing lots of dropped events during
334 * the "touch 100000" files test within t/perf/p7519, but with a
335 * latency of 0.001 I did not see any dropped events. So I'm going
336 * to assume that this is the "correct" value.
337 *
338 * https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
339 */
f67df255
JH
340
341int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
342{
65723b30
JH
343 FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer |
344 kFSEventStreamCreateFlagWatchRoot |
345 kFSEventStreamCreateFlagFileEvents;
346 FSEventStreamContext ctx = {
347 0,
348 state,
349 NULL,
350 NULL,
351 NULL
352 };
353 struct fsmonitor_daemon_backend_data *data;
354 const void *dir_array[2];
355
356 CALLOC_ARRAY(data, 1);
357 state->backend_data = data;
358
359 data->cfsr_worktree_path = CFStringCreateWithCString(
360 NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8);
361 dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path;
362
363 if (state->nr_paths_watching > 1) {
364 data->cfsr_gitdir_path = CFStringCreateWithCString(
365 NULL, state->path_gitdir_watch.buf,
366 kCFStringEncodingUTF8);
367 dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path;
368 }
369
370 data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array,
371 data->nr_paths_watching,
372 NULL);
373 data->stream = FSEventStreamCreate(NULL, fsevent_callback, &ctx,
374 data->cfar_paths_to_watch,
375 kFSEventStreamEventIdSinceNow,
376 0.001, flags);
377 if (data->stream == NULL)
378 goto failed;
379
380 /*
381 * `data->rl` needs to be set inside the listener thread.
382 */
383
384 return 0;
385
386failed:
387 error(_("Unable to create FSEventStream."));
388
389 FREE_AND_NULL(state->backend_data);
f67df255
JH
390 return -1;
391}
392
393void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
394{
65723b30
JH
395 struct fsmonitor_daemon_backend_data *data;
396
397 if (!state || !state->backend_data)
398 return;
399
400 data = state->backend_data;
401
402 if (data->stream) {
403 if (data->stream_started)
404 FSEventStreamStop(data->stream);
405 if (data->stream_scheduled)
406 FSEventStreamInvalidate(data->stream);
407 FSEventStreamRelease(data->stream);
408 }
409
410 FREE_AND_NULL(state->backend_data);
f67df255
JH
411}
412
413void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
414{
65723b30
JH
415 struct fsmonitor_daemon_backend_data *data;
416
417 data = state->backend_data;
418 data->shutdown_style = SHUTDOWN_EVENT;
419
420 CFRunLoopStop(data->rl);
f67df255
JH
421}
422
423void fsm_listen__loop(struct fsmonitor_daemon_state *state)
424{
65723b30
JH
425 struct fsmonitor_daemon_backend_data *data;
426
427 data = state->backend_data;
428
429 data->rl = CFRunLoopGetCurrent();
430
431 FSEventStreamScheduleWithRunLoop(data->stream, data->rl, kCFRunLoopDefaultMode);
432 data->stream_scheduled = 1;
433
434 if (!FSEventStreamStart(data->stream)) {
435 error(_("Failed to start the FSEventStream"));
436 goto force_error_stop_without_loop;
437 }
438 data->stream_started = 1;
439
440 CFRunLoopRun();
441
442 switch (data->shutdown_style) {
443 case FORCE_ERROR_STOP:
444 state->error_code = -1;
445 /* fall thru */
446 case FORCE_SHUTDOWN:
447 ipc_server_stop_async(state->ipc_server_data);
448 /* fall thru */
449 case SHUTDOWN_EVENT:
450 default:
451 break;
452 }
453 return;
454
455force_error_stop_without_loop:
456 state->error_code = -1;
457 ipc_server_stop_async(state->ipc_server_data);
458 return;
f67df255 459}