Lay down some server-client foundations
This commit is contained in:
parent
3ea5918fee
commit
29bec0c0e0
309
autistdraw.c
309
autistdraw.c
|
@ -39,6 +39,22 @@
|
||||||
|
|
||||||
#define BITMAP_BLOCK_SIZE 50 ///< Step for extending bitmap size
|
#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;
|
typedef struct app_context app_context_t;
|
||||||
struct app_context
|
struct app_context
|
||||||
{
|
{
|
||||||
|
@ -49,6 +65,16 @@ struct app_context
|
||||||
uv_timer_t tty_timer; ///< TTY timeout timer
|
uv_timer_t tty_timer; ///< TTY timeout timer
|
||||||
uv_signal_t winch_watcher; ///< SIGWINCH watcher
|
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
|
chtype palette[2 * 9]; ///< Attribute palette
|
||||||
|
|
||||||
uint8_t *bitmap; ///< Canvas data for drawing
|
uint8_t *bitmap; ///< Canvas data for drawing
|
||||||
|
@ -78,11 +104,23 @@ app_init (app_context_t *self)
|
||||||
memset (self, 0, sizeof *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
|
static void
|
||||||
app_free (app_context_t *self)
|
app_free (app_context_t *self)
|
||||||
{
|
{
|
||||||
if (self->tk)
|
if (self->tk)
|
||||||
termo_destroy (self->tk);
|
termo_destroy (self->tk);
|
||||||
|
while (self->clients)
|
||||||
|
remove_client (self, self->clients);
|
||||||
|
|
||||||
free (self->bitmap);
|
free (self->bitmap);
|
||||||
}
|
}
|
||||||
|
@ -465,7 +503,7 @@ export_irc (app_context_t *app)
|
||||||
fclose (fp);
|
fclose (fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// --- Event handlers ----------------------------------------------------------
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
on_key (app_context_t *app, termo_key_t *key)
|
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
|
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)
|
||||||
{
|
{
|
||||||
(void) app;
|
// 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)
|
||||||
|
{
|
||||||
|
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[] =
|
static const struct opt opts[] =
|
||||||
{
|
{
|
||||||
{ 'h', "help", NULL, 0, "display this help and exit" },
|
{ 'h', "help", NULL, 0, "display this help and exit" },
|
||||||
{ 'V', "version", NULL, 0, "output version information 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 }
|
{ 0, NULL, NULL, 0, NULL }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -627,12 +809,39 @@ parse_program_arguments (app_context_t *app, int argc, char **argv)
|
||||||
case 'V':
|
case 'V':
|
||||||
printf (PROJECT_NAME " " PROJECT_VERSION "\n");
|
printf (PROJECT_NAME " " PROJECT_VERSION "\n");
|
||||||
exit (EXIT_SUCCESS);
|
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:
|
default:
|
||||||
fprintf (stderr, "%s: %s", "error", "wrong options");
|
fprintf (stderr, "%s: %s\n", "error", "wrong options");
|
||||||
opt_handler_usage (&oh, stderr);
|
opt_handler_usage (&oh, stderr);
|
||||||
exit (EXIT_FAILURE);
|
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;
|
argc -= optind;
|
||||||
argv += optind;
|
argv += optind;
|
||||||
|
|
||||||
|
@ -645,6 +854,77 @@ parse_program_arguments (app_context_t *app, int argc, char **argv)
|
||||||
opt_handler_free (&oh);
|
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
|
int
|
||||||
main (int argc, char *argv[])
|
main (int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
@ -653,12 +933,25 @@ main (int argc, char *argv[])
|
||||||
|
|
||||||
app_context_t app;
|
app_context_t app;
|
||||||
app_init (&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);
|
termo_t *tk = termo_new (STDIN_FILENO, NULL, 0);
|
||||||
if (!tk)
|
if (!tk)
|
||||||
{
|
{
|
||||||
fprintf (stderr, "Cannot allocate termo instance\n");
|
fprintf (stderr, "%s: %s\n",
|
||||||
|
"error", "cannot allocate termo instance\n");
|
||||||
exit (EXIT_FAILURE);
|
exit (EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -669,7 +962,7 @@ main (int argc, char *argv[])
|
||||||
// Set up curses for our drawing needs
|
// Set up curses for our drawing needs
|
||||||
if (!initscr () || nonl () == ERR || curs_set (0) == ERR)
|
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);
|
exit (EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,8 +987,8 @@ main (int argc, char *argv[])
|
||||||
uv_run (loop, UV_RUN_DEFAULT);
|
uv_run (loop, UV_RUN_DEFAULT);
|
||||||
endwin ();
|
endwin ();
|
||||||
|
|
||||||
uv_loop_close (loop);
|
|
||||||
app_free (&app);
|
app_free (&app);
|
||||||
|
uv_loop_close (loop);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
utils.c
71
utils.c
|
@ -34,6 +34,9 @@
|
||||||
|
|
||||||
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
|
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
|
||||||
|
|
||||||
|
#define BLOCK_START do {
|
||||||
|
#define BLOCK_END } while (0)
|
||||||
|
|
||||||
// --- Safe memory management --------------------------------------------------
|
// --- Safe memory management --------------------------------------------------
|
||||||
|
|
||||||
// When a memory allocation fails and we need the memory, we're usually pretty
|
// 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;
|
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 -------------------------------------------
|
// --- Dynamically allocated strings -------------------------------------------
|
||||||
|
|
||||||
// Basically a string builder to abstract away manual memory management.
|
// Basically a string builder to abstract away manual memory management.
|
||||||
|
@ -190,6 +218,49 @@ str_append_printf (struct str *self, const char *fmt, ...)
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
// --- 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
|
static bool
|
||||||
xstrtoul (unsigned long *out, const char *s, int base)
|
xstrtoul (unsigned long *out, const char *s, int base)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue