1607 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			C
		
	
	
			
		
		
	
	
			1607 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			C
		
	
	
| #define USE_THE_REPOSITORY_VARIABLE
 | |
| #define DISABLE_SIGN_COMPARE_WARNINGS
 | |
| 
 | |
| #include "builtin.h"
 | |
| #include "abspath.h"
 | |
| #include "config.h"
 | |
| #include "dir.h"
 | |
| #include "gettext.h"
 | |
| #include "parse-options.h"
 | |
| #include "fsmonitor-ll.h"
 | |
| #include "fsmonitor-ipc.h"
 | |
| #include "fsmonitor-settings.h"
 | |
| #include "compat/fsmonitor/fsm-health.h"
 | |
| #include "compat/fsmonitor/fsm-listen.h"
 | |
| #include "fsmonitor--daemon.h"
 | |
| 
 | |
| #include "simple-ipc.h"
 | |
| #include "khash.h"
 | |
| #include "run-command.h"
 | |
| #include "trace.h"
 | |
| #include "trace2.h"
 | |
| 
 | |
| static const char * const builtin_fsmonitor__daemon_usage[] = {
 | |
| 	N_("git fsmonitor--daemon start [<options>]"),
 | |
| 	N_("git fsmonitor--daemon run [<options>]"),
 | |
| 	"git fsmonitor--daemon stop",
 | |
| 	"git fsmonitor--daemon status",
 | |
| 	NULL
 | |
| };
 | |
| 
 | |
| #ifdef HAVE_FSMONITOR_DAEMON_BACKEND
 | |
| /*
 | |
|  * Global state loaded from config.
 | |
|  */
 | |
| #define FSMONITOR__IPC_THREADS "fsmonitor.ipcthreads"
 | |
| static int fsmonitor__ipc_threads = 8;
 | |
| 
 | |
| #define FSMONITOR__START_TIMEOUT "fsmonitor.starttimeout"
 | |
| static int fsmonitor__start_timeout_sec = 60;
 | |
| 
 | |
| #define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup"
 | |
| static int fsmonitor__announce_startup = 0;
 | |
| 
 | |
| static int fsmonitor_config(const char *var, const char *value,
 | |
| 			    const struct config_context *ctx, void *cb)
 | |
| {
 | |
| 	if (!strcmp(var, FSMONITOR__IPC_THREADS)) {
 | |
| 		int i = git_config_int(var, value, ctx->kvi);
 | |
| 		if (i < 1)
 | |
| 			return error(_("value of '%s' out of range: %d"),
 | |
| 				     FSMONITOR__IPC_THREADS, i);
 | |
| 		fsmonitor__ipc_threads = i;
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	if (!strcmp(var, FSMONITOR__START_TIMEOUT)) {
 | |
| 		int i = git_config_int(var, value, ctx->kvi);
 | |
| 		if (i < 0)
 | |
| 			return error(_("value of '%s' out of range: %d"),
 | |
| 				     FSMONITOR__START_TIMEOUT, i);
 | |
| 		fsmonitor__start_timeout_sec = i;
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	if (!strcmp(var, FSMONITOR__ANNOUNCE_STARTUP)) {
 | |
| 		int is_bool;
 | |
| 		int i = git_config_bool_or_int(var, value, ctx->kvi, &is_bool);
 | |
| 		if (i < 0)
 | |
| 			return error(_("value of '%s' not bool or int: %d"),
 | |
| 				     var, i);
 | |
| 		fsmonitor__announce_startup = i;
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	return git_default_config(var, value, ctx, cb);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Acting as a CLIENT.
 | |
|  *
 | |
|  * Send a "quit" command to the `git-fsmonitor--daemon` (if running)
 | |
|  * and wait for it to shutdown.
 | |
|  */
 | |
| static int do_as_client__send_stop(void)
 | |
| {
 | |
| 	struct strbuf answer = STRBUF_INIT;
 | |
| 	int ret;
 | |
| 
 | |
| 	ret = fsmonitor_ipc__send_command("quit", &answer);
 | |
| 
 | |
| 	/* The quit command does not return any response data. */
 | |
| 	strbuf_release(&answer);
 | |
| 
 | |
| 	if (ret)
 | |
| 		return ret;
 | |
| 
 | |
| 	trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL);
 | |
| 	while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
 | |
| 		sleep_millisec(50);
 | |
| 	trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int do_as_client__status(void)
 | |
| {
 | |
| 	enum ipc_active_state state = fsmonitor_ipc__get_state();
 | |
| 
 | |
| 	switch (state) {
 | |
| 	case IPC_STATE__LISTENING:
 | |
| 		printf(_("fsmonitor-daemon is watching '%s'\n"),
 | |
| 		       the_repository->worktree);
 | |
| 		return 0;
 | |
| 
 | |
| 	default:
 | |
| 		printf(_("fsmonitor-daemon is not watching '%s'\n"),
 | |
| 		       the_repository->worktree);
 | |
| 		return 1;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| enum fsmonitor_cookie_item_result {
 | |
| 	FCIR_ERROR = -1, /* could not create cookie file ? */
 | |
| 	FCIR_INIT,
 | |
| 	FCIR_SEEN,
 | |
| 	FCIR_ABORT,
 | |
| };
 | |
| 
 | |
| struct fsmonitor_cookie_item {
 | |
| 	struct hashmap_entry entry;
 | |
| 	char *name;
 | |
| 	enum fsmonitor_cookie_item_result result;
 | |
| };
 | |
| 
 | |
| static int cookies_cmp(const void *data UNUSED,
 | |
| 		       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) {
 | |
| 		error_errno(_("could not create fsmonitor cookie '%s'"),
 | |
| 			    cookie->name);
 | |
| 
 | |
| 		cookie->result = FCIR_ERROR;
 | |
| 		goto done;
 | |
| 	}
 | |
| 
 | |
| 	/*
 | |
| 	 * Technically, close() and unlink() can fail, but we don't
 | |
| 	 * care here.  We only created the file to trigger a watch
 | |
| 	 * event from the FS to know that when we're up to date.
 | |
| 	 */
 | |
| 	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);
 | |
| 
 | |
| done:
 | |
| 	hashmap_remove(&state->cookies, &cookie->entry, NULL);
 | |
| 
 | |
| 	result = cookie->result;
 | |
| 
 | |
| 	free(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
 | |
|  * created/deleted/modified files relative to a token.  In the response,
 | |
|  * clients receive a new token for the next (relative) request.
 | |
|  *
 | |
|  *
 | |
|  * Token Format
 | |
|  * ============
 | |
|  *
 | |
|  * The contents of the token are private and provider-specific.
 | |
|  *
 | |
|  * For the built-in fsmonitor--daemon, we define a token as follows:
 | |
|  *
 | |
|  *     "builtin" ":" <token_id> ":" <sequence_nr>
 | |
|  *
 | |
|  * The "builtin" prefix is used as a namespace to avoid conflicts
 | |
|  * with other providers (such as Watchman).
 | |
|  *
 | |
|  * The <token_id> is an arbitrary OPAQUE string, such as a GUID,
 | |
|  * UUID, or {timestamp,pid}.  It is used to group all filesystem
 | |
|  * events that happened while the daemon was monitoring (and in-sync
 | |
|  * with the filesystem).
 | |
|  *
 | |
|  *     Unlike FSMonitor Protocol V1, it is not defined as a timestamp
 | |
|  *     and does not define less-than/greater-than relationships.
 | |
|  *     (There are too many race conditions to rely on file system
 | |
|  *     event timestamps.)
 | |
|  *
 | |
|  * The <sequence_nr> is a simple integer incremented whenever the
 | |
|  * daemon needs to make its state public.  For example, if 1000 file
 | |
|  * system events come in, but no clients have requested the data,
 | |
|  * the daemon can continue to accumulate file changes in the same
 | |
|  * bin and does not need to advance the sequence number.  However,
 | |
|  * as soon as a client does arrive, the daemon needs to start a new
 | |
|  * bin and increment the sequence number.
 | |
|  *
 | |
|  *     The sequence number serves as the boundary between 2 sets
 | |
|  *     of bins -- the older ones that the client has already seen
 | |
|  *     and the newer ones that it hasn't.
 | |
|  *
 | |
|  * When a new <token_id> is created, the <sequence_nr> is reset to
 | |
|  * zero.
 | |
|  *
 | |
|  *
 | |
|  * About Token Ids
 | |
|  * ===============
 | |
|  *
 | |
|  * A new token_id is created:
 | |
|  *
 | |
|  * [1] each time the daemon is started.
 | |
|  *
 | |
|  * [2] any time that the daemon must re-sync with the filesystem
 | |
|  *     (such as when the kernel drops or we miss events on a very
 | |
|  *     active volume).
 | |
|  *
 | |
|  * [3] in response to a client "flush" command (for dropped event
 | |
|  *     testing).
 | |
|  *
 | |
|  * When a new token_id is created, the daemon is free to discard all
 | |
|  * cached filesystem events associated with any previous token_ids.
 | |
|  * Events associated with a non-current token_id will never be sent
 | |
|  * to a client.  A token_id change implicitly means that the daemon
 | |
|  * has gap in its event history.
 | |
|  *
 | |
|  * Therefore, clients that present a token with a stale (non-current)
 | |
|  * token_id will always be given a trivial response.
 | |
|  */
 | |
| struct fsmonitor_token_data {
 | |
| 	struct strbuf token_id;
 | |
| 	struct fsmonitor_batch *batch_head;
 | |
| 	struct fsmonitor_batch *batch_tail;
 | |
| 	uint64_t client_ref_count;
 | |
| };
 | |
| 
 | |
| struct fsmonitor_batch {
 | |
| 	struct fsmonitor_batch *next;
 | |
| 	uint64_t batch_seq_nr;
 | |
| 	const char **interned_paths;
 | |
| 	size_t nr, alloc;
 | |
| 	time_t pinned_time;
 | |
| };
 | |
| 
 | |
| static struct fsmonitor_token_data *fsmonitor_new_token_data(void)
 | |
| {
 | |
| 	static int test_env_value = -1;
 | |
| 	static uint64_t flush_count = 0;
 | |
| 	struct fsmonitor_token_data *token;
 | |
| 	struct fsmonitor_batch *batch;
 | |
| 
 | |
| 	CALLOC_ARRAY(token, 1);
 | |
| 	batch = fsmonitor_batch__new();
 | |
| 
 | |
| 	strbuf_init(&token->token_id, 0);
 | |
| 	token->batch_head = batch;
 | |
| 	token->batch_tail = batch;
 | |
| 	token->client_ref_count = 0;
 | |
| 
 | |
| 	if (test_env_value < 0)
 | |
| 		test_env_value = git_env_bool("GIT_TEST_FSMONITOR_TOKEN", 0);
 | |
| 
 | |
| 	if (!test_env_value) {
 | |
| 		struct timeval tv;
 | |
| 		struct tm tm;
 | |
| 		time_t secs;
 | |
| 
 | |
| 		gettimeofday(&tv, NULL);
 | |
| 		secs = tv.tv_sec;
 | |
| 		gmtime_r(&secs, &tm);
 | |
| 
 | |
| 		strbuf_addf(&token->token_id,
 | |
| 			    "%"PRIu64".%d.%4d%02d%02dT%02d%02d%02d.%06ldZ",
 | |
| 			    flush_count++,
 | |
| 			    getpid(),
 | |
| 			    tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
 | |
| 			    tm.tm_hour, tm.tm_min, tm.tm_sec,
 | |
| 			    (long)tv.tv_usec);
 | |
| 	} else {
 | |
| 		strbuf_addf(&token->token_id, "test_%08x", test_env_value++);
 | |
| 	}
 | |
| 
 | |
| 	/*
 | |
| 	 * We created a new <token_id> and are starting a new series
 | |
| 	 * of tokens with a zero <seq_nr>.
 | |
| 	 *
 | |
| 	 * Since clients cannot guess our new (non test) <token_id>
 | |
| 	 * they will always receive a trivial response (because of the
 | |
| 	 * mismatch on the <token_id>).  The trivial response will
 | |
| 	 * tell them our new <token_id> so that subsequent requests
 | |
| 	 * will be relative to our new series.  (And when sending that
 | |
| 	 * response, we pin the current head of the batch list.)
 | |
| 	 *
 | |
| 	 * Even if the client correctly guesses the <token_id>, their
 | |
| 	 * request of "builtin:<token_id>:0" asks for all changes MORE
 | |
| 	 * RECENT than batch/bin 0.
 | |
| 	 *
 | |
| 	 * This implies that it is a waste to accumulate paths in the
 | |
| 	 * initial batch/bin (because they will never be transmitted).
 | |
| 	 *
 | |
| 	 * So the daemon could be running for days and watching the
 | |
| 	 * file system, but doesn't need to actually accumulate any
 | |
| 	 * paths UNTIL we need to set a reference point for a later
 | |
| 	 * relative request.
 | |
| 	 *
 | |
| 	 * However, it is very useful for testing to always have a
 | |
| 	 * reference point set.  Pin batch 0 to force early file system
 | |
| 	 * events to accumulate.
 | |
| 	 */
 | |
| 	if (test_env_value)
 | |
| 		batch->pinned_time = time(NULL);
 | |
| 
 | |
| 	return token;
 | |
| }
 | |
| 
 | |
| struct fsmonitor_batch *fsmonitor_batch__new(void)
 | |
| {
 | |
| 	struct fsmonitor_batch *batch;
 | |
| 
 | |
| 	CALLOC_ARRAY(batch, 1);
 | |
| 
 | |
| 	return batch;
 | |
| }
 | |
| 
 | |
| void fsmonitor_batch__free_list(struct fsmonitor_batch *batch)
 | |
| {
 | |
| 	while (batch) {
 | |
| 		struct fsmonitor_batch *next = batch->next;
 | |
| 
 | |
| 		/*
 | |
| 		 * The actual strings within the array of this batch
 | |
| 		 * are interned, so we don't own them.  We only own
 | |
| 		 * the array.
 | |
| 		 */
 | |
| 		free(batch->interned_paths);
 | |
| 		free(batch);
 | |
| 
 | |
| 		batch = next;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void fsmonitor_batch__add_path(struct fsmonitor_batch *batch,
 | |
| 			       const char *path)
 | |
| {
 | |
| 	const char *interned_path = strintern(path);
 | |
| 
 | |
| 	trace_printf_key(&trace_fsmonitor, "event: %s", interned_path);
 | |
| 
 | |
| 	ALLOC_GROW(batch->interned_paths, batch->nr + 1, batch->alloc);
 | |
| 	batch->interned_paths[batch->nr++] = interned_path;
 | |
| }
 | |
| 
 | |
| static void fsmonitor_batch__combine(struct fsmonitor_batch *batch_dest,
 | |
| 				     const struct fsmonitor_batch *batch_src)
 | |
| {
 | |
| 	size_t k;
 | |
| 
 | |
| 	ALLOC_GROW(batch_dest->interned_paths,
 | |
| 		   batch_dest->nr + batch_src->nr + 1,
 | |
| 		   batch_dest->alloc);
 | |
| 
 | |
| 	for (k = 0; k < batch_src->nr; k++)
 | |
| 		batch_dest->interned_paths[batch_dest->nr++] =
 | |
| 			batch_src->interned_paths[k];
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * To keep the batch list from growing unbounded in response to filesystem
 | |
|  * activity, we try to truncate old batches from the end of the list as
 | |
|  * they become irrelevant.
 | |
|  *
 | |
|  * We assume that the .git/index will be updated with the most recent token
 | |
|  * any time the index is updated.  And future commands will only ask for
 | |
|  * recent changes *since* that new token.  So as tokens advance into the
 | |
|  * future, older batch items will never be requested/needed.  So we can
 | |
|  * truncate them without loss of functionality.
 | |
|  *
 | |
|  * However, multiple commands may be talking to the daemon concurrently
 | |
|  * or perform a slow command, so a little "token skew" is possible.
 | |
|  * Therefore, we want this to be a little bit lazy and have a generous
 | |
|  * delay.
 | |
|  *
 | |
|  * The current reader thread walked backwards in time from `token->batch_head`
 | |
|  * back to `batch_marker` somewhere in the middle of the batch list.
 | |
|  *
 | |
|  * Let's walk backwards in time from that marker an arbitrary delay
 | |
|  * and truncate the list there.  Note that these timestamps are completely
 | |
|  * artificial (based on when we pinned the batch item) and not on any
 | |
|  * filesystem activity.
 | |
|  *
 | |
|  * Return the obsolete portion of the list after we have removed it from
 | |
|  * the official list so that the caller can free it after leaving the lock.
 | |
|  */
 | |
| #define MY_TIME_DELAY_SECONDS (5 * 60) /* seconds */
 | |
| 
 | |
| static struct fsmonitor_batch *with_lock__truncate_old_batches(
 | |
| 	struct fsmonitor_daemon_state *state,
 | |
| 	const struct fsmonitor_batch *batch_marker)
 | |
| {
 | |
| 	/* assert current thread holding state->main_lock */
 | |
| 
 | |
| 	const struct fsmonitor_batch *batch;
 | |
| 	struct fsmonitor_batch *remainder;
 | |
| 
 | |
| 	if (!batch_marker)
 | |
| 		return NULL;
 | |
| 
 | |
| 	trace_printf_key(&trace_fsmonitor, "Truncate: mark (%"PRIu64",%"PRIu64")",
 | |
| 			 batch_marker->batch_seq_nr,
 | |
| 			 (uint64_t)batch_marker->pinned_time);
 | |
| 
 | |
| 	for (batch = batch_marker; batch; batch = batch->next) {
 | |
| 		time_t t;
 | |
| 
 | |
| 		if (!batch->pinned_time) /* an overflow batch */
 | |
| 			continue;
 | |
| 
 | |
| 		t = batch->pinned_time + MY_TIME_DELAY_SECONDS;
 | |
| 		if (t > batch_marker->pinned_time) /* too close to marker */
 | |
| 			continue;
 | |
| 
 | |
| 		goto truncate_past_here;
 | |
| 	}
 | |
| 
 | |
| 	return NULL;
 | |
| 
 | |
| truncate_past_here:
 | |
| 	state->current_token_data->batch_tail = (struct fsmonitor_batch *)batch;
 | |
| 
 | |
| 	remainder = ((struct fsmonitor_batch *)batch)->next;
 | |
| 	((struct fsmonitor_batch *)batch)->next = NULL;
 | |
| 
 | |
| 	return remainder;
 | |
| }
 | |
| 
 | |
| static void fsmonitor_free_token_data(struct fsmonitor_token_data *token)
 | |
| {
 | |
| 	if (!token)
 | |
| 		return;
 | |
| 
 | |
| 	assert(token->client_ref_count == 0);
 | |
| 
 | |
| 	strbuf_release(&token->token_id);
 | |
| 
 | |
| 	fsmonitor_batch__free_list(token->batch_head);
 | |
| 
 | |
| 	free(token);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Flush all of our cached data about the filesystem.  Call this if we
 | |
|  * lose sync with the filesystem and miss some notification events.
 | |
|  *
 | |
|  * [1] If we are missing events, then we no longer have a complete
 | |
|  *     history of the directory (relative to our current start 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.
 | |
|  *
 | |
|  * Either way, the old token data series is no longer associated with
 | |
|  * our state data.
 | |
|  */
 | |
| static void with_lock__do_force_resync(struct fsmonitor_daemon_state *state)
 | |
| {
 | |
| 	/* assert current thread holding state->main_lock */
 | |
| 
 | |
| 	struct fsmonitor_token_data *free_me = NULL;
 | |
| 	struct fsmonitor_token_data *new_one = NULL;
 | |
| 
 | |
| 	new_one = fsmonitor_new_token_data();
 | |
| 
 | |
| 	if (state->current_token_data->client_ref_count == 0)
 | |
| 		free_me = state->current_token_data;
 | |
| 	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)
 | |
| {
 | |
| 	pthread_mutex_lock(&state->main_lock);
 | |
| 	with_lock__do_force_resync(state);
 | |
| 	pthread_mutex_unlock(&state->main_lock);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Format an opaque token string to send to the client.
 | |
|  */
 | |
| static void with_lock__format_response_token(
 | |
| 	struct strbuf *response_token,
 | |
| 	const struct strbuf *response_token_id,
 | |
| 	const struct fsmonitor_batch *batch)
 | |
| {
 | |
| 	/* assert current thread holding state->main_lock */
 | |
| 
 | |
| 	strbuf_reset(response_token);
 | |
| 	strbuf_addf(response_token, "builtin:%s:%"PRIu64,
 | |
| 		    response_token_id->buf, batch->batch_seq_nr);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Parse an opaque token from the client.
 | |
|  * Returns -1 on error.
 | |
|  */
 | |
| static int fsmonitor_parse_client_token(const char *buf_token,
 | |
| 					struct strbuf *requested_token_id,
 | |
| 					uint64_t *seq_nr)
 | |
| {
 | |
| 	const char *p;
 | |
| 	char *p_end;
 | |
| 
 | |
| 	strbuf_reset(requested_token_id);
 | |
| 	*seq_nr = 0;
 | |
| 
 | |
| 	if (!skip_prefix(buf_token, "builtin:", &p))
 | |
| 		return -1;
 | |
| 
 | |
| 	while (*p && *p != ':')
 | |
| 		strbuf_addch(requested_token_id, *p++);
 | |
| 	if (!*p++)
 | |
| 		return -1;
 | |
| 
 | |
| 	*seq_nr = (uint64_t)strtoumax(p, &p_end, 10);
 | |
| 	if (*p_end)
 | |
| 		return -1;
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal)
 | |
| 
 | |
| static int do_handle_client(struct fsmonitor_daemon_state *state,
 | |
| 			    const char *command,
 | |
| 			    ipc_server_reply_cb *reply,
 | |
| 			    struct ipc_server_reply_data *reply_data)
 | |
| {
 | |
| 	struct fsmonitor_token_data *token_data = NULL;
 | |
| 	struct strbuf response_token = STRBUF_INIT;
 | |
| 	struct strbuf requested_token_id = STRBUF_INIT;
 | |
| 	struct strbuf payload = STRBUF_INIT;
 | |
| 	uint64_t requested_oldest_seq_nr = 0;
 | |
| 	uint64_t total_response_len = 0;
 | |
| 	const char *p;
 | |
| 	const struct fsmonitor_batch *batch_head;
 | |
| 	const struct fsmonitor_batch *batch;
 | |
| 	struct fsmonitor_batch *remainder = NULL;
 | |
| 	intmax_t count = 0, duplicates = 0;
 | |
| 	kh_str_t *shown;
 | |
| 	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:
 | |
| 	 *
 | |
| 	 * <command> := quit NUL
 | |
| 	 *            | flush NUL
 | |
| 	 *            | <V1-time-since-epoch-ns> NUL
 | |
| 	 *            | <V2-opaque-fsmonitor-token> NUL
 | |
| 	 */
 | |
| 
 | |
| 	if (!strcmp(command, "quit")) {
 | |
| 		/*
 | |
| 		 * A client has requested over the socket/pipe that the
 | |
| 		 * daemon shutdown.
 | |
| 		 *
 | |
| 		 * Tell the IPC thread pool to shutdown (which completes
 | |
| 		 * the await in the main thread (which can stop the
 | |
| 		 * fsmonitor listener thread)).
 | |
| 		 *
 | |
| 		 * There is no reply to the client.
 | |
| 		 */
 | |
| 		return SIMPLE_IPC_QUIT;
 | |
| 
 | |
| 	} else if (!strcmp(command, "flush")) {
 | |
| 		/*
 | |
| 		 * Flush all of our cached data and generate a new token
 | |
| 		 * just like if we lost sync with the filesystem.
 | |
| 		 *
 | |
| 		 * Then send a trivial response using the new token.
 | |
| 		 */
 | |
| 		do_flush = 1;
 | |
| 		do_trivial = 1;
 | |
| 
 | |
| 	} else if (!skip_prefix(command, "builtin:", &p)) {
 | |
| 		/* assume V1 timestamp or garbage */
 | |
| 
 | |
| 		char *p_end;
 | |
| 
 | |
| 		strtoumax(command, &p_end, 10);
 | |
| 		trace_printf_key(&trace_fsmonitor,
 | |
| 				 ((*p_end) ?
 | |
| 				  "fsmonitor: invalid command line '%s'" :
 | |
| 				  "fsmonitor: unsupported V1 protocol '%s'"),
 | |
| 				 command);
 | |
| 		do_trivial = 1;
 | |
| 		do_cookie = 1;
 | |
| 
 | |
| 	} else {
 | |
| 		/* We have "builtin:*" */
 | |
| 		if (fsmonitor_parse_client_token(command, &requested_token_id,
 | |
| 						 &requested_oldest_seq_nr)) {
 | |
| 			trace_printf_key(&trace_fsmonitor,
 | |
| 					 "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;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	pthread_mutex_lock(&state->main_lock);
 | |
| 
 | |
| 	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);
 | |
| 
 | |
| 	/*
 | |
| 	 * We mark the current head of the batch list as "pinned" so
 | |
| 	 * that the listener thread will treat this item as read-only
 | |
| 	 * (and prevent any more paths from being added to it) from
 | |
| 	 * now on.
 | |
| 	 */
 | |
| 	token_data = state->current_token_data;
 | |
| 	batch_head = token_data->batch_head;
 | |
| 	((struct fsmonitor_batch *)batch_head)->pinned_time = time(NULL);
 | |
| 
 | |
| 	/*
 | |
| 	 * FSMonitor Protocol V2 requires that we send a response header
 | |
| 	 * with a "new current token" and then all of the paths that changed
 | |
| 	 * since the "requested token".  We send the seq_nr of the just-pinned
 | |
| 	 * head batch so that future requests from a client will be relative
 | |
| 	 * to it.
 | |
| 	 */
 | |
| 	with_lock__format_response_token(&response_token,
 | |
| 					 &token_data->token_id, batch_head);
 | |
| 
 | |
| 	reply(reply_data, response_token.buf, response_token.len + 1);
 | |
| 	total_response_len += response_token.len + 1;
 | |
| 
 | |
| 	trace2_data_string("fsmonitor", the_repository, "response/token",
 | |
| 			   response_token.buf);
 | |
| 	trace_printf_key(&trace_fsmonitor, "response token: %s",
 | |
| 			 response_token.buf);
 | |
| 
 | |
| 	if (!do_trivial) {
 | |
| 		if (strcmp(requested_token_id.buf, token_data->token_id.buf)) {
 | |
| 			/*
 | |
| 			 * The client last spoke to a different daemon
 | |
| 			 * instance -OR- the daemon had to resync with
 | |
| 			 * the filesystem (and lost events), so reject.
 | |
| 			 */
 | |
| 			trace2_data_string("fsmonitor", the_repository,
 | |
| 					   "response/token", "different");
 | |
| 			do_trivial = 1;
 | |
| 
 | |
| 		} else if (requested_oldest_seq_nr <
 | |
| 			   token_data->batch_tail->batch_seq_nr) {
 | |
| 			/*
 | |
| 			 * The client wants older events than we have for
 | |
| 			 * this token_id.  This means that the end of our
 | |
| 			 * batch list was truncated and we cannot give the
 | |
| 			 * client a complete snapshot relative to their
 | |
| 			 * request.
 | |
| 			 */
 | |
| 			trace_printf_key(&trace_fsmonitor,
 | |
| 					 "client requested truncated data");
 | |
| 			do_trivial = 1;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (do_trivial) {
 | |
| 		pthread_mutex_unlock(&state->main_lock);
 | |
| 
 | |
| 		reply(reply_data, "/", 2);
 | |
| 
 | |
| 		trace2_data_intmax("fsmonitor", the_repository,
 | |
| 				   "response/trivial", 1);
 | |
| 
 | |
| 		goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	/*
 | |
| 	 * We're going to hold onto a pointer to the current
 | |
| 	 * token-data while we walk the list of batches of files.
 | |
| 	 * During this time, we will NOT be under the lock.
 | |
| 	 * So we ref-count it.
 | |
| 	 *
 | |
| 	 * This allows the listener thread to continue prepending
 | |
| 	 * new batches of items to the token-data (which we'll ignore).
 | |
| 	 *
 | |
| 	 * AND it allows the listener thread to do a token-reset
 | |
| 	 * (and install a new `current_token_data`).
 | |
| 	 */
 | |
| 	token_data->client_ref_count++;
 | |
| 
 | |
| 	pthread_mutex_unlock(&state->main_lock);
 | |
| 
 | |
| 	/*
 | |
| 	 * The client request is relative to the token that they sent,
 | |
| 	 * so walk the batch list backwards from the current head back
 | |
| 	 * to the batch (sequence number) they named.
 | |
| 	 *
 | |
| 	 * We use khash to de-dup the list of pathnames.
 | |
| 	 *
 | |
| 	 * NEEDSWORK: each batch contains a list of interned strings,
 | |
| 	 * so we only need to do pointer comparisons here to build the
 | |
| 	 * hash table.  Currently, we're still comparing the string
 | |
| 	 * values.
 | |
| 	 */
 | |
| 	shown = kh_init_str();
 | |
| 	for (batch = batch_head;
 | |
| 	     batch && batch->batch_seq_nr > requested_oldest_seq_nr;
 | |
| 	     batch = batch->next) {
 | |
| 		size_t k;
 | |
| 
 | |
| 		for (k = 0; k < batch->nr; k++) {
 | |
| 			const char *s = batch->interned_paths[k];
 | |
| 			size_t s_len;
 | |
| 
 | |
| 			if (kh_get_str(shown, s) != kh_end(shown))
 | |
| 				duplicates++;
 | |
| 			else {
 | |
| 				kh_put_str(shown, s, &hash_ret);
 | |
| 
 | |
| 				trace_printf_key(&trace_fsmonitor,
 | |
| 						 "send[%"PRIuMAX"]: %s",
 | |
| 						 count, s);
 | |
| 
 | |
| 				/* Each path gets written with a trailing NUL */
 | |
| 				s_len = strlen(s) + 1;
 | |
| 
 | |
| 				if (payload.len + s_len >=
 | |
| 				    LARGE_PACKET_DATA_MAX) {
 | |
| 					reply(reply_data, payload.buf,
 | |
| 					      payload.len);
 | |
| 					total_response_len += payload.len;
 | |
| 					strbuf_reset(&payload);
 | |
| 				}
 | |
| 
 | |
| 				strbuf_add(&payload, s, s_len);
 | |
| 				count++;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (payload.len) {
 | |
| 		reply(reply_data, payload.buf, payload.len);
 | |
| 		total_response_len += payload.len;
 | |
| 	}
 | |
| 
 | |
| 	kh_release_str(shown);
 | |
| 
 | |
| 	pthread_mutex_lock(&state->main_lock);
 | |
| 
 | |
| 	if (token_data->client_ref_count > 0)
 | |
| 		token_data->client_ref_count--;
 | |
| 
 | |
| 	if (token_data->client_ref_count == 0) {
 | |
| 		if (token_data != state->current_token_data) {
 | |
| 			/*
 | |
| 			 * The listener thread did a token-reset while we were
 | |
| 			 * walking the batch list.  Therefore, this token is
 | |
| 			 * stale and can be discarded completely.  If we are
 | |
| 			 * the last reader thread using this token, we own
 | |
| 			 * that work.
 | |
| 			 */
 | |
| 			fsmonitor_free_token_data(token_data);
 | |
| 		} else if (batch) {
 | |
| 			/*
 | |
| 			 * We are holding the lock and are the only
 | |
| 			 * reader of the ref-counted portion of the
 | |
| 			 * list, so we get the honor of seeing if the
 | |
| 			 * list can be truncated to save memory.
 | |
| 			 *
 | |
| 			 * The main loop did not walk to the end of the
 | |
| 			 * list, so this batch is the first item in the
 | |
| 			 * batch-list that is older than the requested
 | |
| 			 * end-point sequence number.  See if the tail
 | |
| 			 * end of the list is obsolete.
 | |
| 			 */
 | |
| 			remainder = with_lock__truncate_old_batches(state,
 | |
| 								    batch);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	pthread_mutex_unlock(&state->main_lock);
 | |
| 
 | |
| 	if (remainder)
 | |
| 		fsmonitor_batch__free_list(remainder);
 | |
| 
 | |
| 	trace2_data_intmax("fsmonitor", the_repository, "response/length", total_response_len);
 | |
| 	trace2_data_intmax("fsmonitor", the_repository, "response/count/files", count);
 | |
| 	trace2_data_intmax("fsmonitor", the_repository, "response/count/duplicates", duplicates);
 | |
| 
 | |
| cleanup:
 | |
| 	strbuf_release(&response_token);
 | |
| 	strbuf_release(&requested_token_id);
 | |
| 	strbuf_release(&payload);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static ipc_server_application_cb handle_client;
 | |
| 
 | |
| static int handle_client(void *data,
 | |
| 			 const char *command, size_t command_len,
 | |
| 			 ipc_server_reply_cb *reply,
 | |
| 			 struct ipc_server_reply_data *reply_data)
 | |
| {
 | |
| 	struct fsmonitor_daemon_state *state = data;
 | |
| 	int result;
 | |
| 
 | |
| 	/*
 | |
| 	 * The Simple IPC API now supports {char*, len} arguments, but
 | |
| 	 * FSMonitor always uses proper null-terminated strings, so
 | |
| 	 * we can ignore the command_len argument.  (Trust, but verify.)
 | |
| 	 */
 | |
| 	if (command_len != strlen(command))
 | |
| 		BUG("FSMonitor assumes text messages");
 | |
| 
 | |
| 	trace_printf_key(&trace_fsmonitor, "requested token: %s", command);
 | |
| 
 | |
| 	trace2_region_enter("fsmonitor", "handle_client", the_repository);
 | |
| 	trace2_data_string("fsmonitor", the_repository, "request", command);
 | |
| 
 | |
| 	result = do_handle_client(state, command, reply, reply_data);
 | |
| 
 | |
| 	trace2_region_leave("fsmonitor", "handle_client", the_repository);
 | |
| 
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| #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)
 | |
| {
 | |
| 	if (fspathncmp(rel, ".git", 4))
 | |
| 		return IS_WORKDIR_PATH;
 | |
| 	rel += 4;
 | |
| 
 | |
| 	if (!*rel)
 | |
| 		return IS_DOT_GIT;
 | |
| 	if (*rel != '/')
 | |
| 		return IS_WORKDIR_PATH; /* e.g. .gitignore */
 | |
| 	rel++;
 | |
| 
 | |
| 	if (!fspathncmp(rel, FSMONITOR_COOKIE_PREFIX,
 | |
| 			strlen(FSMONITOR_COOKIE_PREFIX)))
 | |
| 		return IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX;
 | |
| 
 | |
| 	return IS_INSIDE_DOT_GIT;
 | |
| }
 | |
| 
 | |
| enum fsmonitor_path_type fsmonitor_classify_path_gitdir_relative(
 | |
| 	const char *rel)
 | |
| {
 | |
| 	if (!fspathncmp(rel, FSMONITOR_COOKIE_PREFIX,
 | |
| 			strlen(FSMONITOR_COOKIE_PREFIX)))
 | |
| 		return IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX;
 | |
| 
 | |
| 	return IS_INSIDE_GITDIR;
 | |
| }
 | |
| 
 | |
| static enum fsmonitor_path_type try_classify_workdir_abs_path(
 | |
| 	struct fsmonitor_daemon_state *state,
 | |
| 	const char *path)
 | |
| {
 | |
| 	const char *rel;
 | |
| 
 | |
| 	if (fspathncmp(path, state->path_worktree_watch.buf,
 | |
| 		       state->path_worktree_watch.len))
 | |
| 		return IS_OUTSIDE_CONE;
 | |
| 
 | |
| 	rel = path + state->path_worktree_watch.len;
 | |
| 
 | |
| 	if (!*rel)
 | |
| 		return IS_WORKDIR_PATH; /* it is the root dir exactly */
 | |
| 	if (*rel != '/')
 | |
| 		return IS_OUTSIDE_CONE;
 | |
| 	rel++;
 | |
| 
 | |
| 	return fsmonitor_classify_path_workdir_relative(rel);
 | |
| }
 | |
| 
 | |
| enum fsmonitor_path_type fsmonitor_classify_path_absolute(
 | |
| 	struct fsmonitor_daemon_state *state,
 | |
| 	const char *path)
 | |
| {
 | |
| 	const char *rel;
 | |
| 	enum fsmonitor_path_type t;
 | |
| 
 | |
| 	t = try_classify_workdir_abs_path(state, path);
 | |
| 	if (state->nr_paths_watching == 1)
 | |
| 		return t;
 | |
| 	if (t != IS_OUTSIDE_CONE)
 | |
| 		return t;
 | |
| 
 | |
| 	if (fspathncmp(path, state->path_gitdir_watch.buf,
 | |
| 		       state->path_gitdir_watch.len))
 | |
| 		return IS_OUTSIDE_CONE;
 | |
| 
 | |
| 	rel = path + state->path_gitdir_watch.len;
 | |
| 
 | |
| 	if (!*rel)
 | |
| 		return IS_GITDIR; /* it is the <gitdir> exactly */
 | |
| 	if (*rel != '/')
 | |
| 		return IS_OUTSIDE_CONE;
 | |
| 	rel++;
 | |
| 
 | |
| 	return fsmonitor_classify_path_gitdir_relative(rel);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * We try to combine small batches at the front of the batch-list to avoid
 | |
|  * having a long list.  This hopefully makes it a little easier when we want
 | |
|  * to truncate and maintain the list.  However, we don't want the paths array
 | |
|  * to just keep growing and growing with realloc, so we insert an arbitrary
 | |
|  * limit.
 | |
|  */
 | |
| #define MY_COMBINE_LIMIT (1024)
 | |
| 
 | |
| void fsmonitor_publish(struct fsmonitor_daemon_state *state,
 | |
| 		       struct fsmonitor_batch *batch,
 | |
| 		       const struct string_list *cookie_names)
 | |
| {
 | |
| 	if (!batch && !cookie_names->nr)
 | |
| 		return;
 | |
| 
 | |
| 	pthread_mutex_lock(&state->main_lock);
 | |
| 
 | |
| 	if (batch) {
 | |
| 		struct fsmonitor_batch *head;
 | |
| 
 | |
| 		head = state->current_token_data->batch_head;
 | |
| 		if (!head) {
 | |
| 			BUG("token does not have batch");
 | |
| 		} else if (head->pinned_time) {
 | |
| 			/*
 | |
| 			 * We cannot alter the current batch list
 | |
| 			 * because:
 | |
| 			 *
 | |
| 			 * [a] it is being transmitted to at least one
 | |
| 			 * client and the handle_client() thread has a
 | |
| 			 * ref-count, but not a lock on the batch list
 | |
| 			 * starting with this item.
 | |
| 			 *
 | |
| 			 * [b] it has been transmitted in the past to
 | |
| 			 * at least one client such that future
 | |
| 			 * requests are relative to this head batch.
 | |
| 			 *
 | |
| 			 * So, we can only prepend a new batch onto
 | |
| 			 * the front of the list.
 | |
| 			 */
 | |
| 			batch->batch_seq_nr = head->batch_seq_nr + 1;
 | |
| 			batch->next = head;
 | |
| 			state->current_token_data->batch_head = batch;
 | |
| 		} else if (!head->batch_seq_nr) {
 | |
| 			/*
 | |
| 			 * Batch 0 is unpinned.  See the note in
 | |
| 			 * `fsmonitor_new_token_data()` about why we
 | |
| 			 * don't need to accumulate these paths.
 | |
| 			 */
 | |
| 			fsmonitor_batch__free_list(batch);
 | |
| 		} else if (head->nr + batch->nr > MY_COMBINE_LIMIT) {
 | |
| 			/*
 | |
| 			 * The head batch in the list has never been
 | |
| 			 * transmitted to a client, but folding the
 | |
| 			 * contents of the new batch onto it would
 | |
| 			 * exceed our arbitrary limit, so just prepend
 | |
| 			 * the new batch onto the list.
 | |
| 			 */
 | |
| 			batch->batch_seq_nr = head->batch_seq_nr + 1;
 | |
| 			batch->next = head;
 | |
| 			state->current_token_data->batch_head = batch;
 | |
| 		} else {
 | |
| 			/*
 | |
| 			 * We are free to add the paths in the given
 | |
| 			 * batch onto the end of the current head batch.
 | |
| 			 */
 | |
| 			fsmonitor_batch__combine(head, batch);
 | |
| 			fsmonitor_batch__free_list(batch);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (cookie_names->nr)
 | |
| 		with_lock__mark_cookies_seen(state, cookie_names);
 | |
| 
 | |
| 	pthread_mutex_unlock(&state->main_lock);
 | |
| }
 | |
| 
 | |
| static void *fsm_health__thread_proc(void *_state)
 | |
| {
 | |
| 	struct fsmonitor_daemon_state *state = _state;
 | |
| 
 | |
| 	trace2_thread_start("fsm-health");
 | |
| 
 | |
| 	fsm_health__loop(state);
 | |
| 
 | |
| 	trace2_thread_exit();
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static void *fsm_listen__thread_proc(void *_state)
 | |
| {
 | |
| 	struct fsmonitor_daemon_state *state = _state;
 | |
| 
 | |
| 	trace2_thread_start("fsm-listen");
 | |
| 
 | |
| 	trace_printf_key(&trace_fsmonitor, "Watching: worktree '%s'",
 | |
| 			 state->path_worktree_watch.buf);
 | |
| 	if (state->nr_paths_watching > 1)
 | |
| 		trace_printf_key(&trace_fsmonitor, "Watching: gitdir '%s'",
 | |
| 				 state->path_gitdir_watch.buf);
 | |
| 
 | |
| 	fsm_listen__loop(state);
 | |
| 
 | |
| 	pthread_mutex_lock(&state->main_lock);
 | |
| 	if (state->current_token_data &&
 | |
| 	    state->current_token_data->client_ref_count == 0)
 | |
| 		fsmonitor_free_token_data(state->current_token_data);
 | |
| 	state->current_token_data = NULL;
 | |
| 	pthread_mutex_unlock(&state->main_lock);
 | |
| 
 | |
| 	trace2_thread_exit();
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state)
 | |
| {
 | |
| 	struct ipc_server_opts ipc_opts = {
 | |
| 		.nr_threads = fsmonitor__ipc_threads,
 | |
| 
 | |
| 		/*
 | |
| 		 * We know that there are no other active threads yet,
 | |
| 		 * so we can let the IPC layer temporarily chdir() if
 | |
| 		 * it needs to when creating the server side of the
 | |
| 		 * Unix domain socket.
 | |
| 		 */
 | |
| 		.uds_disallow_chdir = 0
 | |
| 	};
 | |
| 	int health_started = 0;
 | |
| 	int listener_started = 0;
 | |
| 	int err = 0;
 | |
| 
 | |
| 	/*
 | |
| 	 * Start the IPC thread pool before the we've started the file
 | |
| 	 * system event listener thread so that we have the IPC handle
 | |
| 	 * before we need it.
 | |
| 	 */
 | |
| 	if (ipc_server_init_async(&state->ipc_server_data,
 | |
| 				  state->path_ipc.buf, &ipc_opts,
 | |
| 				  handle_client, state))
 | |
| 		return error_errno(
 | |
| 			_("could not start IPC thread pool on '%s'"),
 | |
| 			state->path_ipc.buf);
 | |
| 
 | |
| 	/*
 | |
| 	 * Start the fsmonitor listener thread to collect filesystem
 | |
| 	 * events.
 | |
| 	 */
 | |
| 	if (pthread_create(&state->listener_thread, NULL,
 | |
| 			   fsm_listen__thread_proc, state)) {
 | |
| 		ipc_server_stop_async(state->ipc_server_data);
 | |
| 		err = error(_("could not start fsmonitor listener thread"));
 | |
| 		goto cleanup;
 | |
| 	}
 | |
| 	listener_started = 1;
 | |
| 
 | |
| 	/*
 | |
| 	 * Start the health thread to watch over our process.
 | |
| 	 */
 | |
| 	if (pthread_create(&state->health_thread, NULL,
 | |
| 			   fsm_health__thread_proc, state)) {
 | |
| 		ipc_server_stop_async(state->ipc_server_data);
 | |
| 		err = error(_("could not start fsmonitor health thread"));
 | |
| 		goto cleanup;
 | |
| 	}
 | |
| 	health_started = 1;
 | |
| 
 | |
| 	/*
 | |
| 	 * The daemon is now fully functional in background threads.
 | |
| 	 * Our primary thread should now just wait while the threads
 | |
| 	 * do all the work.
 | |
| 	 */
 | |
| cleanup:
 | |
| 	/*
 | |
| 	 * Wait for the IPC thread pool to shutdown (whether by client
 | |
| 	 * request, from filesystem activity, or an error).
 | |
| 	 */
 | |
| 	ipc_server_await(state->ipc_server_data);
 | |
| 
 | |
| 	/*
 | |
| 	 * The fsmonitor listener thread may have received a shutdown
 | |
| 	 * event from the IPC thread pool, but it doesn't hurt to tell
 | |
| 	 * it again.  And wait for it to shutdown.
 | |
| 	 */
 | |
| 	if (listener_started) {
 | |
| 		fsm_listen__stop_async(state);
 | |
| 		pthread_join(state->listener_thread, NULL);
 | |
| 	}
 | |
| 
 | |
| 	if (health_started) {
 | |
| 		fsm_health__stop_async(state);
 | |
| 		pthread_join(state->health_thread, NULL);
 | |
| 	}
 | |
| 
 | |
| 	if (err)
 | |
| 		return err;
 | |
| 	if (state->listen_error_code)
 | |
| 		return state->listen_error_code;
 | |
| 	if (state->health_error_code)
 | |
| 		return state->health_error_code;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int fsmonitor_run_daemon(void)
 | |
| {
 | |
| 	struct fsmonitor_daemon_state state;
 | |
| 	const char *home;
 | |
| 	int err;
 | |
| 
 | |
| 	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.listen_error_code = 0;
 | |
| 	state.health_error_code = 0;
 | |
| 	state.current_token_data = fsmonitor_new_token_data();
 | |
| 
 | |
| 	/* Prepare to (recursively) watch the <worktree-root> directory. */
 | |
| 	strbuf_init(&state.path_worktree_watch, 0);
 | |
| 	strbuf_addstr(&state.path_worktree_watch,
 | |
| 		      absolute_path(repo_get_work_tree(the_repository)));
 | |
| 	state.nr_paths_watching = 1;
 | |
| 
 | |
| 	strbuf_init(&state.alias.alias, 0);
 | |
| 	strbuf_init(&state.alias.points_to, 0);
 | |
| 	if ((err = fsmonitor__get_alias(state.path_worktree_watch.buf, &state.alias)))
 | |
| 		goto done;
 | |
| 
 | |
| 	/*
 | |
| 	 * We create and delete cookie files somewhere inside the .git
 | |
| 	 * directory to help us keep sync with the file system.  If
 | |
| 	 * ".git" is not a directory, then <gitdir> is not inside the
 | |
| 	 * cone of <worktree-root>, so set up a second watch to watch
 | |
| 	 * the <gitdir> so that we get events for the cookie files.
 | |
| 	 */
 | |
| 	strbuf_init(&state.path_gitdir_watch, 0);
 | |
| 	strbuf_addbuf(&state.path_gitdir_watch, &state.path_worktree_watch);
 | |
| 	strbuf_addstr(&state.path_gitdir_watch, "/.git");
 | |
| 	if (!is_directory(state.path_gitdir_watch.buf)) {
 | |
| 		strbuf_reset(&state.path_gitdir_watch);
 | |
| 		strbuf_addstr(&state.path_gitdir_watch,
 | |
| 			      absolute_path(repo_get_git_dir(the_repository)));
 | |
| 		strbuf_strip_suffix(&state.path_gitdir_watch, "/.");
 | |
| 		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, '/');
 | |
| 
 | |
| 	/*
 | |
| 	 * We create a named-pipe or unix domain socket inside of the
 | |
| 	 * ".git" directory.  (Well, on Windows, we base our named
 | |
| 	 * pipe in the NPFS on the absolute path of the git
 | |
| 	 * directory.)
 | |
| 	 */
 | |
| 	strbuf_init(&state.path_ipc, 0);
 | |
| 	strbuf_addstr(&state.path_ipc,
 | |
| 		absolute_path(fsmonitor_ipc__get_path(the_repository)));
 | |
| 
 | |
| 	/*
 | |
| 	 * Confirm that we can create platform-specific resources for the
 | |
| 	 * filesystem listener before we bother starting all the threads.
 | |
| 	 */
 | |
| 	if (fsm_listen__ctor(&state)) {
 | |
| 		err = error(_("could not initialize listener thread"));
 | |
| 		goto done;
 | |
| 	}
 | |
| 
 | |
| 	if (fsm_health__ctor(&state)) {
 | |
| 		err = error(_("could not initialize health thread"));
 | |
| 		goto done;
 | |
| 	}
 | |
| 
 | |
| 	/*
 | |
| 	 * CD out of the worktree root directory.
 | |
| 	 *
 | |
| 	 * The common Git startup mechanism causes our CWD to be the
 | |
| 	 * root of the worktree.  On Windows, this causes our process
 | |
| 	 * to hold a locked handle on the CWD.  This prevents the
 | |
| 	 * worktree from being moved or deleted while the daemon is
 | |
| 	 * running.
 | |
| 	 *
 | |
| 	 * We assume that our FS and IPC listener threads have either
 | |
| 	 * opened all of the handles that they need or will do
 | |
| 	 * everything using absolute paths.
 | |
| 	 */
 | |
| 	home = getenv("HOME");
 | |
| 	if (home && *home && chdir(home))
 | |
| 		die_errno(_("could not cd home '%s'"), home);
 | |
| 
 | |
| 	err = fsmonitor_run_daemon_1(&state);
 | |
| 
 | |
| done:
 | |
| 	pthread_cond_destroy(&state.cookies_cond);
 | |
| 	pthread_mutex_destroy(&state.main_lock);
 | |
| 	fsm_listen__dtor(&state);
 | |
| 	fsm_health__dtor(&state);
 | |
| 
 | |
| 	ipc_server_free(state.ipc_server_data);
 | |
| 
 | |
| 	strbuf_release(&state.path_worktree_watch);
 | |
| 	strbuf_release(&state.path_gitdir_watch);
 | |
| 	strbuf_release(&state.path_cookie_prefix);
 | |
| 	strbuf_release(&state.path_ipc);
 | |
| 	strbuf_release(&state.alias.alias);
 | |
| 	strbuf_release(&state.alias.points_to);
 | |
| 
 | |
| 	return err;
 | |
| }
 | |
| 
 | |
| static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED)
 | |
| {
 | |
| 	/*
 | |
| 	 * Technically, we don't need to probe for an existing daemon
 | |
| 	 * process, since we could just call `fsmonitor_run_daemon()`
 | |
| 	 * and let it fail if the pipe/socket is busy.
 | |
| 	 *
 | |
| 	 * However, this method gives us a nicer error message for a
 | |
| 	 * common error case.
 | |
| 	 */
 | |
| 	if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
 | |
| 		die(_("fsmonitor--daemon is already running '%s'"),
 | |
| 		    the_repository->worktree);
 | |
| 
 | |
| 	if (fsmonitor__announce_startup) {
 | |
| 		fprintf(stderr, _("running fsmonitor-daemon in '%s'\n"),
 | |
| 			the_repository->worktree);
 | |
| 		fflush(stderr);
 | |
| 	}
 | |
| 
 | |
| #ifdef GIT_WINDOWS_NATIVE
 | |
| 	if (detach_console)
 | |
| 		FreeConsole();
 | |
| #endif
 | |
| 
 | |
| 	return !!fsmonitor_run_daemon();
 | |
| }
 | |
| 
 | |
| static start_bg_wait_cb bg_wait_cb;
 | |
| 
 | |
| static int bg_wait_cb(const struct child_process *cp UNUSED,
 | |
| 		      void *cb_data UNUSED)
 | |
| {
 | |
| 	enum ipc_active_state s = fsmonitor_ipc__get_state();
 | |
| 
 | |
| 	switch (s) {
 | |
| 	case IPC_STATE__LISTENING:
 | |
| 		/* child is "ready" */
 | |
| 		return 0;
 | |
| 
 | |
| 	case IPC_STATE__NOT_LISTENING:
 | |
| 	case IPC_STATE__PATH_NOT_FOUND:
 | |
| 		/* give child more time */
 | |
| 		return 1;
 | |
| 
 | |
| 	default:
 | |
| 	case IPC_STATE__INVALID_PATH:
 | |
| 	case IPC_STATE__OTHER_ERROR:
 | |
| 		/* all the time in world won't help */
 | |
| 		return -1;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static int try_to_start_background_daemon(void)
 | |
| {
 | |
| 	struct child_process cp = CHILD_PROCESS_INIT;
 | |
| 	enum start_bg_result sbgr;
 | |
| 
 | |
| 	/*
 | |
| 	 * Before we try to create a background daemon process, see
 | |
| 	 * if a daemon process is already listening.  This makes it
 | |
| 	 * easier for us to report an already-listening error to the
 | |
| 	 * console, since our spawn/daemon can only report the success
 | |
| 	 * of creating the background process (and not whether it
 | |
| 	 * immediately exited).
 | |
| 	 */
 | |
| 	if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING)
 | |
| 		die(_("fsmonitor--daemon is already running '%s'"),
 | |
| 		    the_repository->worktree);
 | |
| 
 | |
| 	if (fsmonitor__announce_startup) {
 | |
| 		fprintf(stderr, _("starting fsmonitor-daemon in '%s'\n"),
 | |
| 			the_repository->worktree);
 | |
| 		fflush(stderr);
 | |
| 	}
 | |
| 
 | |
| 	cp.git_cmd = 1;
 | |
| 
 | |
| 	strvec_push(&cp.args, "fsmonitor--daemon");
 | |
| 	strvec_push(&cp.args, "run");
 | |
| 	strvec_push(&cp.args, "--detach");
 | |
| 	strvec_pushf(&cp.args, "--ipc-threads=%d", fsmonitor__ipc_threads);
 | |
| 
 | |
| 	cp.no_stdin = 1;
 | |
| 	cp.no_stdout = 1;
 | |
| 	cp.no_stderr = 1;
 | |
| 
 | |
| 	sbgr = start_bg_command(&cp, bg_wait_cb, NULL,
 | |
| 				fsmonitor__start_timeout_sec);
 | |
| 
 | |
| 	switch (sbgr) {
 | |
| 	case SBGR_READY:
 | |
| 		return 0;
 | |
| 
 | |
| 	default:
 | |
| 	case SBGR_ERROR:
 | |
| 	case SBGR_CB_ERROR:
 | |
| 		return error(_("daemon failed to start"));
 | |
| 
 | |
| 	case SBGR_TIMEOUT:
 | |
| 		return error(_("daemon not online yet"));
 | |
| 
 | |
| 	case SBGR_DIED:
 | |
| 		return error(_("daemon terminated"));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int cmd_fsmonitor__daemon(int argc,
 | |
| 			  const char **argv,
 | |
| 			  const char *prefix,
 | |
| 			  struct repository *repo UNUSED)
 | |
| {
 | |
| 	const char *subcmd;
 | |
| 	enum fsmonitor_reason reason;
 | |
| 	int detach_console = 0;
 | |
| 
 | |
| 	struct option options[] = {
 | |
| 		OPT_BOOL(0, "detach", &detach_console, N_("detach from console")),
 | |
| 		OPT_INTEGER(0, "ipc-threads",
 | |
| 			    &fsmonitor__ipc_threads,
 | |
| 			    N_("use <n> ipc worker threads")),
 | |
| 		OPT_INTEGER(0, "start-timeout",
 | |
| 			    &fsmonitor__start_timeout_sec,
 | |
| 			    N_("max seconds to wait for background daemon startup")),
 | |
| 
 | |
| 		OPT_END()
 | |
| 	};
 | |
| 
 | |
| 	git_config(fsmonitor_config, NULL);
 | |
| 
 | |
| 	argc = parse_options(argc, argv, prefix, options,
 | |
| 			     builtin_fsmonitor__daemon_usage, 0);
 | |
| 	if (argc != 1)
 | |
| 		usage_with_options(builtin_fsmonitor__daemon_usage, options);
 | |
| 	subcmd = argv[0];
 | |
| 
 | |
| 	if (fsmonitor__ipc_threads < 1)
 | |
| 		die(_("invalid 'ipc-threads' value (%d)"),
 | |
| 		    fsmonitor__ipc_threads);
 | |
| 
 | |
| 	prepare_repo_settings(the_repository);
 | |
| 	/*
 | |
| 	 * If the repo is fsmonitor-compatible, explicitly set IPC-mode
 | |
| 	 * (without bothering to load the `core.fsmonitor` config settings).
 | |
| 	 *
 | |
| 	 * If the repo is not compatible, the repo-settings will be set to
 | |
| 	 * incompatible rather than IPC, so we can use one of the __get
 | |
| 	 * routines to detect the discrepancy.
 | |
| 	 */
 | |
| 	fsm_settings__set_ipc(the_repository);
 | |
| 
 | |
| 	reason = fsm_settings__get_reason(the_repository);
 | |
| 	if (reason > FSMONITOR_REASON_OK)
 | |
| 		die("%s",
 | |
| 		    fsm_settings__get_incompatible_msg(the_repository,
 | |
| 						       reason));
 | |
| 
 | |
| 	if (!strcmp(subcmd, "start"))
 | |
| 		return !!try_to_start_background_daemon();
 | |
| 
 | |
| 	if (!strcmp(subcmd, "run"))
 | |
| 		return !!try_to_run_foreground_daemon(detach_console);
 | |
| 
 | |
| 	if (!strcmp(subcmd, "stop"))
 | |
| 		return !!do_as_client__send_stop();
 | |
| 
 | |
| 	if (!strcmp(subcmd, "status"))
 | |
| 		return !!do_as_client__status();
 | |
| 
 | |
| 	die(_("Unhandled subcommand '%s'"), subcmd);
 | |
| }
 | |
| 
 | |
| #else
 | |
| int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix UNUSED, struct repository *repo UNUSED)
 | |
| {
 | |
| 	struct option options[] = {
 | |
| 		OPT_END()
 | |
| 	};
 | |
| 
 | |
| 	show_usage_with_options_if_asked(argc, argv,
 | |
| 					 builtin_fsmonitor__daemon_usage, options);
 | |
| 
 | |
| 	die(_("fsmonitor--daemon not supported on this platform"));
 | |
| }
 | |
| #endif
 |