diff options
Diffstat (limited to 'src/tcp_connect.c')
-rw-r--r-- | src/tcp_connect.c | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/src/tcp_connect.c b/src/tcp_connect.c new file mode 100644 index 000000000..bea823955 --- /dev/null +++ b/src/tcp_connect.c @@ -0,0 +1,252 @@ +/* + * 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 "config.h" +#include "tcp_connect.h" +#include "fd_util.h" +#include "io_thread.h" +#include "glib_compat.h" +#include "glib_socket.h" + +#include <assert.h> +#include <errno.h> + +#ifdef WIN32 +#define WINVER 0x0501 +#include <ws2tcpip.h> +#include <winsock.h> +#else +#include <sys/socket.h> +#include <unistd.h> +#endif + +struct tcp_connect { + const struct tcp_connect_handler *handler; + void *handler_ctx; + + int fd; + GSource *source; + + unsigned timeout_ms; + GSource *timeout_source; +}; + +static bool +is_in_progress_errno(int e) +{ +#ifdef WIN32 + return e == WSAEINPROGRESS || e == WSAEWOULDBLOCK; +#else + return e == EINPROGRESS; +#endif +} + +static gboolean +tcp_connect_event(G_GNUC_UNUSED GIOChannel *source, + G_GNUC_UNUSED GIOCondition condition, + gpointer data) +{ + struct tcp_connect *c = data; + + assert(c->source != NULL); + assert(c->timeout_source != NULL); + + /* clear the socket source */ + g_source_unref(c->source); + c->source = NULL; + + /* delete the timeout source */ + g_source_destroy(c->timeout_source); + g_source_unref(c->timeout_source); + c->timeout_source = NULL; + + /* obtain the connect result */ + int s_err = 0; + socklen_t s_err_size = sizeof(s_err); + if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, + (char*)&s_err, &s_err_size) < 0) + s_err = errno; + + if (s_err == 0) { + /* connection established successfully */ + + c->handler->success(c->fd, c->handler_ctx); + } else { + /* there was an I/O error; close the socket and pass + the error to the handler */ + + close_socket(c->fd); + + GError *error = + g_error_new_literal(g_file_error_quark(), s_err, + g_strerror(s_err)); + c->handler->error(error, c->handler_ctx); + } + + return false; +} + +static gboolean +tcp_connect_timeout(gpointer data) +{ + struct tcp_connect *c = data; + + assert(c->source != NULL); + assert(c->timeout_source != NULL); + + /* clear the timeout source */ + g_source_unref(c->timeout_source); + c->timeout_source = NULL; + + /* delete the socket source */ + g_source_destroy(c->source); + g_source_unref(c->source); + c->source = NULL; + + /* report timeout to handler */ + c->handler->timeout(c->handler_ctx); + + return false; +} + +static gpointer +tcp_connect_init(gpointer data) +{ + struct tcp_connect *c = data; + + /* create a connect source */ + GIOChannel *channel = g_io_channel_new_socket(c->fd); + c->source = g_io_create_watch(channel, G_IO_OUT); + g_io_channel_unref(channel); + + g_source_set_callback(c->source, (GSourceFunc)tcp_connect_event, c, + NULL); + g_source_attach(c->source, io_thread_context()); + + /* create a timeout source */ + if (c->timeout_ms > 0) + c->timeout_source = + io_thread_timeout_add(c->timeout_ms, + tcp_connect_timeout, c); + + return NULL; +} + +void +tcp_connect_address(const struct sockaddr *address, size_t address_length, + unsigned timeout_ms, + const struct tcp_connect_handler *handler, void *ctx, + struct tcp_connect **handle_r) +{ + assert(address != NULL); + assert(address_length > 0); + assert(handler != NULL); + assert(handler->success != NULL); + assert(handler->error != NULL); + assert(handler->canceled != NULL); + assert(handler->timeout != NULL || timeout_ms == 0); + assert(handle_r != NULL); + assert(*handle_r == NULL); + + int fd = socket_cloexec_nonblock(address->sa_family, SOCK_STREAM, 0); + if (fd < 0) { + GError *error = + g_error_new_literal(g_file_error_quark(), errno, + g_strerror(errno)); + handler->error(error, ctx); + return; + } + + int ret = connect(fd, address, address_length); + if (ret >= 0) { + /* quick connect, no I/O thread */ + handler->success(fd, ctx); + return; + } + + if (!is_in_progress_errno(errno)) { + GError *error = + g_error_new_literal(g_file_error_quark(), errno, + g_strerror(errno)); + close_socket(fd); + handler->error(error, ctx); + return; + } + + /* got EINPROGRESS, use the I/O thread to wait for the + operation to finish */ + + struct tcp_connect *c = g_new(struct tcp_connect, 1); + c->handler = handler; + c->handler_ctx = ctx; + c->fd = fd; + c->source = NULL; + c->timeout_ms = timeout_ms; + c->timeout_source = NULL; + + *handle_r = c; + + io_thread_call(tcp_connect_init, c); +} + +static gpointer +tcp_connect_cancel_callback(gpointer data) +{ + struct tcp_connect *c = data; + + assert((c->source == NULL) == (c->timeout_source == NULL)); + + if (c->source == NULL) + return NULL; + + /* delete the socket source */ + g_source_destroy(c->source); + g_source_unref(c->source); + c->source = NULL; + + /* delete the timeout source */ + g_source_destroy(c->timeout_source); + g_source_unref(c->timeout_source); + c->timeout_source = NULL; + + /* close the socket */ + close_socket(c->fd); + + /* notify the handler */ + c->handler->canceled(c->handler_ctx); + + return NULL; +} + +void +tcp_connect_cancel(struct tcp_connect *c) +{ + if (c->source == NULL) + return; + + io_thread_call(tcp_connect_cancel_callback, c); +} + +void +tcp_connect_free(struct tcp_connect *c) +{ + assert(c->source == NULL); + + g_free(c); +} |