diff options
author | Max Kellermann <max@duempel.org> | 2011-01-29 09:26:22 +0100 |
---|---|---|
committer | Max Kellermann <max@duempel.org> | 2011-01-29 10:43:54 +0100 |
commit | f8b09c194fe20192c4ac45697e9d0f00e8a96c2c (patch) | |
tree | d45c30ce117a7e01431078305ffe2e96d016dc5c | |
parent | 0e69ad32c16eb6449a8952f894c6f239f2e2c52f (diff) | |
download | mpd-f8b09c194fe20192c4ac45697e9d0f00e8a96c2c.tar.gz mpd-f8b09c194fe20192c4ac45697e9d0f00e8a96c2c.tar.xz mpd-f8b09c194fe20192c4ac45697e9d0f00e8a96c2c.zip |
protocol: support client-to-client communication
-rw-r--r-- | Makefile.am | 4 | ||||
-rw-r--r-- | NEWS | 2 | ||||
-rw-r--r-- | doc/protocol.xml | 113 | ||||
-rw-r--r-- | src/client_internal.h | 28 | ||||
-rw-r--r-- | src/client_message.c | 96 | ||||
-rw-r--r-- | src/client_message.h | 72 | ||||
-rw-r--r-- | src/client_new.c | 4 | ||||
-rw-r--r-- | src/client_subscribe.c | 123 | ||||
-rw-r--r-- | src/client_subscribe.h | 59 | ||||
-rw-r--r-- | src/command.c | 172 | ||||
-rw-r--r-- | src/idle.c | 2 | ||||
-rw-r--r-- | src/idle.h | 6 |
12 files changed, 681 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am index 92bd0173d..d4fec31ff 100644 --- a/Makefile.am +++ b/Makefile.am @@ -285,6 +285,10 @@ src_mpd_SOURCES = \ src/client_process.c \ src/client_read.c \ src/client_write.c \ + src/client_message.h \ + src/client_message.c \ + src/client_subscribe.h \ + src/client_subscribe.c \ src/server_socket.c \ src/listen.c \ src/log.c \ @@ -1,4 +1,6 @@ ver 0.17 (2011/??/??) +* protocol: + - support client-to-client communication * input: - cdio_paranoia: new input plugin to play audio CDs - curl: enable CURLOPT_NETRC diff --git a/doc/protocol.xml b/doc/protocol.xml index a02a94e32..cf4de27c1 100644 --- a/doc/protocol.xml +++ b/doc/protocol.xml @@ -204,6 +204,19 @@ has been modified. </para> </listitem> + <listitem> + <para> + <returnvalue>subscription</returnvalue>: a client + has subscribed or unsubscribed to a channel + </para> + </listitem> + <listitem> + <para> + <returnvalue>message</returnvalue>: a message was + received on a channel this client is subscribed to; + this event is only emitted when the queue is empty + </para> + </listitem> </itemizedlist> <para> While a client is waiting for <command>idle</command> @@ -1676,5 +1689,105 @@ suffix: mpc</programlisting> </varlistentry> </variablelist> </section> + + <section> + <title>Client to client</title> + + <para> + Clients can communicate with each others over "channels". A + channel is created by a client subscribing to it. More than + one client can be subscribed to a channel at a time; all of + them will receive the messages which get sent to it. + </para> + + <para> + Each time a client subscribes or unsubscribes, the global idle + event <varname>subscription</varname> is generated. In + conjunction with the <command>channels</command> command, this + may be used to auto-detect clients providing additional + services. + </para> + + <para> + A new messages is indicated by the <varname>message</varname> + idle event. + </para> + + <variablelist> + <varlistentry id="command_subscribe"> + <term> + <cmdsynopsis> + <command>subscribe</command> + <arg choice="req"><replaceable>NAME</replaceable></arg> + </cmdsynopsis> + </term> + <listitem> + <para> + Subscribe to a channel. The channel is created if it + does not exist already. The name may consist of + alphanumeric ASCII characters plus underscore, dash, dot + and colon. + </para> + </listitem> + </varlistentry> + + <varlistentry id="command_unsubscribe"> + <term> + <cmdsynopsis> + <command>unsubscribe</command> + <arg choice="req"><replaceable>NAME</replaceable></arg> + </cmdsynopsis> + </term> + <listitem> + <para> + Unsubscribe from a channel. + </para> + </listitem> + </varlistentry> + + <varlistentry id="command_channels"> + <term> + <cmdsynopsis> + <command>channels</command> + </cmdsynopsis> + </term> + <listitem> + <para> + Obtain a list of all channels. The response is a list + of "channel:" lines. + </para> + </listitem> + </varlistentry> + + <varlistentry id="command_readmessages"> + <term> + <cmdsynopsis> + <command>readmessages</command> + </cmdsynopsis> + </term> + <listitem> + <para> + Reads messages for this client. The response is a list + of "channel:" and "message:" lines. + </para> + </listitem> + </varlistentry> + + <varlistentry id="command_sendmessage"> + <term> + <cmdsynopsis> + <command>sendmessage</command> + <arg choice="req"><replaceable>CHANNEL</replaceable></arg> + <arg choice="req"><replaceable>TEXT</replaceable></arg> + </cmdsynopsis> + </term> + <listitem> + <para> + Send a message to the specified channel. + </para> + </listitem> + </varlistentry> + </variablelist> + </section> </chapter> </book> diff --git a/src/client_internal.h b/src/client_internal.h index d675ed7c6..ba97e4b8f 100644 --- a/src/client_internal.h +++ b/src/client_internal.h @@ -21,11 +21,17 @@ #define MPD_CLIENT_INTERNAL_H #include "client.h" +#include "client_message.h" #include "command.h" #undef G_LOG_DOMAIN #define G_LOG_DOMAIN "client" +enum { + CLIENT_MAX_SUBSCRIPTIONS = 16, + CLIENT_MAX_MESSAGES = 64, +}; + struct deferred_buffer { size_t size; char data[sizeof(long)]; @@ -69,6 +75,28 @@ struct client { /** idle flags that the client wants to receive */ unsigned idle_subscriptions; + + /** + * A list of channel names this client is subscribed to. + */ + GSList *subscriptions; + + /** + * The number of subscriptions in #subscriptions. Used to + * limit the number of subscriptions. + */ + unsigned num_subscriptions; + + /** + * A list of messages this client has received in reverse + * order (latest first). + */ + GSList *messages; + + /** + * The number of messages in #messages. + */ + unsigned num_messages; }; extern unsigned int client_max_connections; diff --git a/src/client_message.c b/src/client_message.c new file mode 100644 index 000000000..b681b4e7f --- /dev/null +++ b/src/client_message.c @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2003-2011 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "client_message.h" + +#include <assert.h> +#include <glib.h> + +G_GNUC_PURE +static bool +valid_channel_char(const char ch) +{ + return g_ascii_isalnum(ch) || + ch == '_' || ch == '-' || ch == '.' || ch == ':'; +} + +bool +client_message_valid_channel_name(const char *name) +{ + do { + if (!valid_channel_char(*name)) + return false; + } while (*++name != 0); + + return true; +} + +void +client_message_init_null(struct client_message *msg) +{ + assert(msg != NULL); + + msg->channel = NULL; + msg->message = NULL; +} + +void +client_message_init(struct client_message *msg, + const char *channel, const char *message) +{ + assert(msg != NULL); + + msg->channel = g_strdup(channel); + msg->message = g_strdup(message); +} + +void +client_message_copy(struct client_message *dest, + const struct client_message *src) +{ + assert(dest != NULL); + assert(src != NULL); + assert(client_message_defined(src)); + + client_message_init(dest, src->channel, src->message); +} + +struct client_message * +client_message_dup(const struct client_message *src) +{ + struct client_message *dest = g_slice_new(struct client_message); + client_message_copy(dest, src); + return dest; +} + +void +client_message_deinit(struct client_message *msg) +{ + assert(msg != NULL); + + g_free(msg->channel); + g_free(msg->message); +} + +void +client_message_free(struct client_message *msg) +{ + client_message_deinit(msg); + g_slice_free(struct client_message, msg); +} diff --git a/src/client_message.h b/src/client_message.h new file mode 100644 index 000000000..5c7e86c15 --- /dev/null +++ b/src/client_message.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2003-2011 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_CLIENT_MESSAGE_H +#define MPD_CLIENT_MESSAGE_H + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> +#include <glib.h> + +/** + * A client-to-client message. + */ +struct client_message { + char *channel; + + char *message; +}; + +G_GNUC_PURE +bool +client_message_valid_channel_name(const char *name); + +G_GNUC_PURE +static inline bool +client_message_defined(const struct client_message *msg) +{ + assert(msg != NULL); + assert((msg->channel == NULL) == (msg->message == NULL)); + + return msg->channel != NULL; +} + +void +client_message_init_null(struct client_message *msg); + +void +client_message_init(struct client_message *msg, + const char *channel, const char *message); + +void +client_message_copy(struct client_message *dest, + const struct client_message *src); + +G_GNUC_MALLOC G_GNUC_PURE +struct client_message * +client_message_dup(const struct client_message *src); + +void +client_message_deinit(struct client_message *msg); + +void +client_message_free(struct client_message *msg); + +#endif diff --git a/src/client_new.c b/src/client_new.c index ffe7c7ce6..5b2dfde65 100644 --- a/src/client_new.c +++ b/src/client_new.c @@ -121,6 +121,10 @@ client_new(struct player_control *player_control, client->send_buf_used = 0; + client->subscriptions = NULL; + client->messages = NULL; + client->num_messages = 0; + (void)send(fd, GREETING, sizeof(GREETING) - 1, 0); client_list_add(client); diff --git a/src/client_subscribe.c b/src/client_subscribe.c new file mode 100644 index 000000000..c65a7ed31 --- /dev/null +++ b/src/client_subscribe.c @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2003-2011 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "client_subscribe.h" +#include "client_internal.h" +#include "client_idle.h" +#include "idle.h" + +#include <string.h> + +G_GNUC_PURE +static GSList * +client_find_subscription(const struct client *client, const char *channel) +{ + for (GSList *i = client->subscriptions; i != NULL; i = g_slist_next(i)) + if (strcmp((const char *)i->data, channel) == 0) + return i; + + return NULL; +} + +enum client_subscribe_result +client_subscribe(struct client *client, const char *channel) +{ + assert(client != NULL); + assert(channel != NULL); + + if (!client_message_valid_channel_name(channel)) + return CLIENT_SUBSCRIBE_INVALID; + + if (client_find_subscription(client, channel) != NULL) + return CLIENT_SUBSCRIBE_ALREADY; + + if (client->num_subscriptions >= CLIENT_MAX_SUBSCRIPTIONS) + return CLIENT_SUBSCRIBE_FULL; + + client->subscriptions = g_slist_prepend(client->subscriptions, + g_strdup(channel)); + ++client->num_subscriptions; + + idle_add(IDLE_SUBSCRIPTION); + + return CLIENT_SUBSCRIBE_OK; +} + +bool +client_unsubscribe(struct client *client, const char *channel) +{ + GSList *i = client_find_subscription(client, channel); + if (i == NULL) + return false; + + assert(client->num_subscriptions > 0); + + client->subscriptions = g_slist_remove(client->subscriptions, i->data); + --client->num_subscriptions; + + idle_add(IDLE_SUBSCRIPTION); + + assert((client->num_subscriptions == 0) == + (client->subscriptions == NULL)); + + return true; +} + +void +client_unsubscribe_all(struct client *client) +{ + for (GSList *i = client->subscriptions; i != NULL; i = g_slist_next(i)) + g_free(i->data); + + g_slist_free(client->subscriptions); + client->subscriptions = NULL; + client->num_subscriptions = 0; +} + +bool +client_push_message(struct client *client, const struct client_message *msg) +{ + assert(client != NULL); + assert(msg != NULL); + assert(client_message_defined(msg)); + + if (client->num_messages >= CLIENT_MAX_MESSAGES || + client_find_subscription(client, msg->channel) == NULL) + return false; + + if (client->messages == NULL) + client_idle_add(client, IDLE_MESSAGE); + + client->messages = g_slist_prepend(client->messages, + client_message_dup(msg)); + ++client->num_messages; + + return true; +} + +GSList * +client_read_messages(struct client *client) +{ + GSList *messages = g_slist_reverse(client->messages); + + client->messages = NULL; + client->num_messages = 0; + + return messages; +} diff --git a/src/client_subscribe.h b/src/client_subscribe.h new file mode 100644 index 000000000..09f864417 --- /dev/null +++ b/src/client_subscribe.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2003-2011 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_CLIENT_SUBSCRIBE_H +#define MPD_CLIENT_SUBSCRIBE_H + +#include <stdbool.h> +#include <glib.h> + +struct client; +struct client_message; + +enum client_subscribe_result { + /** success */ + CLIENT_SUBSCRIBE_OK, + + /** invalid channel name */ + CLIENT_SUBSCRIBE_INVALID, + + /** already subscribed to this channel */ + CLIENT_SUBSCRIBE_ALREADY, + + /** too many subscriptions */ + CLIENT_SUBSCRIBE_FULL, +}; + +enum client_subscribe_result +client_subscribe(struct client *client, const char *channel); + +bool +client_unsubscribe(struct client *client, const char *channel); + +void +client_unsubscribe_all(struct client *client); + +bool +client_push_message(struct client *client, const struct client_message *msg); + +G_GNUC_MALLOC +GSList * +client_read_messages(struct client *client); + +#endif diff --git a/src/command.c b/src/command.c index 354bf85f5..aeed55a1b 100644 --- a/src/command.c +++ b/src/command.c @@ -46,6 +46,7 @@ #include "client.h" #include "client_idle.h" #include "client_internal.h" +#include "client_subscribe.h" #include "tag_print.h" #include "path.h" #include "replay_gain_config.h" @@ -1837,6 +1838,172 @@ handle_sticker(struct client *client, int argc, char *argv[]) } #endif +static enum command_return +handle_subscribe(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) +{ + assert(argc == 2); + + switch (client_subscribe(client, argv[1])) { + case CLIENT_SUBSCRIBE_OK: + return COMMAND_RETURN_OK; + + case CLIENT_SUBSCRIBE_INVALID: + command_error(client, ACK_ERROR_ARG, + "invalid channel name"); + return COMMAND_RETURN_ERROR; + + case CLIENT_SUBSCRIBE_ALREADY: + command_error(client, ACK_ERROR_EXIST, + "already subscribed to this channel"); + return COMMAND_RETURN_ERROR; + + case CLIENT_SUBSCRIBE_FULL: + command_error(client, ACK_ERROR_EXIST, + "subscription list is full"); + return COMMAND_RETURN_ERROR; + } + + /* unreachable */ + return COMMAND_RETURN_OK; +} + +static enum command_return +handle_unsubscribe(struct client *client, G_GNUC_UNUSED int argc, char *argv[]) +{ + assert(argc == 2); + + if (client_unsubscribe(client, argv[1])) + return COMMAND_RETURN_OK; + else { + command_error(client, ACK_ERROR_NO_EXIST, + "not subscribed to this channel"); + return COMMAND_RETURN_ERROR; + } +} + +struct channels_context { + GStringChunk *chunk; + + GHashTable *channels; +}; + +static void +collect_channels(gpointer data, gpointer user_data) +{ + struct channels_context *context = user_data; + const struct client *client = data; + + for (GSList *i = client->subscriptions; i != NULL; + i = g_slist_next(i)) { + const char *channel = i->data; + + if (g_hash_table_lookup(context->channels, channel) == NULL) { + char *channel2 = g_string_chunk_insert(context->chunk, + channel); + g_hash_table_insert(context->channels, channel2, + context); + } + } +} + +static void +print_channel(gpointer key, G_GNUC_UNUSED gpointer value, gpointer user_data) +{ + struct client *client = user_data; + const char *channel = key; + + client_printf(client, "channel: %s\n", channel); +} + +static enum command_return +handle_channels(struct client *client, + G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) +{ + assert(argc == 1); + + struct channels_context context = { + .chunk = g_string_chunk_new(1024), + .channels = g_hash_table_new(g_str_hash, g_str_equal), + }; + + client_list_foreach(collect_channels, &context); + + g_hash_table_foreach(context.channels, print_channel, client); + + g_hash_table_destroy(context.channels); + g_string_chunk_free(context.chunk); + + return COMMAND_RETURN_OK; +} + +static enum command_return +handle_read_messages(struct client *client, + G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) +{ + assert(argc == 1); + + GSList *messages = client_read_messages(client); + + for (GSList *i = messages; i != NULL; i = g_slist_next(i)) { + struct client_message *msg = i->data; + + client_printf(client, "channel: %s\nmessage: %s\n", + msg->channel, msg->message); + client_message_free(msg); + } + + g_slist_free(messages); + + return COMMAND_RETURN_OK; +} + +struct send_message_context { + struct client_message msg; + + bool sent; +}; + +static void +send_message(gpointer data, gpointer user_data) +{ + struct send_message_context *context = user_data; + struct client *client = data; + + if (client_push_message(client, &context->msg)) + context->sent = true; +} + +static enum command_return +handle_send_message(struct client *client, + G_GNUC_UNUSED int argc, G_GNUC_UNUSED char *argv[]) +{ + assert(argc == 3); + + if (!client_message_valid_channel_name(argv[1])) { + command_error(client, ACK_ERROR_ARG, + "invalid channel name"); + return COMMAND_RETURN_ERROR; + } + + struct send_message_context context = { + .sent = false, + }; + + client_message_init(&context.msg, argv[1], argv[2]); + + client_list_foreach(send_message, &context); + + client_message_deinit(&context.msg); + + if (context.sent) + return COMMAND_RETURN_OK; + else { + command_error(client, ACK_ERROR_NO_EXIST, + "nobody is subscribed to this channel"); + return COMMAND_RETURN_ERROR; + } +} + /** * The command registry. * @@ -1845,6 +2012,7 @@ handle_sticker(struct client *client, int argc, char *argv[]) static const struct command commands[] = { { "add", PERMISSION_ADD, 1, 1, handle_add }, { "addid", PERMISSION_ADD, 1, 2, handle_addid }, + { "channels", PERMISSION_READ, 0, 0, handle_channels }, { "clear", PERMISSION_CONTROL, 0, 0, handle_clear }, { "clearerror", PERMISSION_CONTROL, 0, 0, handle_clearerror }, { "close", PERMISSION_NONE, -1, -1, handle_close }, @@ -1895,6 +2063,7 @@ static const struct command commands[] = { { "plchangesposid", PERMISSION_READ, 1, 1, handle_plchangesposid }, { "previous", PERMISSION_CONTROL, 0, 0, handle_previous }, { "random", PERMISSION_CONTROL, 1, 1, handle_random }, + { "readmessages", PERMISSION_READ, 0, 0, handle_read_messages }, { "rename", PERMISSION_CONTROL, 2, 2, handle_rename }, { "repeat", PERMISSION_CONTROL, 1, 1, handle_repeat }, { "replay_gain_mode", PERMISSION_CONTROL, 1, 1, @@ -1907,6 +2076,7 @@ static const struct command commands[] = { { "search", PERMISSION_READ, 2, -1, handle_search }, { "seek", PERMISSION_CONTROL, 2, 2, handle_seek }, { "seekid", PERMISSION_CONTROL, 2, 2, handle_seekid }, + { "sendmessage", PERMISSION_CONTROL, 2, 2, handle_send_message }, { "setvol", PERMISSION_CONTROL, 1, 1, handle_setvol }, { "shuffle", PERMISSION_CONTROL, 0, 1, handle_shuffle }, { "single", PERMISSION_CONTROL, 1, 1, handle_single }, @@ -1916,9 +2086,11 @@ static const struct command commands[] = { { "sticker", PERMISSION_ADMIN, 3, -1, handle_sticker }, #endif { "stop", PERMISSION_CONTROL, 0, 0, handle_stop }, + { "subscribe", PERMISSION_READ, 1, 1, handle_subscribe }, { "swap", PERMISSION_CONTROL, 2, 2, handle_swap }, { "swapid", PERMISSION_CONTROL, 2, 2, handle_swapid }, { "tagtypes", PERMISSION_READ, 0, 0, handle_tagtypes }, + { "unsubscribe", PERMISSION_READ, 1, 1, handle_unsubscribe }, { "update", PERMISSION_ADMIN, 0, 1, handle_update }, { "urlhandlers", PERMISSION_READ, 0, 0, handle_urlhandlers }, }; diff --git a/src/idle.c b/src/idle.c index 7b9f658c6..2d174d78a 100644 --- a/src/idle.c +++ b/src/idle.c @@ -42,6 +42,8 @@ static const char *const idle_names[] = { "options", "sticker", "update", + "subscription", + "message", NULL }; diff --git a/src/idle.h b/src/idle.h index 52adc4d6e..0156933c0 100644 --- a/src/idle.h +++ b/src/idle.h @@ -53,6 +53,12 @@ enum { /** a database update has started or finished. */ IDLE_UPDATE = 0x100, + + /** a client has subscribed or unsubscribed to/from a channel */ + IDLE_SUBSCRIPTION = 0x200, + + /** a message on the subscribed channel was receivedd */ + IDLE_MESSAGE = 0x400, }; /** |