|
|
researchv10 Dan Cross
#if TRACING | PSTATISTICS
#include <stdio.h>
#endif
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stream.h>
#include <sys/ttyio.h>
#include <sys/filio.h>
#include <signal.h>
#include <errno.h>
#include <jioctl.h>
#include <tty.h>
#include "msgs.h"
#include "pconfig.h"
#include "proto.h"
#include "packets.h"
#include "pstats.h"
/*
* One layer structure per file-descriptor
*/
struct layer {
char chan; /* jerq protocol channel */
char busy;
char dx, dy; /* Window size in characters */
short bitsx, bitsy; /* Window size in bits */
int more;
struct ttychars ttychars;
char bchan; /* holding area for blocked data */
char bcount;
char bbuf[MAXPKTDSIZE];
};
#define NLAYERS 16 /* Same as in jerq itself */
#define NSELFD 20 /* Maximum file descriptors for 'select' */
#define SELTIMO (1000*3) /* 'select' timeout in millisecs */
#define CDSIZE (sizeof(struct sgttyb)-1)
char *jerqprog;
char *jerqstart;
char *progname;
char *shell;
int quitflag;
fd_set rdfd;
int enabled=1;
int blocked=0;
struct layer layer[NSELFD];
char buf[MAXPKTDSIZE+MSGHLEN];
struct sgttyb sttymodes;
struct sgttyb sttysave;
struct ttydevb devmodes;
struct ttydevb devsave;
struct tchars tcharssave;
struct ttychars ttychars;
struct ttychars zerochars;
short booted;
extern int receive();
extern int creceive();
void dosig();
int twrite();
void wrmesgb();
extern char *sys_errlist[];
extern char *getenv();
extern char *itoa();
extern char *strcpy();
extern char *strcat();
extern int tty_ld;
extern int mesg_ld;
extern int errno;
extern int sys_nerr;
extern int strlen();
extern int write();
struct{
short speed;
short bytes;
} speeds[] = {
EXTA, 1920,
B9600, 960,
B4800, 480,
B1200, 120,
B300, 30,
0, 960, /* default */
};
#define NSPEEDS ((sizeof speeds)/(sizeof speeds[0]))
#define max(A,B) (((A)>(B))?(A):(B))
struct Pchannel pconvs[NLAYERS];
struct Pconfig pconfig = {
# ifndef TRACING
write,
# else
twrite,
# endif
receive,
(void(*)())creceive,
};
#ifdef TRACING
int twrite();
FILE *tracefd;
void trace();
void tread();
#define ifdeftracing(a) a
#else
#define trace(a, b)
#define tread(a, b)
#define ifdeftracing(a)
#endif
#if TRACING==1 || PDEBUG==1
char tracefile[]="traces";
#define _exit exit
#endif
main(argc, argv)
char *argv[];
{
register int n;
char cmdline[64];
progname=argv[0];
for(n=1; n<argc; n++) {
if(strcmp(argv[n], "-L")==0)
jerqstart=argv[++n];
else if(strcmp(argv[n], "-l")==0){
register fd; char jfd[32];
fd = creat(mktemp(strcpy(jfd, "/tmp/.muxXXXXXX")), 0744);
write(fd, "#!/bin/sh\n", 10);
++n, write(fd, argv[n], strlen(argv[n]));
close(fd);
strcpy(cmdline, jfd);
strcat(strcat(strcat(cmdline, "; rm "), jfd), "; exit");
jerqstart = cmdline;
}else{
n=argc+1; break;
}
}
if(n!=argc)
return service(argc-1, argv+1);
if((jerqprog=getenv("MUXTERM")) == 0)
jerqprog="/usr/jerq/lib/muxterm";
if((shell=getenv("SHELL")) == 0)
shell="sh";
ioctl(0, TIOCGETP, &sttymodes);
sttysave=sttymodes;
if(ioctl(0, TIOCGDEV, &devmodes)>=0) /* band-aid for old systems */
devsave=devmodes;
else {
devsave.ispeed=devmodes.ispeed=sttymodes.sg_ispeed;
devsave.ospeed=devmodes.ospeed=sttymodes.sg_ospeed;
}
ioctl(0, TIOCGETC, &tcharssave);
setmodes(&ttychars, &sttymodes);
settchars(&ttychars, &tcharssave);
sttymodes.sg_flags|=RAW;
sttymodes.sg_flags&=~ECHO;
ioctl(0, TIOCSETP, &sttymodes);
devmodes.flags|=F8BIT;
ioctl(0, TIOCSDEV, &devmodes);
signal(SIGPIPE, (int (*)())1);
#ifdef TRACING
tracefd=fopen(tracefile, "w");
#ifdef PDEBUG
ptracefd = tracefd;
#endif
#endif
#if TRACING!=1 && PDEBUG==1
ptracefd=fopen(tracefile, "w");
#endif
if(boot(jerqprog))
quit("can't boot terminal program");
booted++;
ioctl(0, TIOCEXCL, 0);
trace(0, 0);
trace("start\n", 0);
for(n=0; n<NSPEEDS; n++)
if(speeds[n].speed<=devmodes.ospeed)
break;
n=speeds[n].bytes;
Pxtimeout=max((((NLAYERS-2)*sizeof(struct Packet)*NPCBUFS+n-1)/n), 3);
Prtimeout=max(((sizeof(struct Packet)+n-1)/n), 2);
Pscanrate=1;
trace("speed = %d", n);
trace(" xtimo = %d", Pxtimeout);
trace(" rtimo = %d\n", Prtimeout);
Pxfdesc=1;
if(pinit(NLAYERS)==-1)
quit("bad protocol initialization");
buf[0]=JTIMO;
buf[1]=Prtimeout;
buf[2]=Pxtimeout;
if (jerqstart && (n = strlen(jerqstart)) <= MAXPKTDSIZE-3) {
strncpy(&buf[3], jerqstart, n);
n += 3;
} else
n = 3;
(void)psend(0, buf, n);
while(scan()!=-1){
if(quitflag)
quit(NULL);
ifdeftracing(fflush(tracefd));
}
trace("errno = %d\n", errno);
quit("select");
}
scan()
{
register fd, bit, n, ret=0;
trace(0, 0);
trace("enabled %o\n", enabled);
if(blocked){ /* try to clear blocked channels */
for(fd=0, bit=1; fd<NSELFD; fd++, bit<<=1)
if((bit&blocked) &&
psend(layer[fd].bchan, layer[fd].bbuf, layer[fd].bcount)!=-1){
blocked&=~bit;
return 0;
}
}
rdfd.fds_bits[0]=enabled&~blocked;
while(select(NSELFD, &rdfd, (fd_set *)0, SELTIMO)==-1){
if(errno!=EINTR)
return -1;
ret++;
}
trace(0, 0);
trace("selected %o\n", rdfd.fds_bits[0]);
if(rdfd.fds_bits[0]==0) {
if(Ptflag)
ptimeout(SIGALRM);
}else for(fd=0, bit=1; fd<NSELFD; fd++, bit<<=1)
if(bit&rdfd.fds_bits[0]){
while((n=read(fd, buf, sizeof buf))==-1)
if(errno!=EINTR)
return -1;
else{
trace("read error, errno=%d\n", errno);
return 0;
}
ifdeftracing(if(n==0) trace("0 byte read", 0));
if(fd==0){
if(n==0)
quit("EOF on jerq");
tread(buf, n);
precv(buf, n);
} else if(unpack(fd, buf, n))
enabled&=~bit;
}
return ret; /* used in quit */
}
psend_hold(chan, bufp, count, fd)
int chan;
register char *bufp;
int count;
register fd;
{
int ret;
register i;
if((ret=psend(chan, bufp, count))==-1){
trace("psend hold on fd %d\n", fd);
layer[fd].bchan=chan;
layer[fd].bcount=count;
for(i=0; i<count; i++)
layer[fd].bbuf[i]=bufp[i];
blocked|=1<<fd;
}
return ret;
}
quit(s)
register char *s;
{
register l, i;
ifdeftracing(trace("\nmux: %s\n", s); trace(0, 0); fflush(tracefd));
if(booted){
for(i=0; i<NSELFD; i++)
if(layer[i].busy)
(void)close(i);
layer[0].chan=0;
sendioctl(0, JTERM); /* kill demux ==> boot terminal */
for(i=Pxtimeout+1; Ptflag && i>0;){
enabled=1;
if((l=scan())==-1)
break;
i-=l;
}
alarm(0);
}
ioctl(0, TIOCSDEV, &devsave);
ioctl(0, TIOCSETP, &sttysave);
ioctl(0, TIOCNXCL, 0);
sleep(2);
if (s) {
write(2, progname, strlen(progname));
write(2, ": ", 2);
write(2, s, strlen(s));
write(2, "\n", 1);
}
#ifdef PSTATISTICS
for(i=0, l=0; i<PS_NSTATS; i++)
if (pstats[i].count) {
if(l++==0)
fprintf(stderr, "\nPacket protocol statistics:\n");
fprintf(stderr, "%6ld %s\n"
,pstats[i].count
#ifdef PSTATSDESC
,pstats[i].descp
#else
,""
#endif
);
trace("%6ld ", pstats[i].count);
trace("%s\n", pstats[i].descp);
}
fflush(stderr);
#endif
#if TRACING == 1 || PDEBUG == 1
fprintf(stderr, "\nThere are traces in '%s'\n", tracefile);
#endif
ifdeftracing(fflush(tracefd); abort());
#ifdef MONITOR
monitor(0);
#endif
_exit(0);
}
/*
* Unpack a message buffer bp of length n.
*/
unpack(fd, bp, n)
register int fd;
char *bp;
int n;
{
register struct mesg *mp;
struct ttychars tempchars;
static char cdbuf[256];
char *s;
register int size;
mp = (struct mesg *)bp;
size = mp->losize + (mp->hisize<<8);
trace("unpack fd %d", fd);
trace(" size %d", n);
trace(" count %d\n", size);
if(n<=0)
mp->type=M_HANGUP;
else if(layer[fd].more>0){
layer[fd].more-=n;
return sendchars(fd, bp, n);
}
switch (mp->type) {
case M_HANGUP:
trace("shell died\n", 0);
wait((int *)0);
if(layer[fd].busy){
sendioctl(fd, JDELETE);
/*(void)psend(layer[fd].chan, "Shell died.\n", 12);*/
}
layer[fd].busy = 0;
close(fd);
enabled &= ~(1<<fd);
return 1;
case M_DELAY:
default:
trace("ignore type 0%o\n", mp->type);
return 0;
case M_DELIM:
case M_DATA:
if(size==0){
trace("size 0 %s ignored\n", mp->type==M_DELIM? "delim" : "data");
return 0;
}
break;
case M_IOCTL:
mp->type = M_IOCACK;
switch (*(int *)(bp+MSGHLEN)) {
case TIOCSETP:
case TIOCSETN:
tempchars=layer[fd].ttychars;
setmodes(&tempchars, (struct sgttyb *)(bp+MSGHLEN+sizeof(int)));
ttyset(fd, &tempchars);
size = 0;
break;
case TIOCGETP:
tempchars=layer[fd].ttychars;
getmodes(&tempchars, (struct sgttyb *)(bp+MSGHLEN+sizeof(int)));
size=sizeof(struct sgttyb)+sizeof(int);
ttyset(fd, &tempchars);
break;
case TIOCSETC:
tempchars=layer[fd].ttychars;
settchars(&tempchars, (struct tchars *)(bp+MSGHLEN+sizeof(int)));
ttyset(fd, &tempchars);
size=0;
break;
case TIOCGETC:
gettchars(&layer[fd].ttychars, (struct tchars *)(bp+MSGHLEN+sizeof(int)));
size=sizeof (struct tchars) + sizeof (int);
break;
case TIOCSDEV:
size=0;
break;
case TIOCGDEV:
size=sizeof(devsave)+sizeof(int);
*(struct ttydevb *)(bp+MSGHLEN+sizeof(int))=devsave;
break;
case JMUX:
size=0;
break;
case JWINSIZE:
*((int *)(bp+MSGHLEN))=JWINSIZE; /* answering JWINSIZE ioctl */
#define BP ((struct winsize *)(bp+MSGHLEN+sizeof(int)))
BP->bytesx=layer[fd].dx;
BP->bytesy=layer[fd].dy;
BP->bitsx=layer[fd].bitsx;
BP->bitsy=layer[fd].bitsy;
size=sizeof(struct winsize)+sizeof(int);
break;
case JTERM:
case JBOOT:
case JZOMBOOT:
case JEXIT:
sendioctl(fd, *(int *)(bp+MSGHLEN));
size=0;
break;
case JCHDIR:
s=bp+MSGHLEN+sizeof(int);
if(*s==0){
if(chdir(cdbuf)!=0)
mp->type = M_IOCNAK;
cdbuf[0]=0;
}else
strcat(cdbuf, s);
size = 0;
break;
default:
mp->type = M_IOCNAK;
size = 0;
}
mp->magic = MSGMAGIC; /* safety net */
mp->losize = size;
mp->hisize = size>>8;
write(fd, bp, MSGHLEN+size);
trace("unpack ioctl type '%c'", *(int *)(bp+MSGHLEN)>>8);
trace(" %d\n", *(int *)(bp+MSGHLEN)&0xff);
return 0;
}
if(size>MAXPKTDSIZE){
layer[fd].more=size-MAXPKTDSIZE;
size=MAXPKTDSIZE;
}
return sendchars(fd, bp+MSGHLEN, size);
}
getmodes(tp, bp)
register struct ttychars *tp;
register struct sgttyb *bp;
{
bp->sg_ispeed=sttysave.sg_ispeed;
bp->sg_ospeed=sttysave.sg_ospeed;
bp->sg_flags=(tp->flags1<<8)|(tp->flags0&0xFF);
bp->sg_erase=tp->erase;
bp->sg_kill=tp->kill;
}
setmodes(tp, bp)
register struct ttychars *tp;
register struct sgttyb *bp;
{
tp->flags0=bp->sg_flags;
tp->flags1=bp->sg_flags>>8;
tp->erase=bp->sg_erase;
tp->kill=bp->sg_kill;
}
gettchars(tp, bp)
register struct ttychars *tp;
register struct tchars *bp;
{
bp->t_intrc=tp->intrc;
bp->t_quitc=tp->quitc;
bp->t_startc=tp->startc;
bp->t_stopc=tp->stopc;
bp->t_eofc=tp->eofc;
bp->t_brkc=tp->brkc;
}
settchars(tp, bp)
register struct ttychars *tp;
register struct tchars *bp;
{
tp->intrc=bp->t_intrc;
tp->quitc=bp->t_quitc;
tp->startc=bp->t_startc;
tp->stopc=bp->t_stopc;
tp->eofc=bp->t_eofc;
tp->brkc=bp->t_brkc;
}
ttyset(fd, tp)
struct ttychars *tp;
{
register char *p, *q;
register i;
static struct ttycmesg m={JTTYC};
for(i=0, p=(char *)tp, q=(char *)&layer[fd].ttychars; *p++==*q++; i++)
if(i>=sizeof(struct ttychars))
return; /* no need to send; they're identical */
m.chan=layer[fd].chan;
layer[fd].ttychars=*tp;
m.ttychars=*tp;
(void)psend_hold(0, (char *)&m, sizeof m, fd);
}
sendioctl(fd, cmd)
{
char ioctlvec[2];
ioctlvec[0]=cmd;
ioctlvec[1]=layer[fd].chan;
if(psend_hold(0, ioctlvec, sizeof ioctlvec, fd)!=-1)
unblock(fd);
}
int
sendchars(fd, s, cc)
char *s;
int cc;
{
register int l=layer[fd].chan;
register int n;
# ifdef TRACING
char buf[256];
trace("write %d chars ", cc);
trace("to layer %d\n", l);
strncpy(buf, s, cc);
buf[cc]=0;
trace("<%s>\n", buf);
# endif
if(fd!=0 && layer[fd].busy==0)
return 0; /* layer was deleted, but there's still data */
if(cc>0)
do{
if((n=cc)>MAXPKTDSIZE)
n=MAXPKTDSIZE;
if(psend(l, s, n)==-1){
trace("layer %d blocked\n", l);
return fd; /* BUG */
}
}while(s+=n, (cc-=n)>0);
unblock(fd);
return 0;
}
unblock(fd)
int fd;
{
register Pch_p pcp=&pconvs[layer[fd].chan];
trace("unblock for layer %d", layer[fd].chan);
trace(" freepkts=%d\n", pcp->freepkts);
if(fd==0)
return;
if(pcp->freepkts>=1)
enabled|=1<<fd;
else
enabled&=~(1<<fd);
}
void
lerror(l, s, t)
int l;
char *s, t;
{
char ebuf[128];
int busy;
strcpy(ebuf, s);
if(errno){
strcat(ebuf, ": ");
if(errno < sys_nerr)
strcat(ebuf, sys_errlist[errno]);
else{
strcat(ebuf, "error ");
strcat(ebuf, itoa(errno));
}
errno=0;
}
strcat(ebuf, "\n");
trace("lerror type %d", t);
trace(" for layer %d", l);
trace(" %s\n", ebuf);
layer[0].chan=l;
sendchars(0, ebuf, strlen(ebuf));
}
int
creceive(l, s, n)
char *s;
{
if(s[0]!=C_UNBLK || n!=1)
quit("bad control type");
(void)receive(l, s, n);
}
int
receive(l, s, cc)
int l;
register char *s;
register int cc;
{
register int i;
struct mesg hupmsg;
if((i=ltofd(l))==-1)
switch(*s){
case C_NEW:
case C_EXIT:
break;
default:
errno = 0;
lerror(l, "inactive layer", *s);
case C_UNBLK:
return 0;
}
while(cc--){
trace("receive C type %d", *s);
trace(" for layer %d", l);
trace(" fd %d\n", i);
switch(*s++){
case C_SENDCHAR: /* send layer char */
wrmesgb(i, s++, 1);
delim(i);
cc--;
break;
case C_DELIM: /* send delimiter */
delim(i);
break;
case C_NEW: /* make layer */
if((i=doshell())==-1){
lerror(l, "new layer", C_NEW);
trace("can't start\n", i);
cc-=6;
break;
}
layer[i].busy=1;
layer[i].chan=l;
ttyset(i, &ttychars);
trace("new fd %d ", i);
trace("layer %d ", l);
enabled |= (1<<i);
case C_RESHAPE:
layer[i].dx=*s++;
trace("x wid %d ", layer[i].dx);
layer[i].dy=*s++;
trace("y wid %d\n", layer[i].dy);
layer[i].bitsx=(unsigned char)*s++;
layer[i].bitsx|=(*s++)<<8;
layer[i].bitsy=(unsigned char)*s++;
layer[i].bitsy|=(*s++)<<8;
cc-=6;
break;
case C_UNBLK: /* unblock layer */
unblock(i);
break;
case C_PUSHLD: /* obsolescent */
case C_POPLD:
break;
case C_DELETE: /* delete layer */
hupmsg.losize=0;
hupmsg.hisize=0;
hupmsg.magic=MSGMAGIC;
hupmsg.type=M_HANGUP;
write(i, (char *)&hupmsg, MSGHLEN);
layer[i].busy=0;
pconvs[layer[i].chan].freepkts=1; /* hack */
unblock(i);
layer[i].ttychars=zerochars;
break;
case C_EXIT: /* exit */
quitflag++;
return 0;
case C_SENDNCHARS: /* send cc characters */
wrmesgb(i, s, cc);
return 0;
case C_SENDNCHARD: /* send cc characters with delimeter */
wrmesgb(i, s, cc);
delim(i);
return 0;
case C_KILL: /* send layer signal */
dosig(i, *s++);
cc--;
break;
default:
quit("unknown state incase 0");
}
ifdeftracing(if(cc<0) quit("bad count in receive"));
}
return 0;
}
int
ltofd(l)
{
register i;
if(l==0)
return 0;
for(i=1; i<NSELFD; i++)
if(layer[i].busy && layer[i].chan==l)
return i;
trace("unknown layer %d\n", l);
return -1;
}
void
dosig(fd, sig) /* Interrupt shell */
{
char sigbuf[MSGHLEN+1];
register struct mesg *mp;
mp = (struct mesg *)sigbuf;
mp->type=M_SIGNAL;
mp->magic=MSGMAGIC;
mp->losize=sizeof(char);
mp->hisize=0;
sigbuf[MSGHLEN]=sig;
write(fd, sigbuf, MSGHLEN+1);
mp->type=M_FLUSH;
mp->magic=MSGMAGIC;
mp->losize=0;
mp->hisize=0;
write(fd, sigbuf, MSGHLEN);
}
void
wrmesgb(fd, cp, n)
register char *cp;
int n;
{
char wrbuf[MAXPKTDSIZE+MSGHLEN];
register char *bp;
register struct mesg *mp;
register int i;
ifdeftracing(fprintf(tracefd, "mesg to fd %d: <%.*s>\n", fd, n, cp));
mp = (struct mesg *)wrbuf;
mp->type=M_DATA;
mp->magic=MSGMAGIC;
mp->losize=n;
mp->hisize=n>>8;
bp=wrbuf+MSGHLEN;
i=n;
while(i--)
*bp++=*cp++;
write(fd, wrbuf, MSGHLEN+n);
}
delim(fd)
{
struct mesg delbuf;
ifdeftracing(fprintf(tracefd, "delim fd %d\n", fd));
delbuf.type=M_DELIM;
delbuf.magic=MSGMAGIC;
delbuf.losize=0;
delbuf.hisize=0;
write(fd, (char *)&delbuf, MSGHLEN);
}
int
doshell()
{
register fd, slave;
int fp[2];
trace("do shell\n", 0);
if(pipe(fp)<0){
trace("can't pipe, errno=%d\n", errno);
return -1;
}
slave=fp[0];
fd=fp[1];
trace("pipe %d\n", fd);
if(ioctl(fd, FIOPUSHLD, &mesg_ld) == -1){
trace("FIOPUSHLD fails, errno=%d\n", errno);
close(fd);
return -1;
}
setdelim(fd); /* hack */
switch(fork()){
case 0:
/* close every file descriptor in sight, and then some */
for(fd=0; fd<5+NLAYERS; fd++)
if(fd!=slave)
close(fd);
dup(slave); dup(slave); dup(slave); dup(slave);
close(slave);
ioctl(0, TIOCSPGRP, 0);
signal(SIGPIPE, (int (*)())0);
execlp(shell, shell, 0);
perror(shell);
exit(1);
break;
case -1:
close(fd);
close(slave);
return -1;
}
trace("doshell succeeds\n", 0);
close(slave);
return fd;
}
setdelim(fd)
{
struct mesg m;
m.type = M_YDEL;
m.magic = MSGMAGIC;
m.losize = m.hisize = 0;
write(fd, (char *)&m, MSGHLEN);
}
int
boot(s)
char *s;
{
if(system("/usr/jerq/bin/32ld", "32ld", s))
return 1;
sleep(2);
return 0;
}
int
system(s, t, u)
char *s, *t, *u;
{
int status, pid, l;
if ((pid=fork())==0){
execl(s, t, u, 0);
_exit(127);
}
while ((l = wait(&status)) != pid && l != -1)
;
if (l == -1)
status = -1;
return(status);
}
char *
itoa(i)
register int i;
{
static char str[11];
register char * sp = &str[sizeof str];
*--sp = '\0';
if(i>0){
do
*--sp=i%10+'0';
while((i/=10)>0);
}else
*--sp='0';
return sp;
}
service(argc, argv)
char *argv[];
{
if(strcmp(argv[0], "cd")==0){
char *where=argv[1];
char buf[CDSIZE+1];
buf[CDSIZE]=0;
if(where==0 && (where=getenv("HOME"))==0){
write(2, "cd: no HOME set\n", 16);
return 1;
}
while(*where){
strncpy(buf, where, CDSIZE);
ioctl(0, JCHDIR, buf);
where+=strlen(buf);
}
if(ioctl(0, JCHDIR, where)!=0){
write(2, "cd: bad directory\n", 18);
return 1;
}
return 0;
}
if(strcmp(argv[0], "exit")==0)
return ioctl(0, JEXIT, 0);
write(2, "mux: no such command ", 21);
write(2, argv[0], strlen(argv[0]));
write(2, "\n", 1);
return 1;
}
#ifdef TRACING
/*VARARGS1*/
void
trace(s, a)
char *s, *a;
{
long t;
extern long time();
extern char *ctime();
if(s)
fprintf(tracefd, s, a);
else{
(void)time(&t);
fprintf(tracefd, "%.9s", ctime(&t)+11);
}
}
int
twrite(fd, s, n)
unsigned char * s;
{
register i;
fprintf(tracefd, "to jerq: ");
for(i=0; i<n; i++)
fprintf(tracefd, "<%o>", s[i]);
fprintf(tracefd, "\n");
return write(fd, s, n);
}
void
tread(s, n)
unsigned char * s;
{
register i;
fprintf(tracefd, "from jerq: ");
for(i=0; i<n; i++)
fprintf(tracefd, "<%o>", s[i]);
fprintf(tracefd, "\n");
}
#endif
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.