You are not logged in.

#1 2011-11-26 02:28:40

cmtptr
Member
Registered: 2008-09-01
Posts: 135

curses multi-terminal interface - fundamental design questions

I recently had an idea for a client application that would use multiple curses displays for its user interface, but with a single network connection to the server.  I had originally planned to implement this using UNIX sockets like so:

(a very, very rough high level flowchart, ignoring errors and race conditions)

       (start)
          |
          V
[open socket as client]
          |
          V
      <success?>
     yes       no
     |         |
     |         V
     |      [fork()]
     |   parent   child
     |     |        |
     |     V        |
     |[open socket] |
     |     |        |
     |    /         V
     |  /      [connect to server,]
     |/        [create and listen ]
     |         [on socket         ]
     V
[curses ui]

So the child of the fork becomes a sort of local distributor for the connection to the server.  The parent becomes a curses interface displaying information from and sending user input to the child which is routed to the remote server.  The child is capable of managing multiple clients, so that multiple terminals may be launched and connected to it for a multi-curses interface.  When the last client disconnects, then the child closes its connection with the server and exits.

Then, I stumbled across this in curs_initscr(3X):

`man 3X curs_initscr` wrote:

A program that outputs to more than one terminal should use the newterm routine for each terminal instead of initscr.

Now I'm thinking, "wow, it would be cool if the local-side clients were like dumb terminals and the server connection process also did all of the curses stuff for each open interface!"  Here are the problems that I see:

1. When I launch a client UI, how do I pass my $TERM and tty to the main process?  It would need to be simple  enough that it doesn't make the original method more attractive.
(edit: Solved.  I create a FIFO before forking, then write ttyname() and $TERM to it before leaving the parent process.  In the child, I block for the first read (to guarantee the parent terminal is received), then select() for subsequent reads in my main loop.  Further invokations see the FIFO already exists and so they just write their ttyname() and $TERM and then exit.)

2. How will the client process know for how long to block before exiting?  Does it even need to block?  When curses initializes on a terminal other than where it was started from, is it going to battle with the shell for control over that terminal?
(edit: If I need to block, I can use the FIFO to signal back to client processes when they're finished.  The rest of this question seems to be what I'm battling now.  I'm not getting clean behavior when trying to take over a terminal already controlled by another shell.)
(edit: Just kidding.  I can't signal back to the clients through the FIFO because there's no way to direct which client I'm signaling to!  I'm still thinking in sockets...)

3. I continued reading the manpage.  That paragraph ends on this note:

`man 3X curs_initscr` wrote:

The program must also call endwin for each terminal being used before exiting from curses.  If newterm is called more than once for the same terminal, the first terminal referred to must be the last one for which endwin is called.

This is confusing to me.  For one, endwin takes no arguments.  How am I supposed to "call endwin for each terminal being used" if I can't tell it which one?  I can only assume that curses is going to decide which terminal to end for me, which sort of agrees with this part: "the first terminal referred to must be the last one for which endwin is called," except that it specifies this is only true "If newterm is called more than once for the same terminal."  I don't intend to call it more than once for the same terminal; I want to call it more than once in a single application for different terminals.
(edit: Nevermind this one; it's been a while since I've curses'd.  I went to bed and when I woke up, I said to myself "hey, many of the other curses routines also take (void) arguments.  I'll bet there's a selector function!"  And there is.  It's called set_term().)

Googling on the subject has turned up little.  So when the documentation is confusing, and Google can't find others with a similar problem, this is the point where I generally turn to desperate cries for help.  Can curses be used in the way that I described?  Am I trying to use it in a way that was never intended?  Do you see advantages or disadvantages to either model over the other (client/server socket method versus monolith/dumb terminal method) that I have not already pointed out?

Thanks.

Last edited by cmtptr (2011-11-27 00:37:52)

Offline

#2 2011-11-27 18:25:58

cmtptr
Member
Registered: 2008-09-01
Posts: 135

Re: curses multi-terminal interface - fundamental design questions

I almost have this working.  Unfortunately the FIFO thing just wasn't doing it for me, so then I tried a new approach:

I returned to using UNIX sockets, but this time with the intention of having the client be a dumb passthru process for forwarding stdin to the socket and the socket to stdout.  Then, in the core process, I would initialize curses on the client socket and leave all of the curses-ing on the core process.  The passthru portion of this worked, but it seemed like curses can't initialize properly on a socket.  I had to call newterm(getenv("TERM"), stdout, stdin) on the passthru process to disable line buffering and character echo, which made me sad because I wanted to avoid making any curses calls on the passthru side.  It seemed as though some of the other curses mechanisms couldn't work, as well (i.e. getmaxyx() returned the default 80x24 instead of the true terminal size).  I was disappointed by this; for some reason I thought curses worked entirely on stdin/stdout.

So now I'm using a sort of hybrid approach between my FIFO and socket models.  I open a UNIX socket and use it to send my $TERM and ttyname() to the core process.  Then I select() on the socket forever until the core process hangs up.  On the core side, I accept connections and then parse out the $TERM and ttyname() that were sent.  I use these to open the TTY and initialize curses on it.  Curses takes over the console, and all is well.  When the user enters a 'q' it's caught by curses, then endwin(), close the TTY, and finally close the socket which allows the passthru process to fall through its select() and finally exit.

Here is my code so far in case anyone wants to play with it.  Since I haven't gotten any responses so far, maybe this should be moved to the "Try This" forum...

/*
 * compile with:
 * gcc -ansi -pedantic echosrv.c -lcurses
 *
 * To use it, invoke ./a.out on one terminal to launch a session.
 * Now, from a different terminal, invoke ./a.out to open a second session.
 * (from the same working directory since the socket is lazily a relative path for now)
 * This is a simple echo server, except that it echoes top-to-bottom rather than left-to-right.
 * All input and output is handled by a single core process.  The "client" processes are idle for the duration.
 * Any number of clients may be opened.  They can be disconnected (type 'q') in any order.
 * The "server" process will terminate when the last client disconnects.
 *
 * If you have problems with the first client reporting "connect(): No such file or directory,"
 * try increasing the pause timeval under the comment that says "TODO block until child creates the socket."
 * This is something I just haven't had the interest to address yet.
 *
 */


/*************
 * from ui.c *
 *************/

#include <curses.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static struct _Ui {
	int        sock;
	FILE       *tty;
	SCREEN     *screen;
	int        ymax, xmax;
	int        y,    x;
	struct _Ui *next;
} *_uis;

int ui_update()
{
	struct _Ui **ui;

	for (ui = &_uis; *ui; ui = &(*ui)->next) {
		struct _Ui *next;
		static char buffer[2] = {0, 0};
		set_term((*ui)->screen);
		switch ((buffer[0] = getch())) {
			case ERR:
				break;
			case 'q':
				endwin();
				delscreen((*ui)->screen);
				fclose((*ui)->tty);
				close((*ui)->sock);
				next = (*ui)->next;
				free(*ui);
				*ui = next;
				break;
			default:
				mvaddstr((*ui)->y, (*ui)->x, buffer);
				if (++ (*ui)->y >= (*ui)->ymax) {
					(*ui)->y = 0;
					(*ui)->x ++;
				}
		}
		if (!*ui) break;
		refresh();
	}

	return _uis ? 1 : 0;
}

void ui_new(int sock, const char *__restrict__ tty, char *__restrict__ term)
{
	struct _Ui *ui;

	ui         = malloc(sizeof *ui);
	/* TODO handle malloc failure */
	ui->sock   = sock;
	ui->tty    = fopen(tty, "r+");
	ui->screen = newterm(term, ui->tty, ui->tty);
	ui->next   = _uis;
	_uis       = ui;

	def_prog_mode();

	cbreak();
	curs_set(0);
	noecho();
	nonl();
	nodelay(stdscr, 1);
	raw();

	ui->y = ui->x = 0;
	getmaxyx(stdscr, ui->ymax, ui->xmax);
}


/***************
 * from main.c *
 ***************/


#include <alloca.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>

/* accept incoming connections on the core process's listener socket */
static void _accept(int sock)
{
	int     incoming;
	char    buffer[64];
	ssize_t len;
	ssize_t total;
	char    *tty;
	char    *term;

	incoming = accept(sock, 0, 0);
	total    = 0;
	while (total < sizeof buffer / sizeof *buffer
	   && (len = read(incoming, buffer + total, sizeof buffer / sizeof *buffer - total))) {
		total += len;
		if (!buffer[len - 1])
			break;
	}

	tty  = strtok(buffer, "\n");
	term = strtok(     0, "\n");

	ui_new(incoming, tty, term);
}

/* connect from an interface process to the core process */
static void _connect(int sock)
{
	const char *tty;
	const char *term;
	char       *buffer;
	size_t     len;

	tty    = ttyname(STDIN_FILENO);
	term   = getenv("TERM");
	len    = strlen(tty) + strlen(term) + 2;
	buffer = alloca(len);

	sprintf(buffer, "%s\n%s", tty, term);
	write(sock, buffer, len);
}

/* redirect input on one file descriptor out to another file descriptor */
static size_t _passthru(int in, int out)
{
	char    buffer[1920];  /* 80x24 */
	ssize_t len;
	ssize_t total;

	total = 0;
	while ((len = read(in, buffer, sizeof buffer / sizeof *buffer)) > 0) {
		write(out, buffer, len);
		total += len;
	}
	
	return total;
}

int main(int argc, char **argv)
{
	const char         *path = "socket";
	int                sock;
	struct sockaddr_un addr;
	socklen_t          len;
	fd_set             set;

	sock              = socket(AF_UNIX, SOCK_STREAM, 0);
	addr.sun_family   = AF_UNIX;
	len               = sizeof addr.sun_family;
	len              += sprintf(addr.sun_path, "%s", path);

	if (connect(sock, (struct sockaddr *)&addr, len)) {
		close(sock);

		if (errno != ENOENT) {
			perror("connect()");
			return -1;
		}

		switch (fork()) {

			case -1:
				perror("fork()");
				return -1;

			case 0:
				/* core process */
				sock = socket(AF_UNIX, SOCK_STREAM, 0);
				bind(sock, (struct sockaddr *)&addr, len);
				listen(sock, 0);

				/* block for the first client to guarantee the parent connects */
				FD_ZERO(&set); FD_SET(sock, &set);
				if (select(sock + 1, &set, 0, 0, 0) == -1) {
					perror("select()");
					close(sock);
					unlink(path);
					return -1;
				}
				_accept(sock);

				while (1) {
					struct timeval dtMax = {0, 31250};
					FD_ZERO(&set); FD_SET(sock, &set);
					if (select(sock + 1, &set, 0, 0, &dtMax) > 0)
						_accept(sock);
					if (!ui_update())
						break;
				}

				close(sock);
				unlink(path);
				puts("core exit");
				return 0;

			default:
				{
					/* TODO block until child creates the socket */
					struct timeval tv = {0, 31250};
					select(0, 0, 0, 0, &tv);
				}
				sock = socket(AF_UNIX, SOCK_STREAM, 0);
				if (connect(sock, (struct sockaddr *)&addr, len)) {
					perror("connect()");
					close(sock);
					return -1;
				}

		}
	}

	/* interface process */
	_connect(sock);
	FD_ZERO(&set); FD_SET(sock, &set);
	if (select(sock + 1, &set, 0, 0, 0) == -1) {
		perror("select()");
		close(sock);
		return -1;
	}

	close(sock);
	return 0;
}

You'll notice if you compile with -Wall that _passthru() is never called.  That's what I was using to forward stdin->sock and sock->stdout.  If there's a better way to do this, please don't hesitate to let me know.  Today I was made aware of fcntl's F_DUPFD, but I don't think that's what I want.  If I understand F_DUPFD correctly, it creates a sort of alias for a file descriptor, whereas I want to really forward traffic from one incoming file descriptor out another file descriptor.

This code works as intended except when I reconnect using the terminal from where the core process was lauched.  Reconnecting from any other terminal works as expected.  By this, I mean:

TERM_A $ ./a.out  # works
TERM_B $ ./a.out  # works
# now on TERM_A, type 'q' to quit
TERM_A $ ./a.out  # strange behavior that I can't explain yet.

Offline

Board footer

Powered by FluxBB