~rcr/rirc

0a771b180f0f019b3e804e0548b0068b2eff3f81 — Richard Robbins 3 years ago ff0f074
Fix broken atexit calls
M CHANGELOG => CHANGELOG +1 -0
@@ 10,6 10,7 @@ Summary of notable changes and features
### Fixes
 - fix error in sending PING
 - fix error messages on socket disconnect
 - fix fatal error on /quit
 - close/cleanup servers on :quit

## [0.1.0]

M README.md => README.md +1 -2
@@ 1,9 1,8 @@
[![Coverity Scan Build](https://scan.coverity.com/projects/4940/badge.svg)](https://scan.coverity.com/projects/4940)
![Sonarcloud](https://sonarcloud.io/api/project_badges/measure?project=rcr_rirc&metric=ncloc)
<p align="center"><img src="https://raw.githubusercontent.com/rcr/rirc/master/docs/birb.jpg" alt="birb"/></p>

# rirc
![rirc](docs/birb.jpg?raw=true "rirc")

A minimalistic irc client written in C.

While still under development, it currently supports

M TODO => TODO +32 -16
@@ 1,15 1,39 @@
FIXME:
UNHANDLED ~ 461 PING :Not enough parameters
 - add basic ssl support, with ifdef guards, see lessandro's patch
 - remove server channel from channel list? does this break searching?
   what benefit is there to this? s->channel can then just be s->clist.head
 - are join/part/quit messages working with the threshold?
 - server: channel/ulist/clist
 - posix 2001 with removal of recusive mutex?
 - fatal/error/debug macros with reusage

 - when reconnecting, if cant rejoin channel (e.g. archlinux) and bumped
   to -unregistered... closing it causes 442: not on that channel...
   what happens to messages sent to it?
	If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
	or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
	bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
	as parted, ie could potentially send messages to the channel, but won't receive any
		- or it returns 403, and cant /join the channel because it's not parted

TODO:
 - Make sure all the thread related code is using thread-safe functions,
   and that proper cancellation points are used. Make sure no resources
   can be left hanging (eg: sockets left open when a thread is canceled?)


TODO
---------------------------
next iteration
	- RFC2812 compliance
	- Split and prototype:
		src/handlers
		  \ recv
		  \ send
		  \ command
		  \ numeric
		  \ ctcp
		  \ sasl

Continue refactoring:
	- Rewrite input DLL as circular buffer of gap buffers, with copy on repeat,
		use abstractions in _draw_input
	- Error/warning writing from stateless components, e.g.:
		- mode.c should be able to call err("...") on some passed parameter
		  and have it written to the appropriate buffer with the appropriate


@@ 21,6 45,7 @@ Continue refactoring:
		  be added
	- more debug information, levels, for various events, a debug pannel as a new channel
	  type when debug flag is set
	- split action handling out

action_error
display error to user for confirmation, e.g. :connect <server>


@@ 39,13 64,14 @@ Testing:
	- Fuzzing handlers (afl)
	- Try musl C
	- include-what-you-use for header detangling
	- clang tidy, .clang-tidy config

buffer performance improvements:
	- reduce overall size, pack strings in contiguous realloc'ed array
	- cache line breaks
	- pre-format time

abstract list.h from channel/server/input lists
abstract list.h from channel/server

SASL auth:
	- cli -a/--auth


@@ 452,10 478,6 @@ show disconnected/parted in status bar for channels instead of 0 user count

Keep state of tab complete for successively getting the next nick lexicographically

Make sure all the thread related code is using thread-safe functions,
and that proper cancellation points are used. Make sure no resources
can be left hanging (eg: sockets left open when a thread is canceled?)

Empty trailing should be null, e.g.:
":nick!~user@host PART #channel :" -> "< ~ nick!~user@host has left #channel ()"



@@ 470,12 492,6 @@ parsing whatever it last parsed. Instead, a function like strsep should be imple
which correctly handles NULL input in this case
	-> replace stringtok/stringtok_r with getarg

If in ##channel that requires authentication (ie bumps you to ##channel-unauthorized
or similar), /disconnect, /connect, rirc attempts to join ##channel, can't, and is
bumped to ##channel-unauthorized, but ##channel buffer remains open and not flagged
as parted, ie could potentially send messages to the channel, but won't receive any
	- or it returns 403, and cant /join the channel because it's not parted

use the alternate screen buffer to restore screen when rirc exits

##linux ~ cannot send to channel

M src/components/server.c => src/components/server.c +8 -6
@@ 141,7 141,9 @@ server_free(struct server *s)
	// FIXME: add this back when removing it from
	// server's channel_list
	// channel_free(s->channel);

	channel_list_free(&(s->clist));
	channel_list_free(&(s->ulist));
	user_list_free(&(s->ignore));

	free((void *)s->host);


@@ 181,7 183,7 @@ server_set_004(struct server *s, char *str)

	if (user_modes) {

		DEBUG_MSG("Setting numeric 004 user_modes: %s", user_modes);
		debug("Setting numeric 004 user_modes: %s", user_modes);

		if (mode_cfg(&(s->mode_cfg), user_modes, MODE_CFG_USERMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 user_modes: %s", user_modes);


@@ 189,7 191,7 @@ server_set_004(struct server *s, char *str)

	if (chan_modes) {

		DEBUG_MSG("Setting numeric 004 chan_modes: %s", chan_modes);
		debug("Setting numeric 004 chan_modes: %s", chan_modes);

		if (mode_cfg(&(s->mode_cfg), chan_modes, MODE_CFG_CHANMODES) != MODE_ERR_NONE)
			newlinef(c, 0, "-!!-", "invalid numeric 004 chan_modes: %s", chan_modes);


@@ 350,7 352,7 @@ server_set_CASEMAPPING(struct server *s, char *val)
static int
server_set_CHANMODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 CHANMODES: %s", val);
	debug("Setting numeric 005 CHANMODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_SUBTYPES) != MODE_ERR_NONE);
}


@@ 358,7 360,7 @@ server_set_CHANMODES(struct server *s, char *val)
static int
server_set_MODES(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 MODES: %s", val);
	debug("Setting numeric 005 MODES: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_MODES) != MODE_ERR_NONE);
}


@@ 366,7 368,7 @@ server_set_MODES(struct server *s, char *val)
static int
server_set_PREFIX(struct server *s, char *val)
{
	DEBUG_MSG("Setting numeric 005 PREFIX: %s", val);
	debug("Setting numeric 005 PREFIX: %s", val);

	return (mode_cfg(&(s->mode_cfg), val, MODE_CFG_PREFIX) != MODE_ERR_NONE);
}


@@ 374,7 376,7 @@ server_set_PREFIX(struct server *s, char *val)
void
server_nick_set(struct server *s, const char *nick)
{
	DEBUG_MSG("Setting server nick: %s", nick);
	debug("Setting server nick: %s", nick);

	if (s->nick)
		free((void *)s->nick);

M src/components/server.h => src/components/server.h +2 -1
@@ 21,6 21,7 @@ struct server
	} nicks;
	struct channel *channel;
	struct channel_list clist;
	struct channel_list ulist; // TODO: seperate privmsg
	struct mode usermodes;
	struct mode_str mode_str;
	struct mode_cfg mode_cfg;


@@ 48,7 49,7 @@ struct server* server(

struct server* server_list_add(struct server_list*, struct server*);
struct server* server_list_del(struct server_list*, struct server*);
struct server* server_list_get(struct server_list*, const char *, const char*);
struct server* server_list_get(struct server_list*, const char*, const char*);

void server_set_004(struct server*, char*);
void server_set_005(struct server*, char*);

M src/io.c => src/io.c +69 -130
@@ 62,14 62,13 @@
#error "IO_RECONNECT_BACKOFF_MAX: [0, 86400]"
#endif

#define PT_CF(X) do { int ret; if ((ret = (X)) < 0) fatal("%s: %s", (#X), strerror(ret)); } while (0)
#define PT_CF(X) do { io_check_fatal((#X), (X)); } while (0)
#define PT_LK(X) PT_CF(pthread_mutex_lock((X)))
#define PT_UL(X) PT_CF(pthread_mutex_unlock((X)))
#define PT_CB(R, ...) \
	do { \
		PT_LK(&cb_mutex); \
		io_cb((R), __VA_ARGS__); \
		PT_UL(&cb_mutex); \
#define PT_CB(...) \
	do { PT_LK(&cb_mutex); \
	     io_cb(__VA_ARGS__); \
	     PT_UL(&cb_mutex); \
	} while (0)

enum io_err_t


@@ 80,13 79,12 @@ enum io_err_t
	IO_ERR_DXED,
	IO_ERR_FMT,
	IO_ERR_SEND,
	IO_ERR_TERM,
	IO_ERR_TRUNC,
};

struct io_lock
{
	pthread_cond_t  cnd;
	pthread_cond_t cnd;
	pthread_mutex_t mtx;
	volatile int predicate;
};


@@ 103,7 101,6 @@ struct connection
		IO_ST_CXNG, /* Socket connection in progress */
		IO_ST_CXED, /* Socket connected */
		IO_ST_PING, /* Socket connected, network state in question */
		IO_ST_TERM /* Terminal thread state */
	} st_c, /* current thread state */
	  st_f; /* forced thread state */
	char ip[INET6_ADDRSTRLEN];


@@ 116,6 113,7 @@ struct connection
	} read;
	struct io_lock lock;
	unsigned rx_backoff;
	pthread_t pt_tid;
};

static const char* io_strerror(struct connection*, int);


@@ 124,30 122,32 @@ static enum io_state_t io_state_cxng(struct connection*);
static enum io_state_t io_state_dxed(struct connection*);
static enum io_state_t io_state_ping(struct connection*);
static enum io_state_t io_state_rxng(struct connection*);
static void io_init(void);
static void io_init_sig(void);
static void io_init_tty(void);
static void io_lock_init(struct io_lock*);
static void io_lock_term(struct io_lock*);
static void io_check_fatal(const char*, int);
static void io_lock_wait(struct io_lock*, struct timespec*);
static void io_lock_wake(struct io_lock*);
static void io_net_set_timeout(struct connection*, unsigned);
static void io_recv(struct connection*, const char*, size_t);
static void io_sig_init(void);
static void io_soc_close(int*);
static void io_soc_shutdown(int);
static void io_state_force(struct connection*, enum io_state_t);
static void io_term(void);
static void io_term_tty(void);
static void io_tty_winsize(void);
static void io_tty_init(void);
static void io_tty_term(void);
static void io_tty_winsize(unsigned*, unsigned*);
static void* io_thread(void*);

static pthread_mutex_t cb_mutex;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static int io_running;
static pthread_mutex_t cb_mutex = PTHREAD_MUTEX_INITIALIZER;
static struct termios term;
static struct winsize tty_ws;
static volatile sig_atomic_t flag_sigwinch_cb; /* sigwinch callback */
static volatile sig_atomic_t flag_tty_resized; /* sigwinch ws resize */

static void
io_check_fatal(const char *f, int ret)
{
	if (ret < 0)
		fatal("%s: %s", f, strerror(ret));
}

static const char*
io_strerror(struct connection *c, int errnum)
{


@@ 173,56 173,6 @@ io_soc_shutdown(int soc)
}

static void
io_init(void)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_ERRORCHECK));
	PT_CF(pthread_mutex_init(&cb_mutex, &m_attr));
	PT_CF(pthread_mutexattr_destroy(&m_attr));

	/* atexit doesn't set errno */
	if (atexit(io_term) != 0)
		fatal("atexit");
}

static void
io_term(void)
{
	int ret;

	if ((ret = pthread_mutex_trylock(&cb_mutex)) < 0 && ret != EBUSY)
		fatal("pthread_mutex_trylock: %s", strerror(ret));

	PT_UL(&cb_mutex);
	PT_CF(pthread_mutex_destroy(&cb_mutex));
}

static void
io_lock_init(struct io_lock *lock)
{
	pthread_mutexattr_t m_attr;

	PT_CF(pthread_mutexattr_init(&m_attr));
	PT_CF(pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE));

	PT_CF(pthread_cond_init(&(lock->cnd), NULL));
	PT_CF(pthread_mutex_init(&(lock->mtx), &m_attr));

	PT_CF(pthread_mutexattr_destroy(&m_attr));

	lock->predicate = 0;
}

static void
io_lock_term(struct io_lock *lock)
{
	PT_CF(pthread_cond_destroy(&(lock->cnd)));
	PT_CF(pthread_mutex_destroy(&(lock->mtx)));
}

static void
io_lock_wait(struct io_lock *lock, struct timespec *timeout)
{
	PT_LK(&(lock->mtx));


@@ 245,15 195,6 @@ io_lock_wait(struct io_lock *lock, struct timespec *timeout)
	PT_UL(&(lock->mtx));
}

static void
io_lock_wake(struct io_lock *lock)
{
	PT_LK(&(lock->mtx));
	lock->predicate = 1;
	PT_CF(pthread_cond_signal(&(lock->cnd)));
	PT_UL(&(lock->mtx));
}

struct connection*
connection(const void *obj, const char *host, const char *port)
{


@@ 267,17 208,9 @@ connection(const void *obj, const char *host, const char *port)
	c->port = strdup(port);
	c->st_c = IO_ST_DXED;
	c->st_f = IO_ST_INVALID;
	io_lock_init(&(c->lock));

	PT_CF(pthread_once(&init_once, io_init));

	pthread_attr_t pt_attr;
	pthread_t pt_tid;

	PT_CF(pthread_attr_init(&pt_attr));
	PT_CF(pthread_attr_setdetachstate(&pt_attr, PTHREAD_CREATE_DETACHED));
	PT_CF(pthread_create(&pt_tid, &pt_attr, io_thread, c));
	PT_CF(pthread_attr_destroy(&pt_attr));
	PT_CF(pthread_cond_init(&(c->lock.cnd), NULL));
	PT_CF(pthread_mutex_init(&(c->lock.mtx), NULL));
	PT_CF(pthread_create(&c->pt_tid, NULL, io_thread, c));

	return c;
}


@@ 305,7 238,7 @@ io_sendf(struct connection *c, const char *fmt, ...)
	if (len >= sizeof(sendbuf) - 2)
		return IO_ERR_TRUNC;

	DEBUG_MSG("send: (%zu) %s", len, sendbuf);
	debug("send: (%zu) %s", len, sendbuf);

	sendbuf[len++] = '\r';
	sendbuf[len++] = '\n';


@@ 329,7 262,6 @@ io_cx(struct connection *c)
		case IO_ST_CXNG: err = IO_ERR_CXNG; break;
		case IO_ST_CXED: err = IO_ERR_CXED; break;
		case IO_ST_PING: err = IO_ERR_CXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_CXNG);
	}


@@ 350,7 282,6 @@ io_dx(struct connection *c)

	switch (c->st_c) {
		case IO_ST_DXED: err = IO_ERR_DXED; break;
		case IO_ST_TERM: err = IO_ERR_TERM; break;
		default:
			io_state_force(c, IO_ST_DXED);
	}


@@ 363,11 294,16 @@ io_dx(struct connection *c)
void
io_free(struct connection *c)
{
	/* Force a socket thread into the IO_ST_TERM state */
	pthread_t pt_tid = c->pt_tid;

	PT_LK(&(c->lock.mtx));
	io_state_force(c, IO_ST_TERM);
	PT_UL(&(c->lock.mtx));
	PT_CF(pthread_cancel(pt_tid));
	PT_CF(pthread_join(pt_tid, NULL));
	PT_CF(pthread_cond_destroy(&(c->lock.cnd)));
	PT_CF(pthread_mutex_destroy(&(c->lock.mtx)));
	io_soc_close(&(c->soc));
	free((void*)c->host);
	free((void*)c->port);
	free(c);
}

static void


@@ 380,15 316,14 @@ io_state_force(struct connection *c, enum io_state_t st_f)
	switch (c->st_c) {
		case IO_ST_DXED: /* io_lock_wait() */
		case IO_ST_RXNG: /* io_lock_wait() */
			io_lock_wake(&(c->lock));
			c->lock.predicate = 1;
			PT_CF(pthread_cond_signal(&(c->lock.cnd)));
			break;
		case IO_ST_CXNG: /* connect() */
		case IO_ST_CXED: /* recv() */
		case IO_ST_PING: /* recv() */
			io_soc_shutdown(c->soc);
			break;
		case IO_ST_TERM:
			break;
		default:
			fatal("Unknown net state: %d", c->st_c);
	}


@@ 437,6 372,7 @@ io_state_cxng(struct connection *c)
	/* TODO: mutex should protect access to c->soc, else race condition
	 *       when the main thread tries to shutdown() for cancel */
	/* TODO: how to cancel getaddrinfo/getnameinfo? */
	/* FIXME: addrinfo leak if canceled during connection */

	int ret, soc = -1;



@@ 592,9 528,6 @@ io_thread(void *arg)

		PT_UL(&(c->lock.mtx));

		if (c->st_c == IO_ST_TERM)
			break;

		if (st_f == IO_ST_PING && st_t == IO_ST_CXED)
			PT_CB(IO_CB_PING_0, c->obj, 0);



@@ 608,14 541,6 @@ io_thread(void *arg)
			c->rx_backoff = 0;
	}

	io_soc_shutdown(c->soc);
	io_soc_close(&(c->soc));
	io_lock_term(&(c->lock));

	free((void*)c->host);
	free((void*)c->port);
	free(c);

	return NULL;
}



@@ 632,7 557,7 @@ io_recv(struct connection *c, const char *buf, size_t n)

			c->read.buf[ci] = 0;

			DEBUG_MSG("recv: (%zu) %s", ci, c->read.buf);
			debug("recv: (%zu) %s", ci, c->read.buf);

			PT_LK(&cb_mutex);
			io_cb_read_soc(c->read.buf, ci, c->obj);


@@ 669,7 594,7 @@ sigaction_sigwinch(int sig)
}

static void
io_init_sig(void)
io_sig_init(void)
{
	struct sigaction sa;



@@ 682,7 607,7 @@ io_init_sig(void)
}

static void
io_init_tty(void)
io_tty_init(void)
{
	struct termios nterm;



@@ 700,27 625,29 @@ io_init_tty(void)
	if (tcsetattr(STDIN_FILENO, TCSANOW, &nterm) < 0)
		fatal("tcsetattr: %s", strerror(errno));

	/* atexit doesn't set errno */
	if (atexit(io_term_tty) < 0)
	if (atexit(io_tty_term) != 0)
		fatal("atexit");
}

static void
io_term_tty(void)
io_tty_term(void)
{
	/* Exit handler, must return normally */

	if (tcsetattr(STDIN_FILENO, TCSADRAIN, &term) < 0)
		fatal("tcsetattr: %s", strerror(errno));
		fatal_noexit("tcsetattr: %s", strerror(errno));
}

void
io_loop(void)
io_init(void)
{
	PT_CF(pthread_once(&init_once, io_init));
	io_sig_init();
	io_tty_init();

	io_init_sig();
	io_init_tty();
	io_running = 1;

	while (io_running) {

	for (;;) {
		char buf[128];
		ssize_t ret = read(STDIN_FILENO, buf, sizeof(buf));



@@ 743,29 670,42 @@ io_loop(void)
	}
}

void
io_term(void)
{
	io_running = 0;
}

static void
io_tty_winsize(void)
io_tty_winsize(unsigned *rows, unsigned *cols)
{
	static struct winsize tty_ws;

	if (flag_tty_resized == 0) {
		flag_tty_resized = 1;

		if (ioctl(0, TIOCGWINSZ, &tty_ws) < 0)
			fatal("ioctl: %s", strerror(errno));
	}

	*rows = tty_ws.ws_row;
	*cols = tty_ws.ws_col;
}

unsigned
io_tty_cols(void)
{
	io_tty_winsize();
	return tty_ws.ws_col;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return cols;
}

unsigned
io_tty_rows(void)
{
	io_tty_winsize();
	return tty_ws.ws_row;
	unsigned rows, cols;
	io_tty_winsize(&rows, &cols);
	return rows;
}

const char*


@@ 778,7 718,6 @@ io_err(int err)
		case IO_ERR_DXED:  return "socket not connected";
		case IO_ERR_FMT:   return "failed to format message";
		case IO_ERR_SEND:  return "failed to send message";
		case IO_ERR_TERM:  return "thread is terminating";
		case IO_ERR_TRUNC: return "data truncated";
		default:
			return "unknown error";

M src/io.h => src/io.h +13 -12
@@ 15,16 15,16 @@
 *                            +--------+
 *                 +----(B)-- |  rxng  |
 *                 |          +--------+
 *  INIT           |           |      ^
 *    v            |         (A,C)    |
 *    |            |           |     (E)
 *                 |           |      ^
 *   INIT          |         (A,C)    |
 *    v            |           |     (E)
 *    |            v           v      |
 *    +--> +--------+ --(A)-> +--------+
 *         |  dxed  |         |  cxng  | <--+
 *    +--< +--------+ <-(B)-- +--------+    |
 *    |     ^      ^           |      ^    (F)
 *    v     |      |          (D)     |     |
 *  TERM    |      |           |     (F)    |
 *    |    +--------+ --(A)-> +--------+
 *    +--> |  dxed  |         |  cxng  | <--+
 *         +--------+ <-(B)-- +--------+    |
 *          ^      ^           |      ^    (F)
 *          |      |          (D)     |     |
 *          |      |           |     (F)    |
 *          |      |           v      |     |
 *          |      |          +--------+    |
 *          |      +----(B)-- |  cxed  |    |


@@ 69,7 69,7 @@
 *   t(n) = t(n - 1) * factor
 *   t(0) = base
 *
 * Calling io_loop starts the io context and never returns
 * Calling io_init starts the io context and doesn't return until io_term
 */

#define IO_MAX_CONNECTIONS 8


@@ 119,8 119,9 @@ void io_cb(enum io_cb_t, const void*, ...);
void io_cb_read_inp(char*, size_t);
void io_cb_read_soc(char*, size_t, const void*);

/* Start non-returning IO context */
void io_loop(void);
/* Start/stop IO context */
void io_init(void);
void io_term(void);

/* Get tty dimensions */
unsigned io_tty_cols(void);

M src/rirc.c => src/rirc.c +20 -13
@@ 13,14 13,13 @@
#define arg_error(...) \
	do { fprintf(stderr, "%s: ", runtime_name); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	     fprintf(stderr, "%s --help for usage\n", runtime_name); \
	     exit(EXIT_FAILURE); \
	     fprintf(stderr, "\n%s --help for usage\n", runtime_name); \
	     return 1; \
	} while (0)

static const char* opt_arg_str(char);
static const char* getpwuid_pw_name(void);
static void parse_args(int, char**);
static int parse_args(int, char**);

#ifndef DEBUG
const char *runtime_name = "rirc";


@@ 103,7 102,7 @@ getpwuid_pw_name(void)
	return passwd->pw_name;
}

static void
static int
parse_args(int argc, char **argv)
{
	int opt_c = 0,


@@ 126,8 125,8 @@ parse_args(int argc, char **argv)
		{"chans",    required_argument, 0, 'c'},
		{"username", required_argument, 0, 'u'},
		{"realname", required_argument, 0, 'r'},
		{"version",  no_argument,       0, 'v'},
		{"help",     no_argument,       0, 'h'},
		{"version",  no_argument,       0, 'v'},
		{0, 0, 0, 0}
	};



@@ 143,7 142,7 @@ parse_args(int argc, char **argv)
	} cli_servers[IO_MAX_CONNECTIONS];

	/* FIXME: getopt_long is a GNU extension */
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:vh", long_opts, &opt_i))) {
	while (0 < (opt_c = getopt_long(argc, argv, ":s:p:w:n:c:r:u:hv", long_opts, &opt_i))) {

		switch (opt_c) {



@@ 202,14 201,14 @@ parse_args(int argc, char **argv)

			#undef CHECK_SERVER_OPTARG

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case 'h':
				puts(rirc_usage);
				exit(EXIT_SUCCESS);

			case 'v':
				puts(rirc_version);
				exit(EXIT_SUCCESS);

			case '?':
				arg_error("unknown options '%s'", argv[optind - 1]);



@@ 263,11 262,19 @@ parse_args(int argc, char **argv)

	for (size_t i = 0; i < n_servers; i++)
		io_cx(cli_servers[i].s->connection);

	return 0;
}

int
main(int argc, char **argv)
{
	parse_args(argc, argv);
	io_loop();
	int ret;

	if ((ret = parse_args(argc, argv)) == 0)
		io_init();

	state_term();

	return ret;
}

M src/state.c => src/state.c +18 -19
@@ 20,7 20,6 @@
// See: https://vt100.net/docs/vt100-ug/chapter3.html
#define CTRL(k) ((k) & 0x1f)

static void state_term(void);
static void _newline(struct channel*, enum buffer_line_t, const char*, const char*, va_list);
static void state_io_cxed(struct server*);
static void state_io_dxed(struct server*, va_list);


@@ 91,10 90,6 @@ redraw(void)
void
state_init(void)
{
	/* atexit doesn't set errno */
	if (atexit(state_term) != 0)
		fatal("atexit");

	state.default_channel = state.current_channel = new_channel("rirc", NULL, CHANNEL_T_OTHER);

	/* Splashscreen */


@@ 115,7 110,7 @@ state_init(void)
	redraw();
}

static void
void
state_term(void)
{
	/* Exit handler; must return normally */


@@ 124,6 119,13 @@ state_term(void)

	channel_free(state.default_channel);

	/* Reset terminal colours */
	printf("\x1b[38;0;m");
	printf("\x1b[48;0;m");

	/* Clear screen */
	printf("\x1b[H\x1b[J");

	if ((s1 = state_server_list()->head) == NULL)
		return;



@@ 133,16 135,6 @@ state_term(void)
		io_free(s2->connection);
		server_free(s2);
	} while (s1 != state_server_list()->head);

	/* Reset terminal colours */
	printf("\x1b[38;0;m");
	printf("\x1b[48;0;m");

#ifndef DEBUG
	/* Clear screen */
	if (!fatal_exit)
		printf("\x1b[H\x1b[J");
#endif
}

void


@@ 598,8 590,12 @@ buffer_scrollback_forw(struct channel *c)
	draw_status();
}

/* Usefull server/channel structure abstractions for drawing */

/* FIXME:
 *  - These abstractions should take into account the new component hierarchy
 *    and have the backwards pointer from channel to server removed, in favour
 *    of passing a current_server() to handlers
 *  - The server's channel should not be part of the server's channel_list
 */
struct channel*
channel_get_first(void)
{


@@ 833,7 829,7 @@ send_cmnd(struct channel *c, char *buf)
	}

	if (!strcasecmp(cmnd, "quit")) {
		exit(EXIT_SUCCESS);
		io_term();
	}

	if (!strcasecmp(cmnd, "connect")) {


@@ 865,7 861,10 @@ send_cmnd(struct channel *c, char *buf)

				s = server(host, port, pass, user, real);
				s->connection = connection(s, host, port);

				// TODO: here just get/set the connection object
				server_list_add(state_server_list(), s);

				channel_set_current(s->channel);
				io_cx(s->connection);
				draw_all();

M src/state.h => src/state.h +1 -0
@@ 22,6 22,7 @@ struct channel* current_channel(void);
struct server_list* state_server_list(void);

void state_init(void);
void state_term(void);

// TODO: most of this stuff can be static
//TODO: move to channel.c, function of server's channel list

M src/utils/utils.c => src/utils/utils.c +0 -2
@@ 36,8 36,6 @@

static inline int irc_toupper(int);

int fatal_exit;

char*
getarg(char **str, const char *sep)
{

M src/utils/utils.h => src/utils/utils.h +11 -16
@@ 15,26 15,23 @@

#define UNUSED(X) ((void)(X))

#define message(TYPE, ...) \
	fprintf(stderr, "%s %s:%d:%s ", (TYPE), __FILE__, __LINE__, __func__); \
	fprintf(stderr, __VA_ARGS__); \
	fprintf(stderr, "\n");

#if (defined DEBUG) && !(defined TESTING)
#define DEBUG_MSG(...) \
	do { fprintf(stderr, "%s:%d:%-12s\t", __FILE__, __LINE__, __func__); \
	     fprintf(stderr,  __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	} while (0)
#define debug(...) \
	do { message("DEBUG", __VA_ARGS__); } while (0)
#else
#define DEBUG_MSG(...)
#define debug(...)
#endif

/* Irrecoverable error
 *   precluded in test.h to aggregate fatal errors in testcases */
#ifndef fatal
#define fatal(...) \
	do { fprintf(stderr, "FATAL ERROR: %s:%d:%s: ", __FILE__, __LINE__, __func__); \
	     fprintf(stderr, __VA_ARGS__); \
	     fprintf(stderr, "\n"); \
	     fatal_exit = 1; \
	     exit(EXIT_FAILURE); \
	} while (0)
	do { message("FATAL", __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#define fatal_noexit(...) \
	do { message("FATAL", __VA_ARGS__); } while (0)
#endif

/* FIXME: don't seperate trailing from params


@@ 69,6 66,4 @@ int check_pinged(const char*, const char*);
int parse_mesg(struct parsed_mesg*, char*);
int skip_sp(char**);

extern int fatal_exit;

#endif

M test/io.c => test/io.c +0 -2
@@ 80,8 80,6 @@ test_io_recv(void)
int
main(void)
{
	PT_CF(pthread_once(&init_once, io_init));

	testcase tests[] = {
		TESTCASE(test_io_recv),
	};

M test/test.h => test/test.h +3 -1
@@ 80,7 80,7 @@ static void _print_testcase_name_(const char*);
#ifdef fatal
#error "test.h" should be the first include within testcase files
#else
#define fatal(...) \
#define _fatal(...) \
	do { \
		if (_assert_fatal_) { \
			longjmp(_tc_fatal_expected_, 1); \


@@ 90,6 90,8 @@ static void _print_testcase_name_(const char*);
			longjmp(_tc_fatal_unexpected_, 1); \
		} \
	} while (0)
#define fatal        _fatal
#define fatal_noexit _fatal
#endif

#define assert_fatal(X) \