From 29bec0c0e0658b51f11ea38c04df0e7aa514572c Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sun, 26 Oct 2014 18:57:28 +0100 Subject: Lay down some server-client foundations --- autistdraw.c | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- utils.c | 71 ++++++++++++++ 2 files changed, 372 insertions(+), 8 deletions(-) diff --git a/autistdraw.c b/autistdraw.c index 3938855..3e7b6c4 100644 --- a/autistdraw.c +++ b/autistdraw.c @@ -39,6 +39,22 @@ #define BITMAP_BLOCK_SIZE 50 ///< Step for extending bitmap size +typedef enum network_mode network_mode_t; +enum network_mode +{ + NETWORK_MODE_STANDALONE, ///< No networking taking place + NETWORK_MODE_SERVER, ///< We're the server + NETWORK_MODE_CLIENT ///< We're a client +}; + +typedef struct client client_t; +struct client +{ + LIST_HEADER (client_t) + + uv_tcp_t handle; ///< TCP connection handle +}; + typedef struct app_context app_context_t; struct app_context { @@ -49,6 +65,16 @@ struct app_context uv_timer_t tty_timer; ///< TTY timeout timer uv_signal_t winch_watcher; ///< SIGWINCH watcher + network_mode_t mode; ///< Networking mode + char read_buf[8192]; ///< Global read buffer for libuv + + // Client: + uv_tcp_t server_fd; ///< Connection to the server + + // Server: + uv_tcp_t listen_fd; ///< Listening FD + client_t *clients; ///< Client connections + chtype palette[2 * 9]; ///< Attribute palette uint8_t *bitmap; ///< Canvas data for drawing @@ -78,11 +104,23 @@ app_init (app_context_t *self) memset (self, 0, sizeof *self); } +static void +remove_client (app_context_t *app, client_t *client) +{ + // TODO: cancel any write requests? + // XXX: should we unref it? + uv_close ((uv_handle_t *) &client->handle, NULL); + LIST_UNLINK (app->clients, client); + free (client); +} + static void app_free (app_context_t *self) { if (self->tk) termo_destroy (self->tk); + while (self->clients) + remove_client (self, self->clients); free (self->bitmap); } @@ -465,7 +503,7 @@ export_irc (app_context_t *app) fclose (fp); } -// ----------------------------------------------------------------------------- +// --- Event handlers ---------------------------------------------------------- static bool on_key (app_context_t *app, termo_key_t *key) @@ -602,14 +640,158 @@ on_tty_readable (uv_poll_t *handle, int status, int events) } static void -parse_program_arguments (app_context_t *app, int argc, char **argv) +app_uv_allocator (uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) +{ + // Let's just use a single "global" buffer + (void) suggested_size; + + app_context_t *app = handle->loop->data; + buf->base = app->read_buf; + buf->len = sizeof app->read_buf; +} + +static void +on_server_data (uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) +{ + (void) buf; + + app_context_t *app = stream->loop->data; + if (nread == UV_EOF || nread < 0) + { + // TODO: cancel any write requests? + // XXX: should we unref it? + uv_close ((uv_handle_t *) &app->server_fd, NULL); + + display ("Disconnected!"); + beep (); // Beep beep! Made a boo-boo. + + // Let the user save the picture at least. + // Also prevents us from trying to use the dead server handle. + app->mode = NETWORK_MODE_STANDALONE; + return; + } + + // TODO: process the data +} + +static void +on_client_data (uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) +{ + (void) buf; + + app_context_t *app = stream->loop->data; + client_t *client = stream->data; + if (nread == UV_EOF || nread < 0) + { + remove_client (app, client); + return; + } + + // TODO: process the data +} + +static void +on_new_client (uv_stream_t *server, int status) +{ + app_context_t *app = server->loop->data; + if (status) + return; + + int err; + client_t *client = xcalloc (1, sizeof *client); + if ((err = uv_tcp_init (server->loop, &client->handle))) + goto free_client; + if ((err = uv_accept (server, (uv_stream_t *) &client->handle)) + || (err = uv_read_start ((uv_stream_t *) &client->handle, + app_uv_allocator, on_client_data))) + // XXX: do we need to un-accept? + goto free_handle; + + client->handle.data = client; + LIST_PREPEND (app->clients, client); + return; + +free_handle: + uv_close ((uv_handle_t *) &client->handle, NULL); + // XXX: should we unref it? +free_client: + free (client); +} + +// --- Program startup --------------------------------------------------------- + +typedef struct app_options app_options_t; +struct app_options +{ + struct addrinfo *client_address; ///< Address to connect to + struct addrinfo *server_address; ///< Address to listen at +}; + +static void +app_options_init (app_options_t *self) { - (void) app; + memset (self, 0, sizeof *self); +} +static void +app_options_free (app_options_t *self) +{ + if (self->client_address) freeaddrinfo (self->client_address); + if (self->server_address) freeaddrinfo (self->server_address); +} + +static struct addrinfo * +parse_address (const char *address, int flags) +{ + char address_copy[strlen (address) + 1]; + strcpy (address_copy, address); + + char *colon = strrchr (address_copy, ':'); + if (!colon) + { + fprintf (stderr, "error: no port number specified in `%s'\n", address); + return false; + } + + char *host = address_copy, *service = colon + 1; + + if (host == colon) + host = NULL; + else if (host < colon && *host == '[' && colon[-1] == ']') + { + // Remove IPv6 RFC 2732-style [] brackets from the host, if present. + // This also makes it possible to take the usage string literally. :)) + host++; + colon[-1] = '\0'; + } + else + *colon = '\0'; + + struct addrinfo *result, hints = + { + .ai_socktype = SOCK_STREAM, + .ai_protocol = IPPROTO_TCP, + .ai_flags = flags, + }; + int err = getaddrinfo (host, service, &hints, &result); + if (err) + { + fprintf (stderr, "error: cannot resolve `%s', port `%s': %s\n", + host, service, gai_strerror (err)); + return false; + } + return result; +} + +static void +parse_program_arguments (app_options_t *options, int argc, char **argv) +{ static const struct opt opts[] = { { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, + { 's', "server", "[ADDRESS]:PORT", 0, "start a server" }, + { 'c', "client", "[ADDRESS]:PORT", 0, "connect to a server" }, { 0, NULL, NULL, 0, NULL } }; @@ -627,12 +809,39 @@ parse_program_arguments (app_context_t *app, int argc, char **argv) case 'V': printf (PROJECT_NAME " " PROJECT_VERSION "\n"); exit (EXIT_SUCCESS); + case 's': + if (options->server_address) + { + fprintf (stderr, "%s: %s\n", + "error", "cannot specify multiple listening addresses"); + exit (EXIT_FAILURE); + } + if (!(options->server_address = parse_address (optarg, AI_PASSIVE))) + exit (EXIT_FAILURE); + break; + case 'c': + if (options->client_address) + { + fprintf (stderr, "%s: %s\n", + "error", "cannot specify multiple addresses to connect to"); + exit (EXIT_FAILURE); + } + if (!(options->client_address = parse_address (optarg, 0))) + exit (EXIT_FAILURE); + break; default: - fprintf (stderr, "%s: %s", "error", "wrong options"); + fprintf (stderr, "%s: %s\n", "error", "wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } + if (options->client_address && options->server_address) + { + fprintf (stderr, "%s: %s\n", + "error", "cannot be both a server and a client"); + exit (EXIT_FAILURE); + } + argc -= optind; argv += optind; @@ -645,6 +854,77 @@ parse_program_arguments (app_context_t *app, int argc, char **argv) opt_handler_free (&oh); } +static void +initialize_client (app_context_t *app, struct addrinfo *address) +{ + app->mode = NETWORK_MODE_CLIENT; + + int sock_fd, err; + for (; address; address = address->ai_next) + { + sock_fd = socket (address->ai_family, + address->ai_socktype, address->ai_protocol); + if (sock_fd == -1) + continue; + + char host_buf[NI_MAXHOST], serv_buf[NI_MAXSERV]; + err = getnameinfo (address->ai_addr, address->ai_addrlen, + host_buf, sizeof host_buf, serv_buf, sizeof serv_buf, + NI_NUMERICHOST | NI_NUMERICSERV); + if (err) + { + fprintf (stderr, "%s: %s: %s\n", + "error", "getnameinfo", gai_strerror (err)); + fprintf (stderr, "connecting...\n"); + } + else + { + char *x = format_host_port_pair (host_buf, serv_buf); + fprintf (stderr, "connecting to %s...\n", x); + free (x); + } + + if (!connect (sock_fd, address->ai_addr, address->ai_addrlen)) + break; + + xclose (sock_fd); + } + + if (!address) + { + fprintf (stderr, "%s: %s\n", "error", "connection failed"); + exit (EXIT_FAILURE); + } + + set_blocking (sock_fd, false); + if ((err = uv_tcp_init (uv_default_loop (), &app->server_fd)) + || (err = uv_tcp_open (&app->server_fd, sock_fd)) + || (err = uv_tcp_keepalive (&app->server_fd, true, 30)) + || (err = uv_read_start ((uv_stream_t *) &app->server_fd, + app_uv_allocator, on_server_data))) + { + fprintf (stderr, "%s: %s: %s\n", + "error", "initialization failed", uv_strerror (err)); + exit (EXIT_FAILURE); + } +} + +static void +initialize_server (app_context_t *app, struct addrinfo *address) +{ + app->mode = NETWORK_MODE_SERVER; + + int err; + if ((err = uv_tcp_init (uv_default_loop (), &app->listen_fd)) + || (err = uv_tcp_bind (&app->listen_fd, address->ai_addr, 0)) + || (err = uv_listen ((uv_stream_t *) &app->listen_fd, 10, on_new_client))) + { + fprintf (stderr, "%s: %s: %s\n", + "error", "initialization failed", uv_strerror (err)); + exit (EXIT_FAILURE); + } +} + int main (int argc, char *argv[]) { @@ -653,12 +933,25 @@ main (int argc, char *argv[]) app_context_t app; app_init (&app); - parse_program_arguments (&app, argc, argv); + + app_options_t options; + app_options_init (&options); + parse_program_arguments (&options, argc, argv); + + if (options.client_address) + initialize_client (&app, options.client_address); + else if (options.server_address) + initialize_server (&app, options.server_address); + else + app.mode = NETWORK_MODE_STANDALONE; + + app_options_free (&options); termo_t *tk = termo_new (STDIN_FILENO, NULL, 0); if (!tk) { - fprintf (stderr, "Cannot allocate termo instance\n"); + fprintf (stderr, "%s: %s\n", + "error", "cannot allocate termo instance\n"); exit (EXIT_FAILURE); } @@ -669,7 +962,7 @@ main (int argc, char *argv[]) // Set up curses for our drawing needs if (!initscr () || nonl () == ERR || curs_set (0) == ERR) { - fprintf (stderr, "Cannot initialize curses\n"); + fprintf (stderr, "%s: %s\n", "error", "cannot initialize curses"); exit (EXIT_FAILURE); } @@ -694,8 +987,8 @@ main (int argc, char *argv[]) uv_run (loop, UV_RUN_DEFAULT); endwin (); - uv_loop_close (loop); app_free (&app); + uv_loop_close (loop); return 0; } diff --git a/utils.c b/utils.c index 27d9e8d..e1580e0 100644 --- a/utils.c +++ b/utils.c @@ -34,6 +34,9 @@ #define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0])) +#define BLOCK_START do { +#define BLOCK_END } while (0) + // --- Safe memory management -------------------------------------------------- // When a memory allocation fails and we need the memory, we're usually pretty @@ -76,6 +79,31 @@ xrealloc (void *o, size_t n) return p; } +// --- Double-linked list helpers ---------------------------------------------- + +#define LIST_HEADER(type) \ + type *next; \ + type *prev; + +#define LIST_PREPEND(head, link) \ + BLOCK_START \ + (link)->prev = NULL; \ + (link)->next = (head); \ + if ((link)->next) \ + (link)->next->prev = (link); \ + (head) = (link); \ + BLOCK_END + +#define LIST_UNLINK(head, link) \ + BLOCK_START \ + if ((link)->prev) \ + (link)->prev->next = (link)->next; \ + else \ + (head) = (link)->next; \ + if ((link)->next) \ + (link)->next->prev = (link)->prev; \ + BLOCK_END + // --- Dynamically allocated strings ------------------------------------------- // Basically a string builder to abstract away manual memory management. @@ -190,6 +218,49 @@ str_append_printf (struct str *self, const char *fmt, ...) // --- Utilities --------------------------------------------------------------- +static bool +set_blocking (int fd, bool blocking) +{ + int flags = fcntl (fd, F_GETFL); + bool prev = !(flags & O_NONBLOCK); + if (blocking) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + return prev; +} + +static void +xclose (int fd) +{ + while (close (fd) == -1) + if (errno != EINTR) + break; +} + +static char *xstrdup_printf (const char *, ...) ATTRIBUTE_PRINTF (1, 2); + +static char * +xstrdup_printf (const char *format, ...) +{ + va_list ap; + struct str tmp; + str_init (&tmp); + va_start (ap, format); + str_append_vprintf (&tmp, format, ap); + va_end (ap); + return str_steal (&tmp); +} + +static char * +format_host_port_pair (const char *host, const char *port) +{ + // IPv6 addresses mess with the "colon notation"; let's go with RFC 2732 + if (strchr (host, ':')) + return xstrdup_printf ("[%s]:%s", host, port); + return xstrdup_printf ("%s:%s", host, port); +} + static bool xstrtoul (unsigned long *out, const char *s, int base) { -- cgit v1.2.3-70-g09d2