]>
Commit | Line | Data |
---|---|---|
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 | ||
207534e4 | 30 | struct fsm_listen_data |
65723b30 JH |
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 | ||
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 | ||
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 | ||
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 | */ | |
137 | static 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 |
153 | static 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; | |
207534e4 | 161 | struct fsm_listen_data *data = state->listen_data; |
65723b30 JH |
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 | ||
314 | force_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 | |
341 | int 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 | }; | |
207534e4 | 353 | struct fsm_listen_data *data; |
65723b30 JH |
354 | const void *dir_array[2]; |
355 | ||
356 | CALLOC_ARRAY(data, 1); | |
207534e4 | 357 | state->listen_data = data; |
65723b30 JH |
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 | ||
386 | failed: | |
387 | error(_("Unable to create FSEventStream.")); | |
388 | ||
207534e4 | 389 | FREE_AND_NULL(state->listen_data); |
f67df255 JH |
390 | return -1; |
391 | } | |
392 | ||
393 | void fsm_listen__dtor(struct fsmonitor_daemon_state *state) | |
394 | { | |
207534e4 | 395 | struct fsm_listen_data *data; |
65723b30 | 396 | |
207534e4 | 397 | if (!state || !state->listen_data) |
65723b30 JH |
398 | return; |
399 | ||
207534e4 | 400 | data = state->listen_data; |
65723b30 JH |
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 | ||
207534e4 | 410 | FREE_AND_NULL(state->listen_data); |
f67df255 JH |
411 | } |
412 | ||
413 | void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) | |
414 | { | |
207534e4 | 415 | struct fsm_listen_data *data; |
65723b30 | 416 | |
207534e4 | 417 | data = state->listen_data; |
65723b30 JH |
418 | data->shutdown_style = SHUTDOWN_EVENT; |
419 | ||
420 | CFRunLoopStop(data->rl); | |
f67df255 JH |
421 | } |
422 | ||
423 | void fsm_listen__loop(struct fsmonitor_daemon_state *state) | |
424 | { | |
207534e4 | 425 | struct fsm_listen_data *data; |
65723b30 | 426 | |
207534e4 | 427 | data = state->listen_data; |
65723b30 JH |
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: | |
207534e4 | 444 | state->listen_error_code = -1; |
65723b30 JH |
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 | ||
455 | force_error_stop_without_loop: | |
207534e4 | 456 | state->listen_error_code = -1; |
65723b30 JH |
457 | ipc_server_stop_async(state->ipc_server_data); |
458 | return; | |
f67df255 | 459 | } |