Subject: [PATCH 3/6] read-cache: connect to file watcher

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Sun, 12 Jan 2014 18:03:39 +0700

This patch establishes a connection between a new file watcher daemon
and git. Each index file may have at most one file watcher attached to
it. The file watcher maintains a UNIX socket at
$GIT_DIR/index.watcher. Any process that has write access to $GIT_DIR
can talk to the file watcher.

A validation is performed after git connects to the file watcher to
make sure both sides have the same view. This is done by exchanging
the index signature (*) The file watcher keeps a copy of the signature
locally while git computes the signature from the index. If the
signatures do not match, something has gone wrong so both sides
reinitialize wrt. to file watching: the file watcher clears all
watches while git clears CE_WATCHED flags.

If the signatures match, we can trust the file watcher and git can
start asking questions that are not important to this patch.

TODO: do not let git hang if the file watcher refuses to
answer. Timeout and move on without file watcher support after 20ms or
so.

(*) for current index versions, the signature is the index SHA-1
trailer. But it could be something else (e.g. v5 does not have SHA-1
trailer)

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 .gitignore           |   1 +
 Makefile             |   1 +
 cache.h              |   1 +
 file-watcher.c (new) | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++
 git-compat-util.h    |   5 ++
 read-cache.c         |  48 ++++++++++++++++++
 wrapper.c            |  27 ++++++++++
 7 files changed, 219 insertions(+)
 create mode 100644 file-watcher.c

diff --git a/.gitignore b/.gitignore
index b5f9def..12c78f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,7 @@
 /git-fast-import
 /git-fetch
 /git-fetch-pack
+/git-file-watcher
 /git-filter-branch
 /git-fmt-merge-msg
 /git-for-each-ref
diff --git a/Makefile b/Makefile
index b4af1e2..ca5dc96 100644
--- a/Makefile
+++ b/Makefile
@@ -536,6 +536,7 @@ PROGRAMS += $(EXTRA_PROGRAMS)
 PROGRAM_OBJS += credential-store.o
 PROGRAM_OBJS += daemon.o
 PROGRAM_OBJS += fast-import.o
+PROGRAM_OBJS += file-watcher.o
 PROGRAM_OBJS += http-backend.o
 PROGRAM_OBJS += imap-send.o
 PROGRAM_OBJS += sh-i18n--envsubst.o
diff --git a/cache.h b/cache.h
index dfa8622..6a182b5 100644
--- a/cache.h
+++ b/cache.h
@@ -281,6 +281,7 @@ struct index_state {
 	struct hash_table name_hash;
 	struct hash_table dir_hash;
 	unsigned char sha1[20];
+	int watcher;
 };
 
 extern struct index_state the_index;
diff --git a/file-watcher.c b/file-watcher.c
new file mode 100644
index 0000000..66b44e5
--- /dev/null
+++ b/file-watcher.c
@@ -0,0 +1,136 @@
+#include "cache.h"
+#include "sigchain.h"
+
+static char index_signature[41];
+
+static int handle_command(int fd, char *msg, int msgsize)
+{
+	struct sockaddr_un sun;
+	int len;
+	socklen_t socklen;
+	const char *arg;
+
+	socklen = sizeof(sun);
+	len = recvfrom(fd, msg, msgsize, 0, &sun, &socklen);
+	if (!len)
+		return -1;
+	if (len == -1)
+		die_errno("read");
+	msg[len] = '\0';
+
+	if ((arg = skip_prefix(msg, "hello "))) {
+		sendtof(fd, 0, &sun, socklen, "hello %s", index_signature);
+		if (!strcmp(index_signature, arg))
+			return 0;
+		/*
+		 * Index SHA-1 mismatch, something has gone
+		 * wrong. Clean up and start over.
+		 */
+		strlcpy(index_signature, arg, sizeof(index_signature));
+	} else {
+		die("unrecognized command %s", msg);
+	}
+	return 0;
+}
+
+static const char *socket_path;
+static int do_not_clean_up;
+
+static void cleanup(void)
+{
+	if (do_not_clean_up)
+		return;
+	unlink(socket_path);
+}
+
+static void cleanup_on_signal(int signo)
+{
+	cleanup();
+	sigchain_pop(signo);
+	raise(signo);
+}
+
+static void daemonize(void)
+{
+#ifdef NO_POSIX_GOODIES
+	die("fork not supported on this platform");
+#else
+	switch (fork()) {
+		case 0:
+			break;
+		case -1:
+			die_errno("fork failed");
+		default:
+			do_not_clean_up = 1;
+			exit(0);
+	}
+	if (setsid() == -1)
+		die_errno("setsid failed");
+	close(0);
+	close(1);
+	close(2);
+	sanitize_stdfds();
+#endif
+}
+
+int main(int argc, char **argv)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct sockaddr_un sun;
+	struct pollfd pfd[2];
+	int fd, err, nr;
+	int msgsize;
+	char *msg;
+	socklen_t vallen = sizeof(msgsize);
+	int no_daemon = 0;
+
+	if (!strcmp(argv[1], "--no-daemon")) {
+		no_daemon =1;
+		argv++;
+		argc--;
+	}
+	if (argc < 2)
+		die("insufficient arguments");
+	socket_path = argv[1];
+	memset(index_signature, 0, sizeof(index_signature));
+	fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, socket_path, sizeof(sun.sun_path));
+	if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)))
+		die_errno("unable to bind to %s", socket_path);
+	atexit(cleanup);
+	sigchain_push_common(cleanup_on_signal);
+
+	if (getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &msgsize, &vallen))
+		die_errno("could not get SO_SNDBUF");
+	msg = xmalloc(msgsize + 1);
+
+	if (!no_daemon) {
+		strbuf_addf(&sb, "%s.log", socket_path);
+		err = open(sb.buf, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+		if (err == -1)
+			die_errno("unable to create %s", sb.buf);
+		daemonize();
+		dup2(err, 1);
+		dup2(err, 2);
+		close(err);
+	}
+
+	nr = 0;
+	pfd[nr].fd = fd;
+	pfd[nr++].events = POLLIN;
+
+	for (;;) {
+		if (poll(pfd, nr, -1) < 0) {
+			if (errno != EINTR) {
+				error("Poll failed, resuming: %s", strerror(errno));
+				sleep(1);
+			}
+			continue;
+		}
+
+		if ((pfd[0].revents & POLLIN) && handle_command(fd, msg, msgsize))
+			break;
+	}
+	return 0;
+}
diff --git a/git-compat-util.h b/git-compat-util.h
index b73916b..c119a94 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -536,6 +536,11 @@ extern void *xcalloc(size_t nmemb, size_t size);
 extern void *xmmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
 extern ssize_t xread(int fd, void *buf, size_t len);
 extern ssize_t xwrite(int fd, const void *buf, size_t len);
+extern ssize_t writef(int fd, const char *fmt, ...)
+	__attribute__((format (printf, 2, 3)));
+extern ssize_t sendtof(int sockfd, int flags, const void *dest_addr,
+		       socklen_t addrlen, const char *fmt, ...)
+	__attribute__((format (printf, 5, 6)));
 extern int xdup(int fd);
 extern FILE *xfdopen(int fd, const char *mode);
 extern int xmkstemp(char *template);
diff --git a/read-cache.c b/read-cache.c
index 098d3b6..506d488 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1443,6 +1443,49 @@ static struct cache_entry *create_from_disk(struct ondisk_cache_entry *ondisk,
 	return ce;
 }
 
+static void connect_watcher(struct index_state *istate, const char *path)
+{
+	struct stat st;
+	struct strbuf sb = STRBUF_INIT;
+	int i;
+
+	strbuf_addf(&sb, "%s.watcher", path);
+	if (!stat(sb.buf, &st) && S_ISSOCK(st.st_mode)) {
+		struct sockaddr_un sun;
+		istate->watcher = socket(AF_UNIX, SOCK_DGRAM, 0);
+		sun.sun_family = AF_UNIX;
+		strlcpy(sun.sun_path, sb.buf, sizeof(sun.sun_path));
+		if (connect(istate->watcher, (struct sockaddr *)&sun, sizeof(sun))) {
+			perror("connect");
+			close(istate->watcher);
+			istate->watcher = -1;
+		}
+		sprintf(sun.sun_path, "%c%"PRIuMAX, 0, (uintmax_t)getpid());
+		bind(istate->watcher, (struct sockaddr *)&sun, sizeof(sun));
+	} else
+		istate->watcher = -1;
+	strbuf_release(&sb);
+	if (istate->watcher != -1) {
+		char line[1024];
+		int len;
+		strbuf_addf(&sb, "hello %s", sha1_to_hex(istate->sha1));
+		write(istate->watcher, sb.buf, sb.len);
+		len = read(istate->watcher, line, sizeof(line) - 1);
+		if (len > 0) {
+			line[len] = '\0';
+			if (!strcmp(sb.buf, line))
+				return; /* good */
+		}
+	}
+
+	/* No the file watcher is out of date, clear everything */
+	for (i = 0; i < istate->cache_nr; i++)
+		if (istate->cache[i]->ce_flags & CE_WATCHED) {
+			istate->cache[i]->ce_flags &= ~CE_WATCHED;
+			istate->cache_changed = 1;
+		}
+}
+
 /* remember to discard_cache() before reading a different cache! */
 int read_index_from(struct index_state *istate, const char *path)
 {
@@ -1528,6 +1571,7 @@ int read_index_from(struct index_state *istate, const char *path)
 		src_offset += extsize;
 	}
 	munmap(mmap, mmap_size);
+	connect_watcher(istate, path);
 	return istate->cache_nr;
 
 unmap:
@@ -1557,6 +1601,10 @@ int discard_index(struct index_state *istate)
 	free(istate->cache);
 	istate->cache = NULL;
 	istate->cache_alloc = 0;
+	if (istate->watcher != -1) {
+		close(istate->watcher);
+		istate->watcher = -1;
+	}
 	return 0;
 }
 
diff --git a/wrapper.c b/wrapper.c
index 0cc5636..29e3b35 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -455,3 +455,30 @@ struct passwd *xgetpwuid_self(void)
 		    errno ? strerror(errno) : _("no such user"));
 	return pw;
 }
+
+ssize_t writef(int fd, const char *fmt, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	int ret;
+	va_list ap;
+	va_start(ap, fmt);
+	strbuf_vaddf(&sb, fmt, ap);
+	va_end(ap);
+	ret = write(fd, sb.buf, sb.len);
+	strbuf_release(&sb);
+	return ret;
+}
+
+ssize_t sendtof(int sockfd, int flags, const void *dest_addr, socklen_t addrlen,
+		const char *fmt, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	int ret;
+	va_list ap;
+	va_start(ap, fmt);
+	strbuf_vaddf(&sb, fmt, ap);
+	va_end(ap);
+	ret = sendto(sockfd, sb.buf, sb.len, flags, dest_addr, addrlen);
+	strbuf_release(&sb);
+	return ret;
+}
-- 
1.8.5.2.240.g8478abd