X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/0eaf64e8d26c08228401fa89741c63fc2ff064c3..fde67de26e36cf02d5632d934623bbc054a3c1d9:/lib/trackdb.c diff --git a/lib/trackdb.c b/lib/trackdb.c index 3647f10..85d38dd 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -2,20 +2,18 @@ * This file is part of DisOrder * Copyright (C) 2005-2008 Richard Kettlewell * - * This program is free software; you can redistribute it and/or modify + * 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 + * the Free Software Foundation, either version 3 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. - * + * + * 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-1307 - * USA + * along with this program. If not, see . */ /** @file lib/trackdb.c * @brief Track database @@ -60,6 +58,7 @@ #include "unicode.h" #include "unidata.h" #include "base64.h" +#include "sendmail.h" #define RESCAN "disorder-rescan" #define DEADLOCK "disorder-deadlock" @@ -83,8 +82,14 @@ int trackdb_existing_database; /* setup and teardown ********************************************************/ -static const char *home; /* home had better not change */ -DB_ENV *trackdb_env; /* db environment */ +/** @brief Database home directory + * + * All database files live below here. It had better never change. + */ +static const char *home; + +/** @brief Database environment */ +DB_ENV *trackdb_env; /** @brief The tracks database * - Keys are UTF-8(NFC(unicode(path name))) @@ -158,11 +163,55 @@ DB *trackdb_scheduledb; */ DB *trackdb_usersdb; -static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ -static pid_t rescan_pid = -1; /* rescanner PID */ -static int initialized, opened; /* state */ +/** @brief The playlists database + * - Keys are playlist names + * - Values are encoded key-value pairs + * - Data is user data and cannot be reconstructed + */ +DB *trackdb_playlistsdb; + +/** @brief Deadlock manager PID */ +static pid_t db_deadlock_pid = -1; + +/** @brief Rescanner PID */ +static pid_t rescan_pid = -1; + +/** @brief Set when the database environment exists */ +static int initialized; + +/** @brief Set when databases are open */ +static int opened; + +/** @brief Current stats subprocess PIDs */ +static hash *stats_pids; + +/** @brief PID of current random track chooser (disorder-choose) */ +static pid_t choose_pid = -1; + +/** @brief Our end of pipe from disorder-choose */ +static int choose_fd; + +/** @brief Callback to supply random track to */ +static random_callback *choose_callback; + +/** @brief Accumulator for output from disorder-choose */ +static struct dynstr choose_output; + +/** @brief Current completion status of disorder-choose + * A bitmap of @ref CHOOSE_READING and @ref CHOOSE_RUNNING. + */ +static unsigned choose_complete; + +/* @brief Exit status from disorder-choose */ +static int choose_status; -/* comparison function for keys */ +/** @brief disorder-choose process is running */ +#define CHOOSE_RUNNING 1 + +/** @brief disorder-choose pipe is still open */ +#define CHOOSE_READING 2 + +/** @brief Comparison function for filename-based keys */ static int compare(DB attribute((unused)) *db_, const DBT *a, const DBT *b) { return compare_path_raw(a->data, a->size, b->data, b->size); @@ -256,7 +305,7 @@ void trackdb_init(int flags) { D(("initialized database environment")); } -/* called when deadlock manager terminates */ +/** @brief Called when deadlock manager terminates */ static int reap_db_deadlock(ev_source attribute((unused)) *ev, pid_t attribute((unused)) pid, int status, @@ -271,6 +320,18 @@ static int reap_db_deadlock(ev_source attribute((unused)) *ev, return 0; } +/** @brief Start a subprogram + * @param ev Event loop + * @param outputfd File descriptor to redirect @c stdout to, or -1 + * @param prog Program name + * @param ... Arguments + * @return PID + * + * Starts a subprocess. Adds the following arguments: + * - @c --config to ensure the right config file is used + * - @c --debug or @c --no-debug to match debug settings + * - @c --syslog or @c --no-syslog to match log settings + */ static pid_t subprogram(ev_source *ev, int outputfd, const char *prog, ...) { pid_t pid; @@ -310,7 +371,11 @@ static pid_t subprogram(ev_source *ev, int outputfd, const char *prog, return pid; } -/* start deadlock manager */ +/** @brief Start deadlock manager + * @param ev Event loop + * + * Called from the main server (only). + */ void trackdb_master(ev_source *ev) { assert(db_deadlock_pid == -1); db_deadlock_pid = subprogram(ev, -1, DEADLOCK, (char *)0); @@ -318,8 +383,34 @@ void trackdb_master(ev_source *ev) { D(("started deadlock manager")); } -/* close environment */ -void trackdb_deinit(void) { +/** @brief Kill a subprocess and wait for it to terminate + * @param ev Event loop or NULL + * @param pid Process ID or -1 + * @param what Description of subprocess + * + * Used during trackdb_deinit(). This function blocks so don't use it for + * normal teardown as that will hang the server. + */ +static void terminate_and_wait(ev_source *ev, + pid_t pid, + const char *what) { + int err; + + if(pid == -1) + return; + if(kill(pid, SIGTERM) < 0) + fatal(errno, "error killing %s", what); + /* wait for the rescanner to finish */ + while(waitpid(pid, &err, 0) == -1 && errno == EINTR) + ; + if(ev) + ev_child_cancel(ev, pid); +} + +/** @brief Close database environment + * @param ev Event loop + */ +void trackdb_deinit(ev_source *ev) { int err; /* sanity checks */ @@ -330,32 +421,40 @@ void trackdb_deinit(void) { if((err = trackdb_env->close(trackdb_env, 0))) fatal(0, "trackdb_env->close: %s", db_strerror(err)); - if(rescan_pid != -1) { - /* shut down the rescanner */ - if(kill(rescan_pid, SIGTERM) < 0) - fatal(errno, "error killing rescanner"); - /* wait for the rescanner to finish */ - while(waitpid(rescan_pid, &err, 0) == -1 && errno == EINTR) - ; - } + terminate_and_wait(ev, rescan_pid, "disorder-rescan"); + rescan_pid = -1; + terminate_and_wait(ev, choose_pid, "disorder-choose"); + choose_pid = -1; - /* TODO kill any stats subprocesses */ + if(stats_pids) { + char **ks = hash_keys(stats_pids); - /* finally terminate the deadlock manager */ - if(db_deadlock_pid != -1 && kill(db_deadlock_pid, SIGTERM) < 0) - fatal(errno, "error killing deadlock manager"); - db_deadlock_pid = -1; + while(*ks) { + pid_t pid = atoi(*ks++); + terminate_and_wait(ev, pid, "disorder-stats"); + } + stats_pids = NULL; + } + terminate_and_wait(ev, db_deadlock_pid, "disorder-deadlock"); + db_deadlock_pid = -1; D(("deinitialized database environment")); } -/* open a specific database */ +/** @brief Open a specific database + * @param path Relative path to database + * @param dbflags Database flags: DB_DUP, DB_DUPSORT, etc + * @param dbtype Database type: DB_HASH, DB_BTREE, etc + * @param openflags Open flags: DB_RDONLY, DB_CREATE, etc + * @param mode Permission mask: usually 0666 + * @return Database handle + */ static DB *open_db(const char *path, u_int32_t dbflags, DBTYPE dbtype, u_int32_t openflags, int mode) { - int err; + int err, err2; DB *db; D(("open %s", path)); @@ -370,8 +469,14 @@ static DB *open_db(const char *path, fatal(0, "db->set_bt_compare %s: %s", path, db_strerror(err)); if((err = db->open(db, 0, path, 0, dbtype, openflags | DB_AUTO_COMMIT, mode))) { - if((openflags & DB_CREATE) || errno != ENOENT) + if((openflags & DB_CREATE) || errno != ENOENT) { + if((err2 = db->close(db, 0))) + error(0, "db->close: %s", db_strerror(err2)); + trackdb_close(); + trackdb_env->close(trackdb_env,0); + trackdb_env = 0; fatal(0, "db->open %s: %s", path, db_strerror(err)); + } db->close(db, 0); db = 0; } @@ -467,7 +572,8 @@ void trackdb_open(int flags) { trackdb_noticeddb = open_db("noticed.db", DB_DUPSORT, DB_BTREE, dbflags, 0666); trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666); - if(!trackdb_existing_database) { + trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666); + if(!trackdb_existing_database && !(flags & TRACKDB_READ_ONLY)) { /* Stash the database version */ char buf[32]; @@ -478,38 +584,39 @@ void trackdb_open(int flags) { D(("opened databases")); } -/* close track databases */ +/** @brief Close track databases */ void trackdb_close(void) { int err; /* sanity checks */ assert(opened == 1); --opened; - if((err = trackdb_tracksdb->close(trackdb_tracksdb, 0))) - fatal(0, "error closing tracks.db: %s", db_strerror(err)); - if((err = trackdb_searchdb->close(trackdb_searchdb, 0))) - fatal(0, "error closing search.db: %s", db_strerror(err)); - if((err = trackdb_tagsdb->close(trackdb_tagsdb, 0))) - fatal(0, "error closing tags.db: %s", db_strerror(err)); - if((err = trackdb_prefsdb->close(trackdb_prefsdb, 0))) - fatal(0, "error closing prefs.db: %s", db_strerror(err)); - if((err = trackdb_globaldb->close(trackdb_globaldb, 0))) - fatal(0, "error closing global.db: %s", db_strerror(err)); - if((err = trackdb_noticeddb->close(trackdb_noticeddb, 0))) - fatal(0, "error closing noticed.db: %s", db_strerror(err)); - if((err = trackdb_scheduledb->close(trackdb_scheduledb, 0))) - fatal(0, "error closing schedule.db: %s", db_strerror(err)); - if((err = trackdb_usersdb->close(trackdb_usersdb, 0))) - fatal(0, "error closing users.db: %s", db_strerror(err)); - trackdb_tracksdb = trackdb_searchdb = trackdb_prefsdb = 0; - trackdb_tagsdb = trackdb_globaldb = 0; +#define CLOSE(N, V) do { \ + if(V && (err = V->close(V, 0))) \ + fatal(0, "error closing %s: %s", N, db_strerror(err)); \ + V = 0; \ +} while(0) + CLOSE("tracks.db", trackdb_tracksdb); + CLOSE("search.db", trackdb_searchdb); + CLOSE("tags.db", trackdb_tagsdb); + CLOSE("prefs.db", trackdb_prefsdb); + CLOSE("global.db", trackdb_globaldb); + CLOSE("noticed.db", trackdb_noticeddb); + CLOSE("schedule.db", trackdb_scheduledb); + CLOSE("users.db", trackdb_usersdb); + CLOSE("playlists.db", trackdb_playlistsdb); D(("closed databases")); } /* generic db routines *******************************************************/ -/* fetch and decode a database entry. Returns 0, DB_NOTFOUND or - * DB_LOCK_DEADLOCK. */ +/** @brief Fetch and decode a database entry + * @param db Database + * @param track Track name + * @param kp Where to put decoded list (or NULL if you don't care) + * @param tid Owning transaction + * @return 0, @c DB_NOTFOUND or @c DB_LOCK_DEADLOCK + */ int trackdb_getdata(DB *db, const char *track, struct kvp **kp, @@ -520,10 +627,12 @@ int trackdb_getdata(DB *db, switch(err = db->get(db, tid, make_key(&key, track), prepare_data(&data), 0)) { case 0: - *kp = kvp_urldecode(data.data, data.size); + if(kp) + *kp = kvp_urldecode(data.data, data.size); return 0; case DB_NOTFOUND: - *kp = 0; + if(kp) + *kp = 0; return err; case DB_LOCK_DEADLOCK: error(0, "error querying database: %s", db_strerror(err)); @@ -533,8 +642,14 @@ int trackdb_getdata(DB *db, } } -/* encode and store a database entry. Returns 0, DB_KEYEXIST or - * DB_LOCK_DEADLOCK. */ +/** @brief Encode and store a database entry + * @param db Database + * @param track Track name + * @param k List of key/value pairs to store + * @param tid Owning transaction + * @param flags DB flags e.g. DB_NOOVERWRITE + * @return 0, DB_KEYEXIST or DB_LOCK_DEADLOCK + */ int trackdb_putdata(DB *db, const char *track, const struct kvp *k, @@ -580,7 +695,11 @@ int trackdb_delkey(DB *db, } } -/* open a database cursor */ +/** @brief Open a database cursor + * @param db Database + * @param tid Owning transaction + * @return Cursor + */ DBC *trackdb_opencursor(DB *db, DB_TXN *tid) { int err; DBC *c; @@ -592,7 +711,10 @@ DBC *trackdb_opencursor(DB *db, DB_TXN *tid) { return c; } -/* close a database cursor; returns 0 or DB_LOCK_DEADLOCK */ +/** @brief Close a database cursor + * @param c Cursor + * @return 0 or DB_LOCK_DEADLOCK + */ int trackdb_closecursor(DBC *c) { int err; @@ -608,7 +730,16 @@ int trackdb_closecursor(DBC *c) { } } -/* delete a (key,data) pair. Returns 0, DB_NOTFOUND or DB_LOCK_DEADLOCK. */ +/** @brief Delete a key/data pair + * @param db Database + * @param word Key + * @param track Data + * @param tid Owning transaction + * @return 0, DB_NOTFOUND or DB_LOCK_DEADLOCK + * + * Used by the search and tags databases, hence the odd parameter names. + * See also register_word(). + */ int trackdb_delkeydata(DB *db, const char *word, const char *track, @@ -646,7 +777,9 @@ int trackdb_delkeydata(DB *db, return err; } -/* start a transaction */ +/** @brief Start a transaction + * @return Transaction + */ DB_TXN *trackdb_begin_transaction(void) { DB_TXN *tid; int err; @@ -656,7 +789,11 @@ DB_TXN *trackdb_begin_transaction(void) { return tid; } -/* abort transaction */ +/** @brief Abort transaction + * @param tid Transaction (or NULL) + * + * If @p tid is NULL then nothing happens. + */ void trackdb_abort_transaction(DB_TXN *tid) { int err; @@ -665,7 +802,9 @@ void trackdb_abort_transaction(DB_TXN *tid) { fatal(0, "tid->abort: %s", db_strerror(err)); } -/* commit transaction */ +/** @brief Commit transaction + * @param tid Transaction (must not be NULL) + */ void trackdb_commit_transaction(DB_TXN *tid) { int err; @@ -675,12 +814,26 @@ void trackdb_commit_transaction(DB_TXN *tid) { /* search/tags shared code ***************************************************/ -/* comparison function used by dedupe() */ +/** @brief Comparison function used by dedupe() + * @param a Pointer to first key + * @param b Pointer to second key + * @return -1, 0 or 1 + * + * Passed to qsort(). + */ static int wordcmp(const void *a, const void *b) { return strcmp(*(const char **)a, *(const char **)b); } -/* sort and de-dupe VEC */ +/** @brief Sort and de-duplicate @p vec + * @param vec Vector to sort + * @param nvec Length of @p vec + * @return @p vec + * + * The returned vector is NULL-terminated, and there must be room for this NULL + * even if there are no duplicates (i.e. it must have more than @p nvec + * elements.) + */ static char **dedupe(char **vec, int nvec) { int m, n; @@ -696,7 +849,17 @@ static char **dedupe(char **vec, int nvec) { return vec; } -/* update a key/track database. Returns 0 or DB_DEADLOCK. */ +/** @brief Store a key/data pair + * @param db Database + * @param what Description + * @param track Data + * @param word Key + * @param tid Owning transaction + * @return 0 or DB_DEADLOCK + * + * Used by the search and tags databases, hence the odd parameter names. + * See also trackdb_delkeydata(). + */ static int register_word(DB *db, const char *what, const char *track, const char *word, DB_TXN *tid) { @@ -718,13 +881,22 @@ static int register_word(DB *db, const char *what, /* search primitives *********************************************************/ -/* return true iff NAME is a trackname_display_ pref */ +/** @brief Return true iff @p name is a trackname_display_ pref + * @param name Preference name + * @return Non-zero iff @p name is a trackname_display_ pref + */ static int is_display_pref(const char *name) { static const char prefix[] = "trackname_display_"; return !strncmp(name, prefix, (sizeof prefix) - 1); } -/** @brief Word_Break property tailor that treats underscores as spaces */ +/** @brief Word_Break property tailor that treats underscores as spaces + * @param c Code point + * @return Tailored property or -1 to use standard value + * + * Passed to utf32_word_split() when splitting a track name into words. + * See word_split() and @ref unicode_property_tailor. + */ static int tailor_underscore_Word_Break_Other(uint32_t c) { switch(c) { default: @@ -750,7 +922,20 @@ static size_t remove_combining_chars(uint32_t *s, size_t ns) { return t - start; } -/** @brief Normalize and split a string using a given tailoring */ +/** @brief Normalize and split a string using a given tailoring + * @param v Where to store words from string + * @param s Input string + * @param pt Word_Break property tailor, or NULL + * + * The output words will be: + * - case-folded + * - have any combination characters stripped + * - not include any word break code points (as tailored) + * + * Used by track_to_words(), with @p pt set to @ref + * tailor_underscore_Word_Break_Other, and by normalize_tag() with no + * tailoring. + */ static void word_split(struct vector *v, const char *s, unicode_property_tailor *pt) { @@ -806,7 +991,11 @@ static char *normalize_tag(const char *s, size_t ns) { return d->vec; } -/* compute the words of a track name */ +/** @brief Compute the words of a track name + * @param track Track name + * @param p Preferences (for display prefs) + * @return NULL-terminated, de-duplicated list or words + */ static char **track_to_words(const char *track, const struct kvp *p) { struct vector v; @@ -824,7 +1013,10 @@ static char **track_to_words(const char *track, return dedupe(v.vec, v.nvec); } -/* return nonzero iff WORD is a stopword */ +/** @brief Test for a stopword + * @param word Word + * @return Non-zero if @p word is a stopword + */ static int stopword(const char *word) { int n; @@ -834,7 +1026,12 @@ static int stopword(const char *word) { return n < config->stopword.n; } -/* record that WORD appears in TRACK. Returns 0 or DB_LOCK_DEADLOCK. */ +/** @brief Register a search term + * @param track Track name + * @param word A word that appears in the name of @p track + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int register_search_word(const char *track, const char *word, DB_TXN *tid) { if(stopword(word)) return 0; @@ -843,7 +1040,13 @@ static int register_search_word(const char *track, const char *word, /* Tags **********************************************************************/ -/* Return nonzero if C is a valid tag character */ +/** @brief Test for tag characters + * @param c Character + * @return Non-zero if @p c is a tag character + * + * The current rule is that commas and the control characters 0-31 are not + * allowed but anything else is permitted. This is arguably a bit loose. + */ static int tagchar(int c) { switch(c) { case ',': @@ -853,7 +1056,12 @@ static int tagchar(int c) { } } -/* Parse and de-dupe a tag list. If S=0 then assumes "". */ +/** @brief Parse a tag list + * @param s Tag list or NULL (equivalent to "") + * @return Parsed tag list + * + * The tags will be normalized (as per normalize_tag()) and de-duplicated. + */ char **parsetags(const char *s) { const char *t; struct vector v; @@ -882,15 +1090,38 @@ char **parsetags(const char *s) { return dedupe(v.vec, v.nvec); } -/* Record that TRACK has TAG. Returns 0 or DB_LOCK_DEADLOCK. */ +/** @brief Register a tag + * @param track Track name + * @param tag Tag name + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int register_tag(const char *track, const char *tag, DB_TXN *tid) { return register_word(trackdb_tagsdb, "tags", track, tag, tid); } /* aliases *******************************************************************/ -/* compute the alias and store at aliasp. Returns 0 or DB_LOCK_DEADLOCK. If - * there is no alias sets *aliasp to 0. */ +/** @brief Compute an alias + * @param aliasp Where to put alias (gets NULL if none) + * @param track Track to find alias for + * @param p Prefs for @p track + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + * + * This function looks up the track name parts for @p track. By default these + * amount to the original values from the track name but are overridden by + * preferences. + * + * These values are then substituted into the pattern defined by the @b alias + * command; see disorder_config(5) for the syntax. + * + * The track is only considered to have an alias if all of the following are + * true: + * - a preference was used for at least one name part + * - the result differs from the original track name + * - the result does not match any existing track or alias + */ static int compute_alias(char **aliasp, const char *track, const struct kvp *p, @@ -953,15 +1184,28 @@ static int compute_alias(char **aliasp, } } -/* get track and prefs data (if tp/pp not null pointers). Returns 0 on - * success, DB_NOTFOUND if the track does not exist or DB_LOCK_DEADLOCK. - * Always sets the return values, even if only to null pointers. */ +/** @brief Assert that no alias is allowed for gettrackdata() */ +#define GTD_NOALIAS 0x0001 + +/** @brief Get all track data + * @param track Track to look up; aliases allowed unless @ref GTD_NOALIAS + * @param tp Where to put track data (if not NULL) + * @param pp Where to put preferences (if not NULL) + * @param actualp Where to put real (i.e. non-alias) path (if not NULL) + * @param flags Flag values, see below + * @param tid Owning transaction + * @return 0, DB_NOTFOUND (track doesn't exist) or DB_LOCK_DEADLOCK + * + * Possible flags values are: + * - @ref GTD_NOALIAS to assert that an alias is not allowed + * + * The return values are always set (even if to NULL). + */ static int gettrackdata(const char *track, struct kvp **tp, struct kvp **pp, const char **actualp, unsigned flags, -#define GTD_NOALIAS 0x0001 DB_TXN *tid) { int err; const char *actual = track; @@ -991,8 +1235,12 @@ done: /* trackdb_notice() **********************************************************/ -/** @brief notice a possibly new track +/** @brief Notice a possibly new track + * @param track NFC UTF-8 track name + * @param path Raw path name (i.e. the bytes that came out of readdir()) * @return @c DB_NOTFOUND if new, 0 if already known + * + * @c disorder-rescan is responsible for normalizing the track name. */ int trackdb_notice(const char *track, const char *path) { @@ -1011,11 +1259,13 @@ int trackdb_notice(const char *track, return err; } -/** @brief notice a possibly new track +/** @brief Notice a possibly new track * @param track NFC UTF-8 track name - * @param path Raw path name - * @param tid Transaction ID + * @param path Raw path name (i.e. the bytes that came out of readdir()) + * @param tid Owning transaction * @return @c DB_NOTFOUND if new, 0 if already known, @c DB_LOCK_DEADLOCK also + * + * @c disorder-rescan is responsible for normalizing the track name. */ int trackdb_notice_tid(const char *track, const char *path, @@ -1035,7 +1285,7 @@ int trackdb_notice_tid(const char *track, /* this is a real track */ t_changed += kvp_set(&t, "_alias_for", 0); t_changed += kvp_set(&t, "_path", path); - time(&now); + xtime(&now); if(ret == DB_NOTFOUND) { /* It's a new track; record the time */ byte_xasprintf(¬iced, "%lld", (long long)now); @@ -1084,7 +1334,14 @@ int trackdb_notice_tid(const char *track, /* trackdb_obsolete() ********************************************************/ -/* obsolete a track */ +/** @brief Obsolete a track + * @param track Track name + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + * + * Discards a track from the database when it's known not to exist any more. + * Returns 0 even if it wasn't recorded. + */ int trackdb_obsolete(const char *track, DB_TXN *tid) { int err, n; struct kvp *p; @@ -1168,7 +1425,14 @@ static const struct statinfo { B(bt_over_pgfree), }; -/* look up stats for DB */ +/** @brief Look up DB statistics + * @param v Where to store stats + * @param database Database + * @param si Pointer to table of stats + * @param nsi Size of @p si + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int get_stats(struct vector *v, DB *database, const struct statinfo *si, @@ -1235,7 +1499,12 @@ static int register_search_entry(struct search_entry *se, return nse; } -/* find the top COUNT words in the search database */ +/** @brief Find the top @p count words in the search database + * @param v Where to format the result + * @param count Maximum number of words + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int search_league(struct vector *v, int count, DB_TXN *tid) { struct search_entry *se; DBT k, d; @@ -1284,7 +1553,13 @@ static int search_league(struct vector *v, int count, DB_TXN *tid) { #define SI(what) statinfo_##what, \ sizeof statinfo_##what / sizeof (struct statinfo) -/* return a list of database stats */ +/** @brief Return a list of database stats + * @param nstatsp Where to store number of lines (or NULL) + * @return Database stats output + * + * This is called by @c disorder-stats. Don't call it directly from elsewhere + * as it can take unreasonably long. + */ char **trackdb_stats(int *nstatsp) { DB_TXN *tid; struct vector v; @@ -1313,6 +1588,7 @@ fail: return v.vec; } +/** @brief State structure tracking @c disorder-stats */ struct stats_details { void (*done)(char *data, void *u); void *u; @@ -1322,6 +1598,12 @@ struct stats_details { struct dynstr data[1]; /* data read from pipe */ }; +/** @brief Called when @c disorder-stats may have completed + * @param d Pointer to state structure + * + * Called from stats_finished() and stats_read(). Only proceeds when the + * process has terminated and the output is complete. + */ static void stats_complete(struct stats_details *d) { char *s; @@ -1338,8 +1620,16 @@ static void stats_complete(struct stats_details *d) { d->done(d->data->vec, d->u); } +/** @brief Called when @c disorder-stats exits + * @param ev Event loop + * @param pid Process ID + * @param status Exit status + * @param rusage Resource usage + * @param u Pointer to state structure (@ref stats_details) + * @return 0 + */ static int stats_finished(ev_source attribute((unused)) *ev, - pid_t attribute((unused)) pid, + pid_t pid, int status, const struct rusage attribute((unused)) *rusage, void *u) { @@ -1349,9 +1639,21 @@ static int stats_finished(ev_source attribute((unused)) *ev, if(status) error(0, "disorder-stats %s", wstat(status)); stats_complete(d); + char *k; + byte_xasprintf(&k, "%lu", (unsigned long)pid); + hash_remove(stats_pids, k); return 0; } +/** @brief Called when pipe from @c disorder-stats is readable + * @param ev Event loop + * @param reader Reader state + * @param ptr Pointer to bytes read + * @param bytes Number of bytes available + * @param eof Set at end of file + * @param u Pointer to state structure (@ref stats_details) + * @return 0 + */ static int stats_read(ev_source attribute((unused)) *ev, ev_reader *reader, void *ptr, @@ -1368,6 +1670,12 @@ static int stats_read(ev_source attribute((unused)) *ev, return 0; } +/** @brief Called when pipe from @c disorder-stats errors + * @param ev Event loop + * @param errno_value Error code + * @param u Pointer to state structure (@ref stats_details) + * @return 0 + */ static int stats_error(ev_source attribute((unused)) *ev, int errno_value, void *u) { @@ -1379,6 +1687,14 @@ static int stats_error(ev_source attribute((unused)) *ev, return 0; } +/** @brief Get database statistics via background process + * @param ev Event loop + * @param done Called on completion + * @param u Passed to @p done + * + * Within the main server use this instead of trackdb_stats(), which can take + * unreasonably long. + */ void trackdb_stats_subprocess(ev_source *ev, void (*done)(char *data, void *u), void *u) { @@ -1393,7 +1709,15 @@ void trackdb_stats_subprocess(ev_source *ev, pid = subprogram(ev, p[1], "disorder-stats", (char *)0); xclose(p[1]); ev_child(ev, pid, 0, stats_finished, d); - ev_reader_new(ev, p[0], stats_read, stats_error, d, "disorder-stats reader"); + if(!ev_reader_new(ev, p[0], stats_read, stats_error, d, + "disorder-stats reader")) + fatal(0, "ev_reader_new for disorder-stats reader failed"); + /* Remember the PID */ + if(!stats_pids) + stats_pids = hash_new(1); + char *k; + byte_xasprintf(&k, "%lu", (unsigned long)pid); + hash_add(stats_pids, k, "", HASH_INSERT); } /** @brief Parse a track name part preference @@ -1450,7 +1774,12 @@ static const char *trackdb__default(const char *track, const char *name) { return 0; } -/* set a pref (remove if value=0) */ +/** @brief Set a preference + * @param track Track to modify + * @param name Preference name + * @param value New value, or NULL to erase any existing value + * @return 0 on success or non-zero if not allowed to set preference + */ int trackdb_set(const char *track, const char *name, const char *value) { @@ -1547,13 +1876,20 @@ fail: return err == 0 ? 0 : -1; } -/* get a pref */ +/** @brief Get the value of a preference + * @param track Track name + * @param name Preference name + * @return Preference value or NULL if it's not set + */ const char *trackdb_get(const char *track, const char *name) { return kvp_get(trackdb_get_all(track), name); } -/* get all prefs as a 0-terminated array */ +/** @brief Get all preferences for a track + * @param track Track name + * @return Linked list of preferences + */ struct kvp *trackdb_get_all(const char *track) { struct kvp *t, *p, **pp; DB_TXN *tid; @@ -1573,7 +1909,10 @@ fail: return p; } -/* resolve alias */ +/** @brief Resolve an alias + * @param track Track name (might be an alias) + * @return Real track name (definitely not an alias) or NULL if no such track + */ const char *trackdb_resolve(const char *track) { DB_TXN *tid; const char *actual; @@ -1590,13 +1929,20 @@ fail: return actual; } +/** @brief Detect an alias + * @param track Track name + * @return Nonzero if @p track exists and is an alias + */ int trackdb_isalias(const char *track) { const char *actual = trackdb_resolve(track); return strcmp(actual, track); } -/* test whether a track exists (perhaps an alias) */ +/** @brief Detect whether a track exists + * @param track Track name (can be an alias) + * @return Nonzero if @p track exists (whether or not it's an alias) + */ int trackdb_exists(const char *track) { DB_TXN *tid; int err; @@ -1614,7 +1960,9 @@ fail: return (err == 0); } -/* return the list of tags */ +/** @brief Return list of all known tags + * @return NULL-terminated tag list + */ char **trackdb_alltags(void) { int e; struct vector v[1]; @@ -1654,6 +2002,13 @@ int trackdb_listkeys(DB *db, struct vector *v, DB_TXN *tid) { } /* return 1 iff sorted tag lists A and B have at least one member in common */ +/** @brief Detect intersecting tag lists + * @param a First list of tags (NULL-terminated) + * @param b Second list of tags (NULL-terminated) + * @return 1 if @p a and @p b have at least one member in common + * + * @p a and @p must be sorted. + */ int tag_intersection(char **a, char **b) { int cmp; @@ -1666,15 +2021,13 @@ int tag_intersection(char **a, char **b) { return 0; } -static pid_t choose_pid = -1; -static int choose_fd; -static random_callback *choose_callback; -static struct dynstr choose_output; -static unsigned choose_complete; -static int choose_status; -#define CHOOSE_RUNNING 1 -#define CHOOSE_READING 2 - +/** @brief Called when disorder-choose might have completed + * @param ev Event loop + * @param which @ref CHOOSE_RUNNING or @ref CHOOSE_READING + * + * Once called with both @p which values, @ref choose_callback is called + * (usually chosen_random_track()). + */ static void choose_finished(ev_source *ev, unsigned which) { choose_complete |= which; if(choose_complete != (CHOOSE_RUNNING|CHOOSE_READING)) @@ -1687,7 +2040,14 @@ static void choose_finished(ev_source *ev, unsigned which) { choose_callback(ev, 0); } -/** @brief Called when @c disorder-choose terminates */ +/** @brief Called when @c disorder-choose terminates + * @param ev Event loop + * @param pid Process ID + * @param status Exit status + * @param rusage Resource usage + * @param u User data + * @return 0 + */ static int choose_exited(ev_source *ev, pid_t attribute((unused)) pid, int status, @@ -1700,7 +2060,15 @@ static int choose_exited(ev_source *ev, return 0; } -/** @brief Called with data from @c disorder-choose pipe */ +/** @brief Called with data from @c disorder-choose pipe + * @param ev Event loop + * @param reader Reader state + * @param ptr Data read + * @param bytes Number of bytes read + * @param eof Set at end of file + * @param u User data + * @return 0 + */ static int choose_readable(ev_source *ev, ev_reader *reader, void *ptr, @@ -1714,6 +2082,12 @@ static int choose_readable(ev_source *ev, return 0; } +/** @brief Called when @c disorder-choose pipe errors + * @param ev Event loop + * @param errno_value Error code + * @param u User data + * @return 0 + */ static int choose_read_error(ev_source *ev, int errno_value, void attribute((unused)) *u) { @@ -1749,14 +2123,23 @@ int trackdb_request_random(ev_source *ev, choose_callback = callback; choose_output.nvec = 0; choose_complete = 0; - ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0, - "disorder-choose reader"); /* owns p[0] */ + if(!ev_reader_new(ev, p[0], choose_readable, choose_read_error, 0, + "disorder-choose reader")) /* owns p[0] */ + fatal(0, "ev_reader_new for disorder-choose reader failed"); ev_child(ev, choose_pid, 0, choose_exited, 0); /* owns the subprocess */ return 0; } -/* get a track name given the prefs. Set *used_db to 1 if we got the answer - * from the prefs. */ +/** @brief Get a track name part, using prefs + * @param track Track name + * @param context Context ("display" etc) + * @param part Part ("album" etc) + * @param p Preference + * @param used_db Set if a preference is used + * @return Name part (never NULL) + * + * Used by compute_alias() and trackdb_getpart(). + */ static const char *getpart(const char *track, const char *context, const char *part, @@ -1774,8 +2157,14 @@ static const char *getpart(const char *track, return result; } -/* get a track name part, like trackname_part(), but taking the database into - * account. */ +/** @brief Get a track name part + * @param track Track name + * @param context Context ("display" etc) + * @param part Part ("album" etc) + * @return Name part (never NULL) + * + * This is interface used by c_part(). + */ const char *trackdb_getpart(const char *track, const char *context, const char *part) { @@ -1799,7 +2188,12 @@ fail: return getpart(actual, context, part, p, &used_db); } -/* get the raw path name for @track@ (might be an alias) */ +/** @brief Get the raw (filesystem) path for @p track + * @param track track Track name (can be an alias) + * @return Raw path (never NULL) + * + * The raw path is the actual bytes that came out of readdir() etc. + */ const char *trackdb_rawpath(const char *track) { DB_TXN *tid; struct kvp *t; @@ -1825,6 +2219,19 @@ fail: /* return true if the basename of TRACK[0..TL-1], as defined by DL, matches RE. * If RE is a null pointer then it matches everything. */ +/** @brief Match a track against a rgeexp + * @param dl Length of directory part of track + * @param track Track name + * @param tl Length of track name + * @param re Regular expression or NULL + * @return Nonzero on match + * + * @p tl is the total length of @p track, @p dl is the length of the directory + * part (the index of the final "/"). The subject of the regexp match is the + * basename, i.e. the part after @p dl. + * + * If @p re is NULL then always matches. + */ static int track_matches(size_t dl, const char *track, size_t tl, const pcre *re) { int ovec[3], rc; @@ -1844,6 +2251,14 @@ static int track_matches(size_t dl, const char *track, size_t tl, } } +/** @brief Generate a list of tracks and/or directories in @p dir + * @param v Where to put results + * @param dir Directory to list + * @param what Bitmap of objects to return + * @param re Regexp to filter matches (or NULL to accept all) + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int do_list(struct vector *v, const char *dir, enum trackdb_listable what, const pcre *re, DB_TXN *tid) { DBC *cursor; @@ -1935,7 +2350,13 @@ deadlocked: return err; } -/* return the directories or files below @dir@ */ +/** @brief Get the directories or files below @p dir + * @param dir Directory to list + * @param np Where to put number of results (or NULL) + * @param what Bitmap of objects to return + * @param re Regexp to filter matches (or NULL to accept all) + * @return List of tracks + */ char **trackdb_list(const char *dir, int *np, enum trackdb_listable what, const pcre *re) { DB_TXN *tid; @@ -1965,7 +2386,12 @@ fail: return v.vec; } -/* If S is tag:something, return something. Else return 0. */ +/** @brief Detect a tag element in a search string + * @param s Element of search string + * @return Pointer to tag name (in @p s) if this is a tag: search, else NULL + * + * Tag searches take the form "tag:TAG". + */ static const char *checktag(const char *s) { if(!strncmp(s, "tag:", 4)) return s + 4; @@ -2105,6 +2531,17 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) { /* trackdb_scan **************************************************************/ +/** @brief Visit every track + * @param root Root to scan or NULL for all + * @param callback Callback for each track + * @param u Passed to @p callback + * @param tid Owning transaction + * @return 0, DB_LOCK_DEADLOCK or EINTR + * + * Visits every track and calls @p callback. @p callback will get the track + * data and preferences and should return 0 to continue scanning or EINTR to + * stop. + */ int trackdb_scan(const char *root, int (*callback)(const char *track, struct kvp *data, @@ -2240,7 +2677,7 @@ static int reap_rescan(ev_source attribute((unused)) *ev, * @param ev Event loop or 0 to block * @param recheck 1 to recheck lengths, 0 to suppress check * @param rescanned Called on completion (if not NULL) - * @param u Passed to @p rescanned + * @param ru Passed to @p rescanned */ void trackdb_rescan(ev_source *ev, int recheck, void (*rescanned)(void *ru), @@ -2267,6 +2704,9 @@ void trackdb_rescan(ev_source *ev, int recheck, } } +/** @brief Cancel a rescan + * @return Nonzero if a rescan was cancelled + */ int trackdb_rescan_cancel(void) { if(rescan_pid == -1) return 0; if(kill(rescan_pid, SIGTERM) < 0) @@ -2282,6 +2722,11 @@ int trackdb_rescan_underway(void) { /* global prefs **************************************************************/ +/** @brief Set a global preference + * @param name Global preference name + * @param value New value + * @param who Who is setting it + */ void trackdb_set_global(const char *name, const char *value, const char *who) { @@ -2313,6 +2758,11 @@ void trackdb_set_global(const char *name, } } +/** @brief Set a global preference + * @param name Global preference name + * @param value New value + * @param tid Owning transaction + */ int trackdb_set_global_tid(const char *name, const char *value, DB_TXN *tid) { @@ -2337,6 +2787,10 @@ int trackdb_set_global_tid(const char *name, return 0; } +/** @brief Get a global preference + * @param name Global preference name + * @return Value of global preference, or NULL if it's not set + */ const char *trackdb_get_global(const char *name) { DB_TXN *tid; int err; @@ -2352,6 +2806,12 @@ const char *trackdb_get_global(const char *name) { return r; } +/** @brief Get a global preference + * @param name Global preference name + * @param tid Owning transaction + * @param rp Where to store value (will get NULL if preference not set) + * @return 0 or DB_LOCK_DEADLOCK + */ int trackdb_get_global_tid(const char *name, DB_TXN *tid, const char **rp) { @@ -2406,9 +2866,6 @@ char **trackdb_new(int *ntracksp, * @return null-terminated array of track names, or NULL on deadlock * * The most recently added track is first in the array. - * - * TODO: exclude tracks that have been deleted again. - * */ static char **trackdb_new_tid(int *ntracksp, int maxtracks, @@ -2417,12 +2874,24 @@ static char **trackdb_new_tid(int *ntracksp, DBT k, d; int err = 0; struct vector tracks[1]; + hash *h = hash_new(1); vector_init(tracks); c = trackdb_opencursor(trackdb_noticeddb, tid); while((maxtracks <= 0 || tracks->nvec < maxtracks) - && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV))) - vector_append(tracks, xstrndup(d.data, d.size)); + && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV))) { + char *const track = xstrndup(d.data, d.size); + /* Don't add any track more than once */ + if(hash_add(h, track, "", HASH_INSERT)) + continue; + /* See if the track still exists */ + err = trackdb_getdata(trackdb_tracksdb, track, NULL/*kp*/, tid); + if(err == DB_NOTFOUND) + continue; /* It doesn't, skip it */ + if(err == DB_LOCK_DEADLOCK) + break; /* Doh */ + vector_append(tracks, track); + } switch(err) { case 0: /* hit maxtracks */ case DB_NOTFOUND: /* ran out of tracks */ @@ -2500,6 +2969,10 @@ static int trackdb_expire_noticed_tid(time_t earliest, DB_TXN *tid) { /* tidying up ****************************************************************/ +/** @brief Do database garbage collection + * + * Called form periodic_database_gc(). + */ void trackdb_gc(void) { int err; char **logfiles; @@ -2519,7 +2992,12 @@ void trackdb_gc(void) { /* user database *************************************************************/ -/** @brief Return true if @p user is trusted */ +/** @brief Return true if @p user is trusted + * @param user User to look up + * @return Nonzero if they are in the 'trusted' list + * + * Now used only in upgrade from old versions. + */ static int trusted(const char *user) { int n; @@ -2530,12 +3008,16 @@ static int trusted(const char *user) { } /** @brief Return non-zero for a valid username + * @param user Candidate username + * @return Nonzero if it's valid * * Currently we only allow the letters and digits in ASCII. We could be more * liberal than this but it is a nice simple test. It is critical that * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! */ -static int valid_username(const char *user) { +int valid_username(const char *user) { if(!*user) return 0; while(*user) { @@ -2551,7 +3033,16 @@ static int valid_username(const char *user) { return 1; } -/** @brief Add a user */ +/** @brief Add a user + * @param user Username + * @param password Initial password or NULL + * @param rights Initial rights + * @param email Email address or NULL + * @param confirmation Confirmation string to require + * @param tid Owning transaction + * @param flags DB flags e.g. DB_NOOVERWRITE + * @return 0, DB_KEYEXIST or DB_LOCK_DEADLOCK + */ static int create_user(const char *user, const char *password, const char *rights, @@ -2579,12 +3070,19 @@ static int create_user(const char *user, kvp_set(&k, "email", email); if(confirmation) kvp_set(&k, "confirmation", confirmation); - snprintf(s, sizeof s, "%jd", (intmax_t)time(0)); + snprintf(s, sizeof s, "%jd", (intmax_t)xtime(0)); kvp_set(&k, "created", s); return trackdb_putdata(trackdb_usersdb, user, k, tid, flags); } -/** @brief Add one pre-existing user */ +/** @brief Add one pre-existing user + * @param user Username + * @param password password + * @param tid Owning transaction + * @return 0, DB_KEYEXIST or DB_LOCK_DEADLOCK + * + * Used only in upgrade from old versions. + */ static int one_old_user(const char *user, const char *password, DB_TXN *tid) { const char *rights; @@ -2611,6 +3109,10 @@ static int one_old_user(const char *user, const char *password, tid, DB_NOOVERWRITE); } +/** @brief Upgrade old users + * @param tid Owning transaction + * @return 0 or DB_LOCK_DEADLOCK + */ static int trackdb_old_users_tid(DB_TXN *tid) { int n; @@ -2784,8 +3286,8 @@ int trackdb_edituserinfo(const char *user, } } else if(!strcmp(key, "email")) { if(*value) { - if(!strchr(value, '@')) { - error(0, "invalid email address '%s' for user '%s'", user, value); + if(!email_valid(value)) { + error(0, "invalid email address '%s' for user '%s'", value, user); return -1; } } else