/* Allow to share a single invocation of a program to multiple sessions */

#include <stdlib.h>     
#include <unistd.h>
#include <sys/types.h>  /* needed for select */
#include <stdio.h>      /* needed for stderr */
#include <sys/fcntl.h>  /* needed for open */
#include <sys/stropts.h>/* needed for I_PUSH */
#include <sys/socket.h>
#include <sys/un.h>     /* UNIX socket IPC */
#include <errno.h>      /* error codes form system calls */
#include <sys/termios.h>/* needed for "raw" input mode */
#include <sys/signal.h> /* needed for child process monitoring */
#include <string.h>

#include <sys/ioctl.h>
#define SESSDIR "/usr/tmp/session/"

static int window_stdin;
main (int argc, char **argv)
{
  char name[2048], command[2048]; int i;

  if (argc < 2  || strcmp(argv[1],"-n") == 0 && argc < 4) {
    fputs ("usage: session [-n name] command [args ...]\n",stderr); exit(1);
  }
  if (strcmp(argv[1],"-n") == 0)
  {
    strcpy (name,argv[2]);
    /* rebuild command line broken up into argv[] array */
    strcpy (command,"");
    for (i=3; i<argc-1; i++) {strcat(command,argv[i]); strcat(command," ");}
    strcat (command,argv[argc-1]);
  }
  else
  {
    /* rebuild command line broken up into argv[] array */
    strcpy (command,"");
    for (i=1; i<argc-1; i++) {strcat(command,argv[i]); strcat(command," ");}
    strcat (command,argv[argc-1]);
    strcpy (name,command);
  }
  window_stdin = dup(0);
  session (name,command);
}

char* sub (char*,char*,char*);
static int get_win_size (int fd, struct winsize *win);
static void set_window_size (int rows, int cols, char const *device_name);

session (char *name,char *command)
{
  int sock; /* two way pipeline */
  struct sockaddr_un address; /* named UNIX domain socket (path name) */
  int success; /* return value from system call */
  struct termios normal_state; /* used to restore terminal settings on exit */
  char s[2048];

  sock = socket(AF_UNIX,SOCK_STREAM,0);
  if (sock < 0) { perror ("socket"); exit(1); }

  address.sun_family = AF_UNIX;
  strcpy(s,name); sub(s,"/","_"); /* a file name must not contain "?" */
  strcpy (address.sun_path, SESSDIR); strcat (address.sun_path,s);
  success = connect (sock,(struct sockaddr*) &address,sizeof(address));

  if (success < 0 && errno != ENOENT && errno != ECONNREFUSED)
  {
    fputs("connect: ",stderr); perror(address.sun_path); exit(1);
  }
  if (success < 0 && (errno == ENOENT || errno == ECONNREFUSED))
  {
    /* program is not yet running */
    close (sock);
    sock = launch (name,command);
  }

  /* If stdin is a terminal put is in "raw mode"
     (character by character transmission) */

  if (isatty (0))
  {
    struct termios state;

    ioctl (0,TCGETS,&state);
    normal_state = state;
    state.c_iflag &= ~(INPCK | ISTRIP | INLCR | IGNCR | ICRNL | IUCLC);
    state.c_oflag &= ~OPOST;
    state.c_lflag &= ~(ISIG | ICANON | ECHO);
    state.c_cc[VMIN] = 1;
    state.c_cc[VTIME] = 0;
    ioctl (0,TCSETS,&state);
  }

  printf("You may use a triple ^O to quit this instance of session\r\n");
  while(1)
  {
    fd_set rfds,wfds,efds; /* for input/output multiplexing */
    int n, l, inbytes, outbytes; char buffer[2048];

    FD_ZERO(&efds); FD_ZERO(&wfds); FD_ZERO(&rfds);
    FD_SET(0,&rfds); FD_SET(sock,&rfds);

    select (20,&rfds,&wfds,&efds,0); /* wait for input*/

    if (FD_ISSET(0,&rfds))
    {
      inbytes = read(0,buffer,2048);
      /* try to catch a sequence of three ^o 
       * of course if input comes in very quickly there might be more
       * than just one character, but what can I do ?
       */
      if (buffer[0] == 0x0f) {
        l++;
        if ( l == 3 ) {
          printf("Asked to exit this instance of session!\r\n");
          break;
        }
      } else {
        l = 0;
      }
      if (inbytes < 0) {printf("here3\n"); perror("read"); exit (1); }
      if (inbytes == 0) break; /* end of input. shutdown (sock,1) needed ? */
      outbytes = 0;
      while (outbytes < inbytes)
      {
        n = write (sock,buffer+outbytes,inbytes-outbytes);
        if (n < 0) { perror("write"); exit (1); }
        outbytes += n;
      }
    }
    if (FD_ISSET(sock,&rfds))
    {
      inbytes = read(sock,buffer,2048);
      if (inbytes < 0) {printf("here4\n"); perror("read"); exit (1); }
      if (inbytes == 0) break; /* server closed connection */
      outbytes = 0;
      while (outbytes < inbytes)
      {
        n = write (1,buffer+outbytes,inbytes-outbytes);
        if (n < 0) { perror("write"); exit (1); }
        outbytes += n;
      }
    }
  }

  if (isatty (0)) ioctl (0,TCSETS,&normal_state); /*restore terminal settings*/
}


/* Execute an other program as a sub-process with a pseudo terminal as
   standard input and output. Returns a socket for input and output to the
   program */

static int child;   /* command's process id */
static int ptty,pttys;  /* pseudo teletype, master and slave */

int launch (char* name,char* command)
{
  int socks[2]; /* used by socketpair */
  int sock;    /* IPC connection point of server process */
  struct sockaddr_un address; /* socket path name */
  int success; /* system call return value */
  int max;     /* maximum number of open IPC connections */
  int *connection; /* file descriptors for incoming connections */
  int i;
  char s[2048];

//  int ptty,pttys;  /* pseudo teletype, master and slave */
  char *slavename;
  void signal_handler(int);
  void signal_handler_winch(int);
  int status[2]; /* dummy return argument for wait */
  mode_t newmask, oldmask;
  newmask = 0;
  oldmask = umask(newmask);


  /* maximum number of connections is limits by file descriptor table size */
  max = getdtablesize();
  connection = (int*) malloc (max*sizeof(int));
  for (i=0;i<max;i++) connection[i] = -1; /* mark all connections as unused */

  /* create an anonymous pair of interprocess communication end points */
  success = socketpair(AF_UNIX,SOCK_STREAM,0,socks);
  if (success < 0) { perror ("socketpair"); exit(1); }

  if (fork() != 0) { close (socks[1]); return socks[0]; }

  close (socks[0]); connection[0] = socks[1];

  sock = socket(AF_UNIX,SOCK_STREAM,0);
  if (sock < 0) { perror ("socket"); exit(1); }

  address.sun_family = AF_UNIX;
  strcpy(s,name); sub(s,"/","_"); /* a file name must not contain "?" */
  strcpy (address.sun_path,SESSDIR); strcat (address.sun_path,s);
  
  
  /* make sure that directory exists */
  if (access (SESSDIR,W_OK) == -1)
  {
    success = mkdir (SESSDIR,0777);
    if (success<0) {fputs("mkdir: ",stderr); perror(SESSDIR); exit(1);}
  }
  /* make sure that a file with this name does not already exist */
  if (access (address.sun_path,F_OK) == 0)
  {
    success = unlink (address.sun_path);
    if (success<0) {fputs("unlink:",stderr); perror(address.sun_path); exit(1);}
  }
  success = bind (sock,(struct sockaddr*)&address,sizeof(address));
  if (success < 0) {fputs("bind: ",stderr); perror(address.sun_path); exit(1);}

  success = listen (sock,1); /* 1 = queue size for incoming connections */
  if (success < 0) { perror ("listen"); exit(1); }

  /* use a pseudo terminal as input and output for the command */
  ptty = open("/dev/ptmx", O_RDWR); /*master for all pseudo terminal devices*/

  if (ptty < 0) { perror ("/dev/ptmx"); exit(1); }
  grantpt (ptty); /* give read/wirte premissions to slave device */
  unlockpt (ptty); /* clears a lock flag so that the slave can be opened */
  slavename = (char *)ptsname(ptty); /* get device file path name ("/dev/pts/..") */
/* this occurrence of setsid blocked all signals 
 * however we need SIGWINCH for the resize and there didn't seem to be any
 * sense in this setsid here. The one further down was left in there. */
//  success = setsid(); /* set session identity */
//  if (success < 0) { perror ("setsid"); }

  /* the important side effect of this system call is that the process is
    detached from it controlling terminal, so quit and interrupt signals
    generate by the terminal will no longer effect it */


  /* pick up the original size of the launching window!
     who says that size doesn't matter :-)
   */
  signal_handler_winch(SIGWINCH);
  /* Launch the program as a child process with the slave side of the pseudo
     terminal as stdin, stdout and stderr */

  child = fork();

  if (child == 0) /* following lines executed only by child process */
  {
    int pid;

    success = setsid(); /* set session identity - become session leader */
    if (success < 0) { perror ("setsid"); }

    /* The first terminal file opened  by the  session  leader  that  is not
      already associated with a session becomes the controlling terminal for
      that  session. The  controlling  terminal  plays a special role in
      handling quit and interrupt signals */

    pttys = open(slavename, O_RDWR);
    if (pttys < 0) { fputs("open: ",stderr); perror (slavename); exit(1); }
#if !defined(linux)
    success = ioctl(pttys, I_PUSH, "ptem"); /*Pseudo Terminal Emulation module*/
    if (success < 0) { perror ("ioctl I_PUSH \"ptem\""); exit(1); }
    success = ioctl(pttys, I_PUSH, "ldterm"); /*line discipline terminal module*/
    if (success < 0) { perror ("ioctl I_PUSH \"dlterm\""); exit(1); }
    /* use STREAMS module for BDS4 style terminal compatibilty */
    success = ioctl(pttys, I_PUSH, "ttcompat");
#ifdef sun
    if (success < 0) { perror ("ioctl I_PUSH \"ttcompat\""); exit(1); }
#endif
#endif
    close(connection[0]); close(ptty); /* no longer needed */

    /* make pseudo terminal stdin, stdout and stderr */
    close(0); dup(pttys); close(1); dup(pttys); close(2); dup(pttys);
    close(pttys);

    execl ("/bin/sh","sh","-c",command,0);
    perror ("exec failed");
    exit (1);
  }

  /* get notified when child process terminates (default action: ignore) */
  success = (int) signal (SIGCHLD, signal_handler);
  if (success == (int) SIG_ERR) { perror("signal"); exit(1); }
  success = (int) signal (SIGWINCH, signal_handler_winch);
  if (success == (int) SIG_ERR) { perror("signal"); exit(1); }

  while(1) /*run as long as child process runs and there are open connections*/
  {
    fd_set rfds,wfds,efds;      /* for input/output multiplexing */
    int n, inbytes, outbytes; char buffer[2048];

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

    FD_SET (sock,&rfds);
    for (i=0;i<max;i++) if(connection[i] != -1) FD_SET (connection[i],&rfds);
    FD_SET (ptty,&rfds);
    
    /* wait for connection to come in, no timeout */
    success = select (max,&rfds,&wfds,&efds,0);
    if (success < 0 && errno != EINTR) { perror("select"); exit(1); }
    
    /* "kill" with signal = 0 checks if process exists */
    if (kill (child,0) < 0) {child = 0; break; }
    
    if (success < 0 && errno == EINTR) continue;
    /* Interrupted system call in select */

    if (FD_ISSET (sock,&rfds)) /* new incoming connection */
    {
      struct sockaddr addr; int addrlen; /* dummy return args for accept */

      for (i=0;i<max;i++) if (connection[i]==-1) break; /* find unused socket */
      if (i>=20) { fputs ("< 20 connections\n",stderr); continue; }
      connection[i] = accept (sock,&addr,&addrlen); 
      if (connection[i] < 0) { perror ("accept"); exit(1); }
    }
      
    for (i=0;i<max;i++) if (connection[i] !=- 1)
      if (FD_ISSET (connection[i],&rfds))
    {
      inbytes = read(connection[i],buffer,2048);
      if (inbytes < 0) {printf("here1\n"); perror("read"); exit (1); }
      if (inbytes == 0) { close (connection[i]); connection[i] = -1; }
      outbytes = 0;
      while (outbytes < inbytes)
      {
        n = write (ptty,buffer+outbytes,inbytes-outbytes);
        if (n < 0) { perror("write"); exit (1); }
        outbytes += n;
      }
    }
 
    if (FD_ISSET (ptty,&rfds))
    {
      inbytes = read(ptty,buffer,2048);
      if (inbytes < 0 && kill (child,0)!=-1) {perror("read"); exit (1); }
      if (inbytes == 0 && kill (child,0)!=-1) { fputs("read returned 0 - time to quit?\n",stderr); }
      
      for (i=0;i<max;i++) if (connection[i] != -1)
      {
        outbytes = 0;
        while (outbytes < inbytes)
        {
          n = write (connection[i],buffer+outbytes,inbytes-outbytes);
          if (n < 0) { perror("write"); exit (1); }
          outbytes += n;
        }
      }
    }
    
    /* check if there are still open connections */
    n=0; for (i=0;i<max;i++) if (connection[i] != -1) n++;
    if (n == 0) break;
        
  } /* end of while loop */

  /* clean up before exit */
  
  /* remove socket file system entry (not done by UNIX upon close) */
  success = unlink (address.sun_path);
  if (success<0) {fputs("unlink:",stderr); perror(address.sun_path); exit(1);}

  if (child != 0) /* terminate subprocess if still running */
  {
    success = kill (child,SIGHUP);
    if (success < 0) {fprintf(stderr,"%d: ",child); perror("kill"); exit(1);}
  }
  wait(&status);
  rmdir(SESSDIR); /* don't catch any errors, it mightn't be empty! */

  exit(0);
}

/* execution jumps to this point when UNIX sends a signal to the process */

void signal_handler (int sig)
{
  int pid; int status[2]; /* dummy return argument for wait */
  if (sig == SIGCHLD) pid = wait(&status);
}

void signal_handler_winch (int sig)
{
  struct winsize win;
  get_win_size(window_stdin, &win);
//  printf("rows %d; columns %d;\r\n", win.ws_row, win.ws_col);
  set_window_size (win.ws_row, win.ws_col, "Window Size");
}

/* substitute part of string by another string , input parameter "s"
   is overwritten by the result */

char* sub (char* s,char* part,char* replacement)
{
  int l; char *a; char temp[2048];

  l = strlen(part);
  while ((a = strstr(s, part)) != 0)
  {
    strncpy(temp,s,a-s); temp[a-s] = 0;
    strcat (temp,replacement);
    strcat (temp,a+l);
    strcpy (s,temp);
  }
  return s;
}

static int
get_win_size (int fd, struct winsize *win)
{
  int err = ioctl (fd, TIOCGWINSZ, (char *) win);
  return err;
}

static void
set_window_size (int rows, int cols, char const *device_name)
{
  struct winsize win;

  if (get_win_size (ptty, &win)) {
      if (errno != EINVAL) {
        perror("get_win_size"); exit (1);
      }
      memset (&win, 0, sizeof (win));
  }

  if (rows >= 0)
    win.ws_row = rows;
  if (cols >= 0)
    win.ws_col = cols;

  if (ioctl (ptty, TIOCSWINSZ, (char *) &win)) {
    perror("ioctl TIOCSWINSZ"); exit (1);
  }
}
