2 * This file is part of DisOrder
3 * Copyright (C) 2008-2010 Richard Kettlewell
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 3 of the License, or
8 * (at your option) any later version.
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.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 /** @file server/schedule.c
20 * @brief Scheduled events
22 * @ref trackdb_scheduledb is a mapping from ID strings to encoded
23 * key-value pairs called 'actiondata'.
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
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
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
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.
52 * Recurring events are NOT IMPLEMENTED yet but this is the proposed
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.
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.
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
73 * If there are no week or year days then the event recurs every day.
75 * If there are both week and year days then the union of them is
76 * taken, rather than the intersection.
78 * TODO: support recurring events.
80 * TODO: add disorder-dump support
82 #include "disorder-server.h"
84 static int schedule_trigger(ev_source
*ev
,
85 const struct timeval
*now
,
87 static int schedule_lookup(const char *id
,
88 struct kvp
*actiondata
);
90 /** @brief List of required fields in a scheduled event */
91 static const char *const schedule_required
[] = {"when", "who", "action"};
93 /** @brief Number of elements in @ref schedule_required */
94 #define NREQUIRED (int)(sizeof schedule_required / sizeof *schedule_required)
96 /** @brief Parse a scheduled event key and data
97 * @param k Pointer to key
98 * @param d Pointer to data
99 * @param idp Where to store event ID
100 * @param actiondatap Where to store parsed data
101 * @param whenp Where to store timestamp
102 * @return 0 on success, non-0 on error
104 * Rejects entries that are invalid in various ways.
106 static int schedule_parse(const DBT
*k
,
109 struct kvp
**actiondatap
,
112 struct kvp
*actiondata
;
115 /* Reject bogus keys */
116 if(!k
->size
|| k
->size
> 128) {
117 disorder_error(0, "bogus schedule.db key (%lu bytes)",
118 (unsigned long)k
->size
);
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
])) {
126 disorder_error(0, "scheduled event %s: missing required field '%s'",
127 id
, schedule_required
[n
]);
131 /* Return the results */
135 *actiondatap
= actiondata
;
137 *whenp
= (time_t)atoll(kvp_get(actiondata
, "when"));
141 /** @brief Delete via a cursor
142 * @return 0 or @c DB_LOCK_DEADLOCK */
143 static int cdel(DBC
*cursor
) {
146 switch(err
= cursor
->c_del(cursor
, 0)) {
149 case DB_LOCK_DEADLOCK
:
150 disorder_error(0, "error deleting from schedule.db: %s", db_strerror(err
));
153 disorder_fatal(0, "error deleting from schedule.db: %s", db_strerror(err
));
158 /** @brief Initialize the schedule
159 * @param ev Event loop
160 * @param tid Transaction ID
162 * Sets a callback for all action times except for junk actions that are
163 * already in the past, which are discarded.
165 static int schedule_init_tid(ev_source
*ev
,
171 cursor
= trackdb_opencursor(trackdb_scheduledb
, tid
);
172 while(!(err
= cursor
->c_get(cursor
, prepare_data(&k
), prepare_data(&d
),
175 struct kvp
*actiondata
;
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
)))
185 /* The action might be in the past */
186 if(when
.tv_sec
< xtime(0)) {
187 const char *priority
= kvp_get(actiondata
, "priority");
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 */
192 disorder_info("junk event %s is in the past, discarding", id
);
199 /* Arrange a callback when the scheduled event is due */
200 ev_timeout(ev
, 0/*handlep*/, &when
, schedule_trigger
, id
);
206 case DB_LOCK_DEADLOCK
:
207 disorder_error(0, "error querying schedule.db: %s", db_strerror(err
));
210 disorder_fatal(0, "error querying schedule.db: %s", db_strerror(err
));
213 if(trackdb_closecursor(cursor
))
214 err
= DB_LOCK_DEADLOCK
;
218 /** @brief Initialize the schedule
219 * @param ev Event loop
221 * Sets a callback for all action times except for junk actions that are
222 * already in the past, which are discarded.
224 void schedule_init(ev_source
*ev
) {
226 WITH_TRANSACTION(schedule_init_tid(ev
, tid
));
229 /******************************************************************************/
231 /** @brief Create a scheduled event
233 * @param actiondata Action data
234 * @param tid Containing transaction
236 static int schedule_add_tid(const char *id
,
237 struct kvp
*actiondata
,
242 memset(&k
, 0, sizeof k
);
245 switch(err
= trackdb_scheduledb
->put(trackdb_scheduledb
, tid
, &k
,
246 encode_data(&d
, actiondata
),
250 case DB_LOCK_DEADLOCK
:
251 disorder_error(0, "error updating schedule.db: %s", db_strerror(err
));
256 disorder_fatal(0, "error updating schedule.db: %s", db_strerror(err
));
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
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
270 const char *schedule_add(ev_source
*ev
,
271 struct kvp
*actiondata
) {
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
])) {
280 disorder_error(0, "new scheduled event is missing required field '%s'",
281 schedule_required
[n
]);
285 /* Check that the user is allowed to do whatever it is */
286 if(schedule_lookup("[new]", actiondata
) < 0)
288 when
.tv_sec
= atoll(kvp_get(actiondata
, "when"));
290 /* Reject events in the past */
291 if(when
.tv_sec
<= xtime(0)) {
292 disorder_error(0, "new scheduled event is in the past");
297 WITH_TRANSACTION(schedule_add_tid(id
, actiondata
, tid
));
298 } while(e
== DB_KEYEXIST
);
299 ev_timeout(ev
, 0/*handlep*/, &when
, schedule_trigger
, (void *)id
);
303 /******************************************************************************/
305 /** @brief Get the action data for a scheduled event
307 * @return Event data or NULL
309 struct kvp
*schedule_get(const char *id
) {
311 struct kvp
*actiondata
;
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
])) {
317 disorder_error(0, "scheduled event %s is missing required field '%s'",
318 id
, schedule_required
[n
]);
325 /******************************************************************************/
327 /** @brief Delete a scheduled event
328 * @param id Event to delete
329 * @return 0 on success, non-0 if it did not exist
331 int schedule_del(const char *id
) {
334 WITH_TRANSACTION(trackdb_delkey(trackdb_scheduledb
, id
, tid
));
335 return e
== 0 ?
0 : -1;
338 /******************************************************************************/
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
344 char **schedule_list(int *neventsp
) {
349 WITH_TRANSACTION(trackdb_listkeys(trackdb_scheduledb
, v
, tid
));
355 /******************************************************************************/
357 static void schedule_play(ev_source
*ev
,
360 struct kvp
*actiondata
) {
361 const char *track
= kvp_get(actiondata
, "track");
362 struct queue_entry
*q
;
364 /* This stuff has rather a lot in common with c_play() */
366 disorder_error(0, "scheduled event %s: no track field", id
);
369 if(!trackdb_exists(track
)) {
370 disorder_error(0, "scheduled event %s: no such track as %s", id
, track
);
373 if(!(track
= trackdb_resolve(track
))) {
374 disorder_error(0, "scheduled event %s: cannot resolve track %s", id
, track
);
377 disorder_info("scheduled event %s: %s play %s", id
, who
, track
);
378 q
= queue_add(track
, who
, WHERE_START
, NULL
, origin_scheduled
);
380 if(q
== qhead
.next
&& playing
)
385 static void schedule_set_global(ev_source
attribute((unused
)) *ev
,
388 struct kvp
*actiondata
) {
389 const char *key
= kvp_get(actiondata
, "key");
390 const char *value
= kvp_get(actiondata
, "value");
393 disorder_error(0, "scheduled event %s: no key field", id
);
397 disorder_error(0, "scheduled event %s: cannot set internal global preferences (%s)",
402 disorder_info("scheduled event %s: %s set-global %s=%s",
403 id
, who
, key
, value
);
405 disorder_info("scheduled event %s: %s set-global %s unset", id
, who
, key
);
406 trackdb_set_global(key
, value
, who
);
409 /** @brief Table of schedule actions
411 * Must be kept sorted.
415 void (*callback
)(ev_source
*ev
,
416 const char *id
, const char *who
,
417 struct kvp
*actiondata
);
419 } schedule_actions
[] = {
420 { "play", schedule_play
, RIGHT_PLAY
},
421 { "set-global", schedule_set_global
, RIGHT_GLOBAL_PREFS
},
424 /** @brief Look up a scheduled event
426 * @param actiondata Event description
427 * @return index in schedule_actions[] on success, -1 on error
429 * Unknown events are rejected as are those that the user is not allowed to do.
431 static 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");
436 struct kvp
*userinfo
;
440 /* Look up the action */
441 n
= TABLE_FIND(schedule_actions
, name
, action
);
443 disorder_error(0, "scheduled event %s: unrecognized action '%s'",
448 if(!(userinfo
= trackdb_getuserinfo(who
))) {
449 disorder_error(0, "scheduled event %s: user '%s' does not exist", id
, who
);
452 /* Check that they have suitable rights */
453 if(!(rights
= kvp_get(userinfo
, "rights"))) {
454 disorder_error(0, "scheduled event %s: user %s' has no rights???", id
, who
);
457 if(parse_rights(rights
, &r
, 1)) {
458 disorder_error(0, "scheduled event %s: user %s has invalid rights '%s'",
462 if(!(r
& schedule_actions
[n
].right
)) {
463 disorder_error(0, "scheduled event %s: user %s lacks rights for action %s",
470 /** @brief Called when an action is due */
471 static int schedule_trigger(ev_source
*ev
,
472 const struct timeval
attribute((unused
)) *now
,
475 struct kvp
*actiondata
= schedule_get(id
);
480 /* Look up the action */
481 n
= schedule_lookup(id
, actiondata
);
484 /* Go ahead and do it */
485 schedule_actions
[n
].callback(ev
, id
, kvp_get(actiondata
, "who"), actiondata
);
487 /* TODO: rewrite recurring events for their next trigger time,
488 * rather than deleting them */