summaryrefslogtreecommitdiff
path: root/compat/fsmonitor/fsm-listen-win32.c
diff options
context:
space:
mode:
Diffstat (limited to 'compat/fsmonitor/fsm-listen-win32.c')
-rw-r--r--compat/fsmonitor/fsm-listen-win32.c873
1 files changed, 873 insertions, 0 deletions
diff --git a/compat/fsmonitor/fsm-listen-win32.c b/compat/fsmonitor/fsm-listen-win32.c
new file mode 100644
index 0000000..5a21dad
--- /dev/null
+++ b/compat/fsmonitor/fsm-listen-win32.c
@@ -0,0 +1,873 @@
+#include "git-compat-util.h"
+#include "config.h"
+#include "fsmonitor-ll.h"
+#include "fsm-listen.h"
+#include "fsmonitor--daemon.h"
+#include "gettext.h"
+#include "simple-ipc.h"
+#include "trace2.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;
+ wchar_t wpath_longname[MAX_PATH + 1];
+ DWORD wpath_longname_len;
+
+ 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;
+
+ /*
+ * Are shortnames enabled on the containing drive? This is
+ * always true for "C:/" drives and usually never true for
+ * other drives.
+ *
+ * We only set this for the worktree because we only need to
+ * convert shortname paths to longname paths for items we send
+ * to clients. (We don't care about shortname expansion for
+ * paths inside a GITDIR because we never send them to
+ * clients.)
+ */
+ BOOL has_shortnames;
+ BOOL has_tilde;
+ wchar_t dotgit_shortname[16]; /* for 8.3 name */
+};
+
+struct fsm_listen_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 event into UTF8 and normalize it.
+ *
+ * `wpath_len` is in WCHARS not bytes.
+ */
+static int normalize_path_in_utf8(wchar_t *wpath, DWORD wpath_len,
+ struct strbuf *normalized_path)
+{
+ int reserve;
+ int len = 0;
+
+ strbuf_reset(normalized_path);
+ if (!wpath_len)
+ 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 = 2 * wpath_len + 1;
+ strbuf_grow(normalized_path, reserve);
+
+ for (;;) {
+ len = WideCharToMultiByte(CP_UTF8, 0,
+ wpath, wpath_len,
+ 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)wpath_len, wpath);
+ return -1;
+ }
+
+ strbuf_grow(normalized_path,
+ strbuf_avail(normalized_path) + reserve);
+ }
+
+normalize:
+ strbuf_setlen(normalized_path, len);
+ return strbuf_normalize_path(normalized_path);
+}
+
+/*
+ * See if the worktree root directory has shortnames enabled.
+ * This will help us decide if we need to do an expensive shortname
+ * to longname conversion on every notification event.
+ *
+ * We do not want to create a file to test this, so we assume that the
+ * root directory contains a ".git" file or directory. (Our caller
+ * only calls us for the worktree root, so this should be fine.)
+ *
+ * Remember the spelling of the shortname for ".git" if it exists.
+ */
+static void check_for_shortnames(struct one_watch *watch)
+{
+ wchar_t buf_in[MAX_PATH + 1];
+ wchar_t buf_out[MAX_PATH + 1];
+ wchar_t *last;
+ wchar_t *p;
+
+ /* build L"<wt-root-path>/.git" */
+ swprintf(buf_in, ARRAY_SIZE(buf_in) - 1, L"%ls.git",
+ watch->wpath_longname);
+
+ if (!GetShortPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out)))
+ return;
+
+ /*
+ * Get the final filename component of the shortpath.
+ * We know that the path does not have a final slash.
+ */
+ for (last = p = buf_out; *p; p++)
+ if (*p == L'/' || *p == '\\')
+ last = p + 1;
+
+ if (!wcscmp(last, L".git"))
+ return;
+
+ watch->has_shortnames = 1;
+ wcsncpy(watch->dotgit_shortname, last,
+ ARRAY_SIZE(watch->dotgit_shortname));
+
+ /*
+ * The shortname for ".git" is usually of the form "GIT~1", so
+ * we should be able to avoid shortname to longname mapping on
+ * every notification event if the source string does not
+ * contain a "~".
+ *
+ * However, the documentation for GetLongPathNameW() says
+ * that there are filesystems that don't follow that pattern
+ * and warns against this optimization.
+ *
+ * Lets test this.
+ */
+ if (wcschr(watch->dotgit_shortname, L'~'))
+ watch->has_tilde = 1;
+}
+
+enum get_relative_result {
+ GRR_NO_CONVERSION_NEEDED,
+ GRR_HAVE_CONVERSION,
+ GRR_SHUTDOWN,
+};
+
+/*
+ * Info notification paths are relative to the root of the watch.
+ * If our CWD is still at the root, then we can use relative paths
+ * to convert from shortnames to longnames. If our process has a
+ * different CWD, then we need to construct an absolute path, do
+ * the conversion, and then return the root-relative portion.
+ *
+ * We use the longname form of the root as our basis and assume that
+ * it already has a trailing slash.
+ *
+ * `wpath_len` is in WCHARS not bytes.
+ */
+static enum get_relative_result get_relative_longname(
+ struct one_watch *watch,
+ const wchar_t *wpath, DWORD wpath_len,
+ wchar_t *wpath_longname, size_t bufsize_wpath_longname)
+{
+ wchar_t buf_in[2 * MAX_PATH + 1];
+ wchar_t buf_out[MAX_PATH + 1];
+ DWORD root_len;
+ DWORD out_len;
+
+ /*
+ * Build L"<wt-root-path>/<event-rel-path>"
+ * Note that the <event-rel-path> might not be null terminated
+ * so we avoid swprintf() constructions.
+ */
+ root_len = watch->wpath_longname_len;
+ if (root_len + wpath_len >= ARRAY_SIZE(buf_in)) {
+ /*
+ * This should not happen. We cannot append the observed
+ * relative path onto the end of the worktree root path
+ * without overflowing the buffer. Just give up.
+ */
+ return GRR_SHUTDOWN;
+ }
+ wcsncpy(buf_in, watch->wpath_longname, root_len);
+ wcsncpy(buf_in + root_len, wpath, wpath_len);
+ buf_in[root_len + wpath_len] = 0;
+
+ /*
+ * We don't actually know if the source pathname is a
+ * shortname or a longname. This Windows routine allows
+ * either to be given as input.
+ */
+ out_len = GetLongPathNameW(buf_in, buf_out, ARRAY_SIZE(buf_out));
+ if (!out_len) {
+ /*
+ * The shortname to longname conversion can fail for
+ * various reasons, for example if the file has been
+ * deleted. (That is, if we just received a
+ * delete-file notification event and the file is
+ * already gone, we can't ask the file system to
+ * lookup the longname for it. Likewise, for moves
+ * and renames where we are given the old name.)
+ *
+ * Since deleting or moving a file or directory by its
+ * shortname is rather obscure, I'm going ignore the
+ * failure and ask the caller to report the original
+ * relative path. This seems kinder than failing here
+ * and forcing a resync. Besides, forcing a resync on
+ * every file/directory delete would effectively
+ * cripple monitoring.
+ *
+ * We might revisit this in the future.
+ */
+ return GRR_NO_CONVERSION_NEEDED;
+ }
+
+ if (!wcscmp(buf_in, buf_out)) {
+ /*
+ * The path does not have a shortname alias.
+ */
+ return GRR_NO_CONVERSION_NEEDED;
+ }
+
+ if (wcsncmp(buf_in, buf_out, root_len)) {
+ /*
+ * The spelling of the root directory portion of the computed
+ * longname has changed. This should not happen. Basically,
+ * it means that we don't know where (without recomputing the
+ * longname of just the root directory) to split out the
+ * relative path. Since this should not happen, I'm just
+ * going to let this fail and force a shutdown (because all
+ * subsequent events are probably going to see the same
+ * mismatch).
+ */
+ return GRR_SHUTDOWN;
+ }
+
+ if (out_len - root_len >= bufsize_wpath_longname) {
+ /*
+ * This should not happen. We cannot copy the root-relative
+ * portion of the path into the provided buffer without an
+ * overrun. Just give up.
+ */
+ return GRR_SHUTDOWN;
+ }
+
+ /* Return the worktree root-relative portion of the longname. */
+
+ wcscpy(wpath_longname, buf_out + root_len);
+ return GRR_HAVE_CONVERSION;
+}
+
+void fsm_listen__stop_async(struct fsmonitor_daemon_state *state)
+{
+ SetEvent(state->listen_data->hListener[LISTENER_SHUTDOWN]);
+}
+
+static struct one_watch *create_watch(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;
+ DWORD len_longname;
+ wchar_t wpath[MAX_PATH + 1];
+ wchar_t wpath_longname[MAX_PATH + 1];
+
+ 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;
+ }
+
+ len_longname = GetLongPathNameW(wpath, wpath_longname,
+ ARRAY_SIZE(wpath_longname));
+ if (!len_longname) {
+ error(_("[GLE %ld] could not get longname of '%s'"),
+ GetLastError(), path);
+ CloseHandle(hDir);
+ return NULL;
+ }
+
+ if (wpath_longname[len_longname - 1] != L'/' &&
+ wpath_longname[len_longname - 1] != L'\\') {
+ wpath_longname[len_longname++] = L'/';
+ wpath_longname[len_longname] = 0;
+ }
+
+ 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);
+
+ wcscpy(watch->wpath_longname, wpath_longname);
+ watch->wpath_longname_len = len_longname;
+
+ 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 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;
+ }
+
+ /*
+ * GetOverlappedResult() fails if the watched directory is
+ * deleted while we were waiting for an overlapped IO to
+ * complete. The documentation did not list specific errors,
+ * but I observed ERROR_ACCESS_DENIED (0x05) errors during
+ * testing.
+ *
+ * Note that we only get notificaiton events for events
+ * *within* the directory, not *on* the directory itself.
+ * (These might be properies of the parent directory, for
+ * example).
+ *
+ * NEEDSWORK: We might try to check for the deleted directory
+ * case and return a better error message, but I'm not sure it
+ * is worth it.
+ *
+ * Shutdown if we get any error.
+ */
+
+ 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 a single relative pathname event.
+ * Return 1 if we should shutdown.
+ */
+static int process_1_worktree_event(
+ struct string_list *cookie_list,
+ struct fsmonitor_batch **batch,
+ const struct strbuf *path,
+ enum fsmonitor_path_type t,
+ DWORD info_action)
+{
+ const char *slash;
+
+ 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");
+ return 1;
+ }
+ 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);
+ }
+
+ return 0;
+}
+
+/*
+ * 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 fsm_listen_data *data = state->listen_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;
+ wchar_t wpath_longname[MAX_PATH + 1];
+
+ /*
+ * 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;
+ wchar_t *wpath = info->FileName;
+ DWORD wpath_len = info->FileNameLength / sizeof(WCHAR);
+ enum fsmonitor_path_type t;
+ enum get_relative_result grr;
+
+ if (watch->has_shortnames) {
+ if (!wcscmp(wpath, watch->dotgit_shortname)) {
+ /*
+ * This event exactly matches the
+ * spelling of the shortname of
+ * ".git", so we can skip some steps.
+ *
+ * (This case is odd because the user
+ * can "rm -rf GIT~1" and we cannot
+ * use the filesystem to map it back
+ * to ".git".)
+ */
+ strbuf_reset(&path);
+ strbuf_addstr(&path, ".git");
+ t = IS_DOT_GIT;
+ goto process_it;
+ }
+
+ if (watch->has_tilde && !wcschr(wpath, L'~')) {
+ /*
+ * Shortnames on this filesystem have tildes
+ * and the notification path does not have
+ * one, so we assume that it is a longname.
+ */
+ goto normalize_it;
+ }
+
+ grr = get_relative_longname(watch, wpath, wpath_len,
+ wpath_longname,
+ ARRAY_SIZE(wpath_longname));
+ switch (grr) {
+ case GRR_NO_CONVERSION_NEEDED: /* use info buffer as is */
+ break;
+ case GRR_HAVE_CONVERSION:
+ wpath = wpath_longname;
+ wpath_len = wcslen(wpath);
+ break;
+ default:
+ case GRR_SHUTDOWN:
+ goto force_shutdown;
+ }
+ }
+
+normalize_it:
+ if (normalize_path_in_utf8(wpath, wpath_len, &path) == -1)
+ goto skip_this_path;
+
+ t = fsmonitor_classify_path_workdir_relative(path.buf);
+
+process_it:
+ if (process_1_worktree_event(&cookie_list, &batch, &path, t,
+ info->Action))
+ goto force_shutdown;
+
+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.
+ *
+ * Also, we do not care about shortnames within the external <gitdir>, since
+ * we never send these paths to clients.
+ */
+static int process_gitdir_events(struct fsmonitor_daemon_state *state)
+{
+ struct fsm_listen_data *data = state->listen_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;
+
+ if (normalize_path_in_utf8(
+ info->FileName,
+ info->FileNameLength / sizeof(WCHAR),
+ &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 fsm_listen_data *data = state->listen_data;
+ DWORD dwWait;
+ int result;
+
+ state->listen_error_code = 0;
+
+ if (start_rdcw_watch(data->watch_worktree) == -1)
+ goto force_error_stop;
+
+ if (data->watch_gitdir &&
+ start_rdcw_watch(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->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->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->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->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->listen_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 fsm_listen_data *data;
+
+ CALLOC_ARRAY(data, 1);
+
+ data->hEventShutdown = CreateEvent(NULL, TRUE, FALSE, NULL);
+
+ data->watch_worktree = create_watch(state->path_worktree_watch.buf);
+ if (!data->watch_worktree)
+ goto failed;
+
+ check_for_shortnames(data->watch_worktree);
+
+ if (state->nr_paths_watching > 1) {
+ data->watch_gitdir = create_watch(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->listen_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 fsm_listen_data *data;
+
+ if (!state || !state->listen_data)
+ return;
+
+ data = state->listen_data;
+
+ CloseHandle(data->hEventShutdown);
+ destroy_watch(data->watch_worktree);
+ destroy_watch(data->watch_gitdir);
+
+ FREE_AND_NULL(state->listen_data);
+}