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