about summary refs log tree commit diff
path: root/cli
diff options
context:
space:
mode:
authorRichard Nyberg <rnyberg@murmeldjur.se>2006-09-13 07:02:07 +0000
committerRichard Nyberg <rnyberg@murmeldjur.se>2006-09-13 07:02:07 +0000
commit86754d7d53b59ce52de51a32f865d4916c119bdb (patch)
treef017a8199a94e675fb50f55876119fe9e9d1944b /cli
parent763cbbb59f40bdfb2478e58a685acfc262876af6 (diff)
downloadbtpd-86754d7d53b59ce52de51a32f865d4916c119bdb.tar.gz
btpd-86754d7d53b59ce52de51a32f865d4916c119bdb.zip
btpd now has a library of torrents indexed by number and info hash.
The add and del commands adds or removes torrents from this library.
The start and stop commands are used to active or deactivate torrents.
Also, a mechanism for qeurying data on torrents has been added. It's
only used by the btcli list and stat commands yet though.

btcli has been split into different files for each command.

Both btpd and btcli now use misc/btpd_if.h for all ipc definitions.

Misc changes:
- struct metainfo is gone. Use the new mi_* functions.
- Add printf format type checking where appropriate.

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]);
+}