From: Richard Kettlewell Date: Sun, 25 May 2008 11:57:20 +0000 (+0100) Subject: Merge event scheduling implementation. This fixes defect #6, X-Git-Tag: 4.0~65 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/b9bbb6c8bd8630fe4321045ce57174c4890bfa46?hp=0ddbd899ed1c089dba7bac4c925ae1451120ea63 Merge event scheduling implementation. This fixes defect #6, "Schedule tracks for a particular time". --- diff --git a/.bzrignore b/.bzrignore index d14bd69..6e03203 100644 --- a/.bzrignore +++ b/.bzrignore @@ -190,3 +190,4 @@ doc/disorder_templates.5.in lib/t-arcfour lib/t-charset lib/t-event +lib/t-dateparse diff --git a/CHANGES b/CHANGES index 2d6c87d..d484c58 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,13 @@ The 'gap' directive will no longer work. It could be restored if there is real demand. +*** Event Scheduling + +It is now possible to schedule events to occur in the future. Currently +the supported actions are playing a specific track, and changing a +global preference (thus allowing e.g. random play to be turned on or +off). See the schedule-* commands described in disorder(1). + *** Random Track Choice This has been completely rewritten to support new features: @@ -47,6 +54,7 @@ play as well as the local default sound device. ** Bugs Fixed #2 Search results should link to directories + #6 Schedule tracks for a particular time #10 Non-uniform track selection #11 Bias random selection to newly added tracks #16 Cookie expiry causes user to be silently logged out and not diff --git a/README b/README index 5f09fc2..27657db 100644 --- 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) diff --git a/clients/disorder.c b/clients/disorder.c index 2088bc4..0dd71c7 100644 --- a/clients/disorder.c +++ b/clients/disorder.c @@ -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(); diff --git a/configure.ac b/configure.ac index 3c13dac..bfed8b5 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/doc/disorder.1.in b/doc/disorder.1.in index bd22e56..9de40a9 100644 --- a/doc/disorder.1.in +++ b/doc/disorder.1.in @@ -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 diff --git a/doc/disorder_protocol.5.in b/doc/disorder_protocol.5.in index 0847050..f4c5b2f 100644 --- a/doc/disorder_protocol.5.in +++ b/doc/disorder_protocol.5.in @@ -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. diff --git a/lib/Makefile.am b/lib/Makefile.am index 6b7aec8..16cdcea 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -19,10 +19,10 @@ # 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 diff --git a/lib/client.c b/lib/client.c index 4dfd10c..d357a8c 100644 --- a/lib/client.c +++ b/lib/client.c @@ -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 diff --git a/lib/client.h b/lib/client.h index 674272c..abadef6 100644 --- a/lib/client.h +++ b/lib/client.h @@ -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 index 0000000..45de40c --- /dev/null +++ b/lib/dateparse.c @@ -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 +#include "types.h" + +#include + +#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 index 0000000..2f52dac --- /dev/null +++ b/lib/dateparse.h @@ -0,0 +1,41 @@ +/* + * 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); +#if 0 +struct tm *xgetdate(const char *string, + const char *const *template); +#endif +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: +*/ diff --git a/lib/event.h b/lib/event.h index 34a6f4d..c666d81 100644 --- a/lib/event.h +++ b/lib/event.h @@ -21,6 +21,8 @@ #ifndef EVENT_H #define EVENT_H +#include + typedef struct ev_source ev_source; struct rusage; diff --git a/lib/random.c b/lib/random.c index 70e19f8..69e1409 100644 --- a/lib/random.c +++ b/lib/random.c @@ -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, sizeof words / sizeof *words, id, sizeof id, 62); + return xstrdup(id); +} + /* Local Variables: c-basic-offset:2 diff --git a/lib/random.h b/lib/random.h index f125e3c..2451589 100644 --- a/lib/random.h +++ b/lib/random.h @@ -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 index 0000000..1e07c74 --- /dev/null +++ b/lib/t-dateparse.c @@ -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 +#include "dateparse.h" + +static void check_date(time_t when, + const char *fmt, + struct tm *(*convert)(const time_t *)) { + char buffer[128]; + time_t parsed; + + strftime(buffer, sizeof buffer, fmt, convert(&when)); + parsed = dateparse(buffer); + check_integer(parsed, when); + if(parsed != when) + fprintf(stderr, "format=%s formatted=%s\n", fmt, 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: +*/ diff --git a/lib/trackdb-int.h b/lib/trackdb-int.h index ac2d3ea..1cfadbc 100644 --- a/lib/trackdb-int.h +++ b/lib/trackdb-int.h @@ -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 */ diff --git a/lib/trackdb.c b/lib/trackdb.c index 9125c0c..7cc3719 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -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; diff --git a/lib/trackdb.h b/lib/trackdb.h index f97ee5f..e3d61b5 100644 --- a/lib/trackdb.h +++ b/lib/trackdb.h @@ -23,7 +23,10 @@ #ifndef TRACKDB_H #define TRACKDB_H -struct ev_source; +#include + +#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 index 0000000..9d8b492 --- /dev/null +++ b/lib/xgetdate.c @@ -0,0 +1,228 @@ +/* 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 , 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. */ + +#define _GNU_SOURCE 1 /* to expose strptime */ +#include +#include +#include +#include +#include + +#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; +} + + +#if 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) + { + xgetdate_err = errval; + return NULL; + } + + return &tmbuf; +} +#endif diff --git a/python/disorder.py.in b/python/disorder.py.in index ef33106..3cc300c 100644 --- a/python/disorder.py.in +++ b/python/disorder.py.in @@ -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 diff --git a/scripts/completion.bash b/scripts/completion.bash index 8aff361..211e79a 100644 --- a/scripts/completion.bash +++ b/scripts/completion.bash @@ -32,7 +32,8 @@ complete -o default \ search set set-volume shutdown stats unset version resolve part pause resume scratch-id get-global set-global unset-global tags new rtp-address adduser users edituser deluser userinfo - setup-guest + setup-guest schedule-del schedule-list + schedule-set-global schedule-unset-global schedule-play -h --help -H --help-commands --version -V --config -c --length --debug -d" \ disorder diff --git a/server/Makefile.am b/server/Makefile.am index 9ad35b2..a2b5acf 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -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/dump.c b/server/dump.c index 18a2f6e..7b4b3a4 100644 --- a/server/dump.c +++ b/server/dump.c @@ -152,6 +152,24 @@ static void do_dump(FILE *fp, const char *tag, } if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } cursor = 0; + + /* dump the schedule */ + cursor = trackdb_opencursor(trackdb_scheduledb, tid); + err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), + DB_FIRST); + while(err == 0) { + if(fputc('W', fp) < 0 + || urlencode(s, k.data, k.size) + || fputc('\n', fp) < 0 + || urlencode(s, d.data, d.size) + || fputc('\n', fp) < 0) + fatal(errno, "error writing to %s", tag); + err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d), + DB_NEXT); + } + if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; } + cursor = 0; + if(tracksdb) { cursor = trackdb_opencursor(trackdb_tracksdb, tid); @@ -298,6 +316,7 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) { if((err = truncdb(tid, trackdb_searchdb))) return err; if((err = truncdb(tid, trackdb_tagsdb))) return err; if((err = truncdb(tid, trackdb_usersdb))) return err; + if((err = truncdb(tid, trackdb_scheduledb))) return err; c = getc(fp); while(!ferror(fp) && !feof(fp)) { switch(c) { @@ -311,6 +330,7 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) { case 'P': case 'G': case 'U': + case 'W': switch(c) { case 'P': which_db = trackdb_prefsdb; @@ -324,13 +344,17 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) { which_db = trackdb_usersdb; which_name = "users.db"; break; + case 'W': /* for 'when' */ + which_db = trackdb_scheduledb; + which_name = "scheduledb.db"; + break; default: abort(); } if(undump_dbt(fp, tag, prepare_data(&k)) || undump_dbt(fp, tag, prepare_data(&d))) break; - switch(err = trackdb_prefsdb->put(which_db, tid, &k, &d, 0)) { + switch(err = which_db->put(which_db, tid, &k, &d, 0)) { case 0: break; case DB_LOCK_DEADLOCK: diff --git a/server/schedule.c b/server/schedule.c new file mode 100644 index 0000000..a932f2d --- /dev/null +++ b/server/schedule.c @@ -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 +#include "types.h" + +#include +#include +#include +#include + +#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 */ + info("junk event %s is in the past, discarding", id); + 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 + */ +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), + DB_NOOVERWRITE)) { + 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. + */ +const char *schedule_add(ev_source *ev, + struct kvp *actiondata) { + int e, n; + const 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, (void *)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 index 0000000..0c7efa1 --- /dev/null +++ b/server/schedule.h @@ -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); +const 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: +*/ diff --git a/server/server.c b/server/server.c index 60cb869..5df2d33 100644 --- a/server/server.c +++ b/server/server.c @@ -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; + const 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, }, diff --git a/server/state.c b/server/state.c index f88067b..ad90425 100644 --- a/server/state.c +++ b/server/state.c @@ -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(); diff --git a/tests/Makefile.am b/tests/Makefile.am index d21edcb..9358b19 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -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 index 0000000..32532f1 --- /dev/null +++ b/tests/schedule.py @@ -0,0 +1,185 @@ +#! /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 now(): + """Return the current time in whole seconds""" + return int(time.time()) + +def next_playing(c): + print " waiting for track to play" + p = c.playing() + waited = 0 + while p is None and waited < 10: + time.sleep(1) + print " ." + p = c.playing() + assert waited < 10, "track played in a reasonable time" + return p + +def wait_idle(c): + print " waiting for nothing to be playing" + p = c.playing() + waited = 0 + while p is not None and waited < 20: + time.sleep(1) + print " ." + p = c.playing() + assert waited < 20, "idled in a reasonable time" + +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) + print " ." + track = "%s/Joe Bloggs/First Album/05:Fifth track.ogg" % dtest.tracks + print " scheduling a track for the future" + when = now() + 3 + c.schedule_add(when, "normal", "play", track) + print " disorder schedule-list output:" + print string.join(dtest.command(["disorder", + "--config", disorder._configfile, + "--no-per-user-config", + "schedule-list"]), ""), + p = next_playing(c) + assert p["track"] == track, "checking right track played" + assert int(p["when"]) >= when, "checking track played at right time" + assert c.schedule_list() == [], "checking schedule is empty" + wait_idle(c) + print " scheduling an enable-random for the future" + c.schedule_add(now() + 3, "junk", "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"]), ""), + next_playing(c) + print " disabling random play" + c.random_disable() + wait_idle(c) + print " scheduling track to play later via command line" + when = now() + 3 + dtest.command(["disorder", + "--config", disorder._configfile, + "--no-per-user-config", + "schedule-play", + time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(when)), + "normal", + track]) + print " disorder schedule-list output:" + print string.join(dtest.command(["disorder", + "--config", disorder._configfile, + "--no-per-user-config", + "schedule-list"]), ""), + p = next_playing(c) + assert p["track"] == track, "checking right track played" + assert p["when"] >= when, "checking track played at right time" + assert c.schedule_list() == [], "checking schedule is empty" + wait_idle(c) + print " scheduling an enable-random for later via command line" + dtest.command(["disorder", + "--config", disorder._configfile, + "--no-per-user-config", + "schedule-set-global", + time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(now() + 3)), + "normal", + "random-play", + "yes"]) + print " disorder schedule-list output:" + print string.join(dtest.command(["disorder", + "--config", disorder._configfile, + "--no-per-user-config", + "schedule-list"]), ""), + p = next_playing(c) + print " disabling random play" + c.random_disable() + print " waiting for nothing to be playing" + while c.playing() is not None: + time.sleep(1) + print " ." + print " scheduling a track for the future" + c.schedule_add(now() + 3, "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 < 5: + time.sleep(1) + print " ." + 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: + c.schedule_add(now() - 4, "normal", "play", track) + assert False, "checking schedule_add failed" + except disorder.operationError: + pass # good + print " checking scheduled events survive restarts" + when = now() + 3 + c.schedule_add(when, "normal", "play", track) + dtest.stop_daemon() + print " dumping database" + dump = "%s/dumpfile" % dtest.testroot + print dtest.command(["disorder-dump", "--config", disorder._configfile, + "--dump", dump]) + print "restoring database" + print dtest.command(["disorder-dump", "--config", disorder._configfile, + "--undump", dump]) + dtest.start_daemon() + c = disorder.client() + p = next_playing(c) + print " waiting for track to play" + assert p["track"] == track, "checking right track played" + assert p["when"] >= when, "checking track played at right time" + assert c.schedule_list() == [], "checking schedule is empty" + print " checking junk events do not survive restarts" + c.schedule_add(now() + 2, "junk", "play", track) + s = c.schedule_list() + print s + dtest.stop_daemon() + time.sleep(3) + dtest.start_daemon() + c = disorder.client() + print " checking schedule is empty" + s = c.schedule_list() + print s + assert s == [], "checking schedule is empty" + +if __name__ == '__main__': + dtest.run()