/* 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)))
*/
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;
+
+/** @brief disorder-choose process is running */
+#define CHOOSE_RUNNING 1
+
+/** @brief disorder-choose pipe is still open */
+#define CHOOSE_READING 2
-/* comparison function for keys */
+/** @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);
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,
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;
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);
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 */
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,
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];
D(("opened databases"));
}
-/* close track databases */
+/** @brief Close track databases */
void trackdb_close(void) {
int err;
CLOSE("noticed.db", trackdb_noticeddb);
CLOSE("schedule.db", trackdb_scheduledb);
CLOSE("users.db", trackdb_usersdb);
+ CLOSE("playlists.db", trackdb_playlistsdb);
D(("closed databases"));
}
}
}
-/* 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,
}
}
-/* 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;
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;
}
}
-/* 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,
return err;
}
-/* start a transaction */
+/** @brief Start a transaction
+ * @return Transaction
+ */
DB_TXN *trackdb_begin_transaction(void) {
DB_TXN *tid;
int err;
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;
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;
/* 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;
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) {
/* 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:
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) {
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;
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;
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;
/* 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 ',':
}
}
-/* 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;
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);
}
/* 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);
}
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) {
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;
}
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
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
-
static void choose_finished(ev_source *ev, unsigned which) {
choose_complete |= which;
if(choose_complete != (CHOOSE_RUNNING|CHOOSE_READING))
* 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) {
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);
}