v13i048: Resource allocation program

Rich Salz rsalz at bbn.com
Thu Feb 18 13:44:04 AEST 1988


Submitted-by: Rob McMahon <daisy.warwick.ac.uk!cudcv>
Posting-number: Volume 13, Issue 48
Archive-name: with

With will reserve exclusive use of a device by creating a lock file,
changing ownership of the device to your current user id, setting the
device to mode 600 (rw by you, nothing to others), and giving you a new
shell.  When the shell terminates, the device is released.  In the event
that a device is in use by another user you will be notified of this, and
with can wait until it is available.

For example:
	with -n tape "Rob's tape, to read, please"
	with tape "..." -c "tar xvp ; mt offl" > tar.out &
----
#!/bin/sh
echo 'Start of pack.out, part 01 of 01:'
echo 'x - Makefile'
sed 's/^X//' > Makefile << '/'
X# makefile for with
X
X# DESTDIR is the destination directory (in which the compiled source lives)
X# CFLAGS contains the arguments passed to the C compiler
X# SEPID whether to be compiled with -i or -n (not used on the VAX)
X# R prefix to destination names (ie. an optional root)
X# HACKS - local flags to the C compiler to define/undefine symbols
X
XDESTDIR=/usr/local
XHACKS=
XSEPID=
XCFLAGS=-O $(SEPID) $(HACKS)
XR=
X
X.SUFFIXES:	.c,v
X
Xall with: with.o
X	$(CC) $(CFLAGS) with.o -o with
X
Xcp install:     with
X	install -s -o root -g general -m 4710 with $R$(DESTDIR)
X
Xcmp:    with
X	-cmp with $R$(DESTDIR)/with
X	rm -f with *.o
X
Xclean:
X	rm -f with *.o
X
X.c,v.o:
X	co -q $*.c
X	$(CC) $(CFLAGS) -c $*.c
X	rm -f $*.c
X
X# entry points are (at least):
X#  all, with - compile with and leave it where it is
X#  cp, install - compile and install with
X#  cmp         - compile and compare with, remove dross
X#  clean       - removes any garbage
X# default action is to compile and leave the binary in situ
X
X
X# there are problems if the command name is the same as an entry point.
X# there are problems moving mv to /usr/ucb/mv (text in use)
X#       or cping cp to /bin/cp
X#
X# clean does not remove .c files when the source is in sccs s. ... .c format
/
echo 'x - README'
sed 's/^X//' > README << '/'
XWith is based on a set of programs, written by Dave Butterfield at the
XUCLA Dept. of Mathamatics, called get & rls.  These programs had a
Xnumber of problems in our environment, the worst being that you couldn't
Xuse them to run a job in the background.  Out of this grew with.
X
XWith uses a lockfile (LOCKSFILE, "/etc/locks/lockfiles" by default),
Xwith lines consisting of: name of file to create and flock, name of
Xresources as known to user, and a list of up to MAXPDEV (16) devices
Xwhich are chown'ed to the user, and given mode OWNERONLY (0600,
X-rw-------), before a subshell is started.  When the shell exits, the
Xdevices are chown'ed back to Rootuid, Rootgid (0,3), and the flock is
Xremoved by with exiting.  (The lockfile is not removed, as someone else
Xcould have an flock pending on it.)
X
XA second argument can be given to with, and if it is not an empty string
Xit is a request which is sent to the operators via syslog, and with then
Xwaits for a signal back from the operators to say whether the request
Xhas been fulfilled or denied (SIGEMT or SIGTERM).  Any extra arguments
Xare passed to the subshell.
X
XA pseudo-resource '-' is available, which allows a request to the
Xoperators without actually getting any resource.  This is useful for
Xe.g. loading the second tape.
X
Xe.g.
X
Xwith -n tape "Rob's tape, to read, please"
X...
Xmt offl
Xwith - "second tape, please"
X...
Xmt offl
X^D
X
XThe -n tells with not to wait if someone else is already using the tape
Xdeck.  We recommend the 'mt offl's so that your tape is unavailable to
Xanyone else using with after you release the tape deck.
X
XIn an 'at' job:
X
Xwith tape "..." << EOF
X...
Xmt offl
XEOF
X
XIn the background:
X
Xwith tape "..." -c "tar xvp ; mt offl" > tar.out &
X
X---
XUUCP:   ...!mcvax!ukc!warwick!cudcv	PHONE:  +44 203 523037
XJANET:  cudcv at uk.ac.warwick.daisy       ARPA:   cudcv at daisy.warwick.ac.uk
XRob McMahon, Computing Services, Warwick University, Coventry CV4 7AL, England
/
echo 'x - with.1'
sed 's/^X//' > with.1 << '/'
X.TH with 1 local
X.SH NAME
Xwith \- a program to reserve exclusive use of a device
X.SH SYNOPSIS
X.I
X.B with
X[
X.B \-sn
X]
X.I dev
X[
X.I request
X[
X.I shell parameters
X]
X]
X.SH DESCRIPTION
X.I With
Xwill reserve exclusive use of a device by creating a lock file, changing
Xownership of the device to your current user id, setting the device to
Xmode 600 (rw by you, nothing to others), and giving you a new shell.  When the
Xshell terminates, the device is released.
XIn the event that a device is in
Xuse by another user you will be notified of this, and
X.I with
Xwill wait until it is available, or if the
X.B \-n
Xflag is given will return immediately with an indication of failure.
XThe
X.B \-s
Xswitch causes
X.I with
Xto perform its work silently.
X.I With
Xreturns a nonzero return code to indicate failure.
X.PP
XIf a request is specified, the operators are asked to fulfil the request once
Xthe device is available.  E.g.
X.PP
Xwith tape "cudcv's tape to read, please"
X.PP
XA request can be made without actually locking a device by specifying the
Xdevice as `-'.  In this case no shell is started, but with waits until the
Xrequest is fulfilled, and then exits.
X.PP
XArguments after the
X.I request
Xare handed to the shell.  A null request can be specified as `-', to grab a
Xdevice and execute a command without making a request.
X.PP
XCurrently known devices are:
X.TP 8
Xtape
XThe magtape; all 16 logical devices 800 & 1600 BPI: ({,n}{,r}mt{0,4,8,12}).
X.SH FILES
X.TP 30
X/etc/locks/lockfiles
Xfor device names, lock file names, and /dev/minordev names.
X.SH ENVIRONMENT
X.TP 20
XSHELL
Xfor user's default shell
X.SH DIAGNOSTICS
XA non-zero return code indicates one of the devices was not available, or with
Xwas interrupted.
X.nf
Xdev is yours.
Xdev is in use ... waiting
Xdev is in use - try again later.
Xdev released.
XI don't know how to get dev.
X.fi
X.SH BUGS
/
echo 'x - with.c'
sed 's/^X//' > with.c << '/'
X/*
X *      with --- a program to do simple device locking.
X */
X/*
X * format of working file (lockfiles)
X * full_name_of_lock_file name_of_device minor0 minor1 ...
X * full name of files must include path (in great detail /.../.../...)
X * there may be as many as MAXPDEV minor devices associated with name_of_device
X * note that name_of_device need not be the same as any minor device
X * example
X * /etc/tape0.lock tape0 /dev/mt0 /dev/mt4 /dev/rmt0 /dev/rmt4
X * /usr/spool/uucp/LCK..cul0 cul0 /dev/cul0
X *
X * $Log:	with.c,v $
X * Revision 2.0  87/10/26  09:32:15  cudcv
X * "Stable"
X * 
X * Revision 1.11  87/10/26  09:19:37  cudcv
X * Cleanup comments, lint
X * 
X * Revision 1.10  86/09/17  09:01:42  cudcv
X * 'waiting for confirmation' message wasn't getting out if stderr buffered.
X * 
X * Revision 1.9  86/08/06  10:34:33  cudcv
X * Make handling of null resource cleaner, allow - as null request.
X * 
X * Revision 1.8  86/06/17  13:14:44  cudcv
X * Allow further arguments to be passed to the shell
X * Return status from shell
X * 
X * Revision 1.7  86/06/17  12:28:45  cudcv
X * Slip of the editor
X * 
X * Revision 1.6  86/01/24  15:01:36  cudcv
X * Restart shell when with is restarted
X * 
X * Revision 1.5  86/01/24  12:09:59  cudcv
X * With would hang if shell was stopped
X * 
X * Revision 1.4  86/01/23  09:38:23  cudcv
X * Was ignoring last device
X * 
X * Revision 1.3  86/01/23  09:02:20  cudcv
X * Make more portable for Gould
X * 
X * Revision 1.2  86/01/21  15:04:35  cudcv
X * Allow second argument with request to operators
X * Record usercode of requester
X * With no arguments lists current locks
X * 
X * Revision 1.1  85/10/07  10:31:25  cudcv
X * Initial revision
X * 
X */
Xstatic char RCSid[] = "$Header: with.c,v 2.0 87/10/26 09:32:15 cudcv Exp $";
X#include <ctype.h>
X#include <pwd.h>
X#include <signal.h>
X#include <stdio.h>
X#include <strings.h>
X#include <syslog.h>
X#include <sys/time.h>
X#include <sys/types.h>
X#include <sys/dir.h>
X#include <sys/file.h>
X#include <sys/resource.h>
X#include <sys/stat.h>
X#include <sys/wait.h>
X
X#ifndef	PW_NAMEL
X#define	PW_NAMEL	8
X#endif
X
X#ifndef LOCKSFILE
X#define	LOCKSDIR	"/etc/locks"
X#define LOCKSFILE	"/etc/locks/lockfiles"
X#endif
X#define MAXSTLEN        40      /*much more than necessary*/
X#define NLOCKS          10      /* "    "     "      "  */
X#define MAXPDEV         16	/* max # phys. devices associate with a name*/
X#define OWNERONLY       0600    /*protection mode for the devices*/
X#define	LOGLEVEL	LOG_CRIT	/* level for syslog's */
X
X#define subdev(LP,n)     (& ( (LP)->physdev[n][0]))         /*all dave's fault*/
X#define	mask(i)	(1 << (i-1))	/* for signals */
X
XFILE *fopen();
X
X#ifdef	RLIMIT_INTACT
Xstruct rlimit intact = {1, 1};		/* make ourselves interactive */
X#endif
X
Xtypedef struct {
X	char	lok[MAXSTLEN],		/* lock (flock) */
X        	dev[MAXSTLEN],		/* what the user calls it (with dev) */
X        	physdev[MAXPDEV][MAXSTLEN];	/* /dev/dev */
X	int	locked;			/*got lock on this*/
X} dev_lk;
X
Xdev_lk	locks[NLOCKS];
Xdev_lk	nolok;
X
Xchar	*progname;
Xextern	int	optind;
Xchar	*malloc(), *sprintf();
Xstruct passwd *getpwuid();
Xextern	char	*sys_siglist[];
Xextern	char	*sys_errlist[];
Xextern	int	errno;
X
Xint 	silent = 0;
Xint 	nowait = 0;		/* don't wait until available */
Xint 	nerrs = 0;
Xint	nlock = 0;
Xint	Rootuid = 0, Rootgid = 3; 	/* magic numbers */
Xint 	uuid, ugid;
Xchar 	*resource;
Xint	needrequest = 0;	/* got resource, need extra request */
Xint	requested = 0;		/* have warned of need, while waiting */
Xint	fulfilled = 0;		/* request fulfilled */
Xstruct passwd *pw;
Xchar	buf[BUFSIZ];
Xchar	**shellargs;
X
Xvoid	print_locks();
X
Xmain(argc,argv)
Xint argc;
Xchar *argv[];
X{
X        register int 	i, pdev;
X        register dev_lk *lkp;
X	char	*request = NULL;
X        int 	c, lockfile, omask;
X	int 	mypid = getpid();
X	int	cleanup(), fulfil();
X
X	uuid = getuid();
X	ugid = getgid();
X	if ((pw = getpwuid(uuid)) == (struct passwd *)NULL) {
X	    Printf("Who are you ?\n");
X	    exit(1);
X	}
X	(void) endpwent();
X	progname = argv[0];
X	while ((c = getopt(argc, argv, "sn")) != EOF)
X	    switch (c) {
X		case 's':	silent++; break;
X		case 'n':	nowait++; break;
X		case '?':	nerrs++;  break;
X	    }
X        if (nerrs){
X	    Printf("usage: %s [-sn] device [request [parameters]]\n",
X		progname);
X	    Printf("eg:    %s tape \"cudcv's tape for read, please\"",
X		progname);
X	    Printf(" -c dd if=/dev/rmt8 ...\n");
X	    exit(1);
X	}
X        if((nlock = read_loks(LOCKSFILE, locks)) < 0){
X	    Printf("%s:%s unopenable or messed up.\n",progname,LOCKSFILE);
X	    exit(1);
X	}
X        /* locks have been read-in */
X
X	if (argc == optind) {
X	    print_locks();
X	    exit(0);
X	}
X
X        /*can't be interrupted once we start linking things so ...*/
X	omask = sigsetmask((int)0x7fffffff);
X        for(i = 1; i < NSIG; i++) {
X		/* allow stopping */
X		if (i == SIGTSTP || i == SIGTTIN || i == SIGTTOU) continue;
X		/* don't catch those normally ignored */
X		if (i == SIGURG  || i == SIGCONT ||
X		    i == SIGCHLD || i == SIGIO
X#ifdef	SIGENQ
X			         || i == SIGENQ
X#endif
X			) continue;
X		/* don't catch those currently ignored */
X		if (signal(i, cleanup) == SIG_IGN)
X			(void) signal(i, SIG_IGN);
X	}
X	(void) sigsetmask(omask);
X	/* don't want to get clobbered by XCPU either */
X#ifdef	RLIMIT_INTACT
X	if (setrlimit(RLIMIT_INTACT, &intact)) {
X		perror("setrlimit");
X		exit(1);
X	}
X#endif
X        /*now get resource*/
X	resource = argv[optind++];
X	if (argc > optind) {
X	    request = argv[optind++];
X	    if (!*request || !strcmp(request, "-"))
X		request = NULL;
X	    (void) sprintf(buf, "(%s) with", pw->pw_name);
X	    openlog(buf, LOG_PID);
X	}
X	shellargs = &argv[optind-1];
X	if (!strcmp(resource, "-")) {
X		resource = NULL;
X		lkp = &nolok;
X		(void) sprintf(lkp->lok, "%s/%d", LOCKSDIR, getpid());
X		(void) strcpy(lkp->dev, "request");
X	} else {
X		for (i = 0;i < nlock; i++)
X	    		if (strcmp(locks[i].dev, resource) == 0) break;
X        	if(i >= nlock){
X			Printf("I don't know how to get %s.\n", resource);
X			nerrs++;
X			cleanup(0);
X		}
X		lkp = &locks[i];
X	}
X
X	if ((lockfile = open(lkp->lok, O_WRONLY|O_CREAT, 0644)) < 0) {
X	    Printf("%s : cannot open %s : %s\n",
X		progname, lkp->lok, sys_errlist[errno]);
X	    nerrs++;
X	    cleanup(0);
X	}
X	if (flock(lockfile, LOCK_EX|LOCK_NB) < 0) {
X	    if (!resource) {
X		Printf("%s: cannot lock %s : %s\n",
X		    progname, lkp->lok, sys_errlist[errno]);
X		nerrs++;
X		cleanup(0);
X	    }
X	    Printf("%s is in use", lkp->dev);
X	    if (nowait) {
X		Printf(" - try again later.\n");
X		nerrs++;
X		cleanup(0);
X	    } else {
X		Printf(" ... waiting");
X		(void) fflush(stderr);
X		if (request)
X		    syslog(LOGLEVEL, "will need %s, when %s is available",
X			request, resource);
X		requested++;
X		if (flock(lockfile, LOCK_EX) < 0) {
X		    Printf("\n%s : cannot lock : %s\n",
X			progname, sys_errlist[errno]);
X		    nerrs++;
X		    cleanup(0);
X		} else
X		    Printf("\n");
X	    }
X	}
X	(void) write(lockfile, (char *)&mypid, sizeof(int));
X	(void) write(lockfile, pw->pw_name, PW_NAMEL);
X	(void) ftruncate(lockfile, sizeof(int) + PW_NAMEL);
X	lkp->locked++;
X	for(pdev = 0;pdev < MAXPDEV && (*subdev(lkp,pdev));pdev++){
X	    (void) chown( subdev(lkp,pdev),uuid,ugid );
X	    (void) chmod( subdev(lkp,pdev),OWNERONLY);
X	}
X	if (resource)
X		Printf("%s is yours", lkp->dev);
X	if (request) {
X	    if (resource)
X		Printf(" - ");
X	    Printf("waiting for confirmation of request");
X	    (void) fflush(stderr);
X	    omask = sigblock(SIGEMT);
X	    (void) signal(SIGEMT, fulfil);
X	    needrequest++; requested = 0;
X	    (void) write(lockfile, request, strlen(request));
X	    if (resource)
X		syslog(LOGLEVEL, "have %s, need %s", resource, request);
X	    else
X		syslog(LOGLEVEL, "need %s", request);
X	    syslog(LOGLEVEL, "`kill -EMT %d' or `kill -TERM %d'",
X		getpid(), getpid());
X	    do {
X	        sigpause(omask & ~mask(SIGEMT));
X	    } while (!fulfilled);
X	    (void) ftruncate(lockfile, sizeof(int) + PW_NAMEL);
X	    Printf(" - going");
X	    syslog(LOGLEVEL, "going");
X	}
X	Printf("\n");
X
X	if (resource) shell();
X
X	cleanup(0);
X}
X
X
Xint
Xread_loks(lf, lks)
Xchar *lf;
Xregister dev_lk lks[];
X{
X        register FILE *lfp;
X        register dev_lk *dp;
X        register int i;
X        int n;
X        char line[BUFSIZ];
X
X        lfp = fopen(lf, "r");
X        if(lfp == NULL)
X                return -1;      /*error*/
X
X        /*now read in the entries*/
X        for(dp = lks,i=0; i < NLOCKS ;dp++, i++){
X                if( fgets(line,sizeof(line),lfp) == NULL)       /* if EOF*/
X                        break;
X                                /*read at most 2 + MAXPDEV entries from line*/
X                n = sscanf(line,"%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
X				dp->lok, dp->dev,
X                                subdev(dp,0),subdev(dp,1),
X                                subdev(dp,2),subdev(dp,3),
X                                subdev(dp,4),subdev(dp,5),
X                                subdev(dp,6),subdev(dp,7),
X                                subdev(dp,8),subdev(dp,9),
X                                subdev(dp,10),subdev(dp,11),
X                                subdev(dp,12),subdev(dp,13),
X                                subdev(dp,14),subdev(dp,15));
X                if(n < 3){      /*if line was bad format or blank ...*/
X                        dp--;i--;                               /*try again*/
X                        continue;}
X                if(n < MAXPDEV + 2)
X                        *subdev(dp, n - 2) = '\0';              /*no string*/
X		dp->locked = 0;
X                }
X	(void) fclose(lfp);
X        return i;       /*'i' is the # of lines read successfully*/
X}
X
Xvoid
Xprint_locks()
X{
X	register dev_lk *lkp;
X	int nent;
X	struct direct **dp, **names;
X	int pids(), numsort();
X
X	for (lkp = &locks[0]; lkp < &locks[nlock]; lkp++) {
X		print_lock(lkp->lok, lkp->dev);
X	}
X	if (chdir(LOCKSDIR)) {
X		perror(LOCKSDIR);
X		return;
X	}
X	if ((nent = scandir(".", &names, pids, numsort)) < 0) {
X		perror("scandir");
X		return;
X	}
X	for (dp = &names[0]; dp < &names[nent]; dp++) {
X		print_lock((*dp)->d_name, (char *)NULL);
X	}
X}
X
Xprint_lock(lok, dev)
X	char *lok, *dev;
X{
X	int i, lockfile, pid;
X
X	if ((lockfile = open(lok, O_RDONLY)) < 0)
X		return;
X	if (flock(lockfile, LOCK_SH|LOCK_NB) < 0) {
X		(void) read(lockfile, (char *)&pid, sizeof(int));
X		(void) read(lockfile, buf, PW_NAMEL);
X		buf[PW_NAMEL] = '\0';
X		if (dev)
X			Printf("%s in use by ", dev);
X		Printf("%u (%s)", pid, buf);
X		if ((i = read(lockfile, buf, BUFSIZ)) > 0) {
X			buf[i] = '\0';
X			Printf(": needs %s", buf);
X		}
X		Printf("\n");
X	}
X	(void) close(lockfile);
X}
X
Xint
Xpids(dp)
X	struct direct *dp;
X{
X	register char *p = dp->d_name;
X	while (*p)
X		if (!isdigit(*p++))
X			return(0);
X	return(1);
X}
X
Xint
Xnumsort(d1, d2)
X	struct direct **d1, **d2;
X{
X	int p1, p2;
X
X	p1 = atoi((*d1)->d_name);
X	p2 = atoi((*d2)->d_name);
X	return p1 - p2;
X}
X
Xstatic int child;
X
Xsigcont()
X{
X	(void) kill(child, SIGCONT);
X}
X
Xshell()
X{
X	register i;
X	char *myshell, *shellt;
X	union wait status;
X	char *getenv(), *rindex();
X	int omask;
X
X	if (!(myshell = getenv("SHELL")))
X		myshell = "/bin/sh";
X	if (shellt = rindex(myshell, '/'))
X		shellt++;
X	else
X		shellt = myshell;
X
X	/* ignore keyboard generated signals now - could manipulate process
X	 * groups I suppose, but I'm lazy
X	 * First block them over the fork
X	 */
X	omask = sigblock(mask(SIGINT)|mask(SIGQUIT));
X	(void) fflush(stderr);
X	if ((child = vfork()) == 0) {
X		/* normal keyboard signals in shell */
X		(void) sigsetmask(omask);
X		child = getpid();
X		(void) setuid(uuid);
X		(void) setgid(ugid);
X		for(i = getdtablesize(); i > 3; )
X			(void) close(--i);
X		shellargs[0] = shellt;
X		execv(myshell, shellargs);
X		Printf("%s : couldn't execute %s : %s\n",
X			progname, myshell, sys_errlist[errno]);
X		_exit(1);
X	} else if (child < 0) {
X		Printf("%s : couldn't fork : %s\n",
X			progname, sys_errlist[errno]);
X	} else {
X		/* ignore keyboard signals in parent */
X		(void) signal(SIGINT, SIG_IGN);
X		(void) signal(SIGQUIT, SIG_IGN);
X		(void) signal(SIGCONT, sigcont);
X		(void) sigsetmask(omask);
X		do {
X			while((i = wait3(&status,
X					WUNTRACED,
X					(struct rusage *)NULL)) > 0
X				&& i != child);
X			if (status.w_stopval == WSTOPPED)
X				(void) kill(0, (int)status.w_stopsig);
X			else
X				nerrs = status.w_retcode;
X		} while (status.w_stopval == WSTOPPED);
X	}
X}
X
Xrestore(lkp)    /*restores ownership and protections for resources unlocked*/
Xregister dev_lk *lkp;
X{
X        register int pdev;
X
X        for(pdev = 0;pdev < MAXPDEV && (*subdev(lkp,pdev));pdev++){
X                (void) chown( subdev(lkp,pdev),Rootuid,Rootgid );
X                (void) chmod( subdev(lkp,pdev),OWNERONLY);
X		}
X}
X
X/* VARARGS1 */
XPrintf(f, a, b, c, d) char *f;
X{
X	if (!silent) (void) fprintf(stderr, f, a, b, c, d);
X}
X
Xfulfil() { fulfilled = 1; }
X
Xcleanup(sig)
X{
X	register dev_lk *lkp;
X
X	/* wait for child to finish before we go ourselves */
X	if (!nerrs || sig) Printf("\n");
X	if (sig) Printf("%s\n", sys_siglist[sig]);
X	if (requested) syslog(LOGLEVEL, "request withdrawn");
X	if (needrequest) {
X		if (sig) {
X			Printf("Request denied\n");
X			syslog(LOGLEVEL, "request denied");
X		} else if (resource) {
X			syslog(LOGLEVEL, "done with %s", resource);
X		}
X	}
X	/* check if there's anything to wait for */
X	if (!wait3((union wait *)NULL, WNOHANG, (struct rusage *)NULL))
X		Printf("waiting for shell to exit\n");
X	(void) fflush(stderr);
X	while (wait((union wait *)NULL) > 0);
X	for (lkp = &locks[0]; lkp < &locks[nlock]; lkp++)
X	    if (lkp->locked) {
X		restore(lkp);
X		Printf("%s released.\n", lkp->dev);
X	    }
X	if (!resource)
X		(void) unlink(nolok.lok);
X        exit(sig ? -1 : nerrs);
X}
/
echo 'Part 01 of pack.out complete.'
exit
----
UUCP:   ...!mcvax!ukc!warwick!cudcv	PHONE:  +44 203 523037
JANET:  cudcv at uk.ac.warwick.daisy       ARPA:   cudcv at daisy.warwick.ac.uk
Rob McMahon, Computing Services, Warwick University, Coventry CV4 7AL, England
-- 
For comp.sources.unix stuff, mail to sources at uunet.uu.net.



More information about the Comp.sources.unix mailing list