wm: ticl

ref: 99aaa335c1a504b5210fd9e354a49749cec5c88b
dir: /main.c/

View raw version
/*
 * This work is dedicated to the public domain.
 * See COPYING file for more information.
 */

#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <unistd.h>

#include "htable.h"
#include "util.c"

#define BUFFER_LEN	1024
#define MAX_NICK_LEN	16

#define FD_ADDEND	100
#define POLLFD_ADDEND	100
#define NET_ADDEND	10
#define USER_ADDEND	100

#define FIFO_FD		1
#define LINKER_FD	2
#define CLONE_FD	3

typedef struct {
	int	 fd;	/* fd of linker	*/
	int	 join;	/* is joined	*/
	char	*name;	/* name		*/
	char	*symb;	/* symbol	*/
	char	*host;	/* hostname	*/
	char	*port;	/* port		*/
	char	*chan;	/* channel	*/
} Network;

/* user defined data for each fd */
struct fdData {
	int	netid;		/* net index		*/
	int	type;		/* fd type		*/
	int	suffix;		/* fd suffix count	*/
};

static struct pollfd	*pfdset;	/* pollfd set		*/
static struct fdData	*fddata;	/* fd user data set	*/
static int		 pfdlen;	/* pollfd length	*/
static int		 pfdcap;	/* pollfd capacity	*/
static int		*fdtoid;	/* fd to pfdset index	*/
static int		 fdscap;	/* fds capacity		*/

static Network	*networks;	/* linked list of networks	*/
static int	 netlen;	/* current network length	*/
static int	 netcap;	/* total memory allocated	*/
static Htable	*users;		/* users-clones hash table	*/

char		msg[BUFFER_LEN];
static int	isrunning = 1;

/* functions prototype */
void	 handle_fifo_input(int, char *);
void	 handle_server_output(int);
void	 net_add(char *, char *, char *, char *, char *);
void	 net_del(char *);
void	 net_del_raw(int);
void	 net_update(int);
void	 user_add(char *, int);
void	 user_del(char *, char *);
int	*user_fds(char *, int);
int	 clone_add(int, char *);
void	 fd_add(int, int, int, int);
void	 fd_del(int);
void   	 nick_add_symb(char *, int);
void   	 privmsg_update(char *, char *, int);
void   	 terminate(int);
void   	 print_table(void);
void   	 print_htable(void);
void   	 print_users(void);
void	 print_border(void);

int
main(int argc, char *argv[])
{
	int i, n;

	/* set stdout to unbufferd */
	setvbuf(stdout, NULL, _IONBF, 0);

	/* check arguments */
	if (argc != 2) {
		printf("usage: %s <fifo>\n", argv[0]);
		return 1;
	}

	/* init global variables */
	pfdcap = POLLFD_ADDEND;
	pfdset = ecalloc((size_t)pfdcap, sizeof(struct pollfd));
	fddata = ecalloc((size_t)pfdcap, sizeof(struct fdData));
	fdscap = FD_ADDEND;
	fdtoid = ecalloc((size_t)fdscap, sizeof(int));
	netcap = NET_ADDEND;
	networks = ecalloc((size_t)netcap, sizeof(Network));
	/*
	 * hash table of users
	 * key   -> <nickname> + '[' + <network_symbol> + ']'
	 * value -> array of linked clones fds indexed
	 *	    corresponding to its connected network
	 */
	users = htcreate((KeyLenFn *)strlen, (KeyCmpFn *)strcmp, free, free, USER_ADDEND);

	/* add fifo_fd */
	fd_add(fifo_open(argv[1]), 0, FIFO_FD, 0);

	/* select loop */
	while (isrunning) {
		if ((n = poll(pfdset, (nfds_t)pfdlen, -1)) == -1) {
			printf("error: poll: %s\n", strerror(errno));
			terminate(1);
		}
		for (i = 0; i < pfdlen; i++) {
			if (!(pfdset[i].revents & (POLLIN|POLLHUP)))
				continue;
			/* printf("poll: %d\n", pfdset[i].fd); */
			if (fddata[i].type == FIFO_FD)
				handle_fifo_input(i, argv[1]);
			else
				handle_server_output(i);
			/*
			 * handle one ready fd at one time because if we
			 * close upcoming ready fd, it cause infinite loop.
			 */
			break;
		}
	}
	terminate(0);
	return 0;
}

void
handle_fifo_input(int id, char *path)
{
	char buffer[BUFFER_LEN];
	char *buf;
	char *cmd;
	ssize_t n;

	/* if failed to read data */
	if ((n = readline(pfdset[id].fd, buffer, sizeof(buffer))) < 1) {
		if (n == 0) {	/* restart again */
			fd_del(pfdset[id].fd);
			fd_add(fifo_open(path), 0, FIFO_FD, 0);
		} else if ((errno != EAGAIN) && (errno != EINTR)) {
			printf("error: read: %s\n", strerror(errno));
		}
		return;
	}
	/* printf("fifo: %s\n", buffer); */

	buf = buffer;
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "netadd") == 0) {
		char *name = split(&buf, ' ');
		char *symb = split(&buf, ' ');
		char *host = split(&buf, ' ');
		char *port = split(&buf, ' ');
		char *chan = buf;
		if (!*name || !*symb || !*host || !*port || !*chan)
			printf("usage: netadd <name> <symbol> <hostname> <port> <channel>\n");
		else
			net_add(name, symb, host, port, chan);
	} else if (strcmp(cmd, "netdel") == 0) {
		char *name = buf;
		if (!*name)
			printf("usage: netdel <name>\n");
		else
			net_del(name);
	} else if (strcmp(cmd, "print") == 0) {
		print_table();
	} else if (strcmp(cmd, "htable") == 0) {
		print_htable();
	} else if (strcmp(cmd, "users") == 0) {
		print_users();
	} else if (strcmp(cmd, "exit") == 0) {
		isrunning = 0;
	} else {
		printf("error: %s is not a command\n", cmd);
	}
}

void
handle_server_output(int id)
{
	char buffer	[BUFFER_LEN];
	char backup	[BUFFER_LEN];
	char linker_nick [MAX_NICK_LEN];
	char *buf;
	char *cmd;
	char *nick;
	int i, netid;
	ssize_t n;

	/* if failed to read data */
	if ((n = readline(pfdset[id].fd, buffer, sizeof(buffer))) < 1) {
		if (n == 0) {
			printf("error: remote host closed connection: %s\n", strerror(errno));
			terminate(1);
			/* fd_del(pfdset[id].fd); */
		} else if ((errno != EAGAIN) && (errno != EINTR)) {
			printf("error: read: %s\n", strerror(errno));
			terminate(1);
		}
		return;
	}

	/* remove CRLFs */
	for (i = 0; i < (int)strlen(buffer); i++) {
		if (buffer[i] == '\r' || buffer[i] == '\n') {
			buffer[i] = '\0';
			break;
		}
	}

	/* clone the buffer */
	strcpy(backup, buffer);
	buf = buffer;

	netid = fddata[id].netid;
	/* set linker nick */
	strcpy(linker_nick, "linker");
	for (i = 0; i < fddata[id].suffix; i++)
		strcat(linker_nick, "_");

	/* first column */
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "NOTICE") == 0) {
		return;
	} else if (strcmp(cmd, "ERROR") == 0) {
		goto printbuffer;
	} else if (strcmp(cmd, "PING") == 0) {
		snprintf(msg, sizeof(msg), "PONG %s\r\n", buf);
		writeall(pfdset[id].fd, msg);
		return;
	}
	/* strip nick from first column */
	nick = split(&cmd, '!');
	if (nick[0] == ':')
		nick++;

	/* second column */
	cmd = split(&buf, ' ');

	/* ignore all the info messages */
	if ((strcmp(cmd, "002") == 0)
	|| (strcmp(cmd, "003") == 0)
	|| (strcmp(cmd, "004") == 0)
	|| (strcmp(cmd, "005") == 0)
	|| (strcmp(cmd, "003") == 0)
	|| (strcmp(cmd, "251") == 0)
	|| (strcmp(cmd, "252") == 0)
	|| (strcmp(cmd, "253") == 0)	/* unknown connection(s) */
	|| (strcmp(cmd, "254") == 0)
	|| (strcmp(cmd, "255") == 0)
	|| (strcmp(cmd, "265") == 0)
	|| (strcmp(cmd, "266") == 0)
	|| (strcmp(cmd, "250") == 0)
	|| (strcmp(cmd, "375") == 0)
	|| (strcmp(cmd, "372") == 0)
	|| (strcmp(cmd, "376") == 0)
	|| (strcmp(cmd, "396") == 0)
	|| (strcmp(cmd, "366") == 0)
	|| (strcmp(cmd, "MODE") == 0)
	|| (strcmp(cmd, "NOTICE") == 0)) {
		return;
	} else if (strcmp(cmd, "433") == 0) {	/* Nickname already in use */
		split(&buf, ' ');
		nick = split(&buf, ' ');
		if (strlen(nick)+1 > MAX_NICK_LEN) {
			printf("error: cannot append suffix, nick '%s' is too big\n", nick);
			if (strcmp(nick, linker_nick) == 0) {
				net_del(networks[netid].name);
			} else {
				snprintf(msg, sizeof(msg), "QUIT :nick is too big\r\n");
				user_del(nick, msg);
			}
		} else {
			strcat(nick, "_");
			fddata[id].suffix++;
			snprintf(msg, sizeof(msg), "NICK %s\r\n", nick);
			writeall(pfdset[id].fd, msg);
		}
		return;
	} else if (strcmp(cmd, "001") == 0) {
		snprintf(msg, sizeof(msg), "JOIN %s\r\n", networks[netid].chan);
		writeall(pfdset[id].fd, msg);
		return;
	} else if (strcmp(cmd, "PRIVMSG") == 0) {
		char privmsg[BUFFER_LEN] = "";
		int *fds;

		if (fddata[id].type == LINKER_FD) {
			nick_add_symb(nick, netid);
			if ((fds = htsearch(users, nick)) == NULL)
				return;

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(privmsg, buf, netid);
			for (i = 0; i < netlen; i++) {
				if (fds[i] > 0) {
					snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", networks[i].chan, privmsg);
					writeall(fds[i], msg);
				}
			}
		} else {
			char *netsymb;
			char *user = split(&buf, ' ');

			/* ignore messages from channel (it is handled by linker) */
			if ((user[0] == '#') || (user[0] == '&'))
				return;

			nick_add_symb(nick, netid);
			if ((fds = htsearch(users, nick)) == NULL)
				return;

			/* split user nick and network symbol */
			*strrchr(user, ']') = '\0';
			netsymb = strrchr(user, '[');
			*netsymb++ = '\0';

			/* get the network index */
			for (i = 0; i < netlen; i++) {
				if (strcmp(netsymb, networks[i].symb) == 0)
					break;
			}

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(privmsg, buf, netid);
			snprintf(msg, sizeof(msg), "PRIVMSG %s :%s\r\n", user, privmsg);
			writeall(fds[i], msg);
		}
		return;
	}
	/* these messages are handled by linker */
	if (fddata[id].type == CLONE_FD) {
		if ((strcmp(cmd, "353") == 0)
		|| (strcmp(cmd, "JOIN") == 0)
		|| (strcmp(cmd, "QUIT") == 0)
		|| (strcmp(cmd, "PART") == 0)
		|| (strcmp(cmd, "KICK") == 0)
		|| (strcmp(cmd, "NICK") == 0))
			return;
	} else if (strcmp(cmd, "353") == 0) {
		char *nick;
		split(&buf, ':');
		networks[netid].join = 1;
		net_update(netid);
		while (*(nick = split(&buf, ' ')) != '\0') {
			if (*nick == '@'
			|| *nick == '&'
			|| *nick == '~'
			|| *nick == '%'
			|| *nick == '+'
			|| *nick == '\\')
				nick++;
			if (strcmp(nick, linker_nick) != 0)
				user_add(nick, netid);
		}
		return;
	} else if (strcmp(cmd, "JOIN") == 0) {
		if ((strcmp(nick, linker_nick) != 0)
		&& (user_fds(nick, netid) == NULL))	/* if not clone */
			user_add(nick, netid);
		return;
	} else if ((strcmp(cmd, "QUIT") == 0)
		|| (strcmp(cmd, "PART") == 0)) {
		nick_add_symb(nick, netid);
		snprintf(msg, sizeof(msg), "QUIT :%s\r\n", buf);
		user_del(nick, msg);
		return;
	} else if (strcmp(cmd, "NICK") == 0) {
		int *fds, i;
		char *newnick;

		nick_add_symb(nick, netid);
		if ((fds = htsearch(users, nick)) == NULL)
			return;

		/* set buf to new nick */
		split(&buf, ':');
		/* allocate a newnick and append the netsym and then replace the old */
		newnick = ecalloc(strlen(buf) + strlen(networks[netid].symb) + 2 + 1, sizeof(char));
		sprintf(newnick, "%s[%s]", buf, networks[netid].symb);
		htsetkey(users, nick, newnick);

		snprintf(msg, sizeof(msg), "NICK %s\r\n", newnick);
		for (i = 0; i < netlen; i++) {
			if (fds[i] > 0)
				writeall(fds[i], msg);
		}
		return;
	} else if (strcmp(cmd, "KICK") == 0) {
		/* :<nick_which_is_kicking>!~user@host KICK <channel> <nick_which_has_been_kicked> :<kick_msg> */
		int *fds;
		char *chan = split(&buf, ' ');
		char *user = split(&buf, ' ');

		/* set the quit msg */
		snprintf(msg, sizeof(msg), "QUIT : kicked by %s\r\n", nick);

		/* delete whole network if it is the linker */
		if (strcmp(user, linker_nick) == 0) {
			net_del(networks[netid].name);
			return;
		}

		/* delete the user if the message from the same network */
		if ((fds = user_fds(user, netid)) == NULL) {
			nick_add_symb(user, netid);
			user_del(user, msg);
			return;
		}

		/* close the kicked fd */
		writeall(fds[netid], msg);
		fd_del(fds[netid]);
		fds[netid] = -2;

		/*
		 * send notice in the channel through linker
		 */
		/* get the original user netid */
		for (i = 0; i < netlen; i++) {
			if (fds[i] == -1)
				break;
		}
		/* set buf to msg */
		split(&buf, ':');
		/* remove netsymb and suffix */
		*strrchr(user, '[') = '\0';
		/* send notice */
		snprintf(msg, sizeof(msg),
			"PRIVMSG %s :%s was kicked out by %s from network %s %s [%s]\r\n",
			networks[i].chan, user, nick, networks[netid].name, chan, buf);
		writeall(networks[i].fd, msg);
		return;
	}
printbuffer:
	printf("%d: %s\n", pfdset[id].fd, backup);
}

void
net_add(char *name, char *symb, char *host, char *port, char *chan)
{
	int	 i, fd;
	Network *n;

	/* if name, symbol or configuration already exists */
	for (i = 0; i < netlen; i++) {
		if (strcmp(networks[i].name, name) == 0) {
			printf("error: network name '%s' already exists\n", name);
			return;
		}
		if (strcmp(networks[i].symb, symb) == 0) {
			printf("error: network symbol '%s' already exists\n", symb);
			return;
		}
		if ((strcmp(networks[i].host, host) == 0)
		&&  (strcmp(networks[i].port, port) == 0)
		&&  (strcmp(networks[i].chan, chan) == 0)) {
			printf("error: network configuration already exists\n");
			return;
		}
	}

	/* resize if full */
	if (netlen == netcap) {
		Htiter it = {0};
		networks = erecalloc(networks, (size_t)netcap, NET_ADDEND, sizeof(Network));
		while (htiterate(users, &it))
			it.node->val = erecalloc(it.node->val, (size_t)netcap, NET_ADDEND, sizeof(int));
		netcap += NET_ADDEND;
	}

	/* connect */
	if ((fd = dial(host, port)) == -1)
		return;
	fd_add(fd, netlen, LINKER_FD, 0);
	/* send NICK and USER commands */
	snprintf(msg, sizeof(msg), "NICK linker\r\n");
	writeall(fd, msg);
	snprintf(msg, sizeof(msg), "USER linker 0 * :linker\r\n");
	writeall(fd, msg);
	/* add a network */
	n = &networks[netlen];
	n->fd = fd;
	n->join = 0;
	n->name = strdup(name);
	n->symb = strdup(symb);
	n->host = strdup(host);
	n->port = strdup(port);
	n->chan = strdup(chan);
	netlen++;
	printf("%d: network '%s' added\n", fd, name);
}

void
net_del(char *name)
{
	int	i, netid, *fds;

	Htiter	it	= {0};	/* current iterator */
	Htiter	lastit	= {0};	/* last iterator */

	/* get netid */
	netid = -1;
	for (i = 0; i < netlen; i++) {
		if (strcmp(name, networks[i].name) == 0) {
			netid = i;
			break;
		}
	}
	if (netid == -1) {
		printf("error: network '%s' doesn't exist\n", name);
		return;
	}

	/* set the quit msg */
	snprintf(msg, sizeof(msg), "QUIT :unlinking network %s\r\n", name);

	/* reconstruct the user-clones table */
	while (htiterate(users, &it)) {
		fds = (int *)it.node->val;
		/* delete all the users of deleting network */
		if (fds[netid] == -1) {
			user_del(it.node->key, msg);
			/* this node is deleted */
			it = lastit;
		/* delete the clones */
		} else {
			if (fds[netid] > 0) {
				writeall(fds[netid], msg);
				fd_del(fds[netid]);
			}
			/* swap last with current one */
			fds[netid] = fds[netlen-1];
		}
		lastit = it;
	}

	/* set pollfds netid with last netid to current netid. */
	for (i = 0; i < pfdlen; i++) {
		if (fddata[i].netid == netlen-1)
			fddata[i].netid = netid;
	}

	writeall(networks[netid].fd, msg);
	fd_del(networks[netid].fd);
	net_del_raw(netid);
	printf("%d: network '%s' deleted\n", networks[netid].fd, name);
	/* swap the network with the last */
	networks[netid] = networks[netlen-1];
	netlen--;
}

void
net_del_raw(int netid)
{
	Network *n = &networks[netid];
	free(n->name);
	free(n->symb);
	free(n->host);
	free(n->port);
	free(n->chan);
}

void
net_update(int netid)
{
	int	*fds;
	Htiter	 it = {0};

	while (htiterate(users, &it)) {
		fds = (int *)it.node->val;
		fds[netid] = clone_add(netid, it.node->key);
	}
}

void
user_add(char *unick, int netid)
{
	int	i, *fds;
	size_t	len;
	char	*nick;

	len = strlen(unick) + strlen(networks[netid].symb) + 2 + 1;

	/* too long nick */
	if (len-1 > MAX_NICK_LEN) {
		printf("error: user nick '%s' is too big\n", unick);
		return;
	}

	/* resize hash table if store is low */
	if ((users->cap - users->len) < USER_ADDEND)
		htresize(users, users->cap + USER_ADDEND);

	printf("useradd: %s\n", unick);

	/* allocate a new user */
	nick = ecalloc(len, sizeof(char));
	fds = ecalloc((size_t)netcap, sizeof(int));
	sprintf(nick, "%s[%s]", unick, networks[netid].symb);

	/* clone the user on all other network */
	for (i = 0; i < netlen; i++) {
		if (networks[i].join == 0)
			continue;
		if (i == netid) {
			fds[i] = -1;
		} else {
			fds[i] = clone_add(i, nick);
		}
	}
	/* insert it to the users hash table */
	if (htinsert(users, nick, fds) == -1) {
		/* this shouldn't happen as it was already checked */
		printf("error: user '%s' already exists\n", nick);
		terminate(1);
	}
}

void
user_del(char *nick, char *msg)
{
	int *fds, i;
	if ((fds = htsearch(users, nick)) == NULL) {
		printf("error: user '%s' doesn't exists\n", nick);
		return;
	}

	for (i = 0; i < netlen; i++) {
		if (fds[i] > 0) {
			writeall(fds[i], msg);
			fd_del(fds[i]);
		}
	}
	htremove(users, nick);
}

int *
user_fds(char *nick, int netid)
{
	unsigned int suffix;
	int *fds = NULL;

	/* count suffix */
	for (suffix = 0; nick[strlen(nick)-suffix-1] == '_'; suffix++);
	/* remove suffix */
	if (suffix > 0)
		nick[strlen(nick)-suffix] = '\0';

	fds = htsearch(users, nick);
	/* if match but suffix doesn't match */
	if ((fds != NULL) && (fddata[fdtoid[fds[netid]]].suffix != (int)suffix))
		fds = NULL;

	/* add suffix back */
	if (suffix > 0)
		nick[strlen(nick)] = '_';

	return fds;
}

int
clone_add(int netid, char *nick)
{
	int	fd;
	Network	*n = &networks[netid];

	if ((fd = dial(n->host, n->port)) == -1)
		return -1;
	fd_add(fd, netid, CLONE_FD, 0);
	/* send NICK and USER commands */
	snprintf(msg, sizeof(msg), "NICK %s\r\n", nick);
	writeall(fd, msg);
	snprintf(msg, sizeof(msg), "USER user 0 * :user\r\n");
	writeall(fd, msg);
	return fd;
}

void
fd_add(int fd, int netid, int type, int suffix)
{
	/* resize if full */
	if (fd+1 == fdscap) {
		fdtoid = erecalloc(fdtoid, (size_t)fdscap, FD_ADDEND, sizeof(int));
		fdscap += FD_ADDEND;
	}
	if (pfdlen == pfdcap) {
		pfdset = erecalloc(pfdset, (size_t)pfdcap, POLLFD_ADDEND, sizeof(struct pollfd));
		fddata = erecalloc(fddata, (size_t)pfdcap, POLLFD_ADDEND, sizeof(struct fdData));
		pfdcap += POLLFD_ADDEND;
	}
	pfdset[pfdlen].fd = fd;
	pfdset[pfdlen].events = POLLIN;
	fddata[pfdlen].netid = netid;
	fddata[pfdlen].type = type;
	fddata[pfdlen].suffix = suffix;
	fdtoid[fd] = pfdlen;
	pfdlen++;
}

void
fd_del(int fd)
{
	close(fd);
	pfdset[fdtoid[fd]] = pfdset[pfdlen-1];
	fddata[fdtoid[fd]] = fddata[pfdlen-1];
	fdtoid[fd] = pfdlen-1;
	pfdlen--;
}

void
nick_add_symb(char *nick, int netid)
{
	strcat(nick, "[");
	strcat(nick, networks[netid].symb);
	strcat(nick, "]");
}

/*
 * trim all the nicknames to original nick
 * src will be destructed
 */
void
privmsg_update(char *dst, char *src, int netid)
{
	char d;		/* delimiter */
	char *n;

	while (src != NULL) {
		n = strpbrk(src, " :;,<>@&~%+\\");
		if (n == NULL) {
			d = '\0';
		} else {
			d = *n;
			*n = '\0';
			n++;
		}

		/* check if the word is nick */
		if (user_fds(src, netid) != NULL)
			*strrchr(src, '[') = '\0';

		strcat(dst, src);
		strncat(dst, &d, 1);

		src = n;
	}
}

void
terminate(int status)
{
	int i;
	snprintf(msg, sizeof(msg), "QUIT :linker shutting down\r\n");
	for (i = 0; i < pfdlen; i++) {
		if (fddata[i].type != FIFO_FD)
			writeall(pfdset[i].fd, msg);
		fd_del(pfdset[i].fd);
	}
	/* delete all the users */
	htdestroy(users);

	/* delete all the networks */
	for (i = 0; i < netlen; i++)
		net_del_raw(i);
	free(networks);

	free(pfdset);
	free(fddata);
	free(fdtoid);

	if (status == 0) {
		printf("exit successfully\n");
		exit(0);
	} else {
		printf("aborted\n");
		exit(1);
	}
}

void
print_table(void)
{
	int	i, *fds, diff, tabs;
	Htiter	it = {0};
	char	*nick;

	if (netlen == 0)
		return;

	print_border();
	/* print networks */
	printf("Networks\t\t");
	for (i = 0; i < netlen; i++)
		printf("%s(%d)\t", networks[i].symb, networks[i].fd);
	printf("\n");

	while (htiterate(users, &it)) {
		fds  = (int *)it.node->val;
		nick = (char *)it.node->key;
		/* print tabbed user nick */
		printf("%s", nick);
		diff = 24 - (int)strlen(nick);
		tabs = ((diff / 8) + (diff % 8 > 0));
		printf("%.*s", tabs, "\t\t\t");
		/* print tabbed clones fds */
		for (i = 0; i < netlen; i++) {
			printf("%d", fds[i]);
			/* print suffix */
			if ((fds[i] > 0) && (fddata[fdtoid[fds[i]]].suffix > 0))
				printf("(%d)", fddata[fdtoid[fds[i]]].suffix);
			printf("\t");
		}
		printf("\n");
	}
	print_border();
}

void
print_htable(void)
{
	Htiter	it = {0};
	int	index = -1;

	print_border();
	while (htiterate(users, &it)) {
		if (index != (int)it.index) {
			/* ignore first new line */
			if (index != -1)
				printf("\n");
			printf("%d", it.index);
			index = (int)it.index;
		}
		printf(" -> %s", (char *)it.node->key);
	}
	printf("\n");
	print_border();
}

void
print_users(void)
{
	Htiter	it = {0};
	int i = 0;

	print_border();
	while (htiterate(users, &it))
		printf("%d: %s\n", i++, (char *)it.node->key);
	print_border();
}

void
print_border(void)
{
	int i;
	for (i = 0; i < 64; i++)
		printf("-");
	printf("\n");
}