[mod.sources] v06i010: A "talk" for system V.2

sources-request@mirror.UUCP (06/20/86)

Submitted by: genrad!decvax!mcvax!mdtch!watson (James Watson)
Mod.sources: Volume 6, Issue 10
Archive-name: sysVtalkA

[ This is the first of two different USG talk programs.  This one
  uses FIFO's (named pipes).  I have not tried it out.  -r$]
I wrote the following because I got tired of having only write (ugh) on
SVR2.  It could stand some improvement, especially in the area of
handling unlikely errors, but...

    James Watson
    Modulator SA, Koenizstrasse 194,
    3097 Liebefeld-Bern, Switzerland

---------------------CUT HERE---------------------
# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
# Contents:  talk.c
 
echo x - talk.c
sed 's/^XX//' > "talk.c" <<'@//E*O*F talk.c//'
XX#include <sys/types.h>
XX#include <fcntl.h>
XX#include <utmp.h>
XX#include <signal.h>
XX#include <string.h>
XX#include <curses.h>

XX#define BELL 7

XXextern struct utmp *getutent();
XXextern char *ttyname();
XXextern unsigned alarm();

XXchar out_fn[20];

XXterminate () {
XX    unlink (out_fn);
XX    endwin ();
XX    exit (0);
XX}

XXcatch_alarm () {}

XXmain (argc, argv)
XXint argc;
XXchar *argv[];
XX{
XX    struct utmp *utp, ctp;
XX    struct termio tflags;
XX    WINDOW *in_win, *out_win;
XX    int in_pipe, out_pipe, their_fd, count = 0;
XX    char in_c, their_tty[20], in_fn[20];
XX    char *my_tty, my_name[L_cuserid+1], call_mesg[100];

XX    /* Check validity of arguments. */
XX    if (argc < 2 || argc > 3) {
XX	fprintf (stderr, "Use: %s username [ttyn]\n", *argv);
XX	exit (-1);
XX    }
XX    /* We will need to know the tty device name... */
XX    if ((my_tty = ttyname (0)) == NULL) {
XX	fprintf (stderr, "Sorry, I cannot figure out what tty you are on.\n");
XX	exit (-1);
XX    }
XX    /* But only the last component of it. */
XX    my_tty = strrchr (my_tty, '/') + 1;
XX    /*
XX     * Sanity check.  You cannot ask to talk to yourself unless you specify
XX     * some tty other than the one you are on.
XX     */
XX    if (!strncmp (argv[1], cuserid(my_name), L_cuserid) &&
XX	    (argc == 2 || !strncmp (my_tty, argv[2], 12))) {
XX	fprintf (stderr, "Only crazy people talk to themselves...\n");
XX	exit (-1);
XX    }
XX    /* Now find out if the requested user is logged in. */
XX    while (utp = getutent())
XX	/*
XX	 * There are three criteria here:
XX	 *	1. The entry must be for a user process.
XX	 *	2. The user name must match the first argument.
XX	 *	3. If a second argument was given, the tty name
XX	 *	   must match that argument.
XX	 * We have to allow for the possibility that the request is not
XX	 * specific enough - i.e. no tty name was given, and the requested
XX	 * user is logged in more than once.
XX	 */
XX	if (utp->ut_type == USER_PROCESS &&
XX		!strncmp (utp->ut_user, argv[1], 8) &&
XX		(argc == 2 || !strncmp (utp->ut_line, argv[2], 12))) {
XX	    if (count++) {
XX		fprintf (stderr,
XX		 "User '%s' logged in more than once.  Use \"%s %s ttyn\".\n",
XX		 argv[1], argv[0], argv[1]);
XX		exit (-1);
XX	    }
XX	    else ctp = *utp;
XX	}

XX    if (!count) {
XX	fprintf (stderr, "%s not currently logged in", argv[1]);
XX	if (argc == 3)
XX	    fprintf (stderr, " on %s", argv[2]);
XX	fprintf (stderr, ".\n");
XX	exit (-1);
XX    }
XX    /* Finally found someone.  Make sure we are allowed to talk to them. */
XX    sprintf (their_tty, "/dev/%s", ctp.ut_line);
XX    if ((their_fd = open (their_tty, O_WRONLY, 0)) < 0) {
XX	fprintf (stderr, "Sorry, no write permission on %s.\n", their_tty);
XX	exit (-1);
XX    }
XX    /*
XX     * At this point, we know we are going to at least try to talk to
XX     * someone.  Now we do the complete initialization.
XX     *
XX     * Initialize curses.  Note that the signal traps set set immediately
XX     * upon return from iniscr() - before calling any other curses
XX     * functions.  No sense in leaving a bigger window than necessary.
XX     */
XX    initscr ();
XX    signal (SIGINT, terminate);
XX    signal (SIGKILL, terminate);
XX    cbreak ();
XX    noecho ();
XX    /* Declare the windows to be used for the conversation. */
XX    in_win = subwin (stdscr, LINES/2-1, COLS, 0, 0);
XX    out_win = subwin (stdscr, LINES/2-1, COLS, LINES/2+1, 0);
XX    scrollok (out_win, TRUE);
XX    scrollok (in_win, TRUE);
XX    idlok (out_win, TRUE);
XX    idlok (in_win, TRUE);
XX    clear ();
XX    wclear (in_win);
XX    wclear (out_win);
XX    mvaddstr (LINES/2, 0, "------------------------------------------------");
XX    wmove (in_win, 0, 0);
XX    wmove (out_win, 0, 0);
XX    refresh ();
XX    /*
XX     * Here is a decidedly shaky trick to play.  Curses only gives you two
XX     * choices for keyboard reads - completely blocking, or completely
XX     * non-blocking.  Obviously, we cannot use blocking reads, because we
XX     * need to get the input from the other person even when we have
XX     * nothing to say.  On the other hand, completely non-blocking reads
XX     * would put the program in a very tight loop looking at the keyboard
XX     * and the pipe for input - which would put an unnecessarily heavy
XX     * load on the system.  This loop could be slowed down with some kind
XX     * of a sleep() or napms(), but that would make the screen look sort
XX     * of jumpy.  System V termio supports timed out reads, so I choose
XX     * to use them as the gating factor in the loop.  I am assuming that
XX     * when I call cbreak(), curses sets ~ICANON, VMIN = 1 and VTIME = 1.
XX     * If I now sneak in here and set VMIN = 0 and VTIME = 10, keyboard
XX     * reads will time out if there is no input in 1 second, and I can
XX     * pick up the input from the pipe.  The advantage over a simple
XX     * sleep() is that if there IS input from the keyboard, the read will
XX     * return in less than 1 second, and I can read from the pipe that
XX     * much sooner.  This makes the screen updating less jumpy.
XX     */
XX    ioctl (0, TCGETA, &tflags);
XX    tflags.c_cc[VMIN] = 0;
XX    tflags.c_cc[VTIME] =10;
XX    ioctl (0, TCSETA, &tflags);
XX    /*
XX     * The sequence of events during the startup is important, and is
XX     * relatiely interesting.  The situation is this:
XX     * 	   1. The user has requested a talk connection to someone.
XX     * 	   2. We don't know yet if this user is trying to originiate a
XX     *	      connection, or if they are responding to a request from
XX     *	      the other person.
XX     * 	   3. If this is an originating request, we have to notify the
XX     * 	      other user of it, and then wait for that user to respond.
XX     * 	   4. If this is a response to a request from the other user, we
XX     * 	      must not fool around with notification, but rather go
XX     * 	      straight into the conversation.
XX     * We will use several characteristics of Unix pipes to resolve this:
XX     * 	   1. Attempting to open() a named pipe that does not exist will
XX     *	      fail.  (I hope this is not a great surprise...)
XX     * 	   2. Opening a named pipe for reading, with O_NDELAY clear, will
XX     *	      succeed immediately if someone else already has that pipe
XX     * 	      open for writing.
XX     * 	   3. Opening a named pipe for writing, with O_NDELAY clear, will
XX     * 	      block the process until someone opens that pipe for reading.
XX     * So, what we will actually do is this:
XX     * 	   1. Make up a couple of pipe names, based on the tty names
XX     * 	      involved.  Note that this entire sequence could get screwed
XX     * 	      up if someone other than the talk programs opened, deleted,
XX     *	      or otherwise screwed up our pipes.  We therefore choose to
XX     * 	      use dot files in /tmp, so they are not so visible.
XX     * 	   2. Try to open the pipe that should be created by the other
XX     * 	      user's talk program.  If this open succeeds, then we must
XX     * 	      be replying to a call; if it fails, we are originating one.
XX     * 	   3. If the open fails, send a message to the other user, set an
XX     * 	      alarm for 30 seconds, and block the process by trying to
XX     *	      open the pipe we created.  If the user responds, the other
XX     *	      talk program will open our pipe, and then our open will
XX     * 	      succeed.  If there is no response the alarm will fire, and
XX     *	      the open will return -1.
XX     */
XX    sprintf (out_fn, "/tmp/.talk_%s", my_tty);
XX    sprintf (in_fn, "/tmp/.talk_%s", ctp.ut_line);
XX    mknod (out_fn, 0010644, 0);
XX    /* See if they are waiting on us... */
XX    if ((in_pipe = open (in_fn, O_RDONLY | O_NDELAY, 0644)) >= 0)
XX	/* They were, so we can open the other pipe and get to work. */
XX	out_pipe = open (out_fn, O_WRONLY, 0644);
XX    else {
XX	/* Nope, so we are the ones that have to do all the work... */
XX	sprintf (call_mesg,"\
XX[%s is requesting a 'talk' link with you.]\n\
XX[Type 'talk %s %s' to reply.]%c\n",
XX			my_name, my_name, my_tty, BELL);
XX	count = 0;
XX	do {
XX	    write (their_fd, call_mesg, (unsigned) strlen(call_mesg));
XX	    mvwprintw (in_win, 0, 0, "[%d ringy-ding", ++count);
XX	    if (count == 1)
XX		wprintw (in_win, "y]\n");
XX	    else wprintw (in_win, "ies]\n");
XX	    wrefresh (in_win);
XX	    /*
XX	     * Note that the alarm catching routine is a no-op.  All the
XX	     * real work is done right here; we are simply using the alarm
XX	     * to unblock the open() periodically.
XX	     */
XX	    signal (SIGALRM, catch_alarm);
XX	    alarm ((unsigned) 30);
XX	    /* We block here waiting on a reply... */
XX	    out_pipe = open (out_fn, O_WRONLY, 0644);
XX	} while (count < 4 && out_pipe < 0);
XX	close (their_fd);
XX	if (out_pipe < 0) {
XX	    waddstr (in_win, "[No answer.  Giving up.]");
XX	    wrefresh (in_win);
XX	    terminate ();
XX	}
XX	/* OK, they answered, so open the other pipe. */
XX	in_pipe = open (in_fn, O_RDONLY | O_NDELAY, 0644);
XX    }
XX    /*
XX     * We are now ready to talk.  Both processes will hold the named pipes
XX     * open forever from this point, so we can unlink() them, to avoid any
XX     * chance that someone will see them, and say "Gee, I wonder what those
XX     * are?  Maybe I'll cat /etc/termcap to them to see what happens..."
XX     */
XX    unlink (out_fn);
XX    wmove (in_win, 0, 0);
XX    wclrtoeol (in_win);
XX    waddstr (in_win, "[Connected]\n");
XX    wrefresh (in_win);
XX    signal (SIGPIPE, terminate);
XX    /*
XX     * Believe it or not, the preceding 100+ lines of code were necessary
XX     * just to set things up for the following 10 line loop.
XX     *
XX     * The infinite loop is exited only upon receipt of a signal.  The
XX     * possiblilites are:
XX     * 	   1. This user terminates the conversation by typing the
XX     * 	      interrupt or quit character.
XX     * 	   2. The other user terminates the conversation, thereby
XX     * 	      closing the pipes.  The next attempt to write a character
XX     * 	      on the output pipe generates SIGPIPE.
XX     * All three signals are trapped to the terminate() routine.
XX     *
XX     * Note the use of if() when reading the keyboard, and while() when
XX     * reading the pipe.  Since we are using the keyboard timeout to
XX     * control the entire loop, we must drain the pipe each time the
XX     * keyboard read is satisfied or times out.
XX     */
XX    for (;;) {
XX	if (read (0, &in_c, 1) > 0) {
XX	    waddch (out_win, in_c);
XX	    wrefresh (out_win);
XX	    write (out_pipe, &in_c, 1);
XX	}
XX	while (read (in_pipe, &in_c, 1) > 0) {
XX	    waddch (in_win, in_c);
XX	    wrefresh (in_win);
XX	}
XX    }
XX}


@//E*O*F talk.c//
chmod u=rw,g=rw,o=rw talk.c
 
exit 0