/* Copyright 1990, Daniel J. Bernstein. All rights reserved. */

#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <stdio.h>
#include "err.h"
#include "config.h"
#include "pty.h"
#include "master.h"
#include "sig.h"
#include "tty.h"
#include "file.h"
#include "sock.h"
#include "logs.h"
#include "misc.h"

static char fnre[20];

static char fnsess[20];
static int fdsess;

static char *glfnsty;

static char soutbuf[OUTBUFSIZE];
static char sptybuf[OUTBUFSIZE];

static struct ttymodes tmowinpty;
static struct ttymodes tmowintty;

static char *outbuf = soutbuf;
static int outbufsiz = OUTBUFSIZE;
static int outbuflen = 0;
static char *ptybuf = sptybuf;
static int ptybufsiz = OUTBUFSIZE;
static int ptybuflen = 0;

static int flagconnected = 1; /* 0: disconnected. 2: idling for stop. */
                              /* 3: idling for stop but child is dead. */
static int flagchild = 1; /* 0: dead. 2: stopped. */
static int childsig; /* signal that stopped/killed child */
static int flagsigler = 1; /* 0: dead. */
static int siglerpid; /* only defined if flagconnected */
static int slavepid;

static int flagqwinch = 0;

static void quickdeath(i)
int i;
{
 /* All exits from master() go through here. */
 if (flagsession) (void) unlink(fnsess);
 if (flagxchown)
   (void) fchown(fdsty,PTYOWNER,PTYGROUP);
 (void) fchmod(fdsty,UNUSEDPTYMODE);
 date = now();
 if (flagxutmp)
   if (utmp(glfnsty + PTYUTMP_OFFSET,"","",date) == -1)
     ; /* too bad. */
 if (flagxwtmp)
   if (wtmp(glfnsty + PTYWTMP_OFFSET,"","",date) == -1)
     ; /* too bad. */
 fatal(i);
}

static void death(i)
int i;
{
 (void) kill(siglerpid,SIGTERM);
 /* XXX: should wait while flagsigler */
 quickdeath(i);
}

/*ARGSUSED*/
static void sig_force(i)
sig_num i;
{
 /* Forced death, presumably from the sesskill program. */
 sig_ignore(SIGCHLD);
 /* XXX: Should we test for !flagchild here? sesskill does. */
 flagchild = 0;
 quickdeath(SIGCHLD);
}

/*ARGSUSED*/
static void sig_usr2(i)
sig_num i;
{
 if (flagsession)
  {
   int newuid = uid;
   char newsuid[10];
   char foo[100];

   /* XXX: We should have some error recovery here! */

   (void) lseek(fdsess,(long) 0,0);
   (void) read(fdsess,(char *) &newuid,sizeof(int));
   (void) sprintf(newsuid,"%d",newuid);

   (void) chdir("..");
   if (chdir(newsuid) == -1)
    {
     (void) mkdir(newsuid,0700);
     (void) chdir(newsuid);
    }

   (void) sprintf(foo,"../%d/%s",uid,fnsess);
   (void) rename(foo,fnsess);

   (void) sprintf(foo,"../%d/%s",uid,fnre);
   (void) rename(foo,fnre); /* in case we're already disconnected */

   uid = newuid;
   (void) setreuid(uid,euid);
   setusername();

   if (flagxutmp)
     if (utmp(glfnsty + PTYUTMP_OFFSET,username,PTYUTMP_SWHOST,date) == -1)
       ; /* too bad. */
   if (flagxwtmp)
     if (wtmp(glfnsty + PTYWTMP_OFFSET,username,PTYWTMP_SWHOST,date) == -1)
       ; /* too bad. */
   if (flagsigler)
     (void) kill(siglerpid,SIGUSR2);
  }
}

/*ARGSUSED*/
static void sig_pipe(i)
sig_num i;
{
 flagsigler = 0; /* XXX: is this appropriate? race? */
 /* Will end up giving child HUP. */
}

/*ARGSUSED*/
static void sig_chld(i)
sig_num i;
{
 union wait w;

 if (wait3(&w,WNOHANG | WUNTRACED,(struct rusage *) 0) <= 0)
   return; /* why'd we get the CHLD? it must have stopped & restarted? */

 if (w.w_stopval == WSTOPPED)
  {
   childsig = w.w_stopsig;
   flagchild = 2;
  }
 else
  {
   childsig = w.w_termsig; /* can't do much with this */
   flagchild = 0;
  }
}

/*ARGSUSED*/
static void sig_term(i)
sig_num i;
{
 flagsigler = 0;
}

/* If we have made it to being master, we should never get TTIN or TTOU, */
/* except possibly while restarting after a stop (e.g., if the user puts */
/* us back into the background). But we let the signaller handle putting */
/* the tty modes back before restarting us, so we should never, ever, */
/* ever get a TTIN or TTOU. If the user is messing around and we do get */
/* a TTIN or TTOU, we'll just pretend the child died and hope we get */
/* around to telling the signaller about it. */

/*ARGSUSED*/
static void sig_ttin(i)
sig_num i;
{
 if (flagchild)
  {
   childsig = SIGTTIN;
   flagchild = 2;
  }
}

/*ARGSUSED*/
static void sig_ttou(i)
sig_num i;
{
 if (flagchild)
  {
   childsig = SIGTTOU;
   flagchild = 2;
  }
}

/*ARGSUSED*/
static void sig_tstp(i)
sig_num i;
{
 if (flagchild)
  {
   childsig = SIGCONT;
   flagchild = 2;
  }
}

/* Most job-control shells (including csh) behave absolutely miserably. */
/* (Well, that goes without saying.) In particular, rather than sending */
/* a CONT to every one of their children in the process group, they feel */
/* a need to kill the entire process group. Grrrr. Because of this, we */
/* are forced to use the nonintuitive USR1 to communicate CONT, and ignore */
/* CONT entirely. Anyway, read cont as usr1 where necessary. */

/* We can only get USR1 from the signaller (or from us after reconnect). */
/* By convention, the signaller handles setting the tty modes back to */
/* chartty, even though we handled restoring the modes before stop. */

/*ARGSUSED*/
static void sig_cont(i)
sig_num i;
{
 if (flagchild)
  {
   flagchild = 1;
   (void) kill(slavepid,SIGCONT);
   (void) kill(pid,SIGWINCH);
  }
 if (flagconnected == 3)
   flagconnected = 1; /* XXX: should be internal to master() */
 (void) setpgrp(0,pgrp);
}

/* If it weren't for WINCH, which must be in the master if NO_FDPASSING, */
/* and for the stupid conventions surrounding a process's control tty, */
/* then all mention of fdtty could disappear from master. This would */
/* slightly complicate the signaller's T{STP,TIN,TOU} handling but make */
/* reconnect a lot simpler. Sigh. */

/*ARGSUSED*/
static void sig_winch(i)
sig_num i;
{
 int pg;

 flagqwinch = 0;
#ifdef TTY_WINDOWS
/* An unfortunate but slight race: Another handler could change the pgrp */
/* if the child suddenly stops and we're queued for delivery. So we have */
/* to change it back. */
 pg = getpgrp(0);
 (void) setpgrp(0,pgrp);
 if (!flagsigler)
   flagqwinch = 1;
 else
   if (tty_getmodes(fdsty,&tmopty) == 0)
     if (tty_getmodes(fdtty,&tmowintty) == 0)
      {
       tty_copymodes(&tmowinpty,&tmopty);
       tty_copywin(&tmowinpty,&tmowintty);
       (void) tty_modifymodes(fdsty,&tmowinpty,&tmopty);
      }
 (void) setpgrp(0,pg);
#endif
}

static int disconnect(fnsty)
char *fnsty;
{
 if (fdtty != -1)
  {
   (void) tty_dissoc(fdtty); /* must succeed */
   (void) close(fdtty);
   fdtty = -1;
  }
 if (fdpass != -1)
  {
   /* We used to write the dot to fdpass here. It's in sigler now, to */
   /* prevent a race condition. */
   (void) close(fdpass);
   fdpass = -1;
  }
 if (fdin != -1)
  {
   (void) close(fdin);
   fdin = -1;
  }
 if (fdout != -1)
  {
   (void) close(fdout);
   fdout = -1;
  }
 if (fdre != -1)
  {
   (void) close(fdre);
   fdre = -1;
  }

 fdre = pty_readsock(fnsty,fnre);
 if (fdre == -1)
   return -1; /* damn. */
 return 0;
}

static int reconnect()
{
 int t;
 char buf[1];
 char fntty[TTYNAMELEN]; /* sigh */
 int flags = 0;

 t = pty_acceptsock(fdre);
 (void) close(fdre);
 fdre = t;
 if (fdre == -1)
   return -1;

#define VCF (void) close(fdre)
#define BONK(xxx,yyy) if ((xxx) == -1) { VCF; return -1; } else (yyy);

/* What about fd 2 for warnings & errors? No, master doesn't use them. */

/* Must have: in, out, siglerpid, pgrp, flagjobctrl. 1, 2, 16, 32, 256. */
/* Except if NO_FDPASSING: just flagjobctrl in that case. */
/* If fdtty, must have also tmochartty, tmotty, fntty. 8: 64, 128, 1024. */
/* Finally, fdpass is independent of all the rest. */

/* CHANGE: With fdpass, fdin and fdout are irrelevant. */

 if (pty_sendint(fdre,'p',&pid) == -1)
  {
   VCF;
   return -1;
  }

 while (pty_getch(fdre,buf) == 0)
   switch(buf[0])
    {
#ifdef NO_FDPASSING
     case 's': BONK(pty_putgetstr(fdre,'s',fntty),flags |= 8) break;
#else
     case '0': BONK(pty_putgetfd(fdre,'0',&fdin),flags |= 1) break;
     case '1': BONK(pty_putgetfd(fdre,'1',&fdout),flags |= 2) break;
     case 'f': BONK(pty_putgetfd(fdre,'f',&fdpass),flags |= 4) break;
     case 't': BONK(pty_putgetfd(fdre,'t',&fdtty),flags |= 8) break;
     case 's': BONK(pty_putgetstr(fdre,'s',fntty),flags |= 1024) break;
#endif
     case 'p': BONK(pty_putgetint(fdre,'p',&siglerpid),flags |= 16) break;
     case 'g': BONK(pty_putgetint(fdre,'g',&pgrp),flags |= 32) break;
     case 'c': BONK(pty_putgettty(fdre,'c',&tmochartty),flags |= 64) break;
     case 'n': BONK(pty_putgettty(fdre,'n',&tmotty),flags |= 128) break;
     case 'j': BONK(pty_putgetint(fdre,'j',&flagjobctrl),flags |= 256) break;
#ifdef NO_FDPASSING
     case ' ': if ((flags & 256) != 256) { VCF; return -1; }
#else
     case ' ': if (flags & 4) flags |= 3;
	       if ((flags & 307) != 307) { VCF; return -1; }
	       if (flags & 8) if ((flags & 1024) != 1024) { VCF; return -1; }
#endif
	       if (flags & 8) if ((flags & 192) != 192) { VCF; return -1; }

#ifdef NO_FDPASSING
               if ((fdtty = open(fntty,O_RDWR)) == -1)
		 return -1;
	       if ((fdin = dup(fdre)) == -1)
		{
		 (void) close(fdtty);
		 fdtty = -1;
		 return -1;
		}
	       if ((fdout = dup(fdre)) == -1)
		{
		 (void) close(fdtty);
		 fdtty = -1;
		 (void) close(fdout);
		 fdout = -1;
		 return -1;
		}
#endif
	       VCF; /* yahoo! */
	       (void) close(open(fntty,O_RDWR));
	       /* XXX: do we really have to reattach? */
	       /* I wish there were no concept of controlling tty. */
	       /* Instead, an ioctl on /dev/tty (i.e., fd 3) would */
	       /* return a session identifier. */

	       if (fdpass != -1)
		{
		 if (pty_sendint(fdpass,'G',&siglerpid) == -1)
		   return -1;
		   /* XXX: death(1) might be more intuitive. Then */
		   /* again, it may also be much more destructive. */
		 if (pty_sendfd(fdpass,'m',&fdmty) == -1)
		   return -1;
		 if (pty_sendfd(fdpass,'s',&fdsty) == -1)
		   return -1;
		}

	       /* So that we can disconnect again, we have to reset the */
	       /* siglerpid in fdsess. That done, we've totally severed */
	       /* our previous link to a connection. */
               (void) lseek(fdsess,(long) sizeof(int),0);
               (void) write(fdsess,(char *) &siglerpid,sizeof(int));

	       flagsigler = 1;
	       (void) setpgrp(0,pgrp);
	       (void) kill(pid,SIGUSR1); /* grrrr */
	       return 0;
     default: (void) pty_putch(fdre," "); break;
    }
 VCF;
 return -1;
}

struct timeval instant = { 0, 0 };

void master(fnsty,child)
char *fnsty;
int child;
{
 fd_set rfds;
 fd_set wfds;
 int fdnum;
 int r;

 /* XXX: is it a race for child to set pty modes? */

 /* Note that we don't close fdsty. */

 siglerpid = getppid();
 slavepid = child;
 pid = getpid();
 glfnsty = fnsty;

 if (flagsession)
  {
   /* Security note: This is the only file we actually create, */
   /* not counting the reconnect socket. */
   (void) sprintf(fnsess,"sess.%s",fnsty + sizeof(DEVSTY) - 3);
   fdsess = open(fnsess,O_RDWR | O_CREAT | O_TRUNC,0600);
   (void) write(fdsess,(char *) &uid,sizeof(int));
   (void) write(fdsess,(char *) &siglerpid,sizeof(int));
   (void) write(fdsess,(char *) &pid,sizeof(int));
   (void) write(fdsess,(char *) &slavepid,sizeof(int));
   /* We'll never actually bother closing fdsess. Who cares? */
  }

 sig_ignore(SIGURG);
 sig_ignore(SIGIO);
 sig_ignore(SIGHUP);
 sig_ignore(SIGQUIT);
 sig_ignore(SIGINT);
 sig_sethandler(SIGXCPU,sig_force); sig_handle(SIGXCPU);
 sig_ignore(SIGXFSZ);
 sig_ignore(SIGPROF);
 sig_ignore(SIGVTALRM);

 sig_default(SIGEMT); /* XXX: really dump? */
 sig_default(SIGIOT);
 sig_default(SIGTRAP);
 sig_default(SIGSYS);
 sig_default(SIGFPE);
 sig_default(SIGILL);
 sig_default(SIGSEGV);

 sig_default(SIGSTOP);

 sig_sethandler(SIGTTIN,sig_ttin); sig_handle(SIGTTIN);
 sig_sethandler(SIGTTOU,sig_ttou); sig_handle(SIGTTOU);
 sig_sethandler(SIGTSTP,sig_tstp); sig_handle(SIGTSTP);
 sig_sethandler(SIGUSR1,sig_cont); sig_handle(SIGUSR1);
 sig_ignore(SIGCONT); /* grrrr. see explanation above sig_cont. */
 sig_sethandler(SIGPIPE,sig_pipe); sig_handle(SIGPIPE);

 sig_sethandler(SIGCHLD,sig_chld); sig_handle(SIGCHLD);

 sig_sethandler(SIGTERM,sig_term); sig_handle(SIGTERM);
 sig_sethandler(SIGWINCH,sig_winch); sig_handle(SIGWINCH);

 sig_sethandler(SIGUSR2,sig_usr2); sig_handle(SIGUSR2);

 if (fdpass != -1)
  {
   if (pty_sendint(fdpass,'G',&siglerpid) == -1)
     death(1);
   if (pty_sendfd(fdpass,'m',&fdmty) == -1)
     death(1);
   if (pty_sendfd(fdpass,'s',&fdsty) == -1)
     death(1);
  }

#define SET_FDNUM fdnum = fdin; if (fdout > fdnum) fdnum = fdout; \
if (fdmty > fdnum) fdnum = fdmty; fdnum++;

 SET_FDNUM

 if (fdpass == -1)
   (void) fcntl(fdmty,F_SETFL,FNDELAY);
   /* If it doesn't work, too bad. */

#ifdef SIGINTERRUPT
 sig_interrupt();
#endif

 for (;;)
  {
   /* Stage 1: Mangle internal states. This could be made into a */
   /* critical section, but there's no point. */

   if ((flagconnected == 2) && (flagchild != 2))
     flagconnected = 1 + 2 * (flagchild == 0);
   if ((flagconnected != 0) && (flagsigler == 0))
    {
     flagconnected = 0;
     if (flagsession)
      {
       (void) kill(siglerpid,SIGTERM);
#ifdef NO_SESSION
       ; /* impossible */
#else
       if (disconnect(fnsty) == -1)
	 quickdeath(1); /* XXX: sigh */
       if (fdnum <= fdre)
	 fdnum = fdre + 1;
#endif
      }
    }

   /* Stage 2: Prepare fds, and select(). */

   FD_ZERO(&rfds);
   FD_ZERO(&wfds);

   if ((fdpass == -1) && (outbuflen < outbufsiz))
     FD_SET(fdmty,&rfds);
   if ((fdpass == -1) && ptybuflen)
     FD_SET(fdmty,&wfds);
   if ((fdpass == -1)
     &&(ptybuflen < ptybufsiz) && (flagsigler == 1)
     &&(flagconnected == 1) && (flagchild == 1))
     FD_SET(fdin,&rfds);
   if ((fdpass == -1)
     &&(outbuflen) && (flagsigler == 1) && (flagconnected == 1))
     FD_SET(fdout,&wfds);

   if (flagsession && (flagconnected == 0))
     FD_SET(fdre,&rfds);

   /* The times to flush buffers: when the child has stopped and we're */
   /* connected; when the child has died and we're connected; when the */
   /* signaller has died and we don't support sessions. */
   if (((flagconnected == 1) && (flagchild != 1))
     ||((flagconnected == 0) && (flagsession == 0)))
     r = select(fdnum,&rfds,&wfds,(fd_set *) 0,&instant);
   else
     r = select(fdnum,&rfds,&wfds,(fd_set *) 0,(struct timeval *) 0);


   /* Stage 3: Interpret the results and handle special cases. */

   if (r <= 0)
     if (r == -1)
       switch(errno)
        {
         case EBADF: death(1);
                     break;
         case EINTR: break; /* fine. */
         case EINVAL: break; /* impossible. */
         default: break; /* say what? */
        }
     else /* r is 0 */
      {
       if (flagconnected == 1) /* flagchild is 0 or 2 */
	 if (flagchild == 0)
	   break; /* That's it! Child died, and we're outta here! */
	 else
	  { /* done with flush, time to stop sigler & idle */
	   if (flagjobctrl)
	    {
	     /* As usual, if we don't have a tty, tmotty == tmochartty
		and it won't matter that fdtty is undefined. */
	     (void) setpgrp(0,pgrp);
  	     if (tty_modifymodes(fdtty,&tmotty,&tmochartty) == -1)
	       ; /* XXX: what to do? */
	     (void) setpgrp(0,pid);
	     switch(childsig)
	      {
	       case SIGSTOP: (void) kill(siglerpid,SIGSTOP); break;
	       case SIGTTOU: (void) kill(siglerpid,SIGTTOU); break;
	       case SIGTTIN: (void) kill(siglerpid,SIGTTIN); break;
	       case SIGTSTP: (void) kill(siglerpid,SIGTSTP); break;
	       case SIGCONT: break; /* special case---see sig_tstp */
	       default: (void) kill(siglerpid,SIGSTOP); break;
	      }
	     flagconnected = 2;
	    }
	  }
       else if (flagconnected == 0) /* non-session, sigler dead */
	 break; /* Giving pty pgrp a HUP, ho hum */
	 /* Most pgrp-based killing would be more logically done */
	 /* one process at a time, i.e., we should give our child */
	 /* a signal specially. But nobody else does, so we won't. */
      }
   else
    {
#ifndef NO_SESSION
     if (flagconnected == 0)
       if (FD_ISSET(fdre,&rfds))
	 if (reconnect() == -1)
	  {
	   if (disconnect(fnsty) == -1)
	     quickdeath(1); /* sigh */
           if (fdnum <= fdre)
	     fdnum = fdre + 1;
          }
	 else
	  {
	   flagconnected = 1; /* yay! */
	   SET_FDNUM
	   continue; /* XXX */
	  }
#endif


   /* Stage 4: Do normal I/O. */

#ifdef SIGINTERRUPT
     sig_startring(); /* blocking? never heard of it */
#endif

     if (FD_ISSET(fdin,&rfds))
      {
       /* ptybuflen must be smaller than ptybufsiz. */
       r = read(fdin,ptybuf + ptybuflen,ptybufsiz - ptybuflen);
       if (r == -1)
	 switch(errno)
	  {
	   case EINTR: case EWOULDBLOCK: break; /* fine */
	   default: death(1);
	  }
       else if (r == 0) /* EOF */
	{
	 ; /* XXX: there's no way to pass an EOF */
	}
       else
	 ptybuflen += r;
      }
     if (FD_ISSET(fdmty,&rfds))
      {
       /* outbuflen must be smaller than outbufsiz. */
       r = read(fdmty,outbuf + outbuflen,outbufsiz - outbuflen);
       if (r == -1)
	 switch(errno)
	  {
	   case EINTR: case EWOULDBLOCK: break; /* fine */
	   default: death(1);
	  }
       else if (r == 0) /* EOF */
	{
	 ; /* This can't happen. The slave can't pass an EOF. */
	 /* XXX: Should we close fdout anyway? */
	}
       else
	 outbuflen += r;
      }
     if (FD_ISSET(fdout,&wfds))
      {
       r = write(fdout,outbuf,outbuflen);
       if (r == -1)
	 switch(errno)
	  {
	   case EINTR: case EWOULDBLOCK: break; /* fine */
	   default: death(1);
	  }
       else if (r == 0) /* ? */
	 ; /* impossible */
       else if (r == outbuflen)
	 outbuflen = 0;
       else
	{
	 outbuflen -= r;
         copy(outbuf,outbuf + r,outbuflen);
	}
      }
     if (FD_ISSET(fdmty,&wfds))
      {
       r = write(fdmty,ptybuf,ptybuflen);
       if (r == -1)
	 switch(errno)
	  {
	   case EINTR: case EWOULDBLOCK: break; /* fine */
	   default: death(1);
	  }
       else if (r == 0) /* ? */
	 ; /* impossible */
       else if (r == ptybuflen)
	 ptybuflen = 0;
       else
	{
	 ptybuflen -= r;
         copy(ptybuf,ptybuf + r,ptybuflen);
	}
      }

#ifdef SIGINTERRUPT
     sig_stopring();
#endif
    }
  }

 death(0);
}
