about summary refs log tree commit diff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Makefile.am2
-rw-r--r--cli/add.c87
-rw-r--r--cli/btcli.c403
-rw-r--r--cli/btcli.h44
-rw-r--r--cli/btinfo.c35
-rw-r--r--cli/del.c30
-rw-r--r--cli/kill.c35
-rw-r--r--cli/list.c124
-rw-r--r--cli/start.c27
-rw-r--r--cli/stat.c193
-rw-r--r--cli/stop.c27
11 files changed, 633 insertions, 374 deletions
diff --git a/cli/Makefile.am b/cli/Makefile.am
index 84e18f9..3222e95 100644
--- a/cli/Makefile.am
+++ b/cli/Makefile.am
@@ -5,7 +5,7 @@ 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_SOURCES=btcli.c btcli.h add.c del.c list.c kill.c start.c stop.c stat.c
 btcli_LDADD=../misc/libmisc.a -lcrypto -lm
 btcli_CPPFLAGS=-I$(top_srcdir)/misc @openssl_CPPFLAGS@
 btcli_LDFLAGS=@openssl_LDFLAGS@
diff --git a/cli/add.c b/cli/add.c
new file mode 100644
index 0000000..34edb1c
--- /dev/null
+++ b/cli/add.c
@@ -0,0 +1,87 @@
+#include "btcli.h"
+
+void
+usage_add(void)
+{
+    printf(
+        "Add torrents to btpd.\n"
+        "\n"
+        "Usage: add [--topdir] -d dir file\n"
+        "       add file ...\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tOne or more torrents to add.\n"
+        "\n"
+        "Options:\n"
+        "-d dir\n"
+        "\tUse the dir for content.\n"
+        "\n"
+        "--topdir\n"
+        "\tAppend the torrent top directory (if any) to the content path.\n"
+        "\tThis option cannot be used without the '-d' option.\n"
+        "\n"
+        );
+    exit(1);
+}
+
+static struct option add_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    { "topdir", no_argument, NULL, 'T'},
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_add(int argc, char **argv)
+{
+    int ch, topdir = 0;
+    size_t dirlen = 0;
+    char *dir = NULL, *name = NULL;
+
+    while ((ch = getopt_long(argc, argv, "d:n:", add_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'T':
+            topdir = 1;
+            break;
+        case 'd':
+            dir = optarg;
+            if ((dirlen = strlen(dir)) == 0)
+                errx(1, "bad option value for -d");
+            break;
+        case 'n':
+            name = optarg;
+            break;
+        default:
+            usage_add();
+        }
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc != 1 || dir == NULL)
+        usage_add();
+
+    btpd_connect();
+    char *mi;
+    size_t mi_size;
+    char dpath[PATH_MAX];
+    struct io_buffer iob;
+
+    if ((mi = mi_load(argv[0], &mi_size)) == NULL)
+        err(1, "error loading '%s'", argv[0]);
+
+    buf_init(&iob, PATH_MAX);
+    buf_write(&iob, dir, dirlen);
+    if (topdir) {
+        size_t tdlen;
+        const char *td =
+            benc_dget_mem(benc_dget_dct(mi, "info"), "name", &tdlen);
+        buf_swrite(&iob, "/");
+        buf_write(&iob, td, tdlen);
+    }
+    buf_swrite(&iob, "");
+    if (realpath(iob.buf, dpath) == NULL)
+        err(1, "realpath '%s'", dpath);
+    handle_ipc_res(btpd_add(ipc, mi, mi_size, dpath, name), argv[0]);
+    return;
+}
diff --git a/cli/btcli.c b/cli/btcli.c
index 95ae487..f8b6a81 100644
--- a/cli/btcli.c
+++ b/cli/btcli.c
@@ -1,21 +1,4 @@
-#include <sys/types.h>
-#include <sys/stat.h>
-
-#include <err.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <getopt.h>
-#include <inttypes.h>
-#include <limits.h>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "btpd_if.h"
-#include "metainfo.h"
-#include "subr.h"
+#include "btcli.h"
 
 const char *btpd_dir;
 struct ipc *ipc;
@@ -27,377 +10,55 @@ btpd_connect(void)
         err(1, "cannot open connection to btpd in %s", btpd_dir);
 }
 
-enum ipc_code
-handle_ipc_res(enum ipc_code code, const char *target)
+enum ipc_err
+handle_ipc_res(enum ipc_err code, const char *target)
 {
     switch (code) {
     case IPC_OK:
         break;
-    case IPC_FAIL:
-        warnx("btpd couldn't execute the requested operation for %s", target);
-        break;
-    case IPC_ERROR:
-        warnx("btpd encountered an error for %s", target);
-        break;
-    default:
+    case IPC_COMMERR:
         errx(1, "fatal error in communication with btpd");
+    default:
+        warnx("btpd response for '%s': %s", target, ipc_strerror(code));
     }
     return code;
 }
 
 char
-state_char(struct tpstat *ts)
+tstate_char(enum ipc_tstate ts)
 {
-    switch (ts->state) {
-    case T_STARTING:
+    switch (ts) {
+    case IPC_TSTATE_INACTIVE:
+        return 'I';
+    case IPC_TSTATE_START:
         return '+';
-    case T_ACTIVE:
-        return ts->pieces_got == ts->torrent_pieces ? 'S' : 'L';
-    case T_STOPPING:
+    case IPC_TSTATE_STOP:
         return '-';
-    default:
-        return ' ';
+    case IPC_TSTATE_LEECH:
+        return 'L';
+    case IPC_TSTATE_SEED:
+        return 'S';
     }
+    errx(1, "bad state");
 }
 
-void
-print_stat(struct tpstat *ts)
-{
-    printf("%c %5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %4u %5.1f%%",
-        state_char(ts),
-        floor(1000.0 * ts->content_got / ts->content_size) / 10,
-        (double)ts->downloaded / (1 << 20),
-        (double)ts->rate_down / (20 << 10),
-        (double)ts->uploaded / (1 << 20),
-        (double)ts->rate_up / (20 << 10),
-        ts->peers,
-        floor(1000.0 * ts->pieces_seen / ts->torrent_pieces) / 10);
-    if (ts->tr_errors > 0)
-        printf(" E%u", ts->tr_errors);
-    printf("\n");
-}
-
-void
-usage_add(void)
-{
-    printf(
-        "Add torrents to btpd.\n"
-        "\n"
-        "Usage: add [--topdir] -d dir file\n"
-        "       add file ...\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tOne or more torrents to add.\n"
-        "\n"
-        "Options:\n"
-        "-d dir\n"
-        "\tUse the dir for content.\n"
-        "\n"
-        "--topdir\n"
-        "\tAppend the torrent top directory (if any) to the content path.\n"
-        "\tThis option cannot be used without the '-d' option.\n"
-        "\n"
-        );
-    exit(1);
-}
-
-struct option add_opts [] = {
-    { "help", no_argument, NULL, 'H' },
-    { "topdir", no_argument, NULL, 'T'},
-    {NULL, 0, NULL, 0}
-};
-
 int
-content_link(uint8_t *hash, char *buf)
-{
-    int n;
-    char relpath[41];
-    char path[PATH_MAX];
-    for (int i = 0; i < 20; i++)
-        snprintf(relpath + i * 2, 3, "%.2x", hash[i]);
-    snprintf(path, PATH_MAX, "%s/torrents/%s/content", btpd_dir, relpath);
-    if ((n = readlink(path, buf, PATH_MAX)) == -1)
-        return errno;
-    buf[min(n, PATH_MAX)] = '\0';
-    return 0;
-}
-
-void
-cmd_add(int argc, char **argv)
-{
-    int ch, topdir = 0;
-    char *dir = NULL;
-
-    while ((ch = getopt_long(argc, argv, "d:", add_opts, NULL)) != -1) {
-        switch (ch) {
-        case 'T':
-            topdir = 1;
-            break;
-        case 'd':
-            dir = optarg;
-            break;
-        default:
-            usage_add();
-        }
-    }
-    argc -= optind;
-    argv += optind;
-
-    if (argc < 1 || (topdir == 1 && dir == NULL) || (dir != NULL && argc > 1))
-        usage_add();
-
-    btpd_connect();
-    for (int i = 0; i < argc; i++) {
-        struct metainfo *mi;
-        char rdpath[PATH_MAX], dpath[PATH_MAX], fpath[PATH_MAX];
-
-        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
-            warn("error loading torrent %s", argv[i]);
-            continue;
-        }
-
-        if ((topdir &&
-                !(mi->nfiles == 1
-                    && strcmp(mi->name, mi->files[0].path) == 0)))
-            snprintf(dpath, PATH_MAX, "%s/%s", dir, mi->name);
-        else if (dir != NULL)
-            strncpy(dpath, dir, PATH_MAX);
-        else {
-            if (content_link(mi->info_hash, dpath) != 0) {
-                warnx("unknown content dir for %s", argv[i]);
-                errx(1, "use the '-d' option");
-            }
-        }
-
-        if (mkdir(dpath, 0777) != 0 && errno != EEXIST)
-            err(1, "couldn't create directory %s", dpath);
-
-        if (realpath(dpath, rdpath) == NULL)
-            err(1, "path error on %s", dpath);
-
-        if (realpath(argv[i], fpath) == NULL)
-            err(1, "path error on %s", fpath);
-
-        handle_ipc_res(btpd_add(ipc, mi->info_hash, fpath, rdpath), argv[i]);
-        clear_metainfo(mi);
-        free(mi);
-    }
-}
-
-void
-usage_del(void)
-{
-    printf(
-        "Remove torrents from btpd.\n"
-        "\n"
-        "Usage: del file ...\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tThe torrents to remove.\n"
-        "\n");
-    exit(1);
-}
-
-void
-cmd_del(int argc, char **argv)
-{
-    if (argc < 2)
-        usage_del();
-
-    btpd_connect();
-    for (int i = 1; i < argc; i++) {
-        struct metainfo *mi;
-        if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0) {
-            warn("error loading torrent %s", argv[i]);
-            continue;
-        }
-        handle_ipc_res(btpd_del(ipc, mi->info_hash), argv[i]);
-        clear_metainfo(mi);
-        free(mi);
-    }
-}
-
-void
-usage_kill(void)
-{
-    printf(
-        "Shutdown btpd.\n"
-        "\n"
-        "Usage: kill [seconds]\n"
-        "\n"
-        "Arguments:\n"
-        "seconds\n"
-        "\tThe number of seconds btpd waits before giving up on unresponsive\n"
-        "\ttrackers.\n"
-        "\n"
-        );
-    exit(1);
-}
-
-void
-cmd_kill(int argc, char **argv)
-{
-    int seconds = -1;
-    char *endptr;
-
-    if (argc == 2) {
-        seconds = strtol(argv[1], &endptr, 10);
-        if (strlen(argv[1]) > endptr - argv[1] || seconds < 0)
-            usage_kill();
-    } else if (argc > 2)
-        usage_kill();
-
-    btpd_connect();
-    handle_ipc_res(btpd_die(ipc, seconds), "kill");
-}
-
-void
-usage_list(void)
-{
-    printf(
-        "List active torrents.\n"
-        "\n"
-        "Usage: list\n"
-        "\n"
-        );
-    exit(1);
-}
-
-void
-cmd_list(int argc, char **argv)
-{
-    struct btstat *st;
-
-    if (argc > 1)
-        usage_list();
-
-    btpd_connect();
-    if (handle_ipc_res(btpd_stat(ipc, &st), "list") != IPC_OK)
-        exit(1);
-    for (int i = 0; i < st->ntorrents; i++) {
-        struct tpstat *ts = &st->torrents[i];
-        printf("%c. %s\n", state_char(ts), ts->name);
-    }
-    printf("%u torrent%s.\n", st->ntorrents,
-        st->ntorrents == 1 ? "" : "s");
-}
-
-void
-usage_stat(void)
-{
-    printf(
-        "Display stats for active torrents.\n"
-        "The displayed stats are:\n"
-        "%% got, MB down, rate down. MB up, rate up\n"
-        "peer count, %% of pieces seen, tracker errors\n"
-        "\n"
-        "Usage: stat [-i] [-w seconds] [file ...]\n"
-        "\n"
-        "Arguments:\n"
-        "file ...\n"
-        "\tOnly display stats for the given torrent(s).\n"
-        "\n"
-        "Options:\n"
-        "-i\n"
-        "\tDisplay individual lines for each torrent.\n"
-        "\n"
-        "-w n\n"
-        "\tDisplay stats every n seconds.\n"
-        "\n");
-    exit(1);
-}
-
-void
-do_stat(int individual, int seconds, int hash_count, uint8_t (*hashes)[20])
-{
-    struct btstat *st;
-    struct tpstat tot;
-again:
-    bzero(&tot, sizeof(tot));
-    tot.state = -1;
-    if (handle_ipc_res(btpd_stat(ipc, &st), "stat") != IPC_OK)
-        exit(1);
-    for (int i = 0; i < st->ntorrents; i++) {
-        struct tpstat *cur = &st->torrents[i];
-        if (hash_count > 0) {
-            int found = 0;
-            for (int h = 0; !found && h < hash_count; h++)
-                if (bcmp(cur->hash, hashes[h], 20) == 0)
-                    found = 1;
-            if (!found)
-                continue;
-        }
-        tot.uploaded += cur->uploaded;
-        tot.downloaded += cur->downloaded;
-        tot.rate_up += cur->rate_up;
-        tot.rate_down += cur->rate_down;
-        tot.peers += cur->peers;
-        tot.pieces_seen += cur->pieces_seen;
-        tot.torrent_pieces += cur->torrent_pieces;
-        tot.content_got += cur->content_got;
-        tot.content_size += cur->content_size;
-        if (cur->tr_errors > 0)
-            tot.tr_errors++;
-        if (individual) {
-            printf("%s:\n", cur->name);
-            print_stat(cur);
-        }
-    }
-    free_btstat(st);
-    if (individual)
-        printf("Total:\n");
-    print_stat(&tot);
-    if (seconds > 0) {
-        sleep(seconds);
-        goto again;
-    }
-}
-
-struct option stat_opts [] = {
-    { "help", no_argument, NULL, 'H' },
-    {NULL, 0, NULL, 0}
-};
-
-void
-cmd_stat(int argc, char **argv)
+torrent_spec(char *arg, struct ipc_torrent *tp)
 {
-    int ch;
-    int wflag = 0, iflag = 0, seconds = 0;
-    uint8_t (*hashes)[20] = NULL;
-    char *endptr;
-    while ((ch = getopt_long(argc, argv, "iw:", stat_opts, NULL)) != -1) {
-        switch (ch) {
-        case 'i':
-            iflag = 1;
-            break;
-        case 'w':
-            wflag = 1;
-            seconds = strtol(optarg, &endptr, 10);
-            if (strlen(optarg) > endptr - optarg || seconds < 1)
-                usage_stat();
-            break;
-        default:
-            usage_stat();
-        }
+    char *p;
+    tp->u.num = strtoul(arg, &p, 10);
+    if (*p == '\0') {
+        tp->by_hash = 0;
+        return 1;
     }
-    argc -= optind;
-    argv += optind;
-
-    if (argc > 0) {
-        hashes = malloc(argc * 20);
-        for (int i = 0; i < argc; i++) {
-            struct metainfo *mi;
-            if ((errno = load_metainfo(argv[i], -1, 0, &mi)) != 0)
-                err(1, "error loading torrent %s", argv[i]);
-            bcopy(mi->info_hash, hashes[i], 20);
-            clear_metainfo(mi);
-            free(mi);
-        }
+    if ((p = mi_load(arg, NULL)) == NULL) {
+        warnx("bad torrent '%s' (%s)", arg, strerror(errno));
+        return 0;
     }
-    btpd_connect();
-    do_stat(iflag, seconds, argc, hashes);
+    tp->by_hash = 1;
+    mi_info_hash(p, tp->u.hash);
+    free(p);
+    return 1;
 }
 
 struct {
@@ -409,6 +70,8 @@ struct {
     { "del", cmd_del, usage_del },
     { "kill", cmd_kill, usage_kill },
     { "list", cmd_list, usage_list },
+    { "start", cmd_start, usage_start },
+    { "stop", cmd_stop, usage_stop },
     { "stat", cmd_stat, usage_stat }
 };
 
@@ -434,7 +97,9 @@ usage(void)
         "del\n"
         "kill\n"
         "list\n"
+        "start\n"
         "stat\n"
+        "stop\n"
         "\n");
     exit(1);
 }
diff --git a/cli/btcli.h b/cli/btcli.h
new file mode 100644
index 0000000..5b64662
--- /dev/null
+++ b/cli/btcli.h
@@ -0,0 +1,44 @@
+#ifndef BTCLI_H
+#define BTCLI_H
+
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "btpd_if.h"
+#include "metainfo.h"
+#include "subr.h"
+#include "benc.h"
+#include "iobuf.h"
+#include "queue.h"
+
+extern const char *btpd_dir;
+extern struct ipc *ipc;
+
+void btpd_connect(void);
+enum ipc_err handle_ipc_res(enum ipc_err err, const char *target);
+
+char tstate_char(enum ipc_tstate ts);
+int torrent_spec(char *arg, struct ipc_torrent *tp);
+
+void usage_add(void);
+void cmd_add(int argc, char **argv);
+void usage_del(void);
+void cmd_del(int argc, char **argv);
+void usage_list(void);
+void cmd_list(int argc, char **argv);
+void usage_stat(void);
+void cmd_stat(int argc, char **argv);
+void usage_kill(void);
+void cmd_kill(int argc, char **argv);
+void usage_start(void);
+void cmd_start(int argc, char **argv);
+void usage_stop(void);
+void cmd_stop(int argc, char **argv);
+
+#endif
diff --git a/cli/btinfo.c b/cli/btinfo.c
index f451c85..cfd1991 100644
--- a/cli/btinfo.c
+++ b/cli/btinfo.c
@@ -8,6 +8,7 @@
 #include <stdlib.h>
 
 #include "metainfo.h"
+#include "subr.h"
 
 static void
 usage()
@@ -21,11 +22,38 @@ static struct option longopts[] = {
     { NULL, 0, NULL, 0 }
 };
 
+static void
+print_metainfo(const char *mi)
+{
+    uint8_t hash[20];
+    char hex[SHAHEXSIZE];
+    char *name = mi_name(mi);
+    unsigned nfiles = mi_nfiles(mi);
+    struct mi_file *files = mi_files(mi);
+    struct mi_announce *ann = mi_announce(mi);
+    for (int i = 0; i < ann->ntiers; i++)
+        for (int j = 0; j < ann->tiers[i].nurls; j++)
+            printf("%d: %s\n", i, ann->tiers[i].urls[j]);
+    printf("\n");
+    mi_free_announce(ann);
+    mi_info_hash(mi, hash);
+    bin2hex(hash, hex, 20);
+    printf("name: %s\n", name);
+    printf("info hash: %s\n", hex);
+    printf("length: %jd\n", (intmax_t)mi_total_length(mi));
+    printf("piece length: %jd\n", (intmax_t)mi_piece_length(mi));
+    printf("files: %u\n", nfiles);
+    for (unsigned i = 0; i < nfiles; i++)
+        printf("%s(%jd)\n", files[i].path, (intmax_t)files[i].length);
+    free(name);
+}
+
 int
 main(int argc, char **argv)
 {
     int ch;
 
+    srandom(time(NULL));
     while ((ch = getopt_long(argc, argv, "", longopts, NULL)) != -1)
         usage();
 
@@ -36,13 +64,12 @@ main(int argc, char **argv)
         usage();
 
     while (argc > 0) {
-        struct metainfo *mi;
+        char *mi = NULL;
 
-        if ((errno = load_metainfo(*argv, -1, 1, &mi)) != 0)
-            err(1, "load_metainfo: %s", *argv);
+        if ((mi = mi_load(*argv, NULL)) == NULL)
+            err(1, "mi_load: %s", *argv);
 
         print_metainfo(mi);
-        clear_metainfo(mi);
         free(mi);
 
         argc--;
diff --git a/cli/del.c b/cli/del.c
new file mode 100644
index 0000000..eaa0a1e
--- /dev/null
+++ b/cli/del.c
@@ -0,0 +1,30 @@
+#include "btcli.h"
+
+void
+usage_del(void)
+{
+    printf(
+        "Remove torrents from btpd.\n"
+        "\n"
+        "Usage: del torrent ...\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tThe torrents to remove.\n"
+        "\n");
+    exit(1);
+}
+
+void
+cmd_del(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_del();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_del(ipc, &t), argv[i]);
+}
diff --git a/cli/kill.c b/cli/kill.c
new file mode 100644
index 0000000..b2c6862
--- /dev/null
+++ b/cli/kill.c
@@ -0,0 +1,35 @@
+#include "btcli.h"
+
+void
+usage_kill(void)
+{
+    printf(
+        "Shutdown btpd.\n"
+        "\n"
+        "Usage: kill [seconds]\n"
+        "\n"
+        "Arguments:\n"
+        "seconds\n"
+        "\tThe number of seconds btpd waits before giving up on unresponsive\n"
+        "\ttrackers.\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_kill(int argc, char **argv)
+{
+    int seconds = -1;
+    char *endptr;
+
+    if (argc == 2) {
+        seconds = strtol(argv[1], &endptr, 10);
+        if (strlen(argv[1]) > endptr - argv[1] || seconds < 0)
+            usage_kill();
+    } else if (argc > 2)
+        usage_kill();
+
+    btpd_connect();
+    handle_ipc_res(btpd_die(ipc, seconds), "kill");
+}
diff --git a/cli/list.c b/cli/list.c
new file mode 100644
index 0000000..8bdf851
--- /dev/null
+++ b/cli/list.c
@@ -0,0 +1,124 @@
+#include "btcli.h"
+
+void
+usage_list(void)
+{
+    printf(
+        "List torrents.\n"
+        "\n"
+        "Usage: list [-a] [-i]\n"
+        "\n"
+        );
+    exit(1);
+}
+
+struct item {
+    unsigned num;
+    char *name;
+    char st;
+    BTPDQ_ENTRY(item) entry;
+};
+
+struct items {
+    int count;
+    BTPDQ_HEAD(item_tq, item) hd;
+};
+
+void
+itm_insert(struct items *itms, struct item *itm)
+{
+    struct item *p;
+    BTPDQ_FOREACH(p, &itms->hd, entry)
+        if (itm->num < p->num)
+#if 0
+        if (strcmp(itm->name, p->name) < 0)
+#endif
+            break;
+    if (p != NULL)
+        BTPDQ_INSERT_BEFORE(p, itm, entry);
+    else
+        BTPDQ_INSERT_TAIL(&itms->hd, itm, entry);
+}
+
+static void
+list_cb(int obji, enum ipc_err objerr, struct ipc_get_res *res, void *arg)
+{
+    struct items *itms = arg;
+    struct item *itm = calloc(1, sizeof(*itm));
+    itms->count++;
+    itm->num = (unsigned)res[IPC_TVAL_NUM].v.num;
+    itm->st = tstate_char(res[IPC_TVAL_STATE].v.num);
+    if (res[IPC_TVAL_NAME].type == IPC_TYPE_ERR)
+        asprintf(&itm->name, "%s", ipc_strerror(res[IPC_TVAL_NAME].v.num));
+    else
+        asprintf(&itm->name, "%.*s", (int)res[IPC_TVAL_NAME].v.str.l,
+            res[IPC_TVAL_NAME].v.str.p);
+    itm_insert(itms, itm);
+#if 0
+    int *count = arg;
+    (*count)++;
+    printf("%4u %c.", (unsigned)res[IPC_TVAL_NUM].v.num,
+        tstate_char(res[IPC_TVAL_STATE].v.num));
+    if (res[IPC_TVAL_NAME].type == IPC_TYPE_ERR)
+        printf(" %s\n", ipc_strerror(res[IPC_TVAL_NAME].v.num));
+    else
+        printf(" %.*s\n", (int)res[IPC_TVAL_NAME].v.str.l,
+            res[IPC_TVAL_NAME].v.str.p);
+#endif
+}
+
+void
+print_items(struct items* itms)
+{
+    int n;
+    struct item *p;
+    BTPDQ_FOREACH(p, &itms->hd, entry) {
+        n = printf("%u: ", p->num);
+        while (n < 7) {
+            putchar(' ');
+            n++;
+        }
+        printf("%c. %s\n", p->st, p->name);
+    }
+}
+
+static struct option list_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_list(int argc, char **argv)
+{
+    int ch, /*count = 0,*/ inactive = 0, active = 0;
+    enum ipc_twc twc;
+    enum ipc_tval keys[] = { IPC_TVAL_NUM, IPC_TVAL_STATE, IPC_TVAL_NAME };
+    struct items itms;
+    while ((ch = getopt_long(argc, argv, "ai", list_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'a':
+            active = 1;
+            break;
+        case 'i':
+            inactive = 1;
+            break;
+        default:
+            usage_list();
+        }
+    }
+
+    if (inactive == active)
+        twc = IPC_TWC_ALL;
+    else if (inactive)
+        twc = IPC_TWC_INACTIVE;
+    else
+        twc = IPC_TWC_ACTIVE;
+
+    btpd_connect();
+    printf("NUM    ST NAME\n");
+    itms.count = 0;
+    BTPDQ_INIT(&itms.hd);
+    handle_ipc_res(btpd_tget_wc(ipc, twc, keys, 3, list_cb, &itms), "tget");
+    print_items(&itms);
+    printf("Listed %d torrent%s.\n", itms.count, itms.count == 1 ? "" : "s");
+}
diff --git a/cli/start.c b/cli/start.c
new file mode 100644
index 0000000..4772bb6
--- /dev/null
+++ b/cli/start.c
@@ -0,0 +1,27 @@
+#include "btcli.h"
+
+void
+usage_start(void)
+{
+    printf(
+        "Start torrents.\n"
+        "\n"
+        "Usage: start torrent\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_start(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_start();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_start(ipc, &t), argv[i]);
+}
diff --git a/cli/stat.c b/cli/stat.c
new file mode 100644
index 0000000..2af1210
--- /dev/null
+++ b/cli/stat.c
@@ -0,0 +1,193 @@
+#include <math.h>
+
+#include "btcli.h"
+
+void
+usage_stat(void)
+{
+    printf(
+        "Display stats for active torrents.\n"
+        "\n"
+        "Usage: stat [-i] [-w seconds] [file ...]\n"
+        "\n"
+        "Arguments:\n"
+        "file ...\n"
+        "\tOnly display stats for the given torrent(s).\n"
+        "\n"
+        "Options:\n"
+        "-i\n"
+        "\tDisplay individual lines for each torrent.\n"
+        "\n"
+        "-n\n"
+        "\tDisplay the name of each torrent. Implies '-i'.\n"
+        "\n"
+        "-w n\n"
+        "\tDisplay stats every n seconds.\n"
+        "\n");
+    exit(1);
+}
+
+struct btstat {
+    unsigned num;
+    enum ipc_tstate state;
+    unsigned peers, tr_errors;
+    long long content_got, content_size, downloaded, uploaded, rate_up,
+        rate_down;
+    uint32_t pieces_seen, torrent_pieces;
+};
+
+struct cbarg {
+    int individual, names;
+    struct btstat tot;
+};
+
+static enum ipc_tval stkeys[] = {
+    IPC_TVAL_STATE,
+    IPC_TVAL_NUM,
+    IPC_TVAL_NAME,
+    IPC_TVAL_PCOUNT,
+    IPC_TVAL_TRERR,
+    IPC_TVAL_PCCOUNT,
+    IPC_TVAL_PCSEEN,
+    IPC_TVAL_SESSUP,
+    IPC_TVAL_SESSDWN,
+    IPC_TVAL_RATEUP,
+    IPC_TVAL_RATEDWN,
+    IPC_TVAL_CGOT,
+    IPC_TVAL_CSIZE
+};
+
+static size_t nstkeys = sizeof(stkeys) / sizeof(stkeys[0]);
+
+static void
+print_stat(struct btstat *st)
+{
+    printf("%5.1f%% %6.1fM %7.2fkB/s %6.1fM %7.2fkB/s %5u %5.1f%%",
+        floor(1000.0 * st->content_got / st->content_size) / 10,
+        (double)st->downloaded / (1 << 20),
+        (double)st->rate_down / (20 << 10),
+        (double)st->uploaded / (1 << 20),
+        (double)st->rate_up / (20 << 10),
+        st->peers,
+        floor(1000.0 * st->pieces_seen / st->torrent_pieces) / 10);
+    if (st->tr_errors > 0)
+        printf(" E%u", st->tr_errors);
+    printf("\n");
+}
+
+void
+stat_cb(int obji, enum ipc_err objerr, struct ipc_get_res *res, void *arg)
+{
+    struct cbarg *cba = arg;
+    struct btstat st, *tot = &cba->tot;
+    if (objerr != IPC_OK || res[IPC_TVAL_STATE].v.num == IPC_TSTATE_INACTIVE)
+        return;
+    bzero(&st, sizeof(st));
+    st.state = res[IPC_TVAL_STATE].v.num;
+    st.num = res[IPC_TVAL_NUM].v.num;
+    tot->torrent_pieces += (st.torrent_pieces = res[IPC_TVAL_PCCOUNT].v.num);
+    tot->pieces_seen += (st.pieces_seen = res[IPC_TVAL_PCSEEN].v.num);
+    tot->content_got += (st.content_got = res[IPC_TVAL_CGOT].v.num);
+    tot->content_size += (st.content_size = res[IPC_TVAL_CSIZE].v.num);
+    tot->downloaded += (st.downloaded = res[IPC_TVAL_SESSDWN].v.num);
+    tot->uploaded += (st.uploaded = res[IPC_TVAL_SESSUP].v.num);
+    tot->rate_down += (st.rate_down = res[IPC_TVAL_RATEDWN].v.num);
+    tot->rate_up += (st.rate_up = res[IPC_TVAL_RATEUP].v.num);
+    tot->peers += (st.peers = res[IPC_TVAL_PCOUNT].v.num);
+    if ((st.tr_errors = res[IPC_TVAL_TRERR].v.num) > 0)
+        tot->tr_errors++;
+    if (cba->individual) {
+        if (cba->names)
+            printf("%.*s\n", (int)res[IPC_TVAL_NAME].v.str.l,
+                res[IPC_TVAL_NAME].v.str.p);
+        int n = printf("%u:", st.num);
+        while (n < 7) {
+            putchar(' ');
+            n++;
+        }
+        printf("%c. ", tstate_char(st.state));
+        print_stat(&st);
+    }
+}
+
+static void
+do_stat(int individual, int names, int seconds, struct ipc_torrent *tps,
+    int ntps)
+{
+    enum ipc_err err;
+    struct cbarg cba;
+    if (names)
+        individual = 1;
+    if (individual)
+        printf("NUM    ST ");
+    printf("  HAVE   DLOAD       RTDWN   ULOAD        RTUP PEERS  AVAIL\n");
+    cba.individual = individual;
+    cba.names = names;
+again:
+    bzero(&cba.tot, sizeof(cba.tot));
+    cba.tot.state = IPC_TSTATE_INACTIVE;
+    if (tps == NULL)
+        err = btpd_tget_wc(ipc, IPC_TWC_ACTIVE, stkeys, nstkeys,
+            stat_cb, &cba);
+    else
+        err = btpd_tget(ipc, tps, ntps, stkeys, nstkeys, stat_cb, &cba);
+    if (handle_ipc_res(err, "stat") != IPC_OK)
+        exit(1);
+    if (names)
+        printf("-----\n");
+    if (individual)
+        printf("Total:    ");
+    print_stat(&cba.tot);
+    if (seconds > 0) {
+        sleep(seconds);
+        goto again;
+    }
+}
+
+static struct option stat_opts [] = {
+    { "help", no_argument, NULL, 'H' },
+    {NULL, 0, NULL, 0}
+};
+
+void
+cmd_stat(int argc, char **argv)
+{
+    int ch;
+    int wflag = 0, iflag = 0, nflag = 0, seconds = 0;
+    struct ipc_torrent *tps = NULL;
+    int ntps = 0;
+    char *endptr;
+    while ((ch = getopt_long(argc, argv, "inw:", stat_opts, NULL)) != -1) {
+        switch (ch) {
+        case 'i':
+            iflag = 1;
+            break;
+        case 'n':
+            nflag = 1;
+            break;
+        case 'w':
+            wflag = 1;
+            seconds = strtol(optarg, &endptr, 10);
+            if (*endptr != '\0' || seconds < 1)
+                usage_stat();
+            break;
+        default:
+            usage_stat();
+        }
+    }
+    argc -= optind;
+    argv += optind;
+
+    if (argc > 0) {
+        tps = malloc(argc * sizeof(*tps));
+        for (int i = 0; i < argc; i++) {
+            if (torrent_spec(argv[i], &tps[ntps]))
+                ntps++;
+            else
+                exit(1);
+
+        }
+    }
+    btpd_connect();
+    do_stat(iflag, nflag, seconds, tps, ntps);
+}
diff --git a/cli/stop.c b/cli/stop.c
new file mode 100644
index 0000000..caf68f4
--- /dev/null
+++ b/cli/stop.c
@@ -0,0 +1,27 @@
+#include "btcli.h"
+
+void
+usage_stop(void)
+{
+    printf(
+        "Stop torrents.\n"
+        "\n"
+        "Usage: stop torrent ...\n"
+        "\n"
+        );
+    exit(1);
+}
+
+void
+cmd_stop(int argc, char **argv)
+{
+    struct ipc_torrent t;
+
+    if (argc < 2)
+        usage_stop();
+
+    btpd_connect();
+    for (int i = 1; i < argc; i++)
+        if (torrent_spec(argv[i], &t))
+            handle_ipc_res(btpd_stop(ipc, &t), argv[i]);
+}