]> git.tuebingen.mpg.de Git - paraslash.git/blobdiff - afs.c
paraslash 0.7.3
[paraslash.git] / afs.c
diff --git a/afs.c b/afs.c
index 04eed5503d18cb6d3b6e4d0cb127f98637d36b5d..445d5871097b79cdcd14170c2a1f59998354dc98 100644 (file)
--- a/afs.c
+++ b/afs.c
 #include "afs.h"
 #include "net.h"
 #include "server.h"
+#include "daemon.h"
 #include "ipc.h"
 #include "list.h"
 #include "sched.h"
 #include "fd.h"
 #include "signal.h"
-#include "mood.h"
 #include "sideband.h"
 #include "command.h"
 
-/** The osl tables used by afs. \sa \ref blob.c. */
-enum afs_table_num {
-       /** Contains audio file information. See \ref aft.c. */
-       TBLNUM_AUDIO_FILES,
-       /** The table for the paraslash attributes. See \ref attribute.c. */
-       TBLNUM_ATTRIBUTES,
-       /**
-        * Paraslash's scoring system is based on Gaussian normal
-        * distributions, and the relevant data is stored in the rbtrees of an
-        * osl table containing only volatile columns. See \ref score.c for
-        * details.
-        */
-       TBLNUM_SCORES,
-       /**
-        * A standard blob table containing the mood definitions. For details
-        * see \ref mood.c.
-        */
-       TBLNUM_MOODS,
-       /** A blob table containing lyrics on a per-song basis. */
-       TBLNUM_LYRICS,
-       /** Another blob table for images (for example album cover art). */
-       TBLNUM_IMAGES,
-       /** Yet another blob table for storing standard playlists. */
-       TBLNUM_PLAYLIST,
-       /** How many tables are in use? */
-       NUM_AFS_TABLES
-};
-
-static struct afs_table afs_tables[NUM_AFS_TABLES] = {
-       [TBLNUM_AUDIO_FILES] = {.init = aft_init, .name = "audio_files"},
-       [TBLNUM_ATTRIBUTES] = {.init = attribute_init, .name = "attributes"},
-       [TBLNUM_SCORES] = {.init = score_init, .name = "scores"},
-       [TBLNUM_MOODS] = {.init = moods_init, .name = "moods"},
-       [TBLNUM_LYRICS] = {.init = lyrics_init, .name = "lyrics"},
-       [TBLNUM_IMAGES] = {.init = images_init, .name = "images"},
-       [TBLNUM_PLAYLIST] = {.init = playlists_init, .name = "playlists"},
+/**
+ * The array of tables of the audio file selector.
+ *
+ * We organize them in an array to be able to loop over all tables.
+ */
+static const struct afs_table {
+       /** The name is no table operation, so define it here. */
+       const char * const name;
+       /** The only way to invoke the ops is via this pointer. */
+       const struct afs_table_operations *ops;
+} afs_tables[] = {
+       {.name = "audio_files", .ops = &aft_ops},
+       {.name = "attributes", .ops = &attr_ops},
+       {.name = "scores", .ops = &score_ops},
+       {.name = "moods", .ops = &moods_ops},
+       {.name = "lyrics", .ops = &lyrics_ops},
+       {.name = "images", .ops = &images_ops},
+       {.name = "playlists", .ops = &playlists_ops},
 };
+/** Used to loop over the afs tables. */
+#define NUM_AFS_TABLES ARRAY_SIZE(afs_tables)
 
 struct command_task {
        /** The file descriptor for the local socket. */
@@ -91,32 +75,32 @@ static char *current_mop; /* mode or playlist specifier. NULL means dummy mood *
 extern uint32_t afs_socket_cookie;
 
 /**
- * Struct to let command handlers execute a callback in afs context.
- *
- * Commands that need to change the state of afs can't change the relevant data
- * structures directly because commands are executed in a child process, i.e.
- * they get their own virtual address space.
+ * Passed from command handlers to afs.
  *
- * This structure is used by \p send_callback_request() (executed from handler
- * context) in order to let the afs process call the specified function. An
- * instance of that structure is written to a shared memory area together with
- * the arguments to the callback function. The identifier of the shared memory
- * area is written to the command socket.
+ * Command handlers cannot change the afs database directly because they run in
+ * a separate process. The callback query structure circumvents this
+ * restriction as follows. To instruct the afs process to execute a particular
+ * function, the command hander writes an instance of this structure to a
+ * shared memory area, along with the arguments to the callback function. The
+ * identifier of the shared memory area is transferred to the afs process via
+ * the command socket.
  *
- * The afs process accepts connections on the command socket and reads the
- * shared memory id, attaches the corresponding area, calls the given handler to
- * perform the desired action and to optionally compute a result.
+ * The afs process reads the shared memory id from the command socket, attaches
+ * the corresponding area, and calls the callback function whose address is
+ * stored in the area.
  *
- * The result and a \p callback_result structure is then written to another
- * shared memory area. The identifier for that area is written to the handler's
- * command socket, so that the handler process can read the id, attach the
- * shared memory area and use the result.
+ * The command output, if any, is transferred back to the command handler in
+ * the same way: The afs process writes the output to a second shared memory
+ * area together with a fixed size metadata header whose format corresponds to
+ * the \ref callback_result structure. The identifier of this area is sent back
+ * to the command handler which attaches the area and forwards the output to
+ * the remote client.
  *
  * \sa \ref struct callback_result.
  */
 struct callback_query {
        /** The function to be called. */
-       afs_callback *handler;
+       afs_callback *cb;
        /** The number of bytes of the query */
        size_t query_size;
 };
@@ -207,7 +191,7 @@ int send_callback_request(afs_callback *f, struct osl_object *query,
        if (ret < 0)
                goto out;
        cq = query_shm;
-       cq->handler = f;
+       cq->cb = f;
        cq->query_size = query_shm_size - sizeof(*cq);
 
        if (query)
@@ -418,13 +402,13 @@ static int pass_afd(int fd, char *buf, size_t size)
  */
 static int open_next_audio_file(void)
 {
-       struct audio_file_data afd;
-       int ret, shmid;
+       int ret, shmid, fd;
        char buf[8];
 
-       ret = open_and_update_audio_file(&afd);
+       ret = open_and_update_audio_file(&fd);
        if (ret < 0) {
-               PARA_ERROR_LOG("%s\n", para_strerror(-ret));
+               if (ret != -OSL_ERRNO_TO_PARA_ERROR(E_OSL_RB_KEY_NOT_FOUND))
+                       PARA_ERROR_LOG("%s\n", para_strerror(-ret));
                goto no_admissible_files;
        }
        shmid = ret;
@@ -434,8 +418,8 @@ static int open_next_audio_file(void)
        }
        *(uint32_t *)buf = NEXT_AUDIO_FILE;
        *(uint32_t *)(buf + 4) = (uint32_t)shmid;
-       ret = pass_afd(afd.fd, buf, 8);
-       close(afd.fd);
+       ret = pass_afd(fd, buf, 8);
+       close(fd);
        if (ret >= 0)
                return ret;
 destroy:
@@ -447,55 +431,48 @@ no_admissible_files:
        return write_all(server_socket, buf, 8);
 }
 
-/* Never fails if arg == NULL */
-static int activate_mood_or_playlist(const char *arg, int *num_admissible,
-               char **errmsg)
+static int activate_mood_or_playlist(const char *arg, struct para_buffer *pb)
 {
        enum play_mode mode;
        int ret;
+       char *msg;
 
-       if (!arg) {
-               ret = change_current_mood(NULL, NULL); /* always successful */
+       if (!arg) { /* load dummy mood */
+               ret = mood_load(NULL, NULL, &msg);
+               mode = PLAY_MODE_MOOD;
+       } else if (!strncmp(arg, "p/", 2)) {
+               ret = playlist_load(arg + 2, NULL, &msg);
+               mode = PLAY_MODE_PLAYLIST;
+       } else if (!strncmp(arg, "m/", 2)) {
+               ret = mood_load(arg + 2, NULL, &msg);
                mode = PLAY_MODE_MOOD;
        } else {
-               if (!strncmp(arg, "p/", 2)) {
-                       ret = playlist_open(arg + 2);
-                       if (ret < 0 && errmsg)
-                               *errmsg = make_message( "could not open %s",
-                                       arg);
-                       mode = PLAY_MODE_PLAYLIST;
-               } else if (!strncmp(arg, "m/", 2)) {
-                       ret = change_current_mood(arg + 2, errmsg);
-                       mode = PLAY_MODE_MOOD;
-               } else {
-                       if (errmsg)
-                               *errmsg = make_message("%s: parse error", arg);
-                       return -ERRNO_TO_PARA_ERROR(EINVAL);
-               }
-               if (ret < 0)
-                       return ret;
+               ret = -ERRNO_TO_PARA_ERROR(EINVAL);
+               msg = make_message("%s: parse error\n", arg);
        }
-       if (num_admissible)
-               *num_admissible = ret;
+       if (pb)
+               para_printf(pb, "%s", msg);
+       free(msg);
+       if (ret < 0)
+               return ret;
        current_play_mode = mode;
+       /*
+        * We get called with arg == current_mop from the signal dispatcher
+        * after SIGHUP and from the error path of the select command to
+        * re-select the current mood or playlist. In this case the assignment
+        * to current_mop below would result in a use-after-free condition.
+        */
        if (arg != current_mop) {
                free(current_mop);
-               if (arg) {
-                       current_mop = para_strdup(arg);
-                       mutex_lock(mmd_mutex);
-                       strncpy(mmd->afs_mode_string, arg,
-                               sizeof(mmd->afs_mode_string));
-                       mmd->afs_mode_string[sizeof(mmd->afs_mode_string) - 1] = '\0';
-                       mmd->events++;
-                       mutex_unlock(mmd_mutex);
-               } else {
-                       mutex_lock(mmd_mutex);
-                       strcpy(mmd->afs_mode_string, "dummy");
-                       mmd->events++;
-                       mutex_unlock(mmd_mutex);
-                       current_mop = NULL;
-               }
+               current_mop = arg? para_strdup(arg) : NULL;
        }
+       /* Notify the server about the mood/playlist change. */
+       mutex_lock(mmd_mutex);
+       strncpy(mmd->afs_mode_string, arg? arg: "dummy",
+               sizeof(mmd->afs_mode_string));
+       mmd->afs_mode_string[sizeof(mmd->afs_mode_string) - 1] = '\0';
+       mmd->events++;
+       mutex_unlock(mmd_mutex);
        return 1;
 }
 
@@ -548,77 +525,14 @@ static void flush_and_free_pb(struct para_buffer *pb)
        free(pb->buf);
 }
 
-static int com_select_callback(struct afs_callback_arg *aca)
-{
-       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT);
-       const char *arg;
-       int num_admissible, ret;
-       char *errmsg;
-
-       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
-       assert(ret >= 0);
-       arg = lls_input(0, aca->lpr);
-       ret = clear_score_table();
-       if (ret < 0) {
-               para_printf(&aca->pbout, "could not clear score table\n");
-               goto free_lpr;
-       }
-       if (current_play_mode == PLAY_MODE_MOOD)
-               close_current_mood();
-       else
-               playlist_close();
-       ret = activate_mood_or_playlist(arg, &num_admissible, &errmsg);
-       if (ret >= 0)
-               goto out;
-       /* ignore subsequent errors (but log them) */
-       para_printf(&aca->pbout, "%s\n", errmsg);
-       free(errmsg);
-       para_printf(&aca->pbout, "could not activate %s\n", arg);
-       if (current_mop && strcmp(current_mop, arg) != 0) {
-               int ret2;
-               para_printf(&aca->pbout, "switching back to %s\n", current_mop);
-               ret2 = activate_mood_or_playlist(current_mop, &num_admissible,
-                       &errmsg);
-               if (ret2 >= 0)
-                       goto out;
-               para_printf(&aca->pbout, "%s\n", errmsg);
-               free(errmsg);
-               para_printf(&aca->pbout, "could not reactivate %s: %s\n",
-                       current_mop, para_strerror(-ret2));
-       }
-       para_printf(&aca->pbout, "activating dummy mood\n");
-       activate_mood_or_playlist(NULL, &num_admissible, NULL);
-out:
-       para_printf(&aca->pbout, "activated %s (%d admissible file%s)\n",
-               current_mop? current_mop : "dummy mood", num_admissible,
-                       num_admissible == 1? "" : "s");
-free_lpr:
-       lls_free_parse_result(aca->lpr, cmd);
-       return ret;
-}
-
-static int com_select(struct command_context *cc, struct lls_parse_result *lpr)
-{
-       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT);
-       char *errctx;
-       int ret = lls(lls_check_arg_count(lpr, 1, 1, &errctx));
-
-       if (ret < 0) {
-               send_errctx(cc, errctx);
-               return ret;
-       }
-       return send_lls_callback_request(com_select_callback, cmd, lpr, cc);
-}
-EXPORT_SERVER_CMD_HANDLER(select);
-
 static void init_admissible_files(const char *arg)
 {
-       int ret = activate_mood_or_playlist(arg, NULL, NULL);
+       int ret = activate_mood_or_playlist(arg, NULL);
        if (ret < 0) {
-               assert(arg);
-               PARA_WARNING_LOG("could not activate %s: %s\n", arg,
-                       para_strerror(-ret));
-               activate_mood_or_playlist(NULL, NULL, NULL);
+               PARA_WARNING_LOG("could not activate %s: %s\n", arg?
+                       arg : "dummy", para_strerror(-ret));
+               if (arg)
+                       activate_mood_or_playlist(NULL, NULL);
        }
 }
 
@@ -644,9 +558,9 @@ static char *database_dir;
 static void close_afs_tables(void)
 {
        int i;
-       PARA_NOTICE_LOG("closing afs_tables\n");
+       PARA_NOTICE_LOG("closing afs tables\n");
        for (i = 0; i < NUM_AFS_TABLES; i++)
-               afs_tables[i].close();
+               afs_tables[i].ops->close();
        free(database_dir);
        database_dir = NULL;
 }
@@ -659,33 +573,22 @@ static void get_database_dir(void)
                else {
                        char *home = para_homedir();
                        database_dir = make_message(
-                               "%s/.paraslash/afs_database-0.4", home);
+                               "%s/.paraslash/afs_database-0.7", home);
                        free(home);
                }
        }
        PARA_INFO_LOG("afs_database dir %s\n", database_dir);
 }
 
-static int make_database_dir(void)
-{
-       int ret;
-
-       get_database_dir();
-       ret = para_mkdir(database_dir, 0777);
-       if (ret >= 0 || ret == -ERRNO_TO_PARA_ERROR(EEXIST))
-               return 1;
-       return ret;
-}
-
 static int open_afs_tables(void)
 {
        int i, ret;
 
        get_database_dir();
-       PARA_NOTICE_LOG("opening %d osl tables in %s\n", NUM_AFS_TABLES,
+       PARA_NOTICE_LOG("opening %zu osl tables in %s\n", NUM_AFS_TABLES,
                database_dir);
        for (i = 0; i < NUM_AFS_TABLES; i++) {
-               ret = afs_tables[i].open(database_dir);
+               ret = afs_tables[i].ops->open(database_dir);
                if (ret >= 0)
                        continue;
                PARA_ERROR_LOG("could not open %s\n", afs_tables[i].name);
@@ -694,11 +597,11 @@ static int open_afs_tables(void)
        if (ret >= 0)
                return ret;
        while (i)
-               afs_tables[--i].close();
+               afs_tables[--i].ops->close();
        return ret;
 }
 
-static int afs_signal_post_select(struct sched *s, __a_unused void *context)
+static int afs_signal_post_monitor(struct sched *s, __a_unused void *context)
 {
        int signum, ret;
 
@@ -706,7 +609,7 @@ static int afs_signal_post_select(struct sched *s, __a_unused void *context)
                PARA_EMERG_LOG("para_server died\n");
                goto shutdown;
        }
-       signum = para_next_signal(&s->rfds);
+       signum = para_next_signal();
        if (signum == 0)
                return 0;
        if (signum == SIGHUP) {
@@ -734,8 +637,8 @@ static void register_signal_task(struct sched *s)
 
        signal_task->task = task_register(&(struct task_info) {
                .name = "signal",
-               .pre_select = signal_pre_select,
-               .post_select = afs_signal_post_select,
+               .pre_monitor = signal_pre_monitor,
+               .post_monitor = afs_signal_post_monitor,
                .context = signal_task,
 
        }, s);
@@ -753,15 +656,15 @@ struct afs_client {
        struct timeval connect_time;
 };
 
-static void command_pre_select(struct sched *s, void *context)
+static void command_pre_monitor(struct sched *s, void *context)
 {
        struct command_task *ct = context;
        struct afs_client *client;
 
-       para_fd_set(server_socket, &s->rfds, &s->max_fileno);
-       para_fd_set(ct->fd, &s->rfds, &s->max_fileno);
+       sched_monitor_readfd(server_socket, s);
+       sched_monitor_readfd(ct->fd, s);
        list_for_each_entry(client, &afs_client_list, node)
-               para_fd_set(client->fd, &s->rfds, &s->max_fileno);
+               sched_monitor_readfd(client->fd, s);
 }
 
 /**
@@ -814,6 +717,43 @@ err:
        return ret;
 }
 
+/**
+ * Format and send an error message to the command handler.
+ *
+ * To pass an error message from the callback of an afs command to the client,
+ * this function should be called. It formats the message into a buffer which
+ * is passed as a shared memory area to the command handler from where it
+ * propagates to the client.
+ *
+ * The message will be tagged with the ERROR_LOG sideband designator so that
+ * the client writes it to its stderr stream rather than to stdout as with
+ * aca->pbout. In analogy to the default Unix semantics of stderr, the message
+ * is sent without buffering.
+ *
+ * If sending the error message fails, an error is logged on the server side,
+ * but no other action is taken.
+ *
+ * \param aca Used to obtain the fd to send the shmid to.
+ * \param fmt Usual format string.
+ */
+__printf_2_3 void afs_error(const struct afs_callback_arg *aca,
+               const char *fmt,...)
+{
+       va_list argp;
+       char *msg;
+       unsigned n;
+       int ret;
+
+       va_start(argp, fmt);
+       n = xvasprintf(&msg, fmt, argp);
+       va_end(argp);
+       ret = pass_buffer_as_shm(aca->fd, SBD_ERROR_LOG, msg, n + 1);
+       if (ret < 0)
+               PARA_ERROR_LOG("Could not send %s: %s\n", msg,
+                       para_strerror(-ret));
+       free(msg);
+}
+
 static int call_callback(int fd, int query_shmid)
 {
        void *query_shm;
@@ -833,7 +773,7 @@ static int call_callback(int fd, int query_shmid)
                .fd = fd,
                .band = SBD_OUTPUT
        };
-       ret = cq->handler(&aca);
+       ret = cq->cb(&aca);
        ret2 = shm_detach(query_shm);
        if (ret2 < 0) {
                if (ret < 0) /* ignore (but log) detach error */
@@ -853,11 +793,11 @@ static int call_callback(int fd, int query_shmid)
        return ret;
 }
 
-static int execute_server_command(fd_set *rfds)
+static int execute_server_command(void)
 {
        char buf[8];
        size_t n;
-       int ret = read_nonblock(server_socket, buf, sizeof(buf) - 1, rfds, &n);
+       int ret = read_nonblock(server_socket, buf, sizeof(buf) - 1, &n);
 
        if (ret < 0 || n == 0)
                return ret;
@@ -868,13 +808,13 @@ static int execute_server_command(fd_set *rfds)
 }
 
 /* returns 0 if no data available, 1 else */
-static int execute_afs_command(int fd, fd_set *rfds)
+static int execute_afs_command(int fd)
 {
        uint32_t cookie;
        int query_shmid;
        char buf[sizeof(cookie) + sizeof(query_shmid)];
        size_t n;
-       int ret = read_nonblock(fd, buf, sizeof(buf), rfds, &n);
+       int ret = read_nonblock(fd, buf, sizeof(buf), &n);
 
        if (ret < 0)
                goto err;
@@ -908,7 +848,7 @@ err:
 /** Shutdown connection if query has not arrived until this many seconds. */
 #define AFS_CLIENT_TIMEOUT 3
 
-static int command_post_select(struct sched *s, void *context)
+static int command_post_monitor(struct sched *s, void *context)
 {
        struct command_task *ct = context;
        struct sockaddr_un unix_addr;
@@ -918,7 +858,7 @@ static int command_post_select(struct sched *s, void *context)
        ret = task_get_notification(ct->task);
        if (ret < 0)
                return ret;
-       ret = execute_server_command(&s->rfds);
+       ret = execute_server_command();
        if (ret < 0) {
                PARA_EMERG_LOG("%s\n", para_strerror(-ret));
                task_notify_all(s, -ret);
@@ -926,7 +866,7 @@ static int command_post_select(struct sched *s, void *context)
        }
        /* Check the list of connected clients. */
        list_for_each_entry_safe(client, tmp, &afs_client_list, node) {
-               ret = execute_afs_command(client->fd, &s->rfds);
+               ret = execute_afs_command(client->fd);
                if (ret == 0) { /* prevent bogus connection flooding */
                        struct timeval diff;
                        tv_diff(now, &client->connect_time, &diff);
@@ -939,7 +879,7 @@ static int command_post_select(struct sched *s, void *context)
                free(client);
        }
        /* Accept connections on the local socket. */
-       ret = para_accept(ct->fd, &s->rfds, &unix_addr, sizeof(unix_addr), &fd);
+       ret = para_accept(ct->fd, &unix_addr, sizeof(unix_addr), &fd);
        if (ret < 0)
                PARA_NOTICE_LOG("%s\n", para_strerror(-ret));
        if (ret <= 0)
@@ -950,7 +890,7 @@ static int command_post_select(struct sched *s, void *context)
                close(fd);
                return 0;
        }
-       client = para_malloc(sizeof(*client));
+       client = alloc(sizeof(*client));
        client->fd = fd;
        client->connect_time = *now;
        para_list_add(&client->node, &afs_client_list);
@@ -964,12 +904,20 @@ static void register_command_task(struct sched *s)
 
        ct->task = task_register(&(struct task_info) {
                .name = "afs command",
-               .pre_select = command_pre_select,
-               .post_select = command_post_select,
+               .pre_monitor = command_pre_monitor,
+               .post_monitor = command_post_monitor,
                .context = ct,
        }, s);
 }
 
+static int afs_poll(struct pollfd *fds, nfds_t nfds, int timeout)
+{
+       mutex_lock(mmd_mutex);
+       daemon_set_loglevel(mmd->loglevel);
+       mutex_unlock(mmd_mutex);
+       return xpoll(fds, nfds, timeout);
+}
+
 /**
  * Initialize the audio file selector process.
  *
@@ -978,12 +926,10 @@ static void register_command_task(struct sched *s)
 __noreturn void afs_init(int socket_fd)
 {
        static struct sched s;
-       int i, ret;
+       int ret;
 
        register_signal_task(&s);
-       INIT_LIST_HEAD(&afs_client_list);
-       for (i = 0; i < NUM_AFS_TABLES; i++)
-               afs_tables[i].init(&afs_tables[i]);
+       init_list_head(&afs_client_list);
        ret = open_afs_tables();
        if (ret < 0)
                goto out;
@@ -994,8 +940,8 @@ __noreturn void afs_init(int socket_fd)
        PARA_INFO_LOG("server_socket: %d\n", server_socket);
        init_admissible_files(OPT_STRING_VAL(AFS_INITIAL_MODE));
        register_command_task(&s);
-       s.default_timeout.tv_sec = 0;
-       s.default_timeout.tv_usec = 999 * 1000;
+       s.default_timeout = 1000;
+       s.poll_function = afs_poll;
        ret = write(socket_fd, "\0", 1);
        if (ret != 1) {
                if (ret == 0)
@@ -1005,7 +951,8 @@ __noreturn void afs_init(int socket_fd)
        }
        ret = schedule(&s);
        sched_shutdown(&s);
-       close_current_mood();
+       mood_unload(NULL);
+       playlist_unload(NULL);
 out_close:
        close_afs_tables();
 out:
@@ -1018,6 +965,57 @@ out:
        exit(EXIT_FAILURE);
 }
 
+static int com_select_callback(struct afs_callback_arg *aca)
+{
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT);
+       const char *arg;
+       int ret;
+       struct para_buffer *pbout;
+
+       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
+       assert(ret >= 0);
+       arg = lls_input(0, aca->lpr);
+       pbout = SERVER_CMD_OPT_GIVEN(SELECT, VERBOSE, aca->lpr)?
+               &aca->pbout : NULL;
+       score_clear();
+       if (current_play_mode == PLAY_MODE_MOOD)
+               mood_unload(NULL);
+       else
+               playlist_unload(NULL);
+       ret = activate_mood_or_playlist(arg, pbout);
+       if (ret >= 0)
+               goto free_lpr;
+       /* ignore subsequent errors (but log them) */
+       if (current_mop && strcmp(current_mop, arg) != 0) {
+               int ret2;
+               afs_error(aca, "switching back to %s\n", current_mop);
+               ret2 = activate_mood_or_playlist(current_mop, pbout);
+               if (ret2 >= 0)
+                       goto free_lpr;
+               afs_error(aca, "could not reactivate %s: %s\n", current_mop,
+                       para_strerror(-ret2));
+       }
+       activate_mood_or_playlist(NULL, pbout);
+free_lpr:
+       lls_free_parse_result(aca->lpr, cmd);
+       return ret;
+}
+
+static int com_select(struct command_context *cc, struct lls_parse_result *lpr)
+{
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SELECT);
+       char *errctx;
+       int ret = lls(lls_check_arg_count(lpr, 1, 1, &errctx));
+
+       if (ret < 0) {
+               send_errctx(cc, errctx);
+               return ret;
+       }
+       ret = send_lls_callback_request(com_select_callback, cmd, lpr, cc);
+       return ret == osl(-E_OSL_RB_KEY_NOT_FOUND)? -E_BAD_MOP : ret;
+}
+EXPORT_SERVER_CMD_HANDLER(select);
+
 static int com_init_callback(struct afs_callback_arg *aca)
 {
        uint32_t table_mask = *(uint32_t *)aca->query.data;
@@ -1026,16 +1024,15 @@ static int com_init_callback(struct afs_callback_arg *aca)
        close_afs_tables();
        get_database_dir();
        for (i = 0; i < NUM_AFS_TABLES; i++) {
-               struct afs_table *t = &afs_tables[i];
+               const struct afs_table *t = afs_tables + i;
 
                if (!(table_mask & (1 << i)))
                        continue;
-               if (!t->create)
+               if (!t->ops->create)
                        continue;
-               ret = t->create(database_dir);
+               ret = t->ops->create(database_dir);
                if (ret < 0) {
-                       para_printf(&aca->pbout, "cannot create table %s\n",
-                               t->name);
+                       afs_error(aca, "cannot create table %s\n", t->name);
                        goto out;
                }
                para_printf(&aca->pbout, "successfully created %s table\n",
@@ -1043,7 +1040,8 @@ static int com_init_callback(struct afs_callback_arg *aca)
        }
        ret = open_afs_tables();
        if (ret < 0)
-               para_printf(&aca->pbout, "cannot open afs tables\n");
+               afs_error(aca, "cannot open afs tables: %s\n",
+                       para_strerror(-ret));
 out:
        return ret;
 }
@@ -1056,14 +1054,15 @@ static int com_init(struct command_context *cc, struct lls_parse_result *lpr)
                .size = sizeof(table_mask)};
        unsigned num_inputs = lls_num_inputs(lpr);
 
-       ret = make_database_dir();
+       get_database_dir();
+       ret = para_mkdir(database_dir);
        if (ret < 0)
                return ret;
        if (num_inputs > 0) {
                table_mask = 0;
                for (i = 0; i < num_inputs; i++) {
                        for (j = 0; j < NUM_AFS_TABLES; j++) {
-                               struct afs_table *t = &afs_tables[j];
+                               const struct afs_table *t = afs_tables + j;
 
                                if (strcmp(lls_input(i, lpr), t->name))
                                        continue;
@@ -1137,10 +1136,10 @@ __must_check int afs_event(enum afs_events event, struct para_buffer *pb,
        int i, ret;
 
        for (i = 0; i < NUM_AFS_TABLES; i++) {
-               struct afs_table *t = &afs_tables[i];
-               if (!t->event_handler)
+               const struct afs_table *t = afs_tables + i;
+               if (!t->ops->event_handler)
                        continue;
-               ret = t->event_handler(event, pb, data);
+               ret = t->ops->event_handler(event, pb, data);
                if (ret < 0) {
                        PARA_CRIT_LOG("table %s, event %u: %s\n", t->name,
                                event, para_strerror(-ret));