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