Show changes to 5 files +657 −4
.gitignore, Makefile, file-watcher.c, t/t7513-file-watcher.sh, test-file-watcher.c
diff --git a/.gitignore b/.gitignore
index 12c78f0..277f929 100644
--- a/.gitignore
+++ b/.gitignore
@@ -181,6 +181,7 @@
/test-date
/test-delta
/test-dump-cache-tree
+/test-file-watcher
/test-scrap-cache-tree
/test-genrandom
/test-index-version
diff --git a/Makefile b/Makefile
index 1c4d659..f0dc2cc 100644
--- a/Makefile
+++ b/Makefile
@@ -555,6 +555,7 @@ TEST_PROGRAMS_NEED_X += test-ctype
TEST_PROGRAMS_NEED_X += test-date
TEST_PROGRAMS_NEED_X += test-delta
TEST_PROGRAMS_NEED_X += test-dump-cache-tree
+TEST_PROGRAMS_NEED_X += test-file-watcher
TEST_PROGRAMS_NEED_X += test-genrandom
TEST_PROGRAMS_NEED_X += test-index-version
TEST_PROGRAMS_NEED_X += test-line-buffer
diff --git a/file-watcher.c b/file-watcher.c
index 1e45b25..3ab0a11 100644
--- a/file-watcher.c
+++ b/file-watcher.c
@@ -65,7 +65,8 @@ struct connection {
static struct connection **conns;
static struct pollfd *pfd;
static int conns_alloc, pfd_nr, pfd_alloc;
-static int inotify_fd;
+static int inotify_fd, test_mode;
+static int wd_counter = 1;
/*
* IN_DONT_FOLLOW does not matter now as we do not monitor
@@ -78,10 +79,19 @@ static struct dir *create_dir(struct dir *parent, const char *path,
const char *basename)
{
struct dir *d;
- int wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASKS);
+ int wd;
+ if (!test_mode)
+ wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASKS);
+ else {
+ wd = wd_counter++;
+ if (wd > 8)
+ wd = -1;
+ }
if (wd < 0)
return NULL;
+ trace_printf_key("GIT_TRACE_WATCHER", "inotify: watch %d %s\n",
+ wd, path);
d = xmalloc(sizeof(*d));
memset(d, 0, sizeof(*d));
d->wd = wd;
@@ -124,7 +134,9 @@ static void free_dir(struct dir *d, int topdown)
if (d->repo)
d->repo->root = NULL;
wds[d->wd] = NULL;
- inotify_rm_watch(inotify_fd, d->wd);
+ if (!test_mode)
+ inotify_rm_watch(inotify_fd, d->wd);
+ trace_printf_key("GIT_TRACE_WATCHER", "inotify: unwatch %d\n", d->wd);
if (topdown) {
int i;
for (i = 0; i < d->nr_subdirs; i++)
@@ -265,6 +277,7 @@ static inline void queue_file_changed(struct file *f, struct strbuf *sb)
int len = sb->len;
strbuf_addf(sb, "%s%s", f->parent->parent ? "/" : "", f->name);
string_list_append(&f->repo->updated, sb->buf);
+ trace_printf_key("GIT_TRACE_WATCHER", "watcher: changed %s\n", sb->buf);
f->repo->updated_sorted = 0;
strbuf_setlen(sb, len);
}
@@ -324,6 +337,10 @@ static int do_handle_inotify(const struct inotify_event *event)
struct dir *d;
int pos;
+ trace_printf_key("GIT_TRACE_WATCHER", "inotify: event %08x wd %d %s\n",
+ event->mask, event->wd,
+ event->len ? event->name : "N/A");
+
if (event->mask & (IN_Q_OVERFLOW | IN_UNMOUNT)) {
int i;
for (i = 0; i < nr_repos; i++)
@@ -385,6 +402,81 @@ static int handle_inotify(int fd)
return ret;
}
+struct constant {
+ const char *name;
+ int value;
+};
+
+#define CONSTANT(x) { #x, x }
+static const struct constant inotify_masks[] = {
+ CONSTANT(IN_DELETE_SELF),
+ CONSTANT(IN_MOVE_SELF),
+ CONSTANT(IN_ATTRIB),
+ CONSTANT(IN_DELETE),
+ CONSTANT(IN_MODIFY),
+ CONSTANT(IN_MOVED_FROM),
+ CONSTANT(IN_MOVED_TO),
+ CONSTANT(IN_Q_OVERFLOW),
+ CONSTANT(IN_UNMOUNT),
+ { NULL, 0 },
+};
+
+static void inject_inotify(const char *msg)
+{
+ char buf[sizeof(struct inotify_event) + NAME_MAX + 1];
+ struct inotify_event *event = (struct inotify_event *)buf;
+ char *end, *p;
+ int i;
+ memset(event, 0, sizeof(*event));
+ event->wd = strtol(msg, &end, 0);
+ if (*end++ != ' ')
+ die("expect a space after watch descriptor");
+ p = end;
+ end = strchrnul(p, ' ');
+ if (*end)
+ strcpy(event->name, end + 1);
+ while (p < end) {
+ char *sep = strchrnul(p, '|');
+ if (sep > end)
+ sep = end;
+ *sep = '\0';
+ for (i = 0; inotify_masks[i].name; i++)
+ if (!strcmp(inotify_masks[i].name, p))
+ break;
+ if (!inotify_masks[i].name)
+ die("unrecognize event mask %s", p);
+ event->mask |= inotify_masks[i].value;
+ p = sep + 1;
+ }
+ do_handle_inotify(event);
+}
+
+static void dump_watches(struct dir *d, struct strbuf *sb, struct strbuf *out)
+{
+ int i, len = sb->len;
+ strbuf_addstr(sb, d->name);
+ strbuf_addf(out, "%s %d%c", sb->buf[0] ? sb->buf : ".", d->wd, '\0');
+ if (d->name[0])
+ strbuf_addch(sb, '/');
+ for (i = 0; i < d->nr_subdirs; i++)
+ dump_watches(d->subdirs[i], sb, out);
+ for (i = 0; i < d->nr_files; i++)
+ strbuf_addf(out, "%s%s%c", sb->buf, d->files[i]->name, '\0');
+ strbuf_setlen(sb, len);
+}
+
+static void dump_changes(struct repository *repo, struct strbuf *sb)
+{
+ int i;
+ if (!repo->updated_sorted) {
+ sort_string_list(&repo->updated);
+ repo->updated_sorted = 1;
+ }
+ for (i = 0; i < repo->updated.nr; i++)
+ strbuf_add(sb, repo->updated.items[i].string,
+ strlen(repo->updated.items[i].string) + 1);
+}
+
static void get_changed_list(int conn_id)
{
struct strbuf sb = STRBUF_INIT;
@@ -483,11 +575,13 @@ static void unchange(int conn_id, unsigned long size)
item = string_list_lookup(&repo->updated, p);
if (!item)
continue;
+ trace_printf_key("GIT_TRACE_WATCHER", "watcher: unchange %s\n", p);
unsorted_string_list_delete_item(&repo->updated,
item - repo->updated.items, 0);
}
strbuf_release(&sb);
}
+ trace_printf_key("GIT_TRACE_WATCHER", "watcher: unchange complete\n");
memcpy(repo->index_signature, conn->new_index, 40);
/*
* If other connections on this repo are in some sort of
@@ -540,6 +634,13 @@ static void reset_watches(struct repository *repo)
static void reset_repo(struct repository *repo, ino_t inode)
{
+ if (test_mode)
+ /*
+ * test-mode is designed for single repo, we can
+ * safely reset wd counter because all wd should be
+ * deleted
+ */
+ wd_counter = 1;
reset_watches(repo);
string_list_clear(&repo->updated, 0);
memcpy(repo->index_signature, invalid_signature, 40);
@@ -560,6 +661,7 @@ static int shutdown_connection(int id)
return 0;
}
+static void cleanup(void);
static int handle_command(int conn_id)
{
int fd = conns[conn_id]->sock;
@@ -754,6 +856,71 @@ static int handle_command(int conn_id)
}
unchange(conn_id, n);
}
+
+ /*
+ * Testing and debugging support
+ */
+ else if (!strcmp(msg, "test-mode") && getenv("GIT_TEST_WATCHER")) {
+ test_mode = 1;
+ packet_write(fd, "test mode on");
+ }
+ else if (starts_with(msg, "setenv ")) {
+ /* useful for setting GIT_TRACE_WATCHER or GIT_TRACE_PACKET */
+ char *sep = strchr(msg + 7, ' ');
+ if (!sep) {
+ packet_write(fd, "error invalid setenv line %s", msg);
+ return shutdown_connection(conn_id);
+ }
+ *sep = '\0';
+ setenv(msg + 7, sep + 1, 1);
+ }
+ else if (starts_with(msg, "log ")) {
+ ; /* do nothing, if GIT_TRACE_PACKET is on, it's already logged */
+ }
+ else if (!strcmp(msg, "die") && getenv("GIT_TEST_WATCHER")) {
+ /*
+ * The client will wait for "see you" before it may
+ * run another daemon with the same path. So there's
+ * no racing on unlink() and listen() on the same
+ * socket path.
+ */
+ cleanup();
+ packet_write(fd, "see you");
+ close(fd);
+ exit(0);
+ }
+ else if (starts_with(msg, "dump ") && getenv("GIT_TEST_WATCHER")) {
+ struct strbuf sb = STRBUF_INIT;
+ struct strbuf out = STRBUF_INIT;
+ const char *reply = NULL;
+ if (!strcmp(msg + 5, "watches")) {
+ if (conns[conn_id]->repo) {
+ if (conns[conn_id]->repo->root)
+ dump_watches(conns[conn_id]->repo->root, &sb, &out);
+ } else {
+ int i;
+ for (i = 0; i < nr_repos; i++) {
+ strbuf_addf(&out, "%s%c", repos[i]->work_tree, '\0');
+ if (repos[i]->root)
+ dump_watches(repos[i]->root, &sb, &out);
+ strbuf_reset(&out);
+ strbuf_reset(&sb);
+ }
+ }
+ reply = "watching";
+ } else if (!strcmp(msg + 5, "changes")) {
+ dump_changes(conns[conn_id]->repo, &out);
+ reply = "changed";
+ }
+ packet_write(fd, "%s %d", reply, (int)out.len);
+ if (out.len)
+ write_in_full(fd, out.buf, out.len);
+ strbuf_release(&out);
+ strbuf_release(&sb);
+ }
+ else if (starts_with(msg, "inotify ") && getenv("GIT_TEST_WATCHER")) {
+ inject_inotify(msg + 8);
+ }
else {
packet_write(fd, "error unrecognized command %s", msg);
return shutdown_connection(conn_id);
@@ -848,11 +1015,13 @@ int main(int argc, const char **argv)
{
struct strbuf sb = STRBUF_INIT;
int i, new_nr, fd, quit = 0, nr_common;
- int daemon = 0;
+ int daemon = 0, check_support = 0;
time_t last_checked;
struct option options[] = {
OPT_BOOL(0, "detach", &daemon,
N_("run in background")),
+ OPT_BOOL(0, "check-support", &check_support,
+ N_("return zero file watcher is available")),
OPT_END()
};
@@ -865,6 +1034,10 @@ int main(int argc, const char **argv)
argc = parse_options(argc, argv, NULL, options,
file_watcher_usage, 0);
+
+ if (check_support)
+ return 0;
+
if (argc < 1)
die(_("socket path missing"));
else if (argc > 1)
diff --git a/t/t7513-file-watcher.sh b/t/t7513-file-watcher.sh
new file mode 100755
index 0000000..bf64fc4
--- /dev/null
+++ b/t/t7513-file-watcher.sh
@@ -0,0 +1,382 @@
+#!/bin/sh
+
+test_description='File watcher daemon tests'
+
+. ./test-lib.sh
+
+if git file-watcher --check-support && test_have_prereq POSIXPERM; then
+ : # good
+else
+ skip_all="file-watcher not supported on this system"
+ test_done
+fi
+
+kill_it() {
+ test-file-watcher "$1" <<EOF >/dev/null
+<die
+>see you
+EOF
+}
+
+GIT_TEST_WATCHER=1
+export GIT_TEST_WATCHER
+
+test_expect_success 'test-file-watcher can kill the daemon' '
+ chmod 700 . &&
+ git file-watcher --detach . &&
+ cat >expect <<EOF &&
+<die
+>see you
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ ! test -S socket
+'
+
+test_expect_success 'exchange hello' '
+ git file-watcher --detach . &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<die
+>see you
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'normal index sequence' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>inconsistent
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ cat >expect2 <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+# inconsistent again because new-index has not been issued yet
+>inconsistent
+<new-index $SIG
+<<unchange
+<<
+EOF
+ test-file-watcher . >actual2 <expect2 &&
+ test_cmp expect2 actual2 &&
+ cat >expect3 <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>ok
+<die
+>see you
+EOF
+ test-file-watcher . >actual3 <expect3 &&
+ test_cmp expect3 actual3
+'
+
+test_expect_success 'unaccepted index: hello not sent' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<index $SIG $TRASH_DIRECTORY
+>error why did you not greet me? go away
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ kill_it .
+'
+
+test_expect_success 'unaccepted index: signature too short' '
+ git file-watcher --detach . &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index 1234 $TRASH_DIRECTORY
+>error invalid index line index 1234 $TRASH_DIRECTORY
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ kill_it .
+'
+
+test_expect_success 'unaccepted index: worktree unavailable' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY/non-existent
+>error work tree does not exist: No such file or directory
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ kill_it .
+'
+
+test_expect_success 'watch foo and abc/bar' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>inconsistent
+<test-mode
+>test mode on
+<<watch
+<<foo
+<<abc/bar
+<<
+>watched 2
+<dump watches
+>>watching
+>>. 1
+>>abc 2
+>>abc/bar
+>>foo
+<new-index $SIG
+<<unchange
+<<
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'modify abc/bar' '
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>ok
+<inotify 2 IN_MODIFY bar
+<dump watches
+>>watching
+>>. 1
+>>foo
+<dump changes
+>>changed
+>>abc/bar
+<die
+>see you
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'delete abc makes abc/bar changed' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>inconsistent
+<test-mode
+>test mode on
+<<watch
+<<foo/abc/bar
+<<
+>watched 1
+<dump watches
+>>watching
+>>. 1
+>>foo 2
+>>foo/abc 3
+>>foo/abc/bar
+<inotify 2 IN_DELETE_SELF
+<dump watches
+>>watching
+<dump changes
+>>changed
+>>foo/abc/bar
+<new-index $SIG
+<<unchange
+<<
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'get changed list' '
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>ok
+<get-changed
+>>changed
+>>foo/abc/bar
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'incomplete new-index request' '
+ SIG=0123456789012345678901234567890123456789 &&
+ SIG2=9123456789012345678901234567890123456780 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>ok
+<new-index $SIG2
+<dump changes
+>>changed
+>>foo/abc/bar
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'delete abc/bar from changed list' '
+ SIG=0123456789012345678901234567890123456789 &&
+ SIG2=9123456789012345678901234567890123456780 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>ok
+<new-index $SIG2
+<<unchange
+<<foo/abc/bar
+<<
+<dump changes
+>>changed
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'file-watcher index updated after new-index' '
+ SIG2=9123456789012345678901234567890123456780 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG2 $TRASH_DIRECTORY
+>ok
+<die
+>see you
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+# When test-mode is on, file-watch only accepts 8 directories
+test_expect_success 'watch too many directories' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>inconsistent
+# Do not call inotify_add_watch()
+<test-mode
+>test mode on
+# First batch should be all ok
+<<watch
+<<dir1/foo
+<<dir2/foo
+<<dir3/foo
+<<dir4/foo
+<<
+>watched 4
+# Second batch hits the limit
+<<watch
+<<dir5/foo
+<<dir6/foo
+<<dir7/foo
+<<dir8/foo
+<<dir9/foo
+<<
+>watched 3
+# The third batch is already registered, should accept too
+<<watch
+<<dir5/foo
+<<dir6/foo
+<<dir7/foo
+<<
+>watched 3
+# Try again, see if it still rejects
+<<watch
+<<dir8/foo
+<<dir9/foo
+<<
+>watched 0
+<dump watches
+>>watching
+>>. 1
+>>dir1 2
+>>dir1/foo
+>>dir2 3
+>>dir2/foo
+>>dir3 4
+>>dir3/foo
+>>dir4 5
+>>dir4/foo
+>>dir5 6
+>>dir5/foo
+>>dir6 7
+>>dir6/foo
+>>dir7 8
+>>dir7/foo
+<die
+>see you
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'event overflow' '
+ git file-watcher --detach . &&
+ SIG=0123456789012345678901234567890123456789 &&
+ cat >expect <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+>inconsistent
+<test-mode
+>test mode on
+<<watch
+<<foo
+<<abc/bar
+<<
+>watched 2
+<inotify 2 IN_MODIFY bar
+<dump watches
+>>watching
+>>. 1
+>>foo
+<dump changes
+>>changed
+>>abc/bar
+<inotify -1 IN_Q_OVERFLOW
+<dump watches
+>>watching
+<dump changes
+>>changed
+EOF
+ test-file-watcher . >actual <expect &&
+ test_cmp expect actual &&
+ cat >expect2 <<EOF &&
+<hello
+>hello
+<index $SIG $TRASH_DIRECTORY
+# Must be inconsistent because of IN_Q_OVERFLOW
+>inconsistent
+<die
+>see you
+EOF
+ test-file-watcher . >actual2 <expect2 &&
+ test_cmp expect2 actual2
+'
+
+test_done
diff --git a/test-file-watcher.c b/test-file-watcher.c
new file mode 100644
index 0000000..ffff198
--- /dev/null
+++ b/test-file-watcher.c
@@ -0,0 +1,96 @@
+#include "cache.h"
+#include "unix-socket.h"
+#include "pkt-line.h"
+#include "strbuf.h"
+
+int main(int ac, char **av)
+{
+ struct strbuf sb = STRBUF_INIT;
+ struct strbuf packed = STRBUF_INIT;
+ char *packing = NULL;
+ int last_command_is_reply = 0;
+ int fd;
+
+ strbuf_addf(&sb, "%s/socket", av[1]);
+ fd = unix_stream_connect(sb.buf);
+ if (fd < 0)
+ die_errno("connect");
+ strbuf_reset(&sb);
+
+ /*
+ * test-file-watcher crashes sometimes, make sure to flush
+ */
+ setbuf(stdout, NULL);
+
+ while (!strbuf_getline(&sb, stdin, '\n')) {
+ if (sb.buf[0] == '#') {
+ puts(sb.buf);
+ continue;
+ }
+ if (sb.buf[0] == '>') {
+ if (last_command_is_reply)
+ continue;
+ last_command_is_reply = 1;
+ } else
+ last_command_is_reply = 0;
+
+ if (sb.buf[0] == '<' && sb.buf[1] == '<') {
+ puts(sb.buf);
+ if (!packing) {
+ packing = xstrdup(sb.buf + 2);
+ strbuf_reset(&packed);
+ continue;
+ }
+ if (!sb.buf[2]) {
+ packet_write(fd, "%s %d", packing, (int)packed.len);
+ if (packed.len)
+ write_in_full(fd, packed.buf, packed.len);
+ free(packing);
+ packing = NULL;
+ } else
+ strbuf_add(&packed, sb.buf + 2, sb.len - 2 + 1);
+ continue;
+ }
+ if (sb.buf[0] == '<') {
+ packet_write(fd, "%s", sb.buf + 1);
+ puts(sb.buf);
+ continue;
+ }
+ if (sb.buf[0] == '>' && sb.buf[1] == '>') {
+ int len;
+ char *p, *reply = packet_read_line(fd, &len);
+ if (!starts_with(reply, sb.buf + 2) ||
+ reply[sb.len - 2] != ' ') {
+ printf(">%s\n", reply);
+ continue;
+ } else {
+ p = reply + sb.len - 2;
+ printf(">>%.*s\n", (int)(p - reply), reply);
+ len = atoi(p + 1);
+ if (!len)
+ continue;
+ }
+ strbuf_reset(&packed);
+ strbuf_grow(&packed, len);
+ if (read_in_full(fd, packed.buf, len) <= 0)
+ return 1;
+ strbuf_setlen(&packed, len);
+ for (p = packed.buf; p - packed.buf < packed.len; p += len + 1) {
+ len = strlen(p);
+ printf(">>%s\n", p);
+ }
+ continue;
+ }
+ if (sb.buf[0] == '>') {
+ int len;
+ char *reply = packet_read_line(fd, &len);
+ if (!reply)
+ puts(">");
+ else
+ printf(">%s\n", reply);
+ continue;
+ }
+ die("unrecognize command %s", sb.buf);
+ }
+ return 0;
+}