chiark / gitweb /
Fix random_id(). Oops.
[disorder] / server / schedule.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2008 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20
21 /** @file server/schedule.c
22  * @brief Scheduled events
23  *
24  * @ref trackdb_scheduledb is a mapping from ID strings to encoded
25  * key-value pairs called 'actiondata'.
26  *
27  * Possible actiondata keys are:
28  * - @b when: when to perform this action (required)
29  * - @b who: originator for action (required)
30  * - @b action: action to perform (required)
31  * - @b track: for @c action=play, the track to play
32  * - @b key: for @c action=set-global, the global pref to set
33  * - @b value: for @c action=set-global, the value to set (omit to unset)
34  * - @b priority: the importance of this action
35  * - @b recurs: how the event recurs; NOT IMPLEMENTED
36  * - ...others to be defined
37  *
38  * Possible actions are:
39  * - @b play: play a track
40  * - @b set-global: set or unset a global pref
41  * - ...others to be defined
42  *
43  * Possible priorities are:
44  * - @b junk: junk actions that are in the past at startup are discarded
45  * - @b normal: normal actions that are in the past at startup are run
46  *   immediately.  (This the default.)
47  * - ...others to be defined
48  *
49  * On startup the schedule database is read and a timeout set on the event loop
50  * for each action.  Similarly when an action is added, a timeout is set on the
51  * event loop.  The timeout has the ID attached as user data so that the action
52  * can easily be found again.
53  *
54  * Recurring events are NOT IMPLEMENTED yet but this is the proposed
55  * interface:
56  *
57  * Recurring events are updated with a new 'when' field when they are processed
58  * (event on error).  Non-recurring events are just deleted after processing.
59  *
60  * The recurs field is a whitespace-delimited list of criteria:
61  * - nn:nn or nn:nn:nn define a time of day, in local time.  There must be
62  *   at least one of these but can be more than one.
63  * - a day name (monday, tuesday, ...) defines the days of the week on
64  *   which the event will recur.  There can be more than one.
65  * - a day number and month name (1 january, 5 february, ...) defines
66  *   the days of the year on which the event will recur.  There can be
67  *   more than one of these.
68  *
69  * Day and month names are case insensitive.  Multiple languages are
70  * likely to be supported, especially if people send me pointers to
71  * their month and day names.  Abbreviations are NOT supported, as
72  * there is more of a risk of a clash between different languages that
73  * way.
74  *
75  * If there are no week or year days then the event recurs every day.
76  *
77  * If there are both week and year days then the union of them is
78  * taken, rather than the intersection.
79  *
80  * TODO: support recurring events.
81  *
82  * TODO: add disorder-dump support
83  */
84
85 #include <config.h>
86 #include "types.h"
87
88 #include <string.h>
89 #include <db.h>
90 #include <time.h>
91 #include <stddef.h>
92
93 #include "trackdb.h"
94 #include "trackdb-int.h"
95 #include "schedule.h"
96 #include "table.h"
97 #include "kvp.h"
98 #include "log.h"
99 #include "queue.h"
100 #include "server-queue.h"
101 #include "state.h"
102 #include "play.h"
103 #include "mem.h"
104 #include "random.h"
105 #include "vector.h"
106
107 static int schedule_trigger(ev_source *ev,
108                             const struct timeval *now,
109                             void *u);
110 static int schedule_lookup(const char *id,
111                            struct kvp *actiondata);
112
113 /** @brief List of required fields in a scheduled event */
114 static const char *const schedule_required[] = {"when", "who", "action"};
115
116 /** @brief Number of elements in @ref schedule_required */
117 #define NREQUIRED (int)(sizeof schedule_required / sizeof *schedule_required)
118
119 /** @brief Parse a scheduled event key and data
120  * @param k Pointer to key
121  * @param whenp Where to store timestamp
122  * @return 0 on success, non-0 on error
123  *
124  * Rejects entries that are invalid in various ways.
125  */
126 static int schedule_parse(const DBT *k,
127                           const DBT *d,
128                           char **idp,
129                           struct kvp **actiondatap,
130                           time_t *whenp) {
131   char *id;
132   struct kvp *actiondata;
133   int n;
134
135   /* Reject bogus keys */
136   if(!k->size || k->size > 128) {
137     error(0, "bogus schedule.db key (%lu bytes)", (unsigned long)k->size);
138     return -1;
139   }
140   id = xstrndup(k->data, k->size);
141   actiondata = kvp_urldecode(d->data, d->size);
142   /* Reject items without the required fields */
143   for(n = 0; n < NREQUIRED; ++n) {
144     if(!kvp_get(actiondata, schedule_required[n])) {
145       error(0, "scheduled event %s: missing required field '%s'",
146             id, schedule_required[n]);
147       return -1;
148     }
149   }
150   /* Return the results */
151   if(idp)
152     *idp = id;
153   if(actiondatap)
154     *actiondatap = actiondata;
155   if(whenp)
156     *whenp = (time_t)atoll(kvp_get(actiondata, "when"));
157   return 0;
158 }
159
160 /** @brief Delete via a cursor
161  * @return 0 or @c DB_LOCK_DEADLOCK */
162 static int cdel(DBC *cursor) {
163   int err;
164
165   switch(err = cursor->c_del(cursor, 0)) {
166   case 0:
167     break;
168   case DB_LOCK_DEADLOCK:
169     error(0, "error deleting from schedule.db: %s", db_strerror(err));
170     break;
171   default:
172     fatal(0, "error deleting from schedule.db: %s", db_strerror(err));
173   }
174   return err;
175 }
176
177 /** @brief Initialize the schedule
178  * @param ev Event loop
179  * @param tid Transaction ID
180  *
181  * Sets a callback for all action times except for junk actions that are
182  * already in the past, which are discarded.
183  */
184 static int schedule_init_tid(ev_source *ev,
185                              DB_TXN *tid) {
186   DBC *cursor;
187   DBT k, d;
188   int err;
189
190   cursor = trackdb_opencursor(trackdb_scheduledb, tid);
191   while(!(err = cursor->c_get(cursor, prepare_data(&k),  prepare_data(&d),
192                               DB_NEXT))) {
193     struct timeval when;
194     struct kvp *actiondata;
195     char *id;
196
197     /* Parse the key.  We destroy bogus entries on sight. */
198     if(schedule_parse(&k, &d, &id, &actiondata, &when.tv_sec)) {
199       if((err = cdel(cursor)))
200         goto deadlocked;
201       continue;
202     }
203     when.tv_usec = 0;
204     /* The action might be in the past */
205     if(when.tv_sec < time(0)) {
206       const char *priority = kvp_get(actiondata, "priority");
207
208       if(priority && !strcmp(priority, "junk")) {
209         /* Junk actions that are in the past are discarded during startup */
210         /* TODO recurring events should be handled differently here */
211         if(cdel(cursor))
212           goto deadlocked;
213         /* Skip this time */
214         continue;
215       }
216     }
217     /* Arrange a callback when the scheduled event is due */
218     ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id);
219   }
220   switch(err) {
221   case DB_NOTFOUND:
222     err = 0;
223     break;
224   case DB_LOCK_DEADLOCK:
225     error(0, "error querying schedule.db: %s", db_strerror(err));
226     break;
227   default:
228     fatal(0, "error querying schedule.db: %s", db_strerror(err));
229   }
230 deadlocked:
231   if(trackdb_closecursor(cursor))
232     err = DB_LOCK_DEADLOCK;
233   return err;
234 }
235
236 /** @brief Initialize the schedule
237  * @param ev Event loop
238  *
239  * Sets a callback for all action times except for junk actions that are
240  * already in the past, which are discarded.
241  */
242 void schedule_init(ev_source *ev) {
243   int e;
244   WITH_TRANSACTION(schedule_init_tid(ev, tid));
245 }
246
247 /******************************************************************************/
248
249 /** @brief Create a scheduled event
250  * @param ev Event loop
251  * @param actiondata Action data
252  */
253 static int schedule_add_tid(const char *id,
254                             struct kvp *actiondata,
255                             DB_TXN *tid) {
256   int err;
257   DBT k, d;
258
259   memset(&k, 0, sizeof k);
260   k.data = (void *)id;
261   k.size = strlen(id);
262   switch(err = trackdb_scheduledb->put(trackdb_scheduledb, tid, &k,
263                                        encode_data(&d, actiondata),
264                                        DB_NOOVERWRITE)) {
265   case 0:
266     break;
267   case DB_LOCK_DEADLOCK:
268     error(0, "error updating schedule.db: %s", db_strerror(err));
269     return err;
270   case DB_KEYEXIST:
271     return err;
272   default:
273     fatal(0, "error updating schedule.db: %s", db_strerror(err));
274   }
275   return 0;
276 }
277
278 /** @brief Create a scheduled event
279  * @param ev Event loop
280  * @param actiondata Action actiondata
281  * @return Scheduled event ID or NULL on error
282  *
283  * Events are rejected if they lack the required fields, if the user
284  * is not allowed to perform them or if they are scheduled for a time
285  * in the past.
286  */
287 const char *schedule_add(ev_source *ev,
288                          struct kvp *actiondata) {
289   int e, n;
290   const char *id;
291   struct timeval when;
292
293   /* TODO: handle recurring events */
294   /* Check that the required field are present */
295   for(n = 0; n < NREQUIRED; ++n) {
296     if(!kvp_get(actiondata, schedule_required[n])) {
297       error(0, "new scheduled event is missing required field '%s'",
298             schedule_required[n]);
299       return 0;
300     }
301   }
302   /* Check that the user is allowed to do whatever it is */
303   if(schedule_lookup("[new]", actiondata) < 0)
304     return 0;
305   when.tv_sec = atoll(kvp_get(actiondata, "when"));
306   when.tv_usec = 0;
307   /* Reject events in the past */
308   if(when.tv_sec <= time(0)) {
309     error(0, "new scheduled event is in the past");
310     return 0;
311   }
312   do {
313     id = random_id();
314     WITH_TRANSACTION(schedule_add_tid(id, actiondata, tid));
315   } while(e == DB_KEYEXIST);
316   ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, (void *)id);
317   return id;
318 }
319
320 /******************************************************************************/
321
322 /** @brief Get the action data for a scheduled event
323  * @param id Event ID
324  * @return Event data or NULL
325  */
326 struct kvp *schedule_get(const char *id) {
327   int e, n;
328   struct kvp *actiondata;
329   
330   WITH_TRANSACTION(trackdb_getdata(trackdb_scheduledb, id, &actiondata, tid));
331   /* Check that the required field are present */
332   for(n = 0; n < NREQUIRED; ++n) {
333     if(!kvp_get(actiondata, schedule_required[n])) {
334       error(0, "scheduled event %s is missing required field '%s'",
335             id, schedule_required[n]);
336       return 0;
337     }
338   }
339   return actiondata;
340 }
341
342 /******************************************************************************/
343
344 /** @brief Delete a scheduled event
345  * @param id Event to delete
346  * @return 0 on success, non-0 if it did not exist
347  */
348 int schedule_del(const char *id) {
349   int e;
350
351   WITH_TRANSACTION(trackdb_delkey(trackdb_scheduledb, id, tid));
352   return e == 0 ? 0 : -1;
353 }
354
355 /******************************************************************************/
356
357 /** @brief Get a list of scheduled events
358  * @param neventsp Where to put count of events (or NULL)
359  * @return 0-terminate list of ID strings
360  */
361 char **schedule_list(int *neventsp) {
362   int e;
363   struct vector v[1];
364
365   vector_init(v);
366   WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
367   if(neventsp)
368     *neventsp = v->nvec;
369   return v->vec;
370 }
371
372 /******************************************************************************/
373
374 static void schedule_play(ev_source *ev,
375                           const char *id,
376                           const char *who,
377                           struct kvp *actiondata) {
378   const char *track = kvp_get(actiondata, "track");
379   struct queue_entry *q;
380
381   /* This stuff has rather a lot in common with c_play() */
382   if(!track) {
383     error(0, "scheduled event %s: no track field", id);
384     return;
385   }
386   if(!trackdb_exists(track)) {
387     error(0, "scheduled event %s: no such track as %s", id, track);
388     return;
389   }
390   if(!(track = trackdb_resolve(track))) {
391     error(0, "scheduled event %s: cannot resolve track %s", id, track);
392     return;
393   }
394   info("scheduled event %s: %s play %s", id,  who, track);
395   q = queue_add(track, who, WHERE_START);
396   queue_write();
397   if(q == qhead.next && playing)
398     prepare(ev, q);
399   play(ev);
400 }
401
402 static void schedule_set_global(ev_source attribute((unused)) *ev,
403                                 const char *id,
404                                 const char *who,
405                                 struct kvp *actiondata) {
406   const char *key = kvp_get(actiondata, "key");
407   const char *value = kvp_get(actiondata, "value");
408
409   if(!key) {
410     error(0, "scheduled event %s: no key field", id);
411     return;
412   }
413   if(key[0] == '_') {
414     error(0, "scheduled event %s: cannot set internal global preferences (%s)",
415           id, key);
416     return;
417   }
418   if(value)
419     info("scheduled event %s: %s set-global %s=%s", id, who, key, value);
420   else
421     info("scheduled event %s: %s set-global %s unset", id,  who, key);
422   trackdb_set_global(key, value, who);
423 }
424
425 /** @brief Table of schedule actions
426  *
427  * Must be kept sorted.
428  */
429 static struct {
430   const char *name;
431   void (*callback)(ev_source *ev,
432                    const char *id, const char *who,
433                    struct kvp *actiondata);
434   rights_type right;
435 } schedule_actions[] = {
436   { "play", schedule_play, RIGHT_PLAY },
437   { "set-global", schedule_set_global, RIGHT_GLOBAL_PREFS },
438 };
439
440 /** @brief Look up a scheduled event
441  * @param actiondata Event description
442  * @return index in schedule_actions[] on success, -1 on error
443  *
444  * Unknown events are rejected as are those that the user is not allowed to do.
445  */
446 static int schedule_lookup(const char *id,
447                            struct kvp *actiondata) {
448   const char *who = kvp_get(actiondata, "who");
449   const char *action = kvp_get(actiondata, "action");
450   const char *rights;
451   struct kvp *userinfo;
452   rights_type r;
453   int n;
454
455   /* Look up the action */
456   n = TABLE_FIND(schedule_actions, typeof(schedule_actions[0]), name, action);
457   if(n < 0) {
458     error(0, "scheduled event %s: unrecognized action '%s'", id, action);
459     return -1;
460   }
461   /* Find the user */
462   if(!(userinfo = trackdb_getuserinfo(who))) {
463     error(0, "scheduled event %s: user '%s' does not exist", id, who);
464     return -1;
465   }
466   /* Check that they have suitable rights */
467   if(!(rights = kvp_get(userinfo, "rights"))) {
468     error(0, "scheduled event %s: user %s' has no rights???", id, who);
469     return -1;
470   }
471   if(parse_rights(rights, &r, 1)) {
472     error(0, "scheduled event %s: user %s has invalid rights '%s'",
473           id, who, rights);
474     return -1;
475   }
476   if(!(r & schedule_actions[n].right)) {
477     error(0, "scheduled event %s: user %s lacks rights for action %s",
478           id, who, action);
479     return -1;
480   }
481   return n;
482 }
483
484 /** @brief Called when an action is due */
485 static int schedule_trigger(ev_source *ev,
486                             const struct timeval attribute((unused)) *now,
487                             void *u) {
488   const char *action, *id = u;
489   struct kvp *actiondata = schedule_get(id);
490   int n;
491
492   if(!actiondata)
493     return 0;
494   /* schedule_get() enforces these being present */
495   action = kvp_get(actiondata, "action");
496   /* Look up the action */
497   n = schedule_lookup(id, actiondata);
498   if(n < 0)
499     goto done;
500   /* Go ahead and do it */
501   schedule_actions[n].callback(ev, id, kvp_get(actiondata, "who"), actiondata);
502 done:
503   /* TODO: rewrite recurring events for their next trigger time,
504    * rather than deleting them */
505   schedule_del(id);
506   return 0;
507 }
508
509 /*
510 Local Variables:
511 c-basic-offset:2
512 comment-column:40
513 fill-column:79
514 indent-tabs-mode:nil
515 End:
516 */