@@ -92,6 +92,149 @@ 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);
+
+ /*
+ * NEEDSWORK: 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.
+ */
+ 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
@@ -395,6 +538,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 readering the current token data
* series, we can free it now. Otherwise, let the last reader free
* it.
@@ -416,6 +562,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)
@@ -490,6 +638,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:
@@ -522,6 +672,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
*/
do_flush = 1;
do_trivial = 1;
+ do_cookie = 1;
} else if (!skip_prefix(command, "builtin:", &p)) {
/* assume V1 timestamp or garbage */
@@ -535,6 +686,7 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
"fsmonitor: unsupported V1 protocol '%s'"),
command);
do_trivial = 1;
+ do_cookie = 1;
} else {
/* We have "builtin:*" */
@@ -544,12 +696,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state,
"fsmonitor: invalid V2 protocol token '%s'",
command);
do_trivial = 1;
+ do_cookie = 1;
} else {
/*
* We have a V2 valid token:
* "builtin:<token_id>:<seq_nr>"
*/
+ do_cookie = 1;
}
}
@@ -558,6 +712,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);
@@ -769,7 +947,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)
@@ -922,6 +1102,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);
}
@@ -1011,7 +1194,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();
@@ -1035,6 +1220,23 @@ 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>.
+ */
+ 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.
@@ -1047,6 +1249,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);
fsmonitor_fs_listen__dtor(&state);
@@ -1054,6 +1257,11 @@ static int fsmonitor_run_daemon(void)
strbuf_release(&state.path_worktree_watch);
strbuf_release(&state.path_gitdir_watch);
+ strbuf_release(&state.path_cookie_prefix);
+
+ /*
+ * NEEDSWORK: Consider "rm -rf <gitdir>/<fsmonitor-dir>"
+ */
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;