summary refs log tree commit diff
path: root/cli
diff options
context:
space:
mode:
authorRichard Nyberg <rnyberg@murmeldjur.se>2005-06-24 09:51:38 +0000
committerRichard Nyberg <rnyberg@murmeldjur.se>2005-06-24 09:51:38 +0000
commitdd0d462afae75ff243f8cd1528963f9ad489706d (patch)
tree2ef874a1fe5212245814d16f4c9b389524aed9d1 /cli
downloadbtpd-dd0d462afae75ff243f8cd1528963f9ad489706d.tar.gz
btpd-dd0d462afae75ff243f8cd1528963f9ad489706d.zip
Import btpd-0.1.
git-svn-id: file:///home/rnyberg/svngit/btpd/releases/0.1@1 76a1f634-46fa-0310-9943-bd1476092a85
Diffstat (limited to 'cli')
-rw-r--r--cli/Makefile.am11
-rw-r--r--cli/btcli.c577
-rw-r--r--cli/btinfo.c53
-rw-r--r--cli/btpd_if.c221
-rw-r--r--cli/btpd_if.h21
5 files changed, 883 insertions, 0 deletions
diff --git a/cli/Makefile.am b/cli/Makefile.am
new file mode 100644
index 0000000..84e18f9
--- /dev/null
+++ b/cli/Makefile.am
@@ -0,0 +1,11 @@
+bin_PROGRAMS=btinfo btcli
+
+btinfo_SOURCES=btinfo.c
+btinfo_LDADD=../misc/libmisc.a -lcrypto -lm
+btinfo_CPPFLAGS=-I$(top_srcdir)/misc @openssl_CPPFLAGS@
+btinfo_LDFLAGS=@openssl_LDFLAGS@
+
+btcli_SOURCES=btcli.c btpd_if.c btpd_if.h
+btcli_LDADD=../misc/libmisc.a -lcrypto -lm
+btcli_CPPFLAGS=-I$(top_srcdir)/misc @openssl_CPPFLAGS@
+btcli_LDFLAGS=@openssl_LDFLAGS@
diff --git a/cli/btcli.c b/cli/btcli.c
new file mode 100644
index 0000000..2e266a1
--- /dev/null
+++ b/cli/btcli.c
@@ -0,0 +1,577 @@
+#include <sys/types.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <openssl/sha.h>
+
+#include "benc.h"
+#include "metainfo.h"
+#include "stream.h"
+#include "subr.h"
+#include "btpd_if.h"
+
+static void
+usage()
+{
+    printf("Usage: btcli command [options] [files]\n"
+	   "Commands:\n"
+	   "add <file_1> ... [file_n]\n"
+	   "\tAdd the given torrents to btpd.\n"
+	   "\n"
+	   "del <file_1> ... [file_n]\n"
+	   "\tRemove the given torrents from btpd.\n"
+	   "\n"
+	   "die\n"
+	   "\tShut down btpd.\n"
+	   "\n"
+           "list\n"
+           "\tList active torrents.\n"
+           "\n"
+           "stat [-i] [-w n] [file_1] ... [file_n]\n"
+           "\tShow stats for either all active or the given torrents.\n"
+           "\tThe stats displayed are:\n"
+           "\t%% of pieces seen, %% of pieces verified, \n"
+           "\tMB down, rate down, MB up, rate up, no peers\n"
+           "-i\n"
+           "\tShow stats per torrent in addition to total stats.\n"
+           "-w n\n"
+           "\tRepeat every n seconds.\n"
+           "\n"
+	   "Common options:\n"
+	   "--ipc key\n"
+	   "\tTalk to the btpd started with the same key.\n"
+	   "\n"
+	   "--help\n"
+	   "\tShow this help.\n"
+	   "\n");
+    exit(1);
+}
+
+static void
+handle_error(int error)
+{
+    switch (error) {
+    case 0:
+	break;
+    case ENOENT:
+    case ECONNREFUSED:
+	errx(1, "Couldn't connect. Check that btpd is running.");
+    default:
+	errx(1, "%s", strerror(error));
+    }
+}
+
+static void
+do_ipc_open(char *ipctok, struct ipc **ipc)
+{
+    switch (ipc_open(ipctok, ipc)) {
+    case 0:
+	break;
+    case EINVAL:
+	errx(1, "--ipc argument only takes letters and digits.");
+    case ENAMETOOLONG:
+	errx(1, "--ipc argument is too long.");
+    }
+}
+
+struct cb {
+    char *path;
+    uint8_t *piece_field;
+    uint32_t have;
+    struct metainfo *meta;
+};
+
+static void
+hash_cb(uint32_t index, uint8_t *hash, void *arg)
+{
+    struct cb *cb = arg;
+    if (hash != NULL)
+	if (bcmp(hash, cb->meta->piece_hash[index], SHA_DIGEST_LENGTH) == 0) {
+	    set_bit(cb->piece_field, index);
+            cb->have++;
+	}
+    printf("\rTested: %5.1f%%", 100.0 * (index + 1) / cb->meta->npieces);
+    fflush(stdout);
+}
+
+static int
+fd_cb(const char *path, int *fd, void *arg)
+{
+    struct cb *fp = arg;
+    return vopen(fd, O_RDONLY, "%s.d/%s", fp->path, path);
+}
+
+static void
+gen_ifile(char *path)
+{
+    int fd;
+    struct cb cb;
+    struct metainfo *mi;
+    size_t field_len;
+
+    if ((errno = load_metainfo(path, -1, 1, &mi)) != 0)
+	err(1, "load_metainfo: %s", path);
+
+    field_len = ceil(mi->npieces / 8.0);
+    cb.path = path;
+    cb.piece_field = calloc(1, field_len);
+    cb.have = 0;
+    cb.meta = mi;
+
+    if (cb.piece_field == NULL)
+	errx(1, "Out of memory.\n");
+
+    if ((errno = bts_hashes(mi, fd_cb, hash_cb, &cb)) != 0)
+	err(1, "bts_hashes");
+    printf("\nHave: %5.1f%%\n", 100.0 * cb.have / cb.meta->npieces);
+
+    if ((errno = vopen(&fd, O_WRONLY|O_CREAT, "%s.i", path)) != 0)
+	err(1, "opening %s.i", path);
+
+    if (ftruncate(fd,
+            field_len +
+	    (off_t)ceil(mi->npieces * mi->piece_length / (double)(1<<17))) < 0)
+	err(1, "ftruncate: %s", path);
+
+    if (write(fd, cb.piece_field, field_len) != field_len)
+	err(1, "write %s.i", path);
+
+    if (close(fd) < 0)
+	err(1, "close %s.i", path);
+
+    clear_metainfo(mi);
+    free(mi);
+}
+
+static struct option add_opts[] = {
+    { "ipc", required_argument, NULL, 1 },
+    { "help", required_argument, NULL, 2},
+    {NULL, 0, NULL, 0}
+};
+
+static void
+do_add(char *ipctok, char **paths, int npaths, char **out)
+{
+    struct ipc *ipc;
+    do_ipc_open(ipctok, &ipc);
+    handle_error(btpd_add(ipc, paths, npaths, out));
+    ipc_close(ipc);
+}
+
+static void
+cmd_add(int argc, char **argv)
+{
+    int ch;
+    char *ipctok = NULL;
+    while ((ch = getopt_long(argc, argv, "", add_opts, NULL)) != -1) {
+	switch(ch) {
+	case 1:
+	    ipctok = optarg;
+	    break;
+	default:
+	    usage();
+	}
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc < 1)
+	usage();
+
+    for (int i = 0; i < argc; i++) {
+	int64_t code;
+	char *res;
+	int fd;
+	char *path;
+	errno = vopen(&fd, O_RDONLY, "%s.i", argv[i]);
+	if (errno == ENOENT) {
+	    printf("Testing %s for content.\n", argv[i]);
+	    gen_ifile(argv[i]);
+	} else if (errno != 0)
+	    err(1, "open %s.i", argv[i]);
+	else
+	    close(fd);
+
+	if ((errno = canon_path(argv[i], &path)) != 0)
+	    err(1, "canon_path");
+	do_add(ipctok, &path, 1, &res);
+	free(path);
+	benc_dget_int64(benc_first(res), "code", &code);
+	if (code == EEXIST)
+	    printf("btpd already had %s.\n", argv[i]);
+	else if (code != 0) {
+	    printf("btpd indicates error: %s for %s.\n",
+	        strerror(code), argv[i]);
+	}
+        free(res);
+    }
+}
+
+static struct option del_opts[] = {
+    { "ipc", required_argument, NULL, 1 },
+    { "help", required_argument, NULL, 2},
+    {NULL, 0, NULL, 0}
+};
+
+static void
+do_del(char *ipctok, uint8_t (*hashes)[20], int nhashes, char **out)
+{
+    struct ipc *ipc;
+    do_ipc_open(ipctok, &ipc);
+    handle_error(btpd_del(ipc, hashes, nhashes, out));
+    ipc_close(ipc);
+}
+
+static void
+cmd_del(int argc, char **argv)
+{
+    int ch;
+    char *ipctok = NULL;
+    while ((ch = getopt_long(argc, argv, "", del_opts, NULL)) != -1) {
+	switch(ch) {
+	case 1:
+	    ipctok = optarg;
+	    break;
+	default:
+	    usage();
+	}
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc < 1)
+	usage();
+
+    uint8_t hashes[argc][20];
+    char *res;
+    const char *d;
+
+    for (int i = 0; i < argc; i++) {
+	struct metainfo *mi;
+	if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0)
+	    err(1, "load_metainfo: %s", argv[i]);
+	bcopy(mi->info_hash, hashes[i], 20);
+	clear_metainfo(mi);
+	free(mi);	
+    }
+    
+    do_del(ipctok, hashes, argc, &res);
+    d = benc_first(res);
+    for (int i = 0; i < argc; i++) {
+	int64_t code;
+	benc_dget_int64(d, "code", &code);
+	if (code == ENOENT)
+	    printf("btpd didn't have %s.\n", argv[i]);
+	else if (code != 0) {
+	    printf("btpd indicates error: %s for %s.\n",
+		   strerror(code), argv[i]);
+	}
+	d = benc_next(d);
+    }
+    free(res);
+}
+
+static struct option die_opts[] = {
+    { "ipc", required_argument, NULL, 1 },
+    { "help", no_argument, NULL, 2 },
+    {NULL, 0, NULL, 0}
+};
+
+static void
+do_die(char *ipctok)
+{
+    struct ipc *ipc;
+    do_ipc_open(ipctok, &ipc);
+    handle_error(btpd_die(ipc));
+    ipc_close(ipc);
+}
+
+static void
+cmd_die(int argc, char **argv)
+{
+    int ch;
+    char *ipctok = NULL;
+
+    while ((ch = getopt_long(argc, argv, "", die_opts, NULL)) != -1) {
+	switch (ch) {
+	case 1:
+	    ipctok = optarg;
+	    break;
+	default:
+	    usage();
+	}
+    }
+    do_die(ipctok);
+}
+
+static struct option stat_opts[] = {
+    { "ipc", required_argument, NULL, 1 },
+    { "help", no_argument, NULL, 2 },
+    {NULL, 0, NULL, 0}
+};
+
+static void
+do_stat(char *ipctok, char **out)
+{
+    struct ipc *ipc;
+    do_ipc_open(ipctok, &ipc);
+    handle_error(btpd_stat(ipc, out));
+    ipc_close(ipc);
+}
+
+struct tor {
+    char *path;
+    uint8_t hash[20];
+    uint64_t down;
+    uint64_t up;
+    uint64_t npeers;
+    uint64_t npieces;
+    uint64_t have_npieces;
+    uint64_t seen_npieces;
+};
+
+struct tor **parse_tors(char *res, uint8_t (*hashes)[20], int nhashes)
+{
+    struct tor **tors;
+    int64_t num;
+    const char *p;
+    benc_dget_int64(res, "ntorrents", &num);
+    benc_dget_lst(res, "torrents", &p);
+
+    tors = calloc(sizeof(*tors), num + 1);
+    int i = 0;
+    for (p = benc_first(p); p; p = benc_next(p)) {
+        int j;
+        const char *hash;
+        benc_dget_str(p, "hash", &hash, NULL);
+
+        for (j = 0; j < nhashes; j++) {
+            if (bcmp(hashes[i], hash, 20) == 0)
+                break;
+        }
+        if (j < nhashes || nhashes == 0) {
+            tors[i] = calloc(sizeof(*tors[i]), 1);
+            bcopy(hash, tors[i]->hash, 20);
+            benc_dget_int64(p, "down", &tors[i]->down);
+            benc_dget_int64(p, "up", &tors[i]->up);
+            benc_dget_int64(p, "npeers", &tors[i]->npeers);
+            benc_dget_int64(p, "npieces", &tors[i]->npieces);
+            benc_dget_int64(p, "have npieces", &tors[i]->have_npieces);
+            benc_dget_int64(p, "seen npieces", &tors[i]->seen_npieces);
+            benc_dget_strz(p, "path", &tors[i]->path, NULL);
+            i++;
+        }
+    }
+    return tors;
+}
+
+static void
+free_tors(struct tor **tors)
+{
+    for (int i = 0; tors[i] != NULL; i++) {
+        free(tors[i]->path);
+        free(tors[i]);
+    }
+    free(tors);
+}
+
+static void
+print_stat(struct tor *cur, struct tor *old, int wait)
+{
+    if (old == NULL) {
+        printf("%5.1f%% %5.1f%% %6.1fM      - kB/s %6.1fM      - kB/s %4u\n",
+               100 * cur->seen_npieces / (double)cur->npieces,
+               100 * cur->have_npieces / (double)cur->npieces,
+               cur->down / (double)(1 << 20),
+               cur->up / (double)(1 << 20),
+               (unsigned)cur->npeers);
+    } else {
+        printf("%5.1f%% %5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u\n",
+               100 * cur->seen_npieces / (double)cur->npieces,
+               100 * cur->have_npieces / (double)cur->npieces,
+               cur->down / (double)(1 << 20),
+               (cur->down - old->down) / (double)wait / (double)(1 << 10),
+               cur->up / (double)(1 << 20),
+               (cur->up - old->up) / (double)wait / (double)(1 << 10),
+               (unsigned)cur->npeers
+            );
+    }
+}
+
+static void
+grok_stat(char *ipctok, int iflag, int wait,
+          uint8_t (*hashes)[20], int nhashes)
+{
+    int i, j;
+    char *res;
+    struct tor **cur, **old = NULL;
+    struct tor curtot, oldtot;
+
+again:
+    do_stat(ipctok, &res);
+    cur = parse_tors(res, hashes, nhashes);
+    free(res);
+
+    if (iflag) {
+        for (i = 0; cur[i] != NULL; i++) {
+            if (old == NULL) {
+                printf("%s:\n", rindex(cur[i]->path, '/') + 1);
+                print_stat(cur[i], NULL, wait);
+            } else {
+                for (j = 0; old[j] != NULL; j++)
+                    if (bcmp(cur[i]->hash, old[j]->hash, 20) == 0)
+                        break;
+                printf("%s:\n", rindex(cur[i]->path, '/') + 1);
+                print_stat(cur[i], old[j], wait);
+            }
+        }
+    }
+
+    bzero(&curtot, sizeof(curtot));
+    for (i = 0; cur[i] != NULL; i++) {
+        curtot.down += cur[i]->down;
+        curtot.up += cur[i]->up;
+        curtot.npeers += cur[i]->npeers;
+        curtot.npieces += cur[i]->npieces;
+        curtot.have_npieces += cur[i]->have_npieces;
+        curtot.seen_npieces += cur[i]->seen_npieces;
+    }
+    if (iflag)
+        printf("Total:\n");
+    if (old != NULL)
+        print_stat(&curtot, &oldtot, wait);
+    else
+        print_stat(&curtot, NULL, wait);
+
+    if (wait) {
+        if (old != NULL)
+            free_tors(old);
+        old = cur;
+        oldtot = curtot;
+        sleep(wait);
+        goto again;
+    }
+    free_tors(cur);
+}
+
+static void
+cmd_stat(int argc, char **argv)
+{
+    int ch;
+    char *ipctok = NULL;
+    int wait = 0;
+    int iflag = 0;
+
+    while ((ch = getopt_long(argc, argv, "iw:", stat_opts, NULL)) != -1) {
+	switch (ch) {
+        case 'i':
+            iflag = 1;
+            break;
+        case 'w':
+            wait = atoi(optarg);
+            if (wait <= 0)
+                errx(1, "-w argument must be an integer > 0.");
+            break;
+	case 1:
+	    ipctok = optarg;
+	    break;
+	default:
+	    usage();
+	}
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc > 0) {
+        uint8_t hashes[argc][20];
+        for (int i = 0; i < argc; i++) {
+            struct metainfo *mi;
+            if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0)
+                err(1, "load_metainfo: %s", argv[i]);
+            bcopy(mi->info_hash, hashes[i], 20);
+            clear_metainfo(mi);
+            free(mi);
+        }
+        grok_stat(ipctok, iflag, wait, hashes, argc);
+    } else
+        grok_stat(ipctok, iflag, wait, NULL, 0);
+}
+
+static struct option list_opts[] = {
+    { "ipc", required_argument, NULL, 1 },
+    { "help", no_argument, NULL, 2 },
+    {NULL, 0, NULL, 0}
+};
+
+static void
+cmd_list(int argc, char **argv)
+{
+    int ch;
+    char *ipctok = NULL;
+
+    while ((ch = getopt_long(argc, argv, "", list_opts, NULL)) != -1) {
+	switch (ch) {
+	case 1:
+	    ipctok = optarg;
+	    break;
+	default:
+	    usage();
+	}
+    }
+    char *res;
+    const char *p;
+    char *path;
+    do_stat(ipctok, &res);
+
+    benc_dget_lst(res, "torrents", &p);
+    int count = 0;
+    for (p = benc_first(p); p; p = benc_next(p)) {
+        count++;
+        benc_dget_strz(p, "path", &path, NULL);
+        printf("%s\n", path);
+        free(path);
+    }
+    printf("%d torrents.\n", count);
+}
+
+static struct {
+    const char *name;
+    void (*fun)(int, char **);
+} cmd_table[] = {
+    { "add", cmd_add },
+    { "del", cmd_del },
+    { "die", cmd_die },
+    { "list", cmd_list},
+    { "stat", cmd_stat }
+};
+
+static int ncmds = sizeof(cmd_table) / sizeof(cmd_table[0]);
+
+int
+main(int argc, char **argv)
+{
+    if (argc < 2)
+	usage();
+    
+    int found = 0;
+    for (int i = 0; !found && i < ncmds; i++) {
+	if (strcmp(argv[1], cmd_table[i].name) == 0) {
+	    found = 1;
+	    cmd_table[i].fun(argc - 1, argv + 1);
+	}
+    }
+
+    if (!found)
+	usage();
+
+    return 0;
+}
diff --git a/cli/btinfo.c b/cli/btinfo.c
new file mode 100644
index 0000000..bd4a33b
--- /dev/null
+++ b/cli/btinfo.c
@@ -0,0 +1,53 @@
+#include <sys/types.h>
+
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "metainfo.h"
+
+static void
+usage()
+{
+    fprintf(stderr, "Usage: btinfo file ...\n\n");
+    exit(1);
+}
+
+static struct option longopts[] = {
+    { "help", no_argument, NULL, 1 },
+    { NULL, 0, NULL, 0 }
+};
+
+int
+main(int argc, char **argv)
+{
+    int ch;
+
+    while ((ch = getopt_long(argc, argv, "", longopts, NULL)) != -1)
+	usage();
+
+    argc -= optind;
+    argv += optind;
+
+    if (argc < 1)
+	usage();
+
+    while (argc > 0) {
+	struct metainfo *mi;
+
+	if ((errno = load_metainfo(*argv, -1, 1, &mi)) != 0)
+	    err(1, "load_metainfo: %s", *argv);
+
+	print_metainfo(mi);
+	clear_metainfo(mi);
+	free(mi);
+
+	argc--;
+	argv++;
+    }
+
+    return 0;
+}
diff --git a/cli/btpd_if.c b/cli/btpd_if.c
new file mode 100644
index 0000000..9990446
--- /dev/null
+++ b/cli/btpd_if.c
@@ -0,0 +1,221 @@
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "benc.h"
+#include "iobuf.h"
+#include "btpd_if.h"
+
+int
+ipc_open(const char *key, struct ipc **out)
+{
+    size_t plen;
+    size_t keylen;
+    struct ipc *res;
+
+    if (key == NULL)
+	key = "default";
+    keylen = strlen(key);
+    for (int i = 0; i < keylen; i++)
+	if (!isalnum(key[i]))
+	    return EINVAL;
+
+    res = malloc(sizeof(*res));
+    if (res == NULL)
+	return ENOMEM;
+
+    plen = sizeof(res->addr.sun_path);
+    if (snprintf(res->addr.sun_path, plen,
+		 "/tmp/btpd_%u_%s", geteuid(), key) >= plen) {
+	free(res);
+	return ENAMETOOLONG;
+    }
+    res->addr.sun_family = AF_UNIX;
+    *out = res;
+    return 0;
+}
+
+int
+ipc_close(struct ipc *ipc)
+{
+    free(ipc);
+    return 0;
+}
+
+static int
+ipc_connect(struct ipc *ipc, FILE **out)
+{
+    FILE *fp;
+    int sd;
+    int error;
+
+    if ((sd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+	return errno;
+
+    if (connect(sd, (struct sockaddr *)&ipc->addr, sizeof(ipc->addr)) == -1)
+	goto error;
+
+    if ((fp = fdopen(sd, "r+")) == NULL)
+	goto error;
+
+    *out = fp;
+    return 0;
+error:
+    error = errno;
+    close(sd);
+    return error;
+}
+
+static int
+ipc_response(FILE *fp, char **out, uint32_t *len)
+{
+    uint32_t size;
+    char *buf;
+
+    if (fread(&size, sizeof(size), 1, fp) != 1) {
+	if (ferror(fp))
+	    return errno;
+	else
+	    return ECONNRESET;
+    }
+
+    if (size == 0)
+	return EINVAL;
+
+    if ((buf = malloc(size)) == NULL)
+	return ENOMEM;
+
+    if (fread(buf, 1, size, fp) != size) {
+	if (ferror(fp))
+	    return errno;
+	else
+	    return ECONNRESET;
+    }
+
+    *out = buf;
+    *len = size;
+    return 0;
+}
+
+static int
+ipc_req_res(struct ipc *ipc,
+	    const char *req, uint32_t qlen,
+	    char **res, uint32_t *rlen)
+{
+    FILE *fp;
+    int error;
+
+    if ((error = ipc_connect(ipc, &fp)) != 0)
+	return error;
+
+    if (fwrite(&qlen, sizeof(qlen), 1, fp) != 1)
+	goto error;
+    if (fwrite(req, 1, qlen, fp) != qlen)
+	goto error;
+    if (fflush(fp) != 0)
+	goto error;
+    if ((errno = ipc_response(fp, res, rlen)) != 0)
+	goto error;
+    if ((errno = benc_validate(*res, *rlen)) != 0)
+	goto error;
+
+    fclose(fp);
+    return 0;
+error:
+    error = errno;
+    fclose(fp);
+    return error;
+}
+
+int
+btpd_die(struct ipc *ipc)
+{
+    int error;
+    char *response = NULL;
+    const char shutdown[] = "l3:diee";
+    uint32_t size = sizeof(shutdown) - 1;
+    uint32_t rsiz;
+
+    if ((error = ipc_req_res(ipc, shutdown, size, &response, &rsiz)) != 0)
+	return error;
+
+    error = benc_validate(response, rsiz);
+
+    if (error == 0) {
+	int64_t tmp;
+        benc_dget_int64(response, "code", &tmp);
+	error = tmp;
+    }
+
+    free(response);
+    return error;
+}
+
+int
+btpd_add(struct ipc *ipc, char **paths, unsigned npaths, char **out)
+{
+    int error;
+    struct io_buffer iob;
+    char *res = NULL;
+    uint32_t reslen;
+
+    buf_init(&iob, 1024);
+    buf_print(&iob, "l3:add");
+    for (unsigned i = 0; i < npaths; i++) {
+        int plen = strlen(paths[i]);
+        buf_print(&iob, "%d:", plen);
+        buf_write(&iob, paths[i], plen);
+    }
+    buf_print(&iob, "e");
+    
+    error = ipc_req_res(ipc, iob.buf, iob.buf_off, &res, &reslen);
+    free(iob.buf);
+    if (error == 0)
+	*out = res;
+    
+    return error;
+}
+
+int
+btpd_stat(struct ipc *ipc, char **out)
+{
+    const char cmd[] = "l4:state";
+    uint32_t cmdlen = sizeof(cmd) - 1;
+    char *res;
+    uint32_t reslen;
+
+    if ((errno = ipc_req_res(ipc, cmd, cmdlen, &res, &reslen)) != 0)
+        return errno;
+    *out = res;
+    return 0;
+}
+
+int
+btpd_del(struct ipc *ipc, uint8_t (*hash)[20], unsigned nhashes, char **out)
+{
+    int error;
+    struct io_buffer iob;
+    char *res = NULL;
+    uint32_t reslen;
+
+    buf_init(&iob, 1024);
+    buf_write(&iob, "l3:del", 6);
+    for (unsigned i = 0; i < nhashes; i++) {
+        buf_write(&iob, "20:", 3);
+        buf_write(&iob, hash[i], 20);
+    }
+    buf_write(&iob, "e", 1);
+    
+    error = ipc_req_res(ipc, iob.buf, iob.buf_off, &res, &reslen);
+    free(iob.buf);
+    if (error != 0)
+	return error;
+
+    *out = res;
+    return 0;
+}
diff --git a/cli/btpd_if.h b/cli/btpd_if.h
new file mode 100644
index 0000000..bda9739
--- /dev/null
+++ b/cli/btpd_if.h
@@ -0,0 +1,21 @@
+#ifndef BTPD_IF_H
+#define BTPD_IF_H
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+struct ipc {
+    struct sockaddr_un addr;
+};
+
+int ipc_open(const char *key, struct ipc **out);
+int ipc_close(struct ipc *ipc);
+
+int btpd_add(struct ipc *ipc, char **path, unsigned npaths, char **out);
+int btpd_del(struct ipc *ipc, uint8_t (*hash)[20],
+	     unsigned nhashes, char **out);
+int btpd_die(struct ipc *ipc);
+int btpd_stat(struct ipc *ipc, char **out);
+
+#endif