From 533a6b0240c10755b9c1e47ab20611f289dac412 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Sun, 28 Aug 2011 21:52:16 +0200 Subject: tcp_connect: generic library for establishing TCP connections --- Makefile.am | 11 +++ src/tcp_connect.c | 252 +++++++++++++++++++++++++++++++++++++++++++++++++ src/tcp_connect.h | 96 +++++++++++++++++++ test/run_tcp_connect.c | 165 ++++++++++++++++++++++++++++++++ 4 files changed, 524 insertions(+) create mode 100644 src/tcp_connect.c create mode 100644 src/tcp_connect.h create mode 100644 test/run_tcp_connect.c diff --git a/Makefile.am b/Makefile.am index 900f0b421..87eaf5af0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -299,6 +299,7 @@ src_mpd_SOURCES = \ src/client_message.c \ src/client_subscribe.h \ src/client_subscribe.c \ + src/tcp_connect.c src/tcp_connect.h \ src/tcp_socket.c src/tcp_socket.h \ src/udp_server.c src/udp_server.h \ src/server_socket.c \ @@ -904,6 +905,7 @@ noinst_PROGRAMS = \ $(C_TESTS) \ test/read_conf \ test/run_resolver \ + test/run_tcp_connect \ test/run_input \ test/dump_playlist \ test/run_decoder \ @@ -932,6 +934,15 @@ test_run_resolver_LDADD = $(MPD_LIBS) \ test_run_resolver_SOURCES = test/run_resolver.c \ src/resolver.c +test_run_tcp_connect_CPPFLAGS = $(AM_CPPFLAGS) +test_run_tcp_connect_LDADD = $(MPD_LIBS) \ + $(GLIB_LIBS) +test_run_tcp_connect_SOURCES = test/run_tcp_connect.c \ + src/io_thread.c src/io_thread.h \ + src/fd_util.c \ + src/resolver.c \ + src/tcp_connect.c + test_run_input_CPPFLAGS = $(AM_CPPFLAGS) \ $(ARCHIVE_CFLAGS) \ $(INPUT_CFLAGS) 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 +#include + +#ifdef WIN32 +#define WINVER 0x0501 +#include +#include +#else +#include +#include +#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); +} diff --git a/src/tcp_connect.h b/src/tcp_connect.h new file mode 100644 index 000000000..bdbe85c14 --- /dev/null +++ b/src/tcp_connect.h @@ -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. + */ + +#ifndef MPD_TCP_CONNECT_H +#define MPD_TCP_CONNECT_H + +#include + +struct sockaddr; + +struct tcp_connect_handler { + /** + * The connection was established successfully. + * + * @param fd a file descriptor that must be closed with + * close_socket() when finished + */ + void (*success)(int fd, void *ctx); + + /** + * An error has occurred. The method is responsible for + * freeing the GError. + */ + void (*error)(GError *error, void *ctx); + + /** + * The connection could not be established in the specified + * time span. + */ + void (*timeout)(void *ctx); + + /** + * The operation was canceled before a result was available. + */ + void (*canceled)(void *ctx); +}; + +struct tcp_connect; + +/** + * Establish a TCP connection to the specified address. + * + * Note that the result may be available before this function returns. + * + * The caller must free this object with tcp_connect_free(). + * + * @param timeout_ms time out after this number of milliseconds; 0 + * means no timeout + * @param handle_r a handle that can be used to cancel the operation; + * the caller must initialize it to 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); + +/** + * Cancel the operation. It is possible that the result is delivered + * before the operation has been canceled; in that case, the + * canceled() handler method will not be invoked. + * + * Even after calling this function, tcp_connect_free() must still be + * called to free memory. + */ +void +tcp_connect_cancel(struct tcp_connect *handle); + +/** + * Free memory used by this object. + * + * This function is not thread safe. It must not be called while + * other threads are still working with it. If no callback has been + * invoked so far, then you must call tcp_connect_cancel() to release + * I/O thread resources, before calling this function. + */ +void +tcp_connect_free(struct tcp_connect *handle); + +#endif diff --git a/test/run_tcp_connect.c b/test/run_tcp_connect.c new file mode 100644 index 000000000..aa8fb05f5 --- /dev/null +++ b/test/run_tcp_connect.c @@ -0,0 +1,165 @@ +/* + * 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 "resolver.h" +#include "io_thread.h" +#include "tcp_connect.h" +#include "fd_util.h" + +#include +#include + +#ifdef WIN32 +#define WINVER 0x0501 +#include +#include +#else +#include +#include +#endif + +static struct tcp_connect *handle; +static GMutex *mutex; +static GCond *cond; +static bool done, success; + +static void +my_tcp_connect_success(int fd, G_GNUC_UNUSED void *ctx) +{ + assert(!done); + assert(!success); + + close_socket(fd); + g_print("success\n"); + + g_mutex_lock(mutex); + done = success = true; + g_cond_signal(cond); + g_mutex_unlock(mutex); +} + +static void +my_tcp_connect_error(GError *error, G_GNUC_UNUSED void *ctx) +{ + assert(!done); + assert(!success); + + g_printerr("error: %s\n", error->message); + g_error_free(error); + + g_mutex_lock(mutex); + done = true; + g_cond_signal(cond); + g_mutex_unlock(mutex); +} + +static void +my_tcp_connect_timeout(G_GNUC_UNUSED void *ctx) +{ + assert(!done); + assert(!success); + + g_printerr("timeout\n"); + + g_mutex_lock(mutex); + done = true; + g_cond_signal(cond); + g_mutex_unlock(mutex); +} + +static void +my_tcp_connect_canceled(G_GNUC_UNUSED void *ctx) +{ + assert(!done); + assert(!success); + + g_printerr("canceled\n"); + + g_mutex_lock(mutex); + done = true; + g_cond_signal(cond); + g_mutex_unlock(mutex); +} + +static const struct tcp_connect_handler my_tcp_connect_handler = { + .success = my_tcp_connect_success, + .error = my_tcp_connect_error, + .timeout = my_tcp_connect_timeout, + .canceled = my_tcp_connect_canceled, +}; + +int main(int argc, char **argv) +{ + if (argc != 2) { + g_printerr("Usage: run_tcp_connect IP:PORT\n"); + return 1; + } + + GError *error = NULL; + struct addrinfo *ai = resolve_host_port(argv[1], 80, 0, SOCK_STREAM, + &error); + if (ai == NULL) { + g_printerr("%s\n", error->message); + g_error_free(error); + return EXIT_FAILURE; + } + + /* initialize GLib */ + + g_thread_init(NULL); + + /* initialize MPD */ + + io_thread_init(); + if (!io_thread_start(&error)) { + freeaddrinfo(ai); + g_printerr("%s", error->message); + g_error_free(error); + return EXIT_FAILURE; + } + + /* open the connection */ + + mutex = g_mutex_new(); + cond = g_cond_new(); + + tcp_connect_address(ai->ai_addr, ai->ai_addrlen, 5000, + &my_tcp_connect_handler, NULL, + &handle); + freeaddrinfo(ai); + + if (handle != NULL) { + g_mutex_lock(mutex); + while (!done) + g_cond_wait(cond, mutex); + g_mutex_unlock(mutex); + + tcp_connect_free(handle); + } + + g_cond_free(cond); + g_mutex_free(mutex); + + /* deinitialize everything */ + + io_thread_deinit(); + + return EXIT_SUCCESS; +} -- cgit v1.2.3