Subject: [PATCH v3 23/26] file-watcher: tests for the daemon

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Mon, 3 Feb 2014 11:29:11 +0700

test-file-watcher is a simple chat program to talk to file
watcher. Typically you would write something like this

  cat >expect <<EOF
  # send "hello". Oh and this is a comment!
  <hello
  # Wait for reply and print to stdout.
  # test-file-watcher does not care about anything after '>'
  >hello
  <index foo bar
  >ok
  EOF
  test-file-watcher . <expect >actual

and test-file-watcher will execute the commands and get responses. If
all go well, "actual" should be the same as "expect". '<' and '>'
denote send and receive packets respectively. '<<' and '>>' can be
used to send and receive a list of NUL-terminated paths.

$GIT_TEST_WATCHER enables a few more commands for testing purposes.
The most important one is 'test-mode' where system inotify is taken
out and inotify events could be injected via test-file-watcher.

There are two debug commands in file-watcher that's not used by the
test suite, but would help debugging: setenv and log. They can be used
to turn on GIT_TRACE_PACKET then any "log" command will show, which
functions as barrier between events file watcher.

GIT_TRACE_WATCHER can also be enabled (dynamically or at startup) to
track inotify events.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 .gitignore                       |   1 +
 Makefile                         |   1 +
 file-watcher.c                   | 181 ++++++++++++++++++-
 t/t7513-file-watcher.sh (new +x) | 382 +++++++++++++++++++++++++++++++++++++++
 test-file-watcher.c (new)        |  96 ++++++++++
 5 files changed, 657 insertions(+), 4 deletions(-)
 create mode 100755 t/t7513-file-watcher.sh
 create mode 100644 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;
+}
-- 
1.8.5.2.240.g8478abd