From: Andre Noll Date: Sat, 28 Oct 2023 17:20:14 +0000 (+0200) Subject: Merge topic branch t/ls-L into master X-Git-Tag: v0.7.3~10 X-Git-Url: http://git.tuebingen.mpg.de/?a=commitdiff_plain;h=624910afb851cf78669be188214b0332d5d5ee12;hp=be78128f5b0e9605de29724b620c9c54f312281e;p=paraslash.git Merge topic branch t/ls-L into master A single patch which implements the new --limit option for the ls server command. The confict in aft.c was trivial to fix. * refs/heads/t/ls-L: server: Implement ls --limit. --- diff --git a/NEWS.md b/NEWS.md index 598db71f..5430790b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,29 @@ NEWS ==== +---------------------------------------------- +0.7.3 (to be announced) "weighted correctness" +---------------------------------------------- + +- Old style PEM keys are now deprecated. They still work but their + use results in a run-time warning. The removal of PEM key support is + scheduled for paraslash-0.8.0. +- Version 1.0 of the openssl library has been deprecated. A warning + is printed at compile-time on systems which have this outdated version + because it will no longer be supported once paraslash-0.8.0 comes out. +- A spring cleanup for the senescent code in fd.c. +- The --admissible option of the ls command now takes an optional + argument. When invoked like --admissible=m/foo, only files which are + admissible with respect to mood foo are listed. +- The select server command is now quiet by default, The new --verbose + option can be used to show information about the newly loaded mood + or playlist. +- The ls server command gained the --limit option to force a limit + on the number of files listed. + +Downloads: +[tarball](./releases/paraslash-git.tar.xz) + ------------------------------------- 0.7.2 (2023-03-08) "optical friction" ------------------------------------- diff --git a/afs.c b/afs.c index 78e42eae..445d5871 100644 --- a/afs.c +++ b/afs.c @@ -437,18 +437,18 @@ static int activate_mood_or_playlist(const char *arg, struct para_buffer *pb) int ret; char *msg; - if (!arg) { - ret = mood_load(NULL, &msg); + 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, &msg); + ret = playlist_load(arg + 2, NULL, &msg); mode = PLAY_MODE_PLAYLIST; } else if (!strncmp(arg, "m/", 2)) { - ret = mood_load(arg + 2, &msg); + ret = mood_load(arg + 2, NULL, &msg); mode = PLAY_MODE_MOOD; } else { ret = -ERRNO_TO_PARA_ERROR(EINVAL); - msg = make_message("%s: parse error", arg); + msg = make_message("%s: parse error\n", arg); } if (pb) para_printf(pb, "%s", msg); @@ -580,17 +580,6 @@ static void get_database_dir(void) 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; @@ -962,7 +951,8 @@ __noreturn void afs_init(int socket_fd) } ret = schedule(&s); sched_shutdown(&s); - mood_unload(); + mood_unload(NULL); + playlist_unload(NULL); out_close: close_afs_tables(); out: @@ -980,29 +970,32 @@ 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(); + mood_unload(NULL); else - playlist_unload(); - ret = activate_mood_or_playlist(arg, &aca->pbout); + 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, &aca->pbout); + 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, &aca->pbout); + activate_mood_or_playlist(NULL, pbout); free_lpr: lls_free_parse_result(aca->lpr, cmd); return ret; @@ -1061,7 +1054,8 @@ 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) { diff --git a/afs.h b/afs.h index 9a1d7d9c..e8b8c865 100644 --- a/afs.h +++ b/afs.h @@ -238,10 +238,12 @@ int for_each_matching_row(struct pattern_match_data *pmd); /* score */ extern const struct afs_table_operations score_ops; -int score_loop(osl_rbtree_loop_func *func, void *data); +void score_open(struct osl_table **result); +void score_close(struct osl_table *t); +int score_loop(osl_rbtree_loop_func *func, struct osl_table *t, void *data); int score_get_best(struct osl_row **aft_row, long *score); int get_score_and_aft_row(struct osl_row *score_row, long *score, struct osl_row **aft_row); -int score_add(const struct osl_row *row, long score); +int score_add(const struct osl_row *aft_row, long score, struct osl_table *t); int score_update(const struct osl_row *aft_row, long new_score); int score_delete(const struct osl_row *aft_row); void score_clear(void); @@ -268,13 +270,17 @@ int aft_check_callback(struct afs_callback_arg *aca); void free_status_items(void); /* mood */ -int mood_load(const char *mood_name, char **msg); -void mood_unload(void); +struct mood_instance; +int mood_load(const char *mood_name, struct mood_instance **result, char **msg); +int mood_loop(struct mood_instance *m, osl_rbtree_loop_func *func, void *data); +void mood_unload(struct mood_instance *m); int mood_check_callback(struct afs_callback_arg *aca); /* playlist */ -int playlist_load(const char *name, char **msg); -void playlist_unload(void); +struct playlist_instance; +int playlist_load(const char *name, struct playlist_instance **result, char **msg); +int playlist_loop(struct playlist_instance *pi, osl_rbtree_loop_func *func, void *data); +void playlist_unload(struct playlist_instance *pi); int playlist_check_callback(struct afs_callback_arg *aca); /** evaluates to 1 if x < y, to -1 if x > y and to 0 if x == y */ diff --git a/aft.c b/aft.c index ff75361d..f1aca7fb 100644 --- a/aft.c +++ b/aft.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -1361,24 +1362,61 @@ err: return ret; } +static int mop_loop(const char *arg, struct afs_callback_arg *aca, + struct ls_options *opts) +{ + int ret; + char *msg; + + if (!arg || strcmp(arg, ".") == 0) + return score_loop(prepare_ls_row, NULL, opts); + if (!strncmp(arg, "m/", 2)) { + struct mood_instance *m; + ret = mood_load(arg + 2, &m, &msg); + if (ret < 0) + afs_error(aca, "%s", msg); + free(msg); + if (ret < 0) + return ret; + ret = mood_loop(m, prepare_ls_row, opts); + mood_unload(m); + return ret; + } + if (!strncmp(arg, "p/", 2)) { + struct playlist_instance *pi; + ret = playlist_load(arg + 2, &pi, &msg); + if (ret < 0) + afs_error(aca, "%s", msg); + free(msg); + if (ret < 0) + return ret; + ret = playlist_loop(pi, prepare_ls_row, opts); + playlist_unload(pi); + return ret; + } + afs_error(aca, "bad mood/playlist specifier: %s\n", arg); + return -ERRNO_TO_PARA_ERROR(EINVAL); +} + static int com_ls_callback(struct afs_callback_arg *aca) { const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS); struct ls_options *opts = aca->query.data; int ret; time_t current_time; - const struct lls_opt_result *r_r; + const struct lls_opt_result *r_r, *r_a; uint32_t limit, k, n; ret = lls_deserialize_parse_result( (char *)aca->query.data + sizeof(*opts), cmd, &opts->lpr); assert(ret >= 0); r_r = SERVER_CMD_OPT_RESULT(LS, REVERSE, opts->lpr); - + r_a = SERVER_CMD_OPT_RESULT(LS, ADMISSIBLE, opts->lpr); aca->pbout.flags = (opts->mode == LS_MODE_PARSER)? PBF_SIZE_PREFIX : 0; - if (admissible_only(opts)) - ret = score_loop(prepare_ls_row, opts); - else + if (admissible_only(opts)) { + const char *arg = lls_string_val(0, r_a); + ret = mop_loop(arg, aca, opts); + } else ret = osl(osl_rbtree_loop(audio_file_table, AFTCOL_PATH, opts, prepare_ls_row)); if (ret < 0) @@ -1902,6 +1940,53 @@ out_free: return send_ret; } +/* + * Call back once for each regular file below a directory. + * + * Traverse the given directory recursively and call the supplied callback for + * each regular file encountered. The first argument to the callback will be + * the path to the regular file and the second argument will be the data + * pointer. All file types except regular files and directories are ignored. In + * particular, symlinks are not followed. Subdirectories are ignored silently + * if the calling process has insufficient access permissions. + */ +static int for_each_file_in_dir(const char *dirname, + int (*func)(const char *, void *), void *data) +{ + int ret; + DIR *dir; + struct dirent *entry; + + dir = opendir(dirname); + if (!dir) + return errno == EACCES? 1 : -ERRNO_TO_PARA_ERROR(errno); + /* scan cwd recursively */ + while ((entry = readdir(dir))) { + char *tmp; + struct stat s; + + if (!strcmp(entry->d_name, ".")) + continue; + if (!strcmp(entry->d_name, "..")) + continue; + tmp = make_message("%s/%s", dirname, entry->d_name); + ret = 0; + if (lstat(tmp, &s) != -1) { + if (S_ISREG(s.st_mode)) + ret = func(tmp, data); + else if (S_ISDIR(s.st_mode)) + ret = for_each_file_in_dir(tmp, func, data); + } + free(tmp); + if (ret < 0) + goto out; + } + ret = 1; +out: + closedir(dir); + return ret; +} + static int com_add(struct command_context *cc, struct lls_parse_result *lpr) { int i, ret; diff --git a/audiod.c b/audiod.c index 0e8e5981..0d8f039c 100644 --- a/audiod.c +++ b/audiod.c @@ -958,7 +958,7 @@ static int init_default_filters(void) */ if (strcmp(name, "udp") == 0 || strcmp(name, "dccp") == 0) { tmp = para_strdup("fecdec"); - add_filter(i, tmp); + ret = add_filter(i, tmp); free(tmp); if (ret < 0) goto out; diff --git a/audiod_command.c b/audiod_command.c index 743d73a9..df357c7a 100644 --- a/audiod_command.c +++ b/audiod_command.c @@ -81,13 +81,6 @@ static int num_clients; /** The list of all status items used by para_{server,audiod,gui}. */ const char *status_item_list[] = {STATUS_ITEMS}; -static void dump_stat_client_list(void) -{ - struct stat_client *sc; - - list_for_each_entry(sc, &client_list, node) - PARA_INFO_LOG("stat client on fd %d\n", sc->fd); -} /** * Add a status client to the list. * @@ -121,7 +114,6 @@ static int stat_client_add(int fd, uint64_t mask, int parser_friendly) if (parser_friendly) new_client->flags = SCF_PARSER_FRIENDLY; para_list_add(&new_client->node, &client_list); - dump_stat_client_list(); num_clients++; return 1; } @@ -180,7 +172,6 @@ void stat_client_write_item(int item_num) continue; /* write error or short write */ close_stat_client(sc); - dump_stat_client_list(); } free(pb.buf); free(pfpb.buf); diff --git a/client_common.c b/client_common.c index 43527ddb..3b90000f 100644 --- a/client_common.c +++ b/client_common.c @@ -578,8 +578,9 @@ int client_parse_config(int argc, char *argv[], struct client_task **ct_ptr, if (CLIENT_OPT_GIVEN(KEY_FILE, lpr)) kf = para_strdup(CLIENT_OPT_STRING_VAL(KEY_FILE, lpr)); else { + struct stat statbuf; kf = make_message("%s/.paraslash/key.%s", home, user); - if (!file_exists(kf)) { + if (stat(kf, &statbuf) != 0) { /* assume file does not exist */ free(kf); kf = make_message("%s/.ssh/id_rsa", home); } diff --git a/configure.ac b/configure.ac index c7258111..92560e00 100644 --- a/configure.ac +++ b/configure.ac @@ -102,9 +102,14 @@ if test $HAVE_OPENSSL = yes; then if test "$ac_cv_have_decl_RSA_set0_key" != "$ac_cv_lib_crypto_RSA_set0_key"; then AC_MSG_ERROR([openssl header/library mismatch]) fi - test "$ac_cv_have_decl_RSA_set0_key" = yes && + if test "$ac_cv_have_decl_RSA_set0_key" = yes; then AC_DEFINE([HAVE_RSA_SET0_KEY], [1], [openssl >= 1.1]) - + else + AC_MSG_WARN([ + Old openssl library detected. Support for openssl-1.0 and earlier + will be removed in the next major paraslash release. Please upgrade + your openssl installation.]) + fi HAVE_CRYPTO_CLEANUP_ALL_EX_DATA=yes AC_CHECK_DECL([CRYPTO_cleanup_all_ex_data], [], [HAVE_CRYPTO_CLEANUP_ALL_EX_DATA=no], diff --git a/crypt_common.c b/crypt_common.c index d7471235..286ebe38 100644 --- a/crypt_common.c +++ b/crypt_common.c @@ -295,7 +295,7 @@ int decode_private_key(const char *key_file, unsigned char **result, key_type = PKT_PEM; begin = map + strlen(PRIVATE_PEM_KEY_HEADER); footer = strstr(map, PRIVATE_PEM_KEY_FOOTER); - PARA_INFO_LOG("detected legacy PEM key %s\n", key_file); + PARA_WARNING_LOG("detected legacy PEM key %s\n", key_file); } else if (strncmp(map, PRIVATE_OPENSSH_KEY_HEADER, strlen(PRIVATE_OPENSSH_KEY_HEADER)) == 0) { key_type = PKT_OPENSSH; diff --git a/fd.c b/fd.c index 763f756c..1af902f9 100644 --- a/fd.c +++ b/fd.c @@ -37,26 +37,27 @@ int xrename(const char *oldpath, const char *newpath) } /** - * Write an array of buffers to a file descriptor. + * Write an array of buffers, handling non-fatal errors. * - * \param fd The file descriptor. + * \param fd The file descriptor to write to. * \param iov Pointer to one or more buffers. * \param iovcnt The number of buffers. * - * EAGAIN/EWOULDBLOCK is not considered a fatal error condition. For example - * DCCP CCID3 has a sending wait queue which fills up and is emptied - * asynchronously. The EAGAIN case means that there is currently no space in - * the wait queue, but this can change at any moment. + * EAGAIN, EWOULDBLOCK and EINTR are not considered error conditions. If a + * write operation fails with EAGAIN or EWOULDBLOCK, the number of bytes that + * have been written so far is returned. In the EINTR case the operation is + * retried. Short writes are handled by issuing a subsequent write operation + * for the remaining part. * * \return Negative on fatal errors, number of bytes written else. * * For blocking file descriptors, this function returns either the sum of all - * buffer sizes, or the error code of the fatal error that caused the last - * write call to fail. + * buffer sizes or a negative error code which indicates the fatal error that + * caused a write call to fail. * - * For nonblocking file descriptors there is a third possibility: Any positive - * return value less than the sum of the buffer sizes indicates that some bytes - * have been written but the next write would block. + * For nonblocking file descriptors there is a third possibility: Any + * non-negative return value less than the sum of the buffer sizes indicates + * that a write operation returned EAGAIN/EWOULDBLOCK. * * \sa writev(2), \ref xwrite(). */ @@ -126,14 +127,15 @@ int xwrite(int fd, const char *buf, size_t len) } /** - * Write all data to a file descriptor. + * Write to a file descriptor, fail on short writes. * * \param fd The file descriptor. - * \param buf The buffer to be sent. - * \param len The length of \a buf. + * \param buf The buffer to be written. + * \param len The length of the buffer. * - * This is like \ref xwrite() but returns \p -E_SHORT_WRITE if not - * all data could be written. + * For blocking file descriptors this function behaves identical to \ref + * xwrite(). For non-blocking file descriptors it returns -E_SHORT_WRITE + * (rather than a value less than len) if not all data could be written. * * \return Number of bytes written on success, negative error code else. */ @@ -149,12 +151,20 @@ int write_all(int fd, const char *buf, size_t len) } /** - * Write a buffer given by a format string. + * A fprintf-like function for raw file descriptors. + * + * This function creates a string buffer according to the given format and + * writes this buffer to a file descriptor. * * \param fd The file descriptor. * \param fmt A format string. * + * The difference to fprintf(3) is that the first argument is a file + * descriptor, not a FILE pointer. This function does not rely on stdio. + * * \return The return value of the underlying call to \ref write_all(). + * + * \sa fprintf(3), \ref xvasprintf(). */ __printf_2_3 int write_va_buffer(int fd, const char *fmt, ...) { @@ -250,64 +260,44 @@ int read_nonblock(int fd, void *buf, size_t sz, size_t *num_bytes) } /** - * Read a buffer and check its content for a pattern. - * - * \param fd The file descriptor to receive from. - * \param pattern The expected pattern. - * \param bufsize The size of the internal buffer. + * Read a buffer and compare its contents to a string, ignoring case. * - * This function tries to read at most \a bufsize bytes from the non-blocking - * file descriptor \a fd. If at least \p strlen(\a pattern) bytes have been - * received, the beginning of the received buffer is compared with \a pattern, - * ignoring case. + * \param fd The file descriptor to read from. + * \param expectation The expected string to compare to. * - * \return Positive if \a pattern was received, negative on errors, zero if no data - * was available to read. + * The given file descriptor is expected to be in non-blocking mode. The string + * comparison is performed using strncasecmp(3). * - * \sa \ref read_nonblock(), \sa strncasecmp(3). + * \return Zero if no data was available, positive if a buffer was read whose + * contents compare as equal to the expected string, negative otherwise. + * Possible errors: (a) not enough data was read, (b) the buffer contents + * compared as non-equal, (c) a read error occurred. In the first two cases, + * -E_READ_PATTERN is returned. In the read error case the (negative) return + * value of the underlying call to \ref read_nonblock() is returned. */ -int read_pattern(int fd, const char *pattern, size_t bufsize) +int read_and_compare(int fd, const char *expectation) { - size_t n, len; - char *buf = alloc(bufsize + 1); - int ret = read_nonblock(fd, buf, bufsize, &n); + size_t n, len = strlen(expectation); + char *buf = alloc(len + 1); + int ret = read_nonblock(fd, buf, len, &n); - buf[n] = '\0'; if (ret < 0) goto out; + buf[n] = '\0'; ret = 0; if (n == 0) goto out; ret = -E_READ_PATTERN; - len = strlen(pattern); if (n < len) goto out; - if (strncasecmp(buf, pattern, len) != 0) + if (strncasecmp(buf, expectation, len) != 0) goto out; ret = 1; out: - if (ret < 0) { - PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); - PARA_NOTICE_LOG("recvd %zu bytes: %s\n", n, buf); - } free(buf); return ret; } -/** - * Check whether a file exists. - * - * \param fn The file name. - * - * \return True iff file exists. - */ -bool file_exists(const char *fn) -{ - struct stat statbuf; - - return !stat(fn, &statbuf); -} - /** * Set a file descriptor to blocking mode. * @@ -397,86 +387,32 @@ int para_open(const char *path, int flags, mode_t mode) } /** - * Wrapper for chdir(2). - * - * \param path The specified directory. + * Create a directory, don't fail if it already exists. * - * \return Standard. - */ -int para_chdir(const char *path) -{ - int ret = chdir(path); - - if (ret >= 0) - return 1; - return -ERRNO_TO_PARA_ERROR(errno); -} - -/** - * Save the cwd and open a given directory. - * - * \param dirname Path to the directory to open. - * \param dir Result pointer. - * \param cwd File descriptor of the current working directory. - * - * \return Standard. - * - * 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. - * - * If \a cwd is not \p NULL "." is opened and the resulting file descriptor is - * stored in \a cwd. If the function returns success, and \a cwd is not \p - * NULL, the caller must close this file descriptor (probably after calling - * fchdir(*cwd)). - * - * On errors, the function undos everything, so the caller needs neither close - * any files, nor change back to the original working directory. + * \param path Name of the directory to create. * - * \sa getcwd(3). + * This function passes the fixed mode value 0777 to mkdir(3) (which consults + * the file creation mask and restricts this value). * + * \return Zero if the path already existed as a directory or as a symbolic + * link which leads to a directory, one if the path did not exist and the + * directory has been created successfully, negative error code else. */ -static int para_opendir(const char *dirname, DIR **dir, int *cwd) +int para_mkdir(const char *path) { - int ret; + /* + * We call opendir(3) rather than relying on stat(2) because this way + * we don't need extra code to get the symlink case right. + */ + DIR *dir = opendir(path); - *dir = NULL; - if (cwd) { - ret = para_open(".", O_RDONLY, 0); - if (ret < 0) - return ret; - *cwd = ret; - } - ret = para_chdir(dirname); - if (ret < 0) - goto close_cwd; - *dir = opendir("."); - if (*dir) - return 1; - ret = -ERRNO_TO_PARA_ERROR(errno); - /* Ignore return value of fchdir() and close(). We're busted anyway. */ - if (cwd) { - int __a_unused ret2 = fchdir(*cwd); /* STFU, gcc */ + if (dir) { + closedir(dir); + return 0; } -close_cwd: - if (cwd) - close(*cwd); - return ret; -} - -/** - * A wrapper for mkdir(2). - * - * \param path Name of the directory to create. - * \param mode The permissions to use. - * - * \return Standard. - */ -int para_mkdir(const char *path, mode_t mode) -{ - if (!mkdir(path, mode)) - return 1; - return -ERRNO_TO_PARA_ERROR(errno); + if (errno != ENOENT) + return -ERRNO_TO_PARA_ERROR(errno); + return mkdir(path, 0777) == 0? 1 : -ERRNO_TO_PARA_ERROR(errno); } /** @@ -549,22 +485,21 @@ out: * \param start The start address of the memory mapping. * \param length The size of the mapping. * - * \return Standard. + * If NULL is passed as the start address, the length value is ignored and the + * function does nothing. + * + * \return Zero if NULL was passed, one if the memory area was successfully + * unmapped, a negative error code otherwise. * * \sa munmap(2), \ref mmap_full_file(). */ int para_munmap(void *start, size_t length) { - int err; - if (!start) return 0; if (munmap(start, length) >= 0) return 1; - err = errno; - PARA_ERROR_LOG("munmap (%p/%zu) failed: %s\n", start, length, - strerror(err)); - return -ERRNO_TO_PARA_ERROR(err); + return -ERRNO_TO_PARA_ERROR(errno); } /** @@ -643,64 +578,3 @@ void valid_fd_012(void) } } } - -/** - * Traverse the given directory recursively. - * - * \param dirname The directory to traverse. - * \param func The function to call for each entry. - * \param private_data Pointer to an arbitrary data structure. - * - * For each regular file under \a dirname, the supplied function \a func is - * called. The full path of the regular file and the \a private_data pointer - * are passed to \a func. Directories for which the calling process has no - * permissions to change to are silently ignored. - * - * \return Standard. - */ -int for_each_file_in_dir(const char *dirname, - int (*func)(const char *, void *), void *private_data) -{ - DIR *dir; - struct dirent *entry; - int cwd_fd, ret = para_opendir(dirname, &dir, &cwd_fd); - - if (ret < 0) - return ret == -ERRNO_TO_PARA_ERROR(EACCES)? 1 : ret; - /* scan cwd recursively */ - while ((entry = readdir(dir))) { - mode_t m; - char *tmp; - struct stat s; - - if (!strcmp(entry->d_name, ".")) - continue; - if (!strcmp(entry->d_name, "..")) - continue; - if (lstat(entry->d_name, &s) == -1) - continue; - m = s.st_mode; - if (!S_ISREG(m) && !S_ISDIR(m)) - continue; - tmp = make_message("%s/%s", dirname, entry->d_name); - if (!S_ISDIR(m)) { - ret = func(tmp, private_data); - free(tmp); - if (ret < 0) - goto out; - continue; - } - /* directory */ - ret = for_each_file_in_dir(tmp, func, private_data); - free(tmp); - if (ret < 0) - goto out; - } - ret = 1; -out: - closedir(dir); - if (fchdir(cwd_fd) < 0 && ret >= 0) - ret = -ERRNO_TO_PARA_ERROR(errno); - close(cwd_fd); - return ret; -} diff --git a/fd.h b/fd.h index 270d0ce2..e4f30903 100644 --- a/fd.h +++ b/fd.h @@ -5,14 +5,12 @@ int xrename(const char *oldpath, const char *newpath); int write_all(int fd, const char *buf, size_t len); __printf_2_3 int write_va_buffer(int fd, const char *fmt, ...); -bool file_exists(const char *); int xpoll(struct pollfd *fds, nfds_t nfds, int timeout); __must_check int mark_fd_nonblocking(int fd); __must_check int mark_fd_blocking(int fd); int para_mmap(size_t length, int prot, int flags, int fd, void *map); int para_open(const char *path, int flags, mode_t mode); -int para_mkdir(const char *path, mode_t mode); -int para_chdir(const char *path); +int para_mkdir(const char *path); int mmap_full_file(const char *filename, int open_mode, void **map, size_t *size, int *fd_ptr); int para_munmap(void *start, size_t length); @@ -21,11 +19,10 @@ int write_ok(int fd); void valid_fd_012(void); int readv_nonblock(int fd, struct iovec *iov, int iovcnt, size_t *num_bytes); int read_nonblock(int fd, void *buf, size_t sz, size_t *num_bytes); -int read_pattern(int fd, const char *pattern, size_t bufsize); +int read_and_compare(int fd, const char *expectation); int xwrite(int fd, const char *buf, size_t len); int xwritev(int fd, struct iovec *iov, int iovcnt); -int for_each_file_in_dir(const char *dirname, - int (*func)(const char *, void *), void *private_data); + /** * Write a \p NULL-terminated buffer. * diff --git a/http_recv.c b/http_recv.c index 32c9e7b9..8d2add19 100644 --- a/http_recv.c +++ b/http_recv.c @@ -105,7 +105,7 @@ static int http_recv_post_monitor(struct sched *s, void *context) return 0; } if (phd->status == HTTP_SENT_GET_REQUEST) { - ret = read_pattern(rn->fd, HTTP_OK_MSG, strlen(HTTP_OK_MSG)); + ret = read_and_compare(rn->fd, HTTP_OK_MSG); if (ret < 0) { PARA_ERROR_LOG("did not receive HTTP OK message\n"); goto out; diff --git a/http_send.c b/http_send.c index 90e3ee57..429b4662 100644 --- a/http_send.c +++ b/http_send.c @@ -170,7 +170,7 @@ static void http_post_monitor(__a_unused struct sched *s) case HTTP_STREAMING: /* nothing to do */ break; case HTTP_CONNECTED: /* need to recv get request */ - ret = read_pattern(sc->fd, HTTP_GET_MSG, MAXLINE); + ret = read_and_compare(sc->fd, HTTP_GET_MSG); if (ret < 0) phsd->status = HTTP_INVALID_GET_REQUEST; else if (ret > 0) { diff --git a/m4/lls/server_cmd.suite.m4 b/m4/lls/server_cmd.suite.m4 index cafdbccf..02afaabb 100644 --- a/m4/lls/server_cmd.suite.m4 +++ b/m4/lls/server_cmd.suite.m4 @@ -243,9 +243,19 @@ m4_include(`com_ll.m4') [option admissible] short_opt = a summary = list only admissible files + arg_type = string + arg_info = optional_arg + typestr = specifier/name + default_val = . [help] - List only files which are admissible with respect to the current mood - or playlist. + If the optional argument is supplied, it must be of the form "p/foo" + or "m/bar" (which refer to the playlist named "foo" and the mood named + "bar", respectively). The command then restricts its output to the set + of files which are admissible with respect to the thusly identified + mood or playlist. + + If no argument is given, or if the argument is the special value ".", + the current mood or playlist is assumed. [/help] [option reverse] short_opt = r @@ -417,6 +427,9 @@ m4_include(`com_ll.m4') activates the mood named 'foo'. [/description] + [option verbose] + short_opt = v + summary = print information about the loaded mood or playlist [subcommand sender] purpose = control paraslash senders diff --git a/mood.c b/mood.c index e85cf36a..1e15ef0e 100644 --- a/mood.c +++ b/mood.c @@ -59,6 +59,8 @@ struct mood_instance { struct mp_context *parser_context; /** To compute the score. */ struct afs_statistics stats; + /** NULL means to operate on the global score table. */ + struct osl_table *score_table; }; /* @@ -132,6 +134,8 @@ static void destroy_mood(struct mood_instance *m) if (!m) return; mp_shutdown(m->parser_context); + if (m->score_table) + score_close(m->score_table); free(m->name); free(m); } @@ -437,7 +441,7 @@ static void update_afs_statistics(struct afs_info *old_afsi, } static int add_to_score_table(const struct osl_row *aft_row, - const struct afs_statistics *stats) + struct mood_instance *m) { long score; struct afs_info afsi; @@ -445,8 +449,8 @@ static int add_to_score_table(const struct osl_row *aft_row, if (ret < 0) return ret; - score = compute_score(&afsi, stats); - return score_add(aft_row, score); + score = compute_score(&afsi, &m->stats); + return score_add(aft_row, score, m->score_table); } static int delete_from_statistics_and_score_table(const struct osl_row *aft_row) @@ -504,7 +508,7 @@ static int mood_update_audio_file(const struct osl_row *aft_row, ret = add_afs_statistics(aft_row, ¤t_mood->stats); if (ret < 0) return ret; - return add_to_score_table(aft_row, ¤t_mood->stats); + return add_to_score_table(aft_row, current_mood); } /* update score */ ret = get_afsi_of_row(aft_row, &afsi); @@ -529,24 +533,37 @@ static char *get_statistics(struct mood_instance *m, int64_t sse) unsigned n = m->stats.num; int mean_days, sigma_days; + if (n == 0) + return make_message("no admissible files\n"); mean_days = (sse - m->stats.last_played_sum / n) / 3600 / 24; sigma_days = int_sqrt(m->stats.last_played_qd / n) / 3600 / 24; return make_message( "loaded mood %s (%u files)\n" "last_played mean/sigma: %d/%d days\n" "num_played mean/sigma: %" PRId64 "/%" PRIu64 "\n" + "correction factor ratio: %.2lf\n" , m->name? m->name : "(dummy)", n, mean_days, sigma_days, m->stats.num_played_sum / n, - int_sqrt(m->stats.num_played_qd / n) + int_sqrt(m->stats.num_played_qd / n), + 86400.0 * m->stats.last_played_correction / + m->stats.num_played_correction ); } -/** Free all resources of the current mood, if any. */ -void mood_unload(void) +/** + * Free all resources of a mood instance. + * + * \param m As obtained by \ref mood_load(). If NULL, unload the current mood. + * + * It's OK to call this with m == NULL even if no current mood is loaded. + */ +void mood_unload(struct mood_instance *m) { + if (m) + return destroy_mood(m); destroy_mood(current_mood); current_mood = NULL; } @@ -568,23 +585,42 @@ static void compute_correction_factors(int64_t sse, struct afs_statistics *s) } /** - * Change the current mood. + * Populate a score table with admissible files for the given mood. + * + * This consults the mood table to initialize the mood parser with the mood + * expression stored in the blob object which corresponds to the given name. A + * score table is allocated and populated with references to those entries of + * the audio file table which evaluate as admissible with respect to the mood + * expression. For each audio file a score value is computed and stored along + * with the file reference. * * \param mood_name The name of the mood to load. + * \param result Opaque, refers to the mood parser and the score table. * \param msg Error message or mood info is returned here. * - * If \a mood_name is \a NULL, load the dummy mood that accepts every audio file - * and uses a scoring method based only on the \a last_played information. + * If the mood name is NULL, the dummy mood is loaded. This mood regards every + * audio file as admissible. + * + * A NULL result pointer instructs the function to operate on the current mood. + * That is, on the mood instance which is used by the server to select the next + * audio file for streaming. In this mode of operation, the mood which was + * active before the call, if any, is unloaded on success. + * + * If result is not NULL, the current mood is unaffected and *result points to + * an initialized mood instance on success. The caller can pass this reference + * to \ref mood_loop() to iterate over the admissible files, and should call + * \ref mood_unload() to free the mood instance afterwards. * * If the message pointer is not NULL, a suitable message is returned there in * all cases. The caller must free this string. * - * \return The number of admissible files on success, negative on errors. It is + * \return The number of admissible files on success, negative on errors. On + * errors, the current mood remains unaffected even if result is NULL. It is * not considered an error if no files are admissible. * - * \sa struct \ref afs_info::last_played, \ref mp_eval_row(). + * \sa \ref mp_eval_row(). */ -int mood_load(const char *mood_name, char **msg) +int mood_load(const char *mood_name, struct mood_instance **result, char **msg) { int i, ret; struct admissible_array aa = {.size = 0}; @@ -609,14 +645,10 @@ int mood_load(const char *mood_name, char **msg) } clock_get_realtime(&rnow); compute_correction_factors(rnow.tv_sec, &aa.m->stats); - if (aa.m->stats.num == 0) { - if (msg) - *msg = make_message("no admissible files\n"); - ret = 0; - goto out; - } + if (result) + score_open(&aa.m->score_table); for (i = 0; i < aa.m->stats.num; i++) { - ret = add_to_score_table(aa.array[i], &aa.m->stats); + ret = add_to_score_table(aa.array[i], aa.m); if (ret < 0) { if (msg) *msg = make_message( @@ -628,8 +660,12 @@ int mood_load(const char *mood_name, char **msg) if (msg) *msg = get_statistics(aa.m, rnow.tv_sec); ret = aa.m->stats.num; - mood_unload(); - current_mood = aa.m; + if (result) + *result = aa.m; + else { + mood_unload(NULL); + current_mood = aa.m; + } ret = 1; out: free(aa.array); @@ -638,12 +674,29 @@ out: return ret; } +/** + * Iterate over the admissible files of a mood instance. + * + * This wrapper around \ref score_loop() is the mood counterpart of \ref + * playlist_loop(). + * + * \param m Determines the score table to iterate. Must not be NULL. + * \param func See \ref score_loop(). + * \param data See \ref score_loop(). + * + * \return See \ref score_loop(), \ref playlist_loop(). + */ +int mood_loop(struct mood_instance *m, osl_rbtree_loop_func *func, void *data) +{ + return score_loop(func, m->score_table, data); +} + /* * Empty the score table and start over. * - * This function is called on events which render the current list of - * admissible files useless, for example if an attribute is removed from the - * attribute table. + * This function is called on events which render the current set of admissible + * files invalid, for example if an attribute is removed from the attribute + * table. */ static int reload_current_mood(void) { @@ -656,8 +709,8 @@ static int reload_current_mood(void) current_mood->name : "(dummy)"); if (current_mood->name) mood_name = para_strdup(current_mood->name); - mood_unload(); - ret = mood_load(mood_name, NULL); + mood_unload(NULL); + ret = mood_load(mood_name, NULL, NULL); free(mood_name); return ret; } diff --git a/play.c b/play.c index 4024c8ea..bd183b6b 100644 --- a/play.c +++ b/play.c @@ -1048,9 +1048,9 @@ static void session_open(void) char *dot_para = make_message("%s/.paraslash", home); free(home); - ret = para_mkdir(dot_para, 0777); + ret = para_mkdir(dot_para); /* warn, but otherwise ignore mkdir error */ - if (ret < 0 && ret != -ERRNO_TO_PARA_ERROR(EEXIST)) + if (ret < 0) PARA_WARNING_LOG("Can not create %s: %s\n", dot_para, para_strerror(-ret)); history_file = make_message("%s/play.history", dot_para); diff --git a/playlist.c b/playlist.c index d02ade3b..c145b0fd 100644 --- a/playlist.c +++ b/playlist.c @@ -14,12 +14,18 @@ /** \file playlist.c Functions for loading and saving playlists. */ -/** Structure used for adding entries to a playlist. */ +/** + * The state of a playlist instance. + * + * A structure of this type is allocated and initialized at playlist load time. + */ struct playlist_instance { /** The name of the playlist. */ char *name; /** The number of entries currently in the playlist. */ unsigned length; + /** Contains all valid paths of the playlist. */ + struct osl_table *score_table; }; static struct playlist_instance current_playlist; @@ -38,7 +44,7 @@ static int playlist_update_audio_file(const struct osl_row *aft_row) static int add_playlist_entry(char *path, void *data) { - struct playlist_instance *playlist = data; + struct playlist_instance *pi = data; struct osl_row *aft_row; int ret = aft_get_row_of_path(path, &aft_row); @@ -46,12 +52,12 @@ static int add_playlist_entry(char *path, void *data) PARA_NOTICE_LOG("%s: %s\n", path, para_strerror(-ret)); return 1; } - ret = score_add(aft_row, -playlist->length); + ret = score_add(aft_row, -pi->length, pi->score_table); if (ret < 0) { PARA_ERROR_LOG("failed to add %s: %s\n", path, para_strerror(-ret)); return ret; } - playlist->length++; + pi->length++; return 1; } @@ -103,11 +109,22 @@ int playlist_check_callback(struct afs_callback_arg *aca) check_playlist)); } -/** Free all resources of the current playlist, if any. */ -void playlist_unload(void) +/** + * Free all resources of the given/current playlist. + * + * \param pi NULL means to unload the current playlist. + */ +void playlist_unload(struct playlist_instance *pi) { + if (pi) { + score_close(pi->score_table); + free(pi->name); + free(pi); + return; + } if (!current_playlist.name) return; + score_clear(); free(current_playlist.name); current_playlist.name = NULL; current_playlist.length = 0; @@ -122,43 +139,78 @@ void playlist_unload(void) * corresponding row of the audio file table is added to the score table. * * \param name The name of the playlist to load. + * \param result Opaque, refers to the underlying score table. * \param msg Error message or playlist info is returned here. * * \return The length of the loaded playlist on success, negative error code * else. Files which are listed in the playlist, but are not contained in the * database are ignored. This is not considered an error. */ -int playlist_load(const char *name, char **msg) +int playlist_load(const char *name, struct playlist_instance **result, char **msg) { int ret; - struct playlist_instance *playlist = ¤t_playlist; + struct playlist_instance *pi; struct osl_object playlist_def; - ret = pl_get_def_by_name(name, &playlist_def); - if (ret < 0) { - *msg = make_message("could not read playlist %s\n", name); - return ret; + if (!name || !*name) { + if (msg) + *msg = make_message("empty playlist name\n"); + return -ERRNO_TO_PARA_ERROR(EINVAL); } - playlist_unload(); + ret = pl_get_def_by_name(name, &playlist_def); + if (ret < 0) + goto err; + pi = zalloc(sizeof(*pi)); + if (result) + score_open(&pi->score_table); ret = for_each_line(FELF_READ_ONLY, playlist_def.data, - playlist_def.size, add_playlist_entry, playlist); + playlist_def.size, add_playlist_entry, pi); osl_close_disk_object(&playlist_def); if (ret < 0) - goto err; + goto close_score_table; ret = -E_PLAYLIST_EMPTY; - if (!playlist->length) - goto err; - playlist->name = para_strdup(name); - *msg = make_message("loaded playlist %s (%u files)\n", playlist->name, - playlist->length); + if (pi->length == 0) + goto close_score_table; /* success */ - return current_playlist.length; + if (msg) + *msg = make_message("loaded playlist %s (%u files)\n", name, + pi->length); + pi->name = para_strdup(name); + if (result) + *result = pi; + else { + playlist_unload(NULL); + current_playlist = *pi; + } + return pi->length; +close_score_table: + if (result) + score_close(pi->score_table); + free(pi); err: PARA_NOTICE_LOG("unable to load playlist %s\n", name); - *msg = make_message("unable to load playlist %s\n", name); + if (msg) + *msg = make_message("unable to load playlist %s\n", name); return ret; } +/** + * Iterate over all admissible audio files of a playlist instance. + * + * This wrapper around \ref score_loop() is the playlist counterpart of \ref + * mood_loop(). + * + * \param pi Determines the score table to iterate. Must not be NULL. + * \param func See \ref score_loop(). + * \param data See \ref score_loop(). + * + * \return See \ref score_loop(), \ref mood_loop(). + */ +int playlist_loop(struct playlist_instance *pi, osl_rbtree_loop_func *func, void *data) +{ + return score_loop(func, pi->score_table, data); +} + static int search_path(char *path, void *data) { if (strcmp(path, data)) @@ -196,7 +248,7 @@ static int handle_audio_file_event(enum afs_events event, void *data) } /* !was_admissible && is_admissible */ current_playlist.length++; - return score_add(row, 0); /* play it immediately */ + return score_add(row, 0, NULL); /* play it immediately */ } /** diff --git a/score.c b/score.c index 10cd254a..c03e3472 100644 --- a/score.c +++ b/score.c @@ -77,10 +77,10 @@ static struct osl_table_description score_table_desc = { }; /* On errors (negative return value) the content of score is undefined. */ -static int get_score_of_row(void *score_row, long *score) +static int get_score_of_row(struct osl_table *t, void *score_row, long *score) { struct osl_object obj; - int ret = osl(osl_get_object(score_table, score_row, SCORECOL_SCORE, &obj)); + int ret = osl(osl_get_object(t, score_row, SCORECOL_SCORE, &obj)); if (ret >= 0) *score = *(long *)obj.data; @@ -88,14 +88,15 @@ static int get_score_of_row(void *score_row, long *score) } /** - * Add an entry to the table of admissible files. + * Add a (row, score) pair to the score table. * - * \param aft_row The audio file to be added. - * \param score The score for this file. + * \param aft_row Identifies the audio file to be added. + * \param score The score value of the audio file. + * \param t NULL means to operate on the currently active table. * * \return The return value of the underlying call to osl_add_row(). */ -int score_add(const struct osl_row *aft_row, long score) +int score_add(const struct osl_row *aft_row, long score, struct osl_table *t) { int ret; struct osl_object score_objs[NUM_SCORE_COLUMNS]; @@ -112,7 +113,7 @@ int score_add(const struct osl_row *aft_row, long score) *(long *)(score_objs[SCORECOL_SCORE].data) = score; // PARA_DEBUG_LOG("adding %p\n", *(void **) (score_objs[SCORECOL_AFT_ROW].data)); - ret = osl(osl_add_row(score_table, score_objs)); + ret = osl(osl_add_row(t? t : score_table, score_objs)); if (ret < 0) { PARA_ERROR_LOG("%s\n", para_strerror(-ret)); free(score_objs[SCORECOL_SCORE].data); @@ -152,7 +153,7 @@ int score_update(const struct osl_row *aft_row, long percent) ret = osl(osl_get_nth_row(score_table, SCORECOL_SCORE, new_pos, &rrow)); if (ret < 0) return ret; - ret = get_score_of_row(rrow, &new_score); + ret = get_score_of_row(score_table, rrow, &new_score); if (ret < 0) return ret; new_score--; @@ -176,7 +177,7 @@ int get_score_and_aft_row(struct osl_row *score_row, long *score, struct osl_row **aft_row) { struct osl_object obj; - int ret = get_score_of_row(score_row, score); + int ret = get_score_of_row(score_table, score_row, score); if (ret < 0) return ret; @@ -187,26 +188,28 @@ int get_score_and_aft_row(struct osl_row *score_row, long *score, return 1; } -static int get_score_row_from_aft_row(const struct osl_row *aft_row, - struct osl_row **score_row) +static int get_score_row_from_aft_row(struct osl_table *t, + const struct osl_row *aft_row, struct osl_row **score_row) { struct osl_object obj = {.data = (struct osl_row *)aft_row, .size = sizeof(aft_row)}; - return osl(osl_get_row(score_table, SCORECOL_AFT_ROW, &obj, score_row)); + return osl(osl_get_row(t, SCORECOL_AFT_ROW, &obj, score_row)); } /** * Call the given function for each row of the score table. * * \param func Callback, called once per row. + * \param t NULL means to use the currently active score table. * \param data Passed verbatim to the callback. * * \return The return value of the underlying call to osl_rbtree_loop(). The * loop terminates early if the callback returns negative. */ -int score_loop(osl_rbtree_loop_func *func, void *data) +int score_loop(osl_rbtree_loop_func *func, struct osl_table *t, void *data) { - return osl(osl_rbtree_loop(score_table, SCORECOL_SCORE, data, func)); + return osl(osl_rbtree_loop(t? t : score_table, SCORECOL_SCORE, data, + func)); } /** @@ -229,7 +232,7 @@ int score_get_best(struct osl_row **aft_row, long *score) if (ret < 0) return ret; *aft_row = obj.data; - return get_score_of_row(row, score); + return get_score_of_row(score_table, row, score); } /** @@ -244,7 +247,7 @@ int score_get_best(struct osl_row **aft_row, long *score) int score_delete(const struct osl_row *aft_row) { struct osl_row *score_row; - int ret = get_score_row_from_aft_row(aft_row, &score_row); + int ret = get_score_row_from_aft_row(score_table, aft_row, &score_row); if (ret < 0) return ret; @@ -263,7 +266,7 @@ int score_delete(const struct osl_row *aft_row) bool row_belongs_to_score_table(const struct osl_row *aft_row) { struct osl_row *score_row; - int ret = get_score_row_from_aft_row(aft_row, &score_row); + int ret = get_score_row_from_aft_row(score_table, aft_row, &score_row); if (ret == -OSL_ERRNO_TO_PARA_ERROR(E_OSL_RB_KEY_NOT_FOUND)) return false; @@ -271,29 +274,56 @@ bool row_belongs_to_score_table(const struct osl_row *aft_row) return true; } -static void score_close(void) +/** + * Free all volatile objects, then close the table. + * + * \param t As returned from \ref score_open(). + * + * This either succeeds or terminates the calling process. + */ +void score_close(struct osl_table *t) +{ + assert(osl_close_table(t? t : score_table, OSL_FREE_VOLATILE) >= 0); +} + +static void close_global_table(void) { - osl_close_table(score_table, OSL_FREE_VOLATILE); - score_table = NULL; + score_close(NULL); } -static int score_open(__a_unused const char *dir) +static int open_global_table(__a_unused const char *dir) { - assert(osl_open_table(&score_table_desc, &score_table) >= 0); + assert(osl(osl_open_table(&score_table_desc, &score_table)) >= 0); return 1; } +/** + * Allocate a score table instance. + * + * \param result NULL means to open the currently active score table. + * + * Since the score table does no filesystem I/O, this function always succeeds. + * \sa \ref score_close(). + */ +void score_open(struct osl_table **result) +{ + if (result) + assert(osl(osl_open_table(&score_table_desc, result)) >= 0); + else + open_global_table(NULL); +} + /** * Remove all entries from the score table, but keep the table open. */ void score_clear(void) { - score_close(); - score_open(NULL); + close_global_table(); + open_global_table(NULL); } /** The score table stores (aft row, score) pairs in memory. */ const struct afs_table_operations score_ops = { - .open = score_open, - .close = score_close, + .open = open_global_table, + .close = close_global_table, }; diff --git a/t/test-lib.sh b/t/test-lib.sh index f98d76c7..1ba70632 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -17,9 +17,12 @@ get_audio_file_paths() say_color() { + local severity=$1 + + shift if [[ "$o_nocolor" != "true" && -n "$1" ]]; then export TERM=$ORIGINAL_TERM - case "$1" in + case "$severity" in error) tput $C_BOLD; tput $C_SETAF 1;; skip) tput $C_SETAF 5;; ok) @@ -32,8 +35,11 @@ say_color() tput $C_SETAF 6;; esac fi - shift - printf "%s\n" "$*" + if [[ "$severity" == 'error' ]]; then + printf "%s\n" "$*" 1>&2 + else + printf "%s\n" "$*" + fi if [[ "$o_nocolor" != "true" && -n "$1" ]]; then tput $C_SGR0 export TERM=dumb