jfh@rpp386.cactus.org (John F Haugh II) (01/04/91)
sm du jour - this version uses non-blocking reads from the master side of the pty to improve performance. also, the labeled session feature of the shell layer manager has been added - you may refer to sessions by name as well as number. credit for putting in non-blocking reads is due to pat myrto who has been very motivational. adding the shell layer features was prompted by axel fischer. unshar and enjoy! -- #! /bin/sh # This is a shell archive, meaning: # 1. Remove everything above the #! /bin/sh line. # 2. Save the resulting text in a file. # 3. Execute the file with /bin/sh (not csh) to create: # sm.c # This archive created: Fri Jan 4 09:49:03 1991 # By: John F Haugh II (River Parishes Programming, Austin TX) export PATH; PATH=/bin:/usr/bin:$PATH echo shar: "extracting 'sm.c'" '(18474 characters)' if test -f 'sm.c' then echo shar: "will not over-write existing file 'sm.c'" else sed 's/^X//' << \SHAR_EOF > 'sm.c' X/* X * This code is in the public domain. THIS CODE IS PROVIDED ON AN AS-IS X * BASIS. THE USER ACCEPTS ALL RISKS ASSOCIATED WITH USING THIS CODE AND X * IS SOLELY RESPONSIBLE FOR CORRECTING ANY SOFTWARE ERRORS OR DAMAGE X * CAUSED BY SOFTWARE ERRORS OR MISUSE. X * X * Written By: John F Haugh II, 12/21/90 X * X * Modified to include suggestions made by Pat Myrto, Dan Bernstein, X * and Axel Fischer. X */ X X#include <sys/types.h> X#include <sys/termio.h> X#include <sys/stat.h> X#include <string.h> X#include <stdio.h> X#include <signal.h> X#include <time.h> X#include <fcntl.h> X#include <errno.h> X#include <ctype.h> X#include <pwd.h> X X/* X * MAXSESSIONS is the number of sessions which a single user can X * manage with this program at a single time. 16 is plenty. 4 or X * 5 might be a better idea if pty's are a scarce resource. 8 X * should be a nice compromise. X */ X X#define MAXSESSIONS 8 X Xint childpids[MAXSESSIONS]; /* Process ID of each session leader */ Xchar *labels[MAXSESSIONS]; /* Names of sessions */ Xint writepid; /* Process ID of PTY writing process */ Xint pspid; /* Obfuscation is my life */ Xint masters[MAXSESSIONS]; /* File descriptor for PTY master */ Xint nsessions; /* High-water mark for session count */ Xint current = -1; /* Currently active session */ Xint last = -1; /* Previously active session */ Xint caught = 0; /* Some signal was caught */ X Xstruct termio sanetty; /* Initial TTY modes on entry */ Xstruct termio rawtty; /* Modes used when session is active */ X Xvoid exit (); Xvoid _exit (); Xchar *getlogin (); Xchar *getenv (); Xchar *malloc (); Xstruct passwd *getpwuid (); Xextern char **environ; X X/* X * parse - see if "s" and "pat" smell alike X */ X Xchar * Xparse (s, pat) Xchar *s; Xchar *pat; X{ X int match = 0; X int star = 0; X X /* X * Match all of the characters which are identical. The '*' X * character is used to denote the end of the unique suffix X * for a pattern. Everything after that is optional, but X * must be matched exactly if given. X */ X X while (*s && *pat) { X if (*s == *pat && *s) { X s++, pat++; X continue; X } X if (*pat == '*') { X star++; X pat++; X continue; X } X if ((*s == ' ' || *s == '\t') && star) X return s; X else X return 0; X } X X /* X * The pattern has been used up - see if whitespace X * follows, or if the input string is also finished. X */ X X if (! *pat && (*s == '\0' || *s == ' ' || *s == '\t')) X return s; X X /* X * The input string has been used up. The unique X * prefix must have been matched. X */ X X if (! *s && (star || *pat == '*')) X return s; X X return 0; X} X X/* X * murder - reap a single child process X */ X Xvoid Xmurder (sig) Xint sig; X{ X int pid; X int i; X X pid = wait ((int *) 0); X X /* X * See what children have died recently. X */ X X for (i = 0;pid != -1 && i < nsessions;i++) { X X /* X * Close their master sides and mark the X * session as "available". X */ X X if (pid == childpids[i]) { X childpids[i] = -1; X if (masters[i] != -1) { X close (masters[i]); X if (writepid != -1) X kill (writepid, SIGTERM); X X masters[i] = -1; X } X if (labels[i]) { X free (labels[i]); X labels[i] = 0; X } X break; X } X } X if (writepid != -1 && pid == writepid) X writepid = -1; X X if (pspid != -1 && pid == pspid) X pspid = -1; X X signal (sig, murder); X} X X/* X * catch - catch a signal and set a flag X */ X Xvoid Xcatch (sig) Xint sig; X{ X caught = 1; X signal (sig, catch); X} X X/* X * reader - read characters from the pty and write to the screen X */ X Xint Xreader (fd) Xint fd; X{ X char buf[16]; /* do reads in 16 byte bursts */ X int cnt; /* how many bytes were read */ X int wanted = sizeof buf; /* how may bytes to try reading */ X int flags; /* fcntl flags */ X X /* X * Ignore the SIGINT and SIGQUIT signals. X */ X X signal (SIGINT, SIG_IGN); X signal (SIGQUIT, SIG_IGN); X X flags = fcntl (fd, F_GETFL, 0); X fcntl (fd, F_SETFL, flags|O_NDELAY); X X while (1) { X if ((cnt = read (fd, buf, wanted)) == -1) { X if (errno != EINTR) X return -1; X X if (caught) X return 0; X else X continue; X } X if (cnt == 0) { X wanted = 1; X fcntl (fd, F_SETFL, flags & ~O_NDELAY); X continue; X } X if (wanted == 1) X flags = fcntl (fd, F_GETFL, flags|O_NDELAY); X X wanted = sizeof buf; X write (1, buf, cnt); X } X} X X/* X * writer - write characters read from the keyboard down the pty X */ X Xwriter (fd) Xint fd; X{ X char c; X int cnt; X int zflg = 0; X X signal (SIGINT, SIG_IGN); X signal (SIGQUIT, SIG_IGN); X signal (SIGHUP, _exit); X X /* X * Read characters until an error is returned or ^Z is seen X * followed by a non-^Z character. X */ X X while (1) { X errno = 0; X if ((cnt = read (0, &c, 1)) == 0) X continue; X X /* X * Some signal may have occured, so retry X * the read. X */ X X if (cnt == -1) { X if (errno == EINTR && caught) X continue; X else X exit (0); X } X X /* X * Process a ^Z. If one was not seen earlier, X * set a flag and go read another character. X */ X X if (c == ('z' & 037)) { X if (! zflg++) X continue; X } X X /* X * See if a ^Z was seen before. If so, signal X * the master and exit. X */ X X else if (zflg) { X kill (getppid (), SIGUSR1); X exit (0); X } X X /* X * Just output the character as is. X */ X X zflg = 0; X if (write (fd, &c, 1) != 1) X break; X } X exit (0); X} X X/* X * usage - command line syntax X */ X Xusage () X{ X fprintf (stderr, "usage: sm\n"); X exit (1); X} X X/* X * help - built-in command syntax X */ X Xhelp () X{ X fprintf (stderr, "Valid commands are:\n"); X fprintf (stderr, "\tconnect [ # ]\t(connects to session)\n"); X fprintf (stderr, "\tcreate\t\t(sets up a new pty session)\n"); X fprintf (stderr, "\tcurrent\t\t(shows current session number)\n"); X fprintf (stderr, "\tdelete #\t(deletes session)\n"); X fprintf (stderr, "\thelp\t\tdisplay this message\n"); X fprintf (stderr, "\tjobs\t\t(shows a ps listing of current session)\n"); X fprintf (stderr, "\tquit\tor exit (terminate session manager)\n"); X fprintf (stderr, "\tset #\t\t(# is 0-15 - selets current session)\n"); X fprintf (stderr, "\ttoggle\t\t(switch to previous session)\n\n"); X fprintf (stderr, "Commands may be abbreviated to a unique prefix\n\n"); X fprintf (stderr, "Note - to exit a session back into sm so one can\n"); X fprintf (stderr, " select another session, type a ^Z and a return\n"); X fprintf (stderr, " To send ^Z to the shell, type two ^Z chars\n\n"); X} X X/* X * session - create a new session on a pty X */ X Xint Xsession (prompt, init) Xchar *prompt; Xint init; X{ X char mastername[BUFSIZ]; X char slavename[BUFSIZ]; X char newprompt[32]; X char newshell[32]; X char *digits = "0123456789abcdef"; X char *letters = "pqrs"; X char *shell; X char *arg; X int oumask; X int i; X int pty; X int ptys = 64; X struct stat sb; X struct passwd *pwd; X X /* X * Find the number of the new session. An error will be X * given if no sessions are available. X */ X X for (i = 0;i < nsessions && masters[i] != -1;i++) X ; X X if (i == MAXSESSIONS) { X printf ("out of sessions\n"); X return 0; X } X if (i == nsessions) X nsessions++; X X /* X * Save the previous sesssion number. This is so the X * "toggle" command will work after a "create". X */ X X if (current != -1) X last = current; X X current = i; X X /* X * Go find the master side of a PTY to use. Masters are X * found by trying to open them. Each PTY master is an X * exclusive access device. If every pty is tried but no X * available ones are found, scream. X */ X X for (pty = 0;pty < ptys;pty++) { X sprintf (mastername, "/dev/pty%c%c", X letters[pty >> 4], digits[pty & 0xf]); X if ((masters[i] = open (mastername, O_RDWR)) != -1) X break; X } X if (masters[i] == -1) { X printf ("out of ptys\n"); X return 0; X } X X /* X * Let's make a child process. X */ X X switch (childpids[i] = fork ()) { X case -1: X printf ("out of processes\n"); X return 0; X case 0: X X /* X * Disassociate from the parent process group X * and tty's. X */ X X close (0); X close (1); X for (i = 0;i < nsessions;i++) X close (masters[i]); X X setpgrp (); X X /* X * Reset any signals that have been upset. X */ X X signal (SIGINT, SIG_DFL); X signal (SIGQUIT, SIG_DFL); X signal (SIGCLD, SIG_DFL); X signal (SIGHUP, SIG_DFL); X signal (SIGUSR1, SIG_DFL); X X /* X * Make up the name of the slave side of the X * PTY and open it. It will be opened as stdin. X */ X X sprintf (slavename, "/dev/tty%c%c", X letters[pty >> 4], digits[pty & 0xf]); X X if (open (slavename, O_RDWR) == -1) { X fprintf (stderr, "can't open %s\n", slavename); X _exit (-1); X } X X /* X * Try to change the owner of the master and slave X * side of the PTY. This will only work if the X * invoker has an effective UID of 0. Change the X * mode of the slave to be the same as the parent X * tty. X */ X X (void) chown (mastername, getuid (), getgid ()); X (void) chown (slavename, getuid (), getgid ()); X if (fstat (2, &sb) == 0) X (void) chmod (slavename, sb.st_mode & 0777); X X /* X * Close the last open file descriptor and make X * the new stdout and stderr descriptors. Copy X * the tty modes from the parent tty to the slave X * pty. X */ X X close (2); X dup (0); X dup (0); X ioctl (0, TCSETAF, &sanetty); X X /* X * See if the invoker has a shell in their X * environment and use the default value if X * not. X */ X X if (! (shell = getenv ("SHELL"))) X shell = "/bin/sh"; X X /* X * Set the PS1 variable to "prompt " if X * "prompt" looks reasonable. X */ X X if (! isalpha (*prompt)) X prompt = strrchr (slavename, '/') + 1; X X sprintf (newprompt, "PS1=%.16s%c ", X prompt, getuid () ? '$':'#'); X X for (i = 0;environ[i];i++) X if (! strncmp (environ[i], "PS1=", 4)) X break; X X if (environ[i]) X environ[i] = newprompt; X X /* X * See about changing current directory X */ X X if (init && (pwd = getpwuid (getuid ()))) X chdir (pwd->pw_dir); X X endpwent (); X X /* X * Undo any set-UID or set-GID bits on the X * executable. X */ X X setgid (getgid ()); X setuid (getuid ()); X X /* X * Start off the new session. X */ X X if (arg = strrchr (shell, '/')) X arg++; X else X arg = shell; X X if (init) X sprintf (newshell, "-%s", arg), arg = newshell; X X execl (shell, arg, 0); X _exit (-1); X } X i = fcntl (masters[current], F_GETFL, 0); X fcntl (masters[current], F_SETFL, i|O_NDELAY); X return 1; X} X X/* X * quit - kill all active sessions X */ X Xquit (sig) Xint sig; X{ X int i; X X for (i = 0;i < nsessions;i++) { X if (masters[i] != -1) { X close (masters[i]); X masters[i] = -1; X X if (childpids[i] != -1) X kill (- childpids[i], SIGHUP); X } X } X exit (sig); X} X X/* X * label - manipulate session labels X * X * label() will either create a new label (if 'sess' != -1) or X * return the session number which matches 'name'. X */ X Xint Xlabel (sess, name) Xint sess; Xchar *name; X{ X char buf[16]; X int i; X char *cp; X X /* X * Make "name" into something with no leading X * whitespace that consists of characters in X * [a-zA-Z0-9]. X */ X X for (cp = name;isspace (*cp);cp++) X ; X X for (i = 0;cp[i] && i < (sizeof buf - 1);i++) { X if (isalnum (cp[i])) X buf[i] = cp[i]; X else X return -1; X } X buf[i] = '\0'; X cp = buf; X X /* X * Look-up the session named "name" X */ X X if (sess == -1) { X X /* X * See if 'name' is a label X */ X X for (i = 0;i < nsessions;i++) X if (strcmp (labels[i], cp) == 0) X return i; X X /* X * Perhaps 'name' is a number?!? X */ X X i = strtol (name, &cp, 10); X if (*cp) X return -1; X else X return i; X } X X /* X * Add a new name, can't be a number. X */ X X if (isdigit (*cp)) X return -1; X X /* X * Add a new session named "name" X */ X X if (labels[sess]) X free (labels[sess]); X X if (! (labels[sess] = malloc (strlen (cp) + 1))) X return -1; X X strcpy (labels[sess], cp); X return sess; X} X X/* X * sm - manage pty sessions X */ X Xmain (argc, argv) Xint argc; Xchar **argv; X{ X char buf[BUFSIZ]; X char *cp; X int i; X int pid; X FILE *fp; X X /* X * No arguments are allowed on the command line X */ X X if (argc > 1) X usage (); X X /* X * Set up all the file descriptors and process IDs X */ X X for (i = 0;i < MAXSESSIONS;i++) { X childpids[i] = -1; X masters[i] = -1; X } X X /* X * Get the current tty settings, and make a copy that can X * be tinkered with. The sane values are used while getting X * commands. The raw values are used while sessions are X * active. New sessions are set to have the same tty values X * as the sane values. X */ X X ioctl (0, TCGETA, &sanetty); X rawtty = sanetty; X X rawtty.c_oflag &= ~OPOST; X rawtty.c_lflag = 0; X rawtty.c_cc[VMIN] = 1; X rawtty.c_cc[VTIME] = 1; X X /* X * SIGCLG is caught to detect when a session has died or when X * the writer for the session has exited. SIGUSR1 is used to X * signal that a ^Z has been seen. X */ X X signal (SIGCLD, murder); X signal (SIGUSR1, catch); X X /* X * The file $HOME/.smrc is read for initializing commands. X */ X X if (cp = getenv ("HOME")) { X sprintf (buf, "%s/.smrc", cp); X if (access (buf, 04) != 0 || ! (fp = fopen (buf, "r"))) X fp = stdin; X } X X /* X * This is the main loop. A line is read and executed. If X * EOF is read, the loop is exited (except if input is the X * .smrc file). X */ X X while (1) { X X /* X * Keyboard signals cause an exit. X */ X X signal (SIGINT, quit); X signal (SIGQUIT, quit); X X /* X * Prompt for input only when it is not coming from X * the .smrc file. A single line will be read and X * executed. The read is retried if an interrupt X * has been seen. X */ X X if (fp == stdin) { X printf ("pty-> "); X fflush (stdout); X } X while (errno = 0, fgets (buf, sizeof buf, fp) == 0) { X if (errno == EINTR) { X continue; X } else if (fp != stdin) { X fclose (fp); X fp = stdin; X buf[0] = '\0'; X break; X } else { X strcpy (buf, "quit"); X break; X } X } X if (cp = strchr (buf, '\n')) X *cp = '\0'; X X if (! buf[0]) X continue; X X /* X * Parse the command. Each command consists of a X * verb, with some commands accepting a session ID X * to act on. The command will be accepted if a X * unique prefix of the command is entered. X */ X X if (parse (buf, "q*uit") || parse (buf, "e*xit")) { X X /* X * Just give up. X */ X X quit (0); X } else if (cp = parse (buf, "cr*eate")) { X int init = 0; X X /* X * Create a new session and make it current X */ X X while (*cp == ' ' || *cp == '\t') X cp++; X X if (*cp == '-') X init++, cp++; X X if (session (cp, init)) X label (current, cp); X X continue; X } else if (parse (buf, "cu*rrent")) { X X /* X * Give the session ID of the current session. X */ X X if (current != -1) { X if (labels[current]) X printf ("current session is \"%s\"\n", X labels[current]); X else X printf ("current session is %d\n", X current); X } else X printf ("no current session\n"); X X continue; X } else if (cp = parse (buf, "s*et")) { X X /* X * Set the current session ID to # X */ X X if (*cp) { X if ((i = label (-1, cp)) != -1) { X last = current; X current = i; X continue; X } X /* FALLTHROUGH */ X } X printf ("eh?\n"); X continue; X } else if (parse (buf, "a*ctive")) { X X /* X * List the session IDs of all active sessions X */ X X for (i = 0;i < nsessions;i++) { X if (masters[i] != -1) { X if (labels[i]) X printf ("%s ", labels[i]); X else X printf ("%d ", i); X X } X } X putchar ('\n'); X continue; X } else if (cp = parse (buf, "j*obs")) { X int pids = 0; X X /* X * Look for a "-" flag X */ X X while (isspace (*cp)) X cp++; X X if (*cp != '-') { X for (pids = i = 0;i < nsessions;i++) { X if (childpids[i] == -1) X continue; X else X pids++; X X if (labels[i]) X printf ("%s (%d)\n", labels[i], X childpids[i]); X else X printf ("#%d (%d)\n", i, X childpids[i]); X } X if (! pids) X printf ("no jobs\n"); X X continue; X } X X /* X * Give a "ps" listing of all active sessions X */ X X buf[0] = '\0'; X for (i = 0;i < nsessions;i++) { X if (childpids[i] != -1) { X if (pids++) X strcat (buf, ","); X X sprintf (buf + strlen (buf), "%d", X childpids[i]); X } X } X if (pids) { X if (! (pspid = fork ())) { X setgid (getgid ()); X setuid (getuid ()); X execl ("/bin/ps", "ps", "-fp", buf, 0); X _exit (1); X } X while (pspid != -1) X pause (); X } else { X printf ("no jobs\n"); X } X continue; X } else if (cp = parse (buf, "co*nnect")) { X X /* X * Connect to the current or named session. X */ X X if (*cp) { X if ((i = label (-1, cp)) != -1) { X last = current; X current = i; X /* FALLTHROUGH */ X } else { X printf ("eh?\n"); X continue; X } X } X } else if (parse (buf, "t*oggle")) { X X /* X * Toggle between the previous and current session X */ X X i = current; X current = last; X last = i; X /* FALLTHROUGH */ X } else if (cp = parse (buf, "d*elete")) { X X /* X * Delete the named session X */ X X if (*cp) { X i = label (-1, cp); X if (i >= 0 && i < MAXSESSIONS) { X if (masters[i] != -1) { X close (masters[i]); X masters[i] = -1; X if (childpids[i] != -1) X kill (- childpids[i], SIGHUP); X X continue; X } X } X /* FALLTHROUGH */ X } X printf ("eh?\n"); X continue; X } else if ((i = label (-1, buf)) != -1) { X if (i >= 0 && i < MAXSESSIONS && masters[i]) { X last = current; X current = i; X } else { X printf ("eh?\n"); X continue; X } X } else { X X /* X * The command was not recognized X */ X X help (); X continue; X } X X /* X * Validate the session number. It must be in the X * range 0 .. (MAXSESSIONS-1) to be valid. The current X * session must also be associated with an open PTY. X */ X X if (current < 0 || current >= MAXSESSIONS) X current = -1; X X if (current == -1 || masters[current] == -1) { X printf ("no current session\n"); X current = -1; X continue; X } X X /* X * Let's make a process to read from the child ... X */ X X switch (writepid = fork ()) { X case -1: X if (childpids[current] != -1) X kill (childpids[current], SIGKILL); X X perror ("fork"); X break; X case 0: X writer (masters[current]); X exit (1); X } X X /* X * Set up the raw TTY modes and start writing to X * the child. X */ X X ioctl (0, TCSETAF, &rawtty); X X if (reader (masters[current]) == -1) { X close (masters[current]); X masters[current] = -1; X childpids[current] = -1; X current = -1; X if (writepid > 0) X kill (writepid, SIGTERM); X } X X /* X * Reset the tty modes to resume the command loop. X */ X X ioctl (0, TCSETA, &sanetty); X } X exit (0); X} SHAR_EOF if test 18474 -ne "`wc -c < 'sm.c'`" then echo shar: "error transmitting 'sm.c'" '(should have been 18474 characters)' fi fi exit 0 # End of shell archive -- John F. Haugh II UUCP: ...!cs.utexas.edu!rpp386!jfh Ma Bell: (512) 832-8832 Domain: jfh@rpp386.cactus.org "While you are here, your wives and girlfriends are dating handsome American movie and TV stars. Stars like Tom Selleck, Bruce Willis, and Bart Simpson."