#ifndef __clang__ #include #include "fsm-darwin-gcc.h" #else #include #include #ifndef AVAILABLE_MAC_OS_X_VERSION_10_13_AND_LATER /* * This enum value was added in 10.13 to: * * /Applications/Xcode.app/Contents/Developer/Platforms/ \ * MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/ \ * Library/Frameworks/CoreServices.framework/Frameworks/ \ * FSEvents.framework/Versions/Current/Headers/FSEvents.h * * If we're compiling against an older SDK, this symbol won't be * present. Silently define it here so that we don't have to ifdef * the logging or masking below. This should be harmless since older * versions of macOS won't ever emit this FS event anyway. */ #define kFSEventStreamEventFlagItemCloned 0x00400000 #endif #endif #include "git-compat-util.h" #include "fsmonitor.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" #include "fsmonitor-path-utils.h" #include "gettext.h" #include "string-list.h" struct fsm_listen_data { CFStringRef cfsr_worktree_path; CFStringRef cfsr_gitdir_path; CFArrayRef cfar_paths_to_watch; int nr_paths_watching; FSEventStreamRef stream; dispatch_queue_t dq; pthread_cond_t dq_finished; pthread_mutex_t dq_lock; enum shutdown_style { SHUTDOWN_EVENT = 0, FORCE_SHUTDOWN, FORCE_ERROR_STOP, } shutdown_style; unsigned int stream_scheduled:1; unsigned int stream_started:1; }; static void log_flags_set(const char *path, const FSEventStreamEventFlags flag) { struct strbuf msg = STRBUF_INIT; if (flag & kFSEventStreamEventFlagMustScanSubDirs) strbuf_addstr(&msg, "MustScanSubDirs|"); if (flag & kFSEventStreamEventFlagUserDropped) strbuf_addstr(&msg, "UserDropped|"); if (flag & kFSEventStreamEventFlagKernelDropped) strbuf_addstr(&msg, "KernelDropped|"); if (flag & kFSEventStreamEventFlagEventIdsWrapped) strbuf_addstr(&msg, "EventIdsWrapped|"); if (flag & kFSEventStreamEventFlagHistoryDone) strbuf_addstr(&msg, "HistoryDone|"); if (flag & kFSEventStreamEventFlagRootChanged) strbuf_addstr(&msg, "RootChanged|"); if (flag & kFSEventStreamEventFlagMount) strbuf_addstr(&msg, "Mount|"); if (flag & kFSEventStreamEventFlagUnmount) strbuf_addstr(&msg, "Unmount|"); if (flag & kFSEventStreamEventFlagItemChangeOwner) strbuf_addstr(&msg, "ItemChangeOwner|"); if (flag & kFSEventStreamEventFlagItemCreated) strbuf_addstr(&msg, "ItemCreated|"); if (flag & kFSEventStreamEventFlagItemFinderInfoMod) strbuf_addstr(&msg, "ItemFinderInfoMod|"); if (flag & kFSEventStreamEventFlagItemInodeMetaMod) strbuf_addstr(&msg, "ItemInodeMetaMod|"); if (flag & kFSEventStreamEventFlagItemIsDir) strbuf_addstr(&msg, "ItemIsDir|"); if (flag & kFSEventStreamEventFlagItemIsFile) strbuf_addstr(&msg, "ItemIsFile|"); if (flag & kFSEventStreamEventFlagItemIsHardlink) strbuf_addstr(&msg, "ItemIsHardlink|"); if (flag & kFSEventStreamEventFlagItemIsLastHardlink) strbuf_addstr(&msg, "ItemIsLastHardlink|"); if (flag & kFSEventStreamEventFlagItemIsSymlink) strbuf_addstr(&msg, "ItemIsSymlink|"); if (flag & kFSEventStreamEventFlagItemModified) strbuf_addstr(&msg, "ItemModified|"); if (flag & kFSEventStreamEventFlagItemRemoved) strbuf_addstr(&msg, "ItemRemoved|"); if (flag & kFSEventStreamEventFlagItemRenamed) strbuf_addstr(&msg, "ItemRenamed|"); if (flag & kFSEventStreamEventFlagItemXattrMod) strbuf_addstr(&msg, "ItemXattrMod|"); if (flag & kFSEventStreamEventFlagOwnEvent) strbuf_addstr(&msg, "OwnEvent|"); if (flag & kFSEventStreamEventFlagItemCloned) strbuf_addstr(&msg, "ItemCloned|"); trace_printf_key(&trace_fsmonitor, "fsevent: '%s', flags=0x%x %s", path, flag, msg.buf); strbuf_release(&msg); } static int ef_is_root_changed(const FSEventStreamEventFlags ef) { return (ef & kFSEventStreamEventFlagRootChanged); } static int ef_is_root_delete(const FSEventStreamEventFlags ef) { return (ef & kFSEventStreamEventFlagItemIsDir && ef & kFSEventStreamEventFlagItemRemoved); } static int ef_is_root_renamed(const FSEventStreamEventFlags ef) { return (ef & kFSEventStreamEventFlagItemIsDir && ef & kFSEventStreamEventFlagItemRenamed); } static int ef_is_dropped(const FSEventStreamEventFlags ef) { return (ef & kFSEventStreamEventFlagMustScanSubDirs || ef & kFSEventStreamEventFlagKernelDropped || ef & kFSEventStreamEventFlagUserDropped); } /* * If an `xattr` change is the only reason we received this event, * then silently ignore it. Git doesn't care about xattr's. We * have to be careful here because the kernel can combine multiple * events for a single path. And because events always have certain * bits set, such as `ItemIsFile` or `ItemIsDir`. * * Return 1 if we should ignore it. */ static int ef_ignore_xattr(const FSEventStreamEventFlags ef) { static const FSEventStreamEventFlags mask = kFSEventStreamEventFlagItemChangeOwner | kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemFinderInfoMod | kFSEventStreamEventFlagItemInodeMetaMod | kFSEventStreamEventFlagItemModified | kFSEventStreamEventFlagItemRemoved | kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemXattrMod | kFSEventStreamEventFlagItemCloned; return ((ef & mask) == kFSEventStreamEventFlagItemXattrMod); } /* * On MacOS we have to adjust for Unicode composition insensitivity * (where NFC and NFD spellings are not respected). The different * spellings are essentially aliases regardless of how the path is * actually stored on the disk. * * This is related to "core.precomposeUnicode" (which wants to try * to hide NFD completely and treat everything as NFC). Here, we * don't know what the value the client has (or will have) for this * config setting when they make a query, so assume the worst and * emit both when the OS gives us an NFD path. */ static void my_add_path(struct fsmonitor_batch *batch, const char *path) { char *composed; /* add the NFC or NFD path as received from the OS */ fsmonitor_batch__add_path(batch, path); /* if NFD, also add the corresponding NFC spelling */ composed = (char *)precompose_string_if_needed(path); if (!composed || composed == path) return; fsmonitor_batch__add_path(batch, composed); free(composed); } static void fsevent_callback(ConstFSEventStreamRef streamRef, void *ctx, size_t num_of_events, void *event_paths, const FSEventStreamEventFlags event_flags[], const FSEventStreamEventId event_ids[]) { struct fsmonitor_daemon_state *state = ctx; struct fsm_listen_data *data = state->listen_data; char **paths = (char **)event_paths; struct fsmonitor_batch *batch = NULL; struct string_list cookie_list = STRING_LIST_INIT_DUP; const char *path_k; const char *slash; char *resolved = NULL; struct strbuf tmp = STRBUF_INIT; int k; /* * Build a list of all filesystem changes into a private/local * list and without holding any locks. */ for (k = 0; k < num_of_events; k++) { /* * On Mac, we receive an array of absolute paths. */ free(resolved); resolved = fsmonitor__resolve_alias(paths[k], &state->alias); if (resolved) path_k = resolved; else path_k = paths[k]; /* * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR. * Please don't log them to Trace2. * * trace_printf_key(&trace_fsmonitor, "Path: '%s'", path_k); */ /* * If event[k] is marked as dropped, we assume that we have * lost sync with the filesystem and should flush our cached * data. We need to: * * [1] Abort/wake any client threads waiting for a cookie and * flush the cached state data (the current token), and * create a new token. * * [2] Discard the batch that we were locally building (since * they are conceptually relative to the just flushed * token). */ if (ef_is_dropped(event_flags[k])) { if (trace_pass_fl(&trace_fsmonitor)) log_flags_set(path_k, event_flags[k]); fsmonitor_force_resync(state); fsmonitor_batch__free_list(batch); string_list_clear(&cookie_list, 0); batch = NULL; /* * We assume that any events that we received * in this callback after this dropped event * may still be valid, so we continue rather * than break. (And just in case there is a * delete of ".git" hiding in there.) */ continue; } if (ef_is_root_changed(event_flags[k])) { /* * The spelling of the pathname of the root directory * has changed. This includes the name of the root * directory itself or of any parent directory in the * path. * * (There may be other conditions that throw this, * but I couldn't find any information on it.) * * Force a shutdown now and avoid things getting * out of sync. The Unix domain socket is inside * the .git directory and a spelling change will make * it hard for clients to rendezvous with us. */ trace_printf_key(&trace_fsmonitor, "event: root changed"); goto force_shutdown; } if (ef_ignore_xattr(event_flags[k])) { trace_printf_key(&trace_fsmonitor, "ignore-xattr: '%s', flags=0x%x", path_k, event_flags[k]); continue; } switch (fsmonitor_classify_path_absolute(state, path_k)) { case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: /* special case cookie files within .git or gitdir */ /* Use just the filename of the cookie file. */ slash = find_last_dir_sep(path_k); string_list_append(&cookie_list, slash ? slash + 1 : path_k); break; case IS_INSIDE_DOT_GIT: case IS_INSIDE_GITDIR: /* ignore all other paths inside of .git or gitdir */ break; case IS_DOT_GIT: case IS_GITDIR: /* * If .git directory is deleted or renamed away, * we have to quit. */ if (ef_is_root_delete(event_flags[k])) { trace_printf_key(&trace_fsmonitor, "event: gitdir removed"); goto force_shutdown; } if (ef_is_root_renamed(event_flags[k])) { trace_printf_key(&trace_fsmonitor, "event: gitdir renamed"); goto force_shutdown; } break; case IS_WORKDIR_PATH: /* try to queue normal pathnames */ if (trace_pass_fl(&trace_fsmonitor)) log_flags_set(path_k, event_flags[k]); /* * Because of the implicit "binning" (the * kernel calls us at a given frequency) and * de-duping (the kernel is free to combine * multiple events for a given pathname), an * individual fsevent could be marked as both * a file and directory. Add it to the queue * with both spellings so that the client will * know how much to invalidate/refresh. */ if (event_flags[k] & (kFSEventStreamEventFlagItemIsFile | kFSEventStreamEventFlagItemIsSymlink)) { const char *rel = path_k + state->path_worktree_watch.len + 1; if (!batch) batch = fsmonitor_batch__new(); my_add_path(batch, rel); } if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) { const char *rel = path_k + state->path_worktree_watch.len + 1; strbuf_reset(&tmp); strbuf_addstr(&tmp, rel); strbuf_addch(&tmp, '/'); if (!batch) batch = fsmonitor_batch__new(); my_add_path(batch, tmp.buf); } break; case IS_OUTSIDE_CONE: default: trace_printf_key(&trace_fsmonitor, "ignoring '%s'", path_k); break; } } free(resolved); fsmonitor_publish(state, batch, &cookie_list); string_list_clear(&cookie_list, 0); strbuf_release(&tmp); return; force_shutdown: free(resolved); fsmonitor_batch__free_list(batch); string_list_clear(&cookie_list, 0); pthread_mutex_lock(&data->dq_lock); data->shutdown_style = FORCE_SHUTDOWN; pthread_cond_broadcast(&data->dq_finished); pthread_mutex_unlock(&data->dq_lock); strbuf_release(&tmp); return; } /* * In the call to `FSEventStreamCreate()` to setup our watch, the * `latency` argument determines the frequency of calls to our callback * with new FS events. Too slow and events get dropped; too fast and * we burn CPU unnecessarily. Since it is rather obscure, I don't * think this needs to be a config setting. I've done extensive * testing on my systems and chosen the value below. It gives good * results and I've not seen any dropped events. * * With a latency of 0.1, I was seeing lots of dropped events during * the "touch 100000" files test within t/perf/p7519, but with a * latency of 0.001 I did not see any dropped events. So I'm going * to assume that this is the "correct" value. * * https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate */ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) { FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagWatchRoot | kFSEventStreamCreateFlagFileEvents; FSEventStreamContext ctx = { 0, state, NULL, NULL, NULL }; struct fsm_listen_data *data; const void *dir_array[2]; CALLOC_ARRAY(data, 1); state->listen_data = data; data->cfsr_worktree_path = CFStringCreateWithCString( NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8); dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path; if (state->nr_paths_watching > 1) { data->cfsr_gitdir_path = CFStringCreateWithCString( NULL, state->path_gitdir_watch.buf, kCFStringEncodingUTF8); dir_array[data->nr_paths_watching++] = data->cfsr_gitdir_path; } data->cfar_paths_to_watch = CFArrayCreate(NULL, dir_array, data->nr_paths_watching, NULL); data->stream = FSEventStreamCreate(NULL, fsevent_callback, &ctx, data->cfar_paths_to_watch, kFSEventStreamEventIdSinceNow, 0.001, flags); if (!data->stream) goto failed; return 0; failed: error(_("Unable to create FSEventStream.")); FREE_AND_NULL(state->listen_data); return -1; } void fsm_listen__dtor(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; if (!state || !state->listen_data) return; data = state->listen_data; if (data->stream) { if (data->stream_started) FSEventStreamStop(data->stream); if (data->stream_scheduled) FSEventStreamInvalidate(data->stream); FSEventStreamRelease(data->stream); } if (data->dq) dispatch_release(data->dq); pthread_cond_destroy(&data->dq_finished); pthread_mutex_destroy(&data->dq_lock); FREE_AND_NULL(state->listen_data); } void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; data = state->listen_data; pthread_mutex_lock(&data->dq_lock); data->shutdown_style = SHUTDOWN_EVENT; pthread_cond_broadcast(&data->dq_finished); pthread_mutex_unlock(&data->dq_lock); } void fsm_listen__loop(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; data = state->listen_data; pthread_mutex_init(&data->dq_lock, NULL); pthread_cond_init(&data->dq_finished, NULL); data->dq = dispatch_queue_create("FSMonitor", NULL); FSEventStreamSetDispatchQueue(data->stream, data->dq); data->stream_scheduled = 1; if (!FSEventStreamStart(data->stream)) { error(_("Failed to start the FSEventStream")); goto force_error_stop_without_loop; } data->stream_started = 1; pthread_mutex_lock(&data->dq_lock); pthread_cond_wait(&data->dq_finished, &data->dq_lock); pthread_mutex_unlock(&data->dq_lock); switch (data->shutdown_style) { case FORCE_ERROR_STOP: state->listen_error_code = -1; /* fall thru */ case FORCE_SHUTDOWN: ipc_server_stop_async(state->ipc_server_data); /* fall thru */ case SHUTDOWN_EVENT: default: break; } return; force_error_stop_without_loop: state->listen_error_code = -1; ipc_server_stop_async(state->ipc_server_data); return; }