// Copied from desktop-tools, should go to liberty if it proves useful // --- MPD client interface ---------------------------------------------------- // This is a rather thin MPD client interface intended for basic tasks #define MPD_SUBSYSTEM_TABLE(XX) \ XX (DATABASE, 0, "database") \ XX (UPDATE, 1, "update") \ XX (STORED_PLAYLIST, 2, "stored_playlist") \ XX (PLAYLIST, 3, "playlist") \ XX (PLAYER, 4, "player") \ XX (MIXER, 5, "mixer") \ XX (OUTPUT, 6, "output") \ XX (OPTIONS, 7, "options") \ XX (STICKER, 8, "sticker") \ XX (SUBSCRIPTION, 9, "subscription") \ XX (MESSAGE, 10, "message") enum mpd_subsystem { #define XX(a, b, c) MPD_SUBSYSTEM_ ## a = (1 << b), MPD_SUBSYSTEM_TABLE (XX) #undef XX MPD_SUBSYSTEM_MAX }; static const char *mpd_subsystem_names[] = { #define XX(a, b, c) [b] = c, MPD_SUBSYSTEM_TABLE (XX) #undef XX }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum mpd_client_state { MPD_DISCONNECTED, ///< Not connected MPD_CONNECTING, ///< Currently connecting MPD_CONNECTED ///< Connected }; struct mpd_response { bool success; ///< OK or ACK // ACK-only fields: int error; ///< Numeric error value (ack.h) int list_offset; ///< Offset of command in list char *current_command; ///< Name of the erroring command char *message_text; ///< Error message }; /// Task completion callback typedef void (*mpd_client_task_cb) (const struct mpd_response *response, const struct str_vector *data, void *user_data); struct mpd_client_task { LIST_HEADER (struct mpd_client_task) mpd_client_task_cb callback; ///< Callback on completion void *user_data; ///< User data }; struct mpd_client { struct poller *poller; ///< Poller // Connection: enum mpd_client_state state; ///< Connection state struct connector *connector; ///< Connection establisher int socket; ///< MPD socket struct str read_buffer; ///< Input yet to be processed struct str write_buffer; ///< Outut yet to be be sent out struct poller_fd socket_event; ///< We can read from the socket struct poller_timer timeout_timer; ///< Connection seems to be dead // Protocol: bool got_hello; ///< Got the OK MPD hello message bool idling; ///< Sent idle as the last command unsigned idling_subsystems; ///< Subsystems we're idling for bool in_list; ///< We're inside a command list struct mpd_client_task *tasks; ///< Task queue struct mpd_client_task *tasks_tail; ///< Tail of task queue struct str_vector data; ///< Data from last command // User configuration: void *user_data; ///< User data for callbacks /// Callback after connection has been successfully established void (*on_connected) (void *user_data); /// Callback for general failures or even normal disconnection; /// the interface is reinitialized void (*on_failure) (void *user_data); /// Callback to receive "idle" updates. /// Remember to restart the idle if needed. void (*on_event) (unsigned subsystems, void *user_data); }; static void mpd_client_reset (struct mpd_client *self); static void mpd_client_destroy_connector (struct mpd_client *self); static void mpd_client_init (struct mpd_client *self, struct poller *poller) { memset (self, 0, sizeof *self); self->poller = poller; self->socket = -1; str_init (&self->read_buffer); str_init (&self->write_buffer); str_vector_init (&self->data); poller_fd_init (&self->socket_event, poller, -1); poller_timer_init (&self->timeout_timer, poller); } static void mpd_client_free (struct mpd_client *self) { // So that we don't have to repeat most of the stuff mpd_client_reset (self); str_free (&self->read_buffer); str_free (&self->write_buffer); str_vector_free (&self->data); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Reinitialize the interface so that you can reconnect anew static void mpd_client_reset (struct mpd_client *self) { if (self->state == MPD_CONNECTING) mpd_client_destroy_connector (self); if (self->socket != -1) xclose (self->socket); self->socket = -1; self->socket_event.closed = true; poller_fd_reset (&self->socket_event); poller_timer_reset (&self->timeout_timer); str_reset (&self->read_buffer); str_reset (&self->write_buffer); str_vector_reset (&self->data); self->got_hello = false; self->idling = false; self->idling_subsystems = 0; self->in_list = false; LIST_FOR_EACH (struct mpd_client_task, iter, self->tasks) free (iter); self->tasks = self->tasks_tail = NULL; self->state = MPD_DISCONNECTED; } static void mpd_client_fail (struct mpd_client *self) { mpd_client_reset (self); if (self->on_failure) self->on_failure (self->user_data); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool mpd_client_parse_response (const char *p, struct mpd_response *response) { if (!strcmp (p, "OK")) return response->success = true; if (!strcmp (p, "list_OK")) // TODO: either implement this or fail the connection properly hard_assert (!"command_list_ok_begin not implemented"); char *end = NULL; if (*p++ != 'A' || *p++ != 'C' || *p++ != 'K' || *p++ != ' ' || *p++ != '[') return false; errno = 0; response->error = strtoul (p, &end, 10); if (errno != 0 || end == p) return false; p = end; if (*p++ != '@') return false; errno = 0; response->list_offset = strtoul (p, &end, 10); if (errno != 0 || end == p) return false; p = end; if (*p++ != ']' || *p++ != ' ' || *p++ != '{' || !(end = strchr (p, '}'))) return false; response->current_command = xstrndup (p, end - p); p = end + 1; if (*p++ != ' ') return false; response->message_text = xstrdup (p); response->success = false; return true; } static void mpd_client_dispatch (struct mpd_client *self, struct mpd_response *response) { struct mpd_client_task *task; if (!(task = self->tasks)) return; if (task->callback) task->callback (response, &self->data, task->user_data); str_vector_reset (&self->data); LIST_UNLINK_WITH_TAIL (self->tasks, self->tasks_tail, task); free (task); } static bool mpd_client_parse_hello (struct mpd_client *self, const char *line) { const char hello[] = "OK MPD "; if (strncmp (line, hello, sizeof hello - 1)) { print_debug ("invalid MPD hello message"); return false; } // TODO: call "on_connected" now. We should however also set up a timer // so that we don't wait on this message forever. return self->got_hello = true; } static bool mpd_client_parse_line (struct mpd_client *self, const char *line) { print_debug ("MPD >> %s", line); if (!self->got_hello) return mpd_client_parse_hello (self, line); struct mpd_response response; memset (&response, 0, sizeof response); if (mpd_client_parse_response (line, &response)) { mpd_client_dispatch (self, &response); free (response.current_command); free (response.message_text); } else str_vector_add (&self->data, line); return true; } /// All output from MPD commands seems to be in a trivial "key: value" format static char * mpd_client_parse_kv (char *line, char **value) { char *sep; if (!(sep = strstr (line, ": "))) return NULL; *sep = 0; *value = sep + 2; return line; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void mpd_client_update_poller (struct mpd_client *self) { poller_fd_set (&self->socket_event, self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN); } static bool mpd_client_process_input (struct mpd_client *self) { // Split socket input at newlines and process them separately struct str *rb = &self->read_buffer; char *start = rb->str, *end = start + rb->len; for (char *p = start; p < end; p++) { if (*p != '\n') continue; *p = 0; if (!mpd_client_parse_line (self, start)) return false; start = p + 1; } str_remove_slice (rb, 0, start - rb->str); return true; } static void mpd_client_on_ready (const struct pollfd *pfd, void *user_data) { (void) pfd; struct mpd_client *self = user_data; if (socket_io_try_read (self->socket, &self->read_buffer) != SOCKET_IO_OK || !mpd_client_process_input (self) || socket_io_try_write (self->socket, &self->write_buffer) != SOCKET_IO_OK) mpd_client_fail (self); else mpd_client_update_poller (self); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool mpd_client_must_quote_char (char c) { return (unsigned char) c <= ' ' || c == '"' || c == '\''; } static bool mpd_client_must_quote (const char *s) { if (!*s) return true; for (; *s; s++) if (mpd_client_must_quote_char (*s)) return true; return false; } static void mpd_client_quote (const char *s, struct str *output) { str_append_c (output, '"'); for (; *s; s++) { if (mpd_client_must_quote_char (*s)) str_append_c (output, '\\'); str_append_c (output, *s); } str_append_c (output, '"'); } /// Beware that delivery of the event isn't deferred and you musn't make /// changes to the interface while processing the event! static void mpd_client_add_task (struct mpd_client *self, mpd_client_task_cb cb, void *user_data) { // This only has meaning with command_list_ok_begin, and then it requires // special handling (all in-list tasks need to be specially marked and // later flushed if an early ACK or OK arrives). hard_assert (!self->in_list); struct mpd_client_task *task = xcalloc (1, sizeof *self); task->callback = cb; task->user_data = user_data; LIST_APPEND_WITH_TAIL (self->tasks, self->tasks_tail, task); } /// Send a command. Remember to call mpd_client_add_task() to handle responses, /// unless the command is being sent in a list. static void mpd_client_send_command (struct mpd_client *self, const char *command, ...) ATTRIBUTE_SENTINEL; static void mpd_client_send_commandv (struct mpd_client *self, char **commands) { // Automatically interrupt idle mode if (self->idling) { poller_timer_reset (&self->timeout_timer); self->idling = false; self->idling_subsystems = 0; mpd_client_send_command (self, "noidle", NULL); } struct str line; str_init (&line); for (; *commands; commands++) { if (line.len) str_append_c (&line, ' '); if (mpd_client_must_quote (*commands)) mpd_client_quote (*commands, &line); else str_append (&line, *commands); } print_debug ("MPD << %s", line.str); str_append_c (&line, '\n'); str_append_str (&self->write_buffer, &line); str_free (&line); mpd_client_update_poller (self); } static void mpd_client_send_command (struct mpd_client *self, const char *command, ...) { struct str_vector v; str_vector_init (&v); va_list ap; va_start (ap, command); for (; command; command = va_arg (ap, const char *)) str_vector_add (&v, command); va_end (ap); mpd_client_send_commandv (self, v.vector); str_vector_free (&v); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void mpd_client_list_begin (struct mpd_client *self) { hard_assert (!self->in_list); mpd_client_send_command (self, "command_list_begin", NULL); self->in_list = true; } /// End a list of commands. Remember to call mpd_client_add_task() /// to handle the summary response. static void mpd_client_list_end (struct mpd_client *self) { hard_assert (self->in_list); mpd_client_send_command (self, "command_list_end", NULL); self->in_list = false; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool mpd_resolve_subsystem (const char *name, unsigned *output) { for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++) if (!strcasecmp_ascii (name, mpd_subsystem_names[i])) { *output |= 1 << i; return true; } return false; } static void mpd_client_on_idle_return (const struct mpd_response *response, const struct str_vector *data, void *user_data) { (void) response; struct mpd_client *self = user_data; unsigned subsystems = 0; for (size_t i = 0; i < data->len; i++) { char *value, *key; if (!(key = mpd_client_parse_kv (data->vector[i], &value))) print_debug ("%s: %s", "erroneous MPD output", data->vector[i]); else if (strcasecmp_ascii (key, "changed")) print_debug ("%s: %s", "unexpected idle key", key); else if (!mpd_resolve_subsystem (value, &subsystems)) print_debug ("%s: %s", "unknown subsystem", value); } // Not resetting "idling" here, we may send an extra "noidle" no problem if (self->on_event && subsystems) self->on_event (subsystems, self->user_data); } static void mpd_client_idle (struct mpd_client *self, unsigned subsystems); static void mpd_client_on_timeout (void *user_data) { struct mpd_client *self = user_data; unsigned subsystems = self->idling_subsystems; // Just sending this out should bring a dead connection down over TCP // TODO: set another timer to make sure the ping reply arrives mpd_client_send_command (self, "ping", NULL); mpd_client_add_task (self, NULL, NULL); // Restore the incriminating idle immediately mpd_client_idle (self, subsystems); } /// When not expecting to send any further commands, you should call this /// in order to keep the connection alive. Or to receive updates. static void mpd_client_idle (struct mpd_client *self, unsigned subsystems) { hard_assert (!self->in_list); struct str_vector v; str_vector_init (&v); str_vector_add (&v, "idle"); for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++) if (subsystems & (1 << i)) str_vector_add (&v, mpd_subsystem_names[i]); mpd_client_send_commandv (self, v.vector); str_vector_free (&v); self->timeout_timer.dispatcher = mpd_client_on_timeout; self->timeout_timer.user_data = self; poller_timer_set (&self->timeout_timer, 5 * 60 * 1000); mpd_client_add_task (self, mpd_client_on_idle_return, self); self->idling = true; self->idling_subsystems = subsystems; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void mpd_client_finish_connection (struct mpd_client *self, int socket) { set_blocking (socket, false); self->socket = socket; self->state = MPD_CONNECTED; poller_fd_init (&self->socket_event, self->poller, self->socket); self->socket_event.dispatcher = mpd_client_on_ready; self->socket_event.user_data = self; mpd_client_update_poller (self); if (self->on_connected) self->on_connected (self->user_data); } static void mpd_client_destroy_connector (struct mpd_client *self) { if (self->connector) connector_free (self->connector); free (self->connector); self->connector = NULL; // Not connecting anymore self->state = MPD_DISCONNECTED; } static void mpd_client_on_connector_failure (void *user_data) { struct mpd_client *self = user_data; mpd_client_destroy_connector (self); mpd_client_fail (self); } static void mpd_client_on_connector_connected (void *user_data, int socket, const char *host) { (void) host; struct mpd_client *self = user_data; mpd_client_destroy_connector (self); mpd_client_finish_connection (self, socket); } static bool mpd_client_connect_unix (struct mpd_client *self, const char *address, struct error **e) { int fd = socket (AF_UNIX, SOCK_STREAM, 0); if (fd == -1) { error_set (e, "%s: %s", "socket", strerror (errno)); return false; } // Expand tilde if needed char *expanded = resolve_filename (address, xstrdup); struct sockaddr_un sun; sun.sun_family = AF_UNIX; strncpy (sun.sun_path, expanded, sizeof sun.sun_path); sun.sun_path[sizeof sun.sun_path - 1] = 0; free (expanded); if (connect (fd, (struct sockaddr *) &sun, sizeof sun)) { error_set (e, "%s: %s", "connect", strerror (errno)); return false; } mpd_client_finish_connection (self, fd); return true; } static bool mpd_client_connect (struct mpd_client *self, const char *address, const char *service, struct error **e) { hard_assert (self->state == MPD_DISCONNECTED); // If it looks like a path, assume it's a UNIX socket if (strchr (address, '/')) return mpd_client_connect_unix (self, address, e); struct connector *connector = xmalloc (sizeof *connector); connector_init (connector, self->poller); self->connector = connector; connector->user_data = self; connector->on_connected = mpd_client_on_connector_connected; connector->on_failure = mpd_client_on_connector_failure; connector_add_target (connector, address, service); self->state = MPD_CONNECTING; return true; }