@@@ bench wip
[mLib] / test / tvec-remote.c
index e03297e..c167618 100644 (file)
 /*----- Header files ------------------------------------------------------*/
 
 #include <errno.h>
+#include <signal.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include <sys/types.h>
-#include <sys/uio.h>
+#include <sys/wait.h>
 #include <fcntl.h>
 #include <unistd.h>
 
 #include "alloc.h"
+#include "bench.h"
 #include "buf.h"
+#include "compiler.h"
+#include "fdflags.h"
+#include "growbuf.h"
+#include "lbuf.h"
+#include "mdup.h"
+#include "quis.h"
 #include "tvec.h"
 
-/*----- Data structures ---------------------------------------------------*/
+/*----- Preliminaries -----------------------------------------------------*/
 
-struct tvec_remote {
-  int infd, outfd;
-  dbuf bin, bout;
-  unsigned f;
-#define TVRF_BROKEN 1u
-};
+/* The control macros I'm using below provoke `dangling-else' warnings from
+ * compilers.  Suppress them.  I generally don't care.
+ */
 
-struct tvec_remotectx {
-  struct tvec_remote r;
-  pid_t kid;
-};
+#if GCC_VERSION_P(7, 1)
+#  pragma GCC diagnostic ignored "-Wdangling-else"
+#elif GCC_VERSION_P(4, 2)
+#  pragma GCC diagnostic ignored "-Wparentheses"
+#endif
 
-struct remote_output {
-  struct tvec_output _o;
-  struct tvec_remote r;
-};
+#if CLANG_VERSION_P(3, 1)
+#  pragma clang diagnostic ignored "-Wdangling-else"
+#endif
 
 /*----- Basic I/O ---------------------------------------------------------*/
 
-static int PRINTF_LIKE(3, 4)
-  ioerr(struct tvec_state *tv, struct tvec_remote *r, const char *msg, ...)
+/* --- @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)
+{
+  rc->bin = 0; rc->binsz = 0; DBCREATE(&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) {
+    if (rc->infd != rc->outfd) close(rc->infd);
+    rc->infd = -1;
+  }
+  if (rc->outfd >= 0)
+    { close(rc->outfd); rc->outfd = -1; }
+  rc->f |= TVRF_BROKEN;
+}
+
+/* --- @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); xfree(rc->bin); DBDESTROY(&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->binoff = rc->binlen = 0;
+  rc->f &= ~0xffu;
+}
+
+/* --- @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;
 
   va_start(ap, msg);
-  r->f |= TVRF_BROKEN;
-  tvec_write(tv, msg, &ap);
+  close_comms(rc); rc->f |= TVRF_BROKEN;
+  tvec_report_v(tv, TVLEV_ERR, msg, &ap);
   va_end(ap);
   return (-1);
 }
 
-static int send_all(struct tvec_state *tv, struct tvec_remote *r,
+/* --- @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)
 {
   ssize_t n;
+  int ret;
 
   while (sz) {
-    n = write(r->outfd, p, sz);
+    n = write(rc->outfd, p, sz);
     if (n > 0)
       { p += n; sz -= n; }
-    else
-      return (ioerr(tv, r, "failed to send: %s",
-                   n ? strerror(errno) : "empty write"));
+    else {
+      ret = ioerr(tv, rc, "failed to send: %s",
+                n ? strerror(errno) : "empty write");
+      goto end;
+    }
   }
-  return (0);
+  ret = 0;
+end:
+  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:    A @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
-static int recv_all(struct tvec_state *tv, struct tvec_remote *r,
-                   unsigned char *p, size_t sz, unsigned f)
+
+enum {
+  RECV_FAIL = -1,
+  RECV_OK = 0,
+  RECV_EOF = 1
+};
+
+static int recv_all(struct tvec_state *tv, struct tvec_remotecomms *rc,
+                   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(r->infd, p, sz);
-    if (n > 0)
-      { p += n; sz -= n; ff |= f_any; }
-    else if (!n && (f&RCVF_ALLOWEOF) && !(ff&f_any))
-      return (1);
+    n = read(rc->infd, p, sz);
+    if (n > 0) {
+      p += n; sz -= n; tot += n;
+      if (tot >= min) break;
+    } else if (!n && !tot && (f&RCVF_ALLOWEOF))
+      { rc->f |= TVRF_BROKEN; return (RECV_EOF); }
     else
-      return (ioerr(tv, r, "failed to receive: %s",
+      return (ioerr(tv, rc, "failed to receive: %s",
                    n ? strerror(errno) : "unexpected end-of-file"));
   }
-  return (0);
+  *n_out = tot; return (RECV_OK);
 
 #undef f_any
 }
 
-int tvec_send(struct tvec_state *tv, struct tvec_reomte *r)
+/* --- @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)
+{
+  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; }
+
+  /* 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;
+    }
+
+  /* Transmit the packet. */
+  if (send_all(tv, rc, DBBASE(&rc->bout), DBLEN(&rc->bout)))
+    { ret = -1; goto end; }
+
+  /* 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:    A @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)
 {
-  kludge64 k; unsigned char lenbuf[8];
-  const char *p; size_t sz;
+  size_t sz = 0;
+  int ret;
 
-  if (r->f&TVRF_BROKEN) return (-1);
-  if (BBAD(&r->bout.b))
-    return (ioerr(tv, r, "failed to build output packet buffer");
+  /* If we can supply the caller's requirement from the buffer then do
+   * that.
+   */
+  if (rc->binlen - rc->binoff >= want) return (RECV_OK);
 
-  p = BBASE(r->bout.b); sz = BLEN(&r->bout.b);
-  ASSIGN64(k, sz); STORE64_L_(lenbuf, k);
-  if (send_all(tv, r, lenbuf, sizeof(lenbuf))) return (-1);
-  if (send_all(tv, r, p, sz)) return (-1);
+  /* If the buffer is too small then we must grow it. */
+  GROWBUF_EXTEND(&arena_stdlib, rc->bin, rc->binsz, want, RECVBUFSZ, 1);
 
-  return (0);
+  /* 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);
 }
 
-int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out)
+/* --- @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:    A @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;
-  int rc;
+  kludge64 k, szmax;
+  size_t want;
+  int ret;
 
-  if (r->f&TVRF_BROKEN) return (-1);
-  ASSIGN64(k, (size_t)-1);
-  rc = recv_all(tv, r, lenbuf, sizeof(lenbuf), RCVF_ALLOWEOF);
-    if (rc) return (rc);
-  LOAD64_L_(k, lenbuf);
+  ASSIGN64(szmax, (size_t)-1);
+
+  /* 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, r, "packet size 0x%08lx%08lx out of range",
+    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); buf_reset(&r->bin); p = buf_get(&r->bin.b, sz);
-    if (!p) return (ioerr(tv, r, "failed to allocate receive buffer"));
-  if (recv_all(tv, r, p, sz, 0)) return (-1);
-  buf_init(b_out, p, sz); return (0);
+  /* 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);
 }
 
-/*----- Data formatting primitives ----------------------------------------*/
+/* --- @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.
+ */
+
+#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 TVPK_ERROR     0x0001          /* msg: string */
-#define TVPK_NOTICE    0x0002          /* msg: string */
-#define TVPK_STATUS    0x0003          /* st: char */
+#define TVPF_ACK       0x0001u
+
+#define TVPK_VER       0x0000u         /* --> min, max: u16 *
+                                        * <-- ver: u16 */
+#define TVPK_BGROUP    0x0002u         /* --> name: str16
+                                        * <-- --- */
+#define TVPK_SETVAR    0x0004u         /* --> name: str16, rv: value
+                                        * <-- rc: u8 */
+#define TVPK_TEST      0x0006u         /* --> in: regs
+                                        * <-- --- */
+#define TVPK_EGROUP    0x0008u         /* --> --- *
+                                        * <-- --- */
+
+#define TVPK_REPORT    0x0100u         /* <-- level: u16; msg: string */
+#define TVPK_PROGRESS  0x0102u         /* <-- st: str16 */
 
-#define TVPK_BGROUP    0x0101          /* name: string */
-#define TVPK_TEST      0x0102          /* in: regs */
-#define TVPK_EGROUP    0x0103          /* --- */
+#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    0x010cu         /* <-- ident: str32; unit: u16 */
+#define TVPK_EBENCH    0x010eu         /* <-- ident: str32; unit: u16;
+                                        *     flags: u16; n, t, cy: f64 */
 
-#define TVPK_SKIPGRP   0x0201          /* excuse: string */
-#define TVPK_SKIP      0x0202          /* excuse: string */
-#define TVPK_FAIL      0x0203          /* detail: string */
-#define TVPK_MISMATCH  0x0204          /* in, out: regs */
-#define TVPK_BBENCH    0x0205          /* in: regs */
-#define TVPK_EBENCH    0x0206         /* flags: u16; n: u64; t, cy: float */
+/*----- Server ------------------------------------------------------------*/
 
-/*----- The output driver -------------------------------------------------*/
+/* Forward declaration of output operations. */
+static const struct tvec_outops remote_ops;
 
-#define SENDPK(ro, pk)                                                 \
-       MC_BEFORE(setpk,                                                \
-         { buf_reset(&(ro)->r.bout);                                   \
-           buf_putu16l(&(ro)->r.bout.b, (pk)); })                      \
-       MC_AFTER(send,                                                  \
-         { tvec_send(&ro->_o.tv, &ro->r); })
+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 */
+
+/* --- @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.
+ */
 
-static int sendstr(struct tvec_output *o, unsigned pk,
-                  const char *p, va_list *ap)
+int tvec_setprogress(const char *status, ...)
 {
-  struct remote_output *ro = (struct remote_output *)o;
+  va_list ap;
+  int rc;
 
-  if (ro->r.f&TVRF_BROKEN) return (-1);
-  dbuf_reset(&ro->r.bout);
-  buf_putu16l(&ro->r.bout.b, TVPK_ERROR);
-  buf_vputstrf16l(&ro->r.bout.b, msg, ap);
-  return (tvec_send(ro->_o.tv, &ro->r));
+  va_start(ap, status); rc = tvec_setprogress_v(status, &ap); va_end(ap);
+  return (rc);
 }
 
-static void report(struct tvec_output *o, unsigned pk,
-                  const char *msg, va_list *ap)
+int tvec_setprogress_v(const char *status, va_list *ap)
 {
-  if (sendstr(o, pk, msg, ap)) {
-    fprintf(stderr, "%s: ", QUIS);
-    vfprintf(stderr, msg, *ap);
-    fputc('\n', stderr);
-  }
+  /* 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);
 }
 
-static void remote_error(struct tvec_output *o, const char *msg, va_list *ap)
-  { report(o, TVPK_ERROR, msg, ap); }
-
-static void remote_notice(struct tvec_output *o,
-                         const char *msg, va_list *ap)
-  { report(o, TVPK_NOTICE, msg, ap); }
+/* --- @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.
+ */
 
-static void remote_setstatus(struct tvec_ouptut *o, int st)
+int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config)
 {
-  struct remote_output *ro = (struct remote_output *)o;
-  SENDPK(ro, TVPK_STATUS) buf_putbyte(&ro->r.bout.b, st);
-}
+  uint16 pk, u, v;
+  unsigned i;
+  buf b;
+  dstr d = DSTR_INIT;
+  const struct tvec_test *t;
+  void *p; size_t sz;
+  const struct tvec_env *env = 0;
+  const struct tvec_vardef *vd = 0; void *varctx;
+  struct tvec_reg *r = 0, rbuf, *r_alloc = 0; size_t rsz = 0;
+  void *ctx = 0;
+  int rc;
 
-static void remote_skipgroup(struct tvec_output *o,
-                            const char *excuse, va_list *ap)
-  { sendstr(o, TVPK_SKIPGRP, excuse, ap); }
+  /* Initialize the communication machinery. */
+  setup_comms(&srvrc, infd, outfd);
 
-static void remote_skip(struct tvec_output *o,
-                       const char *excuse, va_list *ap)
-  { sendstr(o, TVPK_SKIP, excuse, ap); }
+  /* Begin a test session using our custom output driver. */
+  tvec_begin(&srvtv, config, &srvout);
 
-static void remote_fail(struct tvec_output *o,
-                       const char *detail, va_list *ap)
-  { sendstr(o, TVPK_FAIL, detail, ap); }
+  /* 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) {
+    rc = ioerr(&srvtv, &srvrc,
+              "unexpected packet type 0x%04x instead of client version",
+              pk);
+    goto end;
+  }
+  if (buf_getu16l(&b, &u) || buf_getu16l(&b, &v)) goto bad;
+  QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_VER | TVPF_ACK)
+    dbuf_putu16l(&srvrc.bout, 0);
+  else { rc = -1; goto end; }
 
-static void remote_mismatch(struct tvec_output *o)
-{
-  struct remote_output *ro = (struct remote_output *)o;
-  struct tvec_state *rv = ro->_o.tv;
+  /* 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 (;;) {
 
-  SENDPK(ro, TVPK_MISMATCH) {
-    tvec_serialize(tv, &ro->r.bout.b, tv->in, tv->nreg, tv->regsz);
-    tvec_serialize(tv, &ro->r.bout.b, tv->out, tv->nrout, tv->regsz);
-  }
-}
+    /* 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;
+    if (buf_getu16l(&b, &pk)) goto bad;
 
-static void remote_bbench(struct tvec_output *o)
-{
-  struct remote_output *ro = (struct remote_output *)o;
-  struct tvec_state *rv = ro->_o.tv;
+    switch (pk) {
 
-  SENDPK(ro, TVPK_BBENCH)
-    tvec_serialize(tv, &ro->r.bout.b, tv->in, tv->nreg, tv->regsz);
-}
+      case TVPK_BGROUP:
+       /* Start a group. */
 
-static void remote_ebench(struct tvec_output *o,
-                         const struct bench_timing *t)
-{
-  struct remote_output *ro = (struct remote_output *)o;
-  kludge64 k;
+       /* 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;
+       rc = ioerr(&srvtv, &srvrc, "unknown test group `%.*s'",
+                  (int)sz, (char *)p);
+       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;
+       if (!env || !env->ctxsz) ctx = 0;
+       else ctx = xmalloc(env->ctxsz);
+       if (env && env->setup) env->setup(&srvtv, env, 0, ctx);
+
+       /* Initialize the registers. */
+       tvec_initregs(&srvtv);
 
-  SENDPK(ro, TVPK_EBENCH) {
-    buf_putu16l(&ro->r.bout.b, t->f);
-    ASSIGN64(k, t->n); buf_putk64l(&ro->r.bout.b, k);
-    if (t->f&BTF_TIMEOK) buf_putf64l(&ro->r.bout.b, t->t);
-    if (t->f&BTF_CYOK) buf_putf64l(&ro->r.bout.b, t->cy);
+       /* 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_SETVAR:
+             /* Set a subenvironment variable. */
+
+             /* Get the variable name. */
+             p = buf_getmem16l(&b, &sz); if (!p) goto bad;
+             DRESET(&d); DPUTM(&d, p, sz); DPUTZ(&d);
+
+             /* Look up the variable definition. */
+             if (env && env->findvar) {
+               vd = env->findvar(&srvtv, d.buf, &varctx, ctx);
+                 if (vd) goto found_var;
+             }
+             rc = tvec_unkreg(&srvtv, d.buf); goto setvar_end;
+           found_var:
+
+             /* Set up the register. */
+             if (vd->regsz <= sizeof(rbuf))
+               r = &rbuf;
+             else {
+               GROWBUF_REPLACE(&arena_stdlib, r_alloc, rsz, vd->regsz,
+                               8*sizeof(void *), 1);
+               r = r_alloc;
+             }
+
+             /* Collect and set the value. */
+             vd->def.ty->init(&r->v, &vd->def);
+             if (vd->def.ty->frombuf(&b, &r->v, &vd->def)) goto bad;
+             if (BLEFT(&b)) goto bad;
+             rc = vd->setvar(&srvtv, d.buf, &r->v, varctx);
+
+             /* Send the reply. */
+           setvar_end:
+             QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_SETVAR | TVPF_ACK)
+               dbuf_putbyte(&srvrc.bout, rc ? 0xff : 0);
+             else { rc = -1; goto end; }
+             if (vd) { vd->def.ty->release(&r->v, &vd->def); vd = 0; }
+             break;
+
+           case TVPK_TEST:
+             /* 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;
+
+               /* 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 {
+                 tvec_setprogress("%%RUN");
+                 if (env && env->run)
+                   env->run(&srvtv, t->fn, ctx);
+                 else {
+                   t->fn(srvtv.in, srvtv.out, ctx);
+                   tvec_check(&srvtv, 0);
+                 }
+               }
+
+               /* Conclude the test. */
+               tvec_setprogress("%%DONE");
+               if (env && env->after) env->after(&srvtv, ctx);
+               tvec_endtest(&srvtv);
+             }
+
+             /* 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; }
+             break;
+
+           default:
+             /* Some other kind of packet.  Complain. */
+
+             rc = ioerr(&srvtv, &srvrc,
+                        "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);
+       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:
+       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);
+  if (vd) vd->def.ty->release(&r->v, &vd->def);
+  xfree(ctx); xfree(r_alloc);
+  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;
 }
 
-static void remote_write(struct tvec_output *o, const char *p, size_t sz)
-  { assert(!"remote_write"); }
-static void remote_bsession(struct tvec_output *o)
-  { assert(!"remote_bsession"); }
+/*----- Server output driver ----------------------------------------------*/
+
+/* --- @remote_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The remote driver does nothing at all.
+ */
+
+static void remote_bsession(struct tvec_output *o, struct tvec_state *tv)
+  { ; }
+
+/* --- @remote_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The remote driver returns a suitable exit code without
+ *             printing anything.
+ */
+
 static int remote_esession(struct tvec_output *o)
-  { assert(!"remote_esession"); return (-1); }
+  { return (srvtv.f&TVSF_ERROR ? 2 : 0); }
+
+/* --- @remote_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             This is a stub which should never be called.
+ */
+
 static void remote_bgroup(struct tvec_output *o)
   { assert(!"remote_bgroup"); }
-static void remote_btest(struct tvec_output *o)
-  { assert(!"remote_btest"); }
-static void remote_egroup(struct tvec_output *o, unsigned outcome)
-  { assert(!"remote_egroup"); }
-static void remote_etest(struct tvec_output *o, unsigned outcome)
-  { assert(!"remote_etest"); }
 
-static void remote_destroy(struct tvec_output *o)
+/* --- @remote_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The remote driver sends a @TVPK_SKIP@ packet to its client.
+ */
+
+static void remote_skipgroup(struct tvec_output *o,
+                            const char *excuse, va_list *ap)
 {
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_SKIPGRP)
+    dbuf_vputstrf16l(&srvrc.bout, excuse, ap);
 }
 
-static const struct tvec_outops remote_ops = {
-  remote_error, remote_notice, remote_setstatus, remote_write,
-  remote_bsession, remote_esession,
-  remote_bgroup, remote_egroup, remote_skip,
-  remote_btest, remote_skip, remote_fail, remote_mismatch, remote_etest,
-  remote_bbench, remote_ebench,
-  remote_destroy
+/* --- @remote_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             This is a stub which should never be called.
+ */
+
+static void remote_egroup(struct tvec_output *o)
+  { assert(!"remote_egroup"); }
+
+/* --- @remote_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             This is a stub which should never be called.
+ */
 
-/*----- Main code ---------------------------------------------------------*/
+static void remote_btest(struct tvec_output *o)
+  { assert(!"remote_btest"); }
+
+/* --- @remote_skip@, @remote_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @unsigned attr@ = attribute to apply to the outcome
+ *             @const char *outcome@ = outcome string to report
+ *             @const char *detail@, @va_list *ap@ = a detail message
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has been skipped or failed.
+ *
+ *             The remote driver sends a @TVPK_SKIP@ or @TVPK_FAIL@ packet
+ *             to its client as appropriate.
+ */
 
+static void remote_skip(struct tvec_output *o,
+                       const char *excuse, va_list *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)
+{
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_FAIL)
+    if (!detail)
+      dbuf_putbyte(&srvrc.bout, 0);
+    else {
+      dbuf_putbyte(&srvrc.bout, 1);
+      dbuf_vputstrf16l(&srvrc.bout, detail, ap);
+    }
+}
+
+/* --- @remote_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The remote driver sends a @TVPK_DUMPREG@ packet to its
+ *             client.  This will only work if the register definition is
+ *             one of those listed in the current test definition.
+ */
+
+static void remote_dumpreg(struct tvec_output *o,
+                          unsigned disp, const union tvec_regval *rv,
+                          const struct tvec_regdef *rd)
+{
+  const struct tvec_regdef *reg;
+  unsigned r;
+
+  /* Find the register definition. */
+  for (reg = srvtv.test->regs, r = 0; reg->name; reg++, r++)
+    if (reg == rd) goto found;
+  assert(!"unexpected register definition");
+
+found:
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_DUMPREG) {
+    dbuf_putu16l(&srvrc.bout, r);
+    dbuf_putu16l(&srvrc.bout, disp);
+    if (!rv)
+      dbuf_putbyte(&srvrc.bout, 0);
+    else {
+      dbuf_putbyte(&srvrc.bout, 1);
+      rd->ty->tobuf(DBUF_BUF(&srvrc.bout), rv, rd);
+    }
+  }
+}
+
+/* --- @remote_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The remote driver does nothing at all.
+ */
+
+static void remote_etest(struct tvec_output *o, unsigned outcome)
+  { ; }
+
+/* --- @remote_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @const char *ident@ = identifying register values
+ *             @unsigned unit@ = measurement unit (@TVBU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The remote driver sends a @TVPK_BBENCH@ packet to its client.
+ */
+
+static void remote_bbench(struct tvec_output *o,
+                         const char *ident, unsigned unit)
+{
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_BBENCH) {
+    dbuf_putstr32l(&srvrc.bout, ident);
+    dbuf_putu16l(&srvrc.bout, unit);
+  }
+}
+
+/* --- @remote_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @const char *ident@ = identifying register values
+ *             @unsigned unit@ = measurement unit (@TVBU_...@)
+ *             @const struct bench_timing *tm@ = measurement
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a benchmark's results
+ *
+ *             The remote driver sends a @TVPK_EBENCH@ packet to its client.
+ */
+
+static void remote_ebench(struct tvec_output *o,
+                         const char *ident, unsigned unit,
+                         const struct bench_timing *t)
+{
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_EBENCH) {
+    dbuf_putstr32l(&srvrc.bout, ident);
+    dbuf_putu16l(&srvrc.bout, unit);
+    if (!t || !(t->f&BTF_ANY))
+      dbuf_putu16l(&srvrc.bout, 0);
+    else {
+      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);
+    }
+  }
+}
+
+/* --- @remote_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             The remote driver sends a @TVPK_REPORT@ packet to its
+ *             client.  If its attempt to transmit the packet fails, then
+ *             the message is written to the standard error stream instead,
+ *             in the hope that this will help it be noticed.
+ */
+
+static void remote_report(struct tvec_output *o, unsigned level,
+                         const char *msg, va_list *ap)
+{
+  QUEUEPK(&srvtv, &srvrc, 0, TVPK_REPORT) {
+    dbuf_putu16l(&srvrc.bout, level);
+    dbuf_vputstrf16l(&srvrc.bout, msg, ap);
+  } else {
+    fprintf(stderr, "%s %s: ", QUIS, tvec_strlevel(level));
+    vfprintf(stderr, msg, *ap);
+    fputc('\n', stderr);
+  }
+}
+
+/* --- @remote_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink (ignored)
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ *
+ *             The remote driver does nothing at all.
+ */
+
+static void remote_destroy(struct tvec_output *o)
+  { ; }
+
+static const struct tvec_outops remote_ops = {
+  remote_bsession, remote_esession,
+  remote_bgroup, remote_skipgroup, remote_egroup,
+  remote_btest, remote_skip, remote_fail, remote_dumpreg, remote_etest,
+  remote_bbench, remote_ebench,
+  remote_report,
+  remote_destroy
+};
+
+/*----- Pseudoregister definitions ----------------------------------------*/
+
+static tvec_setvarfn setvar_local, setvar_remote;
+
+static const struct tvec_flag exit_flags[] = {
+
+  /* Cause codes. */
+  { "running",         TVXF_CAUSEMASK,         TVXST_RUN },
+  { "exited",          TVXF_CAUSEMASK,         TVXST_EXIT },
+  { "killed",          TVXF_CAUSEMASK,         TVXST_KILL },
+  { "stopped",         TVXF_CAUSEMASK,         TVXST_STOP },
+  { "continued",       TVXF_CAUSEMASK,         TVXST_CONT },
+  { "disconnected",    TVXF_CAUSEMASK,         TVXST_DISCONN },
+  { "unknown",         TVXF_CAUSEMASK,         TVXST_UNK },
+  { "error",           TVXF_CAUSEMASK,         TVXST_ERR },
+
+  /*
+    ;;; The signal name table is very boring to type.  To make life less
+    ;;; awful, put the signal names in this list and evaluate the code to
+    ;;; get Emacs to regenerate it.
+
+    (let ((signals '(HUP INT QUIT ILL TRAP ABRT IOT EMT FPE KILL BUS SEGV SYS
+                        PIPE ALRM TERM URG STOP TSTP CONT CHLD CLD TTIN TTOU
+                        POLL IO TIN XCPU XFSZ VTALRM PROF WINCH USR1 USR2
+                        STKFLT INFO PWR THR LWP LIBRT LOST)))
+      (save-excursion
+       (goto-char (point-min))
+       (search-forward (concat "***" "BEGIN siglist" "***"))
+       (beginning-of-line 2)
+       (delete-region (point)
+                      (progn
+                        (search-forward "***END***")
+                        (beginning-of-line)
+                        (point)))
+       (dolist (sig signals)
+         (insert (format "#ifdef SIG%s\n  { \"SIG%s\", TVXF_VALMASK | TVXF_SIG, SIG%s | TVXF_SIG },\n#endif\n"
+                         sig sig sig)))))
+  */
+
+  /***BEGIN siglist***/
+#ifdef SIGHUP
+  { "SIGHUP", TVXF_VALMASK | TVXF_SIG, SIGHUP | TVXF_SIG },
+#endif
+#ifdef SIGINT
+  { "SIGINT", TVXF_VALMASK | TVXF_SIG, SIGINT | TVXF_SIG },
+#endif
+#ifdef SIGQUIT
+  { "SIGQUIT", TVXF_VALMASK | TVXF_SIG, SIGQUIT | TVXF_SIG },
+#endif
+#ifdef SIGILL
+  { "SIGILL", TVXF_VALMASK | TVXF_SIG, SIGILL | TVXF_SIG },
+#endif
+#ifdef SIGTRAP
+  { "SIGTRAP", TVXF_VALMASK | TVXF_SIG, SIGTRAP | TVXF_SIG },
+#endif
+#ifdef SIGABRT
+  { "SIGABRT", TVXF_VALMASK | TVXF_SIG, SIGABRT | TVXF_SIG },
+#endif
+#ifdef SIGIOT
+  { "SIGIOT", TVXF_VALMASK | TVXF_SIG, SIGIOT | TVXF_SIG },
+#endif
+#ifdef SIGEMT
+  { "SIGEMT", TVXF_VALMASK | TVXF_SIG, SIGEMT | TVXF_SIG },
+#endif
+#ifdef SIGFPE
+  { "SIGFPE", TVXF_VALMASK | TVXF_SIG, SIGFPE | TVXF_SIG },
+#endif
+#ifdef SIGKILL
+  { "SIGKILL", TVXF_VALMASK | TVXF_SIG, SIGKILL | TVXF_SIG },
+#endif
+#ifdef SIGBUS
+  { "SIGBUS", TVXF_VALMASK | TVXF_SIG, SIGBUS | TVXF_SIG },
+#endif
+#ifdef SIGSEGV
+  { "SIGSEGV", TVXF_VALMASK | TVXF_SIG, SIGSEGV | TVXF_SIG },
+#endif
+#ifdef SIGSYS
+  { "SIGSYS", TVXF_VALMASK | TVXF_SIG, SIGSYS | TVXF_SIG },
+#endif
+#ifdef SIGPIPE
+  { "SIGPIPE", TVXF_VALMASK | TVXF_SIG, SIGPIPE | TVXF_SIG },
+#endif
+#ifdef SIGALRM
+  { "SIGALRM", TVXF_VALMASK | TVXF_SIG, SIGALRM | TVXF_SIG },
+#endif
+#ifdef SIGTERM
+  { "SIGTERM", TVXF_VALMASK | TVXF_SIG, SIGTERM | TVXF_SIG },
+#endif
+#ifdef SIGURG
+  { "SIGURG", TVXF_VALMASK | TVXF_SIG, SIGURG | TVXF_SIG },
+#endif
+#ifdef SIGSTOP
+  { "SIGSTOP", TVXF_VALMASK | TVXF_SIG, SIGSTOP | TVXF_SIG },
+#endif
+#ifdef SIGTSTP
+  { "SIGTSTP", TVXF_VALMASK | TVXF_SIG, SIGTSTP | TVXF_SIG },
+#endif
+#ifdef SIGCONT
+  { "SIGCONT", TVXF_VALMASK | TVXF_SIG, SIGCONT | TVXF_SIG },
+#endif
+#ifdef SIGCHLD
+  { "SIGCHLD", TVXF_VALMASK | TVXF_SIG, SIGCHLD | TVXF_SIG },
+#endif
+#ifdef SIGCLD
+  { "SIGCLD", TVXF_VALMASK | TVXF_SIG, SIGCLD | TVXF_SIG },
+#endif
+#ifdef SIGTTIN
+  { "SIGTTIN", TVXF_VALMASK | TVXF_SIG, SIGTTIN | TVXF_SIG },
+#endif
+#ifdef SIGTTOU
+  { "SIGTTOU", TVXF_VALMASK | TVXF_SIG, SIGTTOU | TVXF_SIG },
+#endif
+#ifdef SIGPOLL
+  { "SIGPOLL", TVXF_VALMASK | TVXF_SIG, SIGPOLL | TVXF_SIG },
+#endif
+#ifdef SIGIO
+  { "SIGIO", TVXF_VALMASK | TVXF_SIG, SIGIO | TVXF_SIG },
+#endif
+#ifdef SIGTIN
+  { "SIGTIN", TVXF_VALMASK | TVXF_SIG, SIGTIN | TVXF_SIG },
+#endif
+#ifdef SIGXCPU
+  { "SIGXCPU", TVXF_VALMASK | TVXF_SIG, SIGXCPU | TVXF_SIG },
+#endif
+#ifdef SIGXFSZ
+  { "SIGXFSZ", TVXF_VALMASK | TVXF_SIG, SIGXFSZ | TVXF_SIG },
+#endif
+#ifdef SIGVTALRM
+  { "SIGVTALRM", TVXF_VALMASK | TVXF_SIG, SIGVTALRM | TVXF_SIG },
+#endif
+#ifdef SIGPROF
+  { "SIGPROF", TVXF_VALMASK | TVXF_SIG, SIGPROF | TVXF_SIG },
+#endif
+#ifdef SIGWINCH
+  { "SIGWINCH", TVXF_VALMASK | TVXF_SIG, SIGWINCH | TVXF_SIG },
+#endif
+#ifdef SIGUSR1
+  { "SIGUSR1", TVXF_VALMASK | TVXF_SIG, SIGUSR1 | TVXF_SIG },
+#endif
+#ifdef SIGUSR2
+  { "SIGUSR2", TVXF_VALMASK | TVXF_SIG, SIGUSR2 | TVXF_SIG },
+#endif
+#ifdef SIGSTKFLT
+  { "SIGSTKFLT", TVXF_VALMASK | TVXF_SIG, SIGSTKFLT | TVXF_SIG },
+#endif
+#ifdef SIGINFO
+  { "SIGINFO", TVXF_VALMASK | TVXF_SIG, SIGINFO | TVXF_SIG },
+#endif
+#ifdef SIGPWR
+  { "SIGPWR", TVXF_VALMASK | TVXF_SIG, SIGPWR | TVXF_SIG },
+#endif
+#ifdef SIGTHR
+  { "SIGTHR", TVXF_VALMASK | TVXF_SIG, SIGTHR | TVXF_SIG },
+#endif
+#ifdef SIGLWP
+  { "SIGLWP", TVXF_VALMASK | TVXF_SIG, SIGLWP | TVXF_SIG },
+#endif
+#ifdef SIGLIBRT
+  { "SIGLIBRT", TVXF_VALMASK | TVXF_SIG, SIGLIBRT | TVXF_SIG },
+#endif
+#ifdef SIGLOST
+  { "SIGLOST", TVXF_VALMASK | TVXF_SIG, SIGLOST | TVXF_SIG },
+#endif
+  /***END***/
+
+  /* This should be folded into the signal entries above. */
+  { "signal",          TVXF_SIG,               TVXF_SIG },
+
+  TVEC_ENDFLAGS
+};
+static const struct tvec_flaginfo exit_flaginfo =
+  { "exit-status", exit_flags, &tvrange_uint };
+static const struct tvec_vardef exit_var =
+  { sizeof(struct tvec_reg), setvar_local,
+    { "@exit", -1, &tvty_flags, 0, { &exit_flaginfo } } };
+
+/* Progress. */
+
+static const struct tvec_vardef progress_var =
+  { sizeof(struct tvec_reg), setvar_local,
+    { "@progress", -1, &tvty_text, 0 } };
+
+/* Reconnection. */
+
+static const struct tvec_uassoc reconn_assocs[] = {
+  { "on-demand",       TVRCN_DEMAND },
+  { "force",           TVRCN_FORCE },
+  { "skip",            TVRCN_SKIP },
+  TVEC_ENDENUM
+};
+static const struct tvec_uenuminfo reconn_enuminfo =
+  { "remote-reconnection", reconn_assocs, &tvrange_uint };
+static const struct tvec_vardef reconn_var =
+  { sizeof(struct tvec_reg), setvar_local,
+    { "@reconnect", -1, &tvty_uenum, 0, { &reconn_enuminfo } } };
+
+/*----- Client ------------------------------------------------------------*/
+
+/* Connection state. */
+enum {
+  CONN_BROKEN = -2,                    /* previously broken */
+  CONN_FAILED = -1,                    /* attempt freshly failed */
+  CONN_ESTABLISHED = 0,                        /* previously established */
+  CONN_FRESH = 1                       /* freshly connected */
+};
+
+/* --- @handle_packets@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *             @unsigned f@ = receive flags (@RCVF_...@)
+ *             @uint16 end@ = expected end packet type
+ *             @buf *b_out@ = buffer in which to return end packet payload
+ *
+ * Returns:    A @RECV_...@ code.
+ *
+ * Use:                Handles notification packets from the server until a final
+ *             termination packet is received.
+ *
+ *             The client/server protocol consists of a number of flows,
+ *             beginning with a request from the client, followed by a
+ *             number of notifications from the server, and terminated by an
+ *             acknowledgement to the original request indicating that the
+ *             server has completed acting on the original request.
+ *
+ *             This function handles the notifications issued by the server,
+ *             returning when one of the following occurs: (a) a packet of
+ *             type @end@ is received, in which case the function returns
+ *             @RECV_OK@ and the remainder of the packet payload is left in
+ *             @b_out@; (b) the flag @RCVF_ALLOWEOF@ was set in @f@ on entry
+ *             and end-of-file is received at a packet boundary, in which
+ *             case the function returns @RECV_EOF@; or (c) an I/O error
+ *             occurs, in which case @ioerr@ is called and the function
+ *             returns @RECV_FAIL@.
+ */
+
+static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r,
+                         unsigned f, uint16 end, buf *b_out)
+{
+  struct tvec_output *o = tv->output;
+  uint16 pk, u, v;
+  const char *p; size_t n;
+  dstr d = DSTR_INIT;
+  buf *b = b_out;
+  const struct tvec_regdef *rd;
+  struct bench_timing bt;
+  struct tvec_reg *reg = 0;
+  unsigned i;
+  int rc;
+
+  for (;;) {
+
+    /* Read the next packet.  If we didn't receive one then end the loop.
+     * Otherwise, retrieve the packet type and check it against @end@: quit
+     * the loop if we get a match.
+     */
+    rc = remote_recv(tv, &r->rc, f, b); if (rc) break;
+    if (buf_getu16l(b, &pk)) goto bad;
+    if (pk == end) { rc = 0; break; }
+
+    /* Dispatch based on the packet type. */
+    switch (pk) {
+
+      case TVPK_PROGRESS:
+       /* A progress report.  Update the saved progress. */
+
+       p = buf_getmem16l(b, &n); if (!p) goto bad;
+       if (BLEFT(b)) goto bad;
+
+       DRESET(&r->progress); DPUTM(&r->progress, p, n); DPUTZ(&r->progress);
+       break;
+
+      case TVPK_REPORT:
+       /* A report.  Recover the message and pass it along. */
+
+       if (buf_getu16l(b, &u)) goto bad;
+       p = buf_getmem16l(b, &n); if (!p) goto bad;
+       if (BLEFT(b)) goto bad;
+
+       DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+       tvec_report(tv, u, "%s", d.buf);
+       break;
+
+      case TVPK_SKIPGRP:
+       /* A request to skip the group.  Recover the excuse message and pass
+        * it along.
+        */
+
+       p = buf_getmem16l(b, &n); if (!p) goto bad;
+       if (BLEFT(b)) goto bad;
+
+       DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+       tvec_skipgroup(tv, "%s", d.buf);
+       break;
+
+      case TVPK_SKIP:
+       /* A request to skip the test.  Recover the excuse message and pass
+        * it along, if it's not unreasonable.
+        */
+
+       if (!(tv->f&TVSF_ACTIVE)) {
+         rc = ioerr(tv, &r->rc, "test `%s' not active", tv->test->name);
+         goto end;
+       }
+
+       p = buf_getmem16l(b, &n); if (!p) goto bad;
+       if (BLEFT(b)) goto bad;
+
+       DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+       tvec_skip(tv, "%s", d.buf);
+       break;
+
+      case TVPK_FAIL:
+       /* A report that the test failed.  Recover the detail message, if
+        * any, and pass it along, if it's not unreasonable.
+        */
+
+       if (!(tv->f&TVSF_ACTIVE) &&
+           ((tv->f&TVSF_OUTMASK) != (TVOUT_LOSE << TVSF_OUTSHIFT))) {
+         rc = ioerr(tv, &r->rc, "test `%s' not active or failing",
+                    tv->test->name);
+         goto end;
+       }
+
+       rc = buf_getbyte(b); if (rc < 0) goto bad;
+       if (rc) { p = buf_getmem16l(b, &n); if (!p) goto bad; }
+       else p = 0;
+       if (BLEFT(b)) goto bad;
+
+       if (!p)
+         tvec_fail(tv, 0);
+       else {
+         DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+         tvec_fail(tv, "%s", d.buf);
+       }
+       break;
+
+      case TVPK_DUMPREG:
+       /* A request to dump a register. */
+
+       /* Find the register definition. */
+       if (buf_getu16l(b, &u) || buf_getu16l(b, &v)) goto bad;
+       for (rd = tv->test->regs, i = 0; rd->name; rd++, i++)
+         if (i == u) goto found_reg;
+       rc = ioerr(tv, &r->rc,
+                  "register definition %u out of range for test `%s'",
+                  u, tv->test->name);
+       goto end;
+      found_reg:
+       if (v >= TVRD_LIMIT) {
+         rc = ioerr(tv, &r->rc, "register disposition %u out of range", v);
+         goto end;
+       }
+
+       /* Read the flag.  If there's no register value, then `dump' its
+        * absence.  Otherwise retrieve the register value and dump it.
+        */
+       rc = buf_getbyte(b); if (rc < 0) goto bad;
+       if (!rc)
+         tvec_dumpreg(tv, v, 0, rd);
+       else {
+         if (!reg) reg = xmalloc(tv->regsz);
+         rd->ty->init(&reg->v, rd);
+         rc = rd->ty->frombuf(b, &reg->v, rd);
+         if (!rc) tvec_dumpreg(tv, v, &reg->v, rd);
+         rd->ty->release(&reg->v, rd);
+         if (rc) goto bad;
+       }
+       if (BLEFT(b)) goto bad;
+       break;
+
+      case TVPK_BBENCH:
+       /* A report that we're starting a benchmark.  Pass this along. */
+
+       p = buf_getmem32l(b, &n); if (!p) goto bad;
+       if (buf_getu16l(b, &u)) goto bad;
+       if (BLEFT(b)) goto bad;
+       if (u >= TVBU_LIMIT) {
+         rc = ioerr(tv, &r->rc, "unit code %u out of range", u);
+         goto end;
+       }
+
+       DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+       o->ops->bbench(o, d.buf, u);
+       break;
+
+      case TVPK_EBENCH:
+       /* A report that a benchmark completed.  Pass this along. */
+
+       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 ((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;
+       if (BLEFT(b)) goto bad;
+
+       DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d);
+       o->ops->ebench(o, d.buf, u, v&BTF_ANY ? &bt : 0);
+       break;
+
+      default:
+       /* Something else.  This is unexpected. */
+
+       rc = ioerr(tv, &r->rc, "unexpected packet type 0x%04x", pk);
+       goto end;
+    }
+  }
+
+end:
+  DDESTROY(&d);
+  xfree(reg);
+  return (rc);
+bad:
+  rc = malformed(tv, &r->rc); goto end;
+}
+
+/* --- @reap_kid@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *
+ * Returns:    ---
+ *
+ * Use:                Determine the exit status of a broken connection, setting
+ *             @r->exit@ appropriately.
+ *
+ *             If @r->kid@ is negative, the exit status has already been
+ *             set, and nothing further happens; this is not an error.
+ *
+ *             If @r->kid@ is zero, then there is no real child process
+ *             (e.g., because the remote connection is a network connection
+ *             or similar), so @r->exit@ is set equal to @RVXST_DISCONN@.
+ *
+ *             If @r->kid@ is positive, then it holds a child process id;
+ *             the function waits for it to end and collects its exit status
+ *
+ *             It is an error to call this function if the connection is not
+ *             broken.
+ */
+
+static void reap_kid(struct tvec_state *tv, struct tvec_remotectx *r)
+{
+  pid_t kid;
+  int st;
+
+  assert(r->rc.f&TVRF_BROKEN);
+  if (!r->kid)
+    { r->exit = TVXST_DISCONN; r->kid = -1; }
+  else if (r->kid > 0) {
+    kid = waitpid(r->kid, &st, 0);
+    if (kid < 0) {
+      tvec_notice(tv, "failed to wait for remote child: %s",
+                 strerror(errno));
+      r->exit = TVXST_ERR;
+    } else if (!kid) {
+      tvec_notice(tv, "remote child vanished without a trace");
+      r->exit = TVXST_ERR;
+    } else if (WIFCONTINUED(st))
+      r->exit = TVXST_CONT;
+    else if (WIFSIGNALED(st))
+      r->exit = TVXST_KILL | TVXF_SIG | WTERMSIG(st);
+    else if (WIFSTOPPED(st))
+      r->exit = TVXST_STOP | TVXF_SIG | WSTOPSIG(st);
+    else if (WIFEXITED(st))
+      r->exit = TVXST_EXIT | WEXITSTATUS(st);
+    else {
+      tvec_notice(tv, "remote child died with unknown status 0x%04x",
+                 (unsigned)st);
+      r->exit = TVXST_UNK;
+    }
+    r->kid = -1;
+  }
+}
+
+/* --- @report_errline@ --- *
+ *
+ * Arguments:  @char *p@ = pointer to the line
+ *             @size_t n@ = length in characters
+ *             @void *ctx@ = context, secretly a @struct tvec_remotectx@
+ *
+ * Returns:    ---
+ *
+ * Use:                Print a line of stderr output from the child.  If
+ *             @TVRF_MUFFLE@ is set, then discard the line silently.
+ *
+ *             This is an @lbuf_func@, invoked via @drain_errfd@.
+ */
+
+static void report_errline(char *p, size_t n, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  struct tvec_state *tv = r->tv;
+
+  if (p && !(r->rc.f&TVRF_MUFFLE))
+    tvec_notice(tv, "child process stderr: %s", p);
+}
+
+/* --- @drain_errfd@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *             @unsigned f@ = receive flags (@ERF_...@)
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Collect material written by the child to its stderr stream
+ *             and report it.
+ *
+ *             If @f@ has @ERF_SILENT@ set, then discard the stderr material
+ *             without reporting it.  Otherwise it is reported as
+ *             @TVLEV_NOTE@.
+ *
+ *             if @f@ has @ERF_CLOSE@ set, then continue reading until
+ *             end-of-file is received; also, report any final partial line,
+ *             and close @r->errfd@.
+ *
+ *             If @r->errfd@ is already closed, or never established, then
+ *             do nothing and return successfully.
+ */
+
+#define ERF_SILENT 0x0001u
+#define ERF_CLOSE 0x0002u
+static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r,
+                      unsigned f)
+{
+  char *p; size_t sz;
+  ssize_t n;
+  int rc;
+
+  /* Preliminaries.  Bail if there is no error stream to fetch.  Arrange
+   * (rather clumsily) to muffle the output if we're supposed to be client.
+   * And set the nonblocking state on @errfd@ appropriately.
+   */
+  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)) {
+    rc = ioerr(tv, &r->rc, "failed to %s error non-blocking flag",
+              f&ERF_CLOSE ? "clear" : "set");
+    goto end;
+  }
+
+  /* Read pieces of error output and feed them into the line buffer. */
+  for (;;) {
+    sz = lbuf_free(&r->errbuf, &p);
+    n = read(r->errfd, p, sz);
+      if (!n) break;
+      if (n < 0) {
+       if (errno == EINTR) continue;
+       if (!(f&ERF_CLOSE) && (errno == EWOULDBLOCK || errno == EAGAIN))
+         break;
+       rc = ioerr(tv, &r->rc, "failed to read child stderr: %s",
+                  strerror(errno));
+       goto end;
+      }
+    lbuf_flush(&r->errbuf, p, n);
+  }
+
+  /* Done. */
+  rc = 0;
+end:
+  if (f&ERF_CLOSE) {
+    lbuf_close(&r->errbuf);
+    close(r->errfd); r->errfd = -1;
+  }
+  return (rc);
+}
+
+/* --- @disconnect_remote@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *             @unsigned f@ = receive flags (@DCF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Disconnect and shut down all of the remote client state.
+ *
+ *             If @f@ has @DCF_KILL@ set then send the child process (if
+ *             any) @SIGTERM@ to make sure it shuts down in a timely manner.
+ *
+ *             In detail: this function closes the @infd@ and @outfd@
+ *             descriptors, drains and closes @errfd@, and collects the exit
+ *             status (if any).
+ */
+
+#define DCF_KILL 0x0100u
+static void disconnect_remote(struct tvec_state *tv,
+                             struct tvec_remotectx *r, unsigned f)
+{
+  if (r->kid > 0 && (f&DCF_KILL)) kill(r->kid, SIGTERM);
+  close_comms(&r->rc);
+  drain_errfd(tv, r, f | ERF_CLOSE); reap_kid(tv, r);
+}
+
+/* --- @connect_remote@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Connect to the test server.
+ */
+
+static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r)
+{
+  const struct tvec_remoteenv *re = r->re;
+  pid_t kid = 0;
+  buf b;
+  uint16 v;
+  int infd = -1, outfd = -1, errfd = -1, rc;
+
+  /* If we're already connected, then there's nothing to do. */
+  if (r->kid >= 0) { rc = 0; goto end; }
+
+  /* Set the preliminary progress indication. */
+  DRESET(&r->progress); DPUTS(&r->progress, "%INIT");
+
+  /* Call the connection function to establish descriptors. */
+  if (re->r.connect(&kid, &infd, &outfd, &errfd, tv, re))
+    { rc = -1; goto end; }
+
+  /* Establish communications state. */
+  setup_comms(&r->rc, infd, outfd); r->kid = kid; r->errfd = errfd;
+  lbuf_init(&r->errbuf, report_errline, r);
+  r->exit = TVXST_RUN; r->rc.f &= ~TVRF_BROKEN;
+
+  /* Do version negotiation. */
+  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))
+    { rc = -1; goto end; }
+  if (buf_getu16l(&b, &v)) goto bad;
+  if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; }
+  if (v) {
+    rc = ioerr(tv, &r->rc, "protocol version %u not supported", v);
+    goto end;
+  }
+  r->ver = v;
+
+  /* Begin the test group at the server. */
+  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; }
+  if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; }
+
+  /* Done. */
+  rc = 0;
+end:
+  if (rc) disconnect_remote(tv, r, DCF_KILL);
+  return (rc);
+bad:
+  rc = malformed(tv, &r->rc); goto end;
+}
+
+/* --- @check_comms@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *
+ * Returns:    A @CONN_...@ code reflecting the current communication
+ *             state.
+ *
+ * Use:                Determine the current connection state.  If the connection
+ *             has recently broken (i.e., @TVRF_BROKEN@ is set in @r->rc.f@)
+ *             since the last time we checked then disconnect.
+ */
+
+static int check_comms(struct tvec_state *tv, struct tvec_remotectx *r)
+{
+  if (r->kid < 0)
+    return (CONN_BROKEN);
+  else if (r->rc.f&TVRF_BROKEN)
+    { disconnect_remote(tv, r, DCF_KILL); return (CONN_FAILED); }
+  else
+    return (CONN_ESTABLISHED);
+}
+
+/* --- @try_reconnect@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_remotectx *r@ = remote client context
+ *
+ * Returns:    A @CONN_...@ code reflecting the new communication state.
+ *
+ * Use:                Reconnects to the server according to the configured
+ *             @TVRCN_...@ policy.
+ */
+
+static int try_reconnect(struct tvec_state *tv, struct tvec_remotectx *r)
+{
+  int rc;
+
+  switch (r->rc.f&TVRF_RCNMASK) {
+    case TVRCN_DEMAND:
+      rc = check_comms(tv, r);
+      if (rc < CONN_ESTABLISHED) {
+       close_comms(&r->rc);
+       if (connect_remote(tv, r)) rc = CONN_FAILED;
+       else rc = CONN_FRESH;
+      }
+      break;
+    case TVRCN_FORCE:
+      disconnect_remote(tv, r, DCF_KILL);
+      if (connect_remote(tv, r)) rc = CONN_FAILED;
+      else rc = CONN_FRESH;
+      break;
+    case TVRCN_SKIP:
+      rc = check_comms(tv, r);
+      break;
+    default:
+      abort();
+  }
+  return (rc);
+}
+
+/*----- Remote environment ------------------------------------------------*/
+
+/* --- @reset_vars@ --- *
+ *
+ * Arguments:  @struct tvec_remotectx *r@ = remote client context
+ *
+ * Returns:    ---
+ *
+ * Use:                Reset the pseudoregisters set through @tvec_remoteset@.
+ */
+
+static void reset_vars(struct tvec_remotectx *r)
+{
+  const struct tvec_remoteenv *re = r->re;
+
+  r->exwant = TVXST_RUN;
+  r->rc.f = (r->rc.f&~(TVRF_RCNMASK | TVRF_SETMASK)) |
+           (re->r.dflt_reconn&TVRF_RCNMASK);
+  DRESET(&r->prgwant); DPUTS(&r->prgwant, "%DONE");
+}
+
+/* --- @tvec_remotesetup@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const struct tvec_env *env@ = environment description
+ *             @void *pctx@ = parent context (ignored)
+ *             @void *ctx@ = context pointer to initialize
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a timeout environment context.
+ *
+ *             The environment description should be a @struct
+ *             tvec_remoteenv@ subclass suitable for use by the @connect@
+ *             function.
+ */
+
+void tvec_remotesetup(struct tvec_state *tv, const struct tvec_env *env,
+                     void *pctx, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  const struct tvec_remoteenv *re = (const struct tvec_remoteenv *)env;
+  const struct tvec_env *subenv = re->r.env;
+
+  r->tv = tv;
+  init_comms(&r->rc);
+  r->re = re; r->kid = -1;
+  DCREATE(&r->prgwant); DCREATE(&r->progress);
+  if (connect_remote(tv, r))
+    tvec_skipgroup(tv, "failed to connect to test backend");
+  reset_vars(r);
+  if (subenv && subenv->ctxsz) r->subctx = xmalloc(subenv->ctxsz);
+  else r->subctx = 0;
+  if (subenv && subenv->setup) subenv->setup(tv, subenv, r, r->subctx);
+}
+
+/* --- @tvec_remotefindvar@, @setvar_local@, @setvar_remote@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *var@ = variable name to set
+ *             @const union tvec_regval *rv@ = register value
+ *             @void **ctx_out@ = where to put the @setvar@ context
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    @tvec_remotefindvar@ returns a pointer to the variable
+ *             definition, or null; @remote_setvar@ returns zero on success
+ *             or %$-1$% on error.
+ *
+ * Use:                Set a special variable.  The following special variables are
+ *             supported.
+ *
+ *               * %|@exit|% is the expected exit status; see @TVXF_...@ and
+ *                 @TVXST_...@.
+ *
+ *               * %|progress|% is the expected progress token when the test
+ *                 completes.  On successful completion, this will be
+ *                 %|%DONE|%; it's %|%RUN|% on entry to the test function,
+ *                 but that can call @tvec_setprogress@ to change it.
+ *
+ *               * %|reconnect|% is a reconnection policy; see @TVRCN_...@.
+ */
+
+static int setvar_local(struct tvec_state *tv, const char *var,
+                       const union tvec_regval *rv, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+
+  if (STRCMP(var, ==, "@exit")) {
+    if (r->rc.f&TVRF_SETEXIT) return (tvec_dupreg(tv, var));
+    r->exwant = rv->u; r->rc.f |= TVRF_SETEXIT; return (0);
+  } else if (STRCMP(var, ==, "@progress")) {
+    if (r->rc.f&TVRF_SETPRG) return (tvec_dupreg(tv, var));
+    DRESET(&r->prgwant); DPUTM(&r->prgwant, rv->text.p, rv->text.sz);
+    DPUTZ(&r->prgwant);
+    r->rc.f |= TVRF_SETPRG; return (0);
+  } else if (STRCMP(var, ==, "@reconnect")) {
+    if (r->rc.f&TVRF_SETRCN) return (tvec_dupreg(tv, var));
+    r->rc.f = (r->rc.f&~TVRF_RCNMASK) | (rv->u&TVRF_RCNMASK) | TVRF_SETRCN;
+    return (0);
+  } else assert(!"unknown var");
+}
+
+static int setvar_remote(struct tvec_state *tv, const char *var,
+                        const union tvec_regval *rv, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  buf b;
+  int ch, rc;
+
+  if (try_reconnect(tv, r) < 0) { rc = 0; goto end; }
+
+  QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_SETVAR) {
+    dbuf_putstr16l(&r->rc.bout, var);
+    r->vd.def.ty->tobuf(DBUF_BUF(&r->rc.bout), rv, &r->vd.def);
+  } else { rc = -1; goto end; }
+
+  rc = handle_packets(tv, r, 0, TVPK_SETVAR | TVPF_ACK, &b);
+    if (rc) goto end;
+  ch = buf_getbyte(&b);
+    if (ch < 0) { rc = malformed(tv, &r->rc); goto end; }
+  if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; }
+
+  rc = ch ? -1 : 0;
+end:
+  return (rc);
+}
+
+const struct tvec_vardef *tvec_remotefindvar
+  (struct tvec_state *tv, const char *var, void **ctx_out, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  const struct tvec_remoteenv *re = r->re;
+  const struct tvec_env *subenv = re->r.env;
+  const struct tvec_vardef *vd; void *varctx;
+
+  if (STRCMP(var, ==, "@exit"))
+    { *ctx_out = r; return (&exit_var); }
+  else if (STRCMP(var, ==, "@progress"))
+    { *ctx_out = r; return (&progress_var); }
+  else if (STRCMP(var, ==, "@reconnect"))
+    { *ctx_out = r; return (&reconn_var); }
+  else if (subenv && subenv->findvar) {
+    vd = subenv->findvar(tv, var, &varctx, r->subctx);
+    if (!vd) return (0);
+    r->vd.regsz = vd->regsz; r->vd.setvar = setvar_remote;
+    r->vd.def = vd->def;
+    *ctx_out = r; return (&r->vd);
+  } else
+    return (0);
+}
+
+/* --- @tvec_remotebefore@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Invoke the subordinate environment's @before@ function.
+ */
+
+void tvec_remotebefore(struct tvec_state *tv, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  const struct tvec_remoteenv *re = r->re;
+  const struct tvec_env *subenv = re->r.env;
+
+  if (subenv && subenv->before) subenv->before(tv, r->subctx);
+}
+
+/* --- @tvec_remoterun@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @tvec_testfn *fn@ = test function to run
+ *             @void *ctx@ = context pointer for the test function
+ *
+ * Returns:    ---
+ *
+ * Use:                Run a test on a remote server.
+ */
+
+void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  union tvec_regval rv;
+  unsigned f = 0;
+#define f_exit 1u
+#define f_progress 2u
+#define f_fail 4u
+  buf b;
+  int rc;
+
+  /* Reconnect to the server according to policy. */
+  switch (try_reconnect(tv, r)) {
+    case CONN_FAILED:
+      tvec_skip(tv, "failed to connect to test backend"); return;
+    case CONN_BROKEN:
+      tvec_skip(tv, "no connection"); return;
+  }
+
+  /* Set initial progress state. */
+  DRESET(&r->progress); DPUTS(&r->progress, "%IDLE");
+
+  /* Send the command to the server and handle output. */
+  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 { goto fail; }
+  rc = handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_TEST | TVPF_ACK, &b);
+
+  /* Deal with the outcome. */
+  switch (rc) {
+
+    case RECV_FAIL:
+      /* Some kind of error.  Abandon ship. */
+
+    fail:
+      tvec_skip(tv, "remote test runner communications failed");
+      disconnect_remote(tv, r, 0);
+      break;
+
+    case RECV_EOF:
+      /* End-of-file at a packet boundary.  The server crashed trying to run
+       * our test.  Collect the exit status and continue.
+       */
+      reap_kid(tv, r);
+      /* fall through */
+
+    case RECV_OK:
+      /* Successful completion (or EOF). */
+
+      /* Notice if the exit status isn't right. */
+      if (r->exit != r->exwant) f |= f_exit;
+
+      /* Notice if the progress token isn't right. */
+      if (r->progress.len != r->prgwant.len ||
+         MEMCMP(r->progress.buf, !=, r->prgwant.buf, r->progress.len))
+       f |= f_progress;
+
+      /* If we found something wrong but the test is passing so far, then
+       * report the failure and dump the input registers.
+       */
+      if (f && (tv->f&TVSF_ACTIVE))
+       { tvec_fail(tv, 0); tvec_mismatch(tv, TVMF_IN); }
+
+      /* If the test failed, then report the exit and progress states
+       * relative to their expectations.
+       */
+      if (!(tv->f&TVSF_ACTIVE) &&
+         (tv->f&TVSF_OUTMASK) == (TVOUT_LOSE << TVSF_OUTSHIFT)) {
+
+       /* Note here that the test failed. */
+       f |= f_fail;
+
+       /* Report exit status. */
+       rv.u = r->exit;
+       tvec_dumpreg(tv, f&f_exit ? TVRD_FOUND : TVRD_MATCH,
+                    &rv, &exit_var.def);
+       if (f&f_exit) {
+         rv.u = r->exwant;
+         tvec_dumpreg(tv, TVRD_EXPECT, &rv, &exit_var.def);
+       }
+
+       /* Report progress token. */
+       rv.text.p = r->progress.buf; rv.text.sz = r->progress.len;
+       tvec_dumpreg(tv, f&f_progress ? TVRD_FOUND : TVRD_MATCH,
+                    &rv, &progress_var.def);
+       if (f&f_progress) {
+         rv.text.p = r->prgwant.buf; rv.text.sz = r->prgwant.len;
+         tvec_dumpreg(tv, TVRD_EXPECT, &rv, &progress_var.def);
+       }
+      }
+
+      /* If we received end-of-file, then close the connection.  Suppress
+       * error output if the test passed: it was presumably expected.
+       */
+      if (rc == RECV_EOF)
+       disconnect_remote(tv, r, f ? 0 : ERF_SILENT);
+      break;
+  }
+
+#undef f_exit
+#undef f_progress
+#undef f_fail
+}
+
+/* --- @tvec_remoteafter@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Reset variables to their default values.
+ */
+
+void tvec_remoteafter(struct tvec_state *tv, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  const struct tvec_remoteenv *re = r->re;
+  const struct tvec_env *subenv = re->r.env;
+
+  reset_vars(r);
+  if (subenv && subenv->after) subenv->after(tv, r->subctx);
+}
+
+/* --- @tvec_remoteteardown@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Tear down the remote environment.
+ */
+
+void tvec_remoteteardown(struct tvec_state *tv, void *ctx)
+{
+  struct tvec_remotectx *r = ctx;
+  const struct tvec_remoteenv *re = r->re;
+  const struct tvec_env *subenv = re->r.env;
+  buf b;
+
+  if (subenv && subenv->teardown) subenv->teardown(tv, r->subctx);
+  xfree(r->subctx);
+  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 --------------------------------------------------------*/
+
+/* --- @fork_common@ --- *
+ *
+ * Arguments:  @pid_t *kid_out@ = where to put child process-id
+ *             @int *infd_out, *outfd_out, *errfd_out@ = where to put file
+ *                     descriptors
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Common @fork@ machinery for the connectors.  Create a
+ *             subprocess.  On successful return, in the subprocess,
+ *             @*kid_out@ is zero, and the error descriptor replaces the
+ *             standard-error descriptor; in the parent, @*kid_out@ is the
+ *             child process-id, and @*errfd_out@ is a descriptor on which
+ *             the child's standard-error output can be read; in both
+ *             @*infd_out@ and @*outfd_out@ are descriptors for input and
+ *             output respectively -- they're opposite ends of pipes, but
+ *             obviously they're crossed over so that the parent's output
+ *             matches the child's input and %%\emph{vice versa}%%.
+ */
+
+static int fork_common(pid_t *kid_out, int *infd_out, int *outfd_out,
+                      int *errfd_out, struct tvec_state *tv)
+{
+  int p0[2] = { -1, -1 }, p1[2] = { -1, -1 }, pe[2] = { -1, -1 };
+  pid_t kid = -1;
+  int rc;
+
+  /* Try to create the pipes. */
+  if (pipe(p0) || pipe(p1) || pipe(pe) ||
+      fdflags(p0[1], 0, 0, FD_CLOEXEC, FD_CLOEXEC) ||
+      fdflags(p1[0], 0, 0, FD_CLOEXEC, FD_CLOEXEC) ||
+      fdflags(pe[0], 0, 0, FD_CLOEXEC, FD_CLOEXEC)) {
+    tvec_error(tv, "pipe failed: %s", strerror(errno));
+    rc = -1; goto end;
+  }
+
+  /* Flush all of the stream buffers so that we don't get duplicated
+   * output.
+   */
+  fflush(0);
+
+  /* Try to set up the child process. */
+  kid = fork();
+  if (kid < 0) {
+    tvec_error(tv, "fork failed: %s", strerror(errno));
+    rc = -1; goto end;
+  }
+
+  if (!kid) {
+    /* Child process. */
+
+    *kid_out = 0;
+    *infd_out = p0[0]; p0[0] = -1;
+    *outfd_out = p1[1]; p1[1] = -1;
+    if (pe[1] != STDERR_FILENO && dup2(pe[1], STDERR_FILENO) < 0) {
+      fprintf(stderr, "failed to establish child stderr: %s",
+             strerror(errno));
+      _exit(127);
+    }
+  } else {
+    /* Parent process. */
+
+    *kid_out = kid; kid = -1;
+    *infd_out = p1[0]; p1[0] = -1;
+    *outfd_out = p0[1]; p0[1] = -1;
+    *errfd_out = pe[0]; pe[0] = -1;
+  }
+
+  /* All done. */
+  rc = 0;
+
+end:
+  /* Clean up.  So much of this... */
+  if (p0[0] >= 0) close(p0[0]);
+  if (p0[1] >= 0) close(p0[1]);
+  if (p1[0] >= 0) close(p1[0]);
+  if (p1[1] >= 0) close(p1[1]);
+  if (pe[0] >= 0) close(pe[0]);
+  if (pe[1] >= 0) close(pe[1]);
+  return (rc);
+}
+
+/* --- @tvec_fork@ --- *
+ *
+ * Arguments:  @pid_t *kid_out@ = where to put child process-id
+ *             @int *infd_out, *outfd_out, *errfd_out@ = where to put file
+ *                     descriptors
+ *             @struct tvec_state *tv@ = test vector state
+ *             @const struct tvec_remoteenv@ = the remote environment
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Starts a remote server running in a fork of the main
+ *             process.  This is useful for testing functions which might --
+ *             or are even intended to -- crash.
+ */
+
+int tvec_fork(pid_t *kid_out, int *infd_out, int *outfd_out, int *errfd_out,
+             struct tvec_state *tv, const struct tvec_remoteenv *env)
+{
+  struct tvec_config config;
+  const struct tvec_remotefork *rf = (const struct tvec_remotefork *)env;
+  pid_t kid = -1;
+  int infd = -1, outfd = -1, errfd = -1;
+  int rc;
+
+  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;
+    _exit(tvec_remoteserver(infd, outfd, &config));
+  }
+
+  *kid_out = kid; *infd_out = infd; *outfd_out = outfd; *errfd_out = errfd;
+  rc = 0;
+end:
+  return (rc);
+}
+
+/* --- @tvec_exec@ --- *
+ *
+ * Arguments:  @pid_t *kid_out@ = where to put child process-id
+ *             @int *infd_out, *outfd_out, *errfd_out@ = where to put file
+ *                     descriptors
+ *             @struct tvec_state *tv@ = test vector state
+ *             @const struct tvec_remoteenv@ = the remote environment
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Starts a remote server by running some program.  The command
+ *             given in the environment description will probably some hairy
+ *             shell rune allowing for configuration via files or
+ *             environment variables.
+ */
+
+int tvec_exec(pid_t *kid_out, int *infd_out, int *outfd_out, int *errfd_out,
+             struct tvec_state *tv, const struct tvec_remoteenv *env)
+{
+  const struct tvec_remoteexec *rx = (const struct tvec_remoteexec *)env;
+  pid_t kid = -1;
+  int infd = -1, outfd = -1, errfd = -1;
+  mdup_fd v[2];
+  int rc;
+
+  if (fork_common(&kid, &infd, &outfd, &errfd, tv)) { rc = -1; goto end; }
+  if (!kid) {
+    v[0].cur = infd; v[0].want = STDIN_FILENO;
+    v[1].cur = outfd; v[1].want = STDOUT_FILENO;
+    if (mdup(v, 2)) {
+      fprintf(stderr, "failed to establish standard file descriptors: %s",
+             strerror(errno));
+      exit(127);
+    }
+    execvp(rx->x.args[0], (/*uncosnt*/ char *const *)rx->x.args);
+    fprintf(stderr, "failed to invoke test runner: %s", strerror(errno));
+    exit(127);
+  }
+
+  *kid_out = kid; *infd_out = infd; *outfd_out = outfd; *errfd_out = errfd;
+  rc = 0;
+end:
+  return (rc);
+}
 
 /*----- That's all, folks -------------------------------------------------*/