@@@ timeout wip
[mLib] / test / tvec-remote.c
index 3dbb5d3..62dd150 100644 (file)
 #include "quis.h"
 #include "tvec.h"
 
+/*----- Preliminaries -----------------------------------------------------*/
+
+/* The control macros I'm using below provoke `dangling-else' warnings from
+ * compilers.  Suppress them.  I generally don't care.
+ */
+
 #if GCC_VERSION_P(7, 1)
 #  pragma GCC diagnostic ignored "-Wdangling-else"
 #elif GCC_VERSION_P(4, 2)
 
 /*----- Basic I/O ---------------------------------------------------------*/
 
+/* --- @init_comms@ --- *
+ *
+ * Arguments:  @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a communication state.  This doesn't allocate any
+ *             resurces: it just ensures that everything is set up so that
+ *             subsequent operations -- in particular @release_comms@ --
+ *             behave sensibly.
+ */
+
 static void init_comms(struct tvec_remotecomms *rc)
 {
-  dbuf_create(&rc->bin); dbuf_create(&rc->bout);
+  rc->bin = 0; rc->binsz = 0; dbuf_create(&rc->bout);
   rc->infd = rc->outfd = -1; rc->f = 0;
 }
 
+/* --- @close_comms@ --- *
+ *
+ * Arguments:  @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    ---
+ *
+ * Use:                Close the input and output descriptors.
+ *
+ *             If the descriptors are already closed -- or were never opened
+ *             -- then nothing happens.
+ */
+
 static void close_comms(struct tvec_remotecomms *rc)
 {
   if (rc->infd >= 0) { close(rc->infd); rc->infd = -1; }
   if (rc->outfd >= 0) { close(rc->outfd); rc->outfd = -1; }
 }
 
+/* --- @release_comms@ --- *
+ *
+ * Arguments:  @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    ---
+ *
+ * Use:                Releases the resources -- most notably the input and output
+ *             buffers -- held by the communication state.  Also calls
+ *             @close_comms@.
+ */
+
 static void release_comms(struct tvec_remotecomms *rc)
-  { close_comms(rc); dbuf_destroy(&rc->bin); dbuf_destroy(&rc->bout); }
+  { close_comms(rc); xfree(rc->bin); dbuf_destroy(&rc->bout); }
+
+/* --- @setup_comms@ --- *
+ *
+ * Arguments:  @struct tvec_remotecomms *rc@ = communication state
+ *             @int infd, outfd@ = input and output file descriptors
+ *
+ * Returns:    ---
+ *
+ * Use:                Use the given descriptors for communication.
+ *
+ *             Clears the private flags.
+ */
 
 static void setup_comms(struct tvec_remotecomms *rc, int infd, int outfd)
 {
-  rc->infd = infd; rc->outfd = outfd; rc->f &= ~0xffu;
-  dbuf_reset(&rc->bin); dbuf_reset(&rc->bout);
+  rc->infd = infd; rc->outfd = outfd;
+  rc->binoff = rc->binlen = 0;
+  rc->f &= ~0xffu;
 }
 
-static int PRINTF_LIKE(3, 4)
-  ioerr(struct tvec_state *tv, struct tvec_remotecomms *rc,
-       const char *msg, ...)
+/* --- @ioerr@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *             @const char *msg, ...@ = format string and arguments
+ *
+ * Returns:    %$-1$%.
+ *
+ * Use:                Reports the message as an error, closes communications and
+ *             marks them as broken.
+ */
+
+static PRINTF_LIKE(3, 4)
+  int ioerr(struct tvec_state *tv, struct tvec_remotecomms *rc,
+           const char *msg, ...)
 {
   va_list ap;
 
@@ -95,18 +161,24 @@ static int PRINTF_LIKE(3, 4)
   return (-1);
 }
 
+/* --- @send_all@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *             @const unsigned char *p@, @size_t sz@ = output buffer
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Send the output buffer over the communication state's output
+ *             descriptor, even if it has to be written in multiple pieces.
+ */
+
 static int send_all(struct tvec_state *tv, struct tvec_remotecomms *rc,
                    const unsigned char *p, size_t sz)
 {
-  void (*opipe)(int) = SIG_ERR;
   ssize_t n;
   int ret;
 
-  opipe = signal(SIGPIPE, SIG_IGN);
-    if (opipe == SIG_ERR) {
-      ret = ioerr(tv, rc, "failed to ignore `SIGPIPE': %s", strerror(errno));
-      goto end;
-    }
   while (sz) {
     n = write(rc->outfd, p, sz);
     if (n > 0)
@@ -119,131 +191,384 @@ static int send_all(struct tvec_state *tv, struct tvec_remotecomms *rc,
   }
   ret = 0;
 end:
-  if (opipe != SIG_ERR) signal(SIGPIPE, opipe);
   return (ret);
 }
 
+/* --- @recv_all@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *             @unsigned f@ = flags (@RCVF_...@)
+ *             @unsigned char *p@, @size_t sz@ = input buffer
+ *             @size_t min@ = minimum acceptable size to read
+ *             @size_t *n_out@ = size read
+ *
+ * Returns:    An @RECV_...@ code.
+ *
+ * Use:                Receive data on the communication state's input descriptor to
+ *             read at least @min@ bytes into the input buffer, even if it
+ *             has to be done in multiple pieces.  If more data is readily
+ *             available, then up to @sz@ bytes will be read in total.
+ *
+ *             If the descriptor immediately reports end-of-file, and
+ *             @RCVF_ALLOWEOF@ is set in @f@, then return @RECV_EOF@.
+ *             Otherwise, EOF is treated as an I/O error, resulting in a
+ *             call to @ioerr@ and a return code of @RECV_FAIL@.  If the
+ *             read succeeded, then set @*n_out@ to the number of bytes read
+ *             and return @RECV_OK@.
+ */
+
 #define RCVF_ALLOWEOF 1u
+
 enum {
   RECV_FAIL = -1,
   RECV_OK = 0,
   RECV_EOF = 1
 };
+
 static int recv_all(struct tvec_state *tv, struct tvec_remotecomms *rc,
-                   unsigned char *p, size_t sz, unsigned f)
+                   unsigned f, unsigned char *p, size_t sz,
+                   size_t min, size_t *n_out)
 {
+  size_t tot = 0;
   ssize_t n;
-  unsigned ff = 0;
-#define f_any 1u
 
   while (sz) {
     n = read(rc->infd, p, sz);
-    if (n > 0)
-      { p += n; sz -= n; ff |= f_any; }
-    else if (!n && (f&RCVF_ALLOWEOF) && !(ff&f_any))
+    if (n > 0) {
+      p += n; sz -= n; tot += n;
+      if (tot >= min) break;
+    } else if (!n && !tot && (f&RCVF_ALLOWEOF))
       return (RECV_EOF);
     else
       return (ioerr(tv, rc, "failed to receive: %s",
                    n ? strerror(errno) : "unexpected end-of-file"));
   }
-  return (RECV_OK);
+  *n_out = tot; return (RECV_OK);
 
 #undef f_any
 }
 
+/* --- @buferr@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    %$-$%.
+ *
+ * Use:                Report a problem preparing the output buffer.
+ */
+
+static int buferr(struct tvec_state *tv, struct tvec_remotecomms *rc)
+  { return (ioerr(tv, rc, "failed to build output packet")); }
+
+/* --- @malformed@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    %$-$%.
+ *
+ * Use:                Report an I/O error that the incoming packet is malformed.
+ */
+
+static int malformed(struct tvec_state *tv, struct tvec_remotecomms *rc)
+  { return (ioerr(tv, rc, "received malformed packet")); }
+
+/* --- @remote_send@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Send the accuulated contents of the output buffer @rc->bout@.
+ *
+ *             The function arranges to convert @SIGPIPE@ into an error.
+ *
+ *             If the output buffer is broken, report this as an I/O error.
+ */
+
+#define SENDBUFSZ 4096
+
 static int remote_send(struct tvec_state *tv, struct tvec_remotecomms *rc)
 {
-  kludge64 k; unsigned char lenbuf[8];
-  const unsigned char *p; size_t sz;
+  void (*opipe)(int) = SIG_ERR;
+  int ret;
+
+  /* Various preflight checks. */
+  if (rc->f&TVRF_BROKEN) { ret = -1; goto end; }
+  if (DBBAD(&rc->bout)) { ret = buferr(tv, rc); goto end; }
 
-  if (rc->f&TVRF_BROKEN) return (-1);
-  if (BBAD(&rc->bout._b))
-    return (ioerr(tv, rc, "failed to build output packet buffer"));
+  /* Arrange to trap broken-pipe errors. */
+  opipe = signal(SIGPIPE, SIG_IGN);
+    if (opipe == SIG_ERR) {
+      ret = ioerr(tv, rc, "failed to ignore `SIGPIPE': %s", strerror(errno));
+      goto end;
+    }
 
-  p = BBASE(&rc->bout._b); sz = BLEN(&rc->bout._b);
-  ASSIGN64(k, sz); STORE64_L_(lenbuf, k);
-  if (send_all(tv, rc, lenbuf, sizeof(lenbuf))) return (-1);
-  if (send_all(tv, rc, p, sz)) return (-1);
+  /* Transmit the packet. */
+  if (send_all(tv, rc, DBBASE(&rc->bout), DBLEN(&rc->bout)))
+    { ret = -1; goto end; }
 
-  return (0);
+  /* Done.  Put things back the way we found them. */
+  ret = 0;
+end:
+  DBRESET(&rc->bout);
+  if (opipe != SIG_ERR) signal(SIGPIPE, opipe);
+  return (ret);
 }
 
+/* --- @receive_buffered@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *             @unsigned f@ = flags (@RCVF_...@)
+ *             @size_t want@ = data block size required
+ *
+ * Returns:    An @RECV_...@ code.
+ *
+ * Use:                Reads a block of data from the input descriptor into the
+ *             input buffer.
+ *
+ *             This is the main machinery for manipulating the input buffer.
+ *             The buffer has three regions:
+ *
+ *               * from the buffer start to @rc->binoff@ is `consumed';
+ *               * from @rc->binoff@ to @rc->binlen@ is `available'; and
+ *               * from @rc->binlen@ to @rc->binsz@ is `free'.
+ *
+ *             Data is read into the start of the `free' region, and the
+ *             `available' region is extended to include it.  Data in the
+ *             `consumed' region is periodically discarded by moving the
+ *             data from the `available' region to the start of the buffer
+ *             and decreasing @rc->binoff@ and @rc->binlen@.
+ *
+ *             This function ensures that the `available' region contains at
+ *             least @want@ bytes, by (a) extending the buffer, if
+ *             necessary, so that @rc->binsz >= rc->binoff + want@, and (b)
+ *             reading fresh data from the input descriptor to extend the
+ *             `available' region.
+ *
+ *             If absolutely no data is available, and @RCVF_ALLOWEOF@ is
+ *             set in @f@, then return @RECV_EOF@.  On I/O errors, including
+ *             a short read or end-of-file if @RCVF_ALLOWEOF@ is clear,
+ *             return @RECV_FAIL@.  On success, return @RECV_OK@.  The
+ *             amount of data read is indicated by updating the input buffer
+ *             variables as described above.
+ */
+
+#define RECVBUFSZ 4096u
+
+static int receive_buffered(struct tvec_state *tv,
+                           struct tvec_remotecomms *rc,
+                           unsigned f, size_t want)
+{
+  size_t sz;
+  int ret;
+
+  /* If we can supply the caller's requirement from the buffer then do
+   * that.
+   */
+  if (rc->binlen - rc->binoff >= want) return (RECV_OK);
+
+  /* If the buffer is too small then we must grow it. */
+  if (want > rc->binsz) {
+    sz = rc->binsz; if (!sz) sz = RECVBUFSZ;
+    while (sz < want) { assert(sz < (size_t)-1/2); sz *= 2; }
+    if (!rc->bin) rc->bin = xmalloc(sz);
+    else rc->bin = xrealloc(rc->bin, sz, rc->binsz);
+    rc->binsz = sz;
+  }
+
+  /* Shunt the unused existing material to the start of the buffer. */
+  memmove(rc->bin, rc->bin + rc->binoff, rc->binlen - rc->binoff);
+  rc->binlen -= rc->binoff; rc->binoff = 0;
+
+  /* Satisfy the caller from the input stream, and try to fill up as much of
+   * the rest of the buffer as we can.
+   */
+  ret = recv_all(tv, rc, rc->binlen ? 0 : f,
+                rc->bin + rc->binlen, rc->binsz - rc->binlen,
+                want - rc->binlen, &sz);
+    if (ret) return (ret);
+
+  /* Note how much material we have and return. */
+  rc->binlen += sz; return (RECV_OK);
+}
+
+/* --- @remote_recv@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned f@ = flags (@RCVF_...@)
+ *             @buf *b_out@ = buffer to establish around the packet contents
+ *
+ * Returns:    An @RECV_...@ code.
+ *
+ * Use:                Receive a packet into the input buffer @rc->bin@ and
+ *             establish @*b_out@ to read from it.
+ */
+
 static int remote_recv(struct tvec_state *tv, struct tvec_remotecomms *rc,
                       unsigned f, buf *b_out)
 {
-  kludge64 k, szmax; unsigned char lenbuf[8];
-  unsigned char *p;
-  size_t sz;
+  kludge64 k, szmax;
+  size_t want;
   int ret;
 
-  if (rc->f&TVRF_BROKEN) return (RECV_FAIL);
   ASSIGN64(szmax, (size_t)-1);
-  ret = recv_all(tv, rc, lenbuf, sizeof(lenbuf), f);
-    if (ret) return (ret);
-  LOAD64_L_(k, lenbuf);
+
+  /* Preflight checks. */
+  if (rc->f&TVRF_BROKEN) return (RECV_FAIL);
+
+  /* See if we can read the next packet length from what we already have. */
+  ret = receive_buffered(tv, rc, f, 8); if (ret) return (ret);
+  LOAD64_L_(k, rc->bin + rc->binoff); rc->binoff += 8;
   if (CMP64(k, >, szmax))
     return (ioerr(tv, rc, "packet size 0x%08lx%08lx out of range",
                  (unsigned long)HI64(k), (unsigned long)LO64(k)));
+  want = GET64(size_t, k);
 
-  sz = GET64(size_t, k); dbuf_reset(&rc->bin); p = buf_get(&rc->bin._b, sz);
-    if (!p) return (ioerr(tv, rc, "failed to allocate receive buffer"));
-  if (recv_all(tv, rc, p, sz, 0)) return (RECV_FAIL);
-  buf_init(b_out, p, sz); return (RECV_OK);
+  /* Read the next packet payload. */
+  ret = receive_buffered(tv, rc, 0, want); if (ret) return (ret);
+  buf_init(b_out, rc->bin + rc->binoff, want); rc->binoff += want;
+  return (RECV_OK);
 }
 
-#define SENDPK(tv, rc, pk)                                             \
-       if ((rc)->f&TVRF_BROKEN) MC_GOELSE(body); else                  \
-       MC_BEFORE(setpk,                                                \
-         { dbuf_reset(&(rc)->bout);                                    \
-           buf_putu16l(&(rc)->bout._b, (pk)); })                       \
-       MC_ALLOWELSE(body)                                              \
-       MC_AFTER(send,                                                  \
-         { if (remote_send(tv, rc)) MC_GOELSE(body); })                \
+/* --- @QUEUEPK_TAG@, @QUEUEPK@ --- *
+ *
+ * Arguments:  @tag@ = control structure tag
+ *             @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotecomms *rc@ = communication state
+ *             @unsigned fl@ = flags (@QF_...@)
+ *             @unsigned pk@ = packet type
+ *
+ * Use:                This is syntactically a statement head: the syntax is
+ *             @QUEUEPK(tv, rc, f) body [else alt]@.  The @body@ should
+ *             write material to the output buffer @rc->bout@.  The macro
+ *             applies appropriate framing.  If enough material has been
+ *             collected, or if @QF_FORCE@ is set in @fl@, then
+ *             @remote_send@ is invoked to transmit the buffered packets.
+ *             If there is an error of any kind, then the @alt@ statement,
+ *             if any, is executed.
+ */
 
-static int malformed(struct tvec_state *tv, struct tvec_remotecomms *rc)
-  { return (ioerr(tv, rc, "received malformed packet")); }
+#define QF_FORCE 1u
+#define QUEUEPK_TAG(tag, tv, rc, fl, pk)                               \
+       if ((rc)->f&TVRF_BROKEN) MC_GOELSE(tag##__else); else           \
+       MC_ALLOWELSE(tag##__else)                                       \
+       MC_AFTER(tag##__send, {                                         \
+         if ((DBBAD(&(rc)->bout) && (buferr((tv), (rc)), 1)) ||        \
+             ((((fl)&QF_FORCE) || DBLEN(&(rc)->bout) >= SENDBUFSZ) &&  \
+              remote_send(tv, rc)))                                    \
+           MC_GOELSE(tag##__else);                                     \
+       })                                                              \
+       DBUF_ENCLOSEITAG(tag##__frame, &(rc)->bout, (rc)->t, 64_L)      \
+       MC_BEFORE(tag##__pkty, {                                        \
+         dbuf_putu16l(&(rc)->bout, (pk));                              \
+       })
+
+#define QUEUEPK(tv, rc, fl, pk) QUEUEPK_TAG(queue, (tv), (rc), (fl), (pk))
 
 /*----- Packet types ------------------------------------------------------*/
 
 #define TVPF_ACK       0x0001u
 
-#define TVPK_VER       0x0000u         /* --> min, max: u16 */
-                                       /* <-- ver: u16 */
+#define TVPK_VER       0x0000u         /* --> min, max: u16 *
+                                        * <-- ver: u16 */
+#define TVPK_BGROUP    0x0002u         /* --> name: str16
+                                        * <-- --- */
+#define TVPK_TEST      0x0004u         /* --> in: regs
+                                        * <-- --- */
+#define TVPK_EGROUP    0x0006u         /* --> --- *
+                                        * <-- --- */
 
 #define TVPK_REPORT    0x0100u         /* <-- level: u16; msg: string */
 #define TVPK_PROGRESS  0x0102u         /* <-- st: str16 */
 
-#define TVPK_BGROUP    0x0200u         /* --> name: str16
-                                        * <-- --- */
-#define TVPK_TEST      0x0202u         /* --> in: regs
-                                        * <-- --- */
-#define TVPK_EGROUP    0x0204u         /* --> --- */
-
-#define TVPK_SKIPGRP   0x0300u         /* <-- excuse: str16 */
-#define TVPK_SKIP      0x0302u         /* <-- excuse: str16 */
-#define TVPK_FAIL      0x0304u         /* <-- flag: u8, detail: str16 */
-#define TVPK_DUMPREG   0x0306u         /* <-- ri: u16; disp: u16;
+#define TVPK_SKIPGRP   0x0104u         /* <-- excuse: str16 */
+#define TVPK_SKIP      0x0106u         /* <-- excuse: str16 */
+#define TVPK_FAIL      0x0108u         /* <-- flag: u8, detail: str16 */
+#define TVPK_DUMPREG   0x010au         /* <-- ri: u16; disp: u16;
                                         *     flag: u8, rv: value */
-#define TVPK_BBENCH    0x0308u         /* <-- ident: str32; unit: u16 */
-#define TVPK_EBENCH    0x030au         /* <-- ident: str32; unit: u16;
+#define TVPK_BBENCH    0x010cu         /* <-- ident: str32; unit: u16 */
+#define TVPK_EBENCH    0x010eu         /* <-- ident: str32; unit: u16;
                                         *     flags: u16; n, t, cy: f64 */
 
 /*----- Server ------------------------------------------------------------*/
 
+/* Forward declaration of output operations. */
 static const struct tvec_outops remote_ops;
 
-static struct tvec_state srvtv;
-static struct tvec_remotecomms srvrc = TVEC_REMOTECOMMS_INIT;
-static struct tvec_output srvout = { &remote_ops };
+static struct tvec_state srvtv;                /* server's test-vector state */
+static struct tvec_remotecomms srvrc = TVEC_REMOTECOMMS_INIT; /* comms */
+static struct tvec_output srvout = { &remote_ops }; /* output state */
 
-int tvec_setprogress(const char *status)
+/* --- @tvec_setprogress@, @tvec_setprogress_v@ --- *
+ *
+ * Arguments:  @const char *status@ = progress status token format
+ *             @va_list ap@ = argument tail
+ *
+ * Returns:    ---
+ *
+ * Use:                Reports the progress of a test execution to the client.
+ *
+ *             The framework makes use of tokens beginning with %|%|%:
+ *
+ *               * %|%IDLE|%: during the top-level server code;
+ *
+ *               * %|%SETUP|%: during the enclosing environment's @before@
+ *                 function;
+ *
+ *               * %|%RUN|%: during the environment's @run@ function, or the
+ *                 test function; and
+ *
+ *               * %|%DONE|%: during the enclosing environment's @after@
+ *                 function.
+ *
+ *             The intent is that a test can use the progress token to check
+ *             that a function which is expected to crash does so at the
+ *             correct point, so it's expected that more complex test
+ *             functions and/or environments will set their own progress
+ *             tokens to reflect what's going on.
+ */
+
+int tvec_setprogress(const char *status, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, status); rc = tvec_setprogress_v(status, &ap); va_end(ap);
+  return (rc);
+}
+
+int tvec_setprogress_v(const char *status, va_list *ap)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_PROGRESS)
-    buf_putstr16l(&srvrc.bout._b, status);
+  /* Force immediate output in case we crash before the buffer is output
+   * organically.
+   */
+  QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_PROGRESS)
+    dbuf_vputstrf16l(&srvrc.bout, status, ap);
   else return (-1);
   return (0);
 }
 
+/* --- @tvec_remoteserver@ --- *
+ *
+ * Arguments:  @int infd@, @int outfd@ = input and output file descriptors
+ *             @const struct tvec_config *config@ = test configuration
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                Run a test server, reading packets from @infd@ and writing
+ *             responses and notifications to @outfd@, and invoking tests as
+ *             described by @config@.
+ *
+ *             This function is not particularly general purpose.  It
+ *             expects to `take over' the process, and makes use of private
+ *             global variables.
+ */
+
 int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
 {
   uint16 pk, u, v;
@@ -252,14 +577,18 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
   const struct tvec_test *t;
   void *p; size_t sz;
   const struct tvec_env *env = 0;
-  unsigned f = 0;
-#define f_regslive 1u
   void *ctx = 0;
   int rc;
 
+  /* Initialize the communication machinery. */
   setup_comms(&srvrc, infd, outfd);
+
+  /* Begin a test session using our custom output driver. */
   tvec_begin(&srvtv, config, &srvout);
 
+  /* Version negotiation.  Expect a @TVPK_VER@ packet.  At the moment,
+   * there's only version zero, so we return that.
+   */
   if (remote_recv(&srvtv, &srvrc, 0, &b)) { rc = -1; goto end; }
   if (buf_getu16l(&b, &pk)) goto bad;
   if (pk != TVPK_VER) {
@@ -269,12 +598,33 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
     goto end;
   }
   if (buf_getu16l(&b, &u) || buf_getu16l(&b, &v)) goto bad;
-  SENDPK(&srvtv, &srvrc, TVPK_VER | TVPF_ACK) buf_putu16l(&srvrc.bout._b, 0);
+  QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_VER | TVPF_ACK)
+    dbuf_putu16l(&srvrc.bout, 0);
   else { rc = -1; goto end; }
 
-  tvec_setprogress("%IDLE");
-
+  /* Handle packets until the server closes the connection.
+   *
+   * The protocol looks much simpler from our point of view than from the
+   * client.
+   *
+   *   * Receive @TVPK_VER@; respond with @TVPK_VER | TVPF_ACK@.
+   *
+   *   * Receive zero or more @TVPK_BGROUP@.  Open a test group, producing
+   *    output packets, and eventually answer with @TVPK_BGROUP | TVPF_ACK@.
+   *
+   *      -- Receive zero or more @TVPK_TEST@.  Run a test, producing output
+   *         packets, and eventually answer with @TVPK_TEST | TVPF_ACK@.
+   *
+   *      -- Receive @TVPK_EGROUP@.  Maybe produce output packets, and
+   *         answer with @TVPK_EGROUP | TVPF_ACK@.
+   *
+   *  * Read EOF.  Stop.
+   */
   for (;;) {
+
+    /* Read a packet.  End-of-file is expected here (and pretty much nowhere
+     * else).   Otherwise, we expect to see @TVPK_BGROUP@.
+     */
     rc = remote_recv(&srvtv, &srvrc, RCVF_ALLOWEOF, &b);
       if (rc == RECV_EOF) break;
       else if (rc == RECV_FAIL) goto end;
@@ -283,8 +633,13 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
     switch (pk) {
 
       case TVPK_BGROUP:
+       /* Start a group. */
+
+       /* Parse the packet payload. */
        p = buf_getmem16l(&b, &sz); if (!p) goto bad;
        if (BLEFT(&b)) goto bad;
+
+       /* Find the group given its name. */
        for (t = srvtv.tests; t->name; t++)
          if (strlen(t->name) == sz && MEMCMP(t->name, ==, p, sz))
            goto found_group;
@@ -293,6 +648,7 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
        goto end;
 
       found_group:
+       /* Set up the test environment. */
        srvtv.test = t; env = t->env;
        if (env && env->setup == tvec_remotesetup)
          env = ((struct tvec_remoteenv *)env)->r.env;
@@ -300,36 +656,64 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
        else ctx = xmalloc(env->ctxsz);
        if (env && env->setup) env->setup(&srvtv, env, 0, ctx);
 
-       SENDPK(&srvtv, &srvrc, TVPK_BGROUP | TVPF_ACK);
+       /* Initialize the registers. */
+       tvec_initregs(&srvtv);
+
+       /* Report that the group has been opened and that we're ready to run
+        * tests.
+        */
+       QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_BGROUP | TVPF_ACK);
        else { rc = -1; goto end; }
 
+       /* Handle packets until we're told to end the group. */
        for (;;) {
+
+         /* Read a packet.  We expect @TVPK_EGROUP@ or @TVPK_TEST@. */
          if (remote_recv(&srvtv, &srvrc, 0, &b)) { rc = -1; goto end; }
          if (buf_getu16l(&b, &pk)) goto bad;
+
          switch (pk) {
 
            case TVPK_EGROUP:
+             /* End the group. */
+
+             /* Check the payload. */
              if (BLEFT(&b)) goto bad;
+
+             /* Leave the group loop. */
              goto endgroup;
 
            case TVPK_TEST:
-             tvec_initregs(&srvtv); f |= f_regslive;
+             /* Run a test. */
+
+             /* Parse the packet payload. */
              if (tvec_deserialize(srvtv.in, &b, srvtv.test->regs,
                                   srvtv.nreg, srvtv.regsz))
                goto bad;
              if (BLEFT(&b)) goto bad;
 
+             /* If we're not skipping the test group, then actually try to
+              * run the test.
+              */
              if (!(srvtv.f&TVSF_SKIP)) {
+
+               /* Prepare the output registers and reset the test outcome.
+                * (The environment may force a skip.)
+                */
+               for (i = 0; i < srvtv.nrout; i++)
+                 if (TVEC_REG(&srvtv, in, i)->f&TVRF_LIVE)
+                   TVEC_REG(&srvtv, out, i)->f |= TVRF_LIVE;
                srvtv.f |= TVSF_ACTIVE; srvtv.f &= ~TVSF_OUTMASK;
-               tvec_setprogress("%SETUP");
+
+               /* Invoke the environment @before@ function. */
+               tvec_setprogress("%%SETUP");
                if (env && env->before) env->before(&srvtv, ctx);
+
+               /* Run the actual test. */
                if (!(srvtv.f&TVSF_ACTIVE))
                  /* setup forced a skip */;
                else {
-                 for (i = 0; i < srvtv.nrout; i++)
-                   if (TVEC_REG(&srvtv, in, i)->f&TVRF_LIVE)
-                     TVEC_REG(&srvtv, out, i)->f |= TVRF_LIVE;
-                 tvec_setprogress("%RUN");
+                 tvec_setprogress("%%RUN");
                  if (env && env->run)
                    env->run(&srvtv, t->fn, ctx);
                  else {
@@ -337,46 +721,61 @@ int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
                    tvec_check(&srvtv, 0);
                  }
                }
-               tvec_setprogress("%DONE");
+
+               /* Conclude the test. */
+               tvec_setprogress("%%DONE");
                if (env && env->after) env->after(&srvtv, ctx);
                tvec_endtest(&srvtv);
              }
-             tvec_releaseregs(&srvtv); f &= ~f_regslive;
-             SENDPK(&srvtv, &srvrc, TVPK_TEST | TVPF_ACK);
+
+             /* Reset the input registers and report completion. */
+             tvec_releaseregs(&srvtv); tvec_initregs(&srvtv);
+             QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_TEST | TVPF_ACK);
              else { rc = -1; goto end; }
-             tvec_setprogress("%IDLE");
              break;
 
            default:
+             /* Some other kind of packet.  Complain. */
+
              rc = ioerr(&srvtv, &srvrc,
-                        "unexpected packet type 0x%04x", pk);
+                        "unexpected packet type 0x%04x during test group",
+                        pk);
              goto end;
 
          }
        }
 
       endgroup:
+       /* The test group completed. */
+
+       /* Tear down the environment and release other resources. */
        if (env && env->teardown) env->teardown(&srvtv, ctx);
-       xfree(ctx); t = 0; env = 0; ctx = 0;
+       tvec_releaseregs(&srvtv);
+       xfree(ctx); srvtv.test = 0; env = 0; ctx = 0;
+
+       /* Report completion. */
+       QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_EGROUP | TVPF_ACK);
+       else { rc = -1; goto end; }
        break;
 
       default:
-       goto bad;
+       rc = ioerr(&srvtv, &srvrc,
+                  "unexpected packet type 0x%04x at top level", pk);
     }
   }
   rc = 0;
 
 end:
+  /* Clean up and return. */
   if (env && env->teardown) env->teardown(&srvtv, ctx);
   xfree(ctx);
-  if (f&f_regslive) tvec_releaseregs(&srvtv);
-  release_comms(&srvrc);
+  if (srvtv.test) tvec_releaseregs(&srvtv);
+  release_comms(&srvrc); tvec_end(&srvtv);
   return (rc ? 2 : 0);
 
 bad:
+  /* Miscellaneous malformed packet. */
   rc = malformed(&srvtv, &srvrc); goto end;
-
-#undef f_regslive
 }
 
 /*----- Server output driver ----------------------------------------------*/
@@ -384,26 +783,26 @@ bad:
 static void remote_skipgroup(struct tvec_output *o,
                             const char *excuse, va_list *ap)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_SKIPGRP)
-    buf_vputstrf16l(&srvrc.bout._b, excuse, ap);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_SKIPGRP)
+    dbuf_vputstrf16l(&srvrc.bout, excuse, ap);
 }
 
 static void remote_skip(struct tvec_output *o,
                        const char *excuse, va_list *ap)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_SKIP)
-    buf_vputstrf16l(&srvrc.bout._b, excuse, ap);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_SKIP)
+    dbuf_vputstrf16l(&srvrc.bout, excuse, ap);
 }
 
 static void remote_fail(struct tvec_output *o,
                        const char *detail, va_list *ap)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_FAIL)
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_FAIL)
     if (!detail)
-      buf_putbyte(&srvrc.bout._b, 0);
+      dbuf_putbyte(&srvrc.bout, 0);
     else {
-      buf_putbyte(&srvrc.bout._b, 1);
-      buf_vputstrf16l(&srvrc.bout._b, detail, ap);
+      dbuf_putbyte(&srvrc.bout, 1);
+      dbuf_vputstrf16l(&srvrc.bout, detail, ap);
     }
 }
 
@@ -420,14 +819,14 @@ static void remote_dumpreg(struct tvec_output *o,
   assert(!"unexpected register definition");
 
 found:
-  SENDPK(&srvtv, &srvrc, TVPK_DUMPREG) {
-    buf_putu16l(&srvrc.bout._b, r);
-    buf_putu16l(&srvrc.bout._b, disp);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_DUMPREG) {
+    dbuf_putu16l(&srvrc.bout, r);
+    dbuf_putu16l(&srvrc.bout, disp);
     if (!rv)
-      buf_putbyte(&srvrc.bout._b, 0);
+      dbuf_putbyte(&srvrc.bout, 0);
     else {
-      buf_putbyte(&srvrc.bout._b, 1);
-      rd->ty->tobuf(&srvrc.bout._b, rv, rd);
+      dbuf_putbyte(&srvrc.bout, 1);
+      rd->ty->tobuf(DBUF_BUF(&srvrc.bout), rv, rd);
     }
   }
 }
@@ -435,9 +834,9 @@ found:
 static void remote_bbench(struct tvec_output *o,
                          const char *ident, unsigned unit)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_BBENCH) {
-    buf_putstr32l(&srvrc.bout._b, ident);
-    buf_putu16l(&srvrc.bout._b, unit);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_BBENCH) {
+    dbuf_putstr32l(&srvrc.bout, ident);
+    dbuf_putu16l(&srvrc.bout, unit);
   }
 }
 
@@ -445,16 +844,16 @@ static void remote_ebench(struct tvec_output *o,
                          const char *ident, unsigned unit,
                          const struct bench_timing *t)
 {
-  SENDPK(&srvtv, &srvrc, TVPK_EBENCH) {
-    buf_putstr32l(&srvrc.bout._b, ident);
-    buf_putu16l(&srvrc.bout._b, unit);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_EBENCH) {
+    dbuf_putstr32l(&srvrc.bout, ident);
+    dbuf_putu16l(&srvrc.bout, unit);
     if (!t || !(t->f&BTF_ANY))
-      buf_putu16l(&srvrc.bout._b, 0);
+      dbuf_putu16l(&srvrc.bout, 0);
     else {
-      buf_putu16l(&srvrc.bout._b, t->f);
-      buf_putf64l(&srvrc.bout._b, t->n);
-      if (t->f&BTF_TIMEOK) buf_putf64l(&srvrc.bout._b, t->t);
-      if (t->f&BTF_CYOK) buf_putf64l(&srvrc.bout._b, t->cy);
+      dbuf_putu16l(&srvrc.bout, t->f);
+      dbuf_putf64l(&srvrc.bout, t->n);
+      if (t->f&BTF_TIMEOK) dbuf_putf64l(&srvrc.bout, t->t);
+      if (t->f&BTF_CYOK) dbuf_putf64l(&srvrc.bout, t->cy);
     }
   }
 }
@@ -462,18 +861,11 @@ static void remote_ebench(struct tvec_output *o,
 static void remote_report(struct tvec_output *o, unsigned level,
                          const char *msg, va_list *ap)
 {
-  const char *what;
-
-  SENDPK(&srvtv, &srvrc, TVPK_REPORT) {
-    buf_putu16l(&srvrc.bout._b, level);
-    buf_vputstrf16l(&srvrc.bout._b, msg, ap);
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_REPORT) {
+    dbuf_putu16l(&srvrc.bout, level);
+    dbuf_vputstrf16l(&srvrc.bout, msg, ap);
   } else {
-    switch (level) {
-      case TVLEV_NOTE: what = "notice"; break;
-      case TVLEV_ERR: what = "ERROR"; break;
-      default: what = "(?level)"; break;
-    }
-    fprintf(stderr, "%s %s: ", QUIS, what);
+    fprintf(stderr, "%s %s: ", QUIS, tvec_strlevel(level));
     vfprintf(stderr, msg, *ap);
     fputc('\n', stderr);
   }
@@ -726,6 +1118,7 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r,
   for (;;) {
     rc = remote_recv(tv, &r->rc, f, b); if (rc) goto end;
     if (buf_getu16l(b, &pk)) goto bad;
+    if (pk == end) break;
 
     switch (pk) {
 
@@ -831,8 +1224,10 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r,
       case TVPK_EBENCH:
        p = buf_getmem32l(b, &n); if (!p) goto bad;
        if (buf_getu16l(b, &u) || buf_getu16l(b, &v)) goto bad;
-       if (u >= TVBU_LIMIT)
-         { rc = ioerr(tv, &r->rc, "unit code %u out of range", u); goto end; }
+       if (u >= TVBU_LIMIT) {
+         rc = ioerr(tv, &r->rc, "unit code %u out of range", u);
+         goto end;
+       }
        if ((v&BTF_ANY) && buf_getf64l(b, &bt.n)) goto bad;
        if ((v&BTF_TIMEOK) && buf_getf64l(b, &bt.t)) goto bad;
        if ((v&BTF_CYOK) && buf_getf64l(b, &bt.cy)) goto bad;
@@ -843,12 +1238,12 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r,
        break;
 
       default:
-       if (pk == end) { rc = 0; goto end; }
        rc = ioerr(tv, &r->rc, "unexpected packet type 0x%04x", pk);
        goto end;
     }
   }
 
+  rc = RECV_OK;
 end:
   DDESTROY(&d);
   xfree(reg);
@@ -908,6 +1303,7 @@ static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r,
   ssize_t n;
   int rc;
 
+  if (r->errfd < 0) { rc = 0; goto end; }
   if (f&ERF_SILENT) r->rc.f |= TVRF_MUFFLE;
   else r->rc.f &= ~TVRF_MUFFLE;
   if (fdflags(r->errfd, O_NONBLOCK, f&ERF_CLOSE ? 0 : O_NONBLOCK, 0, 0)) {
@@ -934,7 +1330,7 @@ static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r,
 end:
   if (f&ERF_CLOSE) {
     lbuf_close(&r->errbuf);
-    close(r->errfd);
+    close(r->errfd); r->errfd = -1;
   }
   return (rc);
 }
@@ -943,10 +1339,8 @@ end:
 static void disconnect_remote(struct tvec_state *tv,
                              struct tvec_remotectx *r, unsigned f)
 {
-  if (r->kid < 0) return;
   if (r->kid > 0 && (f&DCF_KILL)) kill(r->kid, SIGTERM);
   close_comms(&r->rc);
-  if (r->kid > 0) kill(r->kid, SIGTERM);
   drain_errfd(tv, r, f | ERF_CLOSE); reap_kid(tv, r);
 }
 
@@ -966,9 +1360,9 @@ static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r)
   lbuf_init(&r->errbuf, report_errline, r);
   r->exit = TVXST_RUN; r->rc.f &= ~TVRF_BROKEN;
 
-  SENDPK(tv, &r->rc, TVPK_VER) {
-    buf_putu16l(&r->rc.bout._b, 0);
-    buf_putu16l(&r->rc.bout._b, 0);
+  QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_VER) {
+    dbuf_putu16l(&r->rc.bout, 0);
+    dbuf_putu16l(&r->rc.bout, 0);
   } else { rc = -1; goto end; }
 
   if (handle_packets(tv, r, 0, TVPK_VER | TVPF_ACK, &b))
@@ -980,8 +1374,8 @@ static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r)
     goto end;
   }
 
-  SENDPK(tv, &r->rc, TVPK_BGROUP)
-    buf_putstr16l(&r->rc.bout._b, tv->test->name);
+  QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_BGROUP)
+    dbuf_putstr16l(&r->rc.bout, tv->test->name);
   else { rc = -1; goto end; }
   if (handle_packets(tv, r, 0, TVPK_BGROUP | TVPF_ACK, &b))
     { rc = -1; goto end; }
@@ -1033,7 +1427,8 @@ static int try_reconnect(struct tvec_state *tv, struct tvec_remotectx *r)
 
 static void reset_vars(struct tvec_remotectx *r)
 {
-  r->exwant = TVXST_RUN; r->rc.f = (r->rc.f&~TVRF_RCNMASK) | TVRCN_DEMAND;
+  r->exwant = TVXST_RUN;
+  r->rc.f = (r->rc.f&~(TVRF_RCNMASK | TVRF_SETMASK)) | TVRCN_DEMAND;
   DRESET(&r->prgwant); DPUTS(&r->prgwant, "%DONE");
 }
 
@@ -1054,28 +1449,31 @@ void tvec_remotesetup(struct tvec_state *tv, const struct tvec_env *env,
   reset_vars(r);
 }
 
-int tvec_remoteset(struct tvec_state *tv, const char *var,
-                  const struct tvec_env *env, void *ctx)
+int tvec_remoteset(struct tvec_state *tv, const char *var, void *ctx)
 {
   struct tvec_remotectx *r = ctx;
   union tvec_regval rv;
   int rc;
 
   if (STRCMP(var, ==, "@exit")) {
+    if (r->rc.f&TVRF_SETEXIT) { rc = tvec_dupreg(tv, var); goto end; }
     if (tvty_flags.parse(&rv, &exit_regdef, tv)) { rc = -1; goto end; }
-    if (r) r->exwant = rv.u;
-    rc = 1;
+    r->exwant = rv.u; r->rc.f |= TVRF_SETEXIT; rc = 1;
   } else if (STRCMP(var, ==, "@progress")) {
+    if (r->rc.f&TVRF_SETPRG) { rc = tvec_dupreg(tv, var); goto end; }
     tvty_string.init(&rv, &progress_regdef);
     rc = tvty_string.parse(&rv, &progress_regdef, tv);
-    if (r && !rc)
-      { DRESET(&r->prgwant); DPUTM(&r->prgwant, rv.str.p, rv.str.sz); }
+    if (!rc) {
+      DRESET(&r->prgwant); DPUTM(&r->prgwant, rv.str.p, rv.str.sz);
+      r->rc.f |= TVRF_SETPRG;
+    }
     tvty_string.release(&rv, &progress_regdef);
     if (rc) { rc = -1; goto end; }
     rc = 1;
   } else if (STRCMP(var, ==, "@reconnect")) {
+    if (r->rc.f&TVRF_SETRCN) { rc = tvec_dupreg(tv, var); goto end; }
     if (tvty_uenum.parse(&rv, &reconn_regdef, tv)) { rc = -1; goto end; }
-    if (r) r->rc.f = (r->rc.f&~TVRF_RCNMASK) | (rv.u&TVRF_RCNMASK);
+    r->rc.f = (r->rc.f&~TVRF_RCNMASK) | (rv.u&TVRF_RCNMASK) | TVRF_SETRCN;
     rc = 1;
   } else
     rc = 0;
@@ -1109,8 +1507,9 @@ void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
       tvec_skip(tv, "no connection"); return;
   }
 
-  SENDPK(tv, &r->rc, TVPK_TEST)
-    tvec_serialize(tv->in, &r->rc.bout._b,
+  DRESET(&r->progress); DPUTS(&r->progress, "%IDLE");
+  QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_TEST)
+    tvec_serialize(tv->in, DBUF_BUF(&r->rc.bout),
                   tv->test->regs, tv->nreg, tv->regsz);
   else { rc = -1; goto end; }
   rc = handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_TEST | TVPF_ACK, &b);
@@ -1168,11 +1567,15 @@ end:
 void tvec_remoteteardown(struct tvec_state *tv, void *ctx)
 {
   struct tvec_remotectx *r = ctx;
+  buf b;
 
-  if (r) {
-    disconnect_remote(tv, r, 0); release_comms(&r->rc);
-    DDESTROY(&r->prgwant); DDESTROY(&r->progress);
+  if (r->rc.outfd >= 0) {
+    QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_EGROUP);
+    if (!handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_EGROUP | TVPF_ACK, &b))
+      if (BLEFT(&b)) malformed(tv, &r->rc);
   }
+  disconnect_remote(tv, r, 0); release_comms(&r->rc);
+  DDESTROY(&r->prgwant); DDESTROY(&r->progress);
 }
 
 /*----- Connectors --------------------------------------------------------*/
@@ -1238,6 +1641,7 @@ int tvec_fork(pid_t *kid_out, int *infd_out, int *outfd_out, int *errfd_out,
 
   if (fork_common(&kid, &infd, &outfd, &errfd, tv)) { rc = -1; goto end; }
   if (!kid) {
+    if (tv->fp) fclose(tv->fp);
     config.tests = rf->f.tests ? rf->f.tests : tv->tests;
     config.nrout = tv->nrout; config.nreg = tv->nreg;
     config.regsz = tv->regsz;