chiark / gitweb /
Make postinst group modification call less quiet.
[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 #include "disorder-server.h"
85
86 static int schedule_trigger(ev_source *ev,
87                             const struct timeval *now,
88                             void *u);
89 static int schedule_lookup(const char *id,
90                            struct kvp *actiondata);
91
92 /** @brief List of required fields in a scheduled event */
93 static const char *const schedule_required[] = {"when", "who", "action"};
94
95 /** @brief Number of elements in @ref schedule_required */
96 #define NREQUIRED (int)(sizeof schedule_required / sizeof *schedule_required)
97
98 /** @brief Parse a scheduled event key and data
99  * @param k Pointer to key
100  * @param whenp Where to store timestamp
101  * @return 0 on success, non-0 on error
102  *
103  * Rejects entries that are invalid in various ways.
104  */
105 static int schedule_parse(const DBT *k,
106                           const DBT *d,
107                           char **idp,
108                           struct kvp **actiondatap,
109                           time_t *whenp) {
110   char *id;
111   struct kvp *actiondata;
112   int n;
113
114   /* Reject bogus keys */
115   if(!k->size || k->size > 128) {
116     error(0, "bogus schedule.db key (%lu bytes)", (unsigned long)k->size);
117     return -1;
118   }
119   id = xstrndup(k->data, k->size);
120   actiondata = kvp_urldecode(d->data, d->size);
121   /* Reject items without the required fields */
122   for(n = 0; n < NREQUIRED; ++n) {
123     if(!kvp_get(actiondata, schedule_required[n])) {
124       error(0, "scheduled event %s: missing required field '%s'",
125             id, schedule_required[n]);
126       return -1;
127     }
128   }
129   /* Return the results */
130   if(idp)
131     *idp = id;
132   if(actiondatap)
133     *actiondatap = actiondata;
134   if(whenp)
135     *whenp = (time_t)atoll(kvp_get(actiondata, "when"));
136   return 0;
137 }
138
139 /** @brief Delete via a cursor
140  * @return 0 or @c DB_LOCK_DEADLOCK */
141 static int cdel(DBC *cursor) {
142   int err;
143
144   switch(err = cursor->c_del(cursor, 0)) {
145   case 0:
146     break;
147   case DB_LOCK_DEADLOCK:
148     error(0, "error deleting from schedule.db: %s", db_strerror(err));
149     break;
150   default:
151     fatal(0, "error deleting from schedule.db: %s", db_strerror(err));
152   }
153   return err;
154 }
155
156 /** @brief Initialize the schedule
157  * @param ev Event loop
158  * @param tid Transaction ID
159  *
160  * Sets a callback for all action times except for junk actions that are
161  * already in the past, which are discarded.
162  */
163 static int schedule_init_tid(ev_source *ev,
164                              DB_TXN *tid) {
165   DBC *cursor;
166   DBT k, d;
167   int err;
168
169   cursor = trackdb_opencursor(trackdb_scheduledb, tid);
170   while(!(err = cursor->c_get(cursor, prepare_data(&k),  prepare_data(&d),
171                               DB_NEXT))) {
172     struct timeval when;
173     struct kvp *actiondata;
174     char *id;
175
176     /* Parse the key.  We destroy bogus entries on sight. */
177     if(schedule_parse(&k, &d, &id, &actiondata, &when.tv_sec)) {
178       if((err = cdel(cursor)))
179         goto deadlocked;
180       continue;
181     }
182     when.tv_usec = 0;
183     /* The action might be in the past */
184     if(when.tv_sec < time(0)) {
185       const char *priority = kvp_get(actiondata, "priority");
186
187       if(priority && !strcmp(priority, "junk")) {
188         /* Junk actions that are in the past are discarded during startup */
189         /* TODO recurring events should be handled differently here */
190         info("junk event %s is in the past, discarding", id);
191         if(cdel(cursor))
192           goto deadlocked;
193         /* Skip this time */
194         continue;
195       }
196     }
197     /* Arrange a callback when the scheduled event is due */
198     ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id);
199   }
200   switch(err) {
201   case DB_NOTFOUND:
202     err = 0;
203     break;
204   case DB_LOCK_DEADLOCK:
205     error(0, "error querying schedule.db: %s", db_strerror(err));
206     break;
207   default:
208     fatal(0, "error querying schedule.db: %s", db_strerror(err));
209   }
210 deadlocked:
211   if(trackdb_closecursor(cursor))
212     err = DB_LOCK_DEADLOCK;
213   return err;
214 }
215
216 /** @brief Initialize the schedule
217  * @param ev Event loop
218  *
219  * Sets a callback for all action times except for junk actions that are
220  * already in the past, which are discarded.
221  */
222 void schedule_init(ev_source *ev) {
223   int e;
224   WITH_TRANSACTION(schedule_init_tid(ev, tid));
225 }
226
227 /******************************************************************************/
228
229 /** @brief Create a scheduled event
230  * @param ev Event loop
231  * @param actiondata Action data
232  */
233 static int schedule_add_tid(const char *id,
234                             struct kvp *actiondata,
235                             DB_TXN *tid) {
236   int err;
237   DBT k, d;
238
239   memset(&k, 0, sizeof k);
240   k.data = (void *)id;
241   k.size = strlen(id);
242   switch(err = trackdb_scheduledb->put(trackdb_scheduledb, tid, &k,
243                                        encode_data(&d, actiondata),
244                                        DB_NOOVERWRITE)) {
245   case 0:
246     break;
247   case DB_LOCK_DEADLOCK:
248     error(0, "error updating schedule.db: %s", db_strerror(err));
249     return err;
250   case DB_KEYEXIST:
251     return err;
252   default:
253     fatal(0, "error updating schedule.db: %s", db_strerror(err));
254   }
255   return 0;
256 }
257
258 /** @brief Create a scheduled event
259  * @param ev Event loop
260  * @param actiondata Action actiondata
261  * @return Scheduled event ID or NULL on error
262  *
263  * Events are rejected if they lack the required fields, if the user
264  * is not allowed to perform them or if they are scheduled for a time
265  * in the past.
266  */
267 const char *schedule_add(ev_source *ev,
268                          struct kvp *actiondata) {
269   int e, n;
270   const char *id;
271   struct timeval when;
272
273   /* TODO: handle recurring events */
274   /* Check that the required field are present */
275   for(n = 0; n < NREQUIRED; ++n) {
276     if(!kvp_get(actiondata, schedule_required[n])) {
277       error(0, "new scheduled event is missing required field '%s'",
278             schedule_required[n]);
279       return 0;
280     }
281   }
282   /* Check that the user is allowed to do whatever it is */
283   if(schedule_lookup("[new]", actiondata) < 0)
284     return 0;
285   when.tv_sec = atoll(kvp_get(actiondata, "when"));
286   when.tv_usec = 0;
287   /* Reject events in the past */
288   if(when.tv_sec <= time(0)) {
289     error(0, "new scheduled event is in the past");
290     return 0;
291   }
292   do {
293     id = random_id();
294     WITH_TRANSACTION(schedule_add_tid(id, actiondata, tid));
295   } while(e == DB_KEYEXIST);
296   ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, (void *)id);
297   return id;
298 }
299
300 /******************************************************************************/
301
302 /** @brief Get the action data for a scheduled event
303  * @param id Event ID
304  * @return Event data or NULL
305  */
306 struct kvp *schedule_get(const char *id) {
307   int e, n;
308   struct kvp *actiondata;
309   
310   WITH_TRANSACTION(trackdb_getdata(trackdb_scheduledb, id, &actiondata, tid));
311   /* Check that the required field are present */
312   for(n = 0; n < NREQUIRED; ++n) {
313     if(!kvp_get(actiondata, schedule_required[n])) {
314       error(0, "scheduled event %s is missing required field '%s'",
315             id, schedule_required[n]);
316       return 0;
317     }
318   }
319   return actiondata;
320 }
321
322 /******************************************************************************/
323
324 /** @brief Delete a scheduled event
325  * @param id Event to delete
326  * @return 0 on success, non-0 if it did not exist
327  */
328 int schedule_del(const char *id) {
329   int e;
330
331   WITH_TRANSACTION(trackdb_delkey(trackdb_scheduledb, id, tid));
332   return e == 0 ? 0 : -1;
333 }
334
335 /******************************************************************************/
336
337 /** @brief Get a list of scheduled events
338  * @param neventsp Where to put count of events (or NULL)
339  * @return 0-terminate list of ID strings
340  */
341 char **schedule_list(int *neventsp) {
342   int e;
343   struct vector v[1];
344
345   vector_init(v);
346   WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
347   if(neventsp)
348     *neventsp = v->nvec;
349   return v->vec;
350 }
351
352 /******************************************************************************/
353
354 static void schedule_play(ev_source *ev,
355                           const char *id,
356                           const char *who,
357                           struct kvp *actiondata) {
358   const char *track = kvp_get(actiondata, "track");
359   struct queue_entry *q;
360
361   /* This stuff has rather a lot in common with c_play() */
362   if(!track) {
363     error(0, "scheduled event %s: no track field", id);
364     return;
365   }
366   if(!trackdb_exists(track)) {
367     error(0, "scheduled event %s: no such track as %s", id, track);
368     return;
369   }
370   if(!(track = trackdb_resolve(track))) {
371     error(0, "scheduled event %s: cannot resolve track %s", id, track);
372     return;
373   }
374   info("scheduled event %s: %s play %s", id,  who, track);
375   q = queue_add(track, who, WHERE_START);
376   queue_write();
377   if(q == qhead.next && playing)
378     prepare(ev, q);
379   play(ev);
380 }
381
382 static void schedule_set_global(ev_source attribute((unused)) *ev,
383                                 const char *id,
384                                 const char *who,
385                                 struct kvp *actiondata) {
386   const char *key = kvp_get(actiondata, "key");
387   const char *value = kvp_get(actiondata, "value");
388
389   if(!key) {
390     error(0, "scheduled event %s: no key field", id);
391     return;
392   }
393   if(key[0] == '_') {
394     error(0, "scheduled event %s: cannot set internal global preferences (%s)",
395           id, key);
396     return;
397   }
398   if(value)
399     info("scheduled event %s: %s set-global %s=%s", id, who, key, value);
400   else
401     info("scheduled event %s: %s set-global %s unset", id,  who, key);
402   trackdb_set_global(key, value, who);
403 }
404
405 /** @brief Table of schedule actions
406  *
407  * Must be kept sorted.
408  */
409 static struct {
410   const char *name;
411   void (*callback)(ev_source *ev,
412                    const char *id, const char *who,
413                    struct kvp *actiondata);
414   rights_type right;
415 } schedule_actions[] = {
416   { "play", schedule_play, RIGHT_PLAY },
417   { "set-global", schedule_set_global, RIGHT_GLOBAL_PREFS },
418 };
419
420 /** @brief Look up a scheduled event
421  * @param actiondata Event description
422  * @return index in schedule_actions[] on success, -1 on error
423  *
424  * Unknown events are rejected as are those that the user is not allowed to do.
425  */
426 static int schedule_lookup(const char *id,
427                            struct kvp *actiondata) {
428   const char *who = kvp_get(actiondata, "who");
429   const char *action = kvp_get(actiondata, "action");
430   const char *rights;
431   struct kvp *userinfo;
432   rights_type r;
433   int n;
434
435   /* Look up the action */
436   n = TABLE_FIND(schedule_actions, name, action);
437   if(n < 0) {
438     error(0, "scheduled event %s: unrecognized action '%s'", id, action);
439     return -1;
440   }
441   /* Find the user */
442   if(!(userinfo = trackdb_getuserinfo(who))) {
443     error(0, "scheduled event %s: user '%s' does not exist", id, who);
444     return -1;
445   }
446   /* Check that they have suitable rights */
447   if(!(rights = kvp_get(userinfo, "rights"))) {
448     error(0, "scheduled event %s: user %s' has no rights???", id, who);
449     return -1;
450   }
451   if(parse_rights(rights, &r, 1)) {
452     error(0, "scheduled event %s: user %s has invalid rights '%s'",
453           id, who, rights);
454     return -1;
455   }
456   if(!(r & schedule_actions[n].right)) {
457     error(0, "scheduled event %s: user %s lacks rights for action %s",
458           id, who, action);
459     return -1;
460   }
461   return n;
462 }
463
464 /** @brief Called when an action is due */
465 static int schedule_trigger(ev_source *ev,
466                             const struct timeval attribute((unused)) *now,
467                             void *u) {
468   const char *action, *id = u;
469   struct kvp *actiondata = schedule_get(id);
470   int n;
471
472   if(!actiondata)
473     return 0;
474   /* schedule_get() enforces these being present */
475   action = kvp_get(actiondata, "action");
476   /* Look up the action */
477   n = schedule_lookup(id, actiondata);
478   if(n < 0)
479     goto done;
480   /* Go ahead and do it */
481   schedule_actions[n].callback(ev, id, kvp_get(actiondata, "who"), actiondata);
482 done:
483   /* TODO: rewrite recurring events for their next trigger time,
484    * rather than deleting them */
485   schedule_del(id);
486   return 0;
487 }
488
489 /*
490 Local Variables:
491 c-basic-offset:2
492 comment-column:40
493 fill-column:79
494 indent-tabs-mode:nil
495 End:
496 */