chiark / gitweb /
Log discarded junk events.
[disorder] / server / schedule.c
CommitLineData
fdca70ee
RK
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
107static int schedule_trigger(ev_source *ev,
108 const struct timeval *now,
109 void *u);
110static int schedule_lookup(const char *id,
111 struct kvp *actiondata);
112
113/** @brief List of required fields in a scheduled event */
114static 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 */
126static 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 */
162static 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 */
184static 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 */
5dc143a4 211 info("junk event %s is in the past, discarding", id);
fdca70ee
RK
212 if(cdel(cursor))
213 goto deadlocked;
214 /* Skip this time */
215 continue;
216 }
217 }
218 /* Arrange a callback when the scheduled event is due */
219 ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id);
220 }
221 switch(err) {
222 case DB_NOTFOUND:
223 err = 0;
224 break;
225 case DB_LOCK_DEADLOCK:
226 error(0, "error querying schedule.db: %s", db_strerror(err));
227 break;
228 default:
229 fatal(0, "error querying schedule.db: %s", db_strerror(err));
230 }
231deadlocked:
232 if(trackdb_closecursor(cursor))
233 err = DB_LOCK_DEADLOCK;
234 return err;
235}
236
237/** @brief Initialize the schedule
238 * @param ev Event loop
239 *
240 * Sets a callback for all action times except for junk actions that are
241 * already in the past, which are discarded.
242 */
243void schedule_init(ev_source *ev) {
244 int e;
245 WITH_TRANSACTION(schedule_init_tid(ev, tid));
246}
247
248/******************************************************************************/
249
250/** @brief Create a scheduled event
251 * @param ev Event loop
252 * @param actiondata Action data
fdca70ee
RK
253 */
254static int schedule_add_tid(const char *id,
255 struct kvp *actiondata,
256 DB_TXN *tid) {
257 int err;
258 DBT k, d;
259
260 memset(&k, 0, sizeof k);
261 k.data = (void *)id;
262 k.size = strlen(id);
263 switch(err = trackdb_scheduledb->put(trackdb_scheduledb, tid, &k,
067eeb5f
RK
264 encode_data(&d, actiondata),
265 DB_NOOVERWRITE)) {
fdca70ee
RK
266 case 0:
267 break;
268 case DB_LOCK_DEADLOCK:
269 error(0, "error updating schedule.db: %s", db_strerror(err));
270 return err;
271 case DB_KEYEXIST:
272 return err;
273 default:
274 fatal(0, "error updating schedule.db: %s", db_strerror(err));
275 }
276 return 0;
277}
278
279/** @brief Create a scheduled event
280 * @param ev Event loop
281 * @param actiondata Action actiondata
282 * @return Scheduled event ID or NULL on error
283 *
284 * Events are rejected if they lack the required fields, if the user
285 * is not allowed to perform them or if they are scheduled for a time
286 * in the past.
287 */
067eeb5f
RK
288const char *schedule_add(ev_source *ev,
289 struct kvp *actiondata) {
fdca70ee 290 int e, n;
067eeb5f 291 const char *id;
fdca70ee
RK
292 struct timeval when;
293
294 /* TODO: handle recurring events */
295 /* Check that the required field are present */
296 for(n = 0; n < NREQUIRED; ++n) {
297 if(!kvp_get(actiondata, schedule_required[n])) {
298 error(0, "new scheduled event is missing required field '%s'",
299 schedule_required[n]);
300 return 0;
301 }
302 }
303 /* Check that the user is allowed to do whatever it is */
304 if(schedule_lookup("[new]", actiondata) < 0)
305 return 0;
306 when.tv_sec = atoll(kvp_get(actiondata, "when"));
307 when.tv_usec = 0;
308 /* Reject events in the past */
309 if(when.tv_sec <= time(0)) {
310 error(0, "new scheduled event is in the past");
311 return 0;
312 }
313 do {
314 id = random_id();
315 WITH_TRANSACTION(schedule_add_tid(id, actiondata, tid));
316 } while(e == DB_KEYEXIST);
067eeb5f 317 ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, (void *)id);
fdca70ee
RK
318 return id;
319}
320
321/******************************************************************************/
322
323/** @brief Get the action data for a scheduled event
324 * @param id Event ID
325 * @return Event data or NULL
326 */
327struct kvp *schedule_get(const char *id) {
328 int e, n;
329 struct kvp *actiondata;
330
331 WITH_TRANSACTION(trackdb_getdata(trackdb_scheduledb, id, &actiondata, tid));
332 /* Check that the required field are present */
333 for(n = 0; n < NREQUIRED; ++n) {
334 if(!kvp_get(actiondata, schedule_required[n])) {
335 error(0, "scheduled event %s is missing required field '%s'",
336 id, schedule_required[n]);
337 return 0;
338 }
339 }
340 return actiondata;
341}
342
343/******************************************************************************/
344
345/** @brief Delete a scheduled event
346 * @param id Event to delete
347 * @return 0 on success, non-0 if it did not exist
348 */
349int schedule_del(const char *id) {
350 int e;
351
352 WITH_TRANSACTION(trackdb_delkey(trackdb_scheduledb, id, tid));
353 return e == 0 ? 0 : -1;
354}
355
356/******************************************************************************/
357
358/** @brief Get a list of scheduled events
359 * @param neventsp Where to put count of events (or NULL)
360 * @return 0-terminate list of ID strings
361 */
362char **schedule_list(int *neventsp) {
363 int e;
364 struct vector v[1];
365
758aa6c3 366 vector_init(v);
fdca70ee
RK
367 WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
368 if(neventsp)
369 *neventsp = v->nvec;
370 return v->vec;
371}
372
373/******************************************************************************/
374
375static void schedule_play(ev_source *ev,
376 const char *id,
377 const char *who,
378 struct kvp *actiondata) {
379 const char *track = kvp_get(actiondata, "track");
380 struct queue_entry *q;
381
382 /* This stuff has rather a lot in common with c_play() */
383 if(!track) {
384 error(0, "scheduled event %s: no track field", id);
385 return;
386 }
387 if(!trackdb_exists(track)) {
388 error(0, "scheduled event %s: no such track as %s", id, track);
389 return;
390 }
391 if(!(track = trackdb_resolve(track))) {
392 error(0, "scheduled event %s: cannot resolve track %s", id, track);
393 return;
394 }
395 info("scheduled event %s: %s play %s", id, who, track);
396 q = queue_add(track, who, WHERE_START);
397 queue_write();
398 if(q == qhead.next && playing)
399 prepare(ev, q);
400 play(ev);
401}
402
403static void schedule_set_global(ev_source attribute((unused)) *ev,
404 const char *id,
405 const char *who,
406 struct kvp *actiondata) {
407 const char *key = kvp_get(actiondata, "key");
408 const char *value = kvp_get(actiondata, "value");
409
410 if(!key) {
411 error(0, "scheduled event %s: no key field", id);
412 return;
413 }
414 if(key[0] == '_') {
415 error(0, "scheduled event %s: cannot set internal global preferences (%s)",
416 id, key);
417 return;
418 }
419 if(value)
420 info("scheduled event %s: %s set-global %s=%s", id, who, key, value);
421 else
422 info("scheduled event %s: %s set-global %s unset", id, who, key);
423 trackdb_set_global(key, value, who);
424}
425
426/** @brief Table of schedule actions
427 *
428 * Must be kept sorted.
429 */
430static struct {
431 const char *name;
432 void (*callback)(ev_source *ev,
433 const char *id, const char *who,
434 struct kvp *actiondata);
435 rights_type right;
436} schedule_actions[] = {
437 { "play", schedule_play, RIGHT_PLAY },
438 { "set-global", schedule_set_global, RIGHT_GLOBAL_PREFS },
439};
440
441/** @brief Look up a scheduled event
442 * @param actiondata Event description
443 * @return index in schedule_actions[] on success, -1 on error
444 *
445 * Unknown events are rejected as are those that the user is not allowed to do.
446 */
447static int schedule_lookup(const char *id,
448 struct kvp *actiondata) {
449 const char *who = kvp_get(actiondata, "who");
450 const char *action = kvp_get(actiondata, "action");
451 const char *rights;
452 struct kvp *userinfo;
453 rights_type r;
454 int n;
455
456 /* Look up the action */
457 n = TABLE_FIND(schedule_actions, typeof(schedule_actions[0]), name, action);
458 if(n < 0) {
459 error(0, "scheduled event %s: unrecognized action '%s'", id, action);
460 return -1;
461 }
462 /* Find the user */
463 if(!(userinfo = trackdb_getuserinfo(who))) {
464 error(0, "scheduled event %s: user '%s' does not exist", id, who);
465 return -1;
466 }
467 /* Check that they have suitable rights */
468 if(!(rights = kvp_get(userinfo, "rights"))) {
469 error(0, "scheduled event %s: user %s' has no rights???", id, who);
470 return -1;
471 }
472 if(parse_rights(rights, &r, 1)) {
473 error(0, "scheduled event %s: user %s has invalid rights '%s'",
474 id, who, rights);
475 return -1;
476 }
477 if(!(r & schedule_actions[n].right)) {
478 error(0, "scheduled event %s: user %s lacks rights for action %s",
479 id, who, action);
480 return -1;
481 }
482 return n;
483}
484
485/** @brief Called when an action is due */
486static int schedule_trigger(ev_source *ev,
487 const struct timeval attribute((unused)) *now,
488 void *u) {
489 const char *action, *id = u;
490 struct kvp *actiondata = schedule_get(id);
491 int n;
492
493 if(!actiondata)
494 return 0;
495 /* schedule_get() enforces these being present */
496 action = kvp_get(actiondata, "action");
497 /* Look up the action */
498 n = schedule_lookup(id, actiondata);
499 if(n < 0)
500 goto done;
501 /* Go ahead and do it */
502 schedule_actions[n].callback(ev, id, kvp_get(actiondata, "who"), actiondata);
503done:
504 /* TODO: rewrite recurring events for their next trigger time,
505 * rather than deleting them */
506 schedule_del(id);
507 return 0;
508}
509
510/*
511Local Variables:
512c-basic-offset:2
513comment-column:40
514fill-column:79
515indent-tabs-mode:nil
516End:
517*/