summaryrefslogtreecommitdiff
path: root/compat
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2022-04-04 17:56:24 (GMT)
committerJunio C Hamano <gitster@pobox.com>2022-04-04 17:56:24 (GMT)
commit439c1e6d5d8ad4d1134fc6ff5e514d28ff9ecac4 (patch)
treeb1a7ebce54d1160b78ba93afcca3ccda26d53ce9 /compat
parentba2452b247dfd455bccbcb04acdcfd142cb5397e (diff)
parenta3dfe97f418762ccf1d0601c5cce40c77046d4fc (diff)
downloadgit-439c1e6d5d8ad4d1134fc6ff5e514d28ff9ecac4.zip
git-439c1e6d5d8ad4d1134fc6ff5e514d28ff9ecac4.tar.gz
git-439c1e6d5d8ad4d1134fc6ff5e514d28ff9ecac4.tar.bz2
Merge branch 'jh/builtin-fsmonitor-part2'
Built-in fsmonitor (part 2). * jh/builtin-fsmonitor-part2: (30 commits) t7527: test status with untracked-cache and fsmonitor--daemon fsmonitor: force update index after large responses fsmonitor--daemon: use a cookie file to sync with file system fsmonitor--daemon: periodically truncate list of modified files t/perf/p7519: add fsmonitor--daemon test cases t/perf/p7519: speed up test on Windows t/perf/p7519: fix coding style t/helper/test-chmtime: skip directories on Windows t/perf: avoid copying builtin fsmonitor files into test repo t7527: create test for fsmonitor--daemon t/helper/fsmonitor-client: create IPC client to talk to FSMonitor Daemon help: include fsmonitor--daemon feature flag in version info fsmonitor--daemon: implement handle_client callback compat/fsmonitor/fsm-listen-darwin: implement FSEvent listener on MacOS compat/fsmonitor/fsm-listen-darwin: add MacOS header files for FSEvent compat/fsmonitor/fsm-listen-win32: implement FSMonitor backend on Windows fsmonitor--daemon: create token-based changed path cache fsmonitor--daemon: define token-ids fsmonitor--daemon: add pathname classification fsmonitor--daemon: implement 'start' command ...
Diffstat (limited to 'compat')
-rw-r--r--compat/fsmonitor/fsm-darwin-gcc.h92
-rw-r--r--compat/fsmonitor/fsm-listen-darwin.c427
-rw-r--r--compat/fsmonitor/fsm-listen-win32.c586
-rw-r--r--compat/fsmonitor/fsm-listen.h49
4 files changed, 1154 insertions, 0 deletions
diff --git a/compat/fsmonitor/fsm-darwin-gcc.h b/compat/fsmonitor/fsm-darwin-gcc.h
new file mode 100644
index 0000000..1c75c3d
--- /dev/null
+++ b/compat/fsmonitor/fsm-darwin-gcc.h
@@ -0,0 +1,92 @@
+#ifndef FSM_DARWIN_GCC_H
+#define FSM_DARWIN_GCC_H
+
+#ifndef __clang__
+/*
+ * It is possible to #include CoreFoundation/CoreFoundation.h when compiling
+ * with clang, but not with GCC as of time of writing.
+ *
+ * See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93082 for details.
+ */
+typedef unsigned int FSEventStreamCreateFlags;
+#define kFSEventStreamEventFlagNone 0x00000000
+#define kFSEventStreamEventFlagMustScanSubDirs 0x00000001
+#define kFSEventStreamEventFlagUserDropped 0x00000002
+#define kFSEventStreamEventFlagKernelDropped 0x00000004
+#define kFSEventStreamEventFlagEventIdsWrapped 0x00000008
+#define kFSEventStreamEventFlagHistoryDone 0x00000010
+#define kFSEventStreamEventFlagRootChanged 0x00000020
+#define kFSEventStreamEventFlagMount 0x00000040
+#define kFSEventStreamEventFlagUnmount 0x00000080
+#define kFSEventStreamEventFlagItemCreated 0x00000100
+#define kFSEventStreamEventFlagItemRemoved 0x00000200
+#define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400
+#define kFSEventStreamEventFlagItemRenamed 0x00000800
+#define kFSEventStreamEventFlagItemModified 0x00001000
+#define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000
+#define kFSEventStreamEventFlagItemChangeOwner 0x00004000
+#define kFSEventStreamEventFlagItemXattrMod 0x00008000
+#define kFSEventStreamEventFlagItemIsFile 0x00010000
+#define kFSEventStreamEventFlagItemIsDir 0x00020000
+#define kFSEventStreamEventFlagItemIsSymlink 0x00040000
+#define kFSEventStreamEventFlagOwnEvent 0x00080000
+#define kFSEventStreamEventFlagItemIsHardlink 0x00100000
+#define kFSEventStreamEventFlagItemIsLastHardlink 0x00200000
+#define kFSEventStreamEventFlagItemCloned 0x00400000
+
+typedef struct __FSEventStream *FSEventStreamRef;
+typedef const FSEventStreamRef ConstFSEventStreamRef;
+
+typedef unsigned int CFStringEncoding;
+#define kCFStringEncodingUTF8 0x08000100
+
+typedef const struct __CFString *CFStringRef;
+typedef const struct __CFArray *CFArrayRef;
+typedef const struct __CFRunLoop *CFRunLoopRef;
+
+struct FSEventStreamContext {
+ long long version;
+ void *cb_data, *retain, *release, *copy_description;
+};
+
+typedef struct FSEventStreamContext FSEventStreamContext;
+typedef unsigned int FSEventStreamEventFlags;
+#define kFSEventStreamCreateFlagNoDefer 0x02
+#define kFSEventStreamCreateFlagWatchRoot 0x04
+#define kFSEventStreamCreateFlagFileEvents 0x10
+
+typedef unsigned long long FSEventStreamEventId;
+#define kFSEventStreamEventIdSinceNow 0xFFFFFFFFFFFFFFFFULL
+
+typedef void (*FSEventStreamCallback)(ConstFSEventStreamRef streamRef,
+ void *context,
+ __SIZE_TYPE__ num_of_events,
+ void *event_paths,
+ const FSEventStreamEventFlags event_flags[],
+ const FSEventStreamEventId event_ids[]);
+typedef double CFTimeInterval;
+FSEventStreamRef FSEventStreamCreate(void *allocator,
+ FSEventStreamCallback callback,
+ FSEventStreamContext *context,
+ CFArrayRef paths_to_watch,
+ FSEventStreamEventId since_when,
+ CFTimeInterval latency,
+ FSEventStreamCreateFlags flags);
+CFStringRef CFStringCreateWithCString(void *allocator, const char *string,
+ CFStringEncoding encoding);
+CFArrayRef CFArrayCreate(void *allocator, const void **items, long long count,
+ void *callbacks);
+void CFRunLoopRun(void);
+void CFRunLoopStop(CFRunLoopRef run_loop);
+CFRunLoopRef CFRunLoopGetCurrent(void);
+extern CFStringRef kCFRunLoopDefaultMode;
+void FSEventStreamScheduleWithRunLoop(FSEventStreamRef stream,
+ CFRunLoopRef run_loop,
+ CFStringRef run_loop_mode);
+unsigned char FSEventStreamStart(FSEventStreamRef stream);
+void FSEventStreamStop(FSEventStreamRef stream);
+void FSEventStreamInvalidate(FSEventStreamRef stream);
+void FSEventStreamRelease(FSEventStreamRef stream);
+
+#endif /* !clang */
+#endif /* FSM_DARWIN_GCC_H */
diff --git a/compat/fsmonitor/fsm-listen-darwin.c b/compat/fsmonitor/fsm-listen-darwin.c
new file mode 100644
index 0000000..0741fe8
--- /dev/null
+++ b/compat/fsmonitor/fsm-listen-darwin.c
@@ -0,0 +1,427 @@
+#ifndef __clang__
+#include "fsm-darwin-gcc.h"
+#else
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreServices/CoreServices.h>
+
+#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 "cache.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+
+struct fsmonitor_daemon_backend_data
+{
+ CFStringRef cfsr_worktree_path;
+ CFStringRef cfsr_gitdir_path;
+
+ CFArrayRef cfar_paths_to_watch;
+ int nr_paths_watching;
+
+ FSEventStreamRef stream;
+
+ CFRunLoopRef rl;
+
+ 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=%u %s",
+ path, flag, msg.buf);
+
+ strbuf_release(&msg);
+}
+
+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);
+}
+
+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 fsmonitor_daemon_backend_data *data = state->backend_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;
+ int k;
+ struct strbuf tmp = STRBUF_INIT;
+
+ /*
+ * 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.
+ */
+ 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);
+
+ /*
+ * 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;
+ }
+
+ 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) {
+ const char *rel = path_k +
+ state->path_worktree_watch.len + 1;
+
+ if (!batch)
+ batch = fsmonitor_batch__new();
+ fsmonitor_batch__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();
+ fsmonitor_batch__add_path(batch, tmp.buf);
+ }
+
+ break;
+
+ case IS_OUTSIDE_CONE:
+ default:
+ trace_printf_key(&trace_fsmonitor,
+ "ignoring '%s'", path_k);
+ break;
+ }
+ }
+
+ fsmonitor_publish(state, batch, &cookie_list);
+ string_list_clear(&cookie_list, 0);
+ strbuf_release(&tmp);
+ return;
+
+force_shutdown:
+ fsmonitor_batch__free_list(batch);
+ string_list_clear(&cookie_list, 0);
+
+ data->shutdown_style = FORCE_SHUTDOWN;
+ CFRunLoopStop(data->rl);
+ 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 fsmonitor_daemon_backend_data *data;
+ const void *dir_array[2];
+
+ CALLOC_ARRAY(data, 1);
+ state->backend_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 == NULL)
+ goto failed;
+
+ /*
+ * `data->rl` needs to be set inside the listener thread.
+ */
+
+ return 0;
+
+failed:
+ error(_("Unable to create FSEventStream."));
+
+ FREE_AND_NULL(state->backend_data);
+ return -1;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data;
+
+ if (!state || !state->backend_data)
+ return;
+
+ data = state->backend_data;
+
+ if (data->stream) {
+ if (data->stream_started)
+ FSEventStreamStop(data->stream);
+ if (data->stream_scheduled)
+ FSEventStreamInvalidate(data->stream);
+ FSEventStreamRelease(data->stream);
+ }
+
+ FREE_AND_NULL(state->backend_data);
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data;
+
+ data = state->backend_data;
+ data->shutdown_style = SHUTDOWN_EVENT;
+
+ CFRunLoopStop(data->rl);
+}
+
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data;
+
+ data = state->backend_data;
+
+ data->rl = CFRunLoopGetCurrent();
+
+ FSEventStreamScheduleWithRunLoop(data->stream, data->rl, kCFRunLoopDefaultMode);
+ data->stream_scheduled = 1;
+
+ if (!FSEventStreamStart(data->stream)) {
+ error(_("Failed to start the FSEventStream"));
+ goto force_error_stop_without_loop;
+ }
+ data->stream_started = 1;
+
+ CFRunLoopRun();
+
+ switch (data->shutdown_style) {
+ case FORCE_ERROR_STOP:
+ state->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->error_code = -1;
+ ipc_server_stop_async(state->ipc_server_data);
+ return;
+}
diff --git a/compat/fsmonitor/fsm-listen-win32.c b/compat/fsmonitor/fsm-listen-win32.c
new file mode 100644
index 0000000..5b928ab
--- /dev/null
+++ b/compat/fsmonitor/fsm-listen-win32.c
@@ -0,0 +1,586 @@
+#include "cache.h"
+#include "config.h"
+#include "fsmonitor.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+
+/*
+ * The documentation of ReadDirectoryChangesW() states that the maximum
+ * buffer size is 64K when the monitored directory is remote.
+ *
+ * Larger buffers may be used when the monitored directory is local and
+ * will help us receive events faster from the kernel and avoid dropped
+ * events.
+ *
+ * So we try to use a very large buffer and silently fallback to 64K if
+ * we get an error.
+ */
+#define MAX_RDCW_BUF_FALLBACK (65536)
+#define MAX_RDCW_BUF (65536 * 8)
+
+struct one_watch
+{
+ char buffer[MAX_RDCW_BUF];
+ DWORD buf_len;
+ DWORD count;
+
+ struct strbuf path;
+ HANDLE hDir;
+ HANDLE hEvent;
+ OVERLAPPED overlapped;
+
+ /*
+ * Is there an active ReadDirectoryChangesW() call pending. If so, we
+ * need to later call GetOverlappedResult() and possibly CancelIoEx().
+ */
+ BOOL is_active;
+};
+
+struct fsmonitor_daemon_backend_data
+{
+ struct one_watch *watch_worktree;
+ struct one_watch *watch_gitdir;
+
+ HANDLE hEventShutdown;
+
+ HANDLE hListener[3]; /* we don't own these handles */
+#define LISTENER_SHUTDOWN 0
+#define LISTENER_HAVE_DATA_WORKTREE 1
+#define LISTENER_HAVE_DATA_GITDIR 2
+ int nr_listener_handles;
+};
+
+/*
+ * Convert the WCHAR path from the notification into UTF8 and
+ * then normalize it.
+ */
+static int normalize_path_in_utf8(FILE_NOTIFY_INFORMATION *info,
+ struct strbuf *normalized_path)
+{
+ int reserve;
+ int len = 0;
+
+ strbuf_reset(normalized_path);
+ if (!info->FileNameLength)
+ goto normalize;
+
+ /*
+ * Pre-reserve enough space in the UTF8 buffer for
+ * each Unicode WCHAR character to be mapped into a
+ * sequence of 2 UTF8 characters. That should let us
+ * avoid ERROR_INSUFFICIENT_BUFFER 99.9+% of the time.
+ */
+ reserve = info->FileNameLength + 1;
+ strbuf_grow(normalized_path, reserve);
+
+ for (;;) {
+ len = WideCharToMultiByte(CP_UTF8, 0, info->FileName,
+ info->FileNameLength / sizeof(WCHAR),
+ normalized_path->buf,
+ strbuf_avail(normalized_path) - 1,
+ NULL, NULL);
+ if (len > 0)
+ goto normalize;
+ if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ error(_("[GLE %ld] could not convert path to UTF-8: '%.*ls'"),
+ GetLastError(),
+ (int)(info->FileNameLength / sizeof(WCHAR)),
+ info->FileName);
+ return -1;
+ }
+
+ strbuf_grow(normalized_path,
+ strbuf_avail(normalized_path) + reserve);
+ }
+
+normalize:
+ strbuf_setlen(normalized_path, len);
+ return strbuf_normalize_path(normalized_path);
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+ SetEvent(state->backend_data->hListener[LISTENER_SHUTDOWN]);
+}
+
+static struct one_watch *create_watch(struct fsmonitor_daemon_state *state,
+ const char *path)
+{
+ struct one_watch *watch = NULL;
+ DWORD desired_access = FILE_LIST_DIRECTORY;
+ DWORD share_mode =
+ FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE;
+ HANDLE hDir;
+ wchar_t wpath[MAX_PATH];
+
+ if (xutftowcs_path(wpath, path) < 0) {
+ error(_("could not convert to wide characters: '%s'"), path);
+ return NULL;
+ }
+
+ hDir = CreateFileW(wpath,
+ desired_access, share_mode, NULL, OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
+ NULL);
+ if (hDir == INVALID_HANDLE_VALUE) {
+ error(_("[GLE %ld] could not watch '%s'"),
+ GetLastError(), path);
+ return NULL;
+ }
+
+ CALLOC_ARRAY(watch, 1);
+
+ watch->buf_len = sizeof(watch->buffer); /* assume full MAX_RDCW_BUF */
+
+ strbuf_init(&watch->path, 0);
+ strbuf_addstr(&watch->path, path);
+
+ watch->hDir = hDir;
+ watch->hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
+
+ return watch;
+}
+
+static void destroy_watch(struct one_watch *watch)
+{
+ if (!watch)
+ return;
+
+ strbuf_release(&watch->path);
+ if (watch->hDir != INVALID_HANDLE_VALUE)
+ CloseHandle(watch->hDir);
+ if (watch->hEvent != INVALID_HANDLE_VALUE)
+ CloseHandle(watch->hEvent);
+
+ free(watch);
+}
+
+static int start_rdcw_watch(struct fsmonitor_daemon_backend_data *data,
+ struct one_watch *watch)
+{
+ DWORD dwNotifyFilter =
+ FILE_NOTIFY_CHANGE_FILE_NAME |
+ FILE_NOTIFY_CHANGE_DIR_NAME |
+ FILE_NOTIFY_CHANGE_ATTRIBUTES |
+ FILE_NOTIFY_CHANGE_SIZE |
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
+ FILE_NOTIFY_CHANGE_CREATION;
+
+ ResetEvent(watch->hEvent);
+
+ memset(&watch->overlapped, 0, sizeof(watch->overlapped));
+ watch->overlapped.hEvent = watch->hEvent;
+
+ /*
+ * Queue an async call using Overlapped IO. This returns immediately.
+ * Our event handle will be signalled when the real result is available.
+ *
+ * The return value here just means that we successfully queued it.
+ * We won't know if the Read...() actually produces data until later.
+ */
+ watch->is_active = ReadDirectoryChangesW(
+ watch->hDir, watch->buffer, watch->buf_len, TRUE,
+ dwNotifyFilter, &watch->count, &watch->overlapped, NULL);
+
+ if (watch->is_active)
+ return 0;
+
+ error(_("ReadDirectoryChangedW failed on '%s' [GLE %ld]"),
+ watch->path.buf, GetLastError());
+ return -1;
+}
+
+static int recv_rdcw_watch(struct one_watch *watch)
+{
+ DWORD gle;
+
+ watch->is_active = FALSE;
+
+ /*
+ * The overlapped result is ready. If the Read...() was successful
+ * we finally receive the actual result into our buffer.
+ */
+ if (GetOverlappedResult(watch->hDir, &watch->overlapped, &watch->count,
+ TRUE))
+ return 0;
+
+ gle = GetLastError();
+ if (gle == ERROR_INVALID_PARAMETER &&
+ /*
+ * The kernel throws an invalid parameter error when our
+ * buffer is too big and we are pointed at a remote
+ * directory (and possibly for other reasons). Quietly
+ * set it down and try again.
+ *
+ * See note about MAX_RDCW_BUF at the top.
+ */
+ watch->buf_len > MAX_RDCW_BUF_FALLBACK) {
+ watch->buf_len = MAX_RDCW_BUF_FALLBACK;
+ return -2;
+ }
+
+ /*
+ * NEEDSWORK: If an external <gitdir> is deleted, the above
+ * returns an error. I'm not sure that there's anything that
+ * we can do here other than failing -- the <worktree>/.git
+ * link file would be broken anyway. We might try to check
+ * for that and return a better error message, but I'm not
+ * sure it is worth it.
+ */
+
+ error(_("GetOverlappedResult failed on '%s' [GLE %ld]"),
+ watch->path.buf, gle);
+ return -1;
+}
+
+static void cancel_rdcw_watch(struct one_watch *watch)
+{
+ DWORD count;
+
+ if (!watch || !watch->is_active)
+ return;
+
+ /*
+ * The calls to ReadDirectoryChangesW() and GetOverlappedResult()
+ * form a "pair" (my term) where we queue an IO and promise to
+ * hang around and wait for the kernel to give us the result.
+ *
+ * If for some reason after we queue the IO, we have to quit
+ * or otherwise not stick around for the second half, we must
+ * tell the kernel to abort the IO. This prevents the kernel
+ * from writing to our buffer and/or signalling our event
+ * after we free them.
+ *
+ * (Ask me how much fun it was to track that one down).
+ */
+ CancelIoEx(watch->hDir, &watch->overlapped);
+ GetOverlappedResult(watch->hDir, &watch->overlapped, &count, TRUE);
+ watch->is_active = FALSE;
+}
+
+/*
+ * Process filesystem events that happen anywhere (recursively) under the
+ * <worktree> root directory. For a normal working directory, this includes
+ * both version controlled files and the contents of the .git/ directory.
+ *
+ * If <worktree>/.git is a file, then we only see events for the file
+ * itself.
+ */
+static int process_worktree_events(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data = state->backend_data;
+ struct one_watch *watch = data->watch_worktree;
+ struct strbuf path = STRBUF_INIT;
+ struct string_list cookie_list = STRING_LIST_INIT_DUP;
+ struct fsmonitor_batch *batch = NULL;
+ const char *p = watch->buffer;
+
+ /*
+ * If the kernel gets more events than will fit in the kernel
+ * buffer associated with our RDCW handle, it drops them and
+ * returns a count of zero.
+ *
+ * Yes, the call returns WITHOUT error and with length zero.
+ * This is the documented behavior. (My testing has confirmed
+ * that it also sets the last error to ERROR_NOTIFY_ENUM_DIR,
+ * but we do not rely on that since the function did not
+ * return an error and it is not documented.)
+ *
+ * (The "overflow" case is not ambiguous with the "no data" case
+ * because we did an INFINITE wait.)
+ *
+ * This means we have a gap in coverage. Tell the daemon layer
+ * to resync.
+ */
+ if (!watch->count) {
+ trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
+ "overflow");
+ fsmonitor_force_resync(state);
+ return LISTENER_HAVE_DATA_WORKTREE;
+ }
+
+ /*
+ * On Windows, `info` contains an "array" of paths that are
+ * relative to the root of whichever directory handle received
+ * the event.
+ */
+ for (;;) {
+ FILE_NOTIFY_INFORMATION *info = (void *)p;
+ const char *slash;
+ enum fsmonitor_path_type t;
+
+ strbuf_reset(&path);
+ if (normalize_path_in_utf8(info, &path) == -1)
+ goto skip_this_path;
+
+ t = fsmonitor_classify_path_workdir_relative(path.buf);
+
+ switch (t) {
+ case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
+ /* special case cookie files within .git */
+
+ /* Use just the filename of the cookie file. */
+ slash = find_last_dir_sep(path.buf);
+ string_list_append(&cookie_list,
+ slash ? slash + 1 : path.buf);
+ break;
+
+ case IS_INSIDE_DOT_GIT:
+ /* ignore everything inside of "<worktree>/.git/" */
+ break;
+
+ case IS_DOT_GIT:
+ /* "<worktree>/.git" was deleted (or renamed away) */
+ if ((info->Action == FILE_ACTION_REMOVED) ||
+ (info->Action == FILE_ACTION_RENAMED_OLD_NAME)) {
+ trace2_data_string("fsmonitor", NULL,
+ "fsm-listen/dotgit",
+ "removed");
+ goto force_shutdown;
+ }
+ break;
+
+ case IS_WORKDIR_PATH:
+ /* queue normal pathname */
+ if (!batch)
+ batch = fsmonitor_batch__new();
+ fsmonitor_batch__add_path(batch, path.buf);
+ break;
+
+ case IS_GITDIR:
+ case IS_INSIDE_GITDIR:
+ case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+ default:
+ BUG("unexpected path classification '%d' for '%s'",
+ t, path.buf);
+ }
+
+skip_this_path:
+ if (!info->NextEntryOffset)
+ break;
+ p += info->NextEntryOffset;
+ }
+
+ fsmonitor_publish(state, batch, &cookie_list);
+ batch = NULL;
+ string_list_clear(&cookie_list, 0);
+ strbuf_release(&path);
+ return LISTENER_HAVE_DATA_WORKTREE;
+
+force_shutdown:
+ fsmonitor_batch__free_list(batch);
+ string_list_clear(&cookie_list, 0);
+ strbuf_release(&path);
+ return LISTENER_SHUTDOWN;
+}
+
+/*
+ * Process filesystem events that happened anywhere (recursively) under the
+ * external <gitdir> (such as non-primary worktrees or submodules).
+ * We only care about cookie files that our client threads created here.
+ *
+ * Note that we DO NOT get filesystem events on the external <gitdir>
+ * itself (it is not inside something that we are watching). In particular,
+ * we do not get an event if the external <gitdir> is deleted.
+ */
+static int process_gitdir_events(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data = state->backend_data;
+ struct one_watch *watch = data->watch_gitdir;
+ struct strbuf path = STRBUF_INIT;
+ struct string_list cookie_list = STRING_LIST_INIT_DUP;
+ const char *p = watch->buffer;
+
+ if (!watch->count) {
+ trace2_data_string("fsmonitor", NULL, "fsm-listen/kernel",
+ "overflow");
+ fsmonitor_force_resync(state);
+ return LISTENER_HAVE_DATA_GITDIR;
+ }
+
+ for (;;) {
+ FILE_NOTIFY_INFORMATION *info = (void *)p;
+ const char *slash;
+ enum fsmonitor_path_type t;
+
+ strbuf_reset(&path);
+ if (normalize_path_in_utf8(info, &path) == -1)
+ goto skip_this_path;
+
+ t = fsmonitor_classify_path_gitdir_relative(path.buf);
+
+ switch (t) {
+ case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
+ /* special case cookie files within gitdir */
+
+ /* Use just the filename of the cookie file. */
+ slash = find_last_dir_sep(path.buf);
+ string_list_append(&cookie_list,
+ slash ? slash + 1 : path.buf);
+ break;
+
+ case IS_INSIDE_GITDIR:
+ goto skip_this_path;
+
+ default:
+ BUG("unexpected path classification '%d' for '%s'",
+ t, path.buf);
+ }
+
+skip_this_path:
+ if (!info->NextEntryOffset)
+ break;
+ p += info->NextEntryOffset;
+ }
+
+ fsmonitor_publish(state, NULL, &cookie_list);
+ string_list_clear(&cookie_list, 0);
+ strbuf_release(&path);
+ return LISTENER_HAVE_DATA_GITDIR;
+}
+
+void fsm_listen__loop(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data = state->backend_data;
+ DWORD dwWait;
+ int result;
+
+ state->error_code = 0;
+
+ if (start_rdcw_watch(data, data->watch_worktree) == -1)
+ goto force_error_stop;
+
+ if (data->watch_gitdir &&
+ start_rdcw_watch(data, data->watch_gitdir) == -1)
+ goto force_error_stop;
+
+ for (;;) {
+ dwWait = WaitForMultipleObjects(data->nr_listener_handles,
+ data->hListener,
+ FALSE, INFINITE);
+
+ if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_WORKTREE) {
+ result = recv_rdcw_watch(data->watch_worktree);
+ if (result == -1) {
+ /* hard error */
+ goto force_error_stop;
+ }
+ if (result == -2) {
+ /* retryable error */
+ if (start_rdcw_watch(data, data->watch_worktree) == -1)
+ goto force_error_stop;
+ continue;
+ }
+
+ /* have data */
+ if (process_worktree_events(state) == LISTENER_SHUTDOWN)
+ goto force_shutdown;
+ if (start_rdcw_watch(data, data->watch_worktree) == -1)
+ goto force_error_stop;
+ continue;
+ }
+
+ if (dwWait == WAIT_OBJECT_0 + LISTENER_HAVE_DATA_GITDIR) {
+ result = recv_rdcw_watch(data->watch_gitdir);
+ if (result == -1) {
+ /* hard error */
+ goto force_error_stop;
+ }
+ if (result == -2) {
+ /* retryable error */
+ if (start_rdcw_watch(data, data->watch_gitdir) == -1)
+ goto force_error_stop;
+ continue;
+ }
+
+ /* have data */
+ if (process_gitdir_events(state) == LISTENER_SHUTDOWN)
+ goto force_shutdown;
+ if (start_rdcw_watch(data, data->watch_gitdir) == -1)
+ goto force_error_stop;
+ continue;
+ }
+
+ if (dwWait == WAIT_OBJECT_0 + LISTENER_SHUTDOWN)
+ goto clean_shutdown;
+
+ error(_("could not read directory changes [GLE %ld]"),
+ GetLastError());
+ goto force_error_stop;
+ }
+
+force_error_stop:
+ state->error_code = -1;
+
+force_shutdown:
+ /*
+ * Tell the IPC thead pool to stop (which completes the await
+ * in the main thread (which will also signal this thread (if
+ * we are still alive))).
+ */
+ ipc_server_stop_async(state->ipc_server_data);
+
+clean_shutdown:
+ cancel_rdcw_watch(data->watch_worktree);
+ cancel_rdcw_watch(data->watch_gitdir);
+}
+
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data;
+
+ CALLOC_ARRAY(data, 1);
+
+ data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL);
+
+ data->watch_worktree = create_watch(state,
+ state->path_worktree_watch.buf);
+ if (!data->watch_worktree)
+ goto failed;
+
+ if (state->nr_paths_watching > 1) {
+ data->watch_gitdir = create_watch(state,
+ state->path_gitdir_watch.buf);
+ if (!data->watch_gitdir)
+ goto failed;
+ }
+
+ data->hListener[LISTENER_SHUTDOWN] = data->hEventShutdown;
+ data->nr_listener_handles++;
+
+ data->hListener[LISTENER_HAVE_DATA_WORKTREE] =
+ data->watch_worktree->hEvent;
+ data->nr_listener_handles++;
+
+ if (data->watch_gitdir) {
+ data->hListener[LISTENER_HAVE_DATA_GITDIR] =
+ data->watch_gitdir->hEvent;
+ data->nr_listener_handles++;
+ }
+
+ state->backend_data = data;
+ return 0;
+
+failed:
+ CloseHandle(data->hEventShutdown);
+ destroy_watch(data->watch_worktree);
+ destroy_watch(data->watch_gitdir);
+
+ return -1;
+}
+
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state)
+{
+ struct fsmonitor_daemon_backend_data *data;
+
+ if (!state || !state->backend_data)
+ return;
+
+ data = state->backend_data;
+
+ CloseHandle(data->hEventShutdown);
+ destroy_watch(data->watch_worktree);
+ destroy_watch(data->watch_gitdir);
+
+ FREE_AND_NULL(state->backend_data);
+}
diff --git a/compat/fsmonitor/fsm-listen.h b/compat/fsmonitor/fsm-listen.h
new file mode 100644
index 0000000..f053934
--- /dev/null
+++ b/compat/fsmonitor/fsm-listen.h
@@ -0,0 +1,49 @@
+#ifndef FSM_LISTEN_H
+#define FSM_LISTEN_H
+
+/* This needs to be implemented by each backend */
+
+#ifdef HAVE_FSMONITOR_DAEMON_BACKEND
+
+struct fsmonitor_daemon_state;
+
+/*
+ * Initialize platform-specific data for the fsmonitor listener thread.
+ * This will be called from the main thread PRIOR to staring the
+ * fsmonitor_fs_listener thread.
+ *
+ * Returns 0 if successful.
+ * Returns -1 otherwise.
+ */
+int fsm_listen__ctor(struct fsmonitor_daemon_state *state);
+
+/*
+ * Cleanup platform-specific data for the fsmonitor listener thread.
+ * This will be called from the main thread AFTER joining the listener.
+ */
+void fsm_listen__dtor(struct fsmonitor_daemon_state *state);
+
+/*
+ * The main body of the platform-specific event loop to watch for
+ * filesystem events. This will run in the fsmonitor_fs_listen thread.
+ *
+ * It should call `ipc_server_stop_async()` if the listener thread
+ * prematurely terminates (because of a filesystem error or if it
+ * detects that the .git directory has been deleted). (It should NOT
+ * do so if the listener thread receives a normal shutdown signal from
+ * the IPC layer.)
+ *
+ * It should set `state->error_code` to -1 if the daemon should exit
+ * with an error.
+ */
+void fsm_listen__loop(struct fsmonitor_daemon_state *state);
+
+/*
+ * Gently request that the fsmonitor listener thread shutdown.
+ * It does not wait for it to stop. The caller should do a JOIN
+ * to wait for it.
+ */
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state);
+
+#endif /* HAVE_FSMONITOR_DAEMON_BACKEND */
+#endif /* FSM_LISTEN_H */