chiark / gitweb /
Merge branches 'cleanup' and 'services'
authorMark Wooding <mdw@distorted.org.uk>
Mon, 1 Jan 2007 15:30:39 +0000 (15:30 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 1 Jan 2007 15:30:39 +0000 (15:30 +0000)
* cleanup:
  server/admin: Fix tokenization of statistics output.
  Fix typos in messages.
  doc: Various small cleanups to tripe-admin.5.
  cleanup: Various simple whitespace changes.
  cleanup: Rename a few badly-chosen variables.

* services:
  doc: Document the services messages.
  admin: Implement the main job commands.
  admin: Implement job table infrastructure.
  admin: Service ownership infrastructure and commands.
  servutil: Implement version number comparison.
  admin: New ?TOKENS formatting directive.
  admin: Rename the unknown-service error.
  admin: Improve handling of background jobs.
  admin: Option parser macros.
  admin: Put all command options at the start of the command-line.
  admin: Fix premature close in a_bgrelease.
  admin: Remove locking; new safe client destruction.

Conflicts:

server/admin.c

client/tripectl.c
doc/tripe-admin.5.in
init/tripe-init.in
pkstream/pkstream.c
proxy/tripe-mitm.c
server/admin.c
server/servutil.c
server/tripe.c
server/tripe.h

index 4aece8d10462f2632a93c88796998d0ed2a9ef00..7386b304b3a7dd808b4cd46a978fd5073e543498 100644 (file)
@@ -268,10 +268,7 @@ static void logfile(const char *name)
   }
 }
 
-static void sighup(int sig, void *v)
-{
-  logfile(logname);
-}
+static void sighup(int sig, void *v) { logfile(logname); }
 
 static void cleanup(void)
 {
@@ -287,9 +284,7 @@ static void sigdie(int sig)
 }
 
 static void version(FILE *fp)
-{
-  pquis(fp, "$, TrIPE version " VERSION "\n");
-}
+  { pquis(fp, "$, TrIPE version " VERSION "\n"); }
 
 static void usage(FILE *fp)
 {
index 0d9e7b6641f2a7b9ac667731fab2cd2089abd0a9..822bbf508b588eddb4b7814bc8f0394b76e5d2ac 100644 (file)
@@ -65,10 +65,15 @@ All commands can be run as simple commands.  Long-running commands
 and
 .BR PING )
 block the client until they finish, but the rest of the server continues
-running.
-.SS "Asynchronous messages"
-There are three types of asynchronous messages which
-aren't associated with any particular command.
+running.  See
+.B "Background commands"
+to find out how to issue long-running commands without blocking.
+.SS "Asynchronous broadcasts"
+There are three types of asynchronous broadcast messages which aren't
+associated with any particular command.  Clients can select which
+broadcast messages they're interested in using the
+.B WATCH
+command.
 .PP
 The
 .B WARN
@@ -92,10 +97,6 @@ Finally, the
 .B NOTE
 message is a machine-readable notification about some routine but
 interesting event such as creation or destruction of peers.
-.PP
-The presence of asynchronous messages can be controlled using the
-.B WATCH
-command.
 .SS "Background commands"
 Some commands (e.g.,
 .B ADD
@@ -150,6 +151,64 @@ response: it will always detach and then issue any
 lines followed by
 .B BGOK
 response.
+.SS "Client-provided services"
+.\"* 25 Service-related messages
+An administration client can provide services to other clients.
+Services are given names and versions.  A client can attempt to
+.I claim
+a particular service by issuing the
+.B SVCCLAIM
+command.  This may fail, for example, if some other client already
+provides the same or later version of the service.
+.PP
+Other clients can issue
+.I "service commands"
+using the
+.B "SVCSUBMIT"
+command; the service provider is expected to handle these commands and
+reply to them.
+.PP
+There are three important asynchronous messages which will be sent to
+service providers.
+.SP
+.BI "SVCCANCEL " jobid
+The named job has been cancelled, either because the issuing client has
+disconnected or explicitly cancelled the job using the
+.B BGCANCEL
+command.
+.SP
+.BI "SVCCLAIM " service " " version
+Another client has claimed a later version of the named
+.I service.  The recipient is no longer the provider of this service.
+.SP
+.BI "SVCJOB " jobid " " service " " command " " args \fR...
+Announces the arrival of a new job.  The
+.I jobid
+is a simple token consisting of alphanumeric characters which
+.B tripe
+uses to identify this job.
+.PP
+The service provider can reply to the job using the commands
+.BR SVCINFO ,
+.B SVCOK
+and
+.BR SVCFAIL .
+The first of these sends an
+.B INFO
+response and leaves the job active; the other two send an
+.B OK
+or
+.B FAIL
+response respectively, and mark the job as being complete.
+.PP
+(Since
+.B SVCSUBMIT
+is a potentially long-running command, it can be run in the background.
+This detail is hidden from service providers:
+.B tripe
+will issue the corresponding
+.BR BG ...
+responses when appropriate.)
 .SS "Network addresses"
 A network address is a sequence of words.  The first is a token
 identifying the network address family.  The length of an address and
@@ -164,14 +223,13 @@ An Internet socket, naming an IPv4 address and UDP port.  On output, the
 address is always in numeric dotted-quad form, and the port is given as
 a plain number.  On input, DNS hostnames and symbolic port names are
 permitted.  Name resolution does not block the main server, but will
-block the requesting client.  This hopefully makes life simpler for
-stupid clients.  Complex clients which don't wish to be held up can open
-extra connections or do the resolution themselves.)
+block the requesting client, unless the command is run in the background.
 .PP
 If, on input, no recognised address family token is found, the following
 words are assumed to represent an
 .B INET
-address.
+address.  Addresses output by the server always have an address family
+token.
 .SS "Key-value output"
 Some commands (e.g.,
 .B STATS
@@ -213,7 +271,7 @@ letters control collections of message types.
 .\"* 10 Commands
 The commands provided are:
 .SP
-.BI "ADD " peer " \fR[" options "\fR] " address "\fR..."
+.BI "ADD \fR[" options "\fR] " peer " " address "\fR..."
 Adds a new peer.  The peer is given the name
 .IR peer ;
 the peer's public key is assumed to be in the file
@@ -257,6 +315,10 @@ Emits an
 line reporting the IP address and port number stored for
 .IR peer .
 .SP
+.BI "BGCANCEL " tag
+Cancels the background job with the named
+.IR tag .
+.SP
 .BI "CHECKCHAL " challenge
 Verifies a challenge as being one earlier issued by
 .B GETCHAL
@@ -267,7 +329,7 @@ or in a greeting message.
 .B "DAEMON"
 Causes the server to disassociate itself from its terminal and become a
 background task.  This only works once.  A warning is issued.
-.TP
+.SP
 .BI "EPING \fR[" options "\fR] " peer
 Sends an encrypted ping to the peer, and expects an encrypted response.
 This checks that the peer is running (and not being impersonated), and
@@ -311,6 +373,11 @@ packets which are to be encrypted and sent to
 Used by configuration scripts so that they can set up routing tables
 appropriately after adding new peers.
 .SP
+.B "JOBS"
+Emits an
+.B INFO
+line giving the tag for each outstanding background job.
+.SP
 .BI "KILL " peer
 Causes the server to forget all about
 .IR peer .
@@ -379,10 +446,16 @@ Run the command in the background, using the given
 .BI "\-timeout " time
 Wait for
 .I time
-seconds before giving up on a response.  The default is 5 seconds.  (The
-time format is the same as for the
-.B "ADD \-keepalive"
-option.)
+seconds before giving up on a response.  The default is 5 seconds.  The
+.I time
+is expressed as a nonnegative integer followed optionally by
+.BR d ,
+.BR h ,
+.BR m ,
+or
+.BR s
+for days, hours, minutes, or seconds respectively; if no suffix is
+given, seconds are assumed.
 .\"-opts
 .RE
 .SP
@@ -434,6 +507,108 @@ This is useful if firewalling decisions are made based on interface
 names: a setup script for a particular peer can change the name, and
 then update the server's records so that they're accurate.
 .SP
+.BI "SVCCLAIM " service " " version
+Attempts to claim the named
+.IR service ,
+offering the given
+.IR version .
+The claim is successful if the service is currently unclaimed, or if
+a version earlier than
+.I version
+is provided; otherwise the command fails with the error
+.BR "service-exists" .
+.SP
+.BI "SVCENSURE " service " \fR[" version \fR]
+Ensure that 
+.I service
+is provided, and (if specified) to at least the given
+.IR version .
+An error is reported if these conditions are not met; otherwise the
+command succeeds silently.
+.SP
+.BI "SVCFAIL " jobid " " tokens \fR...
+Send a
+.B FAIL
+(or
+.BR BGFAIL )
+response to the service job with the given
+.IR jobid ,
+passing the 
+.I tokens
+as the reason for failure.  The job is closed.
+.SP
+.BI "SVCINFO " jobid " " tokens \fR...
+Send an
+.B INFO
+(or
+.BR BGINFO )
+response to the service job with the given
+.IR jobid ,
+passing the
+.I tokens
+as the info message.  The job remains open.
+.SP
+.B "SVCLIST"
+Output a line of the form
+.RS
+.IP
+.B INFO
+.I service
+.I version
+.PP
+for each service currently provided.
+.RE
+.SP
+.BI "SVCOK " jobid
+Send an
+.B OK
+(or
+.BR BGINFO )
+response to the service job with the given
+.IR jobid .
+The job is closed.
+.SP
+.BI "SVCQUERY " service
+Emits a number of
+.B info
+lines in key-value format, describing the named
+.IR service.
+The following keys are used.
+.RS
+.TP
+.B name
+The service's name.
+.TP
+.B version
+The service's version string.
+.RE
+.SP
+.BI "SVCRELEASE " service
+Announce that the client no longer wishes to provide the named
+.IR service .
+.SP
+.BI "SVCSUBMIT \fR[" options "\fR] " service " " command " " arguments \fR...
+Submit a job to the provider of the given
+.IR service ,
+passing it the named
+.I command
+and the given
+.IR arguments .
+The following options are accepted.
+.RS
+.\"+opts
+.TP
+.BI "\-background " tag
+Run the command in the background, using the given
+.IR tag .
+.TP
+.BI "\-version " version
+Ensure that at least the given
+.I version
+of the service is available before submitting the job.
+.RE
+.\"-opts
+.SP
 .BI "STATS " peer
 Emits a number of
 .B INFO
@@ -524,15 +699,15 @@ its version string.  The server name
 is reserved to the Straylight/Edgeware implementation.
 .SP
 .BR "WATCH " [\fIoptions\fP]
-Enables or disables asynchronous messages
+Enables or disables asynchronous broadcasts
 .IR "for the current connection only" .
 See
 .B "Trace lists" 
 above.  The default watch state for the connection the server opens
 automatically on stdin/stdout is to show warnings and trace messages;
-other connections show no asynchronous messages.  (This is done in order
-to guarantee that a program reading the server's stdout does not miss
-any warnings.)
+other connections show no asynchronous broadcast messages.  (This is
+done in order to guarantee that a program reading the server's stdout
+does not miss any warnings.)
 .RS
 .PP
 Message types provided are:
@@ -614,6 +789,13 @@ An error occurred during the attempt to become a daemon, as reported by
 .BR ADD .)
 The given port number is out of range.
 .SP
+.BI "not-service-provider " service
+(For 
+.BR SVCRELEASE .)
+The invoking client is not the current provider of the named
+.IR service ,
+and is therefore not allowed to release it.
+.SP
 .BI "peer-create-fail " peer
 (For
 .BR ADD .)
@@ -646,6 +828,30 @@ The DNS name
 .I hostname
 took too long to resolve.
 .SP
+.BI "service-exists " service " " version
+(For
+.BR SVCCLAIM .)
+Another client is already providing the stated
+.I version
+of the
+.IR service .
+.SP
+.BI "service-too-old " service " " version
+(For
+.B SVCENSURE
+and
+.BR SVCSUBMIT .)
+Only the given
+.I version
+of the requested
+.I service
+is available, which does not meet the stated requirements.
+.SP
+.BI "tag-exists " tag
+(For long-running commands.)  The named
+.I tag
+is already the tag of an outstanding job.
+.SP
 .BI "unknown-command " token
 The command
 .B token
@@ -662,13 +868,32 @@ and
 There is no peer called
 .IR name .
 .SP
-.BI "unknown-service " service
+.BI "unknown-port " port
 (For
 .BR ADD .)
-The service name
-.I service
+The port name
+.I port
 couldn't be found in 
 .BR /etc/services .
+.TP
+.BI "unknown-service " service
+(For
+.BR SVCENSURE ,
+.BR SVCQUERY ,
+.BR SVCRELEASE ,
+and
+.BR SVCSUBMIT .)
+The token
+.I service
+is not recognized as the name of a client-provided service.
+.TP
+.BI "unknown-tag " tag
+(For
+.BR BGCANCEL .)
+The given
+.I tag
+is not the tag for any outstanding background job.  It may have just
+finished.
 .SH "NOTIFICATIONS"
 .\"* 30 Notification broadcasts (NOTE codes)
 The following notifications are sent to clients who request them.
@@ -717,6 +942,17 @@ as a result of a
 .B SETIFNAME
 command.
 .SP
+.BI "SVCCLAIM " service " " version
+The named
+.I service
+is now available, at the stated
+.IR version .
+.SP
+.BI "SVCRELEASE " service
+The named
+.I service
+is no longer available.
+.SP
 .BI "USER " tokens\fR...
 An administration client issued a notification using the
 .B NOTIFY
@@ -1096,6 +1332,7 @@ An administration client issued a warning.
 .SH "SUMMARY"
 .SS "Command responses"
 .nf
+.BI "BGDETACH " tag
 .BI "BGFAIL " tag " " tokens \fR...
 .BI "BGINFO " tag " " tokens \fR...
 .BI "BGOK " tag
index e3d835ac6f24a10bf27af251868a5e7963f3dc35..e609a653af0a1765e5c1badef08ff6c0e08b646e 100755 (executable)
@@ -160,7 +160,7 @@ case "$1" in
     elif kill `cat $pidfile`; then
       echo " done (killed violently)"
     else
-      echo " it doesn't want do die!"
+      echo " it doesn't want to die!"
       exit 1
     fi
     ;;
index e291a9bd446850728343f95945f49c16a111b1e1..6e36adc2a945f883b9348e4296cde8231771e02f 100644 (file)
@@ -89,14 +89,10 @@ static size_t pk_nmax = 128, pk_szmax = 1024 * 1024;
 /*----- Main code ---------------------------------------------------------*/
 
 static int nonblockify(int fd)
-{
-  return (fdflags(fd, O_NONBLOCK, O_NONBLOCK, 0, 0));
-}
+  { return (fdflags(fd, O_NONBLOCK, O_NONBLOCK, 0, 0)); }
 
 static int cloexec(int fd)
-{
-  return (fdflags(fd, 0, 0, FD_CLOEXEC, FD_CLOEXEC));
-}
+  { return (fdflags(fd, 0, 0, FD_CLOEXEC, FD_CLOEXEC)); }
 
 static void dolisten(void);
 
@@ -318,9 +314,7 @@ static void usage(FILE *fp)
 }
 
 static void version(FILE *fp)
-{
-  pquis(fp, "$, tripe version " VERSION "\n");
-}
+  { pquis(fp, "$, tripe version " VERSION "\n"); }
 
 static void help(FILE *fp)
 {
index 9aa29901dda5756946376f49bf3db5c738e2fdf5..107362eed6bfb1ca1488cd03e6ddd088316f903f 100644 (file)
@@ -603,14 +603,10 @@ static void parse(char *p)
 /*----- Main driver -------------------------------------------------------*/
 
 static void version(FILE *fp)
-{
-  pquis(fp, "$, TrIPE version " VERSION "\n");
-}
+  { pquis(fp, "$, TrIPE version " VERSION "\n"); }
 
 static void usage(FILE *fp)
-{
-  pquis(fp, "Usage: $ [-k KEYRING] DIRECTIVE...\n");
-}
+  { pquis(fp, "Usage: $ [-k KEYRING] DIRECTIVE...\n"); }
 
 static void help(FILE *fp)
 {
index b166b853dbdb3de411ad610993cc00f8ed1558f8..797b47869589b702813b2e48f71ba9c074064a3c 100644 (file)
@@ -62,8 +62,10 @@ static const trace_opt w_opts[] = {
 /*----- Static variables --------------------------------------------------*/
 
 static admin *admins;
+static admin *a_dead;
 static sel_file sock;
 static const char *sockname;
+static sym_table a_svcs;
 static unsigned flags = 0;
 static admin *a_stdin = 0;
 static sig s_term, s_int, s_hup;
@@ -75,8 +77,6 @@ static sig s_term, s_int, s_hup;
 #define T_PING SEC(5)
 
 static void a_destroy(admin */*a*/);
-static void a_lock(admin */*a*/);
-static void a_unlock(admin */*a*/);
 
 #define BOOL(x) ((x) ? "t" : "nil")
 
@@ -301,6 +301,9 @@ static void a_vformat(dstr *d, const char *fmt, va_list ap)
        base64_encode(&b64, p, n, d);
        base64_encode(&b64, 0, 0, d);
        while (d->len && d->buf[d->len - 1] == '=') d->len--;
+      } else if (strcmp(fmt, "?TOKENS") == 0) {
+       const char *const *av = va_arg(ap, const char *const *);
+       while (*av) quotify(d, *av++);
       } else if (strcmp(fmt, "?PEER") == 0)
        quotify(d, p_name(va_arg(ap, peer *)));
       else if (strcmp(fmt, "?ERRNO") == 0) {
@@ -338,6 +341,7 @@ static void a_vwrite(admin *a, const char *status, const char *tag,
                     const char *fmt, va_list ap)
 {
   dstr d = DSTR_INIT;
+
   if (tag) dstr_puts(&d, "BG");
   dstr_puts(&d, status);
   if (tag) quotify(&d, tag);
@@ -351,6 +355,7 @@ static void a_write(admin *a, const char *status, const char *tag,
                    const char *fmt, ...)
 {
   va_list ap;
+
   va_start(ap, fmt);
   a_vwrite(a, status, tag, fmt, ap);
   va_end(ap);
@@ -372,6 +377,7 @@ static void a_ok(admin *a) { a_write(a, "OK", 0, A_END); }
 static void a_info(admin *a, const char *fmt, ...)
 {
   va_list ap;
+
   va_start(ap, fmt);
   a_vwrite(a, "INFO", 0, fmt, ap);
   va_end(ap);
@@ -380,6 +386,7 @@ static void a_info(admin *a, const char *fmt, ...)
 static void a_fail(admin *a, const char *fmt, ...)
 {
   va_list ap;
+
   va_start(ap, fmt);
   a_vwrite(a, "FAIL", 0, fmt, ap);
   va_end(ap);
@@ -425,7 +432,7 @@ static void a_rawalert(unsigned f_and, unsigned f_eq, const char *status,
   dstr_destroy(&d);
 }
 
-static void a_valert(unsigned f_and, unsigned f_eq, const char *tag,
+static void a_valert(unsigned f_and, unsigned f_eq, const char *status,
                     const char *fmt, va_list ap)
 {
   dstr d = DSTR_INIT;
@@ -433,20 +440,19 @@ static void a_valert(unsigned f_and, unsigned f_eq, const char *tag,
   if (!(flags & F_INIT))
     return;
   a_vformat(&d, fmt, ap);
-  a_rawalert(f_and, f_eq, tag, fmt ? d.buf : 0, fmt ? d.len : 0);
+  a_rawalert(f_and, f_eq, status, fmt ? d.buf : 0, fmt ? d.len : 0);
   dstr_destroy(&d);
 }
 
-#if 0 /*unused*/
-static void a_alert(unsigned f_and, unsigned f_eq, const char *tag,
+static void a_alert(unsigned f_and, unsigned f_eq, const char *status,
                    const char *fmt, ...)
 {
   va_list ap;
+
   va_start(ap, fmt);
-  a_valert(f_and, f_eq, tag, fmt, ap);
+  a_valert(f_and, f_eq, status, fmt, ap);
   va_end(ap);
 }
-#endif
 
 /* --- @a_warn@ --- *
  *
@@ -570,9 +576,7 @@ static void a_sigdie(int sig, void *v)
  */
 
 static void a_sighup(int sig, void *v)
-{
-  a_warn("SERVER", "ignore", "signal", "SIGHUP", A_END);
-}
+  { a_warn("SERVER", "ignore", "signal", "SIGHUP", A_END); }
 
 /* --- @a_parsetime@ --- *
  *
@@ -621,6 +625,25 @@ static peer *a_findpeer(admin *a, const char *pn)
 #define BGTAG(bg)                                                      \
   (((admin_bgop *)(bg))->tag ? ((admin_bgop *)(bg))->tag : "<foreground>")
 
+/* --- @a_bgfind@ --- *
+ *
+ * Arguments:  @admin *a@ = a client block
+ *             @const char *tag@ = the requested tag
+ *
+ * Returns:    The requested background job, or null.
+ */
+
+static admin_bgop *a_bgfind(admin *a, const char *tag)
+{
+  admin_bgop *bg;
+
+  for (bg = a->bg; bg; bg = bg->next) {
+    if (bg->tag && strcmp(tag, bg->tag) == 0)
+      return (bg);
+  }
+  return (0);
+}
+
 /* --- @a_bgrelease@ --- *
  *
  * Arguments:  @admin_bgop *bg@ = backgrounded operation
@@ -642,8 +665,7 @@ static void a_bgrelease(admin_bgop *bg)
   if (bg->prev) bg->prev->next = bg->next;
   else a->bg = bg->next;
   xfree(bg);
-  if (a->f & AF_CLOSE) a_destroy(a);
-  a_unlock(a);
+  if (!a->bg && (a->f & AF_CLOSE)) a_destroy(a);
 }
 
 /* --- @a_bgok@, @a_bginfo@, @a_bgfail@ --- *
@@ -683,16 +705,21 @@ static void a_bgfail(admin_bgop *bg, const char *fmt, ...)
  *             @const char *tag@ = background tag, or null for foreground
  *             @void (*cancel)(admin_bgop *)@ = cancel function
  *
- * Returns:    ---
+ * Returns:    Zero for success, nonzero on failure.
  *
  * Use:                Links a background job into the list.
  */
 
-static void a_bgadd(admin *a, admin_bgop *bg, const char *tag,
-                   void (*cancel)(admin_bgop *))
+static int a_bgadd(admin *a, admin_bgop *bg, const char *tag,
+                  void (*cancel)(admin_bgop *))
 {
-  if (tag)
+  if (tag) {
+    if (a_bgfind(a, tag)) {
+      a_fail(a, "tag-exists", "%s", tag, A_END);
+      return (-1);
+    }
     bg->tag = xstrdup(tag);
+  }
   else {
     bg->tag = 0;
     selbuf_disable(&a->b);
@@ -703,9 +730,260 @@ static void a_bgadd(admin *a, admin_bgop *bg, const char *tag,
   bg->prev = 0;
   if (a->bg) a->bg->prev = bg;
   a->bg = bg;
-  a_lock(a);
   T( trace(T_ADMIN, "admin: add bgop %s", BGTAG(bg)); )
   if (tag) a_write(a, "DETACH", tag, A_END);
+  return (0);
+}
+
+/*----- Job table manipulation --------------------------------------------*/
+
+#define JOB_SHIFT 16
+#define JOB_INDEXMASK ((1ul << JOB_SHIFT) - 1)
+#define JOB_SEQMASK ((1ul << (32 - JOB_SHIFT)) - 1)
+
+#define JOB_END 0xfffffffful
+
+static unsigned long a_joboffset;
+
+/* --- @a_jobidencode@ --- *
+ *
+ * Arguments:  @admin_svcop *svc@ = pointer to a service operation
+ *
+ * Returns:    A jobid for this job, in an internal static buffer.
+ *
+ * Use:                Constructs a jobid.  In order to dissuade people from
+ *             predicting jobids, we obfuscate them.
+ *
+ *             A `raw' jobid consists of two 16-bit fields.  The low 16 bits
+ *             are an index into a big array.  The high 16 bits are a
+ *             sequence number recording how many times that slot has been
+ *             reused.
+ *
+ *             This `raw' jobid is then obfuscated by adding a randomly-
+ *             generated offset, and multiplying (mod %$2^{32}$%) by a fixed
+ *             odd constant.
+ */
+
+static const char *a_jobidencode(admin_svcop *svc)
+{
+  admin_jobtable *j = &svc->prov->j;
+  static char buf[10];
+  unsigned long pre;
+  unsigned seq;
+
+  assert(svc->index <= JOB_INDEXMASK);
+  seq = j->v[svc->index].seq;
+  assert(seq <= JOB_SEQMASK);
+  pre = (unsigned long)svc->index | ((unsigned long)seq << JOB_SHIFT);
+  sprintf(buf, "J%08lx", ((pre + a_joboffset) * 0x0f87a7a3ul) & 0xffffffff);
+  return (buf);
+}
+
+/* --- @a_jobiddecode@ --- *
+ *
+ * Arguments:  @admin_jobtable *j@ = pointer to a job table
+ *             @const char *jid@ = pointer to a jobid string
+ *
+ * Returns:    A pointer to the job's @svcop@ structure.
+ */
+
+static admin_svcop *a_jobiddecode(admin_jobtable *j, const char *jid)
+{
+  unsigned i;
+  unsigned long pre;
+
+  if (jid[0] != 'J')
+    return (0);
+  for (i = 1; i < 9; i++) {
+    if (!isxdigit((unsigned char)jid[i]))
+      return (0);
+  }
+  if (jid[9] != 0)
+    return (0);
+  pre = strtoul(jid + 1, 0, 16);
+  pre = ((pre * 0xbd11c40bul) - a_joboffset) & 0xffffffff;
+  i = pre & JOB_INDEXMASK;
+  if (i >= j->n || j->v[i].seq != (pre >> JOB_SHIFT))
+    return (0);
+  return (j->v[i].u.op);
+}
+
+/* --- @a_jobcreate@ --- *
+ *
+ * Arguments:  @admin *a@ = pointer to administration client
+ *
+ * Returns:    A pointer to a freshly-allocated @svcop@, or null.
+ *
+ * Use:                Allocates a fresh @svcop@ and links it into a job table.
+ */
+
+static admin_svcop *a_jobcreate(admin *a)
+{
+  admin_svcop *svc;
+  unsigned i;
+  unsigned sz;
+  admin_jobtable *j = &a->j;
+
+  if (j->free != JOB_END) {
+    i = j->free;
+    j->free = j->v[i].u.next;
+  } else {
+    if (j->n == j->sz) {
+      if (j->sz > JOB_INDEXMASK)
+       return (0);
+      sz = j->sz;
+      if (!sz) {
+       j->sz = 16;
+       j->v = xmalloc(j->sz * sizeof(*j->v));
+      } else {
+       j->sz = sz << 1;
+       j->v = xrealloc(j->v, j->sz * sizeof(*j->v), sz * sizeof(*j->v));
+      }
+    }
+    i = j->n++;
+    j->v[i].seq = 0;
+  }
+  svc = xmalloc(sizeof(*svc));
+  svc->index = i;
+  svc->prov = a;
+  svc->next = j->active;
+  svc->prev = 0;
+  if (j->active) j->active->prev = svc;
+  j->active = svc;
+  j->v[i].u.op = svc;
+  IF_TRACING(T_ADMIN, {
+    trace(T_ADMIN, "admin: created job %s (%u)", a_jobidencode(svc), i);
+  })
+  return (svc);
+}
+
+/* --- @a_jobdestroy@ --- *
+ *
+ * Arguments:  @admin_svcop *svc@ = pointer to job block
+ *
+ * Returns:    ---
+ *
+ * Use:                Frees up a completed (or cancelled) job.
+ */
+
+static void a_jobdestroy(admin_svcop *svc)
+{
+  admin *a = svc->prov;
+  admin_jobtable *j = &a->j;
+  unsigned i = svc->index;
+
+  IF_TRACING(T_ADMIN, {
+    trace(T_ADMIN, "admin: destroying job %s (%u)", a_jobidencode(svc), i);
+  })
+  assert(j->v[i].u.op = svc);
+  j->v[i].u.next = j->free;
+  j->v[i].seq++;
+  j->free = i;
+  if (svc->next) svc->next->prev = svc->prev;
+  if (svc->prev) svc->prev->next = svc->next;
+  else j->active = svc->next;
+}
+
+/* --- @a_jobtableinit@ --- *
+ *
+ * Arguments:  @admin_jobtable *j@ = pointer to job table
+ *
+ * Returns:    ---
+ *
+ * Use:                Initializes a job table.
+ */
+
+static void a_jobtableinit(admin_jobtable *j)
+{
+  if (!a_joboffset)
+    a_joboffset = GR_RANGE(&rand_global, 0xffffffff) + 1;
+  j->n = j->sz = 0;
+  j->active = 0;
+  j->free = JOB_END;
+  j->v = 0;
+}
+
+/* --- @a_jobtablefinal@ --- *
+ *
+ * Arguments:  @admin_jobtable *j@ = pointer to job table
+ *
+ * Returns:    ---
+ *
+ * Use:                Closes down a job table.
+ */
+
+static void a_jobtablefinal(admin_jobtable *j)
+{
+  admin_svcop *svc, *ssvc;
+
+  for (svc = j->active; svc; svc = ssvc) {
+    ssvc = svc->next;
+    a_bgfail(&svc->bg, "provider-failed", A_END);
+    a_bgrelease(&svc->bg);
+  }
+  if (j->v) xfree(j->v);
+}
+
+/*----- Services infrastructure -------------------------------------------*/
+
+/* --- @a_svcfind@ --- *
+ *
+ * Arguments:  @admin *a@ = the requesting client
+ *             @const char *name@ = service name wanted
+ *
+ * Returns:    The service requested, or null.
+ *
+ * Use:                Finds a service; reports an error if the service couldn't be
+ *             found.
+ */
+
+static admin_service *a_svcfind(admin *a, const char *name)
+{
+  admin_service *svc;
+
+  if ((svc = sym_find(&a_svcs, name, -1, 0, 0)) == 0) {
+    a_fail(a, "unknown-service", "%s", name, A_END);
+    return (0);
+  }
+  return (svc);
+}
+
+/* --- @a_svcunlink@ --- *
+ *
+ * Arguments:  @admin_service *svc@ = pointer to service structure
+ *
+ * Returns:    ---
+ *
+ * Use:                Unlinks the service from its provider, without issuing a
+ *             message or freeing the structure.  The version string is
+ *             freed, however.
+ */
+
+static void a_svcunlink(admin_service *svc)
+{
+  if (svc->next)
+    svc->next->prev = svc->prev;
+  if (svc->prev)
+    svc->prev->next = svc->next;
+  else
+    svc->prov->svcs = svc->next;
+  xfree(svc->version);
+}
+
+/* --- @a_svcrelease@ --- *
+ *
+ * Arguments:  @admin_service *svc@ = pointer to service structure
+ *
+ * Returns:    ---
+ *
+ * Use:                Releases a service and frees its structure.
+ */
+
+static void a_svcrelease(admin_service *svc)
+{
+  a_notify("SVCRELEASE", "%s", SYM_NAME(svc), A_END);
+  a_svcunlink(svc);
+  sym_remove(&a_svcs, svc);
 }
 
 /*----- Name resolution operations ----------------------------------------*/
@@ -837,7 +1115,8 @@ static void a_resolve(admin *a, admin_resop *r, const char *tag,
    * answer straight away.
    */
 
-  a_bgadd(a, &r->bg, tag, a_rescancel);
+  if (a_bgadd(a, &r->bg, tag, a_rescancel))
+    goto fail;
   T( trace(T_ADMIN, "admin: %u, resop %s, hostname `%s'",
           a->seq, BGTAG(r), r->addr); )
 
@@ -865,6 +1144,44 @@ fail:
   xfree(r);
 }
 
+/*----- Option parsing ----------------------------------------------------*/
+
+#define OPTIONS(argc, argv, guts) do {                                 \
+  char **o_av = argv;                                                  \
+  for (;; o_av++) {                                                    \
+    if (!*o_av)                                                                \
+      break;                                                           \
+    if (mystrieq(*o_av, "--")) {                                       \
+      o_av++;                                                          \
+      break;                                                           \
+    }                                                                  \
+    guts                                                               \
+    if (**o_av == '-')                                                 \
+      goto bad_syntax;                                                 \
+    break;                                                             \
+  }                                                                    \
+  argc -= o_av - argv;                                                 \
+  argv = o_av;                                                         \
+} while (0)
+
+#define OPT(name, guts) if (mystrieq(*o_av, name)) { guts continue; }
+
+#define OPTARG(name, arg, guts) OPT(name, {                            \
+  const char *arg;                                                     \
+  arg = *++o_av;                                                       \
+  if (!arg) goto bad_syntax;                                           \
+  guts                                                                 \
+})
+
+#define OPTTIME(name, arg, guts) OPTARG(name, o_arg, {                 \
+  long arg;                                                            \
+  if ((arg = a_parsetime(o_arg)) < 0) {                                        \
+    a_fail(a, "bad-time-spec", "%s", o_arg, A_END);                    \
+    goto fail;                                                         \
+  }                                                                    \
+  guts                                                                 \
+})
+
 /*----- Adding peers ------------------------------------------------------*/
 
 /* --- @a_doadd@ --- *
@@ -910,72 +1227,58 @@ static void a_doadd(admin_resop *r, int rc)
 
 static void acmd_add(admin *a, unsigned ac, char *av[])
 {
-  unsigned i, j;
   const char *tag = 0;
-  admin_addop *add = 0;
+  admin_addop *add;
 
   /* --- Set stuff up --- */
 
   add = xmalloc(sizeof(*add));
-  add->peer.name = xstrdup(av[0]);
+  add->peer.name = 0;
   add->peer.t_ka = 0;
   add->peer.tops = tun_default;
 
-  /* --- Make sure someone's not got there already --- */
-
-  if (p_find(av[0])) {
-    a_fail(a, "peer-exists", "%s", av[0], A_END);
-    goto fail;
-  }
-
   /* --- Parse options --- */
 
-  i = 1;
-  for (;;) {
-    if (!av[i])
-      goto bad_syntax;
-    if (mystrieq(av[i], "-background")) {
-      if (!av[++i]) goto bad_syntax;
-      tag = av[i];
-    } else if (mystrieq(av[i], "-tunnel")) {
-      if (!av[++i]) goto bad_syntax;
-      for (j = 0;; j++) {
-       if (!tunnels[j]) {
-         a_fail(a, "unknown-tunnel", "%s", av[i], A_END);
+  OPTIONS(ac, av, {
+    OPTARG("-background", arg, { tag = arg; })
+    OPTARG("-tunnel", arg, {
+      unsigned i;
+      for (i = 0;; i++) {
+       if (!tunnels[i]) {
+         a_fail(a, "unknown-tunnel", "%s", arg, A_END);
          goto fail;
        }
-       if (mystrieq(av[i], tunnels[j]->name)) {
-         add->peer.tops = tunnels[j];
+       if (mystrieq(arg, tunnels[i]->name)) {
+         add->peer.tops = tunnels[i];
          break;
        }
       }
-    } else if (mystrieq(av[i], "-keepalive")) {
-      long t;
-      if (!av[++i]) goto bad_syntax;
-      if ((t = a_parsetime(av[i])) < 0) {
-       a_fail(a, "bad-time-spec", "%s", av[i], A_END);
-       goto fail;
-      }
-      add->peer.t_ka = t;
-    } else if (mystrieq(av[i], "--")) {
-      i++;
-      break;
-    } else
-      break;
-    i++;
+    })
+    OPTTIME("-keepalive", t, { add->peer.t_ka = t; })
+  });
+
+  /* --- Make sure someone's not got there already --- */
+
+  if (!*av)
+    goto bad_syntax;
+  if (p_find(*av)) {
+    a_fail(a, "peer-exists", "%s", *av, A_END);
+    goto fail;
   }
+  add->peer.name = xstrdup(*av++);
+  ac--;
 
   /* --- Crank up the resolver --- */
 
-  a_resolve(a, &add->r, tag, a_doadd, ac - i, av + i);
+  a_resolve(a, &add->r, tag, a_doadd, ac, av);
   return;
 
   /* --- Clearing up --- */
 
 bad_syntax:
-  a_fail(a, "bad-syntax", "add", "PEER [OPTIONS] ADDR ...", A_END);
+  a_fail(a, "bad-syntax", "add", "[OPTIONS] PEER ADDR ...", A_END);
 fail:
-  xfree(add->peer.name);
+  if (add->peer.name) xfree(add->peer.name);
   xfree(add);
   return;
 }
@@ -1052,38 +1355,21 @@ static void a_ping(admin *a, unsigned ac, char *av[],
                   const char *cmd, unsigned msg)
 {
   long t = T_PING;
-  int i;
   peer *p;
   admin_pingop *pg = 0;
   const char *tag = 0;
 
-  i = 0;
-  for (;;) {
-    if (!av[i])
-      goto bad_syntax;
-    if (mystrieq(av[i], "-background")) {
-      if (!av[++i]) goto bad_syntax;
-      tag = av[i];
-    } else if (mystrieq(av[i], "-timeout")) {
-      if (!av[++i]) goto bad_syntax;
-      if ((t = a_parsetime(av[i])) < 0) {
-       a_fail(a, "bad-time-spec", "%s", av[i], A_END);
-       return;
-      }
-    } else if (mystrieq(av[i], "--")) {
-      i++;
-      break;
-    } else
-      break;
-    i++;
-  }
-
-  if (!av[i]) goto bad_syntax;
-  if ((p = a_findpeer(a, av[i])) == 0)
+  OPTIONS(ac, av, {
+    OPTARG("-background", arg, { tag = arg; })
+    OPTTIME("-timeout", arg, { t = arg; })
+  });
+  if (!*av || av[1]) goto bad_syntax;
+  if ((p = a_findpeer(a, *av)) == 0)
     return;
   pg = xmalloc(sizeof(*pg));
   gettimeofday(&pg->pingtime, 0);
-  a_bgadd(a, &pg->bg, tag, a_pingcancel);
+  if (a_bgadd(a, &pg->bg, tag, a_pingcancel))
+    goto fail;
   T( trace(T_ADMIN, "admin: ping op %s: %s to %s",
           BGTAG(pg), cmd, p_name(p)); )
   if (p_pingsend(p, &pg->ping, msg, t, a_pong, pg)) {
@@ -1094,6 +1380,8 @@ static void a_ping(admin *a, unsigned ac, char *av[],
     
 bad_syntax:
   a_fail(a, "bad-syntax", "%s", cmd, "[OPTIONS] PEER", cmd, A_END);
+fail:
+  if (pg) xfree(pg);
   return;
 }
 
@@ -1102,6 +1390,160 @@ static void acmd_ping(admin *a, unsigned ac, char *av[])
 static void acmd_eping(admin *a, unsigned ac, char *av[])
   { a_ping(a, ac, av, "eping", MISC_EPING); }
 
+/*----- Service commands --------------------------------------------------*/
+
+static void acmd_svcclaim(admin *a, unsigned ac, char *av[])
+{
+  admin_service *svc;
+  unsigned f;
+
+  svc = sym_find(&a_svcs, av[0], -1, sizeof(*svc), &f);
+  if (f) {
+    if (versioncmp(av[1], svc->version) <= 0) {
+      a_fail(a,
+            "service-exists",
+            "%s", SYM_NAME(svc),
+            "%s", svc->version,
+            A_END);
+      return;
+    }
+    a_write(svc->prov, "SVCCLAIM", 0, "%s", av[0], "%s", av[1], A_END);
+    a_svcunlink(svc);
+  }
+  svc->prov = a;
+  svc->version = xstrdup(av[1]);
+  svc->next = a->svcs;
+  svc->prev = 0;
+  if (a->svcs) a->svcs->prev = svc;
+  a->svcs = svc;
+  a_notify("SVCCLAIM", "%s", SYM_NAME(svc), "%s", svc->version, A_END);
+  a_ok(a);
+}
+
+static void acmd_svcrelease(admin *a, unsigned ac, char *av[])
+{
+  admin_service *svc;
+
+  if ((svc = a_svcfind(a, av[0])) == 0)
+    return;
+  if (svc->prov != a) {
+    a_fail(a, "not-service-provider", "%s", SYM_NAME(svc), A_END);
+    return;
+  }
+  a_svcrelease(svc);
+  a_ok(a);
+}
+
+static void acmd_svcensure(admin *a, unsigned ac, char *av[])
+{
+  admin_service *svc;
+
+  if ((svc = a_svcfind(a, av[0])) == 0)
+    return;
+  if (av[1] && versioncmp(svc->version, av[1]) < 0) {
+    a_fail(a, "service-too-old",
+          "%s", SYM_NAME(svc),
+          "%s", svc->version,
+          A_END);
+    return;
+  }
+  a_ok(a);
+}
+
+static void acmd_svcquery(admin *a, unsigned ac, char *av[])
+{
+  admin_service *svc;
+
+  if ((svc = a_svcfind(a, av[0])) != 0) {
+    a_info(a, "name=%s", SYM_NAME(svc), "version=%s", svc->version, A_END);
+    a_ok(a);
+  }
+}
+
+static void acmd_svclist(admin *a, unsigned ac, char *av[])
+{
+  admin_service *svc;
+  sym_iter i;
+
+  for (sym_mkiter(&i, &a_svcs); (svc = sym_next(&i)) != 0; )
+    a_info(a, "%s", SYM_NAME(svc), "%s", svc->version, A_END);
+  a_ok(a);
+}
+
+static void a_canceljob(admin_bgop *bg)
+{
+  admin_svcop *svc = (admin_svcop *)bg;
+
+  a_write(svc->prov, "SVCCANCEL", 0, "%s", a_jobidencode(svc), A_END);
+  a_jobdestroy(svc);
+}
+
+static void acmd_svcsubmit(admin *a, unsigned ac, char *av[])
+{
+  const char *tag = 0;
+  admin_service *svc;
+  admin_svcop *svcop;
+  const char *ver = 0;
+  dstr d = DSTR_INIT;
+
+  OPTIONS(ac, av, {
+    OPTARG("-background", arg, { tag = arg; })
+    OPTARG("-version", arg, { ver = arg; })
+  });
+  if (!*av) goto bad_syntax;
+  if ((svc = a_svcfind(a, *av)) == 0) goto fail;
+  if (ver && versioncmp(svc->version, ver) < 0) {
+    a_fail(a, "service-too-old",
+          "%s", SYM_NAME(svc),
+          "%s", svc->version,
+          A_END);
+    goto fail;
+  }
+  if ((svcop = a_jobcreate(svc->prov)) == 0) {
+    a_fail(a, "provider-overloaded", A_END);
+    goto fail;
+  }
+  if (a_bgadd(a, &svcop->bg, tag, a_canceljob)) {
+    a_jobdestroy(svcop);
+    goto fail;
+  }
+  a_write(svc->prov, "SVCJOB", 0,
+         "%s", a_jobidencode(svcop),
+         "?TOKENS", av,
+         A_END);
+  goto done;
+
+bad_syntax:
+  a_fail(a, "bad-syntax", "svcsubmit", "[OPTIONS] SERVICE ARGS...", A_END);
+fail:
+done:
+  dstr_destroy(&d);
+  return;
+}
+
+static void a_replycmd(admin *a, const char *status, int donep, char *av[])
+{
+  admin_svcop *svc;
+
+  if ((svc = a_jobiddecode(&a->j, av[0])) == 0) {
+    a_fail(a, "unknown-jobid", av[0], A_END);
+    return;
+  }
+  a_write(svc->bg.a, status, svc->bg.tag, "?TOKENS", av + 1, A_END);
+  if (donep) {
+    a_jobdestroy(svc);
+    a_bgrelease(&svc->bg);
+  }
+  a_ok(a);
+}
+
+static void acmd_svcok(admin *a, unsigned ac, char *av[])
+  { a_replycmd(a, "OK", 1, av); }
+static void acmd_svcinfo(admin *a, unsigned ac, char *av[])
+  { a_replycmd(a, "INFO", 0, av); }
+static void acmd_svcfail(admin *a, unsigned ac, char *av[])
+  { a_replycmd(a, "FAIL", 1, av); }
+
 /*----- Administration commands -------------------------------------------*/
 
 /* --- Miscellaneous commands --- */
@@ -1174,35 +1616,18 @@ static void acmd_trace(admin *a, unsigned ac, char *av[])
 #endif
 
 static void acmd_watch(admin *a, unsigned ac, char *av[])
-{
-  traceish(a, ac, av, "watch", w_opts, &a->f);
-}
+  { traceish(a, ac, av, "watch", w_opts, &a->f); }
 
 static void alertcmd(admin *a, unsigned f_and, unsigned f_eq,
-                    const char *tag, unsigned ac, char *av[])
-{
-  dstr d = DSTR_INIT;
-  unsigned i;
-
-  dstr_puts(&d, "USER");
-  for (i = 0; i < ac; i++)
-    quotify(&d, av[i]);
-  dstr_putz(&d);
-  a_rawalert(f_and, f_eq, tag, d.buf, d.len);
-  dstr_destroy(&d);
-  a_ok(a);
-}
-
+                    const char *status, char *av[])
+  { a_alert(f_and, f_eq, status, "USER", "?TOKENS", av, A_END); a_ok(a); }
 static void acmd_notify(admin *a, unsigned ac, char *av[])
-  { alertcmd(a, AF_NOTE, AF_NOTE, "NOTE", ac, av); }
+  { alertcmd(a, AF_NOTE, AF_NOTE, "NOTE", av); }
 static void acmd_warn(admin *a, unsigned ac, char *av[])
-  { alertcmd(a, AF_WARN, AF_WARN, "WARN", ac, av); }
+  { alertcmd(a, AF_WARN, AF_WARN, "WARN", av); }
 
 static void acmd_port(admin *a, unsigned ac, char *av[])
-{
-  a_info(a, "%u", p_port(), A_END);
-  a_ok(a);
-}
+  { a_info(a, "%u", p_port(), A_END); a_ok(a); }
 
 static void acmd_daemon(admin *a, unsigned ac, char *av[])
 {
@@ -1221,9 +1646,34 @@ static void acmd_daemon(admin *a, unsigned ac, char *av[])
   }
 }
 
+static void acmd_jobs(admin *a, unsigned ac, char *av[])
+{
+  admin_bgop *bg;
+
+  for (bg = a->bg; bg; bg = bg->next) {
+    assert(bg->tag);
+    a_info(a, "%s", bg->tag, A_END);
+  }
+  a_ok(a);
+}
+
+static void acmd_bgcancel(admin *a, unsigned ac, char *av[])
+{
+  admin_bgop *bg;
+
+  if ((bg = a_bgfind(a, av[0])) == 0)
+    a_fail(a, "unknown-tag", "%s", av[0], A_END);
+  else {
+    bg->cancel(bg);
+    a_bgrelease(bg);
+    a_ok(a);
+  }
+}
+
 static void acmd_list(admin *a, unsigned ac, char *av[])
 {
   peer *p;
+
   for (p = p_first(); p; p = p_next(p))
     a_info(a, "%s", p_name(p), A_END);
   a_ok(a);
@@ -1339,17 +1789,27 @@ static void acmd_stats(admin *a, unsigned ac, char *av[])
   a_info(a, "start-time=%s", timestr(st->t_start), A_END);
   a_info(a, "last-packet-time=%s", timestr(st->t_last), A_END);
   a_info(a, "last-keyexch-time=%s", timestr(st->t_kx), A_END);
-  a_info(a, "packets-in=%lu bytes-in=%lu", st->n_in, st->sz_in, A_END);
-  a_info(a, "packets-out=%lu bytes-out=%lu",
-        st->n_out, st->sz_out, A_END);
-  a_info(a, "keyexch-packets-in=%lu keyexch-bytes-in=%lu",
-        st->n_kxin, st->sz_kxin, A_END);
-  a_info(a, "keyexch-packets-out=%lu keyexch-bytes-out=%lu",
-        st->n_kxout, st->sz_kxout, A_END);
-  a_info(a, "ip-packets-in=%lu ip-bytes-in=%lu",
-        st->n_ipin, st->sz_ipin, A_END);
-  a_info(a, "ip-packets-out=%lu ip-bytes-out=%lu",
-        st->n_ipout, st->sz_ipout, A_END);
+  a_info(a, "packets-in=%lu", st->n_in, "bytes-in=%lu", st->sz_in, A_END);
+  a_info(a,
+        "packets-out=%lu", st->n_out,
+        "bytes-out=%lu", st->sz_out,
+        A_END);
+  a_info(a,
+        "keyexch-packets-in=%lu", st->n_kxin,
+        "keyexch-bytes-in=%lu", st->sz_kxin,
+        A_END);
+  a_info(a,
+        "keyexch-packets-out=%lu", st->n_kxout,
+        "keyexch-bytes-out=%lu", st->sz_kxout,
+        A_END);
+  a_info(a,
+        "ip-packets-in=%lu", st->n_ipin,
+        "ip-bytes-in=%lu", st->sz_ipin,
+        A_END);
+  a_info(a,
+        "ip-packets-out=%lu", st->n_ipout, 
+        "ip-bytes-out=%lu", st->sz_ipout,
+        A_END);
   a_info(a, "rejected-packets=%lu", st->n_reject, A_END);
   a_ok(a);
 }
@@ -1357,6 +1817,7 @@ static void acmd_stats(admin *a, unsigned ac, char *av[])
 static void acmd_kill(admin *a, unsigned ac, char *av[])
 {
   peer *p;
+
   if ((p = a_findpeer(a, av[0])) != 0) {
     p_destroy(p);
     a_ok(a);
@@ -1366,6 +1827,7 @@ static void acmd_kill(admin *a, unsigned ac, char *av[])
 static void acmd_forcekx(admin *a, unsigned ac, char *av[])
 {
   peer *p;
+
   if ((p = a_findpeer(a, av[0])) != 0) {
     kx_start(&p->kx, 1);
     a_ok(a);
@@ -1379,7 +1841,6 @@ static void acmd_quit(admin *a, unsigned ac, char *av[])
 {
   a_warn("SERVER", "quit", "admin-request", A_END);
   a_ok(a);
-  a_unlock(a);
   a_quit();
 }
 
@@ -1392,6 +1853,7 @@ static void acmd_version(admin *a, unsigned ac, char *av[])
 static void acmd_tunnels(admin *a, unsigned ac, char *av[])
 {
   int i;
+
   for (i = 0; tunnels[i]; i++)
     a_info(a, "%s", tunnels[i]->name, A_END);
   a_ok(a);
@@ -1409,8 +1871,9 @@ typedef struct acmd {
 static void acmd_help(admin */*a*/, unsigned /*ac*/, char */*av*/[]);
 
 static const acmd acmdtab[] = {
-  { "add",     "PEER [OPTIONS] ADDR ...", 2,   0xffff, acmd_add },
+  { "add",     "[OPTIONS] PEER ADDR ...", 2,   0xffff, acmd_add },
   { "addr",    "PEER",                 1,      1,      acmd_addr },
+  { "bgcancel",        "TAG",                  1,      1,      acmd_bgcancel },
   { "checkchal", "CHAL",               1,      1,      acmd_checkchal },
   { "daemon",  0,                      0,      0,      acmd_daemon },
   { "eping",   "[OPTIONS] PEER",       1,      0xffff, acmd_eping },
@@ -1419,6 +1882,7 @@ static const acmd acmdtab[] = {
   { "greet",   "PEER CHAL",            2,      2,      acmd_greet },
   { "help",    0,                      0,      0,      acmd_help },
   { "ifname",  "PEER",                 1,      1,      acmd_ifname },
+  { "jobs",    0,                      0,      0,      acmd_jobs },
   { "kill",    "PEER",                 1,      1,      acmd_kill },
   { "list",    0,                      0,      0,      acmd_list },
   { "notify",  "MESSAGE ...",          1,      0xffff, acmd_notify },
@@ -1429,6 +1893,16 @@ static const acmd acmdtab[] = {
   { "reload",  0,                      0,      0,      acmd_reload },
   { "servinfo",        0,                      0,      0,      acmd_servinfo },
   { "setifname", "PEER NEW-NAME",      2,      2,      acmd_setifname },
+  { "svcclaim",        "SERVICE VERSION",      2,      2,      acmd_svcclaim },
+  { "svcensure", "SERVICE [VERSION]",  1,      2,      acmd_svcensure },
+  { "svcfail", "JOBID TOKENS...",      1,      0xffff, acmd_svcfail },
+  { "svcinfo", "JOBID TOKENS...",      1,      0xffff, acmd_svcinfo },
+  { "svclist", 0,                      0,      0,      acmd_svclist },
+  { "svcok",   "JOBID",                1,      1,      acmd_svcok },
+  { "svcquery",        "SERVICE",              1,      1,      acmd_svcquery },
+  { "svcrelease", "SERVICE",           1,      1,      acmd_svcrelease },
+  { "svcsubmit", "[OPTIONS] SERVICE TOKENS...",
+                                       2,      0xffff, acmd_svcsubmit },
   { "stats",   "PEER",                 1,      1,      acmd_stats },
 #ifndef NTRACE
   { "trace",   "[OPTIONS]",            0,      1,      acmd_trace },
@@ -1443,6 +1917,7 @@ static const acmd acmdtab[] = {
 static void acmd_help(admin *a, unsigned ac, char *av[])
 {
   const acmd *c;
+
   for (c = acmdtab; c->name; c++) {
     if (c->help)
       a_info(a, "%s", c->name, "*%s", c->help, A_END);
@@ -1454,70 +1929,64 @@ static void acmd_help(admin *a, unsigned ac, char *av[])
 
 /*----- Connection handling -----------------------------------------------*/
 
-/* --- @a_lock@ --- *
+/* --- @a_destroypending@ --- *
  *
- * Arguments:  @admin *a@ = pointer to an admin block
- *
- * Returns:    ---
- *
- * Use:                Locks an admin block so that it won't be destroyed
- *             immediately.
- */
-
-static void a_lock(admin *a) { a->ref++; }
-
-/* --- @a_dodestroy@ --- *
- *
- * Arguments:  @admin *a@ = pointer to an admin block
+ * Arguments:  ---
  *
  * Returns:    ---
  *
- * Use:                Actually does the legwork of destroying an admin block.
+ * Use:                Destroys pending admin connections at a safe time.
  */
 
-static void a_dodestroy(admin *a)
+static void a_destroypending(void)
 {
+  admin *a, *aa;
   admin_bgop *bg, *bbg;
+  admin_service *svc, *ssvc;
 
-  T( trace(T_ADMIN, "admin: completing destruction of connection %u",
-          a->seq); )
+  /* --- Destroy connections marked as pending --- */
 
-  selbuf_destroy(&a->b);
-  for (bg = a->bg; bg; bg = bbg) {
-    bbg = bg->next;
-    bg->cancel(bg);
-    if (bg->tag) xfree(bg->tag);
-    xfree(bg);
+  for (a = a_dead; a; a = aa) {
+    aa = a->next;
+    assert(a->f & AF_DEAD);
+
+    /* --- Report what we're doing --- */
+
+    T( trace(T_ADMIN, "admin: completing destruction of connection %u",
+            a->seq); )
+
+    /* --- Abort any background jobs in progress --- */
+
+    for (bg = a->bg; bg; bg = bbg) {
+      bbg = bg->next;
+      bg->cancel(bg);
+      if (bg->tag) xfree(bg->tag);
+      xfree(bg);
+    }
+
+    /* --- Release services I hold, and abort pending jobs --- */
+
+    for (svc = a->svcs; svc; svc = ssvc) {
+      ssvc = svc->next;
+      a_svcrelease(svc);
+    }
+    a_jobtablefinal(&a->j);
+
+    /* --- Close file descriptors and selectory --- */
+
+    selbuf_destroy(&a->b);
+    if (a->b.reader.fd != a->w.fd) close(a->b.reader.fd);
+    close(a->w.fd);
+    if (a_stdin == a) a_stdin = 0;
+
+    /* --- Done --- */
+
+    DESTROY(a);
   }
-  if (a->b.reader.fd != a->w.fd) close(a->b.reader.fd);
-  close(a->w.fd);
-
-  if (a_stdin == a)
-    a_stdin = 0;
-  if (a->next)
-    a->next->prev = a->prev;
-  if (a->prev)
-    a->prev->next = a->next;
-  else
-    admins = a->next;
-  DESTROY(a);
-}
 
-/* --- @a_unlock@ --- *
- *
- * Arguments:  @admin *a@ = pointer to an admin block
- *
- * Returns:    ---
- *
- * Use:                Unlocks an admin block, allowing its destruction.  This is
- *             also the second half of @a_destroy@.
- */
+  /* --- All pending destruction completed --- */
 
-static void a_unlock(admin *a)
-{
-  assert(a->ref);
-  if (!--a->ref && (a->f & AF_DEAD))
-    a_dodestroy(a);
+  a_dead = 0;
 }
 
 /* --- @a_destroy@ --- *
@@ -1543,28 +2012,21 @@ static void freequeue(oqueue *q)
 
 static void a_destroy(admin *a)
 {
-  /* --- Don't multiply destroy admin blocks --- */
-
   if (a->f & AF_DEAD)
     return;
 
-  /* --- Make sure nobody expects it to work --- */
-
-  a->f |= AF_DEAD;
-  T( trace(T_ADMIN, "admin: destroying connection %u", a->seq); )
-
-  /* --- Free the output buffers --- */
+  if (a->next) a->next->prev = a->prev;
+  if (a->prev) a->prev->next = a->next;
+  else admins = a->next;
 
-  if (a->out.hd)
-    sel_rmfile(&a->w);
+  if (a->out.hd) sel_rmfile(&a->w);
   freequeue(&a->out);
 
-  /* --- If the block is locked, that's all we can manage --- */
+  a->f |= AF_DEAD;
+  a->next = a_dead;
+  a_dead = a;
 
-  if (!a->ref)
-    a_dodestroy(a);
-  T( else 
-     trace(T_ADMIN, "admin: deferring destruction..."); )
+  T( trace(T_ADMIN, "admin: killing connection %u", a->seq); )
 }
 
 /* --- @a_line@ --- *
@@ -1608,11 +2070,8 @@ static void a_line(char *p, size_t len, void *vp)
          a_fail(a, "bad-syntax", "%s", c->name, "", A_END);
        else 
          a_fail(a, "bad-syntax", "%s", c->name, "%s", c->help, A_END);
-      } else {
-       a_lock(a);
+      } else
        c->func(a, ac, av + 1);
-       a_unlock(a);
-      }
       return;
     }
   }
@@ -1638,6 +2097,8 @@ void a_create(int fd_in, int fd_out, unsigned f)
   T( trace(T_ADMIN, "admin: accepted connection %u", a->seq); )
   a->bg = 0;
   a->ref = 0;
+  a->svcs = 0;
+  a_jobtableinit(&a->j);
   a->f = f;
   if (fd_in == STDIN_FILENO) a_stdin = a;
   fdflags(fd_in, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC);
@@ -1678,6 +2139,19 @@ static void a_accept(int fd, unsigned mode, void *v)
   a_create(nfd, nfd, 0);
 }
 
+/* --- @a_preselect@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    ---
+ *
+ * Use:                Informs the admin module that we're about to select again,
+ *             and that it should do cleanup things it has delayed until a
+ *             `safe' time.
+ */
+
+void a_preselect(void) { if (a_dead) a_destroypending(); }
+
 /* --- @a_daemon@ --- *
  *
  * Arguments:  ---
@@ -1687,10 +2161,7 @@ static void a_accept(int fd, unsigned mode, void *v)
  * Use:                Informs the admin module that it's a daemon.
  */
 
-void a_daemon(void)
-{
-  flags |= F_DAEMON;
-}
+void a_daemon(void) { flags |= F_DAEMON; }
 
 /* --- @a_init@ --- *
  *
@@ -1709,6 +2180,10 @@ void a_init(const char *name)
   struct sigaction sa;
   size_t sz;
 
+  /* --- Create services table --- */
+
+  sym_create(&a_svcs);
+
   /* --- Set up the socket address --- */
 
   sz = strlen(name) + 1;
index 92a5f775f03b456a0068317508db70294bedcb77..77cfd2c6c7fceba19c55c14ef60b380985be9409 100644 (file)
@@ -140,4 +140,157 @@ int seq_check(seqwin *s, uint32 q, const char *service)
   return (0);
 }
 
+/* --- @versioncmp@ --- *
+ *
+ * Arguments:  @const char *va, *vb@ = two version strings
+ *
+ * Returns:    Less than, equal to, or greater than zero, according to
+ *             whether @va@ is less than, equal to, or greater than @vb@.
+ *
+ * Use:                Compares version number strings.
+ *
+ *             The algorithm is an extension of the Debian version
+ *             comparison algorithm.  A version number consists of three
+ *             components:
+ *
+ *               [EPOCH :] MAIN [- SUB]
+ *
+ *             The MAIN part may contain colons or hyphens if there is an
+ *             EPOCH or SUB, respectively.  Version strings are compared
+ *             componentwise: first epochs, then main parts, and finally
+ *             subparts.
+ *
+ *             The component comparison is done as follows.  First, the
+ *             initial subsequence of nondigit characters is extracted from
+ *             each string, and these are compared lexicographically, using
+ *             ASCII ordering, except that letters precede non-letters.  If
+ *             both are the same, an initial sequence of digits is extracted
+ *             from the remaining parts of the version strings, and these
+ *             are compared numerically (an empty sequence being considered
+ *             to have the value zero).  This process is repeated until we
+ *             have a winner or until both strings are exhausted.
+ */
+
+struct vinfo {
+  const char *e, *el;
+  const char *m, *ml;
+  const char *s, *sl;
+};
+
+static int vint(const char **vv, const char *vl)
+{
+  int n = 0;
+  const char *v = *vv;
+  int ch;
+
+  while (v < vl) {
+    ch = *v;
+    if (!isdigit((unsigned char)ch))
+      break;
+    v++;
+    n = n * 10 + (ch - '0');
+  }
+  *vv = v;
+  return (n);
+}
+
+static const char *vchr(const char **vv, const char *vl)
+{
+  const char *v = *vv;
+  const char *b = v;
+  int ch;
+
+  while (v < vl) {
+    ch = *v;
+    if (isdigit((unsigned char)ch))
+      break;
+    v++;
+  }
+  *vv = v;
+  return (b);
+}
+
+#define CMP(x, y) ((x) < (y) ? -1 : +1)
+
+static int vcmp(const char *va, const char *val,
+               const char *vb, const char *vbl)
+{
+  const char *pa, *pb;
+  int ia, ib;
+
+  for (;;) {
+
+    /* --- See if we're done --- */
+
+    if (va == val && vb == vbl)
+      return (0);
+
+    /* --- Compare nondigit portions --- */
+
+    pa = vchr(&va, val); pb = vchr(&vb, vbl);
+    for (;;) {
+      if (pa == va && pb == vb)
+       break;
+      else if (pa == va)
+       return (-1);
+      else if (pb == vb)
+       return (+1);
+      else if (*pa == *pb) {
+       pa++; pb++;
+       continue;
+      } else if (isalpha((unsigned char)*pa) == isalpha((unsigned char)*pb))
+       return (CMP(*pa, *pb));
+      else if (isalpha((unsigned char)*pa))
+       return (-1);
+      else
+       return (+1);
+    }
+
+    /* --- Compare digit portions --- */
+
+    ia = vint(&va, val); ib = vint(&vb, vbl);
+    if (ia != ib)
+      return (CMP(ia, ib));
+  }
+}
+
+static void vsplit(const char *v, struct vinfo *vi)
+{
+  const char *p;
+  size_t n;
+
+  if ((p = strchr(v, ':')) == 0)
+    vi->e = vi->el = 0;
+  else {
+    vi->e = v;
+    vi->el = p;
+    v = p + 1;
+  }
+
+  n = strlen(v);
+  if ((p = strrchr(v, '-')) == 0)
+    vi->s = vi->sl = 0;
+  else {    
+    vi->s = p + 1;
+    vi->sl = v + n;
+    n = p - v;
+  }
+
+  vi->m = v;
+  vi->ml = v + n;
+}
+
+int versioncmp(const char *va, const char *vb)
+{
+  struct vinfo via, vib;
+  int rc;
+
+  vsplit(va, &via); vsplit(vb, &vib);
+  if ((rc = vcmp(via.e, via.el, vib.e, vib.el)) != 0 ||
+      (rc = vcmp(via.m, via.ml, vib.m, vib.ml)) != 0 ||
+      (rc = vcmp(via.s, via.sl, vib.s, vib.sl)) != 0)
+    return (rc);
+  return (0);
+}
+
 /*----- That's all, folks -------------------------------------------------*/
index be37617a1e57204778fc93f87e2c11cd63fba284..b5bd21c4adcaffbd47bfd1431a43d96f2f51637c 100644 (file)
@@ -96,10 +96,7 @@ static void usage(FILE *fp)
        [-k PRIV-KEYRING] [-K PUB-KEYRING] [-t KEY-TAG]\n");
 }
 
-static void version(FILE *fp)
-{
-  pquis(fp, "$, version " VERSION "\n");
-}
+static void version(FILE *fp) { pquis(fp, "$, version " VERSION "\n"); }
 
 static void help(FILE *fp)
 {
@@ -341,6 +338,7 @@ int main(int argc, char *argv[])
   sel_addtimer(&sel, &it, &tv, interval, 0);
 
   for (;;) {
+    a_preselect();
     if (!sel_select(&sel))
       selerr = 0;
     else if (errno != EINTR && errno != EAGAIN) {
index 6bee0c3d0d6865a4eb7a9d55fc9b7a67281b4264..1d13f32d99180a444c004c1c7362baf371167497 100644 (file)
@@ -391,7 +391,36 @@ typedef struct admin_pingop {
   admin_bgop bg;                       /* Background operation header */
   ping ping;                           /* Ping pending response */
   struct timeval pingtime;             /* Time last ping was sent */
-} admin_pingop;  
+} admin_pingop;
+
+typedef struct admin_service {
+  sym_base _b;                         /* Hash table base structure */
+  char *version;                       /* The provided version */
+  struct admin *prov;                  /* Which client provides me */
+  struct admin_service *next, *prev;   /* Client's list of services */
+} admin_service;
+
+typedef struct admin_svcop {
+  admin_bgop bg;                       /* Background operation header */
+  struct admin *prov;                  /* Client servicing this job */
+  unsigned short index;                        /* This job's index */
+  struct admin_svcop *next, *prev;     /* Links for provider's jobs */
+} admin_svcop;
+
+typedef struct admin_jobentry {
+  unsigned short seq;                  /* Zero if unused */
+  union {
+    admin_svcop *op;                   /* Operation, if slot in use, ... */
+    uint32 next;                       /* ... or index of next free slot */
+  } u;
+} admin_jobentry;
+
+typedef struct admin_jobtable {
+  uint32 n, sz;                                /* Used slots and table size */
+  admin_svcop *active;                 /* List of active jobs */
+  uint32 free;                         /* Index of first free slot */
+  admin_jobentry *v;                   /* And the big array of entries */
+} admin_jobtable;
 
 typedef struct admin {
   struct admin *next, *prev;           /* Links to next and previous */
@@ -403,6 +432,8 @@ typedef struct admin {
   oqueue out;                          /* Output buffer list */
   oqueue delay;                                /* Delayed output buffer list */
   admin_bgop *bg;                      /* Backgrounded operations */
+  admin_service *svcs;                 /* Which services I provide */
+  admin_jobtable j;                    /* Table of outstanding jobs */
   selbuf b;                            /* Line buffer for commands */
   sel_file w;                          /* Selector for write buffering */
 } admin;
@@ -792,6 +823,19 @@ extern void a_create(int /*fd_in*/, int /*fd_out*/, unsigned /*f*/);
 
 extern void a_quit(void);
 
+/* --- @a_preselect@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    ---
+ *
+ * Use:                Informs the admin module that we're about to select again,
+ *             and that it should do cleanup things it has delayed until a
+ *             `safe' time.
+ */
+
+extern void a_preselect(void);
+
 /* --- @a_daemon@ --- *
  *
  * Arguments:  ---
@@ -1133,6 +1177,39 @@ extern void seq_reset(seqwin */*s*/);
 
 extern int seq_check(seqwin */*s*/, uint32 /*q*/, const char */*service*/);
 
+/* --- @versioncmp@ --- *
+ *
+ * Arguments:  @const char *va, *vb@ = two version strings
+ *
+ * Returns:    Less than, equal to, or greater than zero, according to
+ *             whether @va@ is less than, equal to, or greater than @vb@.
+ *
+ * Use:                Compares version number strings.
+ *
+ *             The algorithm is an extension of the Debian version
+ *             comparison algorithm.  A version number consists of three
+ *             components:
+ *
+ *               [EPOCH :] MAIN [- SUB]
+ *
+ *             The MAIN part may contain colons or hyphens if there is an
+ *             EPOCH or SUB, respectively.  Version strings are compared
+ *             componentwise: first epochs, then main parts, and finally
+ *             subparts.
+ *
+ *             The component comparison is done as follows.  First, the
+ *             initial subsequence of nondigit characters is extracted from
+ *             each string, and these are compared lexicographically, using
+ *             ASCII ordering, except that letters precede non-letters.  If
+ *             both are the same, an initial sequence of digits is extracted
+ *             from the remaining parts of the version strings, and these
+ *             are compared numerically (an empty sequence being considered
+ *             to have the value zero).  This process is repeated until we
+ *             have a winner or until both strings are exhausted.
+ */
+
+extern int versioncmp(const char */*va*/, const char */*vb*/);
+
 /*----- That's all, folks -------------------------------------------------*/
 
 #ifdef __cplusplus