X-Git-Url: http://git.tuebingen.mpg.de/?a=blobdiff_plain;f=afs.c;h=0946b6df3b0766f82f95f0cd100f039753543350;hb=52fb766fd183e555d8c85185e35a4dc365097756;hp=f5de6609e4f9c87058676068f3172888106393b9;hpb=c85690666e2ed2327e751b819970658d58479bfb;p=paraslash.git diff --git a/afs.c b/afs.c index f5de6609..0946b6df 100644 --- a/afs.c +++ b/afs.c @@ -1,107 +1,1243 @@ /* - * Copyright (C) 2005-2007 Andre Noll + * Copyright (C) 2007 Andre Noll * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111, USA. + * Licensed under the GPL v2. For licencing details see COPYING. */ +/** \file afs.c Paraslash's audio file selector. */ -/** \file afs.c functions common to all audio file selectors */ +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "server.cmdline.h" -#include "server.h" -#include "vss.h" -#include /* readdir() */ -#include /* stat */ -#include /* mode_t */ +#include "para.h" #include "error.h" +#include "crypt.h" #include "string.h" +#include "afh.h" +#include "afs.h" +#include "server.h" +#include "net.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 blob.c. */ +enum afs_table_num { + /** Contains audio file information. See aft.c. */ + TBLNUM_AUDIO_FILES, + /** The table for the paraslash attributes. See 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 score.c for + * details. + */ + TBLNUM_SCORES, + /** + * A standard blob table containing the mood definitions. For details + * see 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"}, +}; + +struct command_task { + /** The file descriptor for the local socket. */ + int fd; + /** + * Value sent by the command handlers to identify themselves as + * children of the running para_server. + */ + uint32_t cookie; + /** The associated task structure. */ + struct task *task; +}; + +extern int mmd_mutex; +extern struct misc_meta_data *mmd; + +static int server_socket; +static struct command_task command_task_struct; +static struct signal_task *signal_task; + +static enum play_mode current_play_mode; +static char *current_mop; /* mode or playlist specifier. NULL means dummy mood */ + +/** + * A random number used to "authenticate" the connection. + * + * para_server picks this number by random before it forks the afs process. The + * command handlers know this number as well and write it to the afs socket, + * together with the id of the shared memory area which contains the payload of + * the afs command. A local process has to know this number to abuse the afs + * service provided by the local socket. + */ +extern uint32_t afs_socket_cookie; /** - * traverse the given directory recursively + * Struct to let command handlers execute a callback in afs context. * - * @param dirname the directory to traverse - * @param f: the function to call for each entry. + * 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. * - * for each regular file whose filename ends in .yyy, where yyy is a supported - * audio format, the supplied function \a f is called. The directory and - * filename component of the regular file are passed to \a f. + * 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. * - * \return On success, 1 is returned. Otherwise, this function returns a - * negative value which indicates the kind of the error. + * 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 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. + * + * \sa struct callback_result. */ -int find_audio_files(const char *dirname, int (*f)(const char *, const char *)) +struct callback_query { + /** The function to be called. */ + afs_callback *handler; + /** The number of bytes of the query */ + size_t query_size; +}; + +/** + * Structure embedded in the result of a callback. + * + * If the callback produced a result, an instance of that structure is embedded + * into the shared memory area holding the result, mainly to let the command + * handler know the size of the result. + * + * \sa struct callback_query. + */ +struct callback_result { + /** The number of bytes of the result. */ + size_t result_size; + /** The band designator (loglevel for the result). */ + uint8_t band; +}; + +static int dispatch_result(int result_shmid, callback_result_handler *handler, + void *private_result_data) { - DIR *dir = NULL; - struct dirent *entry; + struct osl_object result; + void *result_shm; + /* must attach r/w as result.data might get encrypted in-place. */ + int ret2, ret = shm_attach(result_shmid, ATTACH_RW, &result_shm); + struct callback_result *cr = result_shm; + + if (ret < 0) { + PARA_ERROR_LOG("attach failed: %s\n", para_strerror(-ret)); + return ret; + } + result.size = cr->result_size; + result.data = result_shm + sizeof(*cr); + assert(handler); + ret = handler(&result, cr->band, private_result_data); + ret2 = shm_detach(result_shm); + if (ret2 < 0) { + PARA_ERROR_LOG("detach failed: %s\n", para_strerror(-ret2)); + if (ret >= 0) + ret = ret2; + } + return ret; +} + +/** + * Ask the afs process to call a given function. + * + * \param f The function to be called. + * \param query Pointer to arbitrary data for the callback. + * \param result_handler Called for each shm area sent by the callback. + * \param private_result_data Passed verbatim to \a result_handler. + * + * This function creates a socket for communication with the afs process and a + * shared memory area (sma) to which the buffer pointed to by \a query is + * copied. It then notifies the afs process that the callback function \a f + * should be executed by sending the shared memory identifier (shmid) to the + * socket. + * + * If the callback produces a result, it sends any number of shared memory + * identifiers back via the socket. For each such identifier received, \a + * result_handler is called. The contents of the sma identified by the received + * shmid are passed to that function as an osl object. The private_result_data + * pointer is passed as the second argument to \a result_handler. + * + * \return Number of shared memory areas dispatched on success, negative on errors. + * + * \sa send_option_arg_callback_request(), send_standard_callback_request(). + */ +int send_callback_request(afs_callback *f, struct osl_object *query, + callback_result_handler *result_handler, + void *private_result_data) +{ + struct callback_query *cq; + int ret, fd = -1, query_shmid, result_shmid; + void *query_shm; + char buf[sizeof(afs_socket_cookie) + sizeof(int)]; + size_t query_shm_size = sizeof(*cq); + int dispatch_error = 0, num_dispatched = 0; + + if (query) + query_shm_size += query->size; + ret = shm_new(query_shm_size); + if (ret < 0) + return ret; + query_shmid = ret; + ret = shm_attach(query_shmid, ATTACH_RW, &query_shm); + if (ret < 0) + goto out; + cq = query_shm; + cq->handler = f; + cq->query_size = query_shm_size - sizeof(*cq); + + if (query) + memcpy(query_shm + sizeof(*cq), query->data, query->size); + ret = shm_detach(query_shm); + if (ret < 0) + goto out; + + *(uint32_t *)buf = afs_socket_cookie; + *(int *)(buf + sizeof(afs_socket_cookie)) = query_shmid; + + ret = connect_local_socket(conf.afs_socket_arg); + if (ret < 0) + goto out; + fd = ret; + ret = write_all(fd, buf, sizeof(buf)); + if (ret < 0) + goto out; /* - * Opening the current directory (".") and calling fchdir() to return - * is usually faster and more reliable than saving cwd in some buffer - * and calling chdir() afterwards (see man 3 getcwd). + * Read all shmids from afs. + * + * Even if the dispatcher returns an error we _must_ continue to read + * shmids from fd so that we can destroy all shared memory areas that + * have been created for us by the afs process. */ - int cwd_fd = open(".", O_RDONLY); - struct stat s; - int ret = -1; - -// PARA_DEBUG_LOG("dirname: %s\n", dirname); - if (cwd_fd < 0) - return -E_GETCWD; - ret = -E_CHDIR; - if (chdir(dirname) < 0) + for (;;) { + ret = recv_bin_buffer(fd, buf, sizeof(int)); + if (ret <= 0) + goto out; + assert(ret == sizeof(int)); + ret = *(int *) buf; + assert(ret > 0); + result_shmid = ret; + ret = dispatch_result(result_shmid, result_handler, + private_result_data); + if (ret < 0 && dispatch_error >= 0) + dispatch_error = ret; + ret = shm_destroy(result_shmid); + if (ret < 0) + PARA_CRIT_LOG("destroy result failed: %s\n", + para_strerror(-ret)); + num_dispatched++; + } +out: + if (shm_destroy(query_shmid) < 0) + PARA_CRIT_LOG("shm destroy error\n"); + if (fd >= 0) + close(fd); + if (dispatch_error < 0) + return dispatch_error; + if (ret < 0) + return ret; + return num_dispatched; +} + +/** + * Send a callback request passing an options structure and an argument vector. + * + * \param options pointer to an arbitrary data structure. + * \param argc Argument count. + * \param argv Standard argument vector. + * \param f The callback function. + * \param result_handler See \ref send_callback_request. + * \param private_result_data See \ref send_callback_request. + * + * Some command handlers pass command-specific options to a callback, together + * with a list of further arguments (often a list of audio files). This + * function allows to pass an arbitrary structure (given as an osl object) and + * a usual argument vector to the specified callback. + * + * \return The return value of the underlying call to \ref + * send_callback_request(). + * + * \sa send_standard_callback_request(), send_callback_request(). + */ +int send_option_arg_callback_request(struct osl_object *options, + int argc, char * const * const argv, afs_callback *f, + callback_result_handler *result_handler, + void *private_result_data) +{ + char *p; + int i, ret; + struct osl_object query = {.size = options? options->size : 0}; + + for (i = 0; i < argc; i++) + query.size += strlen(argv[i]) + 1; + query.data = para_malloc(query.size); + p = query.data; + if (options) { + memcpy(query.data, options->data, options->size); + p += options->size; + } + for (i = 0; i < argc; i++) { + strcpy(p, argv[i]); /* OK */ + p += strlen(argv[i]) + 1; + } + ret = send_callback_request(f, &query, result_handler, + private_result_data); + free(query.data); + return ret; +} + +/** + * Send a callback request with an argument vector only. + * + * \param argc The same meaning as in send_option_arg_callback_request(). + * \param argv The same meaning as in send_option_arg_callback_request(). + * \param f The same meaning as in send_option_arg_callback_request(). + * \param result_handler See \ref send_callback_request. + * \param private_result_data See \ref send_callback_request. + * + * This is similar to send_option_arg_callback_request(), but no options buffer + * is passed to the parent process. + * + * \return The return value of the underlying call to + * send_option_arg_callback_request(). + */ +int send_standard_callback_request(int argc, char * const * const argv, + afs_callback *f, callback_result_handler *result_handler, + void *private_result_data) +{ + return send_option_arg_callback_request(NULL, argc, argv, f, result_handler, + private_result_data); +} + +static int action_if_pattern_matches(struct osl_row *row, void *data) +{ + struct pattern_match_data *pmd = data; + struct osl_object name_obj; + const char *p, *name; + int ret = osl(osl_get_object(pmd->table, row, pmd->match_col_num, &name_obj)); + const char *pattern_txt = (const char *)pmd->patterns.data; + + if (ret < 0) + return ret; + name = (char *)name_obj.data; + if ((!name || !*name) && (pmd->pm_flags & PM_SKIP_EMPTY_NAME)) + return 1; + if (pmd->patterns.size == 0 && + (pmd->pm_flags & PM_NO_PATTERN_MATCHES_EVERYTHING)) { + pmd->num_matches++; + return pmd->action(pmd->table, row, name, pmd->data); + } + for (p = pattern_txt; p < pattern_txt + pmd->patterns.size; + p += strlen(p) + 1) { + ret = fnmatch(p, name, pmd->fnmatch_flags); + if (ret == FNM_NOMATCH) + continue; + if (ret) + return -E_FNMATCH; + ret = pmd->action(pmd->table, row, name, pmd->data); + if (ret >= 0) + pmd->num_matches++; + return ret; + } + return 1; +} + +/** + * Execute the given function for each matching row. + * + * \param pmd Describes what to match and how. + * + * \return Standard. + */ +int for_each_matching_row(struct pattern_match_data *pmd) +{ + if (pmd->pm_flags & PM_REVERSE_LOOP) + return osl(osl_rbtree_loop_reverse(pmd->table, pmd->loop_col_num, pmd, + action_if_pattern_matches)); + return osl(osl_rbtree_loop(pmd->table, pmd->loop_col_num, pmd, + action_if_pattern_matches)); +} + +/** + * Compare two osl objects of string type. + * + * \param obj1 Pointer to the first object. + * \param obj2 Pointer to the second object. + * + * In any case, only \p MIN(obj1->size, obj2->size) characters of each string + * are taken into account. + * + * \return It returns an integer less than, equal to, or greater than zero if + * \a obj1 is found, respectively, to be less than, to match, or be greater than + * obj2. + * + * \sa strcmp(3), strncmp(3), osl_compare_func. + */ +int string_compare(const struct osl_object *obj1, const struct osl_object *obj2) +{ + const char *str1 = (const char *)obj1->data; + const char *str2 = (const char *)obj2->data; + return strncmp(str1, str2, PARA_MIN(obj1->size, obj2->size)); +} + +static int pass_afd(int fd, char *buf, size_t size) +{ + struct msghdr msg = {.msg_iov = NULL}; + struct cmsghdr *cmsg; + char control[255] __a_aligned(8); + int ret; + struct iovec iov; + + iov.iov_base = buf; + iov.iov_len = size; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + *(int *)CMSG_DATA(cmsg) = fd; + + /* Sum of the length of all control messages in the buffer */ + msg.msg_controllen = cmsg->cmsg_len; + PARA_DEBUG_LOG("passing %zu bytes and fd %d\n", size, fd); + ret = sendmsg(server_socket, &msg, 0); + if (ret < 0) { + ret = -ERRNO_TO_PARA_ERROR(errno); + return ret; + } + return 1; +} + +/** + * Pass the fd of the next audio file to the server process. + * + * This stores all information for streaming the "best" audio file in a shared + * memory area. The id of that area and an open file descriptor for the next + * audio file are passed to the server process. + * + * \return Standard. + * + * \sa open_and_update_audio_file(). + */ +static int open_next_audio_file(void) +{ + struct audio_file_data afd; + int ret, shmid; + char buf[8]; + + ret = open_and_update_audio_file(&afd); + if (ret < 0) { + PARA_ERROR_LOG("%s\n", para_strerror(-ret)); + goto no_admissible_files; + } + shmid = ret; + if (!write_ok(server_socket)) { + ret = -E_AFS_SOCKET; + goto destroy; + } + *(uint32_t *)buf = NEXT_AUDIO_FILE; + *(uint32_t *)(buf + 4) = (uint32_t)shmid; + ret = pass_afd(afd.fd, buf, 8); + close(afd.fd); + if (ret >= 0) + return ret; +destroy: + shm_destroy(shmid); + return ret; +no_admissible_files: + *(uint32_t *)buf = NO_ADMISSIBLE_FILES; + *(uint32_t *)(buf + 4) = (uint32_t)0; + return write_all(server_socket, buf, 8); +} + +/* Never fails if arg == NULL */ +static int activate_mood_or_playlist(const char *arg, int *num_admissible) +{ + enum play_mode mode; + int ret; + + if (!arg) { + ret = change_current_mood(NULL); /* always successful */ + mode = PLAY_MODE_MOOD; + } else { + if (!strncmp(arg, "p/", 2)) { + ret = playlist_open(arg + 2); + mode = PLAY_MODE_PLAYLIST; + } else if (!strncmp(arg, "m/", 2)) { + ret = change_current_mood(arg + 2); + mode = PLAY_MODE_MOOD; + } else + return -E_AFS_SYNTAX; + if (ret < 0) + return ret; + } + if (num_admissible) + *num_admissible = ret; + current_play_mode = mode; + 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'; + mutex_unlock(mmd_mutex); + } else { + mutex_lock(mmd_mutex); + strcpy(mmd->afs_mode_string, "dummy"); + mutex_unlock(mmd_mutex); + current_mop = NULL; + } + } + return 1; +} + +/** + * Result handler for sending data to the para_client process. + * + * \param result The data to be sent. + * \param band The band designator. + * \param private Pointer to the command context. + * + * \return The return value of the underlying call to \ref command.c::send_sb. + * + * \sa \ref callback_result_handler, \ref command.c::send_sb. + */ +int afs_cb_result_handler(struct osl_object *result, uint8_t band, + void *private) +{ + struct command_context *cc = private; + + assert(cc); + switch (band) { + case SBD_OUTPUT: + case SBD_DEBUG_LOG: + case SBD_INFO_LOG: + case SBD_NOTICE_LOG: + case SBD_WARNING_LOG: + case SBD_ERROR_LOG: + case SBD_CRIT_LOG: + case SBD_EMERG_LOG: + assert(result->size > 0); + return send_sb(&cc->scc, result->data, result->size, band, true); + case SBD_AFS_CB_FAILURE: + return *(int *)(result->data); + default: + return -E_BAD_BAND; + } +} + +static void flush_and_free_pb(struct para_buffer *pb) +{ + int ret; + struct afs_max_size_handler_data *amshd = pb->private_data; + + if (pb->buf && pb->size > 0) { + ret = pass_buffer_as_shm(amshd->fd, amshd->band, pb->buf, + pb->offset); + if (ret < 0) + PARA_ERROR_LOG("%s\n", para_strerror(-ret)); + } + free(pb->buf); +} + +static int com_select_callback(struct afs_callback_arg *aca) +{ + const char *arg = aca->query.data; + int num_admissible, ret; + + ret = clear_score_table(); + if (ret < 0) { + para_printf(&aca->pbout, "could not clear score table: %s\n", + para_strerror(-ret)); + return ret; + } + if (current_play_mode == PLAY_MODE_MOOD) + close_current_mood(); + else + playlist_close(); + ret = activate_mood_or_playlist(arg, &num_admissible); + if (ret >= 0) goto out; - ret = -E_OPENDIR; - dir = opendir("."); - if (!dir) + /* ignore subsequent errors (but log them) */ + para_printf(&aca->pbout, "could not activate %s\n", arg); + if (current_mop) { + int ret2; + para_printf(&aca->pbout, "switching back to %s\n", current_mop); + ret2 = activate_mood_or_playlist(current_mop, &num_admissible); + if (ret2 >= 0) + goto out; + 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); +out: + para_printf(&aca->pbout, "activated %s (%d admissible files)\n", + current_mop? current_mop : "dummy mood", num_admissible); + return ret; +} + +int com_select(struct command_context *cc) +{ + struct osl_object query; + + if (cc->argc != 2) + return -E_AFS_SYNTAX; + query.data = cc->argv[1]; + query.size = strlen(cc->argv[1]) + 1; + return send_callback_request(com_select_callback, &query, + &afs_cb_result_handler, cc); +} + +static void init_admissible_files(char *arg) +{ + if (activate_mood_or_playlist(arg, NULL) < 0) + activate_mood_or_playlist(NULL, NULL); /* always successful */ +} + +static int setup_command_socket_or_die(void) +{ + int ret, socket_fd; + char *socket_name = conf.afs_socket_arg; + + unlink(socket_name); + ret = create_local_socket(socket_name, 0); + if (ret < 0) { + ret = create_local_socket(socket_name, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IROTH); + if (ret < 0) { + PARA_EMERG_LOG("%s: %s\n", para_strerror(-ret), + socket_name); + exit(EXIT_FAILURE); + } + } + socket_fd = ret; + PARA_INFO_LOG("listening on socket %s (fd %d)\n", socket_name, + socket_fd); + return socket_fd; +} + +static void close_afs_tables(void) +{ + int i; + PARA_NOTICE_LOG("closing afs_tables\n"); + for (i = 0; i < NUM_AFS_TABLES; i++) + afs_tables[i].close(); +} + +static char *database_dir; + +static void get_database_dir(void) +{ + if (!database_dir) { + if (conf.afs_database_dir_given) + database_dir = para_strdup(conf.afs_database_dir_arg); + else { + char *home = para_homedir(); + database_dir = make_message( + "%s/.paraslash/afs_database-0.4", 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, + database_dir); + for (i = 0; i < NUM_AFS_TABLES; i++) { + ret = afs_tables[i].open(database_dir); + if (ret >= 0) + continue; + PARA_ERROR_LOG("%s init: %s\n", afs_tables[i].name, + para_strerror(-ret)); + break; + } + if (ret >= 0) + return ret; + while (i) + afs_tables[--i].close(); + return ret; +} + +static int afs_signal_post_select(struct sched *s, __a_unused void *context) +{ + int signum, ret; + + if (getppid() == 1) { + PARA_EMERG_LOG("para_server died\n"); + goto shutdown; + } + signum = para_next_signal(&s->rfds); + if (signum == 0) + return 0; + if (signum == SIGHUP) { + close_afs_tables(); + parse_config_or_die(1); + ret = open_afs_tables(); + if (ret < 0) + return ret; + init_admissible_files(current_mop); + return 0; + } + PARA_EMERG_LOG("terminating on signal %d\n", signum); +shutdown: + task_notify_all(s, E_AFS_SIGNAL); + return -E_AFS_SIGNAL; +} + +static void register_signal_task(struct sched *s) +{ + para_sigaction(SIGPIPE, SIG_IGN); + signal_task = signal_init_or_die(); + para_install_sighandler(SIGINT); + para_install_sighandler(SIGTERM); + para_install_sighandler(SIGHUP); + + signal_task->task = task_register(&(struct task_info) { + .name = "signal", + .pre_select = signal_pre_select, + .post_select = afs_signal_post_select, + .context = signal_task, + + }, s); +} + +static struct list_head afs_client_list; + +/** Describes one connected afs client. */ +struct afs_client { + /** Position in the afs client list. */ + struct list_head node; + /** The socket file descriptor for this client. */ + int fd; + /** The time the client connected. */ + struct timeval connect_time; +}; + +static void command_pre_select(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); + list_for_each_entry(client, &afs_client_list, node) + para_fd_set(client->fd, &s->rfds, &s->max_fileno); +} + +/** + * Send data as shared memory to a file descriptor. + * + * \param fd File descriptor to send the shmid to. + * \param band The band designator for this data. + * \param buf The buffer holding the data to be sent. + * \param size The size of \a buf. + * + * This function creates a shared memory area large enough to hold + * the content given by \a buf and \a size and sends the identifier + * of this area to the file descriptor \a fd. + * + * It is called by the AFS max_size handler as well as directly by the AFS + * command callbacks to send command output to the command handlers. + * + * \return Zero if \a buf is \p NULL or \a size is zero. Negative on errors, + * and positive on success. + */ +int pass_buffer_as_shm(int fd, uint8_t band, const char *buf, size_t size) +{ + int ret, shmid; + void *shm; + struct callback_result *cr; + + if (size == 0) + assert(band != SBD_OUTPUT); + ret = shm_new(size + sizeof(*cr)); + if (ret < 0) + return ret; + shmid = ret; + ret = shm_attach(shmid, ATTACH_RW, &shm); + if (ret < 0) + goto err; + cr = shm; + cr->result_size = size; + cr->band = band; + if (size > 0) + memcpy(shm + sizeof(*cr), buf, size); + ret = shm_detach(shm); + if (ret < 0) + goto err; + ret = write_all(fd, (char *)&shmid, sizeof(int)); + if (ret >= 0) + return ret; +err: + if (shm_destroy(shmid) < 0) + PARA_ERROR_LOG("destroy result failed\n"); + return ret; +} + +static int call_callback(int fd, int query_shmid) +{ + void *query_shm; + struct callback_query *cq; + int ret, ret2; + struct afs_callback_arg aca = {.fd = fd}; + + ret = shm_attach(query_shmid, ATTACH_RW, &query_shm); + if (ret < 0) + return ret; + cq = query_shm; + aca.query.data = (char *)query_shm + sizeof(*cq); + aca.query.size = cq->query_size; + aca.pbout.max_size = shm_get_shmmax(); + aca.pbout.max_size_handler = afs_max_size_handler; + aca.pbout.private_data = &(struct afs_max_size_handler_data) { + .fd = fd, + .band = SBD_OUTPUT + }; + ret = cq->handler(&aca); + ret2 = shm_detach(query_shm); + if (ret2 < 0) { + if (ret < 0) /* ignore (but log) detach error */ + PARA_ERROR_LOG("could not detach sma: %s\n", + para_strerror(-ret2)); + else + ret = ret2; + } + flush_and_free_pb(&aca.pbout); + if (ret < 0) { + ret2 = pass_buffer_as_shm(fd, SBD_AFS_CB_FAILURE, + (const char *)&ret, sizeof(ret)); + if (ret2 < 0) + PARA_ERROR_LOG("could not pass cb failure packet: %s\n", + para_strerror(-ret)); + } + return ret; +} + +static int execute_server_command(fd_set *rfds) +{ + char buf[8]; + size_t n; + int ret = read_nonblock(server_socket, buf, sizeof(buf) - 1, rfds, &n); + + if (ret < 0 || n == 0) + return ret; + buf[n] = '\0'; + if (strcmp(buf, "new")) + return -E_BAD_CMD; + return open_next_audio_file(); +} + +/* returns 0 if no data available, 1 else */ +static int execute_afs_command(int fd, fd_set *rfds, uint32_t expected_cookie) +{ + 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); + + if (ret < 0) + goto err; + if (n == 0) + return 0; + if (n != sizeof(buf)) { + PARA_NOTICE_LOG("short read (%d bytes, expected %lu)\n", + ret, (long unsigned) sizeof(buf)); + return 1; + } + cookie = *(uint32_t *)buf; + if (cookie != expected_cookie) { + PARA_NOTICE_LOG("received invalid cookie (got %u, expected %u)\n", + (unsigned)cookie, (unsigned)expected_cookie); + return 1; + } + query_shmid = *(int *)(buf + sizeof(cookie)); + if (query_shmid < 0) { + PARA_WARNING_LOG("received invalid query shmid %d)\n", + query_shmid); + return 1; + } + ret = call_callback(fd, query_shmid); + if (ret >= 0) + return 1; +err: + PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); + return 1; +} + +/** 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) +{ + struct command_task *ct = context; + struct sockaddr_un unix_addr; + struct afs_client *client, *tmp; + int fd, ret; + + ret = task_get_notification(ct->task); + if (ret < 0) + return ret; + ret = execute_server_command(&s->rfds); + if (ret < 0) { + PARA_EMERG_LOG("%s\n", para_strerror(-ret)); + task_notify_all(s, -ret); + return ret; + } + /* 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, ct->cookie); + if (ret == 0) { /* prevent bogus connection flooding */ + struct timeval diff; + tv_diff(now, &client->connect_time, &diff); + if (diff.tv_sec < AFS_CLIENT_TIMEOUT) + continue; + PARA_WARNING_LOG("connection timeout\n"); + } + close(client->fd); + list_del(&client->node); + free(client); + } + /* Accept connections on the local socket. */ + ret = para_accept(ct->fd, &s->rfds, &unix_addr, sizeof(unix_addr), &fd); + if (ret < 0) + PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); + if (ret <= 0) + return 0; + ret = mark_fd_nonblocking(fd); + if (ret < 0) { + PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); + close(fd); + return 0; + } + client = para_malloc(sizeof(*client)); + client->fd = fd; + client->connect_time = *now; + para_list_add(&client->node, &afs_client_list); + return 0; +} + +static void register_command_task(uint32_t cookie, struct sched *s) +{ + struct command_task *ct = &command_task_struct; + ct->fd = setup_command_socket_or_die(); + ct->cookie = cookie; + + ct->task = task_register(&(struct task_info) { + .name = "afs command", + .pre_select = command_pre_select, + .post_select = command_post_select, + .context = ct, + }, s); +} + +/** + * Initialize the audio file selector process. + * + * \param cookie The value used for "authentication". + * \param socket_fd File descriptor used for communication with the server. + */ +__noreturn void afs_init(uint32_t cookie, int socket_fd) +{ + static struct sched s; + int i, 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]); + ret = open_afs_tables(); + if (ret < 0) goto out; - /* scan cwd recursively */ - while ((entry = readdir(dir))) { - mode_t m; - char *tmp; + server_socket = socket_fd; + ret = mark_fd_nonblocking(server_socket); + if (ret < 0) + goto out_close; + PARA_INFO_LOG("server_socket: %d, afs_socket_cookie: %u\n", + server_socket, (unsigned) cookie); + init_admissible_files(conf.afs_initial_mode_arg); + register_command_task(cookie, &s); + s.default_timeout.tv_sec = 0; + s.default_timeout.tv_usec = 999 * 1000; + ret = write(socket_fd, "\0", 1); + if (ret != 1) { + if (ret == 0) + errno = EINVAL; + ret = -ERRNO_TO_PARA_ERROR(errno); + goto out_close; + } + ret = schedule(&s); + sched_shutdown(&s); +out_close: + close_afs_tables(); +out: + if (ret < 0) + PARA_EMERG_LOG("%s\n", para_strerror(-ret)); + exit(EXIT_FAILURE); +} - if (!strcmp(entry->d_name, ".")) +static int com_init_callback(struct afs_callback_arg *aca) +{ + uint32_t table_mask = *(uint32_t *)aca->query.data; + int i, ret; + + close_afs_tables(); + for (i = 0; i < NUM_AFS_TABLES; i++) { + struct afs_table *t = &afs_tables[i]; + + if (!(table_mask & (1 << i))) continue; - if (!strcmp(entry->d_name, "..")) + if (!t->create) + continue; + ret = t->create(database_dir); + if (ret < 0) { + para_printf(&aca->pbout, "cannot create table %s\n", + t->name); + goto out; + } + para_printf(&aca->pbout, "successfully created %s table\n", + t->name); + } + ret = open_afs_tables(); + if (ret < 0) + para_printf(&aca->pbout, "cannot open afs tables\n"); +out: + return ret; +} + +int com_init(struct command_context *cc) +{ + int i, j, ret; + uint32_t table_mask = (1 << (NUM_AFS_TABLES + 1)) - 1; + struct osl_object query = {.data = &table_mask, + .size = sizeof(table_mask)}; + + ret = make_database_dir(); + if (ret < 0) + return ret; + if (cc->argc != 1) { + table_mask = 0; + for (i = 1; i < cc->argc; i++) { + for (j = 0; j < NUM_AFS_TABLES; j++) { + struct afs_table *t = &afs_tables[j]; + + if (strcmp(cc->argv[i], t->name)) + continue; + table_mask |= (1 << j); + break; + } + if (j == NUM_AFS_TABLES) + return -E_BAD_TABLE_NAME; + } + } + return send_callback_request(com_init_callback, &query, + afs_cb_result_handler, cc); +} + +/** + * Flags for the check command. + * + * \sa com_check(). + */ +enum com_check_flags { + /** Check the audio file table. */ + CHECK_AFT = 1, + /** Check the mood table. */ + CHECK_MOODS = 2, + /** Check the playlist table. */ + CHECK_PLAYLISTS = 4, + /** Check the attribute table against the audio file table. */ + CHECK_ATTS = 8 +}; + +int com_check(struct command_context *cc) +{ + unsigned flags = 0; + int i, ret; + + for (i = 1; i < cc->argc; i++) { + const char *arg = cc->argv[i]; + if (arg[0] != '-') + break; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "-a")) { + flags |= CHECK_AFT; continue; - ret = -E_LSTAT; - if (lstat(entry->d_name, &s) == -1) + } + if (!strcmp(arg, "-A")) { + flags |= CHECK_ATTS; continue; - m = s.st_mode; - if (!S_ISREG(m) && !S_ISDIR(m)) /* skip links, sockets, ... */ + } + if (!strcmp(arg, "-p")) { + flags |= CHECK_PLAYLISTS; continue; - if (S_ISREG(m)) { /* regular file */ - if (guess_audio_format(entry->d_name) < 0) - continue; - ret = f(dirname, entry->d_name); - if (ret < 0) - goto out; + } + if (!strcmp(arg, "-m")) { + flags |= CHECK_MOODS; continue; } - /* directory */ - tmp = make_message("%s/%s", dirname, entry->d_name); - ret = find_audio_files(tmp, f); - free(tmp); + return -E_AFS_SYNTAX; + } + if (i < cc->argc) + return -E_AFS_SYNTAX; + if (!flags) + flags = ~0U; + if (flags & CHECK_AFT) { + ret = send_callback_request(aft_check_callback, NULL, + afs_cb_result_handler, cc); if (ret < 0) - goto out; + return ret; } - ret = 1; -out: - if (dir) - closedir(dir); - if (fchdir(cwd_fd) < 0) - ret = -E_CHDIR; - close(cwd_fd); - if (ret < 0) - PARA_ERROR_LOG("%s\n", PARA_STRERROR(-ret)); - return ret; + if (flags & CHECK_ATTS) { + ret = send_callback_request(attribute_check_callback, NULL, + afs_cb_result_handler, cc); + if (ret < 0) + return ret; + } + if (flags & CHECK_PLAYLISTS) { + ret = send_callback_request(playlist_check_callback, + NULL, afs_cb_result_handler, cc); + if (ret < 0) + return ret; + } + if (flags & CHECK_MOODS) { + ret = send_callback_request(mood_check_callback, NULL, + afs_cb_result_handler, cc); + if (ret < 0) + return ret; + } + return 1; +} + +/** + * The afs event dispatcher. + * + * \param event Type of the event. + * \param pb May be \p NULL. + * \param data Type depends on \a event. + * + * This function calls each table event handler, passing \a pb and \a data + * verbatim. It's up to the handlers to interpret the \a data pointer. If a + * handler returns negative, the loop is aborted. + * + * \return The (negative) error code of the first handler that failed, or non-negative + * if all handlers succeeded. + */ +__must_check int afs_event(enum afs_events event, struct para_buffer *pb, + void *data) +{ + int i, ret; + + for (i = 0; i < NUM_AFS_TABLES; i++) { + struct afs_table *t = &afs_tables[i]; + if (!t->event_handler) + continue; + ret = t->event_handler(event, pb, data); + if (ret < 0) { + PARA_CRIT_LOG("table %s, event %u: %s\n", t->name, + event, para_strerror(-ret)); + return ret; + } + } + return 1; +} + +/** + * Dummy event handler for the images table. + * + * \param event Unused. + * \param pb Unused. + * \param data Unused. + * + * \return The images table does not honor events, so this handler always + * returns success. + */ +__a_const int images_event_handler(__a_unused enum afs_events event, + __a_unused struct para_buffer *pb, __a_unused void *data) +{ + return 1; +} + +/** + * Dummy event handler for the lyrics table. + * + * \param event Unused. + * \param pb Unused. + * \param data Unused. + * + * \return The lyrics table does not honor events, so this handler always + * returns success. + */ +__a_const int lyrics_event_handler(__a_unused enum afs_events event, + __a_unused struct para_buffer *pb, __a_unused void *data) +{ + return 1; }