/*
* This file is part of DisOrder.
- * Copyright (C) 2006 Richard Kettlewell
+ * Copyright (C) 2006, 2007 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
#include <assert.h>
#include <inttypes.h>
#include <stddef.h>
+#include <time.h>
#include "log.h"
#include "mem.h"
/* TODO: more commands */
+/** @brief How often to send data to the server when receiving logs */
+#define LOG_PROD_INTERVAL 10
+
/* Types *********************************************************************/
/** @brief Client state */
/** @brief Client structure */
struct disorder_eclient {
- const char *ident;
+ char *ident;
int fd; /**< @brief connection to server */
enum client_state state; /**< @brief current state */
int authenticated; /**< @brief true when authenicated */
int rc; /**< @brief response code */
char *line; /**< @brief complete line */
struct vector vec; /**< @brief body */
- const disorder_eclient_log_callbacks *log_callbacks; /**< @brief log callbacks */
+
+ const disorder_eclient_log_callbacks *log_callbacks;
+ /**< @brief log callbacks
+ *
+ * Once disorder_eclient_log() has been issued this is always set. When we
+ * re-connect it is checked to re-issue the log command.
+ */
void *log_v; /**< @brief user data */
- unsigned long statebits; /**< @brief current state */
+ unsigned long statebits; /**< @brief latest state */
+
+ time_t last_prod;
+ /**< @brief last time we sent a prod
+ *
+ * When we are receiving log data we send a "prod" byte to the server from
+ * time to time so that we detect broken connections reasonably quickly. The
+ * server just ignores these bytes.
+ */
+
+ /** @brief Protocol version */
+ int protocol;
};
/* Forward declarations ******************************************************/
-static int start_connect(void *cc,
- const struct sockaddr *sa,
- socklen_t len,
- const char *ident);
+static int start_connect(disorder_eclient *c);
static void process_line(disorder_eclient *c, char *line);
-static int start_connect(void *cc,
- const struct sockaddr *sa,
- socklen_t len,
- const char *ident);
static void maybe_connected(disorder_eclient *c);
static void authbanner_opcallback(disorder_eclient *c,
struct operation *op);
static void logentry_scratched(disorder_eclient *c, int nvec, char **vec);
static void logentry_state(disorder_eclient *c, int nvec, char **vec);
static void logentry_volume(disorder_eclient *c, int nvec, char **vec);
+static void logentry_rescanned(disorder_eclient *c, int nvec, char **vec);
/* Tables ********************************************************************/
LE(recent_added, 2, INT_MAX),
LE(recent_removed, 1, 1),
LE(removed, 1, 2),
+ LE(rescanned, 0, 0),
LE(scratched, 2, 2),
LE(state, 1, 1),
LE(volume, 2, 2)
vector_init(&c->vec);
dynstr_init(&c->input);
dynstr_init(&c->output);
- if(!config->password) {
- error(0, "no password set");
- return 0;
- }
return c;
}
xclose(c->fd);
c->fd = -1;
c->state = state_disconnected;
+ c->statebits = 0;
}
c->output.nvec = 0;
c->input.nvec = 0;
/* We'll need to resend all operations */
for(op = c->ops; op; op = op->next)
op->sent = 0;
+ /* Drop our use a hint that we're disconnected */
+ if(c->log_callbacks && c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits);
}
-/** @brief Return true if @c c is connected
- *
- * By connected it is meant that commands have a reasonable chance of being
- * processed soon, not merely that a TCP connection exists - for instance if
- * the client is still authenticating then that does not count as connected.
- */
-int disorder_eclient_connected(const disorder_eclient *c) {
- switch(c->state) {
- case state_disconnected:
- case state_connecting:
- case state_connected:
- return 0;
- case state_idle:
- case state_cmdresponse:
- case state_body:
- case state_log:
- return 1;
- }
- assert(!"reached");
+/** @brief Return current state */
+unsigned long disorder_eclient_state(const disorder_eclient *c) {
+ return c->statebits | (c->state > state_connected ? DISORDER_CONNECTED : 0);
}
/* Error reporting ***********************************************************/
*/
void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
struct operation *op;
+ time_t now;
D(("disorder_eclient_polled fd=%d state=%s mode=[%s %s]",
c->fd, states[c->state],
if(c->state == state_disconnected) {
D(("state_disconnected"));
- with_sockaddr(c, start_connect);
+ /* If there is no password yet then we cannot connect */
+ if(!config->password) {
+ comms_error(c, "no password is configured");
+ return;
+ }
+ start_connect(c);
/* might now be state_disconnected (on error), state_connecting (slow
* connect) or state_connected (fast connect). If state_disconnected then
* we just rely on a periodic callback from the event loop sometime. */
c->callbacks->report(c->u, 0);
}
+ /* Queue up a byte to send */
+ if(c->state == state_log
+ && c->output.nvec == 0
+ && time(&now) - c->last_prod > LOG_PROD_INTERVAL) {
+ put(c, "x", 1);
+ c->last_prod = now;
+ }
+
if(c->state == state_cmdresponse
|| c->state == state_body
|| c->state == state_log) {
}
/** @brief Called to start connecting */
-static int start_connect(void *cc,
- const struct sockaddr *sa,
- socklen_t len,
- const char *ident) {
- disorder_eclient *c = cc;
+static int start_connect(disorder_eclient *c) {
+ struct sockaddr *sa;
+ socklen_t len;
D(("start_connect"));
- c->ident = xstrdup(ident);
+ if((len = find_server(&sa, &c->ident)) == (socklen_t)-1)
+ return comms_error(c, "cannot look up server"); /* TODO better error */
if(c->fd != -1) {
xclose(c->fd);
c->fd = -1;
return comms_error(c, "socket: %s", strerror(errno));
c->eof = 0;
nonblock(c->fd);
+ cloexec(c->fd);
if(connect(c->fd, sa, len) < 0) {
switch(errno) {
case EINTR:
return 0;
default:
/* Signal the error to the caller. */
- return comms_error(c, "connecting to %s: %s", ident, strerror(errno));
+ return comms_error(c, "connecting to %s: %s", c->ident, strerror(errno));
}
} else
c->state = state_connected;
c->state = state_connected;
byte_xasprintf(&r, "connected to %s", c->ident);
c->callbacks->report(c->u, r);
+ /* If this is a log client we expect to get a bunch of updates from the
+ * server straight away */
}
}
size_t nonce_len;
const unsigned char *nonce;
const char *res;
+ char **rvec;
+ int nrvec;
+ const char *protocol, *algorithm, *challenge;
D(("authbanner_opcallback"));
- if(c->rc / 100 != 2) {
- /* Banner told us to go away. We cannot proceed. */
+ if(c->rc / 100 != 2
+ || !(rvec = split(c->line + 4, &nrvec, SPLIT_QUOTES, 0, 0))
+ || nrvec < 1) {
+ /* Banner told us to go away, or was malformed. We cannot proceed. */
+ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+ disorder_eclient_close(c);
+ return;
+ }
+ switch(nrvec) {
+ case 1:
+ protocol = "1";
+ algorithm = "sha1";
+ challenge = *rvec++;
+ break;
+ case 2:
+ protocol = "1";
+ algorithm = *rvec++;
+ challenge = *rvec++;
+ break;
+ case 3:
+ protocol = *rvec++;
+ algorithm = *rvec++;
+ challenge = *rvec++;
+ break;
+ default:
protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
disorder_eclient_close(c);
return;
}
- nonce = unhex(c->line + 4, &nonce_len);
- res = authhash(nonce, nonce_len, config->password);
+ c->protocol = atoi(protocol);
+ if(c->protocol < 1 || c->protocol > 2) {
+ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+ disorder_eclient_close(c);
+ return;
+ }
+ nonce = unhex(challenge, &nonce_len);
+ res = authhash(nonce, nonce_len, config->password, algorithm);
+ if(!res) {
+ protocol_error(c, op, c->rc, "%s: unknown authentication algorithm '%s'",
+ c->ident, algorithm);
+ disorder_eclient_close(c);
+ return;
+ }
stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
"user", quoteutf8(config->username), quoteutf8(res),
(char *)0);
case 3:
/* We need to collect the body. */
c->state = state_body;
- c->vec.nvec = 0;
+ vector_init(&c->vec);
break;
case 4:
assert(c->log_callbacks != 0);
/* Command support ***********************************************************/
-/* for commands with a simple string response */
+/* for commands with a quoted string response */
static void string_response_opcallback(disorder_eclient *c,
struct operation *op) {
D(("string_response_callback"));
if(c->rc / 100 == 2) {
- if(op->completed)
- ((disorder_eclient_string_response *)op->completed)(op->v, c->line + 4);
+ if(op->completed) {
+ if(c->protocol >= 2) {
+ char **rr = split(c->line + 4, 0, SPLIT_QUOTES, 0, 0);
+
+ if(rr && *rr)
+ ((disorder_eclient_string_response *)op->completed)(op->v, *rr);
+ else
+ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+ } else
+ ((disorder_eclient_string_response *)op->completed)(op->v,
+ c->line + 4);
+ }
} else
protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
}
"nop", (char *)0);
}
+/** @brief Get the last @p max added tracks
+ * @param c Client
+ * @param completed Called with list
+ * @param max Number of tracks to get, 0 for all
+ * @param v Passed to @p completed
+ *
+ * The first track in the list is the most recently added.
+ */
+int disorder_eclient_new_tracks(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ int max,
+ void *v) {
+ char limit[32];
+
+ sprintf(limit, "%d", max);
+ return simple(c, list_response_opcallback, (void (*)())completed, v,
+ "new", limit, (char *)0);
+}
+
+static void rtp_response_opcallback(disorder_eclient *c,
+ struct operation *op) {
+ D(("rtp_response_opcallback"));
+ if(c->rc / 100 == 2) {
+ if(op->completed) {
+ int nvec;
+ char **vec = split(c->line + 4, &nvec, SPLIT_QUOTES, 0, 0);
+
+ ((disorder_eclient_list_response *)op->completed)(op->v, nvec, vec);
+ }
+ } else
+ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+}
+
+/** @brief Determine the RTP target address
+ * @param c Client
+ * @param completed Called with address details
+ * @param v Passed to @p completed
+ *
+ * The address details will be two elements, the first being the hostname and
+ * the second the service (port).
+ */
+int disorder_eclient_rtp_address(disorder_eclient *c,
+ disorder_eclient_list_response *completed,
+ void *v) {
+ return simple(c, rtp_response_opcallback, (void (*)())completed, v,
+ "rtp-address", (char *)0);
+}
+
/* Log clients ***************************************************************/
/** @brief Monitor the server log
* Once a client is being used for logging it cannot be used for anything else.
* There is magic in authuser_opcallback() to re-submit the @c log command
* after reconnection.
+ *
+ * NB that the @c state callback may be called from within this function,
+ * i.e. not solely later on from the event loop callback.
*/
int disorder_eclient_log(disorder_eclient *c,
const disorder_eclient_log_callbacks *callbacks,
if(c->log_callbacks) return -1;
c->log_callbacks = callbacks;
c->log_v = v;
+ /* Repoort initial state */
+ if(c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits);
stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v,
"log", (char *)0);
return 0;
char **vec;
uintmax_t when;
- D(("log_opcallback [%s]", line));
+ D(("logline [%s]", line));
vec = split(line, &nvec, SPLIT_QUOTES, logline_error, c);
if(nvec < 2) return; /* probably an error, already
* reported */
static void logentry_completed(disorder_eclient *c,
int attribute((unused)) nvec, char **vec) {
if(!c->log_callbacks->completed) return;
+ c->statebits &= ~DISORDER_PLAYING;
c->log_callbacks->completed(c->log_v, vec[0]);
+ if(c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
}
static void logentry_failed(disorder_eclient *c,
int attribute((unused)) nvec, char **vec) {
if(!c->log_callbacks->failed)return;
+ c->statebits &= ~DISORDER_PLAYING;
c->log_callbacks->failed(c->log_v, vec[0], vec[1]);
+ if(c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
}
static void logentry_moved(disorder_eclient *c,
static void logentry_playing(disorder_eclient *c,
int attribute((unused)) nvec, char **vec) {
if(!c->log_callbacks->playing) return;
+ c->statebits |= DISORDER_PLAYING;
c->log_callbacks->playing(c->log_v, vec[0], vec[1]);
+ if(c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
}
static void logentry_queue(disorder_eclient *c,
c->log_callbacks->removed(c->log_v, vec[0], vec[1]);
}
+static void logentry_rescanned(disorder_eclient *c,
+ int attribute((unused)) nvec,
+ char attribute((unused)) **vec) {
+ if(!c->log_callbacks->rescanned) return;
+ c->log_callbacks->rescanned(c->log_v);
+}
+
static void logentry_scratched(disorder_eclient *c,
int attribute((unused)) nvec, char **vec) {
if(!c->log_callbacks->scratched) return;
+ c->statebits &= ~DISORDER_PLAYING;
c->log_callbacks->scratched(c->log_v, vec[0], vec[1]);
+ if(c->log_callbacks->state)
+ c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
}
static const struct {
{ DISORDER_PLAYING_ENABLED, "enable_play", "disable_play" },
{ DISORDER_RANDOM_ENABLED, "enable_random", "disable_random" },
{ DISORDER_TRACK_PAUSED, "pause", "resume" },
+ { DISORDER_PLAYING, "playing", "completed" },
+ { DISORDER_PLAYING, 0, "scratched" },
+ { DISORDER_PLAYING, 0, "failed" },
};
-#define NSTATES (int)(sizeof states / sizeof *states)
+#define NSTATES (int)(sizeof statestrings / sizeof *statestrings)
static void logentry_state(disorder_eclient *c,
int attribute((unused)) nvec, char **vec) {
int n;
for(n = 0; n < NSTATES; ++n)
- if(!strcmp(vec[0], statestrings[n].enable)) {
+ if(statestrings[n].enable && !strcmp(vec[0], statestrings[n].enable)) {
c->statebits |= statestrings[n].bit;
break;
- } else if(!strcmp(vec[0], statestrings[n].disable)) {
+ } else if(statestrings[n].disable && !strcmp(vec[0], statestrings[n].disable)) {
c->statebits &= ~statestrings[n].bit;
break;
}
if(!c->log_callbacks->state) return;
- c->log_callbacks->state(c->log_v, c->statebits);
+ c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
}
static void logentry_volume(disorder_eclient *c,
c->log_callbacks->volume(c->log_v, (int)l, (int)r);
}
+/** @brief Convert @p statebits to a string */
+char *disorder_eclient_interpret_state(unsigned long statebits) {
+ struct dynstr d[1];
+ size_t n;
+
+ static const struct {
+ unsigned long bit;
+ const char *name;
+ } bits[] = {
+ { DISORDER_PLAYING_ENABLED, "playing_enabled" },
+ { DISORDER_RANDOM_ENABLED, "random_enabled" },
+ { DISORDER_TRACK_PAUSED, "track_paused" },
+ { DISORDER_PLAYING, "playing" },
+ { DISORDER_CONNECTED, "connected" },
+ };
+#define NBITS (sizeof bits / sizeof *bits)
+
+ dynstr_init(d);
+ if(!statebits)
+ dynstr_append(d, '0');
+ for(n = 0; n < NBITS; ++n)
+ if(statebits & bits[n].bit) {
+ if(d->nvec)
+ dynstr_append(d, '|');
+ dynstr_append_string(d, bits[n].name);
+ statebits ^= bits[n].bit;
+ }
+ if(statebits) {
+ char s[20];
+
+ if(d->nvec)
+ dynstr_append(d, '|');
+ sprintf(s, "%#lx", statebits);
+ dynstr_append_string(d, s);
+ }
+ dynstr_terminate(d);
+ return d->vec;
+}
+
/*
Local Variables:
c-basic-offset:2