X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/763d5e6ad88ef3ba1cd1d7742d060e4f1e54c6b8..658c8a359409a339d71910f68452bfaa751734c0:/lib/eclient.c diff --git a/lib/eclient.c b/lib/eclient.c index 971dc4c..1fc0bfe 100644 --- a/lib/eclient.c +++ b/lib/eclient.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder. - * Copyright (C) 2006 Richard Kettlewell + * Copyright (C) 2006-2008 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 @@ -17,23 +17,22 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ +/** @file lib/eclient.c + * @brief Client code for event-driven programs + */ -#include -#include "types.h" +#include "common.h" #include #include #include #include -#include -#include #include #include #include -#include -#include #include #include +#include #include "log.h" #include "mem.h" @@ -55,18 +54,23 @@ /* TODO: more commands */ +/** @brief How often to send data to the server when receiving logs */ +#define LOG_PROD_INTERVAL 10 + /* Types *********************************************************************/ +/** @brief Client state */ enum client_state { - state_disconnected, /* not connected */ - state_connecting, /* waiting for connect() */ - state_connected, /* connected but not authenticated */ - state_idle, /* not doing anything */ - state_cmdresponse, /* waiting for command resonse */ - state_body, /* accumulating body */ - state_log, /* monitoring log */ + state_disconnected, /**< @brief not connected */ + state_connecting, /**< @brief waiting for connect() */ + state_connected, /**< @brief connected but not authenticated */ + state_idle, /**< @brief not doing anything */ + state_cmdresponse, /**< @brief waiting for command resonse */ + state_body, /**< @brief accumulating body */ + state_log, /**< @brief monitoring log */ }; +/** @brief Names for @ref client_state */ static const char *const states[] = { "disconnected", "connecting", @@ -79,55 +83,73 @@ static const char *const states[] = { struct operation; /* forward decl */ +/** @brief Type of an operation callback */ typedef void operation_callback(disorder_eclient *c, struct operation *op); -/* A pending operation. This can be either a command or part of the - * authentication protocol. In the former case new commands are appended to - * the list, in the latter case they are inserted at the front. */ +/** @brief A pending operation. + * + * This can be either a command or part of the authentication protocol. In the + * former case new commands are appended to the list, in the latter case they + * are inserted at the front. */ struct operation { - struct operation *next; /* next operation */ - char *cmd; /* command to send or 0 */ - operation_callback *opcallback; /* internal completion callback */ - void (*completed)(); /* user completion callback or 0 */ - void *v; /* data for COMPLETED */ - disorder_eclient *client; /* owning client */ - int sent; /* true if sent to server */ + struct operation *next; /**< @brief next operation */ + char *cmd; /**< @brief command to send or 0 */ + operation_callback *opcallback; /**< @brief internal completion callback */ + void (*completed)(); /**< @brief user completion callback or 0 */ + void *v; /**< @brief data for COMPLETED */ + disorder_eclient *client; /**< @brief owning client */ + + /** @brief true if sent to server + * + * This is cleared by disorder_eclient_close(), forcing all queued + * commands to be transparently resent. + */ + int sent; }; +/** @brief Client structure */ struct disorder_eclient { - const char *ident; - int fd; - enum client_state state; /* current state */ - int authenticated; /* true when authenicated */ - struct dynstr output; /* output buffer */ - struct dynstr input; /* input buffer */ - int eof; /* input buffer is at EOF */ - /* error reporting callbacks */ - const disorder_eclient_callbacks *callbacks; - void *u; - /* operation queuue */ - struct operation *ops, **opstail; + char *ident; + int fd; /**< @brief connection to server */ + enum client_state state; /**< @brief current state */ + int authenticated; /**< @brief true when authenicated */ + struct dynstr output; /**< @brief output buffer */ + struct dynstr input; /**< @brief input buffer */ + int eof; /**< @brief input buffer is at EOF */ + const disorder_eclient_callbacks *callbacks; /**< @brief error callbacks */ + void *u; /**< @brief user data */ + struct operation *ops; /**< @brief queue of operations */ + struct operation **opstail; /**< @brief queue tail */ /* accumulated response */ - int rc; /* response code */ - char *line; /* complete line */ - struct vector vec; /* body */ - /* log client callback */ + int rc; /**< @brief response code */ + char *line; /**< @brief complete line */ + struct vector vec; /**< @brief body */ + const disorder_eclient_log_callbacks *log_callbacks; - void *log_v; - unsigned long statebits; /* current state */ + /**< @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 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); @@ -157,16 +179,22 @@ static void logentry_removed(disorder_eclient *c, int nvec, char **vec); 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 ********************************************************************/ -static const struct logentry_handler { - const char *name; - int min, max; +/** @brief One possible log entry */ +struct logentry_handler { + const char *name; /**< @brief Entry name */ + int min; /**< @brief Minimum arguments */ + int max; /**< @brief Maximum arguments */ void (*handler)(disorder_eclient *c, int nvec, - char **vec); -} logentry_handlers[] = { + char **vec); /**< @brief Handler function */ +}; + +/** @brief Table for parsing log entries */ +static const struct logentry_handler logentry_handlers[] = { #define LE(X, MIN, MAX) { #X, MIN, MAX, logentry_##X } LE(completed, 1, 1), LE(failed, 2, 2), @@ -176,6 +204,7 @@ static const struct logentry_handler { 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) @@ -183,6 +212,10 @@ static const struct logentry_handler { /* Setup and teardown ********************************************************/ +/** @brief Create a new client + * + * Does NOT connect the client - connections are made (and re-made) on demand. + */ disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, void *u) { disorder_eclient *c = xmalloc(sizeof *c); @@ -197,6 +230,13 @@ disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb, return c; } +/** @brief Disconnect a client + * @param c Client to disconnect + * + * NB that this routine just disconnnects the TCP connection. It does not + * destroy the client! If you continue to use it then it will attempt to + * reconnect. + */ void disorder_eclient_close(disorder_eclient *c) { struct operation *op; @@ -207,6 +247,7 @@ void disorder_eclient_close(disorder_eclient *c) { xclose(c->fd); c->fd = -1; c->state = state_disconnected; + c->statebits = 0; } c->output.nvec = 0; c->input.nvec = 0; @@ -215,11 +256,23 @@ void disorder_eclient_close(disorder_eclient *c) { /* 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 current state */ +unsigned long disorder_eclient_state(const disorder_eclient *c) { + return c->statebits | (c->state > state_connected ? DISORDER_CONNECTED : 0); } /* Error reporting ***********************************************************/ -/* called when a connection error occurs */ +/** @brief called when a connection error occurs + * + * After this called we will be disconnected (by disorder_eclient_close()), + * so there will be a reconnection before any commands can be sent. + */ static int comms_error(disorder_eclient *c, const char *fmt, ...) { va_list ap; char *s; @@ -233,7 +286,7 @@ static int comms_error(disorder_eclient *c, const char *fmt, ...) { return -1; } -/* called when the server reports an error */ +/** @brief called when the server reports an error */ static int protocol_error(disorder_eclient *c, struct operation *op, int code, const char *fmt, ...) { va_list ap; @@ -249,9 +302,19 @@ static int protocol_error(disorder_eclient *c, struct operation *op, /* State machine *************************************************************/ +/** @brief Called when there's something to do + * @param c Client + * @param mode bitmap of @ref DISORDER_POLL_READ and/or @ref DISORDER_POLL_WRITE. + * + * This should be called from by your code when the file descriptor is readable + * or writable (as requested by the @c poll callback, see @ref + * disorder_eclient_callbacks) and in any case from time to time (with @p mode + * = 0) to allow for retries to work. + */ 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], mode & DISORDER_POLL_READ ? "READ" : "", @@ -264,7 +327,12 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) { 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. */ @@ -319,6 +387,14 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) { 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) { @@ -381,15 +457,14 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) { if(c->fd != -1) c->callbacks->poll(c->u, c, c->fd, mode); } -/* Called to start connecting */ -static int start_connect(void *cc, - const struct sockaddr *sa, - socklen_t len, - const char *ident) { - disorder_eclient *c = cc; +/** @brief Called to start connecting */ +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; @@ -398,6 +473,7 @@ static int start_connect(void *cc, 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: @@ -408,14 +484,14 @@ static int start_connect(void *cc, 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; return 0; } -/* Called when maybe connected */ +/** @brief Called when poll triggers while waiting for a connection */ static void maybe_connected(disorder_eclient *c) { /* We either connected, or got an error. */ int err; @@ -430,35 +506,83 @@ static void maybe_connected(disorder_eclient *c) { comms_error(c, "connecting to %s: %s", c->ident, strerror(err)); /* sets state_disconnected */ } else { + char *r; + /* The connection succeeded */ 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 */ } } /* Authentication ************************************************************/ +/** @brief Called with the greeting from the server */ static void authbanner_opcallback(disorder_eclient *c, struct operation *op) { 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; + } + 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(c->line + 4, &nonce_len); - res = authhash(nonce, nonce_len, config->password); + 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); } +/** @brief Called with the response to the @c user command */ static void authuser_opcallback(disorder_eclient *c, struct operation *op) { + char *r; + D(("authuser_opcallback")); if(c->rc / 100 != 2) { /* Wrong password or something. We cannot proceed. */ @@ -468,6 +592,8 @@ static void authuser_opcallback(disorder_eclient *c, } /* OK, we're authenticated now. */ c->authenticated = 1; + byte_xasprintf(&r, "authenticated with %s", c->ident); + c->callbacks->report(c->u, r); if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback)) /* We are a log client, switch to logging mode */ stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v, @@ -546,8 +672,10 @@ static void read_input(disorder_eclient *c) { if(c->state > state_connecting) consume(&c->input, (nl - c->input.vec) + 1); } - if(c->eof) + if(c->eof) { comms_error(c, "reading from %s: server disconnected", c->ident); + c->authenticated = 0; + } } /* called with a line that has just been read */ @@ -567,7 +695,7 @@ static void process_line(disorder_eclient *c, char *line) { 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); @@ -706,38 +834,71 @@ static void stash_command(disorder_eclient *c, /* Command support ***********************************************************/ -/* for commands with a simple string response */ +static const char *errorstring(disorder_eclient *c) { + char *s; + + byte_xasprintf(&s, "%s: %s: %d", c->ident, c->line, c->rc); + return s; +} + +/* for commands with a quoted string response */ static void string_response_opcallback(disorder_eclient *c, struct operation *op) { + disorder_eclient_string_response *completed + = (disorder_eclient_string_response *)op->completed; + D(("string_response_callback")); - if(c->rc / 100 == 2) { - if(op->completed) - ((disorder_eclient_string_response *)op->completed)(op->v, c->line + 4); + if(c->rc / 100 == 2 || c->rc == 555) { + if(op->completed) { + if(c->rc == 555) + completed(op->v, NULL, NULL); + else if(c->protocol >= 2) { + char **rr = split(c->line + 4, 0, SPLIT_QUOTES, 0, 0); + + if(rr && *rr) + completed(op->v, NULL, *rr); + else + /* TODO error message a is bit lame but generally indicates a server + * bug rather than anything the user can address */ + completed(op->v, "error parsing response", NULL); + } else + completed(op->v, NULL, c->line + 4); + } } else - protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + completed(op->v, errorstring(c), NULL); } /* for commands with a simple integer response */ static void integer_response_opcallback(disorder_eclient *c, struct operation *op) { + disorder_eclient_integer_response *completed + = (disorder_eclient_integer_response *)op->completed; + D(("string_response_callback")); if(c->rc / 100 == 2) { - if(op->completed) - ((disorder_eclient_integer_response *)op->completed) - (op->v, strtol(c->line + 4, 0, 10)); + long n; + int e; + + e = xstrtol(&n, c->line + 4, 0, 10); + if(e) + completed(op->v, strerror(e), 0); + else + completed(op->v, 0, n); } else - protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + completed(op->v, errorstring(c), 0); } /* for commands with no response */ static void no_response_opcallback(disorder_eclient *c, struct operation *op) { + disorder_eclient_no_response *completed + = (disorder_eclient_no_response *)op->completed; + D(("no_response_callback")); - if(c->rc / 100 == 2) { - if(op->completed) - ((disorder_eclient_no_response *)op->completed)(op->v); - } else - protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); + if(c->rc / 100 == 2) + completed(op->v, NULL); + else + completed(op->v, errorstring(c)); } /* error callback for queue_unmarshall */ @@ -745,6 +906,7 @@ static void eclient_queue_error(const char *msg, void *u) { struct operation *op = u; + /* TODO don't use protocol_error here */ protocol_error(op->client, op, -1, "error parsing queue entry: %s", msg); } @@ -770,6 +932,7 @@ static void queue_response_opcallback(disorder_eclient *c, if(op->completed) ((disorder_eclient_queue_response *)op->completed)(op->v, qh); } else + /* TODO don't use protocol_error here */ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); } @@ -790,12 +953,14 @@ static void playing_response_opcallback(disorder_eclient *c, q = 0; break; default: + /* TODO don't use protocol_error here */ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); return; } if(op->completed) ((disorder_eclient_queue_response *)op->completed)(op->v, q); } else + /* TODO don't use protocol_error here */ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); } @@ -809,6 +974,7 @@ static void list_response_opcallback(disorder_eclient *c, c->vec.nvec, c->vec.vec); } else + /* TODO don't use protocol_error here */ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); } @@ -821,12 +987,14 @@ static void volume_response_opcallback(disorder_eclient *c, if(c->rc / 100 == 2) { if(op->completed) { if(sscanf(c->line + 4, "%d %d", &l, &r) != 2 || l < 0 || r < 0) + /* TODO don't use protocol_error here */ protocol_error(c, op, -1, "%s: invalid volume response: %s", c->ident, c->line); else ((disorder_eclient_volume_response *)op->completed)(op->v, l, r); } } else + /* TODO don't use protocol_error here */ protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line); } @@ -1067,16 +1235,168 @@ int disorder_eclient_search(disorder_eclient *c, "search", terms, (char *)0); } +int disorder_eclient_nop(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "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 + /* TODO don't use protocol_error here */ + 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); +} + +/** @brief Get the list of users + * @param c Client + * @param completed Called with list of users + * @param v Passed to @p completed + * + * The user list is not sorted in any particular order. + */ +int disorder_eclient_users(disorder_eclient *c, + disorder_eclient_list_response *completed, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "users", (char *)0); +} + +/** @brief Delete a user + * @param c Client + * @param completed Called on completion + * @param user User to delete + * @param v Passed to @p completed + */ +int disorder_eclient_deluser(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *user, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "deluser", user, (char *)0); +} + +/** @brief Get a user property + * @param c Client + * @param completed Called on completion + * @param user User to look up + * @param property Property to look up + * @param v Passed to @p completed + */ +int disorder_eclient_userinfo(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *user, + const char *property, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "userinfo", user, property, (char *)0); +} + +/** @brief Modify a user property + * @param c Client + * @param completed Called on completion + * @param user User to modify + * @param property Property to modify + * @param value New property value + * @param v Passed to @p completed + */ +int disorder_eclient_edituser(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *user, + const char *property, + const char *value, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "edituser", user, property, value, (char *)0); +} + +/** @brief Create a new user + * @param c Client + * @param completed Called on completion + * @param user User to create + * @param password Initial password + * @param rights Initial rights or NULL + * @param v Passed to @p completed + */ +int disorder_eclient_adduser(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *user, + const char *password, + const char *rights, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "adduser", user, password, rights, (char *)0); +} + /* Log clients ***************************************************************/ +/** @brief Monitor the server log + * @param c Client + * @param callbacks Functions to call when anything happens + * @param v Passed to @p callbacks functions + * + * 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, void *v) { 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); + disorder_eclient_polled(c, 0); return 0; } @@ -1091,6 +1411,7 @@ static void log_opcallback(disorder_eclient *c, /* error callback for log line parsing */ static void logline_error(const char *msg, void *u) { disorder_eclient *c = u; + /* TODO don't use protocol_error here */ protocol_error(c, c->ops, -1, "error parsing log line: %s", msg); } @@ -1100,17 +1421,18 @@ static void logline(disorder_eclient *c, const char *line) { 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 */ if(sscanf(vec[0], "%"SCNxMAX, &when) != 1) { /* probably the wrong side of a format change */ + /* TODO don't use protocol_error here */ protocol_error(c, c->ops, -1, "invalid log timestamp '%s'", vec[0]); return; } /* TODO: do something with the time */ - n = TABLE_FIND(logentry_handlers, struct logentry_handler, name, vec[1]); + n = TABLE_FIND(logentry_handlers, name, vec[1]); if(n < 0) return; /* probably a future command */ vec += 2; nvec -= 2; @@ -1121,33 +1443,42 @@ static void logline(disorder_eclient *c, const char *line) { static void logentry_completed(disorder_eclient *c, int attribute((unused)) nvec, char **vec) { - if(!c->log_callbacks->completed) return; - c->log_callbacks->completed(c->log_v, vec[0]); + c->statebits &= ~DISORDER_PLAYING; + if(c->log_callbacks->completed) + 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->log_callbacks->failed(c->log_v, vec[0], vec[1]); + c->statebits &= ~DISORDER_PLAYING; + if(c->log_callbacks->failed) + 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, int attribute((unused)) nvec, char **vec) { - if(!c->log_callbacks->moved) return; - c->log_callbacks->moved(c->log_v, vec[0]); + if(c->log_callbacks->moved) + c->log_callbacks->moved(c->log_v, vec[0]); } static void logentry_playing(disorder_eclient *c, int attribute((unused)) nvec, char **vec) { - if(!c->log_callbacks->playing) return; - c->log_callbacks->playing(c->log_v, vec[0], vec[1]); + c->statebits |= DISORDER_PLAYING; + if(c->log_callbacks->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, int attribute((unused)) nvec, char **vec) { struct queue_entry *q; - if(!c->log_callbacks->completed) return; + if(!c->log_callbacks->queue) return; q = xmalloc(sizeof *q); if(queue_unmarshall_vec(q, nvec, vec, eclient_queue_error, c)) return; /* bogus */ @@ -1167,20 +1498,30 @@ static void logentry_recent_added(disorder_eclient *c, static void logentry_recent_removed(disorder_eclient *c, int attribute((unused)) nvec, char **vec) { - if(!c->log_callbacks->recent_removed) return; - c->log_callbacks->recent_removed(c->log_v, vec[0]); + if(c->log_callbacks->recent_removed) + c->log_callbacks->recent_removed(c->log_v, vec[0]); } static void logentry_removed(disorder_eclient *c, int attribute((unused)) nvec, char **vec) { - if(!c->log_callbacks->removed) return; - c->log_callbacks->removed(c->log_v, vec[0], vec[1]); + if(c->log_callbacks->removed) + 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) + 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->log_callbacks->scratched(c->log_v, vec[0], vec[1]); + c->statebits &= ~DISORDER_PLAYING; + if(c->log_callbacks->scratched) + 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 { @@ -1191,23 +1532,26 @@ 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); + if(c->log_callbacks->state) + c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED); } static void logentry_volume(disorder_eclient *c, @@ -1223,6 +1567,45 @@ 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