aboutsummaryrefslogblamecommitdiffstats
path: root/src/client.c
blob: 991f0d0b246f79f046187f5647035804d280c47e (plain) (tree)
1
2
                                
                                                                   















                                                                            
                   

                    

                   
                       
                  
                  
                        
                  
                 
 

                      
                 






                       
                                                                               
 


                                                                               











                                                                                    
 




                                
               

                                  
                          

                            
 
                                                    
                            



                                                              
                        
                                                           
                                                                           
                                                           
                                                                  
                                                                
                                                   
 
                            
                                                              






                                                                    


                                                          
  
 

                            
 
                                                         
 
                                                       
 
                                                   
 
                              

 




                                               
                                                           



                                  
                                                                      



                                        

                                                            



                                   

 
                                                      
 
                                            
 

                        
                                  



                                 
                            

                                      
                                              
                                   
                                        
                                  
 
                                                     
 


                                               
                                       
 

                                                                      
 
                           

 
                                                            
 
                                                                          
 
 
           
                                                                     
 


                                              
 

                                               



                                      
 

                                   


                                                
         
 

                                                                           
 
                                                   
                     

 

                                                   
 
                             

                                  
               





                                                                                
                










                                                                             
                         

                      
      

                     


                                              
                    

                                     
         
 


                        
                                                             








                                                    



                                              
                          

                                                          

 
                                                                 
 
                    
 




















                                                                          



                                                              





                                                                             


                                                                       

                                                                      
 
                                                          
                                                      
                                                            
 
                                     
                                                        
 



                                                        
                        
                                                      



                                                           
                                                           
                                                            
                                                

                                                                           
                                                     
                                                                    
                                                            
                              
                                                               
                 
                

                                                                
                                

                                                                          
                                
                        

                                                                    
                                                            

                                                                 

                                                          
                                                      
                                                            

                                     
                                                        
 
                                                    
                 

         


                   
                                                                         
 
                                                               
                             
                
 


                                                                           
                                          
                                                    
 



























                                                                      
                 






                                                                

         
                 

 
                                             
 
                          
 


                                                              

                                                               
                                                                        
 
                          
                                                                

                                                     
                         


                                                    

 
                                                                     
 
                              

                     
                                            
 
                                                         

                                                              


                                                    



                 
                                                                      
 
                              


                     
                                                         
                                                                 
                                                                  


                                                    



                 
                           
 

                    
                    
                                  
                
                      
 


                                                        
 
                                                           
 
                           
                                                           
                             
 
                      

                                   
 

                                                                
 
                                                             
 
                              
 

                                                                 

                                                       
                                                           


                                                          
                         


                                                           

                                                      

                                                  

                                                      
                 

         
                 

 
                              
 

                           
 
                                                  
 
                    

                                                                 
                                                                            

                                                                      
                 

         

                                              
                    

                                                                         
                                                                                
                                                                        
                 
              
                                                                        
 

                                                           
                    

                                                           
                                                                               
                                                                               
                 
                                                          

         

                                                            
                    

                                                           
                                                                                
                                                                               
                 
                                                           
         

 
                                  
 




                                                               

 
                                
 
                           
 
                                   

 
                                
 


                                                                 
                                                

                                                                   


                                                                      


                                                                   



                 
                                                        
 
                        
 



                                                                 
                                      
                                                            
 
                                                              
                            
                              
                                                   

                                                                      
                                         
                                                                       
                              
                        

                                                              
 

                                                               

                                                                
                 
                                              

         
                                                      


                                                                   
                                                                  


                                                              
                                           


         


                                                                

                                    
 
                           
 

                                                          






                                                                     
                                           
                       
         
 




                                                      

 

                                                                
 
                    
 
                           
                                                        


                                                          
                                                                  

                                                                            
                                                   


                                                         
                                                                      

         
                                                     
                                                                  



                                                      
                                                                
                       
 
                                                     

                                                             
            

                                                             
 
                                  
 
 

                                                                           




                                                             
                               
 
                                                                         
 



                                                                           




                                                                        
                                                                      







                                                      




























                                                                           
 






                                         
                                      







                                        


                                                                  


















                                                              

                                                                           





                                                    
                                                            



                                      
                                           
 
                                                              




                                           
/* the Music Player Daemon (MPD)
 * Copyright (C) 2003-2007 by Warren Dukes (warren.dukes@gmail.com)
 * This project's homepage is: 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "client.h"
#include "command.h"
#include "conf.h"
#include "log.h"
#include "listen.h"
#include "permission.h"
#include "utils.h"
#include "ioops.h"
#include "main_notify.h"
#include "dlist.h"
#include "idle.h"

#include "../config.h"

#include <glib.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define GREETING				"OK MPD " PROTOCOL_VERSION "\n"

#define CLIENT_LIST_MODE_BEGIN			"command_list_begin"
#define CLIENT_LIST_OK_MODE_BEGIN			"command_list_ok_begin"
#define CLIENT_LIST_MODE_END				"command_list_end"
#define CLIENT_TIMEOUT_DEFAULT			(60)
#define CLIENT_MAX_CONNECTIONS_DEFAULT		(10)
#define CLIENT_MAX_COMMAND_LIST_DEFAULT		(2048*1024)
#define CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT	(8192*1024)

/* set this to zero to indicate we have no possible clients */
static unsigned int client_max_connections;	/*CLIENT_MAX_CONNECTIONS_DEFAULT; */
static int client_timeout = CLIENT_TIMEOUT_DEFAULT;
static size_t client_max_command_list_size =
    CLIENT_MAX_COMMAND_LIST_DEFAULT;
static size_t client_max_output_buffer_size =
    CLIENT_MAX_OUTPUT_BUFFER_SIZE_DEFAULT;

struct deferred_buffer {
	size_t size;
	char data[sizeof(long)];
};

struct client {
	struct list_head siblings;

	char buffer[4096];
	size_t bufferLength;
	size_t bufferPos;

	int fd;	/* file descriptor; -1 if expired */
	unsigned permission;

	/** the uid of the client process, or -1 if unknown */
	int uid;

	time_t lastTime;
	GSList *cmd_list;	/* for when in list mode */
	int cmd_list_OK;	/* print OK after each command execution */
	size_t cmd_list_size;	/* mem cmd_list consumes */
	GQueue *deferred_send;	/* for output if client is slow */
	size_t deferred_bytes;	/* mem deferred_send consumes */
	unsigned int num;	/* client number */

	char send_buf[4096];
	size_t send_buf_used;	/* bytes used this instance */

	/** is this client waiting for an "idle" response? */
	bool idle_waiting;

	/** idle flags pending on this client, to be sent as soon as
	    the client enters "idle" */
	unsigned idle_flags;

	/** idle flags that the client wants to receive */
	unsigned idle_subscriptions;
};

static LIST_HEAD(clients);
static unsigned num_clients;

static void client_write_deferred(struct client *client);

static void client_write_output(struct client *client);

bool client_is_expired(const struct client *client)
{
	return client->fd < 0;
}

int client_get_uid(const struct client *client)
{
	return client->uid;
}

unsigned client_get_permission(const struct client *client)
{
	return client->permission;
}

void client_set_permission(struct client *client, unsigned permission)
{
	client->permission = permission;
}

static inline void client_set_expired(struct client *client)
{
	if (client->fd >= 0) {
		xclose(client->fd);
		client->fd = -1;
	}
}

static void client_init(struct client *client, int fd)
{
	static unsigned int next_client_num;

	assert(fd >= 0);

	client->cmd_list_size = 0;
	client->cmd_list_OK = -1;
	client->bufferLength = 0;
	client->bufferPos = 0;
	client->fd = fd;
	set_nonblocking(fd);
	client->lastTime = time(NULL);
	client->cmd_list = NULL;
	client->deferred_send = g_queue_new();
	client->deferred_bytes = 0;
	client->num = next_client_num++;
	client->send_buf_used = 0;

	client->permission = getDefaultPermissions();

	xwrite(fd, GREETING, strlen(GREETING));
}

static void free_cmd_list(GSList *list)
{
	for (GSList *tmp = list; tmp != NULL; tmp = g_slist_next(tmp))
		g_free(tmp->data);

	g_slist_free(list);
}

static void new_cmd_list_ptr(struct client *client, char *s)
{
	client->cmd_list = g_slist_prepend(client->cmd_list, g_strdup(s));
}

static void
deferred_buffer_free(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
	struct deferred_buffer *buffer = data;
	g_free(buffer);
}

static void client_close(struct client *client)
{
	assert(num_clients > 0);
	assert(!list_empty(&clients));
	list_del(&client->siblings);
	--num_clients;

	client_set_expired(client);

	if (client->cmd_list) {
		free_cmd_list(client->cmd_list);
		client->cmd_list = NULL;
	}

	g_queue_foreach(client->deferred_send, deferred_buffer_free, NULL);
	g_queue_free(client->deferred_send);

	SECURE("client %i: closed\n", client->num);
	free(client);
}

static const char *
sockaddr_to_tmp_string(const struct sockaddr *addr)
{
	const char *hostname;

	switch (addr->sa_family) {
#ifdef HAVE_TCP
	case AF_INET:
		hostname = (const char *)inet_ntoa(((const struct sockaddr_in *)
						    addr)->sin_addr);
		if (!hostname)
			hostname = "error getting ipv4 address";
		break;
#ifdef HAVE_IPV6
	case AF_INET6:
		{
			static char host[INET6_ADDRSTRLEN + 1];
			memset(host, 0, INET6_ADDRSTRLEN + 1);
			if (inet_ntop(AF_INET6, (const void *)
				      &(((const struct sockaddr_in6 *)addr)->
					sin6_addr), host,
				      INET6_ADDRSTRLEN)) {
				hostname = (const char *)host;
			} else {
				hostname = "error getting ipv6 address";
			}
		}
		break;
#endif
#endif /* HAVE_TCP */
#ifdef HAVE_UN
	case AF_UNIX:
		hostname = "local connection";
		break;
#endif /* HAVE_UN */
	default:
		hostname = "unknown";
	}

	return hostname;
}

void client_new(int fd, const struct sockaddr *addr, int uid)
{
	struct client *client;

	if (num_clients >= client_max_connections) {
		ERROR("Max Connections Reached!\n");
		xclose(fd);
		return;
	}

	client = xcalloc(1, sizeof(*client));
	list_add(&client->siblings, &clients);
	++num_clients;
	client_init(client, fd);
	client->uid = uid;
	SECURE("client %i: opened from %s\n", client->num,
	       sockaddr_to_tmp_string(addr));
}

static int client_process_line(struct client *client, char *line)
{
	int ret = 1;

	if (strcmp(line, "noidle") == 0) {
		if (client->idle_waiting) {
			/* send empty idle response and leave idle mode */
			client->idle_waiting = false;
			command_success(client);
			client_write_output(client);
		}

		/* do nothing if the client wasn't idling: the client
		   has already received the full idle response from
		   client_idle_notify(), which he can now evaluate */

		return 0;
	} else if (client->idle_waiting) {
		/* during idle mode, clients must not send anything
		   except "noidle" */
		ERROR("client %i: command \"%s\" during idle\n",
		      client->num, line);
		return COMMAND_RETURN_CLOSE;
	}

	if (client->cmd_list_OK >= 0) {
		if (strcmp(line, CLIENT_LIST_MODE_END) == 0) {
			DEBUG("client %i: process command "
			      "list\n", client->num);

			/* for scalability reasons, we have prepended
			   each new command; now we have to reverse it
			   to restore the correct order */
			client->cmd_list = g_slist_reverse(client->cmd_list);

			ret = command_process_list(client,
						   client->cmd_list_OK,
						   client->cmd_list);
			DEBUG("client %i: process command "
			      "list returned %i\n", client->num, ret);

			if (ret == COMMAND_RETURN_CLOSE ||
			    client_is_expired(client))
				return COMMAND_RETURN_CLOSE;

			if (ret == 0)
				command_success(client);

			client_write_output(client);
			free_cmd_list(client->cmd_list);
			client->cmd_list = NULL;
			client->cmd_list_OK = -1;
		} else {
			size_t len = strlen(line) + 1;
			client->cmd_list_size += len;
			if (client->cmd_list_size >
			    client_max_command_list_size) {
				ERROR("client %i: command "
				      "list size (%lu) is "
				      "larger than the max "
				      "(%lu)\n",
				      client->num,
				      (unsigned long)client->cmd_list_size,
				      (unsigned long)
				      client_max_command_list_size);
				return COMMAND_RETURN_CLOSE;
			} else
				new_cmd_list_ptr(client, line);
		}
	} else {
		if (strcmp(line, CLIENT_LIST_MODE_BEGIN) == 0) {
			client->cmd_list_OK = 0;
			ret = 1;
		} else if (strcmp(line, CLIENT_LIST_OK_MODE_BEGIN) == 0) {
			client->cmd_list_OK = 1;
			ret = 1;
		} else {
			DEBUG("client %i: process command \"%s\"\n",
			      client->num, line);
			ret = command_process(client, line);
			DEBUG("client %i: command returned %i\n",
			      client->num, ret);

			if (ret == COMMAND_RETURN_CLOSE ||
			    client_is_expired(client))
				return COMMAND_RETURN_CLOSE;

			if (ret == 0)
				command_success(client);

			client_write_output(client);
		}
	}

	return ret;
}

static int client_input_received(struct client *client, size_t bytesRead)
{
	char *start = client->buffer + client->bufferPos, *end;
	char *newline, *next;
	int ret;

	assert(client->bufferPos <= client->bufferLength);
	assert(client->bufferLength + bytesRead <= sizeof(client->buffer));

	client->bufferLength += bytesRead;
	end = client->buffer + client->bufferLength;

	/* process all lines */
	while ((newline = memchr(start, '\n', end - start)) != NULL) {
		next = newline + 1;

		if (newline > start && newline[-1] == '\r')
			--newline;
		*newline = 0;

		ret = client_process_line(client, start);
		if (ret == COMMAND_RETURN_KILL ||
		    ret == COMMAND_RETURN_CLOSE)
			return ret;
		if (client_is_expired(client))
			return COMMAND_RETURN_CLOSE;

		start = next;
	}

	/* mark consumed lines */
	client->bufferPos = start - client->buffer;

	/* if we're have reached the buffer's end, close the gab at
	   the beginning */
	if (client->bufferLength == sizeof(client->buffer)) {
		if (client->bufferPos == 0) {
			ERROR("client %i: buffer overflow\n",
			      client->num);
			return COMMAND_RETURN_CLOSE;
		}
		assert(client->bufferLength >= client->bufferPos
		       && "bufferLength >= bufferPos");
		client->bufferLength -= client->bufferPos;
		memmove(client->buffer,
			client->buffer + client->bufferPos,
			client->bufferLength);
		client->bufferPos = 0;
	}

	return 0;
}

static int client_read(struct client *client)
{
	ssize_t bytesRead;

	assert(client->bufferPos <= client->bufferLength);
	assert(client->bufferLength < sizeof(client->buffer));

	bytesRead = read(client->fd,
			 client->buffer + client->bufferLength,
			 sizeof(client->buffer) - client->bufferLength);

	if (bytesRead > 0)
		return client_input_received(client, bytesRead);
	else if (bytesRead < 0 && errno == EINTR)
		/* try again later, after select() */
		return 0;
	else
		/* peer disconnected or I/O error */
		return COMMAND_RETURN_CLOSE;
}

static void client_manager_register_read_fd(fd_set * fds, int *fdmax)
{
	struct client *client;

	FD_ZERO(fds);
	addListenSocketsToFdSet(fds, fdmax);

	list_for_each_entry(client, &clients, siblings) {
		if (!client_is_expired(client) &&
		    g_queue_is_empty(client->deferred_send)) {
			FD_SET(client->fd, fds);
			if (*fdmax < client->fd)
				*fdmax = client->fd;
		}
	}
}

static void client_manager_register_write_fd(fd_set * fds, int *fdmax)
{
	struct client *client;

	FD_ZERO(fds);

	list_for_each_entry(client, &clients, siblings) {
		if (client->fd >= 0 && !client_is_expired(client)
		    && !g_queue_is_empty(client->deferred_send)) {
			FD_SET(client->fd, fds);
			if (*fdmax < client->fd)
				*fdmax = client->fd;
		}
	}
}

int client_manager_io(void)
{
	fd_set rfds;
	fd_set wfds;
	fd_set efds;
	struct client *client, *n;
	int ret;
	int fdmax = 0;

	FD_ZERO( &efds );
	client_manager_register_read_fd(&rfds, &fdmax);
	client_manager_register_write_fd(&wfds, &fdmax);

	registered_IO_add_fds(&fdmax, &rfds, &wfds, &efds);

	main_notify_lock();
	ret = select(fdmax + 1, &rfds, &wfds, &efds, NULL);
	main_notify_unlock();

	if (ret < 0) {
		if (errno == EINTR)
			return 0;

		FATAL("select() failed: %s\n", strerror(errno));
	}

	registered_IO_consume_fds(&ret, &rfds, &wfds, &efds);

	getConnections(&rfds);

	list_for_each_entry_safe(client, n, &clients, siblings) {
		if (FD_ISSET(client->fd, &rfds)) {
			ret = client_read(client);
			if (ret == COMMAND_RETURN_KILL)
				return COMMAND_RETURN_KILL;
			if (ret == COMMAND_RETURN_CLOSE) {
				client_close(client);
				continue;
			}

			assert(!client_is_expired(client));

			client->lastTime = time(NULL);
		}
		if (!client_is_expired(client) &&
		    FD_ISSET(client->fd, &wfds)) {
			client_write_deferred(client);
			client->lastTime = time(NULL);
		}
	}

	return 0;
}

void client_manager_init(void)
{
	char *test;
	ConfigParam *param;

	param = getConfigParam(CONF_CONN_TIMEOUT);

	if (param) {
		client_timeout = strtol(param->value, &test, 10);
		if (*test != '\0' || client_timeout <= 0) {
			FATAL("connection timeout \"%s\" is not a positive "
			      "integer, line %i\n", CONF_CONN_TIMEOUT,
			      param->line);
		}
	}

	param = getConfigParam(CONF_MAX_CONN);

	if (param) {
		client_max_connections = strtol(param->value, &test, 10);
		if (*test != '\0' || client_max_connections <= 0) {
			FATAL("max connections \"%s\" is not a positive integer"
			      ", line %i\n", param->value, param->line);
		}
	} else
		client_max_connections = CLIENT_MAX_CONNECTIONS_DEFAULT;

	param = getConfigParam(CONF_MAX_COMMAND_LIST_SIZE);

	if (param) {
		long tmp = strtol(param->value, &test, 10);
		if (*test != '\0' || tmp <= 0) {
			FATAL("max command list size \"%s\" is not a positive "
			      "integer, line %i\n", param->value, param->line);
		}
		client_max_command_list_size = tmp * 1024;
	}

	param = getConfigParam(CONF_MAX_OUTPUT_BUFFER_SIZE);

	if (param) {
		long tmp = strtol(param->value, &test, 10);
		if (*test != '\0' || tmp <= 0) {
			FATAL("max output buffer size \"%s\" is not a positive "
			      "integer, line %i\n", param->value, param->line);
		}
		client_max_output_buffer_size = tmp * 1024;
	}
}

static void client_close_all(void)
{
	struct client *client, *n;

	list_for_each_entry_safe(client, n, &clients, siblings)
		client_close(client);
	num_clients = 0;
}

void client_manager_deinit(void)
{
	client_close_all();

	client_max_connections = 0;
}

void client_manager_expire(void)
{
	struct client *client, *n;

	list_for_each_entry_safe(client, n, &clients, siblings) {
		if (client_is_expired(client)) {
			DEBUG("client %i: expired\n", client->num);
			client_close(client);
		} else if (!client->idle_waiting && /* idle clients
						       never expire */
			   time(NULL) - client->lastTime >
			   client_timeout) {
			DEBUG("client %i: timeout\n", client->num);
			client_close(client);
		}
	}
}

static void client_write_deferred(struct client *client)
{
	ssize_t ret = 0;

	while (!g_queue_is_empty(client->deferred_send)) {
		struct deferred_buffer *buf =
			g_queue_peek_head(client->deferred_send);

		assert(buf->size > 0);
		assert(buf->size <= client->deferred_bytes);

		ret = write(client->fd, buf->data, buf->size);
		if (ret < 0)
			break;
		else if ((size_t)ret < buf->size) {
			assert(client->deferred_bytes >= (size_t)ret);
			client->deferred_bytes -= ret;
			buf->size -= ret;
			memmove(buf->data, buf->data + ret, buf->size);
			break;
		} else {
			size_t decr = sizeof(*buf) -
				sizeof(buf->data) + buf->size;

			assert(client->deferred_bytes >= decr);
			client->deferred_bytes -= decr;
			free(buf);
			g_queue_pop_head(client->deferred_send);
		}
		client->lastTime = time(NULL);
	}

	if (g_queue_is_empty(client->deferred_send)) {
		DEBUG("client %i: buffer empty %lu\n", client->num,
		      (unsigned long)client->deferred_bytes);
		assert(client->deferred_bytes == 0);
	} else if (ret < 0 && errno != EAGAIN && errno != EINTR) {
		/* cause client to close */
		DEBUG("client %i: problems flushing buffer\n",
		      client->num);
		client_set_expired(client);
	}
}

static void client_defer_output(struct client *client,
				const void *data, size_t length)
{
	size_t alloc;
	struct deferred_buffer *buf;

	assert(length > 0);

	alloc = sizeof(*buf) - sizeof(buf->data) + length;
	client->deferred_bytes += alloc;
	if (client->deferred_bytes > client_max_output_buffer_size) {
		ERROR("client %i: output buffer size (%lu) is "
		      "larger than the max (%lu)\n",
		      client->num,
		      (unsigned long)client->deferred_bytes,
		      (unsigned long)client_max_output_buffer_size);
		/* cause client to close */
		client_set_expired(client);
		return;
	}

	buf = g_malloc(alloc);
	buf->size = length;
	memcpy(buf->data, data, length);

	g_queue_push_tail(client->deferred_send, buf);
}

static void client_write_direct(struct client *client,
				const char *data, size_t length)
{
	ssize_t ret;

	assert(length > 0);
	assert(g_queue_is_empty(client->deferred_send));

	if ((ret = write(client->fd, data, length)) < 0) {
		if (errno == EAGAIN || errno == EINTR) {
			client_defer_output(client, data, length);
		} else {
			DEBUG("client %i: problems writing\n", client->num);
			client_set_expired(client);
			return;
		}
	} else if ((size_t)ret < client->send_buf_used) {
		client_defer_output(client, data + ret, length - ret);
	}

	if (!g_queue_is_empty(client->deferred_send))
		DEBUG("client %i: buffer created\n", client->num);
}

static void client_write_output(struct client *client)
{
	if (client_is_expired(client) || !client->send_buf_used)
		return;

	if (!g_queue_is_empty(client->deferred_send))
		client_defer_output(client, client->send_buf,
				    client->send_buf_used);
	else
		client_write_direct(client, client->send_buf,
				    client->send_buf_used);

	client->send_buf_used = 0;
}

void client_write(struct client *client, const char *buffer, size_t buflen)
{
	/* if the client is going to be closed, do nothing */
	if (client_is_expired(client))
		return;

	while (buflen > 0 && !client_is_expired(client)) {
		size_t copylen;

		assert(client->send_buf_used < sizeof(client->send_buf));

		copylen = sizeof(client->send_buf) - client->send_buf_used;
		if (copylen > buflen)
			copylen = buflen;

		memcpy(client->send_buf + client->send_buf_used, buffer,
		       copylen);
		buflen -= copylen;
		client->send_buf_used += copylen;
		buffer += copylen;
		if (client->send_buf_used >= sizeof(client->send_buf))
			client_write_output(client);
	}
}

void client_puts(struct client *client, const char *s)
{
	client_write(client, s, strlen(s));
}

void client_vprintf(struct client *client, const char *fmt, va_list args)
{
	va_list tmp;
	int length;
	char *buffer;

	va_copy(tmp, args);
	length = vsnprintf(NULL, 0, fmt, tmp);
	va_end(tmp);

	if (length <= 0)
		/* wtf.. */
		return;

	buffer = xmalloc(length + 1);
	vsnprintf(buffer, length + 1, fmt, args);
	client_write(client, buffer, length);
	free(buffer);
}

mpd_fprintf void client_printf(struct client *client, const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
	client_vprintf(client, fmt, args);
	va_end(args);
}

/**
 * Send "idle" response to this client.
 */
static void
client_idle_notify(struct client *client)
{
	unsigned flags, i;
	const char *const* idle_names;

	assert(client->idle_waiting);
	assert(client->idle_flags != 0);

	flags = client->idle_flags;
	client->idle_flags = 0;
	client->idle_waiting = false;

	idle_names = idle_get_names();
	for (i = 0; idle_names[i]; ++i) {
		if (flags & (1 << i) & client->idle_subscriptions)
			client_printf(client, "changed: %s\n",
				      idle_names[i]);
	}

	client_puts(client, "OK\n");
	client->lastTime = time(NULL);
}

void client_manager_idle_add(unsigned flags)
{
	struct client *client;

	assert(flags != 0);

	list_for_each_entry(client, &clients, siblings) {
		if (client_is_expired(client))
			continue;

		client->idle_flags |= flags;
		if (client->idle_waiting
		    && (client->idle_flags & client->idle_subscriptions)) {
			client_idle_notify(client);
			client_write_output(client);
		}
	}
}

bool client_idle_wait(struct client *client, unsigned flags)
{
	assert(!client->idle_waiting);

	client->idle_waiting = true;
	client->idle_subscriptions = flags;

	if (client->idle_flags & client->idle_subscriptions) {
		client_idle_notify(client);
		return true;
	} else
		return false;
}