[net.sources] talk - an alternative to write

john (12/21/82)

echo x - Makefile
cat >Makefile <<'!Funky!Stuff!'
#
# Makefile generated by jpn 7/22/82
#
# Specify -g in the cflags line to provide output for sdb.
CFLAGS=
#
HEADERS= talk.h
#
all: talk central page
#
talk : talk.c talk.h
	cc -o talk $(CFLAGS) talk.c -lcurses -ltermcap
#
central : central.c talk.h
	cc -o central $(CFLAGS) central.c
#
page : page.c
	cc -o page $(CFLAGS) page.c
#
!Funky!Stuff!
echo x - central.c
cat >central.c <<'!Funky!Stuff!'
/* Please read the documentation in talk.c */

#include "talk.h"
#include <signal.h>

char *chname;       /* name of the multiplexed file (channel) being used */

int channel; 		/* fd of channel file */
int online = 0;		/* count of connected users */

struct input_record
	{
	short index;
	short count;
	short ccount;
	char dta[80];
	}input;

struct output_record
	{
	short i;
	short c;
	short cc;
	char *d;
	}output;

struct player
	{
	int indx;
	int xpos;
	int ypos;
	char luid;
	char huid;
	} guys[16];

PACKET info;

/* addnew
 *       find a guy index for the channel index just connected
 */
addnew(index)
int index;
	{
	int i;
	for(i = 0; i < 16; ++i)
		{
		if (guys[i].indx == 0)
			{
			guys[i].indx = index;
			guys[i].xpos = 0;
			guys[i].ypos = 0;
			break;
			}
		}
	++online;
	return(i);
	}

/* whodat
 *       find the guy index associated with the channel index
 */
whodat(index)
int index;
	{
	int i;
	for(i = 0; i < 16; ++i)
		{
		if (guys[i].indx == index)
			return(i);
		}
	}

/* makechan
 * attempt to create channel
 */
makechan()
	{
	int i;
	int save;
	save = umask(0);
	channel = mpx(chname,0666);
	umask(save);

	if (channel < 0)
		{
		write(2,"cannot create channel - ABORTING central.\n",42);
		exit(1);
		}

	output.d = (char *) &info;

	/* set all guys to not connected state */
	for (i = 0; i < 16; ++i)
		guys[i].indx = 0;
	}

/* main
 *     one parameter MUST be given - the multiplexed file to use
 */
main(argc,argv)
int argc;
char **argv;
	{
	register int i;
	int temp;
	int guy;

	if (argc != 2)
		exit(1);

	chname = argv[1];

	/* Don't exit due to any signals! */
	signal(SIGHUP,SIG_IGN);
	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);
	signal(SIGALRM,SIG_IGN);
	signal(SIGTERM,SIG_IGN);
	signal(SIGTSTP,SIG_IGN);
	signal(SIGCONT,SIG_IGN);
	signal(SIGTTIN,SIG_IGN);
	signal(SIGTTOU,SIG_IGN);

	/* create channel for communication */
	makechan();
	while(1)
		{
		/* wait for something to happen */
		read(channel,&input,sizeof(struct input_record));
		if (input.count == 0)
			{
			/* signal - not data */
			switch (input.dta[0])
				{
			case M_WATCH:
				/* somebody new wishes to connect */
				attach(input.index,channel);
				guy = addnew(input.index);
				guys[guy].luid = input.dta[2];
				guys[guy].huid = input.dta[3];

				send(input.index,YOUARE,guy);
				for (i = 0; i < 16; ++i)
					{
					if (guys[i].indx != 0)
						{
						/* let others know */
						send(guys[i].indx,NEWGUY,guy);
						send(guys[i].indx,input.dta[2],input.dta[3]);
						if (guy != i)
							{
							/* let new guy know about any active players */
							send(guys[guy].indx,NEWGUY,i);
							send(guys[guy].indx,guys[i].luid,guys[i].huid);
							}
						}
					}
				break;
			case M_EOT:
				/* this is ignored, just someone clearing his pipe */
				break;
			case M_CLOSE:
			case M_SIG:
				/* somebody just quit */
				detach(input.index,channel);
				guy = whodat(input.index);
				guys[guy].indx = 0;
				--online;
				if(!online)
					{
					close(channel);
					unlink(chname);
					exit(0);
					}
				/* must tell everyone that this guy just quit on us */
				for (i = 0; i < 16; ++i)
					{
					if (guys[i].indx != 0)
						{
						send(guys[i].indx,KILLGUY,guy);
						}
					}
				break;
				}
			}
		else
			{
			/* data sent across channel */
			guy = whodat(input.index);
			sendall(guy,*(input.dta));
			}
		}
	}

/* send
 *     send the control,data pair to the channel index specified
 */
send(index,control,data)
int index;
int control,data;
	{
	output.i = index;
	output.c = 2;
	info.control = control;
	info.data = data;
	write(channel,&output,sizeof(struct output_record));
	}

/* sendall
 *        send the control,data pair to all active channel indexes
 */
sendall(control,data)
int control,data;
	{
	int i;
	for (i = 0; i < 16; ++i)
		{
		if (guys[i].indx != 0)
			{
			send(guys[i].indx,control,data);
			}
		}
	}
!Funky!Stuff!
echo x - page.c
cat >page.c <<'!Funky!Stuff!'
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <utmp.h>

#define WRITE 2
#define MAIL 0100
#define TRUE 1
#define FALSE 0

FILE *fopen();

char devicename[20] = "";
char username[20] = "";
char *userptr = 0;
char *getlogin();
char buffer[128];
int  multi = 0;

/* cmpare
 *       like strcmp except only returns TRUE or FALSE
 */
cmpare(string1,string2)
register char * string1;
register char * string2;
	{
	while(*string1)
		if(*string1++ != *string2++)
			return(0);
	if(*string2 == '\0')
		return(1);
	return(0);
	}

/* whereis
 *         search for person in /etc/utmp
 */
char * whereis(person,tty)
char * person;
char * tty;
	{
	struct utmp string;
	int i = 0;
	int fd;

	fd = open("/etc/utmp",0);

	while(read(fd,&string,20) > 0)
		{
		if(string.ut_line[0] == '\0' || string.ut_name[0] == '\0')
			continue;

		if(cmpare(person,string.ut_name)
					&& (tty == 0 || cmpare(tty,string.ut_line)))
			{
			strcpy(devicename,"/dev/");
			strcpy(&devicename[5],string.ut_line);
			if (tty == 0)
				{
				/* check for multiple logins */
				while(read(fd,&string,20) > 0)
					{
					if(cmpare(person,string.ut_name) && string.ut_line[0]!='\0')
						{
						multi++;
						}
					}
				}
			close(fd);
			return(devicename);
			}
		}
	close(fd);
	return(0);
	}

/* access
 *       check for terminal write permission
 */
access(filename,mode)
char *filename;
unsigned short mode;

	{
	struct stat buf;
	struct stat *bufp = &buf;

	stat(filename,bufp);
	if (buf.st_mode & mode)
		return(TRUE);
	return(FALSE);
	}

/* main
 *     one parameter is required - the login name to be paged
 */
main(argc,argv)
int argc;
char ** argv;
	{
	char * sendto;
	FILE *fd;

	if (argc != 2)
		{
		exit(1);
		}

	sendto = whereis(argv[1],0);

	userptr = getlogin();

	if (userptr)
		strcpy(username,userptr);

	if (sendto)
		{
		if (access(sendto,WRITE))
			{
			fd = fopen (sendto,"w");
			fprintf(fd,"%s on %s wants to talk to you via the talk program\n",username,ttyname(0));
			close(fd);
			if (multi)
				mailto(argv[1]);
			}
		else
			{
			mailto(argv[1]);
			}
		}
	else
		{
		exit(2);
		}
	exit(0);
	}

FILE *popen();
char mailline[] = "mail x               ";

/* mailto
 *     use mail notification to alert name
 */
mailto(name)
char *name;
	{
	register char *ptr;
	int i;
	FILE *fd;
	for(ptr = name, i = 5; *ptr; ++ptr, ++i)
		{
		mailline[i] = *ptr;
		}
	
	if ((fd = popen(mailline,"w")) == 0)
		{
		exit(0);
		}
	fprintf(fd,"%s wants to talk to you via the talk program\n",username);
	fprintf(fd,"Just run \"talk\" if it is not much later than this message time\n");
	close(fd);
	}
!Funky!Stuff!
echo x - talk.c
cat >talk.c <<'!Funky!Stuff!'
/* talk.c
 * talk is a program which allows communication between users that works
 * better than write using curses.  On startup, it will test if there is
 * a central program running, if not, it will start one up (note that the
 * lock routines will prevent starting up more than one central.)
 * it will then fork into two processes, number one polling the keyboard
 * and sending data to the multiplexed file, and number two polling the
 * multiplexed file, and updating the display as necessary.  The display
 * consists of a number of user's areas (LINESPER lines long each) which
 * will be updated whenever a user types a key.
 *
 * note that there are a number of installation dependent parameters -
 * lockfile,linkto,defchan,central, and altcent.
 *
 * lockfile and defchan must be (normally nonexistant) files in a directory
 * writable by all.  Talk will take two different types of command line
 * arguments.  The first is just the login name of someone to notify that
 * a talk session is wanted.  the second type must be prefaced by -c and
 * should be the (full path) name of a (normally nonexistant) file in a
 * directory which is writable by all who will use this channel.
 *
 * The only requirement on linkto is that it is on the same filesystem
 * as lockfile, and that it always exists!
 *
 * central and altcent are the pathname and alternate pathname for the
 * central.c program.
 * 
 */

#include "talk.h"
#include <signal.h>
#include <curses.h>

#define STDIN 0
#define STDOUT 1
#define STDERR 2

#define XWRAP 78		/* position for x to wrap around */
#define XDISPOS 8		/* window's beginning of line */
#define LINESPER 4		/* # lines per talker */

int channel;			/* multiplexed file descriptor */
int locked  = 0;

char errmsg[] = "Some error occurred starting up Talk.\n";
char wait[] = " Please wait ....";

/*************** installation dependant parameters *****************/

                  /* file whose's existance provides locking mechanism */
                  /* MUST be in a directory writable by all! */
char lockfile[] = "/usr/spool/talk/cblock";

                  /* file which lockfile is linked to */
                  /* MUST be on same filesystem as lockfile */
char linkto[] =   "/usr/local/linkto";

                  /* the default channel to use.  See note above!!! */
                  /* MUST be in a directory writable by all! */
char defchan[] =  "/usr/spool/talk/channel";

                  /* pathname of the central program */
char central[] =  "/usr/local/central";
char altcent[] =  "/usr/local/central";

                  /* pathname of the page progam */
char pager[]   =  "/usr/local/page";

/************* end of installation dependant parameters ***************/

/* structure for keeping track of each window for each person (guy) */
#define MAXGUYS 16

WINDOW *win[MAXGUYS];
int yname[MAXGUYS];
WINDOW *msgwin;
WINDOW *pagewin;

int ppid;               /* parent process id */
char *chname;           /* channel name being used */
char pagename[50];      /* login name of person being paged */

PACKET info;            /* communication packet from central program */

int iam = 0;            /* the guy index that I have been assigned */

/* bye
 *     the normal cleanup exit routine
 */
bye()
	{
	close(channel);
	if (locked)
		unlock();
	mvcur(0,COLS-1,LINES-1,0);
	/* refresh(); */
	endwin();
	exit(0);
	}


/* main
 *      main takes as arguments the names of the people who should
 *      be paged.  Also, an alternate channel may be requested by
 *      a command line of the form:  talk -c altchannel
 *      where altchannel is the full pathname of the file to use
 *      as the alternate mutiplexed file.
 */
main(argc,argv)
int argc;
char **argv;
	{
	int gotdata();
	char c;
	int i;

	chname = defchan;
	/* parse command line strings before fucking around with the screen */
	for (i = 1; i < argc; ++i)
		{
		if (strcmp("-c",argv[i]) == 0)
			{
			/* alternate channel request */
			++i;
			if (i >= argc)
				break;
			chname = argv[i];
			}
		else
			{
			if (vfork() == 0)
				{
				execl(pager,"page",argv[i],0);
				_exit(0);
				}
			}
		}

	/* initialize windows */
	for(i = 0; i < MAXGUYS; ++i)
		win[i] = 0;

	/* set up curses */
	initscr();
	leaveok(stdscr,1);
	refresh();

	/* allocate top line for error messages */
	msgwin = newwin(1,0,0,0);
	leaveok(msgwin,1);

	/* allocate bottom line for pageing */
	pagewin = newwin(1,0,LINES-1,0);
	leaveok(pagewin,0);

	lock();

	/* do not allow user to kill program until after channel is created */
	signal(SIGHUP,SIG_IGN);
	signal(SIGINT,SIG_IGN);
	signal(SIGQUIT,SIG_IGN);
	signal(SIGALRM,SIG_IGN);
	signal(SIGTERM,bye);
	signal(SIGTSTP,SIG_IGN);
	signal(SIGCONT,SIG_IGN);
	signal(SIGTTIN,SIG_IGN);
	signal(SIGTTOU,SIG_IGN);

	ppid = getpid();

	/* one character at a time from the terminal */
	crmode();
	noecho();

	channel = open(chname,2);
	if (channel < 0)
		{
		if (vfork() == 0)
			{
			execl(central,"central",chname,0);
			execl(altcent,"central",chname,0);
			clear();
			waddstr(msgwin,errmsg);
			wrefresh(msgwin);
			signal(ppid,SIGTERM);
			_exit(1);
			}
		wclear(msgwin);
		waddstr(msgwin,wait);
		wrefresh(msgwin);

		sleep(5);	/* allow time for central to run and create channel */
		channel = open(chname,2);
		if (channel < 0)
			{
			wclear(msgwin);
			waddstr(msgwin,errmsg);
			wrefresh(msgwin);
			bye();
			}
		}

	/* force ^C ^Y etc to exit cleanly */
	signal(SIGHUP,bye);
	signal(SIGINT,bye);
	signal(SIGQUIT,bye);
	signal(SIGALRM,bye);
	signal(SIGTERM,bye);
	signal(SIGTSTP,bye);
	signal(SIGCONT,bye);
	signal(SIGTTIN,bye);
	signal(SIGTTOU,bye);

	unlock();

	clear();

	if (fork() == 0)
		{
		/* get input only */
		while(1)
			{
			if (read(STDIN,&c,1) < 0)
				{
				kill(ppid,SIGINT);
				exit(0);
				}
			if (c == '\032')
				{
				kill(ppid,SIGINT);
				exit(0);
				}
			if (c == '\n' || c == '\b' || c == '\177' || c == '\014'
				|| c == '\002' || c == '\005')
				write(channel,&c,1);
			else if (c == '\020')	/* ^P pager */
				{
				int savx,savy;

				wclear(pagewin);
				waddstr(pagewin,"Who do you wish to page? ");
				wrefresh(pagewin);

				for(i = 0; ; ++i)
					{
					read(STDIN,&(pagename[i]),1);
					if(pagename[i] == '\177' || pagename[i] == '\b')
						{
						if (i > 0)
							{
							getyx(pagewin,savy,savx);
							wmove(pagewin,savy,savx - 1);
							i -= 2;
							wrefresh(pagewin);
							continue;
							}
						}
					if(pagename[i] == '\n')
						{
						pagename[i] = '\0';
						break;
						}
					waddch(pagewin,pagename[i]);
					wrefresh(pagewin);
					}
				wclear(pagewin);
				wrefresh(pagewin);
				write(channel,"\014",1);
				if (vfork() == 0)
					{
					execl(pager,"page",pagename,0);
					_exit(0);
					}
				}
					
			else if (c < ' ')
				; /* throw away this control character */
			else
				write(channel,&c,1);
			}
		}
	else
		{
		/* display results only */
		while(1)
			{
			read(channel,&info,sizeof (PACKET));
			switch(info.control)
				{
				case YOUARE:
					/* signal from cental specifying my index */
					iam = info.data;
					break;
				case KILLGUY:
					/* someone exited */
					killguy(info.data);
					break;
				case NEWGUY:
					/* new person running the program */
					newguy(info.data);
					break;
				default:
					/* must be data */
					putdat(info.data,info.control);
					break;
				}
			}
		}
	}

/* myrefresh
 *       restore cursor position to the iam window
 */
myrefresh()
	{
	int savx,savy;

	refresh();
	if (win[iam] != 0)
		{
		wrefresh(win[iam]);
		}
	}

/* killguy
 *       someone running the program has exited.  His window will be erased
 */
killguy(guy)
int guy;
	{
	int i;
	werase(win[guy]);
	wrefresh(win[guy]);
	delwin(win[guy]);
	win[guy] = 0;

	move(yname[guy],0);
	addstr("*EOF*");
	clrtoeol();
	myrefresh();
	}

/* newguy
 *        someone new has started up talk.  a new window must be allocated
 *        and initialized.
 */
newguy(guy)
int guy;
	{
	int y, uid, i;
	int savx,savy;
	char pwbuf[120];

	pwbuf[0] = '\0';

	/* read the process id of the newguy from the channel */
	read(channel,&info,sizeof (PACKET));
	uid = (255 & info.control) + ((255 & info.data) << 8);

	/* calculate new window */
	y = (LINES-1) - ((guy + 1) * LINESPER);

	win[guy] = subwin(stdscr, LINESPER - 1, COLS - XDISPOS - 2, y + 1, XDISPOS);
	yname[guy] = y;
	scrollok(win[guy],1);

	leaveok(win[guy],0);
	/* leaveok(win[guy],guy != iam); */

	/* get the login name */
	getpw(uid,pwbuf);
	for (i = 0; pwbuf[i] != ':' ; ++i)
		;
	pwbuf[i] = 0;

	/* update the screen */
	move(y,0);
	addstr(pwbuf);
	clrtoeol();
	myrefresh();
	}

/* putdat
 *       just character data.  Update the display as required
 */
putdat(dat,guy)
int guy,dat;
	{
	int savx,savy;
	register WINDOW *ptr;

	ptr = win[guy];

	switch (dat)
		{
		case '\177':
		case '\b':
			getyx(ptr,savy,savx);
			wmove(ptr,savy, savx > 0 ? savx - 1 : 0);
			break;
		case '\014':
			/* refresh request */
			if (guy == iam)
				{
				wrefresh(curscr);
				myrefresh();
				}
			break;
		case '\n':
		case '\015':
			waddch(ptr,'\n');
			wstandend(ptr);
			break;
		case '\002':
			wstandout(ptr);
			break;
		case '\005':
			wstandend(ptr);
			break;
		default:
			/* this is a kludge to avoid the double scroll generated */
			/* on end of line wraparound ! */
			if (ptr->_curx >= ptr->_maxx - 1)
				{
				waddch(ptr,'\n');
				wstandend(ptr);
				}
			waddch(ptr,dat);
			break;
		}
	wrefresh(ptr);
	myrefresh();
	}

/* lock
 *     lock will not return until no one else is locked
 */
lock()
	{
	locked = 1;
	while(link(linkto,lockfile) < 0)
		{
		wclear(msgwin);
		waddstr(msgwin,wait);
		wrefresh(msgwin);
		sleep(2);
		}
	}

/* unlock
 *       unlock allows others to lock
 */
unlock()
	{
	unlink(lockfile);
	locked = 0;
	}
!Funky!Stuff!
echo x - talk.h
cat >talk.h <<'!Funky!Stuff!'
#include <sys/mx.h>

#define YOUARE	0x42
#define NEWGUY	0x43
#define KILLGUY	0x44
#define DATA	0x45

#define PACKET struct packet
PACKET
	{
	char control;
	char data;
	};
!Funky!Stuff!
echo x - READ_ME
cat >READ_ME <<'!Funky!Stuff!'
	Talk is a program designed to let multiple people communicate
with each other using a window mechanism where each person is assigned a
window which will be updated on all sceens running the program whenever
a key is struck.  Note that talk is still somewhat experimental.  The
current version uses multiplexed files as the communication medium,
and since only Berkley 4.x systems have multiplexed files, talk will
not run on all sites on this net.  I intend to update talk to run
under BSD4.2 as soon as we get that system running here.

Brief Discription:

	When running talk, the cursor should always remain at your own
window's current cursor position.  Talk is written to use the curses
package, and will work on any terminal that has a direct cursor
addressing.

	As currently set up in talk.c, each user is assigned a three line
window which may be modified, as well as one line above his window which
will display the login name of the user id running the program.  As each
character is typed, the cursor position is incremented by one, wrapping
around to the next line if necessary.  If the end of the window is reached,
the window will scroll to provide a new line to type in.

	Most control characters are intercepted and thrown away.  The
only control characters recognized are backspace, delete, return, and
^P, ^B and ^E.  Backspace and delete work identically, and simply decrement
the cursor position without erasing the character at that position.  Return
will put the cursor at the beginning of the next line, and erase the rest
of the line.  Control P will invoke the paging sub-program after prompting
for the login name of the person who is to be paged. The paging program
first looks for the terminal that the person is using (via /etc/utmp,) and
then checks the write permission on that terminal.  If write permission is
turned on, page will directly output a message to the terminal. If write
permission is turned off, or if the person is logged in more than once,
mail will be sent to that person.

	I forgot to mention that the signals TSTP and KILL are intercepted
and will cause talk to exit cleanly.  We use ^Z as end-of-file, so ^Z is
also recognized as being an exit signal.

	Talk occasionally has problems.  These problems seem to be
limitations in the ability of the muliplexed file facility to deal
with a large flow of information.  When too many people are typing
simultaneously (or the system is particularly slow) characters typed at
the keyboard will be lost completely.  This seems to be caused by the
talk program writing to a channel which is already full, causing the
system to throw away the data.  This seems like a stupid thing for the
system to do, but there you are.  Once I was able to hang up the whole
talk system by having three people hold down auto-repeating keys
simultaneously.  Apparently, the multiplexed file became so clogged
that it was not able to recover properly.

	If talk should hang up completely, the solution is to kill
all processes associated with talk (including the "central" program)
and then remove the lock file.  Subsequent instances of talk will
run properly.  Currently there is no way to do this automatically,
it must be done by hand.  The easiest way to avoid the problem is
to have only one user typing at a time, and for no one to hold down
auto-repeating keys

	Talk can also take a filename "alternate channel" parameter.
This allows an alternate conversation to be taking place, or it will
allow a "secret" conversation if the persons running are the only ones
who can access the multiplexed file.  Any other parameters given are
assumed to be persons who are to be "paged".

	Note: Before compiling this program, be sure that you modify
the installation dependant parameters in the first part of talk.c!!!
Be sure to read the header comments at the beginning of talk.c.
!Funky!Stuff!
echo x - talk.1
cat >talk.1 <<'!Funky!Stuff!'
.TH WIDE 1 talk
.SH NAME
talk \- communications program
.SH SYNOPSIS
.B talk [-c channel] [login-name ...]
.SH DESCRIPTION
.I Talk
will configure your terminal into a series of windows each prefaced by the
login name of the person who currently "owns" that window. 
Your window
will have the cursor displayed at the current typing position.  Whenever
anyone running
talk types a character,  it will be simultaneously displayed
on all terminals running
talk.  Most control characters are intercepted and thrown away, but a few
are recognized by
talk as being control functions.
.PP
.I channel
is the (full pathname) file which is to be used to create an alternate
"channel".  If this parameter is given, it must be a file (normally
nonexistant) on a directory which is writable by all who will use this
channel.  This parameter should not normally be used unless you want to
have a private conversation.
.PP
.I
person
is the login name of someone who you want to be notified that a talk
session is requested.  Notification is done by writing to that person's
terminal if possible, or by sending mail if not.
.PP
Control characters recognized by talk:
.PP
^C, ^Z and ^Y will all terminate your copy of
talk, and will erase your name from any other
terminal that is still running it.
.PP
<RETURN> will start a new line,scrolling your window if necessary
.PP
^P will page someone in the same manner as the command line argument
(the name will be prompted for.) 
.PP
^B will begin displaying the characters you type as BOLD.  Bold mode ends
when you type <RETURN> or ^E, or if the line should wrap around.
.PP
^E will end Bold mode.
.PP
^L will redraw the screen if it should be altered by other programs.
.PP
<BACKSPACE> and <DELETE> will both allow you to back up the cursor so
that you can correct typing errors.
.SH AUTHOR
John P. Nelson
.SH FILES
/usr/spool/talk/cblock
.br
/usr/local/linkto
.br
/usr/spool/talk/channel
.br
/usr/local/central
.br
/usr/local/page
.SH BUGS
Occasionally, when too many people are typing simultaneously, talk will
become "clogged" and will lose characters.
.PP
Even worse, if two people hold down auto-repeating keys, talk will "hang"
and nothing more can be typed.  The solution is to exit talk and start it
up again.
!Funky!Stuff!