chiark / gitweb /
Server and Python interface now support schedule-* commands. Tests to
[disorder] / server / server.c
index 5e8f2208baffd6cddba8d72034fc81c2afdb1577..e8e6a68668f1ede6b36df54c01b55a275093bdef 100644 (file)
@@ -71,6 +71,7 @@
 #include "mime.h"
 #include "sendmail.h"
 #include "wstat.h"
+#include "schedule.h"
 
 #ifndef NONCE_SIZE
 # define NONCE_SIZE 16
@@ -124,6 +125,8 @@ struct conn {
   rights_type rights;
   /** @brief Next connection */
   struct conn *next;
+  /** @brief True if pending rescan had 'wait' set */
+  int rescan_wait;
 };
 
 /** @brief Linked list of connections */
@@ -272,9 +275,8 @@ static int c_remove(struct conn *c, char **vec,
   queue_remove(q, c->who);
   /* De-prepare the track. */
   abandon(c->ev, q);
-  /* If we removed a random track then add another one. */
-  if(q->state == playing_random)
-    add_random_track();
+  /* See about adding a new random track */
+  add_random_track(c->ev);
   /* Prepare whatever the next head track is. */
   if(qhead.next != &qhead)
     prepare(c->ev, qhead.next);
@@ -356,13 +358,107 @@ static int c_reconfigure(struct conn *c,
   return 1;                            /* completed */
 }
 
+static void finished_rescan(void *ru) {
+  struct conn *const c = ru;
+
+  sink_writes(ev_writer_sink(c->w), "250 rescan completed\n");
+  /* Turn this connection back on */
+  ev_reader_enable(c->r);
+}
+
+static void start_fresh_rescan(void *ru) {
+  struct conn *const c = ru;
+
+  if(trackdb_rescan_underway()) {
+    /* Some other waiter beat us to it.  However in this case we're happy to
+     * piggyback; the requirement is that a new rescan be started, not that it
+     * was _our_ rescan. */
+    if(c->rescan_wait) {
+      /* We block until the rescan completes */
+      trackdb_add_rescanned(finished_rescan, c);
+    } else {
+      /* We report that the new rescan has started */
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      /* Turn this connection back on */
+      ev_reader_enable(c->r);
+    }
+  } else {
+    /* We are the first connection to get a callback so we must start a
+     * rescan. */
+    if(c->rescan_wait) {
+      /* We want to block until the new rescan completes */
+      trackdb_rescan(c->ev, 1/*check*/, finished_rescan, c);
+    } else {
+      /* We can report back immediately */
+      trackdb_rescan(c->ev, 1/*check*/, 0, 0);
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      /* Turn this connection back on */
+      ev_reader_enable(c->r);
+    }
+  }
+}
+
 static int c_rescan(struct conn *c,
-                   char attribute((unused)) **vec,
-                   int attribute((unused)) nvec) {
-  info("S%x rescan by %s", c->tag, c->who);
-  trackdb_rescan(c->ev, 1/*check*/);
-  sink_writes(ev_writer_sink(c->w), "250 initiated rescan\n");
-  return 1;                            /* completed */
+                   char **vec,
+                   int nvec) {
+  int wait = 0, fresh = 0, n;
+
+  /* Parse flags */
+  for(n = 0; n < nvec; ++n) {
+    if(!strcmp(vec[n], "wait"))
+      wait = 1;                                /* wait for rescan to complete */
+#if 0
+    /* Currently disabled because untested (and hard to test). */
+    else if(!strcmp(vec[n], "fresh"))
+      fresh = 1;                       /* don't piggyback underway rescan */
+#endif
+    else {
+      sink_writes(ev_writer_sink(c->w), "550 unknown flag\n");
+      return 1;                                /* completed */
+    }
+  }
+  /* Report what was requested */
+  info("S%x rescan by %s (%s %s)", c->tag, c->who,
+       wait ? "wait" : "",
+       fresh ? "fresh" : "");
+  if(trackdb_rescan_underway()) {
+    if(fresh) {
+      /* We want a fresh rescan but there is already one underway.  Arrange a
+       * callback when it completes and then set off a new one. */
+      c->rescan_wait = wait;
+      trackdb_add_rescanned(start_fresh_rescan, c);
+      if(wait)
+       return 0;
+      else {
+       sink_writes(ev_writer_sink(c->w), "250 rescan queued\n");
+       return 1;
+      }
+    } else {
+      /* There's a rescan underway, and it's acceptable to piggyback on it */
+      if(wait) {
+       /* We want to block until completion. */
+       trackdb_add_rescanned(finished_rescan, c);
+       return 0;
+      } else {
+       /* We don't want to block.  So we just report that things are in
+        * hand. */
+       sink_writes(ev_writer_sink(c->w), "250 rescan already underway\n");
+       return 1;
+      }
+    }
+  } else {
+    /* No rescan is underway.  fresh is therefore irrelevant. */
+    if(wait) {
+      /* We want to block until completion */
+      trackdb_rescan(c->ev, 1/*check*/, finished_rescan, c);
+      return 0;
+    } else {
+      /* We don't want to block. */
+      trackdb_rescan(c->ev, 1/*check*/, 0, 0);
+      sink_writes(ev_writer_sink(c->w), "250 rescan initiated\n");
+      return 1;                                /* completed */
+    }
+  }
 }
 
 static int c_version(struct conn *c,
@@ -615,9 +711,13 @@ static int c_allfiles(struct conn *c,
 static int c_get(struct conn *c,
                 char **vec,
                 int attribute((unused)) nvec) {
-  const char *v;
+  const char *v, *track;
 
-  if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if(vec[1][0] != '_' && (v = trackdb_get(track, vec[1])))
     sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(v));
   else
     sink_writes(ev_writer_sink(c->w), "555 not found\n");
@@ -643,7 +743,13 @@ static int c_length(struct conn *c,
 static int c_set(struct conn *c,
                 char **vec,
                 int attribute((unused)) nvec) {
-  if(vec[1][0] != '_' && !trackdb_set(vec[0], vec[1], vec[2]))
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  if(vec[1][0] != '_' && !trackdb_set(track, vec[1], vec[2]))
     sink_writes(ev_writer_sink(c->w), "250 OK\n");
   else
     sink_writes(ev_writer_sink(c->w), "550 not found\n");
@@ -654,8 +760,13 @@ static int c_prefs(struct conn *c,
                   char **vec,
                   int attribute((unused)) nvec) {
   struct kvp *k;
+  const char *track;
 
-  k = trackdb_get_all(vec[0]);
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
+  k = trackdb_get_all(track);
   sink_writes(ev_writer_sink(c->w), "253 prefs follow\n");
   for(; k; k = k->next)
     if(k->name[0] != '_')              /* omit internal values */
@@ -668,6 +779,7 @@ static int c_prefs(struct conn *c,
 static int c_exists(struct conn *c,
                    char **vec,
                    int attribute((unused)) nvec) {
+  /* trackdb_exists() does its own alias checking */
   sink_printf(ev_writer_sink(c->w), "252 %s\n", noyes[trackdb_exists(vec[0])]);
   return 1;
 }
@@ -768,7 +880,7 @@ static int c_volume(struct conn *c,
     sink_writes(ev_writer_sink(c->w), "510 Prohibited\n");
     return 1;
   }
-  if(mixer_control(&l, &r, set))
+  if(mixer_control(-1/*as configured*/, &l, &r, set))
     sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n");
   else {
     sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r);
@@ -932,8 +1044,14 @@ static int c_moveafter(struct conn *c,
 static int c_part(struct conn *c,
                  char **vec,
                  int attribute((unused)) nvec) {
+  const char *track;
+
+  if(!(track = trackdb_resolve(vec[0]))) {
+    sink_writes(ev_writer_sink(c->w), "550 cannot resolve track\n");
+    return 1;
+  }
   sink_printf(ev_writer_sink(c->w), "252 %s\n",
-             quoteutf8(trackdb_getpart(vec[0], vec[1], vec[2])));
+             quoteutf8(trackdb_getpart(track, vec[1], vec[2])));
   return 1;
 }
 
@@ -1097,6 +1215,11 @@ static int c_adduser(struct conn *c,
                     int nvec) {
   const char *rights;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote adduser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   if(nvec > 2) {
     rights = vec[2];
     if(parse_rights(vec[2], 0, 1)) {
@@ -1118,6 +1241,11 @@ static int c_deluser(struct conn *c,
                     int attribute((unused)) nvec) {
   struct conn *d;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote deluser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   if(trackdb_deluser(vec[0])) {
     sink_writes(ev_writer_sink(c->w), "550 Cannot delete user\n");
     return 1;
@@ -1135,6 +1263,11 @@ static int c_edituser(struct conn *c,
                      int attribute((unused)) nvec) {
   struct conn *d;
 
+  if(!config->remote_userman && !(c->rights & RIGHT__LOCAL)) {
+    error(0, "S%x: remote edituser", c->tag);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   /* RIGHT_ADMIN can do anything; otherwise you can only set your own email
    * address and password. */
   if((c->rights & RIGHT_ADMIN)
@@ -1154,7 +1287,7 @@ static int c_edituser(struct conn *c,
       /* Update rights for this user */
       rights_type r;
 
-      if(parse_rights(vec[1], &r, 1))
+      if(parse_rights(vec[2], &r, 1))
        for(d = connections; d; d = d->next)
          if(!strcmp(d->who, vec[0]))
            d->rights = r;
@@ -1173,8 +1306,17 @@ static int c_userinfo(struct conn *c,
   struct kvp *k;
   const char *value;
 
+  /* We allow remote querying of rights so that clients can figure out what
+   * they're allowed to do */
+  if(!config->remote_userman
+     && !(c->rights & RIGHT__LOCAL)
+     && strcmp(vec[1], "rights")) {
+    error(0, "S%x: remote userinfo %s %s", c->tag, vec[0], vec[1]);
+    sink_writes(ev_writer_sink(c->w), "550 Remote user management is disabled\n");
+    return 1;
+  }
   /* RIGHT_ADMIN allows anything; otherwise you can only get your own email
-   * address and righst list. */
+   * address and rights list. */
   if((c->rights & RIGHT_ADMIN)
      || (!strcmp(c->who, vec[0])
         && (!strcmp(vec[1], "email")
@@ -1365,6 +1507,105 @@ static int c_reminder(struct conn *c,
   return 0;
 }
 
+static int c_schedule_list(struct conn *c,
+                          char attribute((unused)) **vec,
+                          int attribute((unused)) nvec) {
+  char **ids = schedule_list(0);
+  sink_writes(ev_writer_sink(c->w), "253 ID list follows\n");
+  while(*ids)
+    sink_printf(ev_writer_sink(c->w), "%s\n", *ids++);
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_get(struct conn *c,
+                         char **vec,
+                         int attribute((unused)) nvec) {
+  struct kvp *actiondata = schedule_get(vec[0]), *k;
+
+  if(!actiondata) {
+    sink_writes(ev_writer_sink(c->w), "555 No such event\n");
+    return 1;                          /* completed */
+  }
+  /* Scheduled events are public information.  Anyone with RIGHT_READ can see
+   * them. */
+  sink_writes(ev_writer_sink(c->w), "253 Event information follows\n");
+  for(k = actiondata; k; k = k->next)
+    sink_printf(ev_writer_sink(c->w), " %s %s\n",
+               quoteutf8(k->name),  quoteutf8(k->value));
+  sink_writes(ev_writer_sink(c->w), ".\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_del(struct conn *c,
+                         char **vec,
+                         int attribute((unused)) nvec) {
+  struct kvp *actiondata = schedule_get(vec[0]);
+
+  if(!actiondata) {
+    sink_writes(ev_writer_sink(c->w), "555 No such event\n");
+    return 1;                          /* completed */
+  }
+  /* If you have admin rights you can delete anything.  If you don't then you
+   * can only delete your own scheduled events. */
+  if(!(c->rights & RIGHT_ADMIN)) {
+    const char *who = kvp_get(actiondata, "who");
+
+    if(!who || !c->who || strcmp(who, c->who)) {
+      sink_writes(ev_writer_sink(c->w), "551 Not authorized\n");
+      return 1;                                /* completed */
+    }
+  }
+  if(schedule_del(vec[0]))
+    sink_writes(ev_writer_sink(c->w), "550 Could not delete scheduled event\n");
+  else
+    sink_writes(ev_writer_sink(c->w), "250 Deleted\n");
+  return 1;                            /* completed */
+}
+
+static int c_schedule_add(struct conn *c,
+                         char **vec,
+                         int nvec) {
+  struct kvp *actiondata = 0;
+  char *id;
+
+  /* Standard fields */
+  kvp_set(&actiondata, "who", c->who);
+  kvp_set(&actiondata, "when", vec[0]);
+  kvp_set(&actiondata, "priority", vec[1]);
+  kvp_set(&actiondata, "action", vec[2]);
+  /* Action-dependent fields */
+  if(!strcmp(vec[2], "play")) {
+    if(nvec != 4) {
+      sink_writes(ev_writer_sink(c->w), "550 Wrong number of arguments\n");
+      return 1;
+    }
+    if(!trackdb_exists(vec[3])) {
+      sink_writes(ev_writer_sink(c->w), "550 Track is not in database\n");
+      return 1;
+    }
+    kvp_set(&actiondata, "track", vec[3]);
+  } else if(!strcmp(vec[2], "set-global")) {
+    if(nvec < 4 || nvec > 5) {
+      sink_writes(ev_writer_sink(c->w), "550 Wrong number of arguments\n");
+      return 1;
+    }
+    kvp_set(&actiondata, "key", vec[3]);
+    if(nvec > 4)
+      kvp_set(&actiondata, "value", vec[4]);
+  } else {
+    sink_writes(ev_writer_sink(c->w), "550 Unknown action\n");
+    return 1;
+  }
+  /* schedule_add() checks user rights */
+  id = schedule_add(c->ev, actiondata);
+  if(!id)
+    sink_writes(ev_writer_sink(c->w), "550 Cannot add scheduled event\n");
+  else
+    sink_printf(ev_writer_sink(c->w), "252 %s\n", id);
+  return 1;
+}
+
 static const struct command {
   /** @brief Command name */
   const char *name;
@@ -1420,11 +1661,15 @@ static const struct command {
   { "register",       3, 3,       c_register,       RIGHT_REGISTER|RIGHT__LOCAL },
   { "reminder",       1, 1,       c_reminder,       RIGHT__LOCAL },
   { "remove",         1, 1,       c_remove,         RIGHT_REMOVE__MASK },
-  { "rescan",         0, 0,       c_rescan,         RIGHT_RESCAN },
+  { "rescan",         0, INT_MAX, c_rescan,         RIGHT_RESCAN },
   { "resolve",        1, 1,       c_resolve,        RIGHT_READ },
   { "resume",         0, 0,       c_resume,         RIGHT_PAUSE },
   { "revoke",         0, 0,       c_revoke,         RIGHT_READ },
   { "rtp-address",    0, 0,       c_rtp_address,    0 },
+  { "schedule-add",   3, INT_MAX, c_schedule_add,   RIGHT_READ },
+  { "schedule-del",   1, 1,       c_schedule_del,   RIGHT_READ },
+  { "schedule-get",   1, 1,       c_schedule_get,   RIGHT_READ },
+  { "schedule-list",  0, 0,       c_schedule_list,  RIGHT_READ },
   { "scratch",        0, 1,       c_scratch,        RIGHT_SCRATCH__MASK },
   { "search",         1, 1,       c_search,         RIGHT_READ },
   { "set",            3, 3,       c_set,            RIGHT_PREFS, },