/*
* This file is part of DisOrder.
- * Copyright (C) 2004-2008 Richard Kettlewell
+ * Copyright (C) 2004-2009 Richard Kettlewell
*
* 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
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+/** @file server/play.c
+ * @brief Playing tracks
+ */
#include "disorder-server.h"
#include <ao/ao.h>
#define SPEAKER "disorder-speaker"
+/** @brief The current playing track or NULL */
struct queue_entry *playing;
+
+/** @brief Set when paused */
int paused;
static void finished(ev_source *ev);
+/** @brief File descriptor of our end of the socket to the speaker */
static int speaker_fd = -1;
-static hash *player_pids;
-static int shutting_down;
-
-static void store_player_pid(const char *id, pid_t pid) {
- if(!player_pids) player_pids = hash_new(sizeof (pid_t));
- hash_add(player_pids, id, &pid, HASH_INSERT_OR_REPLACE);
-}
-static pid_t find_player_pid(const char *id) {
- pid_t *pidp;
-
- if(player_pids && (pidp = hash_find(player_pids, id))) return *pidp;
- return -1;
-}
-
-static void forget_player_pid(const char *id) {
- if(player_pids) hash_remove(player_pids, id);
-}
+/** @brief Set when shutting down */
+static int shutting_down;
-/* called when speaker process terminates */
+/** @brief Called when speaker process terminates
+ *
+ * Currently kills of DisOrder completely. A future version could terminate
+ * the speaker when nothing was going on, or recover from failures, though any
+ * tracks with decoders already started would need to have them restarted.
+ */
static int speaker_terminated(ev_source attribute((unused)) *ev,
pid_t attribute((unused)) pid,
int attribute((unused)) status,
wstat(status));
}
-/* called when speaker process has something to say */
+/** @brief Called when we get a message from the speaker process */
static int speaker_readable(ev_source *ev, int fd,
void attribute((unused)) *u) {
struct speaker_message sm;
playing->sofar = sm.data;
break;
default:
- error(0, "unknown message type %d", sm.type);
+ error(0, "unknown speaker message type %d", sm.type);
}
return 0;
}
+/** @brief Initialize the speaker process */
void speaker_setup(ev_source *ev) {
int sp[2];
pid_t pid;
/* Wait for the speaker to be ready */
speaker_recv(speaker_fd, &sm);
nonblock(speaker_fd);
- ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0, "speaker read");
+ if(ev_fd(ev, ev_read, speaker_fd, speaker_readable, 0, "speaker read") < 0)
+ fatal(0, "error registering speaker socket fd");
}
+/** @brief Tell the speaker to reload its configuration */
void speaker_reload(void) {
struct speaker_message sm;
speaker_send(speaker_fd, &sm);
}
-/* Called when the currently playing track finishes playing. This
- * might be because the player finished or because the speaker process
- * told us so. */
+/** @brief Called when the currently playing track finishes playing
+ * @param ev Event loop or NULL
+ *
+ * There are three places this is called from:
+ *
+ * 1) speaker_readable(), when the speaker tells us the playing track finished.
+ * (Technically the speaker lies a little to arrange for gapless play.)
+ *
+ * 2) player_finished(), when the player for a non-raw track (i.e. one that
+ * does not use the speaker) finishes.
+ *
+ * 3) quitting(), after signalling the decoder or player but possible before it
+ * has actually terminated. In this case @p ev is NULL, inhibiting any further
+ * attempt to play anything.
+ */
static void finished(ev_source *ev) {
D(("finished playing=%p", (void *)playing));
if(!playing)
}
queue_played(playing);
recent_write();
- forget_player_pid(playing->id);
playing = 0;
/* Try to play something else */
/* TODO re-support config->gap? */
play(ev);
}
-/* Called when a player terminates. */
+/** @brief Called when a player or decoder process terminates
+ *
+ * This is called when a decoder process terminates (which might actually be
+ * some time before the speaker reports it as finished) or when a non-raw
+ * (i.e. non-speaker) player terminates. In the latter case it's imaginable
+ * that the OS has buffered the last few samples.
+ *
+ */
static int player_finished(ev_source *ev,
pid_t pid,
int status,
/* Record that this PID is dead. If we killed the track we might know this
* already, but also it might have exited or crashed. Either way we don't
* want to end up signalling it. */
- if(pid == find_player_pid(q->id))
- forget_player_pid(q->id);
+ q->pid = -1;
switch(q->state) {
case playing_unplayed:
case playing_random:
/* If this was a pre-prepared track then either it failed or we
- * deliberately stopped it because it was removed from the queue or moved
- * down it. So leave it state alone for future use. */
+ * deliberately stopped it: it might have been removed from the queue, or
+ * moved down the queue, or the speaker might be on a break. So we leave
+ * it state alone for future use.
+ */
break;
default:
/* We actually started playing this track. */
if(status) {
+ /* Don't override 'scratched' with 'failed'. */
if(q->state != playing_scratched)
q->state = playing_failed;
} else
return 0;
}
-/* Find the player for Q */
+/** @brief Find the player for @p q */
static int find_player(const struct queue_entry *q) {
int n;
}
/* Return values from start() */
-#define START_OK 0 /**< @brief Succeeded. */
-#define START_HARDFAIL 1 /**< @brief Track is broken. */
-#define START_SOFTFAIL 2 /**< @brief Track OK, system (temporarily?) broken */
+#define START_OK 0 /**< @brief Succeeded. */
+#define START_HARDFAIL 1 /**< @brief Track is broken. */
+#define START_SOFTFAIL 2 /**< @brief Track OK, system (temporarily?) broken */
/** @brief Play or prepare @p q
* @param ev Event loop
* @param q Track to play/prepare
* @param prepare_only If true, only prepares track
* @return @ref START_OK, @ref START_HARDFAIL or @ref START_SOFTFAIL
+ *
+ * TODO: probably ought to be split up into separate prepare() and start()
+ * operations, the latter calling the former if necessary and perhaps the two
+ * sharing some subprocess creation logic.
+ *
+ * "Preparing" a track means starting a background decoder and connecting it to
+ * the speaker process. Obviously this only applies to raw-format
+ * (i.e. speaker-using) players.
*/
static int start(ev_source *ev,
struct queue_entry *q,
struct timespec ts;
const char *waitdevice = 0;
const char *const *optv;
- pid_t pid, npid;
+ pid_t npid;
struct sockaddr_un addr;
uint32_t l;
memset(&sm, 0, sizeof sm);
D(("start %s %d", q->id, prepare_only));
if(q->prepared) {
- /* The track is alraedy prepared */
+ /* The track is already prepared */
if(!prepare_only) {
/* We want to run it, since it's prepared the answer is to tell the
* speaker to set it off */
if(prepare_only
&& (q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
return START_OK;
- /* Call the prefork function. */
+ /* Call the prefork function in the player module. None of the built-in
+ * modules use this so it's not well tested, unfortunately. */
p = trackdb_rawpath(q->track);
if(q->type & DISORDER_PLAYER_PREFORK)
if(!(q->data = play_prefork(q->pl, p))) {
error(0, "prefork function for %s failed", q->track);
return START_HARDFAIL;
}
- /* Use the second arg as the tag if available (it's probably a command name),
+ /* Capture the player/decoder's stderr and feed it into our logs.
+ *
+ * Use the second arg as the tag if available (it's probably a command name),
* otherwise the module name. */
if(!isatty(2))
lfd = logfd(ev, (config->player.s[n].s[2]
? config->player.s[n].s[2] : config->player.s[n].s[1]));
else
lfd = -1;
+ /* Parse player arguments */
optc = config->player.s[n].n - 2;
optv = (void *)&config->player.s[n].s[2];
while(optc > 0 && optv[0][0] == '-') {
return START_HARDFAIL;
}
}
- switch(pid = fork()) {
- case 0: /* child */
+ /* Create the subprocess */
+ switch(q->pid = fork()) {
+ case 0:
+ /* Child of disorderd */
exitfn = _exit;
progname = "disorderd-fork";
ev_signal_atfork(ev);
signal(SIGPIPE, SIG_DFL);
+ /* Send our log output to DisOrder's logs */
if(lfd != -1) {
xdup2(lfd, 1);
xdup2(lfd, 2);
xclose(lfd); /* tidy up */
}
+ /* Create a new process group, ID = child PID */
setpgid(0, 0);
if((q->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
/* "Raw" format players always have their output send down a pipe
xshutdown(np[0], SHUT_WR); /* normalize reads from np[0] */
blocking(np[0]);
blocking(np[1]);
- /* Start disorder-normalize */
+ /* Start disorder-normalize. We double-fork so that nothing has to wait
+ * for disorder-normalize. */
if(!(npid = xfork())) {
+ /* Grandchild of disorderd */
if(!xfork()) {
+ /* Great-grandchild of disorderd */
/* Connect to the speaker process */
memset(&addr, 0, sizeof addr);
addr.sun_family = AF_UNIX;
sfd = xsocket(PF_UNIX, SOCK_STREAM, 0);
if(connect(sfd, (const struct sockaddr *)&addr, sizeof addr) < 0)
fatal(errno, "connecting to %s", addr.sun_path);
+ /* Send the ID, with a NATIVE-ENDIAN 32 bit length */
l = strlen(q->id);
if(write(sfd, &l, sizeof l) < 0
|| write(sfd, q->id, l) < 0)
fatal(errno, "writing to %s", addr.sun_path);
/* Await the ack */
- read(sfd, &l, 1);
+ if (read(sfd, &l, 1) < 0)
+ fatal(errno, "reading ack from %s", addr.sun_path);
/* Plumbing */
xdup2(np[0], 0);
xdup2(sfd, 1);
xclose(np[1]);
xclose(sfd);
/* Ask the speaker to actually start playing the track; we do it here
- * so it's definitely after ack. */
+ * so it's definitely after ack.
+ *
+ * This is actually insufficient. If the track is prepared and then
+ * very shortly afterwards played, then the race we're trying to
+ * avoid here will still exist. So either the speaker must cope with
+ * SM_PLAY before connection has arrived (in which case we might as
+ * well move the SM_PLAY somewhere saner) or we must do more work
+ * here to avoid the race.
+ *
+ * In fact the current speaker can indeed cope with SM_PLAY before
+ * the connection arrives. So this code can probably be moved
+ * somewhere saner in due course. TODO!
+ */
if(!prepare_only) {
strcpy(sm.id, q->id);
sm.type = SM_PLAY;
"--config", configfile,
(char *)0);
fatal(errno, "executing disorder-normalize");
- /* end of the innermost fork */
+ /* End of the great-grandchild of disorderd */
}
+ /* Back in the grandchild of disorderd */
_exit(0);
- /* end of the middle fork */
+ /* End of the grandchild of disorderd */
}
- /* Wait for the middle fork to finish */
+ /* Back in the child of disorderd */
+ /* Wait for the grandchild of disordered to finish */
while(waitpid(npid, &n, 0) < 0 && errno == EINTR)
;
/* Pass the file descriptor to the driver in an environment
/* Close all the FDs we don't need */
xclose(np[0]);
}
+ /* Wait for a device to clear. This ugliness is now deprecated and will
+ * eventually be removed. */
if(waitdevice) {
ao_initialize();
if(*waitdevice) {
optv, optc,
p,
q->track);
+ /* End of child of disorderd */
_exit(0);
- case -1: /* error */
+ case -1:
+ /* Back in disorderd (child could not be created) */
error(errno, "error calling fork");
if(q->type & DISORDER_PLAYER_PREFORK)
play_cleanup(q->pl, q->data); /* else would leak */
xclose(lfd);
return START_SOFTFAIL;
}
- store_player_pid(q->id, pid);
+ /* Back in disorderd (child was created) */
+ /* This track is prepared. (Non-raw tracks are by implication prepared as
+ * soon as they are playing, but really the question doesn't make much sense
+ * for them.) */
q->prepared = 1;
if(lfd != -1)
xclose(lfd);
- setpgid(pid, pid);
- ev_child(ev, pid, 0, player_finished, q);
- D(("player subprocess ID %lu", (unsigned long)pid));
+ /* Set the child's process group ID.
+ *
+ * But wait, didn't we already set it in the child? Yes, but it's possible
+ * that we'll need to address it by process group ID before it gets that far,
+ * so we set it here too. One or the other may fail but as long as one
+ * succeeds that's fine.
+ */
+ setpgid(q->pid, q->pid);
+ /* Ask the event loop to tell us when the child terminates */
+ ev_child(ev, q->pid, 0, player_finished, q);
+ D(("player subprocess ID %lu", (unsigned long)q->pid));
return START_OK;
}
+/** @brief Prepare a track for later play
+ *
+ * Only applies to raw-format (speaker-using) players.
+ */
int prepare(ev_source *ev,
struct queue_entry *q) {
int n;
+ /* If there's a decoder (or player!) going we do nothing */
+ if(q->pid >= 0)
+ return 0;
/* Find the player plugin */
- if(find_player_pid(q->id) > 0) return 0; /* Already going. */
if((n = find_player(q)) < 0) return -1; /* No player */
q->pl = open_plugin(config->player.s[n].s[1], 0); /* No player */
q->type = play_get_type(q->pl);
return start(ev, q, 1/*prepare_only*/); /* Prepare it */
}
+/** @brief Abandon a queue entry
+ *
+ * Called from c_remove() (but NOT when scratching a track). Only does
+ * anything to raw-format tracks. Terminates the background decoder and tells
+ * the speaker process to cancel the track.
+ */
void abandon(ev_source attribute((unused)) *ev,
struct queue_entry *q) {
struct speaker_message sm;
- pid_t pid = find_player_pid(q->id);
- if(pid < 0) return; /* Not prepared. */
+ if(q->pid < 0)
+ return; /* Not prepared. */
if((q->type & DISORDER_PLAYER_TYPEMASK) != DISORDER_PLAYER_RAW)
return; /* Not a raw player. */
/* Terminate the player. */
- kill(-pid, config->signal);
- forget_player_pid(q->id);
+ kill(-q->pid, config->signal);
/* Cancel the track. */
memset(&sm, 0, sizeof sm);
sm.type = SM_CANCEL;
if(!track)
return;
/* Add the track to the queue */
- q = queue_add(track, 0, WHERE_END);
- q->state = playing_random;
+ q = queue_add(track, 0, WHERE_END, origin_random);
D(("picked %p (%s) at random", (void *)q, q->track));
queue_write();
/* Maybe a track can now be played */
/** @brief Maybe add a randomly chosen track
* @param ev Event loop
+ *
+ * Picking can take some time so the track will only be added after this
+ * function has returned.
*/
void add_random_track(ev_source *ev) {
struct queue_entry *q;
trackdb_request_random(ev, chosen_random_track);
}
-/* try to play a track */
+/** @brief Attempt to play something
+ *
+ * This is called from numerous locations - whenever it might conceivably have
+ * become possible to play something.
+ */
void play(ev_source *ev) {
struct queue_entry *q;
int random_enabled = random_is_enabled();
D(("play playing=%p", (void *)playing));
+ /* If we're shutting down, or there's something playing, or playing is not
+ * enabled, give up now */
if(shutting_down || playing || !playing_is_enabled()) return;
/* See if there's anything to play */
if(qhead.next == &qhead) {
* attempts to add a random track anyway. However they are rarer than
* attempts to force a track so we initiate one now. */
add_random_track(ev);
+ /* chosen_random_track() will call play() when a new random track has been
+ * added to the queue. */
return;
}
/* There must be at least one track in the queue. */
q = qhead.next;
- /* If random play is disabled but the track is a random one then don't play
- * it. play() will be called again when random play is re-enabled. */
- if(!random_enabled && q->state == playing_random)
+ /* If random play is disabled but the track is a non-adopted random one
+ * then don't play it. play() will be called again when random play is
+ * re-enabled. */
+ if(!random_enabled && q->origin == origin_random)
return;
D(("taken %p (%s) from queue", (void *)q, q->track));
/* Try to start playing. */
/* We'll try the same track again shortly. */
break;
case START_OK:
+ /* Remove from the queue */
if(q == qhead.next) {
queue_remove(q, 0);
queue_write();
}
+ /* It's become the playing track */
playing = q;
time(&playing->played);
playing->state = playing_started;
}
}
+/** @brief Return true if play is enabled */
int playing_is_enabled(void) {
const char *s = trackdb_get_global("playing");
return !s || !strcmp(s, "yes");
}
+/** @brief Enable play */
void enable_playing(const char *who, ev_source *ev) {
trackdb_set_global("playing", "yes", who);
/* Add a random track if necessary. */
play(ev);
}
+/** @brief Disable play */
void disable_playing(const char *who) {
trackdb_set_global("playing", "no", who);
}
+/** @brief Return true if random play is enabled */
int random_is_enabled(void) {
const char *s = trackdb_get_global("random-play");
return !s || !strcmp(s, "yes");
}
+/** @brief Enable random play */
void enable_random(const char *who, ev_source *ev) {
trackdb_set_global("random-play", "yes", who);
add_random_track(ev);
play(ev);
}
+/** @brief Disable random play */
void disable_random(const char *who) {
trackdb_set_global("random-play", "no", who);
}
+/** @brief Scratch a track
+ * @param User responsible (or NULL)
+ * @param Track ID (or NULL for current)
+ */
void scratch(const char *who, const char *id) {
struct queue_entry *q;
struct speaker_message sm;
- pid_t pid;
D(("scratch playing=%p state=%d id=%s playing->id=%s",
(void *)playing,
playing ? playing->state : 0,
id ? id : "(none)",
playing ? playing->id : "(none)"));
+ /* There must be a playing track; it must be in a scratchable state; if a
+ * specific ID was mentioned it must be that track. */
if(playing
&& (playing->state == playing_started
|| playing->state == playing_paused)
&& (!id
|| !strcmp(id, playing->id))) {
+ /* Update state (for the benefit of the 'recent' list) */
playing->state = playing_scratched;
playing->scratched = who ? xstrdup(who) : 0;
- if((pid = find_player_pid(playing->id)) > 0) {
- D(("kill -%d %lu", config->signal, (unsigned long)pid));
- kill(-pid, config->signal);
- forget_player_pid(playing->id);
- } else
- error(0, "could not find PID for %s", playing->id);
+ /* Find the player and kill the whole process group */
+ if(playing->pid >= 0) {
+ D(("kill -%d -%lu", config->signal, (unsigned long)playing->pid));
+ kill(-playing->pid, config->signal);
+ }
+ /* Tell the speaker, if we think it'll care */
if((playing->type & DISORDER_PLAYER_TYPEMASK) == DISORDER_PLAYER_RAW) {
memset(&sm, 0, sizeof sm);
sm.type = SM_CANCEL;
* bother if playing is disabled) */
if(playing_is_enabled() && config->scratch.n) {
int r = rand() * (double)config->scratch.n / (RAND_MAX + 1.0);
- q = queue_add(config->scratch.s[r], who, WHERE_START);
- q->state = playing_isscratch;
+ q = queue_add(config->scratch.s[r], who, WHERE_START, origin_scratch);
}
notify_scratch(playing->track, playing->submitter, who,
time(0) - playing->played);
}
}
+/** @brief Called from quit() to tear down eveyrthing belonging to this file */
void quitting(ev_source *ev) {
struct queue_entry *q;
- pid_t pid;
/* Don't start anything new */
shutting_down = 1;
/* Shut down the current player */
if(playing) {
- if((pid = find_player_pid(playing->id)) > 0) {
- kill(-pid, config->signal);
- forget_player_pid(playing->id);
- } else
- error(0, "could not find PID for %s", playing->id);
+ if(playing->pid >= 0)
+ kill(-playing->pid, config->signal);
playing->state = playing_quitting;
finished(0);
}
- /* Zap any other players */
+ /* Zap any background decoders that are going */
for(q = qhead.next; q != &qhead; q = q->next)
- if((pid = find_player_pid(q->id)) > 0) {
- D(("kill -%d %lu", config->signal, (unsigned long)pid));
- kill(-pid, config->signal);
- forget_player_pid(q->id);
- } else
- error(0, "could not find PID for %s", q->id);
+ if(q->pid >= 0) {
+ D(("kill -%d %lu", config->signal, (unsigned long)q->pid));
+ kill(-q->pid, config->signal);
+ }
/* Don't need the speaker any more */
ev_fd_cancel(ev, ev_read, speaker_fd);
xclose(speaker_fd);
}
+/** @brief Pause the playing track */
int pause_playing(const char *who) {
struct speaker_message sm;
long played;
return 0;
}
+/** @brief Resume playing after a pause */
void resume_playing(const char *who) {
struct speaker_message sm;
c-basic-offset:2
comment-column:40
fill-column:79
+indent-tabs-mode:nil
End:
*/