@@ -107,6 +107,155 @@ static int do_as_client__status(void)
}
}
+enum fsmonitor_cookie_item_result {
+ FCIR_ERROR = -1, /* could not create cookie file ? */
+ FCIR_INIT = 0,
+ FCIR_SEEN,
+ FCIR_ABORT,
+};
+
+struct fsmonitor_cookie_item {
+ struct hashmap_entry entry;
+ const char *name;
+ enum fsmonitor_cookie_item_result result;
+};
+
+static int cookies_cmp(const void *data, const struct hashmap_entry *he1,
+ const struct hashmap_entry *he2, const void *keydata)
+{
+ const struct fsmonitor_cookie_item *a =
+ container_of(he1, const struct fsmonitor_cookie_item, entry);
+ const struct fsmonitor_cookie_item *b =
+ container_of(he2, const struct fsmonitor_cookie_item, entry);
+
+ return strcmp(a->name, keydata ? keydata : b->name);
+}
+
+static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie(
+ struct fsmonitor_daemon_state *state)
+{
+ /* assert current thread holding state->main_lock */
+
+ int fd;
+ struct fsmonitor_cookie_item *cookie;
+ struct strbuf cookie_pathname = STRBUF_INIT;
+ struct strbuf cookie_filename = STRBUF_INIT;
+ enum fsmonitor_cookie_item_result result;
+ int my_cookie_seq;
+
+ CALLOC_ARRAY(cookie, 1);
+
+ my_cookie_seq = state->cookie_seq++;
+
+ strbuf_addf(&cookie_filename, "%i-%i", getpid(), my_cookie_seq);
+
+ strbuf_addbuf(&cookie_pathname, &state->path_cookie_prefix);
+ strbuf_addbuf(&cookie_pathname, &cookie_filename);
+
+ cookie->name = strbuf_detach(&cookie_filename, NULL);
+ cookie->result = FCIR_INIT;
+ hashmap_entry_init(&cookie->entry, strhash(cookie->name));
+
+ hashmap_add(&state->cookies, &cookie->entry);
+
+ trace_printf_key(&trace_fsmonitor, "cookie-wait: '%s' '%s'",
+ cookie->name, cookie_pathname.buf);
+
+ /*
+ * Create the cookie file on disk and then wait for a notification
+ * that the listener thread has seen it.
+ */
+ fd = open(cookie_pathname.buf, O_WRONLY | O_CREAT | O_EXCL, 0600);
+ if (fd >= 0) {
+ close(fd);
+ unlink(cookie_pathname.buf);
+
+ /*
+ * Technically, this is an infinite wait (well, unless another
+ * thread sends us an abort). I'd like to change this to
+ * use `pthread_cond_timedwait()` and return an error/timeout
+ * and let the caller do the trivial response thing, but we
+ * don't have that routine in our thread-utils.
+ *
+ * After extensive beta testing I'm not really worried about
+ * this. Also note that the above open() and unlink() calls
+ * will cause at least two FS events on that path, so the odds
+ * of getting stuck are pretty slim.
+ */
+ while (cookie->result == FCIR_INIT)
+ pthread_cond_wait(&state->cookies_cond,
+ &state->main_lock);
+ } else {
+ error_errno(_("could not create fsmonitor cookie '%s'"),
+ cookie->name);
+
+ cookie->result = FCIR_ERROR;
+ }
+
+ hashmap_remove(&state->cookies, &cookie->entry, NULL);
+
+ result = cookie->result;
+
+ free((char*)cookie->name);
+ free(cookie);
+ strbuf_release(&cookie_pathname);
+
+ return result;
+}
+
+/*
+ * Mark these cookies as _SEEN and wake up the corresponding client threads.
+ */
+static void with_lock__mark_cookies_seen(struct fsmonitor_daemon_state *state,
+ const struct string_list *cookie_names)
+{
+ /* assert current thread holding state->main_lock */
+
+ int k;
+ int nr_seen = 0;
+
+ for (k = 0; k < cookie_names->nr; k++) {
+ struct fsmonitor_cookie_item key;
+ struct fsmonitor_cookie_item *cookie;
+
+ key.name = cookie_names->items[k].string;
+ hashmap_entry_init(&key.entry, strhash(key.name));
+
+ cookie = hashmap_get_entry(&state->cookies, &key, entry, NULL);
+ if (cookie) {
+ trace_printf_key(&trace_fsmonitor, "cookie-seen: '%s'",
+ cookie->name);
+ cookie->result = FCIR_SEEN;
+ nr_seen++;
+ }
+ }
+
+ if (nr_seen)
+ pthread_cond_broadcast(&state->cookies_cond);
+}
+
+/*
+ * Set _ABORT on all pending cookies and wake up all client threads.
+ */
+static void with_lock__abort_all_cookies(struct fsmonitor_daemon_state *state)
+{
+ /* assert current thread holding state->main_lock */
+
+ struct hashmap_iter iter;
+ struct fsmonitor_cookie_item *cookie;
+ int nr_aborted = 0;
+
+ hashmap_for_each_entry(&state->cookies, &iter, cookie, entry) {
+ trace_printf_key(&trace_fsmonitor, "cookie-abort: '%s'",
+ cookie->name);
+ cookie->result = FCIR_ABORT;
+ nr_aborted++;
+ }
+
+ if (nr_aborted)
+ pthread_cond_broadcast(&state->cookies_cond);
+}
+
/*
* Requests to and from a FSMonitor Protocol V2 provider use an opaque
* "token" as a virtual timestamp. Clients can request a summary of all
@@ -404,6 +553,9 @@ static void fsmonitor_free_token_data(struct fsmonitor_token_data *token)
* We should create a new token and start fresh (as if we just
* booted up).
*
+ * [2] Some of those lost events may have been for cookie files. We
+ * should assume the worst and abort them rather letting them starve.
+ *
* If there are no concurrent threads reading the current token data
* series, we can free it now. Otherwise, let the last reader free
* it.
@@ -425,6 +577,8 @@ static void with_lock__do_force_resync(struct fsmonitor_daemon_state *state)
state->current_token_data = new_one;
fsmonitor_free_token_data(free_me);
+
+ with_lock__abort_all_cookies(state);
}
void fsmonitor_force_resync(struct fsmonitor_daemon_state *state)
@@ -500,6 +654,8 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
int hash_ret;
int do_trivial = 0;
int do_flush = 0;
+ int do_cookie = 0;
+ enum fsmonitor_cookie_item_result cookie_result;
/*
* We expect `command` to be of the form:
@@ -560,6 +716,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
* We have a V2 valid token:
* "builtin:<token_id>:<seq_nr>"
*/
+ do_cookie = 1;
}
}
@@ -568,6 +725,30 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
if (!state->current_token_data)
BUG("fsmonitor state does not have a current token");
+ /*
+ * Write a cookie file inside the directory being watched in
+ * an effort to flush out existing filesystem events that we
+ * actually care about. Suspend this client thread until we
+ * see the filesystem events for this cookie file.
+ *
+ * Creating the cookie lets us guarantee that our FS listener
+ * thread has drained the kernel queue and we are caught up
+ * with the kernel.
+ *
+ * If we cannot create the cookie (or otherwise guarantee that
+ * we are caught up), we send a trivial response. We have to
+ * assume that there might be some very, very recent activity
+ * on the FS still in flight.
+ */
+ if (do_cookie) {
+ cookie_result = with_lock__wait_for_cookie(state);
+ if (cookie_result != FCIR_SEEN) {
+ error(_("fsmonitor: cookie_result '%d' != SEEN"),
+ cookie_result);
+ do_trivial = 1;
+ }
+ }
+
if (do_flush)
with_lock__do_force_resync(state);
@@ -788,7 +969,9 @@ static int handle_client(void *data,
return result;
}
-#define FSMONITOR_COOKIE_PREFIX ".fsmonitor-daemon-"
+#define FSMONITOR_DIR "fsmonitor--daemon"
+#define FSMONITOR_COOKIE_DIR "cookies"
+#define FSMONITOR_COOKIE_PREFIX (FSMONITOR_DIR "/" FSMONITOR_COOKIE_DIR "/")
enum fsmonitor_path_type fsmonitor_classify_path_workdir_relative(
const char *rel)
@@ -941,6 +1124,9 @@ void fsmonitor_publish(struct fsmonitor_daemon_state *state,
}
}
+ if (cookie_names->nr)
+ with_lock__mark_cookies_seen(state, cookie_names);
+
pthread_mutex_unlock(&state->main_lock);
}
@@ -1032,7 +1218,9 @@ static int fsmonitor_run_daemon(void)
memset(&state, 0, sizeof(state));
+ hashmap_init(&state.cookies, cookies_cmp, NULL, 0);
pthread_mutex_init(&state.main_lock, NULL);
+ pthread_cond_init(&state.cookies_cond, NULL);
state.error_code = 0;
state.current_token_data = fsmonitor_new_token_data();
@@ -1057,6 +1245,44 @@ static int fsmonitor_run_daemon(void)
state.nr_paths_watching = 2;
}
+ /*
+ * We will write filesystem syncing cookie files into
+ * <gitdir>/<fsmonitor-dir>/<cookie-dir>/<pid>-<seq>.
+ *
+ * The extra layers of subdirectories here keep us from
+ * changing the mtime on ".git/" or ".git/foo/" when we create
+ * or delete cookie files.
+ *
+ * There have been problems with some IDEs that do a
+ * non-recursive watch of the ".git/" directory and run a
+ * series of commands any time something happens.
+ *
+ * For example, if we place our cookie files directly in
+ * ".git/" or ".git/foo/" then a `git status` (or similar
+ * command) from the IDE will cause a cookie file to be
+ * created in one of those dirs. This causes the mtime of
+ * those dirs to change. This triggers the IDE's watch
+ * notification. This triggers the IDE to run those commands
+ * again. And the process repeats and the machine never goes
+ * idle.
+ *
+ * Adding the extra layers of subdirectories prevents the
+ * mtime of ".git/" and ".git/foo" from changing when a
+ * cookie file is created.
+ */
+ strbuf_init(&state.path_cookie_prefix, 0);
+ strbuf_addbuf(&state.path_cookie_prefix, &state.path_gitdir_watch);
+
+ strbuf_addch(&state.path_cookie_prefix, '/');
+ strbuf_addstr(&state.path_cookie_prefix, FSMONITOR_DIR);
+ mkdir(state.path_cookie_prefix.buf, 0777);
+
+ strbuf_addch(&state.path_cookie_prefix, '/');
+ strbuf_addstr(&state.path_cookie_prefix, FSMONITOR_COOKIE_DIR);
+ mkdir(state.path_cookie_prefix.buf, 0777);
+
+ strbuf_addch(&state.path_cookie_prefix, '/');
+
/*
* Confirm that we can create platform-specific resources for the
* filesystem listener before we bother starting all the threads.
@@ -1069,6 +1295,7 @@ static int fsmonitor_run_daemon(void)
err = fsmonitor_run_daemon_1(&state);
done:
+ pthread_cond_destroy(&state.cookies_cond);
pthread_mutex_destroy(&state.main_lock);
fsm_listen__dtor(&state);
@@ -1076,6 +1303,7 @@ done:
strbuf_release(&state.path_worktree_watch);
strbuf_release(&state.path_gitdir_watch);
+ strbuf_release(&state.path_cookie_prefix);
return err;
}
@@ -45,6 +45,11 @@ struct fsmonitor_daemon_state {
struct fsmonitor_token_data *current_token_data;
+ struct strbuf path_cookie_prefix;
+ pthread_cond_t cookies_cond;
+ int cookie_seq;
+ struct hashmap cookies;
+
int error_code;
struct fsmonitor_daemon_backend_data *backend_data;