Commit | Line | Data |
---|---|---|
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 | ||
107 | static int schedule_trigger(ev_source *ev, | |
108 | const struct timeval *now, | |
109 | void *u); | |
110 | static int schedule_lookup(const char *id, | |
111 | struct kvp *actiondata); | |
112 | ||
113 | /** @brief List of required fields in a scheduled event */ | |
114 | static 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 | */ | |
126 | static 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 */ | |
162 | static 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 | */ | |
184 | static 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 */ | |
211 | if(cdel(cursor)) | |
212 | goto deadlocked; | |
213 | /* Skip this time */ | |
214 | continue; | |
215 | } | |
216 | } | |
217 | /* Arrange a callback when the scheduled event is due */ | |
218 | ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id); | |
219 | } | |
220 | switch(err) { | |
221 | case DB_NOTFOUND: | |
222 | err = 0; | |
223 | break; | |
224 | case DB_LOCK_DEADLOCK: | |
225 | error(0, "error querying schedule.db: %s", db_strerror(err)); | |
226 | break; | |
227 | default: | |
228 | fatal(0, "error querying schedule.db: %s", db_strerror(err)); | |
229 | } | |
230 | deadlocked: | |
231 | if(trackdb_closecursor(cursor)) | |
232 | err = DB_LOCK_DEADLOCK; | |
233 | return err; | |
234 | } | |
235 | ||
236 | /** @brief Initialize the schedule | |
237 | * @param ev Event loop | |
238 | * | |
239 | * Sets a callback for all action times except for junk actions that are | |
240 | * already in the past, which are discarded. | |
241 | */ | |
242 | void schedule_init(ev_source *ev) { | |
243 | int e; | |
244 | WITH_TRANSACTION(schedule_init_tid(ev, tid)); | |
245 | } | |
246 | ||
247 | /******************************************************************************/ | |
248 | ||
249 | /** @brief Create a scheduled event | |
250 | * @param ev Event loop | |
251 | * @param actiondata Action data | |
252 | * | |
253 | * The caller should set the timeout themselves. | |
254 | */ | |
255 | static int schedule_add_tid(const char *id, | |
256 | struct kvp *actiondata, | |
257 | DB_TXN *tid) { | |
258 | int err; | |
259 | DBT k, d; | |
260 | ||
261 | memset(&k, 0, sizeof k); | |
262 | k.data = (void *)id; | |
263 | k.size = strlen(id); | |
264 | switch(err = trackdb_scheduledb->put(trackdb_scheduledb, tid, &k, | |
265 | encode_data(&d, actiondata), 0)) { | |
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 | */ | |
288 | char *schedule_add(ev_source *ev, | |
289 | struct kvp *actiondata) { | |
290 | int e, n; | |
291 | char *id; | |
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); | |
317 | ev_timeout(ev, 0/*handlep*/, &when, schedule_trigger, id); | |
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 | */ | |
327 | struct 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 | */ | |
349 | int 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 | */ | |
362 | char **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 | ||
375 | static 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 | ||
403 | static 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 | */ | |
430 | static 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 | */ | |
447 | static 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 */ | |
486 | static 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); | |
503 | done: | |
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 | /* | |
511 | Local Variables: | |
512 | c-basic-offset:2 | |
513 | comment-column:40 | |
514 | fill-column:79 | |
515 | indent-tabs-mode:nil | |
516 | End: | |
517 | */ |