[alt.sources] PTY Session Manager

jfh@rpp386.cactus.org (John F Haugh II) (12/22/90)

this is something i whipped up because i wanted to be able to
run multiple sessions on a single tube and i don't have job
control or any such stuff.

it requires ptys to run.  you may want to hack on it and make
it set-uid root so it can steal inactive pty's.  i didn't
bother to do that because i'm lazy this month, and besides,
i own all the pty's on this system anyhow ;-)  if you need a
pty device driver, and you have sco xenix, let me know as i
have one laying around here somewheres.  it was posted to
comp.sources.misc some time back.

it understands a few basic commands -

	create - start a shell on a pty.
	active - list of active (available) sessions.
	current - number of current session.
	jobs - ps output of active shells.
	connect [ # ] - connect keyboard to currently active
	    shell, or session # if # is given.
	quit, exit - quit (or exit ;-)

to get from "connected" state, you may press ^Z followed by
any character to get a "pty->" prompt back.  to send a ^Z
to your shell, press two.  i picked ^Z because that's what
i use for "suspend" on aix, and i'm too lazy to learn a new
keystroke.  someone should probably add a command to make it
setable.

it doesn't keep your utmp file up to date because it would
definitely have to be setuid then.  i had a version that
executed "login -f <your name>" and ran setuid root, but i
didn't think anyone would trust this program to run suid 0.

as always, 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 Dec 21 13:45:53 1990
# By:	John F Haugh II (River Parishes Programming, Austin TX)
export PATH; PATH=/bin:/usr/bin:$PATH
if test -f 'sm.c'
then
	echo shar: "will not over-write existing file 'sm.c'"
else
cat << \SHAR_EOF > 'sm.c'
/*
 * This code is in the public domain.
 *
 * Written By: John F Haugh II, 12/21/90
 */

#include <sys/types.h>
#include <sys/termio.h>
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>

#define	MAXSESSIONS	16

int	childpids[MAXSESSIONS];
int	writepid;
int	masters[MAXSESSIONS];
int	nsessions;
int	current = -1;
int	caught = 0;

struct	termio	sanetty;
struct	termio	rawtty;

void	exit ();
void	_exit ();
char	*getlogin ();
char	*getenv ();
struct	passwd	*getpwnam ();

void
murder (sig)
int	sig;
{
	int	pid;
	int	i;

	pid = wait ((int *) 0);

	/*
	 * See what children have died recently.
	 */

	for (i = 0;pid != -1 && i < nsessions;i++) {
		if (pid == childpids[i]) {
			childpids[i] = -1;
			close (masters[i]);
			masters[i] = -1;
		}
	}
	signal (sig, murder);
}

void
catch (sig)
int	sig;
{
	caught = 1;
	signal (sig, catch);
}

/*
 * reader - read characters from the pty and write to the screen
 */

int
reader (fd)
int	fd;
{
	char	c;
	int	cnt;

	signal (SIGINT, SIG_IGN);
	signal (SIGQUIT, SIG_IGN);

	while (1) {
		if ((cnt = read (fd, &c, 1)) == -1) {
			if (errno != EINTR)
				return -1;

			if (caught)
				return 0;
			else
				continue;
		}
		if (cnt == 0)
			return -1;

		write (1, &c, 1);
	}
}

/*
 * writer - write characters read from the keyboard down the pty
 */

writer (fd)
int	fd;
{
	char	c;
	int	cnt;
	int	zflg = 0;

	signal (SIGINT, SIG_IGN);
	signal (SIGQUIT, SIG_IGN);
	signal (SIGHUP, _exit);

	while (1) {
		errno = 0;
		if ((cnt = read (0, &c, 1)) == 0)
			continue;

		if (cnt == -1) {
			if (errno == EINTR && caught)
				continue;
			else
				exit (0);
		}
		if (c == ('z' & 037)) {
			if (! zflg++)
				continue;
		} else if (zflg) {
			kill (getppid (), SIGUSR1);
			exit (0);
		}
		zflg = 0;
		if (write (fd, &c, 1) != 1)
			break;
	}
	exit (0);
}

usage ()
{
	fprintf (stderr, "usage: ptymgr\n");
	exit (1);
}

session ()
{
	char	mastername[BUFSIZ];
	char	slavename[BUFSIZ];
	char	*digits = "0123456789abcdef";
	char	*letters = "pqrs";
	char	*shell;
	int	i;
	int	pty;
	int	ptys = 64;

	for (i = 0;i < nsessions && masters[i] != -1;i++)
		;

	if (i == MAXSESSIONS)
		return -1;

	if (i == nsessions)
		nsessions++;

	current = i;

	for (pty = 0;pty < ptys;pty++) {
		sprintf (mastername, "/dev/pty%c%c",
			letters[pty >> 4], digits[pty & 0xf]);
		if ((masters[i] = open (mastername, O_RDWR)) != -1)
			break;
	}
	if (masters[i] == -1) {
		fprintf (stderr, "Can't find a pty\n");
		return -1;
	}

	/*
	 * Let's make a child process ...
	 */

	switch (childpids[i] = fork ()) {
		case -1:
			perror ("fork");
			exit (1);
		case 0:
			close (0);
			close (1);
			for (i = 0;i < nsessions;i++)
				close (masters[i]);

			setpgrp ();

			signal (SIGINT, SIG_DFL);
			signal (SIGQUIT, SIG_DFL);
			signal (SIGCLD, SIG_DFL);
			signal (SIGHUP, SIG_DFL);
			signal (SIGUSR1, SIG_DFL);

			sprintf (slavename, "/dev/tty%c%c",
				letters[pty >> 4], digits[pty & 0xf]);

			if (open (slavename, O_RDWR) == -1) {
				fprintf (stderr, "can't open %s\n", slavename);
				_exit (-1);
			}
			close (2);
			dup (0);
			dup (0);
			ioctl (0, TCSETAF, &sanetty);

			if (! (shell = getenv ("SHELL")))
				shell = "/bin/sh";

			execl (shell, strrchr (shell, '/') + 1, 0);
			_exit (-1);
	}
}

main (argc, argv)
int	argc;
char	**argv;
{
	char	buf[BUFSIZ];
	char	*cp;
	int	i;
	int	pid;

	for (i = 0;i < MAXSESSIONS;i++) {
		childpids[i] = -1;
		masters[i] = -1;
	}
	ioctl (0, TCGETA, &sanetty);
	rawtty = sanetty;

	/*
	 * Let's have our own little process group
	 */

	setpgrp ();

	rawtty.c_oflag &= ~OPOST;
	rawtty.c_lflag = 0;
	rawtty.c_cc[VMIN] = 1;
	rawtty.c_cc[VTIME] = 1;

	signal (SIGCLD, murder);
	signal (SIGUSR1, catch);

	while (1) {
		printf ("pty-> ");
		fflush (stdout);

		while (errno = 0, gets (buf) == 0) {
			if (errno == EINTR)
				continue;
			else
				exit (0);
		}
		if (! buf[0])
			continue;

		/*
		 * Get the command
		 */

		if (strcmp (buf, "quit") == 0 || strcmp (buf, "exit") == 0) {
			exit (0);
		} else if (strcmp (buf, "create") == 0) {
			session ();
			continue;
		} else if (strcmp (buf, "current") == 0) {
			printf ("current session is %d\n", current);
			continue;
		} else if (strncmp (buf, "set", 3) == 0) {
			i = strtol (buf + 3, &cp, 10);
			if (buf[3] != '\0' && *cp == '\0')
				current = i;
			else
				printf ("eh?\n");
			continue;
		} else if (strcmp (buf, "active") == 0) {
			for (i = 0;i < nsessions;i++)
				if (masters[i] != -1)
					printf ("%d ", i);

			putchar ('\n');
			continue;
		} else if (strcmp (buf, "jobs") == 0) {
			int	pids = 0;

			strcpy (buf, "ps -fp ");
			for (i = 0;i < nsessions;i++) {
				if (childpids[i] != -1) {
					if (pids++)
						strcat (buf, ",");

					sprintf (buf + strlen (buf), "%d",
						childpids[i]);
				}
			}
			if (pids)
				system (buf);
			continue;
		} else if (strncmp (buf, "connect", 7) != 0) {
			printf ("eh?\n");
			continue;
		}
		i = strtol (buf + 2, &cp, 10);
		if (*cp == '\0' && buf[2]) {
			if (masters[i] != -1)
				current = i;
			else
				current = -1;
		}
		if (current == -1) {
			printf ("no current session\n");
			continue;
		}

		/*
		 * Let's make a process to read from the child ...
		 */

		switch (writepid = fork ()) {
			case -1:
				kill (childpids[current], SIGKILL);
				perror ("fork");
				break;
			case 0:
				writer (masters[current]);
				exit (1);
		}
		ioctl (0, TCSETAF, &rawtty);

		if (reader (masters[current]) == -1) {
			close (masters[current]);
			masters[current] = -1;
			childpids[current] = -1;
			current = -1;
			if (writepid > 0)
				kill (writepid, SIGTERM);
		}
		ioctl (0, TCSETA, &sanetty);
	}
	exit (0);
}
SHAR_EOF
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."

jfh@rpp386.cactus.org (John F Haugh II) (01/26/91)

This is the final version of the code before I send it off to comp.sources.misc
for posting.  I encourage everyone that is using it to try out this version and
send back any bug reports.  I am including the manpage I wrote.  There are a
few small bug fixes from the last version.  Also, it is possible to get entries
in the utmp file now, so your sessions show up with "who" and "users".

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:
#	README
#	sm.1
#	Makefile
#	sm.c
# This archive created: Sat Jan 26 09:49:39 1991
# By:	John F Haugh II (River Parishes Programming, Austin TX)
export PATH; PATH=/bin:/usr/bin:$PATH
echo shar: "extracting 'README'" '(951 characters)'
if test -f 'README'
then
	echo shar: "will not over-write existing file 'README'"
else
sed 's/^X//' << \SHAR_EOF > 'README'
XThis code is in the public domain.  THIS CODE IS PROVIDED ON AN AS-IS
XBASIS.  THE USER ACCEPTS ALL RISKS ASSOCIATED WITH USING THIS CODE AND
XIS SOLELY RESPONSIBLE FOR CORRECTING ANY SOFTWARE ERRORS OR DAMAGE
XCAUSED BY SOFTWARE ERRORS OR MISUSE.
X
XWritten by John F Haugh II and modified to include suggestions made by
XPat Myrto, Dan Bernstein, and Axel Fischer.
X
XThis is the "session manager" command.  It is a very simple program which
Xmanages multiple PTY connections.  It allows a single connection to have
Xmultiple shells active at a single time.  It should work with any system
Xwhich includes pseudo-TTY devices.
X
XThere are quite a few systems which do not correctly support non-blocking
Xreads from the master side of a PTY.  You must edit the sm.c file to
X#define BLOCKING_PTY if you are using one of these systems.  You must
Xdefine BLOCKING_PTY if your system does not properly support either
Xfcntl() or the O_NDELAY option to that system call.
SHAR_EOF
if test 951 -ne "`wc -c < 'README'`"
then
	echo shar: "error transmitting 'README'" '(should have been 951 characters)'
fi
fi
echo shar: "extracting 'sm.1'" '(2860 characters)'
if test -f 'sm.1'
then
	echo shar: "will not over-write existing file 'sm.1'"
else
sed 's/^X//' << \SHAR_EOF > 'sm.1'
X.TH SM 1
X.SH NAME
Xsm \- pty session manager
X.SH SYNOPSIS
X.B sm
X.SH DESCRIPTION
X.B sm
Xmanages \fIsessions\fR connected to PTY devices.
XA user may create one or more
X.I sessions.
XEach
X.I session
Xis a shell command which reads from and writes to
Xa PTY device.
XThe shell command executes as though it were connected
Xto a regular TTY device.
XEach
X.I session
Xis identified either by number or by a string of alphanumeric
Xcharacters.
X.I Sessions
Xare managed using the commands listed below.
X.SH Commands
X.IP \fBactive\fR 8
XThe names or numbers of all existing \fIsessions\fR are given.
X.IP "\fBconnect\fR [ \fIsession\fR ]" 8
XThe last accessed \fIsession\fR is connected to.
XAn error message is given if there is no current
X\fIsession\fR.
XAn optional name or number may be provided as an argument and
Xthat \fIsession\fR will be connected to instead.
XThe number of the previous \fIsession\fR will be remembered for use
Xby the \fBtoggle\fR command.
X.IP "create [ \fIname\fR ]" 8
XA new \fIsession\fR will be created.
XAn optional name may be provided as an argument.
XThat name will be used to identify the new \fIsession\fR.
XThe number of the previous \fIsession\fR will be remembered for use
Xby the \fBtoggle\fR command.
XIf the \fIname\fR is preceeded by a \- character, the new shell
Xwill be treated as a login shell and an entry for this session
Xwill be added to the \fB/etc/utmp\fR file.
X.IP "delete \fIname\fR" 8
XThe requested \fIsession\fR will be deleted.
XThe \fIsession\fR may be identified by name or by number.
XThe pty will be closed and the associated process group signaled.
X.IP exit 8
XAll currently existing \fIsessions\fR are terminated, as with the
X\fBdelete\fR command, and \fBsm\fR exits.
X.IP help 8
XThe list of accepted commands is displayed.
XThis list is also displayed when an unknown command
Xis entered.
X.IP "jobs [ \fB-l\fR ]" 8
XThe list of existing \fIsessions\fR and the process ID for each
Xshell command is displayed.
XIf the \fB-l\fR flag is given, the output will be produced
Xin a longer format similiar to the output of the \fBps(1)\fR
Xcommand.
X.IP quit 8
XSame as the \fBexit\fR command.
X.IP "set \fIname\fR" 8
XSet the current \fIsession\fR to the given value.
XThe value may be either a numerical value or the alphanumeric
Xvalue of an existing \fIsession\fR.
X.IP toggle 8
XMake the previously active \fIsession\fR the currently active
X\fIsession\fR.
XThe \fIsession\fR which was active prior to the last \fBset\fR,
X\fBconnect\fR, \fBtoggle\fR, or \fBcreate\fR command is made
Xthe currently active \fIsession\fR as with the \fBconnect\fR command.
X.SH Initialization
XThe file \fB$HOME/.smrc\fR may be filled with \fBsm\fR commands
Xwhich will be executed whenever \fBsm\fR is started.
XThese commands will be executed before the first \fBsm->\fR
Xprompt is displayed.
X.SH Files
X/dev/ptyp* \- master PTY devices
X.br
X/dev/ttyp* \- slave PTY devices
X.br
SHAR_EOF
if test 2860 -ne "`wc -c < 'sm.1'`"
then
	echo shar: "error transmitting 'sm.1'" '(should have been 2860 characters)'
fi
fi
echo shar: "extracting 'Makefile'" '(530 characters)'
if test -f 'Makefile'
then
	echo shar: "will not over-write existing file 'Makefile'"
else
sed 's/^X//' << \SHAR_EOF > 'Makefile'
X#
X# Makefile for "session manager"
X#
X# Written by: John F. Haugh II (12/28/90)
X# This file is in the public domain.
X#
XBIN = /usr/local/bin
XMAN = /usr/man/man1
XIOCTLS = SYSV_IOCTL
X
Xall: sm
X
Xinstall: sm sm.1
X	cp sm $(BIN)/sm
X	strip $(BIN)/sm
X	chown root $(BIN)/sm
X	chgrp bin $(BIN)/sm
X	chmod 4111 $(BIN)/sm
X	cp sm.1 $(MAN)/sm.1
X	chown bin $(MAN)/sm.1
X	chgrp bin $(MAN)/sm.1
X	chmod 444 $(MAN/sm.1
X
Xsm: sm.c
X	cc -o sm -O -D$(IOCTLS) sm.c
X
Xshar:	sm.shar
X
Xsm.shar: README sm.1 Makefile sm.c
X	shar -a README sm.1 Makefile sm.c > sm.shar
SHAR_EOF
if test 530 -ne "`wc -c < 'Makefile'`"
then
	echo shar: "error transmitting 'Makefile'" '(should have been 530 characters)'
fi
fi
echo shar: "extracting 'sm.c'" '(21195 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#ifdef	SYSV_IOCTL
X#include <sys/termio.h>
X#endif
X#include <sys/stat.h>
X#include <utmp.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 * BLOCKING_PTY is defined if you PTY device driver is incapable
X * of correctly handling non-blocking read requests.
X */
X
X#undef	BLOCKING_PTY
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                 */
Xchar	*ports[MAXSESSIONS];		/* Names of tty's                    */
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
X#ifdef	SYSV_IOCTL
Xstruct	termio	sanetty;		/* Initial TTY modes on entry        */
Xstruct	termio	rawtty;			/* Modes used when session is active */
X#endif
X
Xvoid	exit ();
Xvoid	_exit ();
Xchar	*getlogin ();
Xchar	*getenv ();
Xchar	*malloc ();
Xchar	*whoami;
Xstruct	passwd	*getpwuid ();
Xextern	char	**environ;
X
X/*
X * fix_utmp - add or remove utmp file entries
X *
X *	fix_utmp creates entries for new sessions and removes entries
X *	for sessions which have died.
X */
X
Xvoid
Xfix_utmp (port, pid)
Xchar	*port;
Xint	pid;
X{
X	char	*cp;
X	int	fd;
X	int	found = 0;
X	struct	utmp	utmp;
X
X	if (strncmp (port, "/dev/", 5) == 0)
X		port += 5;
X
X	if ((fd = open ("/etc/utmp", O_RDWR)) == -1)
X		return;
X
X	while (read (fd, &utmp, sizeof utmp) == sizeof utmp) {
X		if (strncmp (port, utmp.ut_line, sizeof utmp.ut_line) == 0) {
X			found++;
X			break;
X		}
X	}
X	if (found)
X		lseek (fd, (long) - sizeof utmp, 1);
X	else if (pid == 0)
X		return;
X
X	if (pid) {			/* Add new utmp entry */
X		memset (&utmp, 0, sizeof utmp);
X
X		strncpy (utmp.ut_user, whoami, sizeof utmp.ut_user);
X		strncpy (utmp.ut_line, port, sizeof utmp.ut_line);
X
X		if (cp = strrchr (port, '/'))
X			cp++;
X		else
X			cp = port;
X
X		if (strncmp (cp, "tty", 3) == 0)
X			cp += 3;
X
X		strncpy (utmp.ut_id, cp, sizeof utmp.ut_id);
X
X		utmp.ut_pid = pid;
X		utmp.ut_type = USER_PROCESS;
X		time (&utmp.ut_time);
X	} else {			/* Remove utmp entry */
X		utmp.ut_type = DEAD_PROCESS;
X	}
X	write (fd, &utmp, sizeof utmp);
X	close (fd);
X}
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			if (ports[i]) {
X				fix_utmp (ports[i], 0);
X				free (ports[i]);
X				ports[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#ifdef	BLOCKING_PTY
X{
X	char	c;		/* do reads a byte at a time */
X	int	cnt;		/* how many bytes were read */
X
X	/*
X	 * Ignore the SIGINT and SIGQUIT signals.
X	 */
X
X	signal (SIGINT, SIG_IGN);
X	signal (SIGQUIT, SIG_IGN);
X
X	while (1) {
X		if ((cnt = read (fd, &c, 1)) == -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			return -1;
X
X		write (1, &c, 1);
X	}
X}
X#else
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 && wanted != 1) {
X			wanted = 1;
X			fcntl (fd, F_SETFL, flags & ~O_NDELAY);
X			continue;
X		}
X		if (cnt == 0 && wanted == 1)
X			return -1;
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#endif
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	 * Determine what the name of the slave will be.  Save it
X	 * for later use.
X	 */
X
X	sprintf (slavename, "/dev/tty%c%c",
X			letters[pty >> 4], digits[pty & 0xf]);
X
X	if (ports[i])
X		free (ports[i]);
X
X	ports[i] = strdup (slavename);
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			 * Open the slave PTY.  It will be opened as stdin.
X			 */
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#ifdef	SYSV_IOCTL
X			ioctl (0, TCSETAF, &sanetty);
X#endif
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			 * Update the utmp file if this is a "login"
X			 * session.
X			 */
X
X			if (init)
X				fix_utmp (slavename, getpid ());
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	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 (*cp == '\0' || isdigit (*cp)) {
X		labels[sess] = 0;
X		return -1;
X	}
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	struct	passwd	*pwd;
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 my login name
X	 */
X
X	if (! (whoami = getlogin ())) {
X		setpwent ();
X		if (pwd = getpwuid (getuid ())) {
X			whoami = strdup (pwd->pw_name);
X		} else {
X			printf ("who are you?\n");
X			exit (1);
X		}
X		endpwent ();
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#ifdef	SYSV_IOCTL
X	ioctl (0, TCGETA, &sanetty);
X	rawtty = sanetty;
X
X	rawtty.c_oflag &= ~OPOST;
X	rawtty.c_iflag &= ~(ICRNL|INLCR);
X	rawtty.c_lflag = 0;
X	rawtty.c_cc[VMIN] = 1;
X	rawtty.c_cc[VTIME] = 1;
X#endif
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 ("sm-> ");
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			last = current;
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						if (i == last)
X							last == -1;
X
X						if (i == current)
X							current == -1;
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 TTY and write to the
X		 * PTY.
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 from the PTY to
X		 * the TTY.
X		 */
X
X#ifdef	SYSV_IOCTL
X		ioctl (0, TCSETAF, &rawtty);
X#endif
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#ifdef	SYSV_IOCTL
X		ioctl (0, TCSETA, &sanetty);
X#endif
X	}
X	exit (0);
X}
SHAR_EOF
if test 21195 -ne "`wc -c < 'sm.c'`"
then
	echo shar: "error transmitting 'sm.c'" '(should have been 21195 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
"13 of 17 valedictorians in Boston High Schools last spring were immigrants
 or children of immigrants"   -- US News and World Report, May 15, 1990