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."