chiark / gitweb /
Shadow disorder.dev
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 24 May 2008 16:18:08 +0000 (17:18 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 24 May 2008 16:18:08 +0000 (17:18 +0100)
27 files changed:
.bzrignore
README
clients/disorder.c
configure.ac
doc/disorder.1.in
doc/disorder_protocol.5.in
lib/Makefile.am
lib/client.c
lib/client.h
lib/dateparse.c [new file with mode: 0644]
lib/dateparse.h [new file with mode: 0644]
lib/event.h
lib/random.c
lib/random.h
lib/t-dateparse.c [new file with mode: 0644]
lib/trackdb-int.h
lib/trackdb.c
lib/trackdb.h
lib/xgetdate.c [new file with mode: 0644]
python/disorder.py.in
server/Makefile.am
server/schedule.c [new file with mode: 0644]
server/schedule.h [new file with mode: 0644]
server/server.c
server/state.c
tests/Makefile.am
tests/schedule.py [new file with mode: 0755]

index d14bd69dbc4e2ec982bd4d976d0c6b5a9f53d56c..6e03203d0865636d48904b1bb26617f9d6a3ad5a 100644 (file)
@@ -190,3 +190,4 @@ doc/disorder_templates.5.in
 lib/t-arcfour
 lib/t-charset
 lib/t-event
+lib/t-dateparse
diff --git a/README b/README
index 5f09fc2ce1ef9170b476ed2b050dee834fc0af16..27657dbd0ffc64b05f7db80d589e2d33722fbb56 100644 (file)
--- a/README
+++ b/README
@@ -273,8 +273,7 @@ Portions copyright (C) 2007 Mark Wooding
 Portions extracted from MPG321, http://mpg321.sourceforge.net/
   Copyright (C) 2001 Joe Drew
   Copyright (C) 2000-2001 Robert Leslie
-Portions Copyright (C) 2000, 2001, 2002, 2003, 2005, 2006 Free Software
-Foundation, Inc.
+Portions Copyright (C) 1997-2006 Free Software Foundation, Inc.
 Binaries may derive extra copyright owners through linkage (binary distributors
 are expected to do their own legwork)
 
index 2088bc4a9fed9224dfd605008b79439d7a4ea160..0dd71c74eb3cfa700f7b420dfc1087175dd5a7e9 100644 (file)
@@ -54,6 +54,7 @@
 #include "authorize.h"
 #include "vector.h"
 #include "version.h"
+#include "dateparse.h"
 
 static disorder_client *client;
 
@@ -468,6 +469,110 @@ static void cf_setup_guest(char **argv) {
     exit(EXIT_FAILURE);
 }
 
+struct scheduled_event {
+  time_t when;
+  struct kvp *actiondata;
+  char *id;
+};
+
+static int compare_event(const void *av, const void *bv) {
+  struct scheduled_event *a = (void *)av, *b = (void *)bv;
+
+  /* Primary sort key is the trigger time */
+  if(a->when < b->when)
+    return -1;
+  else if(a->when > b->when)
+    return 1;
+  /* For events that go off at the same time just sort by ID */
+  return strcmp(a->id, b->id);
+}
+
+static void cf_schedule_list(char attribute((unused)) **argv) {
+  char **ids;
+  int nids, n;
+  struct scheduled_event *events;
+  char tb[128];
+  const char *action, *key, *value, *priority;
+  int prichar;
+
+  /* Get all known events */
+  if(disorder_schedule_list(getclient(), &ids, &nids))
+    exit(EXIT_FAILURE);
+  events = xcalloc(nids, sizeof *events);
+  for(n = 0; n < nids; ++n) {
+    events[n].id = ids[n];
+    if(disorder_schedule_get(getclient(), ids[n], &events[n].actiondata))
+      exit(EXIT_FAILURE);
+    events[n].when = atoll(kvp_get(events[n].actiondata, "when"));
+  }
+  /* Sort by trigger time */
+  qsort(events, nids, sizeof *events, compare_event);
+  /* Display them */
+  for(n = 0; n < nids; ++n) {
+    strftime(tb, sizeof tb, "%Y-%m-%d %H:%M:%S %Z", localtime(&events[n].when));
+    action = kvp_get(events[n].actiondata, "action");
+    priority = kvp_get(events[n].actiondata, "priority");
+    if(!strcmp(priority, "junk"))
+      prichar = 'J';
+    else if(!strcmp(priority, "normal"))
+      prichar = 'N';
+    else
+      prichar = '?';
+    xprintf("%11s %-25s %c %-8s %s",
+           events[n].id, tb, prichar, kvp_get(events[n].actiondata, "who"),
+           action);
+    if(!strcmp(action, "play"))
+      xprintf(" %s",
+             nullcheck(utf82mb(kvp_get(events[n].actiondata, "track"))));
+    else if(!strcmp(action, "set-global")) {
+      key = kvp_get(events[n].actiondata, "key");
+      value = kvp_get(events[n].actiondata, "value");
+      if(value)
+       xprintf(" %s=%s",
+               nullcheck(utf82mb(key)),
+               nullcheck(utf82mb(value)));
+      else
+       xprintf(" %s unset",
+               nullcheck(utf82mb(key)));
+    }
+    xprintf("\n");
+  }
+}
+
+static void cf_schedule_del(char **argv) {
+  if(disorder_schedule_del(getclient(), argv[0]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_play(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          dateparse(argv[0]),
+                          argv[1],
+                          "play",
+                          argv[2]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_set_global(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          dateparse(argv[0]),
+                          argv[1],
+                          "set-global",
+                          argv[2],
+                          argv[3]))
+    exit(EXIT_FAILURE);
+}
+
+static void cf_schedule_unset_global(char **argv) {
+  if(disorder_schedule_add(getclient(),
+                          dateparse(argv[0]),
+                          argv[1],
+                          "set-global",
+                          argv[2],
+                          (char *)0))
+    exit(EXIT_FAILURE);
+}
+
 static const struct command {
   const char *name;
   int min, max;
@@ -480,7 +585,7 @@ static const struct command {
   { "allfiles",       1, 2, cf_allfiles, isarg_regexp, "DIR [~REGEXP]",
                       "List all files and directories in DIR" },
   { "authorize",      1, 2, cf_authorize, isarg_rights, "USERNAME [RIGHTS]",
-                      "Authorize user USERNAME to connect to the server" },
+                      "Authorize user USERNAME to connect" },
   { "deluser",        1, 1, cf_deluser, 0, "USERNAME",
                       "Delete user USERNAME" },
   { "dirs",           1, 2, cf_dirs, isarg_regexp, "DIR [~REGEXP]",
@@ -542,6 +647,16 @@ static const struct command {
                       "Resume after a pause" },
   { "rtp-address",    0, 0, cf_rtp_address, 0, "",
                       "Report server's broadcast address" },
+  { "schedule-del",   1, 1, cf_schedule_del, 0, "EVENT",
+                      "Delete a scheduled event" },
+  { "schedule-list",  0, 0, cf_schedule_list, 0, "",
+                      "List scheduled events" },
+  { "schedule-play",  3, 3, cf_schedule_play, 0, "WHEN PRI TRACK",
+                      "Play TRACK later" },
+  { "schedule-set-global", 4, 4, cf_schedule_set_global, 0, "WHEN PRI NAME VAL",
+                      "Set a global preference later" },
+  { "schedule-unset-global", 3, 3, cf_schedule_unset_global, 0, "WHEN PRI NAME",
+                      "Unset a global preference later" },
   { "scratch",        0, 0, cf_scratch, 0, "",
                       "Scratch the currently playing track" },
   { "scratch-id",     1, 1, cf_scratch, 0, "ID",
@@ -612,6 +727,7 @@ int main(int argc, char **argv) {
   pcre_malloc = xmalloc;
   pcre_free = xfree;
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  if(!setlocale(LC_TIME, "")) fatal(errno, "error calling setlocale");
   while((n = getopt_long(argc, argv, "+hVc:dHlNu:p:", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
index 3c13dacc67f6d923a521eedad037fc2da16ab309..bfed8b54f17d17a335082dcf322a920971e158e5 100644 (file)
@@ -1,4 +1,3 @@
-
 # Process this file with autoconf to produce a configure script.
 #
 # This file is part of DisOrder.
@@ -45,6 +44,7 @@ AC_SET_MAKE
 if test "x$GCC" = xyes; then
   gcc_werror=-Werror
 else
+  AC_MSG_ERROR([GNU C is required to build this program])
   gcc_werror=""
 fi
 
index bd22e56d9baa7de8c721abe92d065a8c57399276..9de40a96d4756128f6d51477c67c8819f7e16859 100644 (file)
@@ -182,6 +182,41 @@ Resume the current track after a pause.
 .B rtp\-address
 Report the RTP brodcast address used by the server (if any).
 .TP
+.B schedule-del \fIEVENT\fR
+Delete a scheduled event.
+.TP
+.B schedule-list
+List scheduled events.
+Each line contains the ID, a timestamp, 'N' or 'J' for normal or junk priority,
+the user, the action and action-specific data.
+.TP
+.B schedule-play \fIWHEN PRIORITY TRACK\fI
+Play \fITRACK\fR at time \fIWHEN\fR.
+Various time/date formats are supported depending on locale but the following
+three will always work:
+.RS
+.RS
+.TP
+.B "YYYY-MM-DD HH:MM:SS"
+.TP
+.B "HH:MM:SS"
+.TP
+.B "HH:MM"
+.RE
+.RE
+.IP
+\fIPRIORITY\fR should be \fBjunk\fR or \fBnormal\fR.
+This determines how the event is handled if it becomes due when the server is
+down.
+Junk events are just discarded in this case, while normal events will be
+executed when the server comes back up, even if this is much later.
+.TP
+.B schedule-set-global \fIWHEN PRIORITY NAME VALUE\fI
+Set global preference \fINAME\fR to \fIVALUE\fR at time \fIWHEN\fR.
+.TP
+.B schedule-unset-global \fIWHEN PRIORITY NAME\fI
+Unset global preference \fINAME\fR at time \fIWHEN\fR.
+.TP
 .B scratch
 Scratch the currently playing track.
 .TP
index 08470508c9be7c5e9c33ed7d12dad4d9fcf0fe74..f4c5b2fbfd446c744c484383229d0ef1f5f0aa71 100644 (file)
@@ -294,6 +294,61 @@ Requires one of the \fBscratch mine\fR, \fBscratch random\fR or
 \fBscratch any\fR rights depending on how the track came to be
 added to the queue.
 .TP
+.B schedule-add \fIWHEN\fR \fIPRIORITY\fR \fIACTION\fR ...
+Schedule an event for the future.
+.IP
+.I WHEN
+is the time when it should happen, as \fBtime_t\fR value.
+It must refer to a time in the future.
+.IP
+.I PRIORITY
+is the event priority.
+This can be \fBnormal\fR, in which case the event will be run at startup if its
+time has past, or \fBjunk\fR in which case it will be discarded if it is found
+to be in the past at startup.
+The meaning of other values is not defined.
+.IP
+.I ACTION
+is the action to perform.
+The choice of action determines the meaning of the remaining arguments.
+Possible actions are:
+.RS
+.TP
+.B play
+Play a track.
+The next argument is the track name.
+Requires the \fBplay\fR right.
+.TP
+.B set-global
+Set a global preference.
+The next argument is the preference name and the final argument is the value to
+set it to (omit it to unset it).
+Requires the \fBglobal prefs\fR right.
+.RE
+.IP
+You need the right at the point you create the event.
+It is not possible to create scheduled events in expectation of a future change
+in rights.
+.TP
+.B schedule-del \fIEVENT\fR
+Deletes a scheduled event.
+Users can always delete their own scheduled events; with the \fBadmin\fR
+right you can delete any event.
+.TP
+.B schedule-get \fIEVENT\fR
+Sends the details of scheduled event \fIEVENT\fR in a response body.
+Each line is a pair of strings quoted in the usual way, the first being the key
+ane the second the value.
+No particular order is used.
+.IP
+Scheduled events are considered public information.
+Right \fBread\fR is sufficient to see details of all events.
+.TP
+.B schedule-list
+Sends the event IDs of all scheduled events in a response body, in no
+particular order.
+Use \fBschedule-get\fR to get the details of each event.
+.TP
 .B search \fITERMS\fR
 Search for tracks matching the search terms.
 The results are put in a response body, one to a line.
index 6b7aec8fee9180a69462428b5a55661f7d33511c..16cdcea4f3a41e270ea8f82d12ab1e7bfc8adbb7 100644 (file)
 #
 
 TESTS=t-addr t-arcfour t-basen t-bits t-cache t-casefold t-charset     \
-       t-cookies t-event t-filepart t-hash t-heap t-hex t-kvp t-mime   \
-       t-printf t-regsub t-selection t-signame t-sink t-split          \
-       t-syscalls t-trackname t-unicode t-url t-utf8 t-vector t-words  \
-       t-wstat t-macros t-cgi
+       t-cookies t-dateparse t-event t-filepart t-hash t-heap t-hex    \
+       t-kvp t-mime t-printf t-regsub t-selection t-signame t-sink     \
+       t-split t-syscalls t-trackname t-unicode t-url t-utf8 t-vector  \
+       t-words t-wstat t-macros t-cgi
 
 noinst_LIBRARIES=libdisorder.a
 include_HEADERS=disorder.h
@@ -48,6 +48,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        client-common.c client-common.h                 \
        configuration.c configuration.h                 \
        cookies.c cookies.h                             \
+       dateparse.c dateparse.h xgetdate.c              \
        defs.c defs.h                                   \
        eclient.c eclient.h                             \
        event.c event.h                                 \
@@ -161,6 +162,10 @@ t_cookies_SOURCES=t-cookies.c test.c test.h
 t_cookies_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_cookies_DEPENDENCIES=libdisorder.a
 
+t_dateparse_SOURCES=t-dateparse.c test.c test.h
+t_dateparse_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
+t_dateparse_DEPENDENCIES=libdisorder.a
+
 t_event_SOURCES=t-event.c test.c test.h
 t_event_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_event_DEPENDENCIES=libdisorder.a
index 4dfd10cc27b9b11e321a9d44405595a12a8c1e5d..d357a8ca67af471db37307bd33e70bebff37d7af 100644 (file)
@@ -58,6 +58,7 @@
 #include "client-common.h"
 #include "rights.h"
 #include "trackdb.h"
+#include "kvp.h"
 
 /** @brief Client handle contents */
 struct disorder_client {
@@ -1215,6 +1216,94 @@ int disorder_reminder(disorder_client *c, const char *user) {
   return disorder_simple(c, 0, "reminder", user, (char *)0);
 }
 
+/** @brief List scheduled events
+ * @param c Client
+ * @param idsp Where to put list of event IDs
+ * @param nidsp Where to put count of event IDs, or NULL
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_list(disorder_client *c, char ***idsp, int *nidsp) {
+  return disorder_simple_list(c, idsp, nidsp, "schedule-list", (char *)0);
+}
+
+/** @brief Delete a scheduled event
+ * @param c Client
+ * @param id Event ID to delete
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_del(disorder_client *c, const char *id) {
+  return disorder_simple(c, 0, "schedule-del", id, (char *)0);
+}
+
+/** @brief Get details of a scheduled event
+ * @param c Client
+ * @param id Event ID
+ * @param actiondatap Where to put details
+ * @return 0 on success, non-0 on error
+ */
+int disorder_schedule_get(disorder_client *c, const char *id,
+                         struct kvp **actiondatap) {
+  char **lines, **bits;
+  int rc, nbits;
+
+  *actiondatap = 0;
+  if((rc = disorder_simple_list(c, &lines, NULL,
+                               "schedule-get", id, (char *)0)))
+    return rc;
+  while(*lines) {
+    if(!(bits = split(*lines++, &nbits, SPLIT_QUOTES, 0, 0))) {
+      error(0, "invalid schedule-get reply: cannot split line");
+      return -1;
+    }
+    if(nbits != 2) {
+      error(0, "invalid schedule-get reply: wrong number of fields");
+      return -1;
+    }
+    kvp_set(actiondatap, bits[0], bits[1]);
+  }
+  return 0;
+}
+
+/** @brief Add a scheduled event
+ * @param c Client
+ * @param when When to trigger the event
+ * @param priority Event priority ("normal" or "junk")
+ * @param action What action to perform
+ * @param ... Action-specific arguments
+ * @return 0 on success, non-0 on error
+ *
+ * For action @c "play" the next argument is the track.
+ *
+ * For action @c "set-global" next argument is the global preference name
+ * and the final argument the value to set it to, or (char *)0 to unset it.
+ */
+int disorder_schedule_add(disorder_client *c,
+                         time_t when,
+                         const char *priority,
+                         const char *action,
+                         ...) {
+  va_list ap;
+  char when_str[64];
+  int rc;
+
+  snprintf(when_str, sizeof when_str, "%lld", (long long)when);
+  va_start(ap, action);
+  if(!strcmp(action, "play"))
+    rc = disorder_simple(c, 0, "schedule-add", when_str, priority,
+                        action, va_arg(ap, char *),
+                        (char *)0);
+  else if(!strcmp(action, "set-global")) {
+    const char *key = va_arg(ap, char *);
+    const char *value = va_arg(ap, char *);
+    rc = disorder_simple(c, 0,"schedule-add",  when_str, priority,
+                        action, key, value,
+                        (char *)0);
+  } else
+    fatal(0, "unknown action '%s'", action);
+  va_end(ap);
+  return rc;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 674272cff6e98f4f3973f25c1480046456e1b1eb..abadef6691ee0d9d807efd3f7ef950510f3a67c5 100644 (file)
@@ -116,6 +116,15 @@ int disorder_make_cookie(disorder_client *c, char **cookiep);
 const char *disorder_last(disorder_client *c);
 int disorder_revoke(disorder_client *c);
 int disorder_reminder(disorder_client *c, const char *user);
+int disorder_schedule_list(disorder_client *c, char ***idsp, int *nidsp);
+int disorder_schedule_del(disorder_client *c, const char *id);
+int disorder_schedule_get(disorder_client *c, const char *id,
+                         struct kvp **actiondatap);
+int disorder_schedule_add(disorder_client *c,
+                         time_t when,
+                         const char *priority,
+                         const char *action,
+                         ...);
 
 #endif /* CLIENT_H */
 
diff --git a/lib/dateparse.c b/lib/dateparse.c
new file mode 100644 (file)
index 0000000..45de40c
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/dateparse.c
+ * @brief Date parsing
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <time.h>
+
+#include "dateparse.h"
+#include "log.h"
+
+/** @brief Date parsing patterns
+ *
+ * This set of patterns is designed to parse a specific time of a specific day,
+ * since that's what the scheduler needs.  Other requirements might need other
+ * pattern lists.
+ */
+static const char *const datemsk[] = {
+  /* ISO format */
+  "%Y-%m-%d %H:%M:%S",
+  /* "%Y-%m-%d %H:%M:%S %Z" - no, not sensibly supported anywhere */
+  /* Locale-specific date + time */
+  "%c",
+  "%Ec",
+  /* Locale-specific time, same day */
+  "%X",
+  "%EX",
+  /* Generic time, same day */
+  "%H:%M",
+  "%H:%M:%S",
+  NULL,
+};
+
+/** @brief Convert string to a @c time_t */
+time_t dateparse(const char *s) {
+  struct tm t;
+  int rc;
+
+  switch(rc = xgetdate_r(s, &t, datemsk)) {
+  case 0:
+    return mktime(&t);
+  case 7:
+    fatal(0, "date string '%s' not in a recognized format", s);
+  case 8:
+    fatal(0, "date string '%s' not representable", s);
+  default:
+    fatal(0, "date string '%s' produced unexpected error %d", s, rc);
+  }
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/dateparse.h b/lib/dateparse.h
new file mode 100644 (file)
index 0000000..d86d14f
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/dateparse.h
+ * @brief Date parsing
+ */
+
+time_t dateparse(const char *s);
+struct tm *xgetdate(const char *string,
+                   const char *const *template);
+int xgetdate_r(const char *string,
+              struct tm *tp,
+              const char *const *template);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 34a6f4db4930c43489bf8191186af2e8792d0250..c666d81f3e50dde414bf839c257f070c4b1dd353 100644 (file)
@@ -21,6 +21,8 @@
 #ifndef EVENT_H
 #define EVENT_H
 
+#include <sys/socket.h>
+
 typedef struct ev_source ev_source;
 
 struct rusage;
index 70e19f8323e9fda0f1d098bea8d435dea53bae23..21d0b09a93a63662dc5177b95f2fa7511544b13e 100644 (file)
@@ -34,6 +34,8 @@
 #include "random.h"
 #include "log.h"
 #include "arcfour.h"
+#include "basen.h"
+#include "mem.h"
 
 static int random_count;
 static int random_fd = -1;
@@ -63,7 +65,7 @@ static void random__rekey(void) {
  * @param ptr Where to put random bytes
  * @param bytes How many random bytes to generate
  */
-void random_get(uint8_t *ptr, size_t bytes) {
+void random_get(void *ptr, size_t bytes) {
   if(random_count == 0)
     random__rekey();
   /* Encrypting 0s == just returning the keystream */
@@ -75,6 +77,16 @@ void random_get(uint8_t *ptr, size_t bytes) {
     random_count -= bytes;
 }
 
+/** @brief Return a random ID string */
+char *random_id(void) {
+  unsigned long words[2];
+  char id[128];
+
+  random_get(words, sizeof words);
+  basen(words, 4, id, sizeof id, 62);
+  return xstrdup(id);
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index f125e3c7d0886e5598d9727e1f583afc4794e532..24515892b803d73befc657713dd3dd300a1526b9 100644 (file)
@@ -25,7 +25,8 @@
 #ifndef RANDOM_H
 #define RANDOM_H
 
-void random_get(uint8_t *ptr, size_t bytes);
+void random_get(void *ptr, size_t bytes);
+char *random_id(void);
 
 #endif /* RANDOM_H */
 
diff --git a/lib/t-dateparse.c b/lib/t-dateparse.c
new file mode 100644 (file)
index 0000000..cd599e8
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "test.h"
+#include <time.h>
+#include "dateparse.h"
+
+static void check_date(time_t when,
+                      const char *format,
+                      struct tm *(*convert)(const time_t *)) {
+  char buffer[128];
+  time_t parsed;
+
+  strftime(buffer, sizeof buffer, format, convert(&when));
+  parsed = dateparse(buffer);
+  check_integer(parsed, when);
+  if(parsed != when)
+    fprintf(stderr, "format=%s formatted=%s\n", format, buffer);
+}
+
+static void test_dateparse(void) {
+  time_t now = time(0);
+  check_date(now, "%Y-%m-%d %H:%M:%S", localtime);
+#if 0         /* see dateparse.c */
+  check_date(now, "%Y-%m-%d %H:%M:%S %Z", localtime);
+  check_date(now, "%Y-%m-%d %H:%M:%S %Z", gmtime);
+#endif
+  check_date(now, "%c", localtime);
+  check_date(now, "%Ec", localtime);
+  check_date(now, "%X", localtime);
+  check_date(now, "%EX", localtime);
+  check_date(now, "%H:%M:%S", localtime);
+  /* This one needs a bodge: */
+  check_date(now - now % 60, "%H:%M", localtime);
+}
+
+TEST(dateparse);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index ac2d3eae95c7e7630db605011d9341bc51d3707c..1cfadbc80f48cbf2df270e1e1e3d6d790fa30d08 100644 (file)
@@ -21,6 +21,8 @@
 #ifndef TRACKDB_INT_H
 #define TRACKDB_INT_H
 
+#include "kvp.h"
+
 struct vector;                          /* forward declaration */
 
 extern DB_ENV *trackdb_env;
@@ -32,6 +34,7 @@ extern DB *trackdb_tagsdb;
 extern DB *trackdb_noticeddb;
 extern DB *trackdb_globaldb;
 extern DB *trackdb_usersdb;
+extern DB *trackdb_scheduledb;
 
 DBC *trackdb_opencursor(DB *db, DB_TXN *tid);
 /* open a transaction */
index 9125c0c3d14f31987574d514580c9779bda9b409..7cc371900349fc5fb3ff6417aa797bc47cf2d050 100644 (file)
@@ -145,6 +145,17 @@ DB *trackdb_globaldb;                   /* global preferences */
  */
 DB *trackdb_noticeddb;                   /* when track noticed */
 
+/** @brief The schedule database
+ *
+ * - Keys are ID strings, generated at random
+ * - Values are encoded key-value pairs
+ * - There can be more than one value per key
+ * - Data cannot be reconstructed
+ *
+ * See @ref server/schedule.c for further information.
+ */
+DB *trackdb_scheduledb;
+
 /** @brief The user database
  * - Keys are usernames
  * - Values are encoded key-value pairs
@@ -460,6 +471,7 @@ void trackdb_open(int flags) {
   trackdb_globaldb = open_db("global.db", 0, DB_HASH, dbflags, 0666);
   trackdb_noticeddb = open_db("noticed.db",
                              DB_DUPSORT, DB_BTREE, dbflags, 0666);
+  trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666);
   if(!trackdb_existing_database) {
     /* Stash the database version */
     char buf[32];
@@ -490,6 +502,8 @@ void trackdb_close(void) {
     fatal(0, "error closing global.db: %s", db_strerror(err));
   if((err = trackdb_noticeddb->close(trackdb_noticeddb, 0)))
     fatal(0, "error closing noticed.db: %s", db_strerror(err));
+  if((err = trackdb_scheduledb->close(trackdb_scheduledb, 0)))
+    fatal(0, "error closing schedule.db: %s", db_strerror(err));
   if((err = trackdb_usersdb->close(trackdb_usersdb, 0)))
     fatal(0, "error closing users.db: %s", db_strerror(err));
   trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0;
index f97ee5f338319d724ae13205afc6bd69b616e0d4..e3d61b564262f8d12dd16d9c80c9e024231ec378 100644 (file)
 #ifndef TRACKDB_H
 #define TRACKDB_H
 
-struct ev_source;
+#include <pcre.h>
+
+#include "event.h"
+#include "rights.h"
 
 extern const struct cache_type cache_files_type;
 extern unsigned long cache_files_hits, cache_files_misses;
diff --git a/lib/xgetdate.c b/lib/xgetdate.c
new file mode 100644 (file)
index 0000000..de1c4d3
--- /dev/null
@@ -0,0 +1,226 @@
+/* Derived from getdate.c in glibc 2.3.6.  This is pretty much
+ * standard getdate() except that you supply the template in an
+ * argument, rather than messing around with environment variables and
+ * files.  */
+
+/* Convert a string representation of time to a time value.
+   Copyright (C) 1997,1998,1999,2000,2001,2003 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+   Contributed by Mark Kettenis <kettenis@phys.uva.nl>, 1997.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, write to the Free
+   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+   02111-1307 USA.  */
+
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "dateparse.h"
+
+#define TM_YEAR_BASE 1900
+
+
+/* Prototypes for local functions.  */
+static int first_wday (int year, int mon, int wday);
+static int check_mday (int year, int mon, int mday);
+
+# define isleap(year)  \
+  ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0))
+
+/* Set to one of the following values to indicate an error.
+     1  the DATEMSK environment variable is null or undefined,
+     2  the template file cannot be opened for reading,
+     3  failed to get file status information,
+     4  the template file is not a regular file,
+     5  an error is encountered while reading the template file,
+     6  memory allication failed (not enough memory available),
+     7  there is no line in the template that matches the input,
+     8  invalid input specification Example: February 31 or a time is
+        specified that can not be represented in a time_t (representing
+       the time in seconds since 00:00:00 UTC, January 1, 1970) */
+int xgetdate_err;
+
+
+/* Returns the first weekday WDAY of month MON in the year YEAR.  */
+static int
+first_wday (int year, int mon, int wday)
+{
+  struct tm tm;
+
+  if (wday == INT_MIN)
+    return 1;
+
+  memset (&tm, 0, sizeof (struct tm));
+  tm.tm_year = year;
+  tm.tm_mon = mon;
+  tm.tm_mday = 1;
+  mktime (&tm);
+
+  return (1 + (wday - tm.tm_wday + 7) % 7);
+}
+
+
+/* Returns 1 if MDAY is a valid day of the month in month MON of year
+   YEAR, and 0 if it is not.  */
+static int
+check_mday (int year, int mon, int mday)
+{
+  switch (mon)
+    {
+    case 0:
+    case 2:
+    case 4:
+    case 6:
+    case 7:
+    case 9:
+    case 11:
+      if (mday >= 1 && mday <= 31)
+       return 1;
+      break;
+    case 3:
+    case 5:
+    case 8:
+    case 10:
+      if (mday >= 1 && mday <= 30)
+       return 1;
+      break;
+    case 1:
+      if (mday >= 1 && mday <= (isleap (year) ? 29 : 28))
+       return 1;
+      break;
+    }
+
+  return 0;
+}
+
+
+int
+xgetdate_r (const char *string, struct tm *tp,
+           const char *const *template)
+{
+  const char *line;
+  size_t len;
+  char *result = NULL;
+  time_t timer;
+  struct tm tm;
+  int mday_ok = 0;
+
+  line = NULL;
+  len = 0;
+  while((line = *template++))
+    {
+      /* Do the conversion.  */
+      tp->tm_year = tp->tm_mon = tp->tm_mday = tp->tm_wday = INT_MIN;
+      tp->tm_hour = tp->tm_sec = tp->tm_min = INT_MIN;
+      tp->tm_isdst = -1;
+      tp->tm_gmtoff = 0;
+      tp->tm_zone = NULL;
+      result = strptime (string, line, tp);
+      if (result && *result == '\0')
+       break;
+    }
+
+  if (result == NULL || *result != '\0')
+    return 7;
+
+  /* Get current time.  */
+  time (&timer);
+  localtime_r (&timer, &tm);
+
+  /* If only the weekday is given, today is assumed if the given day
+     is equal to the current day and next week if it is less.  */
+  if (tp->tm_wday >= 0 && tp->tm_wday <= 6 && tp->tm_year == INT_MIN
+      && tp->tm_mon == INT_MIN && tp->tm_mday == INT_MIN)
+    {
+      tp->tm_year = tm.tm_year;
+      tp->tm_mon = tm.tm_mon;
+      tp->tm_mday = tm.tm_mday + (tp->tm_wday - tm.tm_wday + 7) % 7;
+      mday_ok = 1;
+    }
+
+  /* If only the month is given, the current month is assumed if the
+     given month is equal to the current month and next year if it is
+     less and no year is given (the first day of month is assumed if
+     no day is given.  */
+  if (tp->tm_mon >= 0 && tp->tm_mon <= 11 && tp->tm_mday == INT_MIN)
+    {
+      if (tp->tm_year == INT_MIN)
+       tp->tm_year = tm.tm_year + (((tp->tm_mon - tm.tm_mon) < 0) ? 1 : 0);
+      tp->tm_mday = first_wday (tp->tm_year, tp->tm_mon, tp->tm_wday);
+      mday_ok = 1;
+    }
+
+  /* If no hour, minute and second are given the current hour, minute
+     and second are assumed.  */
+  if (tp->tm_hour == INT_MIN && tp->tm_min == INT_MIN && tp->tm_sec == INT_MIN)
+    {
+      tp->tm_hour = tm.tm_hour;
+      tp->tm_min = tm.tm_min;
+      tp->tm_sec = tm.tm_sec;
+    }
+
+  /* If no date is given, today is assumed if the given hour is
+     greater than the current hour and tomorrow is assumed if
+     it is less.  */
+  if (tp->tm_hour >= 0 && tp->tm_hour <= 23
+      && tp->tm_year == INT_MIN && tp->tm_mon == INT_MIN
+      && tp->tm_mday == INT_MIN && tp->tm_wday == INT_MIN)
+    {
+      tp->tm_year = tm.tm_year;
+      tp->tm_mon = tm.tm_mon;
+      tp->tm_mday = tm.tm_mday + ((tp->tm_hour - tm.tm_hour) < 0 ? 1 : 0);
+      mday_ok = 1;
+    }
+
+  /* Fill in the gaps.  */
+  if (tp->tm_year == INT_MIN)
+    tp->tm_year = tm.tm_year;
+  if (tp->tm_hour == INT_MIN)
+    tp->tm_hour = 0;
+  if (tp->tm_min == INT_MIN)
+    tp->tm_min = 0;
+  if (tp->tm_sec == INT_MIN)
+    tp->tm_sec = 0;
+
+  /* Check if the day of month is within range, and if the time can be
+     represented in a time_t.  We make use of the fact that the mktime
+     call normalizes the struct tm.  */
+  if ((!mday_ok && !check_mday (TM_YEAR_BASE + tp->tm_year, tp->tm_mon,
+                               tp->tm_mday))
+      || mktime (tp) == (time_t) -1)
+    return 8;
+
+  return 0;
+}
+
+
+
+struct tm *
+  xgetdate (const char *string, const char *const *template)
+{
+  /* Buffer returned by getdate.  */
+  static struct tm tmbuf;
+  int errval = xgetdate_r (string, &tmbuf, template);
+
+  if (errval != 0)
+    {
+      getdate_err = errval;
+      return NULL;
+    }
+
+  return &tmbuf;
+}
index ef33106fa8a1f8f007f1405e6889ab29c872619a..3cc300c27c25bd9171d0d097bfd2f7a78bbb5deb 100644 (file)
@@ -883,6 +883,30 @@ class client:
     """Confirm a user registration"""
     res, details = self._simple("confirm", confirmation)
 
+  def schedule_list(self):
+    """Get a list of scheduled events """
+    self._simple("schedule-list")
+    return self._body()
+
+  def schedule_del(self, event):
+    """Delete a scheduled event"""
+    self._simple("schedule-del", event)
+
+  def schedule_get(self, event):
+    """Get the details for an event as a dict (returns None if event not found)"""
+    res, details = self._simple("schedule-get", event)
+    if res == 555:
+      return None
+    d = {}
+    for line in self._body():
+      bits = _split(line)
+      d[bits[0]] = bits[1]
+    return d
+
+  def schedule_add(self, when, priority, action, *rest):
+    """Add a scheduled event"""
+    self._simple("schedule-add", str(when), priority, action, *rest)
+
   ########################################################################
   # I/O infrastructure
 
index 9ad35b22e88415a6c83a673910e0bc1f2bf5a1f2..a2b5acf825f2048144832edd7bd8b9122a9ee41a 100644 (file)
@@ -32,6 +32,7 @@ disorderd_SOURCES=disorderd.c                         \
        server.c server.h                               \
        server-queue.c server-queue.h                   \
        state.c state.h                                 \
+       schedule.c                                      \
        exports.c                                       \
        ../lib/memgc.c
 disorderd_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
diff --git a/server/schedule.c b/server/schedule.c
new file mode 100644 (file)
index 0000000..7776c3c
--- /dev/null
@@ -0,0 +1,517 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file server/schedule.c
+ * @brief Scheduled events
+ *
+ * @ref trackdb_scheduledb is a mapping from ID strings to encoded
+ * key-value pairs called 'actiondata'.
+ *
+ * Possible actiondata keys are:
+ * - @b when: when to perform this action (required)
+ * - @b who: originator for action (required)
+ * - @b action: action to perform (required)
+ * - @b track: for @c action=play, the track to play
+ * - @b key: for @c action=set-global, the global pref to set
+ * - @b value: for @c action=set-global, the value to set (omit to unset)
+ * - @b priority: the importance of this action
+ * - @b recurs: how the event recurs; NOT IMPLEMENTED
+ * - ...others to be defined
+ *
+ * Possible actions are:
+ * - @b play: play a track
+ * - @b set-global: set or unset a global pref
+ * - ...others to be defined
+ *
+ * Possible priorities are:
+ * - @b junk: junk actions that are in the past at startup are discarded
+ * - @b normal: normal actions that are in the past at startup are run
+ *   immediately.  (This the default.)
+ * - ...others to be defined
+ *
+ * On startup the schedule database is read and a timeout set on the event loop
+ * for each action.  Similarly when an action is added, a timeout is set on the
+ * event loop.  The timeout has the ID attached as user data so that the action
+ * can easily be found again.
+ *
+ * Recurring events are NOT IMPLEMENTED yet but this is the proposed
+ * interface:
+ *
+ * Recurring events are updated with a new 'when' field when they are processed
+ * (event on error).  Non-recurring events are just deleted after processing.
+ *
+ * The recurs field is a whitespace-delimited list of criteria:
+ * - nn:nn or nn:nn:nn define a time of day, in local time.  There must be
+ *   at least one of these but can be more than one.
+ * - a day name (monday, tuesday, ...) defines the days of the week on
+ *   which the event will recur.  There can be more than one.
+ * - a day number and month name (1 january, 5 february, ...) defines
+ *   the days of the year on which the event will recur.  There can be
+ *   more than one of these.
+ *
+ * Day and month names are case insensitive.  Multiple languages are
+ * likely to be supported, especially if people send me pointers to
+ * their month and day names.  Abbreviations are NOT supported, as
+ * there is more of a risk of a clash between different languages that
+ * way.
+ *
+ * If there are no week or year days then the event recurs every day.
+ *
+ * If there are both week and year days then the union of them is
+ * taken, rather than the intersection.
+ *
+ * TODO: support recurring events.
+ *
+ * TODO: add disorder-dump support
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <db.h>
+#include <time.h>
+#include <stddef.h>
+
+#include "trackdb.h"
+#include "trackdb-int.h"
+#include "schedule.h"
+#include "table.h"
+#include "kvp.h"
+#include "log.h"
+#include "queue.h"
+#include "server-queue.h"
+#include "state.h"
+#include "play.h"
+#include "mem.h"
+#include "random.h"
+#include "vector.h"
+
+static int schedule_trigger(ev_source *ev,
+                           const struct timeval *now,
+                           void *u);
+static int schedule_lookup(const char *id,
+                          struct kvp *actiondata);
+
+/** @brief List of required fields in a scheduled event */
+static const char *const schedule_required[] = {"when", "who", "action"};
+
+/** @brief Number of elements in @ref schedule_required */
+#define NREQUIRED (int)(sizeof schedule_required / sizeof *schedule_required)
+
+/** @brief Parse a scheduled event key and data
+ * @param k Pointer to key
+ * @param whenp Where to store timestamp
+ * @return 0 on success, non-0 on error
+ *
+ * Rejects entries that are invalid in various ways.
+ */
+static int schedule_parse(const DBT *k,
+                         const DBT *d,
+                         char **idp,
+                         struct kvp **actiondatap,
+                         time_t *whenp) {
+  char *id;
+  struct kvp *actiondata;
+  int n;
+
+  /* Reject bogus keys */
+  if(!k->size || k->size > 128) {
+    error(0, "bogus schedule.db key (%lu bytes)", (unsigned long)k->size);
+    return -1;
+  }
+  id = xstrndup(k->data, k->size);
+  actiondata = kvp_urldecode(d->data, d->size);
+  /* Reject items without the required fields */
+  for(n = 0; n < NREQUIRED; ++n) {
+    if(!kvp_get(actiondata, schedule_required[n])) {
+      error(0, "scheduled event %s: missing required field '%s'",
+           id, schedule_required[n]);
+      return -1;
+    }
+  }
+  /* Return the results */
+  if(idp)
+    *idp = id;
+  if(actiondatap)
+    *actiondatap = actiondata;
+  if(whenp)
+    *whenp = (time_t)atoll(kvp_get(actiondata, "when"));
+  return 0;
+}
+
+/** @brief Delete via a cursor
+ * @return 0 or @c DB_LOCK_DEADLOCK */
+static int cdel(DBC *cursor) {
+  int err;
+
+  switch(err = cursor->c_del(cursor, 0)) {
+  case 0:
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error deleting from schedule.db: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error deleting from schedule.db: %s", db_strerror(err));
+  }
+  return err;
+}
+
+/** @brief Initialize the schedule
+ * @param ev Event loop
+ * @param tid Transaction ID
+ *
+ * Sets a callback for all action times except for junk actions that are
+ * already in the past, which are discarded.
+ */
+static int schedule_init_tid(ev_source *ev,
+                            DB_TXN *tid) {
+  DBC *cursor;
+  DBT k, d;
+  int err;
+
+  cursor = trackdb_opencursor(trackdb_scheduledb, tid);
+  while(!(err = cursor->c_get(cursor, prepare_data(&k),  prepare_data(&d),
+                              DB_NEXT))) {
+    struct timeval when;
+    struct kvp *actiondata;
+    char *id;
+
+    /* Parse the key.  We destroy bogus entries on sight. */
+    if(schedule_parse(&k, &d, &id, &actiondata, &when.tv_sec)) {
+      if((err = cdel(cursor)))
+       goto deadlocked;
+      continue;
+    }
+    when.tv_usec = 0;
+    /* The action might be in the past */
+    if(when.tv_sec < time(0)) {
+      const char *priority = kvp_get(actiondata, "priority");
+
+      if(priority && !strcmp(priority, "junk")) {
+        /* Junk actions that are in the past are discarded during startup */
+       /* TODO recurring events should be handled differently here */
+       if(cdel(cursor))
+         goto deadlocked;
+        /* Skip this time */
+        continue;
+      }
+    }
+    /* Arrange a callback when the scheduled event is due */
+    ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id);
+  }
+  switch(err) {
+  case DB_NOTFOUND:
+    err = 0;
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error querying schedule.db: %s", db_strerror(err));
+    break;
+  default:
+    fatal(0, "error querying schedule.db: %s", db_strerror(err));
+  }
+deadlocked:
+  if(trackdb_closecursor(cursor))
+    err = DB_LOCK_DEADLOCK;
+  return err;
+}
+
+/** @brief Initialize the schedule
+ * @param ev Event loop
+ *
+ * Sets a callback for all action times except for junk actions that are
+ * already in the past, which are discarded.
+ */
+void schedule_init(ev_source *ev) {
+  int e;
+  WITH_TRANSACTION(schedule_init_tid(ev, tid));
+}
+
+/******************************************************************************/
+
+/** @brief Create a scheduled event
+ * @param ev Event loop
+ * @param actiondata Action data
+ *
+ * The caller should set the timeout themselves.
+ */
+static int schedule_add_tid(const char *id,
+                           struct kvp *actiondata,
+                           DB_TXN *tid) {
+  int err;
+  DBT k, d;
+
+  memset(&k, 0, sizeof k);
+  k.data = (void *)id;
+  k.size = strlen(id);
+  switch(err = trackdb_scheduledb->put(trackdb_scheduledb, tid, &k,
+                                       encode_data(&d, actiondata), 0)) {
+  case 0:
+    break;
+  case DB_LOCK_DEADLOCK:
+    error(0, "error updating schedule.db: %s", db_strerror(err));
+    return err;
+  case DB_KEYEXIST:
+    return err;
+  default:
+    fatal(0, "error updating schedule.db: %s", db_strerror(err));
+  }
+  return 0;
+}
+
+/** @brief Create a scheduled event
+ * @param ev Event loop
+ * @param actiondata Action actiondata
+ * @return Scheduled event ID or NULL on error
+ *
+ * Events are rejected if they lack the required fields, if the user
+ * is not allowed to perform them or if they are scheduled for a time
+ * in the past.
+ */
+char *schedule_add(ev_source *ev,
+                  struct kvp *actiondata) {
+  int e, n;
+  char *id;
+  struct timeval when;
+
+  /* TODO: handle recurring events */
+  /* Check that the required field are present */
+  for(n = 0; n < NREQUIRED; ++n) {
+    if(!kvp_get(actiondata, schedule_required[n])) {
+      error(0, "new scheduled event is missing required field '%s'",
+           schedule_required[n]);
+      return 0;
+    }
+  }
+  /* Check that the user is allowed to do whatever it is */
+  if(schedule_lookup("[new]", actiondata) < 0)
+    return 0;
+  when.tv_sec = atoll(kvp_get(actiondata, "when"));
+  when.tv_usec = 0;
+  /* Reject events in the past */
+  if(when.tv_sec <= time(0)) {
+    error(0, "new scheduled event is in the past");
+    return 0;
+  }
+  do {
+    id = random_id();
+    WITH_TRANSACTION(schedule_add_tid(id, actiondata, tid));
+  } while(e == DB_KEYEXIST);
+  ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id);
+  return id;
+}
+
+/******************************************************************************/
+
+/** @brief Get the action data for a scheduled event
+ * @param id Event ID
+ * @return Event data or NULL
+ */
+struct kvp *schedule_get(const char *id) {
+  int e, n;
+  struct kvp *actiondata;
+  
+  WITH_TRANSACTION(trackdb_getdata(trackdb_scheduledb, id, &actiondata, tid));
+  /* Check that the required field are present */
+  for(n = 0; n < NREQUIRED; ++n) {
+    if(!kvp_get(actiondata, schedule_required[n])) {
+      error(0, "scheduled event %s is missing required field '%s'",
+           id, schedule_required[n]);
+      return 0;
+    }
+  }
+  return actiondata;
+}
+
+/******************************************************************************/
+
+/** @brief Delete a scheduled event
+ * @param id Event to delete
+ * @return 0 on success, non-0 if it did not exist
+ */
+int schedule_del(const char *id) {
+  int e;
+
+  WITH_TRANSACTION(trackdb_delkey(trackdb_scheduledb, id, tid));
+  return e == 0 ? 0 : -1;
+}
+
+/******************************************************************************/
+
+/** @brief Get a list of scheduled events
+ * @param neventsp Where to put count of events (or NULL)
+ * @return 0-terminate list of ID strings
+ */
+char **schedule_list(int *neventsp) {
+  int e;
+  struct vector v[1];
+
+  vector_init(v);
+  WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
+  if(neventsp)
+    *neventsp = v->nvec;
+  return v->vec;
+}
+
+/******************************************************************************/
+
+static void schedule_play(ev_source *ev,
+                         const char *id,
+                         const char *who,
+                         struct kvp *actiondata) {
+  const char *track = kvp_get(actiondata, "track");
+  struct queue_entry *q;
+
+  /* This stuff has rather a lot in common with c_play() */
+  if(!track) {
+    error(0, "scheduled event %s: no track field", id);
+    return;
+  }
+  if(!trackdb_exists(track)) {
+    error(0, "scheduled event %s: no such track as %s", id, track);
+    return;
+  }
+  if(!(track = trackdb_resolve(track))) {
+    error(0, "scheduled event %s: cannot resolve track %s", id, track);
+    return;
+  }
+  info("scheduled event %s: %s play %s", id,  who, track);
+  q = queue_add(track, who, WHERE_START);
+  queue_write();
+  if(q == qhead.next && playing)
+    prepare(ev, q);
+  play(ev);
+}
+
+static void schedule_set_global(ev_source attribute((unused)) *ev,
+                               const char *id,
+                               const char *who,
+                               struct kvp *actiondata) {
+  const char *key = kvp_get(actiondata, "key");
+  const char *value = kvp_get(actiondata, "value");
+
+  if(!key) {
+    error(0, "scheduled event %s: no key field", id);
+    return;
+  }
+  if(key[0] == '_') {
+    error(0, "scheduled event %s: cannot set internal global preferences (%s)",
+         id, key);
+    return;
+  }
+  if(value)
+    info("scheduled event %s: %s set-global %s=%s", id, who, key, value);
+  else
+    info("scheduled event %s: %s set-global %s unset", id,  who, key);
+  trackdb_set_global(key, value, who);
+}
+
+/** @brief Table of schedule actions
+ *
+ * Must be kept sorted.
+ */
+static struct {
+  const char *name;
+  void (*callback)(ev_source *ev,
+                  const char *id, const char *who,
+                  struct kvp *actiondata);
+  rights_type right;
+} schedule_actions[] = {
+  { "play", schedule_play, RIGHT_PLAY },
+  { "set-global", schedule_set_global, RIGHT_GLOBAL_PREFS },
+};
+
+/** @brief Look up a scheduled event
+ * @param actiondata Event description
+ * @return index in schedule_actions[] on success, -1 on error
+ *
+ * Unknown events are rejected as are those that the user is not allowed to do.
+ */
+static int schedule_lookup(const char *id,
+                          struct kvp *actiondata) {
+  const char *who = kvp_get(actiondata, "who");
+  const char *action = kvp_get(actiondata, "action");
+  const char *rights;
+  struct kvp *userinfo;
+  rights_type r;
+  int n;
+
+  /* Look up the action */
+  n = TABLE_FIND(schedule_actions, typeof(schedule_actions[0]), name, action);
+  if(n < 0) {
+    error(0, "scheduled event %s: unrecognized action '%s'", id, action);
+    return -1;
+  }
+  /* Find the user */
+  if(!(userinfo = trackdb_getuserinfo(who))) {
+    error(0, "scheduled event %s: user '%s' does not exist", id, who);
+    return -1;
+  }
+  /* Check that they have suitable rights */
+  if(!(rights = kvp_get(userinfo, "rights"))) {
+    error(0, "scheduled event %s: user %s' has no rights???", id, who);
+    return -1;
+  }
+  if(parse_rights(rights, &r, 1)) {
+    error(0, "scheduled event %s: user %s has invalid rights '%s'",
+         id, who, rights);
+    return -1;
+  }
+  if(!(r & schedule_actions[n].right)) {
+    error(0, "scheduled event %s: user %s lacks rights for action %s",
+         id, who, action);
+    return -1;
+  }
+  return n;
+}
+
+/** @brief Called when an action is due */
+static int schedule_trigger(ev_source *ev,
+                           const struct timeval attribute((unused)) *now,
+                           void *u) {
+  const char *action, *id = u;
+  struct kvp *actiondata = schedule_get(id);
+  int n;
+
+  if(!actiondata)
+    return 0;
+  /* schedule_get() enforces these being present */
+  action = kvp_get(actiondata, "action");
+  /* Look up the action */
+  n = schedule_lookup(id, actiondata);
+  if(n < 0)
+    goto done;
+  /* Go ahead and do it */
+  schedule_actions[n].callback(ev, id, kvp_get(actiondata, "who"), actiondata);
+done:
+  /* TODO: rewrite recurring events for their next trigger time,
+   * rather than deleting them */
+  schedule_del(id);
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/schedule.h b/server/schedule.h
new file mode 100644 (file)
index 0000000..d68bafc
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef SCHEDULE_H
+#define SCHEDULE_H
+
+void schedule_init(ev_source *ev);
+char *schedule_add(ev_source *ev,
+                  struct kvp *actiondata);
+int schedule_del(const char *id);
+struct kvp *schedule_get(const char *id);
+char **schedule_list(int *neventsp);
+
+#endif /* SCHEDULE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 60cb869a8972bf85fc55a717e90358b0eb63be17..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
@@ -1506,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;
@@ -1566,6 +1666,10 @@ static const struct command {
   { "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, },
index f88067b80eb8dd7b094fc62793d22bf28d64f6e2..ad90425ff18f72ed776356c0fc1f5c600cb6f019 100644 (file)
@@ -46,6 +46,7 @@
 #include "server.h"
 #include "printf.h"
 #include "addr.h"
+#include "schedule.h"
 
 static const char *current_unix;
 static int current_unix_fd;
@@ -155,6 +156,8 @@ int reconfigure(ev_source *ev, int reload) {
     trackdb_open(TRACKDB_CAN_UPGRADE);
   if(need_another_rescan)
     trackdb_rescan(ev, 1/*check*/, 0, 0);
+  /* Arrange timeouts for schedule actions */
+  schedule_init(ev);
   if(!ret) {
     queue_read();
     recent_read();
index d21edcbdbd57fd0c106d7a10d44273dfd5df4524..9358b196b8609e0d8ce3ce8f1c3b7aa3a0f8ff7c 100644 (file)
@@ -27,7 +27,8 @@ disorder_udplog_LDADD=$(LIBOBJS) ../lib/libdisorder.a
 disorder_udplog_DEPENDENCIES=../lib/libdisorder.a
 
 TESTS=cookie.py dbversion.py dump.py files.py play.py queue.py \
-       recode.py search.py user-upgrade.py user.py aliases.py
+       recode.py search.py user-upgrade.py user.py aliases.py  \
+       schedule.py
 
 TESTS_ENVIRONMENT=${PYTHON} -u
 
diff --git a/tests/schedule.py b/tests/schedule.py
new file mode 100755 (executable)
index 0000000..a3feaa8
--- /dev/null
@@ -0,0 +1,168 @@
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2008 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+import dtest,disorder,time,string
+
+def test():
+    """Exercise schedule support"""
+    dtest.start_daemon()
+    dtest.create_user()
+    c = disorder.client()
+    c.random_disable()
+    dtest.rescan()
+    # Wait until there's no track playing
+    print " waiting for nothing to be playing"
+    while c.playing() is not None:
+        time.sleep(1)
+    now = int(time.time())
+    track = "%s/Joe Bloggs/First Album/05:Fifth track.ogg" % dtest.tracks
+    print " scheduling a track for the future"
+    c.schedule_add(now + 4, "normal", "play", track)
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
+    print " waiting for it to play"
+    waited = 0
+    p = c.playing()
+    while p is None and waited < 10:
+        time.sleep(1)
+        waited += 1
+        p = c.playing()
+    assert waited < 10, "checking track played within a reasonable period"
+    assert waited > 2, "checking track didn't play immediately"
+    assert p["track"] == track, "checking right track played"
+    assert c.schedule_list() == [], "checking schedule is empty"
+    print " waiting for nothing to be playing"
+    while c.playing() is not None:
+        time.sleep(1)
+    print " scheduling an enable-random for the future"
+    now = int(time.time())
+    c.schedule_add(now + 4, "normal", "set-global", "random-play", "yes")
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
+    print " waiting for it to take effect"
+    waited = 0
+    p = c.playing()
+    while p is None and waited < 10:
+        time.sleep(1)
+        waited += 1
+        p = c.playing()
+    assert waited < 10, "checking a track played within a reasonable period"
+    assert waited > 2, "checking a track didn't play immediately"
+    print " disabling random play"
+    c.random_disable()
+    print " waiting for nothing to be playing"
+    while c.playing() is not None:
+        time.sleep(1)
+    print " scheduling track to play later via command line"
+    now = int(time.time())
+    dtest.command(["disorder",
+                   "--config", disorder._configfile,
+                   "--no-per-user-config",
+                   "schedule-play",
+                   time.strftime("%Y-%m-%d %H:%M:%S",
+                                 time.localtime(now + 4)),
+                   "normal",
+                   track])
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
+    print " waiting for it to play"
+    waited = 0
+    p = c.playing()
+    while p is None and waited < 10:
+        time.sleep(1)
+        waited += 1
+        p = c.playing()
+    assert waited < 10, "checking track played within a reasonable period"
+    assert waited > 2, "checking track didn't play immediately"
+    assert p["track"] == track, "checking right track played"
+    assert c.schedule_list() == [], "checking schedule is empty"
+    print " waiting for nothing to be playing"
+    while c.playing() is not None:
+        time.sleep(1)
+    print " scheduling an enable-random for later via command line"
+    now = int(time.time())
+    dtest.command(["disorder",
+                   "--config", disorder._configfile,
+                   "--no-per-user-config",
+                   "schedule-set-global",
+                   time.strftime("%Y-%m-%d %H:%M:%S",
+                                 time.localtime(now + 4)),
+                   "normal",
+                   "random-play",
+                   "yes"])
+    print " disorder schedule-list output:"
+    print string.join(dtest.command(["disorder",
+                                     "--config", disorder._configfile,
+                                     "--no-per-user-config",
+                                     "schedule-list"]), ""),
+    print " waiting for it to take effect"
+    waited = 0
+    p = c.playing()
+    while p is None and waited < 10:
+        time.sleep(1)
+        waited += 1
+        p = c.playing()
+    assert waited < 10, "checking a track played within a reasonable period"
+    assert waited > 2, "checking a track didn't play immediately"
+    print " disabling random play"
+    c.random_disable()
+    print " waiting for nothing to be playing"
+    while c.playing() is not None:
+        time.sleep(1)
+    print " scheduling a track for the future"
+    now = int(time.time())
+    c.schedule_add(now + 4, "normal", "play", track)
+    print " schedule via python:"
+    s = c.schedule_list()
+    for event in s:
+        e = c.schedule_get(event)
+        print "item %s: %s" % (event, e)
+    print " deleting item %s" % s[0]
+    c.schedule_del(s[0])
+    print " checking it's really gone"
+    s = c.schedule_list()
+    assert s == [], "checking schedule is empty"
+    waited = 0
+    p = c.playing()
+    while p is None and waited < 10:
+        time.sleep(1)
+        waited += 1
+        p = c.playing()
+    assert p is None, "checking deleted scheduled event did not run"
+    print " checking you can't schedule events for the past"
+    try:
+        now = int(time.time())
+        c.schedule_add(now - 4, "normal", "play", track)
+        assert False, "checking schedule_add failed"
+    except disorder.operationError:
+      pass                              # good
+        
+
+if __name__ == '__main__':
+    dtest.run()