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 *
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 */
05b75f8d 84#include "disorder-server.h"
fdca70ee
RK
85
86static int schedule_trigger(ev_source *ev,
87 const struct timeval *now,
88 void *u);
89static int schedule_lookup(const char *id,
90 struct kvp *actiondata);
91
92/** @brief List of required fields in a scheduled event */
93static 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 */
105static 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 */
141static 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 */
163static 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 */
5dc143a4 190 info("junk event %s is in the past, discarding", id);
fdca70ee
RK
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 }
210deadlocked:
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 */
222void 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
fdca70ee
RK
232 */
233static 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,
067eeb5f
RK
243 encode_data(&d, actiondata),
244 DB_NOOVERWRITE)) {
fdca70ee
RK
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 */
067eeb5f
RK
267const char *schedule_add(ev_source *ev,
268 struct kvp *actiondata) {
fdca70ee 269 int e, n;
067eeb5f 270 const char *id;
fdca70ee
RK
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);
067eeb5f 296 ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, (void *)id);
fdca70ee
RK
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 */
306struct 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 */
328int 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 */
341char **schedule_list(int *neventsp) {
342 int e;
343 struct vector v[1];
344
758aa6c3 345 vector_init(v);
fdca70ee
RK
346 WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb, v, tid));
347 if(neventsp)
348 *neventsp = v->nvec;
349 return v->vec;
350}
351
352/******************************************************************************/
353
354static 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
382static 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 */
409static 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 */
426static 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 */
ba937f01 436 n = TABLE_FIND(schedule_actions, name, action);
fdca70ee
RK
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 */
465static 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);
482done:
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/*
490Local Variables:
491c-basic-offset:2
492comment-column:40
493fill-column:79
494indent-tabs-mode:nil
495End:
496*/