Path: utzoo!utgpu!jarvis.csri.toronto.edu!mailrus!uwm.edu!rpi!brutus.cs.uiuc.edu!apple!limbo!taylor From: taylor@limbo.Intuitive.Com (Dave Taylor) Newsgroups: alt.sources Subject: BETA RELEASE: new version of "write" Message-ID: <183@limbo.Intuitive.Com> Date: 13 Nov 89 20:49:12 GMT Reply-To: taylor@limbo.Intuitive.Com (Dave Taylor) Organization: Intuitive Systems, Mountain View, CA: +011 (415) 966-1151 Lines: 905 The enclosed is a new version of the "write" program that I have written to smooth some of the rough interface and algorithm edges in the original. This is currently a bit HP-UX specific in its emulation of the (much nicer) BSD tty driver. It should prove to be pretty portable though, and if you read the notes included, you'll see it's easy to build for other machines. Highlights of the new program include: o An "echo" character-by-character transmission mode a la the popular 'talk' program. o A sophisticated algorithm for finding the best line to connect to, rather than just the first in the /etc/utmp file. o Some nifty starting options for added functionality. In summary, it isn't a dramatic new rewrite, but rather a nice new version (written from scratch, for those of you nervous about the origin of commands) that adds some needed functionality. THIS IS A BETA VERSION in the sense that I've only ever run it on my HP-UX 9000/300 computer running HP-UX release 6.5. I make no guarantee that it'll run on any other platform, but I encourage people to try it and make any changes necessary to have it work on your local platform. If there's enough interest/change I would like to submit this to "comp.sources.unix" too at some point. Enjoy! -- Dave Taylor Intuitive Systems Mountain View, California taylor@limbo.intuitive.com or {uunet!}{decwrl,apple}!limbo!taylor -- Attachment: # This is a shell archive. Remove anything before this line, # then unpack it by saving it in a file and typing "sh file". # # Wrapped by Dave Taylor <taylor@limbo> on Mon Nov 13 12:40:33 1989 # # This archive contains: # README write.1 write.c # # Error checking via wc(1) will be performed. # Error checking via sum(1) will be performed. PATH=/bin:/usr/bin:$PATH; export PATH if sum -r </dev/null >/dev/null 2>&1 then sumopt='-r' else sumopt='' fi echo x - README cat >README <<'@EOF' This is a new version of the old Unix standard "write" program intended to improve on some of the rough interface edges of the original. It is currently somewhat wired to an HP computer running HP-UX, however, and might be a bit funky to run on other machines. To compile the program, simply: cc -O write.c -o write or, if you're on a BSD machine: cc -O -DBSD write.c -o write When it's compiled, simply install it in your favorite shared directory, and copy the man page into your local man directory. Bugs, ideas, suggestions, hate mail, etc, please send to me directly at: taylor@limbo.intuitive.com -- Dave Taylor Nov 12, 1989 @EOF set `sum $sumopt <README`; if test $1 -ne 57913 then echo ERROR: README checksum is $1 should be 57913 fi set `wc -lwc <README` if test $1$2$3 != 22111650 then echo ERROR: wc results of README are $* should be 22 111 650 fi chmod 644 README echo x - write.1 cat >write.1 <<'@EOF' .TH WRITE 1L "" "" .SH NAME write \- interactively talk with another user .SH SYNOPSIS .B write [ .I\ -c ] .I user .sp .B write [ .I \-et ] [ .I "-l line" ] .I user .SH DESCRIPTION This new version of the traditional Unix .I write program copies lines from your terminal to another user on the computer. .PP Unlike the original program, however, this can let you interact on a character-by-character basis, as well as line-by-line. Also, the algorithm for choosing which of multiple login sessions has greatly improved, with the program now choosing the most-recently-active of the interactive sessions the "writee" is connected to. .PP A request from the .I write program appears similar to the following: .PP .RS Write requested by Dave Taylor at Monday, Nov 13, 1989 at 11:48 am \t(to respond, please type "write -e taylor" on the command line) .RE .PP Notice that the indication of how to respond also indicates whether or not the person requesting a .I write with you chose to use the (\fIecho\fR) character-by-character transmission mode: the presence of .I \-e indicates that they did. .PP When you have successfully connected to the other users terminal line, the program indicates that by informing you similar to: .PP .RS Okay, you're connected to user taylor. Enter text to send, ^D to quit .RE .PP And upon termination of the connection, both sides see the indication .I "<end-of-transmission>" for each person. .PP If you don't want people to bother you with .I write or .I talk requests, use the .I mesg(1) command to disable writing to your terminal. .PP Since communication of this nature can be quite confusing and jumbled, it is suggested that people stick with the protocol used by radio operators, with "over" and "over and out" being used to indicate the end of a transmission and the end of the session, respectively. Most commonly, these are abbreviated "-o" for "over", and "-oo" for "over and out". .SH OPTIONS The .I write program has the following set of options available: .TP .B "-c" Check for user sessions. This option is an easy way to ascertain if the requested user is logged in, and if so which of their possibly multiple sessions are active. Output is of the form: .RS .in +.25i For user "\fIusername\fR": .br .in +.25i Line tty1 is active .in -.50i .RE .TP .I " " and upon completion of the output, the .I write command quits (e.g. it does not send a message to the other user or otherwise do any of the normal work of the program). .TP .B "-e" Puts the .I write program into .I echo or character-by-character transmission mode. In this mode the person on the other end can see what you're typing, character by character, rather than having to wait for you to complete an entire line. This is the recommended, though not default, way of interacting with another user. .TP .B "-l" \fIline\fR If you want to connect to a specific line instead of letting the .I write program use the builtin algorithm for ascertaining the best line to connect, you can specify the device name with this option. .TP .B "-t" If you'd rather that the person you are requesting the talk from isn't informed of what tty line you're on (which is almost always unnecessary now since the program can pick the most active session, which, if you've just started a .I write is most likely to be the right one). .SH EXAMPLES Let's say that we want to talk to our friend Scott. We can easily do this by typing: .IP .B write scott .P Which will result in Scott seeing a message similar to that shown above. As soon as we've connected, we'll get a message from the .I write program to that effect. At this point .I "anything we type will be displayed on Scott's" .I "terminal too," so we need to be careful! It's usually best just to sit and wait for a minute or two to give the other party time to reply. .PP Scott chooses to connect to us by using the .I write command, and so we promptly see: .PP .RS Write requested by Scott McGregor at Monday, Nov 13, 1989 at 11:53 am \t(to respond, please type "write -e scott" on the command line) .RE .PP which indicates that, since we're already hooked up to him, the two way communication is established. Scott says hi by typing it on his keyboard, and we can imagine the following interaction (with Scott's typing in italics): .PP .in +.25i .nf .I "Hi Dave! How's it going? -o" Not bad. How about with you? -o .I "Quite well. Real busy though. Can we talk later? -o" sure. sorry for the interruption. -oo .I "-oo" <end of transmission> .fi .in -.25i .SH SEE ALSO mail(1), mesg(1), talk(1). .SH AUTHOR Dave Taylor, Intuitive Systems .br <taylor@limbo.Intuitive.Com> .SH NOTES This program uses a modified .I gets(3) routine to do raw mode reads, and might well be HP-UX specific. If you're not on an HP computer, the behaviour of this routine might be most peculiar... .PP People on HP computers, however, may realize while in this program that it offers the functionality of the BSD tty driver, to wit the use of "^W" to erase a word, and "^R" to rewrite the line again. Why they're not part of the standard System V tty driver... .PP This program does, however, run on any computer; if you're not on one that understands the HP-UX 'raw mode' magic, then simply compile it with "-DBSD" to use your standard system .I "gets()" routine. .PP I am indebted to Marvin Raab for suggesting that this type of a .I write program would be a nice addition to the system, and to Jim Davis for testing it out on some other hardware. @EOF set `sum $sumopt <write.1`; if test $1 -ne 6381 then echo ERROR: write.1 checksum is $1 should be 6381 fi set `wc -lwc <write.1` if test $1$2$3 != 1979815520 then echo ERROR: wc results of write.1 are $* should be 197 981 5520 fi chmod 644 write.1 echo x - write.c cat >write.c <<'@EOF' /** write.c **/ /** A new version of the Unix "write" utility that takes advantage of some local environment changes. Most notable change, other than it being friendlier and easier to use, is that when someone is logged in more than once it will connect to the window with the least idle time (that isn't "mesg n") rather than just blindly the first in the /etc/utmp file. (C) Copyright 1989 Dave Taylor (C) Copyright 1989 Intuitive Systems All rights reserved. Possible environment options: BSD to attempt to work in the BSD environment **/ #include <sys/types.h> #include <sys/stat.h> #include <utmp.h> #include <pwd.h> #include <time.h> #include <fcntl.h> #include <signal.h> #include <stdio.h> #ifndef TRUE # define TRUE 1 # define FALSE 0 #endif #define MAX_LOGINS 30 /* max for an individual user */ #define SLEN 256 #define DEVICE_NAME_TEMPLATE "/dev/%s" /** forward declarations **/ long get_idle(); char *get_best_line(), *current_time(), *getenv(), *ttyname(), *readline(); int compare(); /** static data buffers **/ char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; char *days[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; /** global variables **/ struct login_entry { char line[20]; long idle; } login_record[MAX_LOGINS]; FILE *remote_fd; /* the other persons (/dev) tty line */ char requested_user[20], /* whom we want to talk with */ remote_line[40] = { "" }, /* .. what tty line they're on? */ who_we_are[20], /* who we are, login/session name */ fullname[40]; /* .. our full name from /etc/passwd */ char our_tty_line[20]; /* .. and what tty line we're on */ int sessions = 0, /* number of sessions user is running */ raw_echo = 0, /* echo char by char or line by line? */ show_our_tty_line = 0, /* tell them our tty line? */ show_user_idle_time_only = 0; /* only show user idle times? */ extern int optind; /* these are for the getopt() routine */ extern char *optarg; /* .. used in main() below */ main(argc, argv) int argc; char **argv; { struct utmp *utmp_buffer, *getutent(); struct passwd *pwentry, *getpwnam(); char *line_to_use, *cp, c; long idle_time; int unreachable_login = 0; /** deal with the starting arguments **/ while ((c = getopt(argc, argv, "cel:t")) != EOF) { switch (c) { case 'c' : show_user_idle_time_only = TRUE; break; case 'e' : raw_echo = TRUE; break; case 'l' : strcpy(remote_line, optarg); break; case 't' : show_our_tty_line = TRUE; break; default : usage(0); break; } } if (optind == argc) usage(0); strcpy(requested_user, argv[optind]); /** now let's go into the UTMP file and load our data structure **/ while ((utmp_buffer = getutent()) != NULL) { if (utmp_buffer->ut_type != USER_PROCESS || strcmp(utmp_buffer->ut_name, requested_user) != 0) continue; /* make sure there is some reasonable idle time (e.g. no error) */ if ((idle_time = get_idle(utmp_buffer->ut_line)) != (long) -1) { strcpy(login_record[sessions].line, utmp_buffer->ut_line); login_record[sessions++].idle = idle_time; } else unreachable_login++; } endutent(); /** if we can't find the lad or lass, then ... **/ if (sessions == 0) { if (unreachable_login) printf("Sorry, but user %s is not currently allowing messages.\n", requested_user); else printf("Sorry, but user '%s' is not currently logged in.\n", requested_user); exit(1); } /** figure out who we are and what line we're on... **/ if ((cp = getenv("LOGNAME")) != NULL) strcpy(who_we_are, cp); else if ((cp = getenv("USER")) != NULL) strcpy(who_we_are, cp); else { fprintf(stderr, "Can't figure out who you are. Sorry...\n"); exit(1); } /* get our full name from the password file */ if ((pwentry = getpwnam(who_we_are)) == NULL) strcpy(fullname, who_we_are); else strcpy(fullname, pwentry->pw_gecos); /* and what tty line we're on */ if ((cp = ttyname(0)) == NULL) { fprintf(stderr, "Can't figure out what line you're on. Sorry...\n"); exit(1); } else strcpy(our_tty_line, cp); /** otherwise, let's get the best line to try them on... **/ line_to_use = get_best_line(); /** now let's hit that line and go wild... **/ if (! show_user_idle_time_only) initiate_and_run_write_session(line_to_use); /** and we're done. **/ exit(0); } initiate_and_run_write_session(line) char *line; { /** this does all the actual work associated with the transfer of information from the local user to the remote, including opening up the line and spitting out the startup banner. **/ char filename[40], input_line[SLEN]; int exit_gracefully(), signal_restarting(); sprintf(filename, DEVICE_NAME_TEMPLATE, line); if ((remote_fd = fopen(filename, "w")) == NULL) { fprintf(stderr, "Internal error from 'write': can't write to '%s'\n", filename); exit(1); } /** the hello banner... **/ fprintf(remote_fd, "\n\r\n\rWrite requested by %s at %s\n\r", fullname, current_time()); if (show_our_tty_line) fprintf(remote_fd, "\t(to respond, type \"write %s-l %s %s\" on the command line)\n\r\n\r", raw_echo ? "-e " : "", (char *) our_tty_line + 5, who_we_are); else fprintf(remote_fd, "\t(to respond, please type \"write %s%s\" on the command line)\n\r\n\r", raw_echo ? "-e " : "", who_we_are); printf( "Okay, you're connected to user %s. Enter text to send, ^D to quit\n\n", requested_user); /** and now, the main loop... **/ (void) signal(SIGQUIT, exit_gracefully); (void) signal(SIGINT, exit_gracefully); (void) signal(SIGCONT, signal_restarting); while (readline(input_line) != NULL) { if (! raw_echo) fprintf(remote_fd, "%s\n\r", input_line); } fprintf(remote_fd, "<end-of-transmission>\n\r"); } char * get_best_line() { /** use the information we've gained to hit the best of the possible lines. Return its name at this point ... and if the user specified a tty line, try to use that instead or fail if not a good combo. **/ register int i, matched = 0; /** 1. sort information by idle time so newest is up top **/ qsort(login_record, sessions, sizeof(login_record[0]), compare); /** then show what we've gotten so far, shall we? **/ if (show_user_idle_time_only) /* if they're interested */ show_idle_times(); /** if the user specified a tty line, let's see if the person we want is *on* that line... **/ if (remote_line[0] != '\0') { for (i=0; i < sessions; i++) { if (strcmp(remote_line, login_record[i].line) == 0) matched++; } if (! matched) { /* nope! */ printf("User '%s' doesn't appear to be logged in to line '%s'.\n", requested_user, remote_line); exit(1); } return( (char *) remote_line ); /* yep! */ } /** othrewise simply return the top record, or least idle line **/ return( (char *) login_record[0].line ); } show_idle_times() { register int i, hours; printf("\nFor user '%s':\n", requested_user); for (i = 0 ; i < sessions; i++) { if (login_record[i].idle < 10) printf("\tline %s is active\n", login_record[i].line); else if (login_record[i].idle < 60) printf("\tline %s has %d seconds idle time\n", login_record[i].line, login_record[i].idle); else if (login_record[i].idle < 3600) printf("\tline %s has %d minutes idle time\n", login_record[i].line, (login_record[i].idle / 60)); else { hours = login_record[i].idle / 3600; printf("\tline %s has %d hours and %d minutes idle time\n", login_record[i].line, hours, (login_record[i].idle - (hours * 3600)) / 60); } } putchar('\n'); } long get_idle(line) char *line; { /** Given the name of a tty line, get the idle time for that line by doing a stat on that device and comparing it with the current time. Returns the difference between the two... Slightly more useful level of sophistication added: this routine will also check the permissions on the line and will return (-1) if it does *NOT* have write permission to group and other (which indicates that "mesg n" has been typed for that window/line) **/ long thetime; char filename[80]; struct stat statbuffer; thetime = time( (long *) 0); sprintf(filename, DEVICE_NAME_TEMPLATE, line); if (stat(filename, &statbuffer) != 0) return( (long) -1); if (((statbuffer.st_mode & S_IWGRP) == 0) || ((statbuffer.st_mode & S_IWOTH) == 0)) return( (long) -1); /* user doesn't have messages enabled! */ else return( thetime - statbuffer.st_atime ); } int compare(a, b) struct login_entry *a, *b; { /** compare two login structures for qsort. Returns a similar value to what strcmp() would return, only in this we are interested in idle time, not names... **/ return ( (int) (a->idle - b->idle) ); } exit_gracefully() { /** to more pleasantly deal with the user quitting with ^C **/ if (raw_echo > 1) rawmode(0); fprintf(remote_fd, "\n\r<end-of-session>\n\r"); printf("<left with an interrupt. Goodbye!>\n"); exit(0); } signal_restarting() { int signal_restarting(); if (raw_echo > 1) { rawmode(0); rawmode(1); /* force it to be turned on regardless of mode */ printf("<back!>\n"); } (void) signal(SIGCONT, signal_restarting); } char * current_time() { /** return a string containing the current date and time, but in a more readable format that what ctime() returns. **/ long thetime; struct tm *timerec, *localtime(); static char buffer[40]; thetime = time( (long *) 0); /** now let's get that into a ctime structure... **/ timerec = localtime(&thetime); sprintf(buffer, "%s, %s %d, 19%d at %d:%.2d %s", days[timerec->tm_wday], months[timerec->tm_mon], timerec->tm_mday, timerec->tm_year, (timerec->tm_hour > 12 ? timerec->tm_hour - 12 : timerec->tm_hour), timerec->tm_min, (timerec->tm_hour > 11 ? "pm" : "am") ); return( ( char *) buffer); } usage(exit_value) int exit_value; { /** This outputs a friendly and informative usage message. **/ printf("\nUsage: write [-c] [-e] [-l line] [-t] username\n\n"); printf("Where:\n"); printf( "\t-c \trequests a simple check of that user: does not connect\n"); printf( "\t-e \techo (transmit) char-by-char instead of line-by-line\n"); printf("\t-l X \tconnects to the specified user on tty line 'X'\n"); printf( "\t-t \tspecifies that the remote user should be informed of\n"); printf( "\t \twhich tty line you're logged in to (default is least idle)\n"); printf( "\nThis version of \"write\" ensures that you will always connect to the tty\n" ); printf( "line with the least idle time, if the specified user is logged in to more\n"); printf( "than one line. Therefore, the default is not to even show tty lines on\n"); printf("connect...\n\n"); exit(exit_value); } /****** The following code is ripped out of the RAYS software package ******/ /** readline.c **/ /** This routine is used to emulate the "gets()" routine as if we had the BSD tty driver that knows about ^W word deletion and ^X line deletion...on machines that already have this we don't need it. so (C) Copyright 1987, Dave Taylor (C) Copyright 1987, Hewlett-Packard Laboratories **/ #ifndef BSD #include <termio.h> #define output_char(c) { \ if (raw_echo) { \ if (c == '\n') putc('\r', remote_fd); \ putc(c, remote_fd); fflush(remote_fd); \ } \ putchar(c); \ } #define isspace(c) (c == ' ' || c == '\t') #define isstopchar(c) (c == ' ' || c == '\t' || c == '/') #define ctrl(c) (c - 'A' + 1) #define erase_a_char() { output_char(BACKSPACE); output_char(' '); \ output_char(BACKSPACE); fflush(stdout); } #define TTYIN 0 #define BACKSPACE '\b' #ifndef TRUE # define TRUE 1 # define FALSE 0 #endif #define ON 1 #define OFF 0 static int _in_rawmode = FALSE; struct termio _raw_tty, _original_tty; char *readline(buffer) char *buffer; { /** this routine understands ^W to delete back to a stopchar, ^X or ^U to delete the entire line, ^R to request a rewrite of the line so far, and ^M/^J to end the input line. This routine returns either a pointer to the line input or a NULL if the user hit a ^D or we otherwise got an EOF(stdin). Finally, we also hop in and out of raw mode in this routine, thereby mucking with the terminal tty settings... **/ char ch; register int index = 0; if (raw_echo++ == 1) /* once it's on, we'll leave it on ... */ rawmode(ON); do { ch = getchar(); if (ch == ctrl('D') || feof(stdin)) { /* we've hit EOF */ rawmode(OFF); output_char('\n'); return((char *) NULL); } switch (ch) { case BACKSPACE: if (index > 0) { output_char(BACKSPACE); index--; } output_char(' '); output_char(BACKSPACE); fflush(stdout); break; case '\n' : case '\r' : buffer[index] = '\0'; if (! raw_echo) rawmode(OFF); output_char('\n'); return((char *) buffer); case ctrl('W'): if (index == 0) break; /* nothing to do */ index--; if (buffer[index] == '/') { /* special case. */ erase_a_char(); break; } while (index >= 0 && isspace(buffer[index])) { index--; erase_a_char(); } while (index >= 0 && ! isstopchar(buffer[index])) { index--; erase_a_char(); } index++; break; case ctrl('R'): buffer[index] = '\0'; printf("\n%s", buffer); break; case ctrl('X'): case ctrl('U'): while (index>0) { index--; erase_a_char(); } index = 0; break; default : if (ch > 0) { /* job control quirk... */ buffer[index++] = ch; output_char(ch); } } } while (index < SLEN); buffer[index] = '\0'; output_char('\n'); return((char *) buffer); } rawmode(state) int state; { /** state is either ON or OFF, as indicated by call **/ if (state == OFF && _in_rawmode) { (void) ioctl(TTYIN, TCSETAW, &_original_tty); _in_rawmode = 0; } else if (state == ON && ! _in_rawmode) { (void) ioctl(TTYIN, TCGETA, &_original_tty); /** current setting **/ (void) ioctl(TTYIN, TCGETA, &_raw_tty); /** again! **/ _raw_tty.c_lflag &= ~(ICANON | ECHO); /* noecho raw mode */ _raw_tty.c_cc[VMIN] = '\01'; /* minimum # of chars to queue */ _raw_tty.c_cc[VTIME] = '\0'; /* minimum time to wait for input */ (void) ioctl(TTYIN, TCSETAW, &_raw_tty); _in_rawmode = 1; } } #endif @EOF set `sum $sumopt <write.c`; if test $1 -ne 50356 then echo ERROR: write.c checksum is $1 should be 50356 fi set `wc -lwc <write.c` if test $1$2$3 != 580205815148 then echo ERROR: wc results of write.c are $* should be 580 2058 15148 fi chmod 644 write.c exit 0