[comp.os.minix] An implementation of "talk" for Minix

regan@jacobs.CS.ORST.EDU (Dave Regan) (08/02/90)

The following is an implementation of the "talk" program for Minix.
This program is much like "write" except that it provides a full
screen interface allowing both parties to type at the same time without
scrambling their text.


				regan@jacobs.cs.orst.edu

----------------------------------------------------------------------------
echo x - :Info
sed '/^X/s///' > :Info << '/'
XThis is an implementation of "talk" for Minix 1.5.10.
X
XI have found a need for such a beast while working over a modem to
Xa Minix machine and wanted something a little better than "write".
X
XThis program was first posted to mod.sources a long time ago (1986) by:
X	James Watson
X	Modulator SA, Koenizstrasse 194,
X	3097 Liebefeld-Bern, Switzerland
X	
XI have done some severe hacking to make it fit the Minix idea of curses
Xand accomidate some of the "bugs" in Minix w.r.t. named pipes.  The way
Xthat I handled the curses routines probably means that this will not
Xcompile under any other flavor of Un*x.  Sorry.
X
XThis has not undergone a lot of testing.  However, it seems to work
Xon at least three different computers running Minix.
X
XThanks to Gary Oliver and Fred van Kempen for reviewing this and spotting
Xproblems before the release.
X
XI make no claim of copyright for my changes; they are in the public domain.
XHowever, this was derived from another person's work, who did not explicitly
Xput his work in the public domain, although he did post it to USENET.  Use
Xthis as you feel fit.
X
X			Dave Regan
X			25 July 1990
X			regan@jacobs.cs.orst.edu
/
echo x - talk.man
sed '/^X/s///' > talk.man << '/'
X# talk
XCommand:        talk - Have an interactive conversation with another user
XSyntax:         talk user [tty]
XFlags:          (none)
XExamples:       talk ast               # Send a message to ast
X                talk ast tty1          # Send a message to ast on tty1
X
X     Talk lets a user set up a full-screen communications stream with
Xanother logged-in user.  The screen is split into two portions, with the
Xcharacters typed by you in the top half, and characters typed by the other
Xparty going into the lower half.  Backspace is used to delete characters,
Xand ^L can be used to redraw the screen.  The interrupt character is
Xused to exit the "talk" session.
X     Permission to talk may be denied by making your terminal unwritable
Xby other people.  The file /usr/adm/wtmp is searched to determine which
Xtty to send to.  If the user is logged onto more than one terminal, the
Xtty argument selects the terminal.
/
echo x - Makefile
sed '/^X/s///' > Makefile << '/'
Xtalk:	talk.c
X
Xshar:
X	shar :Info talk.man Makefile talk.c >talk.shar
/
echo x - talk.c
sed '/^X/s///' > talk.c << '/'
X/*
X *	talk.c		v2.2
X *
X *	This should compile and run cleanly on a "stock" Minix 1.5.10
X *	(or better).  Compile with:
X *		cc -O -i -o talk talk.c
X *
X *	This was originally posted to mod.sources long ago.  This is a
X *	quick hack to get it running under Minix.  There was a bit of
X *	work to make this run with the distributed "mini" curses package
X *	of Minix.  This will not be compatible with any other flavor of
X *	curses.  Sorry.
X *
X *	mknod of Minix is a little "non-unix".  This caused some problems
X *	for awhile.
X *
X *	It appears that there is (was) a bug in Minix 1.5.10 in that if
X *	there were multiple writers to the same pipe, they would cause
X *	inconsistancies, unless O_APPEND mode is used.  This should be
X *	fixed in FS, but can be bypassed here.
X *
X *	This should use the screen dimensions from the termcap file.
X *
X *	This program knows nothing about talking to other machines.
X *	Right now, it would be sort of hard to implement under Minix.
X *
X *	There are probably interesting race conditions if multiple
X *	people (more than 2) start talking to each other in a
X *	non-pairwise fashion; e.g. A -> B, B -> C, C -> A.  I don't
X *	know what happens, but it probably won't be pretty.
X *
X *	I claim no copyright on this.  The original author did not
X *	have a copyright statement in the program, and posted it to
X *	USENET.  As far as I am concerned, do what you want with it.
X *
X *	Thanks to Gary Oliver and Fred van Kempen for reviewing this
X *	program and pointing out problems before it saw the light of day.
X *
X *			Dave Regan
X *			25 July 1990
X *			regan@jacobs.cs.orst.edu
X *
X ***** orstcs:mod.sources / mirror!sources-request /  9:44 am  Jun 20, 1986
X * Subject: v06i010:  A "talk" for system V.2 (sysVtalkA)
X * 
X * Submitted by: genrad!decvax!mcvax!mdtch!watson (James Watson)
X * Mod.sources: Volume 6, Issue 10
X * Archive-name: sysVtalkA
X * 
X * [ This is the first of two different USG talk programs.  This one
X *   uses FIFO's (named pipes).  I have not tried it out.  -r$]
X * I wrote the following because I got tired of having only write (ugh) on
X * SVR2.  It could stand some improvement, especially in the area of
X * handling unlikely errors, but...
X * 
X *     James Watson
X *     Modulator SA, Koenizstrasse 194,
X *     3097 Liebefeld-Bern, Switzerland
X */
X 
X#include <sys/types.h>
X#include <sys/stat.h>
X#include <fcntl.h>
X#include <utmp.h>
X#include <signal.h>
X#include <string.h>
X#include <curses.h>
X#include <sgtty.h>
X#include <unistd.h>
X#include <stdio.h>
X#include <errno.h>
X
X/*
X *	Local definitions
X */
X#define BELL 7
X#define	REDRAW	('l' & 0x1F)
X
X#ifndef L_cuserid
X#define	L_cuserid	9
X#endif
X#ifndef LINES
X#define	LINES		24	/* Curses only support 24 lines */
X#endif
X#ifndef COLS
X#define	COLS		79
X#endif
X
X/*
X *	External routines
X */
Xextern unsigned alarm();
Xextern char *	ttyname();
X 
X/*
X *	Forward declarations
X */
Xextern void	advance();
Xextern void	catch_alarm();
Xextern void	do_child();
Xextern struct utmp *getutent();		/* Should go into the library	*/
Xextern void	term_child();
Xextern void	terminate();
X
X/*
X *	Global variables used in this program
X */
Xint		Child = -1;	/* KB task PID				*/
Xint		In_pipe;	/* Pipe to read from KB and other guy	*/
Xint		Our_pipe;	/* Pipe for KB task to talk to main task */
Xchar		Out_fn[20];	/* Our pipe name			*/
Xint		Out_pipe;	/* Pipe for KB task to talk to other guy */
Xstruct sgttyb	Xnew_tty;	/* Current terminal characteristics	*/
Xstruct sgttyb	Xold_tty;	/* Startup terminal characteristics	*/
X
X
X/*
X *	main
X *
X *	Open connections
X *	Build a task to handle the keyboard
X *	Process any input and put in the appropriate place on the screen
X */
Xvoid
Xmain(argc, argv)
X  int			argc;
X  char			*argv[];
X    {
X    char		call_mesg[100];
X    int			count = 0;
X    struct utmp		ctp;
X    unsigned char	in_c;
X    char		in_fn[20];
X    int			in_pipe;
X    char		my_name[L_cuserid+1];
X    char		*my_tty;
X    int			our_col;
X    int			our_line;
X    int			their_col;
X    int			their_fd;
X    int			their_line;
X    char		their_tty[20];
X    int			unlink_flag;
X    struct utmp		*utp;
X
X    /* Check validity of arguments. */
X    if (argc < 2 || argc > 3)
X	{
X	fprintf (stderr, "Use: %s username [ttyn]\n", *argv);
X	exit (-1);
X	}
X	
X    /* We will need to know the tty device name... */
X    if ((my_tty = ttyname(0)) == (char *) NULL)
X	{
X	fprintf(stderr, "Sorry, I cannot figure out what tty you are on.\n");
X	exit(-1);
X	}
X	
X    /* But only the last component of it. */
X    my_tty = strrchr(my_tty, '/') + 1;
X    
X    /*
X     * Sanity check.  You cannot ask to talk to yourself unless you specify
X     * some tty other than the one you are on.
X     */
X    if (!strncmp(argv[1], cuserid(my_name), L_cuserid) &&
X	    (argc == 2 || !strncmp(my_tty, argv[2], 12)))
X	{
X	fprintf(stderr, "Only crazy people talk to themselves...\n");
X	exit(-1);
X	}
X	
X    /* Now find out if the requested user is logged in. */
X    while (utp = getutent())
X	{
X	/*
X	 * There are three criteria here:
X	 *	1. The entry must be for a user process.
X	 *	2. The user name must match the first argument.
X	 *	3. If a second argument was given, the tty name
X	 *	   must match that argument.
X	 * We have to allow for the possibility that the request is not
X	 * specific enough - i.e. no tty name was given, and the requested
X	 * user is logged in more than once.
X	 */
X	if (utp->ut_type == USER_PROCESS &&
X		!strncmp(utp->ut_user, argv[1], 8) &&
X		(argc == 2 || !strncmp(utp->ut_line, argv[2], 12)))
X	    {
X	    if (count++)
X		{
X		fprintf(stderr,
X		 "User '%s' logged in more than once.  Use \"%s %s ttyn\".\n",
X		 argv[1], argv[0], argv[1]);
X		exit(-1);
X		}
X	    else ctp = *utp;
X	    }
X	}
X
X    if (!count)
X	{
X	fprintf(stderr, "%s not currently logged in", argv[1]);
X	if (argc == 3)
X	    fprintf(stderr, " on %s", argv[2]);
X	fprintf(stderr, ".\n");
X	exit(-1);
X	}
X	
X    /* Finally found someone.  Make sure we are allowed to talk to them. */
X    sprintf(their_tty, "/dev/%s", ctp.ut_line);
X    if ((their_fd = open(their_tty, O_WRONLY, 0)) < 0)
X	{
X	fprintf(stderr, "Sorry, no write permission on %s.\n", their_tty);
X	exit(-1);
X	}
X	
X    /*
X     * At this point, we know we are going to at least try to talk to
X     * someone.  Now we do the complete initialization.
X     *
X     * Initialize curses.  Note that the signal traps are set immediately
X     * upon return from iniscr() - before calling any other curses
X     * functions.  No sense in leaving a bigger window than necessary.
X     */
X    initscr();
X    ioctl(0, TIOCGETP, &Xold_tty);
X    ioctl(0, TIOCGETP, &Xnew_tty);
X    Xnew_tty.sg_flags |= CBREAK;
X    Xnew_tty.sg_flags &= ~ECHO;
X    ioctl(0, TIOCSETP, &Xnew_tty);
X    signal(SIGINT, terminate);
X    signal(SIGKILL, terminate);
X    clear();
X    move(LINES / 2, 0);
X    addstr("------------------------------ ");
X    addstr(argv[1]);
X    addstr(" ------------------------------");
X    move(0, 0);
X    refresh();
X    
X    /*
X     * The sequence of events during the startup is important, and is
X     * relatiely interesting.  The situation is this:
X     * 	   1. The user has requested a talk connection to someone.
X     * 	   2. We don't know yet if this user is trying to originiate a
X     *	      connection, or if they are responding to a request from
X     *	      the other person.
X     * 	   3. If this is an originating request, we have to notify the
X     * 	      other user of it, and then wait for that user to respond.
X     * 	   4. If this is a response to a request from the other user, we
X     * 	      must not fool around with notification, but rather go
X     * 	      straight into the conversation.
X     * We will use several characteristics of Unix pipes to resolve this:
X     * 	   1. Attempting to open() a named pipe that does not exist will
X     *	      fail.  (I hope this is not a great surprise...)
X     * 	   2. Opening a named pipe for reading, with O_NDELAY clear, will
X     *	      succeed immediately if someone else already has that pipe
X     * 	      open for writing.
X     * 	   3. Opening a named pipe for writing, with O_NDELAY clear, will
X     * 	      block the process until someone opens that pipe for reading.
X     * So, what we will actually do is this:
X     * 	   1. Make up a couple of pipe names, based on the tty names
X     * 	      involved.  Note that this entire sequence could get screwed
X     * 	      up if someone other than the talk programs opened, deleted,
X     *	      or otherwise screwed up our pipes.  We therefore choose to
X     * 	      use dot files in /tmp, so they are not so visible.
X     * 	   2. Try to open the pipe that should be created by the other
X     * 	      user's talk program.  If this open succeeds, then we must
X     * 	      be replying to a call; if it fails, we are originating one.
X     * 	   3. If the open fails, send a message to the other user, set an
X     * 	      alarm for 30 seconds, and block the process by trying to
X     *	      open the pipe we created.  If the user responds, the other
X     *	      talk program will open our pipe, and then our open will
X     * 	      succeed.  If there is no response the alarm will fire, and
X     *	      the open will return -1.
X     * Minix comments:
X     *        Minix 1.5.10 has problems with the O_NONBLOCK (a.k.a O_NDELAY)
X     *        on named pipes.  So, first we look to see if one is there, and
X     *        if so, assume the other guy was the originator.
X     */
X    umask(0);
X    sprintf(Out_fn, "/tmp/.talk_%s", my_tty);
X    sprintf(in_fn, "/tmp/.talk_%s", ctp.ut_line);
X    mknod(Out_fn, 0010666, 0, 0);
X    /* See if they are waiting on us... */
X    if (access(in_fn, R_OK) == 0)
X	{
X	/* They were, so we can open the other pipe and get to work. */
X	in_pipe = open(in_fn, O_RDONLY, 0666);
X	Out_pipe = open(Out_fn, O_WRONLY | O_APPEND, 0666);
X	}
X    else
X	{
X	/* Nope, so we are the ones that have to do all the work... */
X	sprintf(call_mesg,"\
X[%s is requesting a 'talk' link with you.]\n\
X[Type 'talk %s %s' to reply.]%c\n",
X			my_name, my_name, my_tty, BELL);
X	count = 0;
X	do
X	    {
X	    write(their_fd, call_mesg, (unsigned) strlen(call_mesg));
X	    move(0, 0);
X	    printw("[%d ringy-ding", ++count);
X	    if (count == 1)
X		printw("y]\n");
X	    else printw("ies]\n");
X	    refresh();
X	    /*
X	     * Note that the alarm catching routine is a no-op.  All the
X	     * real work is done right here; we are simply using the alarm
X	     * to unblock the open() periodically.
X	     */
X	    signal(SIGALRM, catch_alarm);
X	    alarm((unsigned) 30);
X	    /* We block here waiting on a reply... */
X	    Out_pipe = open(Out_fn, O_WRONLY | O_APPEND, 0666);
X	} while (count < 4 && Out_pipe < 0);
X	alarm((unsigned) 0);
X	close(their_fd);
X	if (Out_pipe < 0)
X	    {
X	    addstr("[No answer.  Giving up.]");
X	    refresh();
X	    terminate();
X	    }
X	/* OK, they answered, so open the other pipe. */
X	in_pipe = open(in_fn, O_RDONLY, 0666);
X	}
X    Our_pipe = open(in_fn, O_WRONLY | O_APPEND, 0666);
X    if (Our_pipe < 0 || Out_pipe < 0 || in_pipe < 0)
X    	{
X    	printf("Error opening pipes.  Shouldn't have happened. %d %d %d\n",
X    		Our_pipe, Out_pipe, in_pipe);
X    	terminate();
X    	}
X	
X    /*
X     * We are now ready to talk.  Both processes will hold the named pipes
X     * open forever from this point, so we can unlink() them, to avoid any
X     * chance that someone will see them, and say "Gee, I wonder what those
X     * are?  Maybe I'll cat /etc/termcap to them to see what happens..."
X     * Oops . . .  Hold off on that thought until we actually get data
X     * from the other guy to ensure that he really opened the pipe the
X     * second time.
X     */
X    move(0, 0);
X    clrtoeol();
X    addstr("[Connected]");
X    refresh();
X    signal(SIGPIPE, terminate);
X    
X    /*
X     * Believe it or not, the preceding 100+ lines of code were necessary
X     * just to set things up for the following little loop.
X     *
X     * The infinite loop is exited only upon receipt of a signal.  The
X     * possiblilites are:
X     * 	   1. This user terminates the conversation by typing the
X     * 	      interrupt or quit character.
X     * 	   2. The other user terminates the conversation, thereby
X     * 	      closing the pipes.  The next attempt to write a character
X     * 	      on the output pipe generates SIGPIPE.
X     * All three signals are trapped to the terminate() routine.
X     */
X    our_line = 1;
X    their_line = LINES / 2 + 1;
X    our_col = their_col = 0; 
X    
X    if ((Child = fork()) == -1)
X	{
X	printf("Cannot create a child process\n");
X	terminate();
X	}
X    else if (Child == 0)
X    	{
X    	close(in_pipe);
X	do_child();
X	}
X    close(Out_pipe);
X    close(Our_pipe);
X    
X    move(1, 0);
X    refresh();
X    unlink_flag = 0;
X    for (;;)
X	{
X	while (read(in_pipe, &in_c, 1) > 0)
X	    {
X	    if (in_c == 0x00)
X		{
X		/* From the other person */
X		if (unlink_flag == 0)
X		    {
X		    /* The other guy is talking.  Remove the pipe from
X		     * the file system to avoid having strangers write
X		     * on the pipe for the fun of it.
X		     */
X		    unlink_flag = 1;
X		    unlink(Out_fn);
X		    }
X		read(in_pipe, &in_c, 1);
X		advance(&their_line, LINES / 2 + 1, LINES, &their_col, in_c);
X		}
X	    else if (in_c == 0xFF)	
X	    	terminate();
X	    else
X		{
X		/* From our keyboard task */
X		if (in_c == REDRAW)
X		    {
X		    touchwin();
X		    refresh();
X		    }
X		else
X		    advance(&our_line, 0, LINES / 2, &our_col, in_c);
X		}
X	    }
X	}
X    terminate();	
X    }
X
X
X/*
X *	advance
X *
X *	Advance the line/col count to the next position.
X */
Xvoid
Xadvance(pline, low, high, pcol, ch)
X  int			*pline;
X  int			low;
X  int			high;
X  int			*pcol;
X  int			ch;
X    {
X    move(*pline, *pcol);
X    if (ch == '\n' || ch == '\r' || *pcol >= COLS)
X	{
X	(*pline)++;
X	if (*pline >= high)
X	    *pline = low;
X        *pcol = 0;
X        move(*pline, 0);
X        clrtoeol();
X        if (*pline + 1 < high)
X            {
X            move(*pline + 1, 0);
X            clrtoeol();
X            }
X        move(*pline, 0);
X        }
X        
X    if (ch == Xold_tty.sg_erase)
X    	{
X    	if (*pcol > 0)
X    	    {
X    	    (*pcol)--;
X    	    move(*pline, *pcol);
X    	    printw(" ");
X    	    move(*pline, *pcol);
X    	    }
X    	}
X    else if (ch != '\n' && ch != '\r')
X	{
X	printw("%c", ch);
X	(*pcol)++;
X	}
X	
X    refresh();
X    }
X
X
X/*
X *	catch_alarm
X *
X *	A dummy routine to catch alarms clock signals.
X */
Xvoid
Xcatch_alarm()
X    {
X    }
X
X
X/*
X *	do_child
X *
X */
Xvoid
Xdo_child()
X    {
X    unsigned char	in_str[2];
X    
X    signal(SIGINT, term_child);
X    signal(SIGKILL, term_child);
X    signal(SIGPIPE, term_child);
X    
X    in_str[0] = '\000';
X    while (read(0, &in_str[1], 1) > 0)
X    	{
X	write(Out_pipe, &in_str[0], 2);
X	write(Our_pipe, &in_str[1], 1);
X	}
X    term_child();
X    }
X    
X
X/*
X *	getutent
X *
X *	This is a reimplementation of the "getutent" routine based
X *	only upon the use given here.  This should be made more
X *	robust and made to match the manual page before being put
X *	into the Minix library.
X */
Xstruct utmp *
Xgetutent()
X    {
X    static int			fd = -1;
X    static struct utmp		udata;
X    
X    if (fd == -1)
X	{
X	if ((fd = open(UTMP, O_RDONLY)) < 0)
X	    return ((struct utmp *) NULL);
X        }
X        
X    if (read(fd, &udata, sizeof(struct utmp)) != sizeof(struct utmp))
X	{
X	close(fd);
X	fd = -1;
X	return ((struct utmp *) NULL);
X	}
X    return (&udata);	
X    }
X    
X
X/*
X *	term_child
X *
X *	This routine is called by the keyboard task for handling signals
X *	such as the interrupt signal.
X *
X *	It tells both mainline tasks to terminate.
X */
Xvoid
Xterm_child()
X    {
X    unsigned char	in_str[2];
X    
X    in_str[0] = '\000';
X    in_str[1] = 0xFF;
X    write(Our_pipe, &in_str[1], 1);
X    write(Out_pipe, &in_str[1], 1);
X    close(Out_pipe);
X    close(Our_pipe);
X    exit(0);
X    }
X    
X    
X/*
X *	terminate
X *
X *	This is called as an interrupt routine or for normal termination
X *	of the mainline code.
X */
Xvoid
Xterminate()
X    {
X    ioctl(0, TIOCSETP, &Xold_tty);
X    unlink(Out_fn);
X    if (Child != -1)
X	kill(Child, SIGKILL);
X    endwin();
X    printf("[Connection closed]\n");
X    close(In_pipe);
X    wait((int *) NULL);
X    exit(0);
X    }
X
/