chiark / gitweb /
Update CHANGES.html
[disorder] / server / schedule.c
CommitLineData
fdca70ee
RK
1/*
2 * This file is part of DisOrder
3 * Copyright (C) 2008 Richard Kettlewell
4 *
e7eb3a27 5 * This program is free software: you can redistribute it and/or modify
fdca70ee 6 * it under the terms of the GNU General Public License as published by
e7eb3a27 7 * the Free Software Foundation, either version 3 of the License, or
fdca70ee
RK
8 * (at your option) any later version.
9 *
e7eb3a27
RK
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 *
fdca70ee 15 * You should have received a copy of the GNU General Public License
e7eb3a27 16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
fdca70ee
RK
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 */
05b75f8d 82#include "disorder-server.h"
fdca70ee
RK
83
84static int schedule_trigger(ev_source *ev,
85 const struct timeval *now,
86 void *u);
87static int schedule_lookup(const char *id,
88 struct kvp *actiondata);
89
90/** @brief List of required fields in a scheduled event */
91static 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
59cf25c4
RK
98 * @param d Pointer to data
99 * @param idp Where to store event ID
100 * @param actiondatap Where to store parsed data
fdca70ee
RK
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 */
106static 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) {
2e9ba080
RK
117 disorder_error(0, "bogus schedule.db key (%lu bytes)",
118 (unsigned long)k->size);
fdca70ee
RK
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])) {
2e9ba080
RK
126 disorder_error(0, "scheduled event %s: missing required field '%s'",
127 id, schedule_required[n]);
fdca70ee
RK
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 */
143static 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:
2e9ba080 150 disorder_error(0, "error deleting from schedule.db: %s", db_strerror(err));
fdca70ee
RK
151 break;
152 default:
2e9ba080 153 disorder_fatal(0, "error deleting from schedule.db: %s", db_strerror(err));
fdca70ee
RK
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 */
165static 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 */
4265e5d3 186 if(when.tv_sec < xtime(0)) {
fdca70ee
RK
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 */
2e9ba080 192 disorder_info("junk event %s is in the past, discarding", id);
fdca70ee
RK
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:
2e9ba080 207 disorder_error(0, "error querying schedule.db: %s", db_strerror(err));
fdca70ee
RK
208 break;
209 default:
2e9ba080 210 disorder_fatal(0, "error querying schedule.db: %s", db_strerror(err));
fdca70ee
RK
211 }
212deadlocked:
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 */
224void 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
59cf25c4 232 * @param id Event ID
fdca70ee 233 * @param actiondata Action data
59cf25c4 234 * @param tid Containing transaction
fdca70ee
RK
235 */
236static 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,
067eeb5f
RK
246 encode_data(&d, actiondata),
247 DB_NOOVERWRITE)) {
fdca70ee
RK
248 case 0:
249 break;
250 case DB_LOCK_DEADLOCK:
2e9ba080 251 disorder_error(0, "error updating schedule.db: %s", db_strerror(err));
fdca70ee
RK
252 return err;
253 case DB_KEYEXIST:
254 return err;
255 default:
2e9ba080 256 disorder_fatal(0, "error updating schedule.db: %s", db_strerror(err));
fdca70ee
RK
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 */
067eeb5f
RK
270const char *schedule_add(ev_source *ev,
271 struct kvp *actiondata) {
fdca70ee 272 int e, n;
067eeb5f 273 const char *id;
fdca70ee
RK
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])) {
2e9ba080
RK
280 disorder_error(0, "new scheduled event is missing required field '%s'",
281 schedule_required[n]);
fdca70ee
RK
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 */
4265e5d3 291 if(when.tv_sec <= xtime(0)) {
2e9ba080 292 disorder_error(0, "new scheduled event is in the past");
fdca70ee
RK
293 return 0;
294 }
295 do {
296 id = random_id();
297 WITH_TRANSACTION(schedule_add_tid(id, actiondata, tid));
298 } while(e == DB_KEYEXIST);
067eeb5f 299 ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, (void *)id);
fdca70ee
RK
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 */
309struct 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])) {
2e9ba080
RK
317 disorder_error(0, "scheduled event %s is missing required field '%s'",
318 id, schedule_required[n]);
fdca70ee
RK
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 */
331int 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 */
344char **schedule_list(int *neventsp) {
345 int e;
346 struct vector v[1];
347
758aa6c3 348 vector_init(v);
fdca70ee
RK
349 WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
350 if(neventsp)
351 *neventsp = v->nvec;
352 return v->vec;
353}
354
355/******************************************************************************/
356
357static 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) {
2e9ba080 366 disorder_error(0, "scheduled event %s: no track field", id);
fdca70ee
RK
367 return;
368 }
369 if(!trackdb_exists(track)) {
2e9ba080 370 disorder_error(0, "scheduled event %s: no such track as %s", id, track);
fdca70ee
RK
371 return;
372 }
373 if(!(track = trackdb_resolve(track))) {
2e9ba080 374 disorder_error(0, "scheduled event %s: cannot resolve track %s", id, track);
fdca70ee
RK
375 return;
376 }
2e9ba080 377 disorder_info("scheduled event %s: %s play %s", id, who, track);
7a853280 378 q = queue_add(track, who, WHERE_START, NULL, origin_scheduled);
fdca70ee
RK
379 queue_write();
380 if(q == qhead.next && playing)
381 prepare(ev, q);
382 play(ev);
383}
384
385static 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) {
2e9ba080 393 disorder_error(0, "scheduled event %s: no key field", id);
fdca70ee
RK
394 return;
395 }
396 if(key[0] == '_') {
2e9ba080
RK
397 disorder_error(0, "scheduled event %s: cannot set internal global preferences (%s)",
398 id, key);
fdca70ee
RK
399 return;
400 }
401 if(value)
2e9ba080
RK
402 disorder_info("scheduled event %s: %s set-global %s=%s",
403 id, who, key, value);
fdca70ee 404 else
2e9ba080 405 disorder_info("scheduled event %s: %s set-global %s unset", id, who, key);
fdca70ee
RK
406 trackdb_set_global(key, value, who);
407}
408
409/** @brief Table of schedule actions
410 *
411 * Must be kept sorted.
412 */
413static 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
59cf25c4 425 * @param id Event ID
fdca70ee
RK
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 */
431static 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 */
ba937f01 441 n = TABLE_FIND(schedule_actions, name, action);
fdca70ee 442 if(n < 0) {
2e9ba080
RK
443 disorder_error(0, "scheduled event %s: unrecognized action '%s'",
444 id, action);
fdca70ee
RK
445 return -1;
446 }
447 /* Find the user */
448 if(!(userinfo = trackdb_getuserinfo(who))) {
2e9ba080 449 disorder_error(0, "scheduled event %s: user '%s' does not exist", id, who);
fdca70ee
RK
450 return -1;
451 }
452 /* Check that they have suitable rights */
453 if(!(rights = kvp_get(userinfo, "rights"))) {
2e9ba080 454 disorder_error(0, "scheduled event %s: user %s' has no rights???", id, who);
fdca70ee
RK
455 return -1;
456 }
457 if(parse_rights(rights, &r, 1)) {
2e9ba080
RK
458 disorder_error(0, "scheduled event %s: user %s has invalid rights '%s'",
459 id, who, rights);
fdca70ee
RK
460 return -1;
461 }
462 if(!(r & schedule_actions[n].right)) {
2e9ba080
RK
463 disorder_error(0, "scheduled event %s: user %s lacks rights for action %s",
464 id, who, action);
fdca70ee
RK
465 return -1;
466 }
467 return n;
468}
469
470/** @brief Called when an action is due */
471static int schedule_trigger(ev_source *ev,
472 const struct timeval attribute((unused)) *now,
473 void *u) {
375d9478 474 const char *id = u;
fdca70ee
RK
475 struct kvp *actiondata = schedule_get(id);
476 int n;
477
478 if(!actiondata)
479 return 0;
fdca70ee
RK
480 /* Look up the action */
481 n = schedule_lookup(id, actiondata);
482 if(n < 0)
483 goto done;
484 /* Go ahead and do it */
485 schedule_actions[n].callback(ev, id, kvp_get(actiondata, "who"), actiondata);
486done:
487 /* TODO: rewrite recurring events for their next trigger time,
488 * rather than deleting them */
489 schedule_del(id);
490 return 0;
491}
492
493/*
494Local Variables:
495c-basic-offset:2
496comment-column:40
497fill-column:79
498indent-tabs-mode:nil
499End:
500*/