/*
 *          Copyright (c) mjh-EDV Beratung, 1996-1999
 *     mjh-EDV Beratung - 63263 Neu-Isenburg - Rosenstrasse 12
 *          Tel +49 6102 328279 - Fax +49 6102 328278
 *                Email info@mjh.teddy-net.com
 *
 *       Author: Jordan Hrycaj <jordan@mjh.teddy-net.com>
 *
 *    $Id: ptycall.c,v 1.21 2000/01/07 12:06:12 jordan Exp $ 
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *   PTYCALL - a simple fork/exec shell using ptys
 *
 *   Acknowledgement (where I got the ideas, from):
 *
 *      File calldbx.c from the xxgdb package
 *      Authors: Po Cheung, March 10, 1989
 *           and Pierre Willard, December 1990
 *
 *  -------------------------------------------------------------------
 *
 *  Functions provided:
 *
 *   FILE *ptyexecvp (const char *file, const char **argv, pid_t *child);
 *
 *     This function creates a child program in the background using
 *     "execvp (file, argv)" and returns a full duplex data stream upon
 *     success.  If the thirs argument "*child" is non NULL, its target
 *     is set to the process id of the child.  In the view of the child,
 *     it is created as "foreground" program with io handled by a pseudo
 *     tty.
 *
 *     The child's stdin, stdout and stderr are assigned to the other
 *     end  of the full duplex data stream, returned as argument.  Also,
 *     the child will have started a new process group.
 *
 *   char ** append_argv (char **argv, char *opt);
 *   void   destroy_argv (char **argv);
 *
 *     These functions help managing an argv argument as passed to the
 *     function ptyexecvp(), above.  The function append_argv() never
 *     returns NULL (but may abort upon memory shortage.)  See the
 *     example below in order to understand, how both functions
 *     append_argv(), and destroy_argv() are applied.
 *
 *
 *   void (*pty_logger(void(*)(const char *, ...)))(const char *, ...);
 *
 +     Assign a printf like function, so that error messages can be
 *     printed
 *
 *  Example:
 *
 *   FILE *fp ;
 *   char *cmd, **argv, buf [1000], *s ;
 *   int pid ;
 *   extern void log_write (const char *format, ...);
 *   
 *   // initialize error messages
 *   pty_logger (log_write);
 *
 *   // list the directory contents using "ls"
 *   cmd  = "/bin/ls";
 *   argv = append_argv      (0, "ls");
 *   argv = append_argv (argv, "/dev");
 *
 *   // the last argument pid might be NULL, but this is not
 *   // recommended, in general
 *   if ((fp = ptyexecvp (cmd, argv, &pid)) != 0) {
 *     fputs ("how much wood could a wood chuck chuck ...\n", fp);
 *     while ((s = fgets (buf, sizeof (buf), fp)) == 0 && errno == EAGAIN)
 *       ;
 *     while (s) {
 *       printf (s);
 *       s = fgets (buf, sizeof (buf), fp) ;
 *     }
 *     fclose (fp);
 *     // Note that the child has become session leader, so this kill()
 *     // might be redundant, here.  Nevertheless you should apply an
 *     // atexit() function that applies the kill() example, below.
 *     kill (pid, SIGTERM);
 *   }
 *   destroy_argv (argv);
 *
 *   // write data to the "cat" program and read it, afterwards
 *   cmd  = "/bin/cat";
 *   argv = append_argv (0, "cat");
 *
 *
 *   // the last argument pid might be NULL, but this is not
 *   // recommended, in general
 *   if ((fp = ptyexecvp (cmd, argv, &pid)) != 0) {
 *     fputs ("how much wood could a wood chuck chuck ...\n", fp);
 *     while ((s = fgets (buf, sizeof (buf), fp)) == 0 && errno == EAGAIN)
 *       ;
 *     while (s) {
 *       printf (s);
 *       s = fgets (buf, sizeof (buf), fp) ;
 *     }
 *     fclose (fp);
 *     // Note that the child has become session leader, so this kill()
 *     // might be redundant, here.  Nevertheless you should apply an
 *     // atexit() function that applies the kill() example, below.
 *     kill (pid, SIGTERM);
 *   }
 *   destroy_argv (argv);
 */

#ifdef linux
/* activate pseudo tty multiplexer stuff */
#define _XOPEN_SOURCE 1000
#endif /* linux */

#define EXPORTING  /* Windows stuff */
#include <includes.h>

#ifndef ExtFunc /* Windows stuff */
#define ExtFunc
#endif

/* windows not supported, here */
#ifndef NESSUSNT
#ifndef _WIN32

#include <stdio.h>
#include <errno.h>

#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif
#ifdef HAVE_STROPTS_H
# include <stropts.h>
#endif
#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
# include <sys/ioctl.h>
#endif
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#ifdef HAVE_SYS_STAT_H
# include <sys/stat.h>
#endif

#ifdef HAVE_TERMIOS_H
# ifdef HAVE_TERMIO_H
#  include <termios.h>
#  include <termio.h>
# else /* HAVE_TERMIO_H */
#  ifdef HAVE_SGTTY_H
#   include <sgtty.h>
#  else
#   include <termios.h>
#  endif
# endif /* HAVE_TERMIO_H */
#else /* HAVE_TERMIOS_H */
# ifdef HAVE_TERMIO_H
#  include <termio.h>
# else
#  ifdef HAVE_SGTTY_H
#   include <sgtty.h>
#  else
#   error Neiter termio.h/termios.h nor sgtty.h available
#  endif
# endif /* HAVE_TERMIO_H */
#endif /* HAVE_TERMIOS_H */

#if HAVE_DIRENT_H
# include <dirent.h>
# define DIRENT struct dirent
#else /* HAVE_DIRENT_H */
# define DIRENT struct direct
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif /* HAVE_DIRENT_H */

#ifndef O_NONBLOCK
# define O_NONBLOCK O_NDELAY
#endif

/* Check whether the tty streams modules are known.  Testing header
   files is a hack, only as the modules "ptem", and "ldterm" may come 
   without a header file. SysV4 always should have these modules. */
#if defined (HAVE_PTEM_H)   || defined (HAVE_SYS_PTEM_H)   ||\
    defined (HAVE_LDTERM_H) || defined (HAVE_SYS_LDTERM_H) ||\
    defined (__svr4__) || defined (__SVR4)
# define HAVE_MOD_LDTERM
# define HAVE_MOD_PTEM
#endif

/* set it for debugging */
#define DUMP_EXECVP

#endif /* _WIN32 */
#endif /* NESSUSNT */

/* ------------------------------------------------------------------------- *
 *                      private variable                                     *
 * ------------------------------------------------------------------------- */

static void (*xlog) (const char *, ...) = 0;

/* ------------------------------------------------------------------------- *
 *                      private definitions                                  *
 * ------------------------------------------------------------------------- */

#ifdef _WIN32
/* we do not expect anything with windows */
# define XMALLOC(x)   malloc (x)
# define XFREE(x)       free (x)
#else
/* The XMALLOC() is assumed to always (not NULL) return zero
   initialized memory */
# define XMALLOC(x)   emalloc (x)
# define XFREE(x)     efree (&(x))
#endif /* _WIN32 */

/* XREALLOC() always returns -- possible uninitialized -- memory */
#define WANT_PTYREALLOC
#define XREALLOC(x,y)  ptyrealloc (x,y) /* defined below */

/* to be called XERROR (("problem", ..)) */
#define XERROR(msg)  {if (xlog != 0) (*xlog) msg; }

#ifndef _WIN32
#ifndef HAVE_UNLOCKPT
#undef HAVE_DEV_PTMX
#endif
#ifndef HAVE_GRANTPT
#undef HAVE_DEV_PTMX
#endif
#ifndef HAVE_PTSNAME
#undef HAVE_DEV_PTMX
#endif

#ifndef HAVE_DEV_PTMX
typedef
struct _find_pty {
  DIR *dev ;
  char path [100] ;
} find_pty ;
#endif /* HAVE_DEV_PTMX */

typedef
struct _io_dev {
  char *name ;
  int fd ;
} io_dev ;
#endif /* _WIN32 */

/* ------------------------------------------------------------------------- *
 *                          private helpers                                  *
 * ------------------------------------------------------------------------- */

#ifdef WANT_PTYREALLOC
static void *
ptyrealloc 
  (void  *p, 
   size_t n)
{
  if ((p =  realloc (p, n)) == 0) {
    XERROR (("Cannot reallocate %u bytes -- aborting\n", n)) ;
    exit (1) ;
  }
  return p ;
}
#endif

#ifndef NESSUSNT /* not ported to windows */
#ifndef _WIN32

#ifndef HAVE_SETSID
#define setsid() setpgrp()
#endif

static void
child_detach_tty
  (void)
{
  int fd;

  /* become session leader */
  setsid ();

  /* lose controlling tty */
# ifdef TIOCNOTTY
  if ((fd = open ("/dev/tty", O_RDWR)) > 0) {
    ioctl (fd, TIOCNOTTY, 0);
    close (fd);
  }
# endif
}


static void
update_line_discipline
  (int fd)
{
  /* modify local and output mode of slave pty */
# ifdef HAVE_TERMIO_H
  struct termio tio;
  ioctl (fd, TCGETA, &tio);
  tio.c_lflag &= ~ECHO;		/* No echo */
  tio.c_oflag &= ~ONLCR;	/* Do not map NL to CR-NL on output */
  ioctl (fd, TCSETA, &tio);
# else
  struct sgttyb tio;
  ioctl (fd, TIOCGETP, &tio);
  tio.sg_flags &= ~ECHO;	
  tio.sg_flags &= ~CRMOD;
  ioctl (fd, TIOCSETP, &tio);
#endif
}

/* ------------------------------------------------------------------------- *
 *             private functions: create/destroy pty descriptor              *
 * ------------------------------------------------------------------------- */

static io_dev*
create_dev
  (void)
{
  io_dev *tty = XMALLOC (sizeof (*tty)) ;
  tty->fd = -1;
  return tty ;
}

static void
destroy_dev
  (io_dev *tty)
{
  if (tty == 0)
    return ;
  if (tty->name != 0)
    XFREE (tty->name) ;
  if (tty->fd >= 0)
    close (tty->fd);
  XFREE (tty);
}

/* ------------------------------------------------------------------------- *
 *                private functions: open master pty                         *
 * ------------------------------------------------------------------------- */

#ifndef HAVE_DEV_PTMX
/* Cycle through a list of pseudo tty names.  To do so, check all the
   entries as allocated in the device directory.  The ptys currently
   availabe depends on the current system configuration. */

static void
close_devpty
  (find_pty *state)
{
  if (state == 0)
    return;
  closedir (state->dev);
  XFREE (state);
}

static find_pty *
open_devpty
  (void)
{
  int err ;
  DIRENT *dirent;
  find_pty *state = XMALLOC (sizeof (*state));

  /* open directory and skip the first two entries . and .. */
  if ((state->dev = opendir ("/dev")) == 0) {
    err = errno ;
    XERROR (("Cannot open directory \"/dev\" for reading: %s.\n",
	      strerror (errno)));
    goto error ;
  }

  if ((dirent = readdir (state->dev)) == 0 ||
      dirent->d_name [0] != '.' ||
      dirent->d_name [1] != '\0') {
    err = errno ;
    XERROR (("Cannot read directory entry \"/dev/.\"\n"));
    goto close_error ;
  }

  if ((dirent = readdir (state->dev)) == 0 ||
      dirent->d_name [0] != '.' ||
      dirent->d_name [1] != '.' ||
      dirent->d_name [2] != '\0') {
    err = errno ;
    XERROR (("Cannot read directory entry \"/dev/..\"\n"));
    goto close_error ;
  }

  strcpy (state->path, "/dev/");
  return state;
  /*@NOTREACHED@*/

 close_error:
  close_devpty (state);
 error:
  XFREE (state);
  errno = err ;
  return 0;
}

static char*
next_devpty
  (find_pty *state)
{
  DIRENT *dirent ;
 
  /* get the next /dev/pty?? entry */
  if (state != 0) {
    while ((dirent = readdir (state->dev)) != 0)
      if (memcmp (dirent->d_name, "pty", 3) == 0 &&
	  dirent->d_name [3] >= 'p' &&
	  strlen (dirent->d_name) <= sizeof (state->path) - 10) {
	strcpy (state->path + 5, dirent->d_name);
	return state->path;
      }
    close_devpty (state);
  }
  return 0;
}
#endif

static io_dev*
master_pty
  (void)
{
  int err ;
  io_dev *dev = create_dev ();
  
# ifdef HAVE_DEV_PTMX
  if((dev->fd = open ("/dev/ptmx", O_RDWR)) >= 0) {
    dev->name = strcpy (XMALLOC (10), "/dev/ptmx");
    return dev;
  }
  XERROR (("Cannot open master pty \"/dev/ptmx\": %s.\n", 
	    strerror (errno)));
  
# else
  find_pty *p = open_devpty ();
  char *s;

  /* find the next pty, available */
  while ((s = next_devpty (p)) != 0)
    if ((dev->fd = open (s, O_RDWR)) >= 0) {
      dev->name = strcpy (XMALLOC (strlen (s) + 1), s);
      close_devpty (p);
      return dev;
    }
  close_devpty (p);
# endif

  destroy_dev (dev);
  XERROR (("Cannot open master pty: no more ptys available.\n"));
  errno = 0 ;
  return 0;
}

/* ------------------------------------------------------------------------- *
 *                private functions: open slave pty                          *
 * ------------------------------------------------------------------------- */

static io_dev*
slave_pty
  (io_dev *master)
{
  int err ;
  io_dev *slave = create_dev ();

# ifdef HAVE_DEV_PTMX
  if (grantpt  (master->fd) != 0) { 
    err = errno ;
    XERROR (("Cannot configure master pty: grantpt(): %s.\n", 
	     strerror (err)));
    goto error_exit ;
  }
  if (unlockpt (master->fd) != 0) {
    err = errno ;
    XERROR (("Cannot configure master pty: unlockpt(): %s.\n", 
	     strerror (err))); 
    goto error_exit ;
  }
  if ((slave->name = ptsname (master->fd)) == 0) {
    err = errno ;
    XERROR (("Cannot assign slave pty name: ptsname(): %s.\n", 
	     strerror (err)));
    goto error_exit ;
  }
  slave->name = strcpy (XMALLOC (strlen (slave->name) + 1), slave->name);

# else
  /* derive the slave name /dev/ttyYX from the master name /dev/ptyXY */
  slave->name = strcpy (XMALLOC (strlen (master->name) + 1), master->name);
  slave->name [5] = 't' ;
# endif /*  HAVE_DEV_PTMX */

  /* open the slave pty */
  if ((slave->fd = open (slave->name, O_RDWR)) < 0) {
    err = errno ;
    XERROR (("Cannot open slave pty \"%s\": %s.\n", 
	     slave->name, strerror (err)));
    goto error_exit ;
  }

# ifdef HAVE_DEV_PTMX
# ifdef HAVE_MOD_PTEM
  if (ioctl (slave->fd, I_PUSH, "ptem") < 0) {
    err = errno ;
    close (slave->fd);
    XERROR (("Cannot push \"ptem\" onto slave pty \"%s\": %s.\n",
	     slave->name, strerror (err)));
    goto error_exit ;
  }
# endif
# ifdef HAVE_MOD_LDTERM
  if (ioctl (slave->fd, I_PUSH, "ldterm") < 0) {
    err = errno ;
    close (slave->fd);
    XERROR (("Cannot push \"ldterm\" onto slave pty \"%s\": %s.\n",
	     slave->name, strerror (err)));
    goto error_exit ;
  }
# endif
# endif /*  HAVE_DEV_PTMX */

  return slave;
  /*@NOTREACHED@*/

 error_exit:
  destroy_dev (slave);
  errno = err ;
  return 0;
}

/* ------------------------------------------------------------------------- *
 *                     public functions: pty stuff                           *
 * ------------------------------------------------------------------------- */

FILE *
ptyexecvp
 (const char  *file,
  const char **argv,
  pid_t     *child)
{
  int pid ;
  FILE *fp ;
  char *s ;
  io_dev *slave, *master;

  if ((master = master_pty ()) == 0)
    return 0;

  if ((pid = fork ()) == -1) {
    int err = errno ;
    XERROR (("Cannot fork with \"%s\": %s.\n", argv [0], strerror (err)));
    errno = err ;
    return 0;
  }
  
  if (pid) { 
    /* parent */
    
    /* set to nonblocking mode */
    fcntl (master->fd, F_SETFL, O_NONBLOCK);

    /* open with read/write access to program */
    if ((fp = fdopen (master->fd, "r+")) == 0) {
      int err = errno ;
      XERROR (("Associating data stream with pty master \"%s\" failed: %s.\n",
	       master->name, strerror (err)));
      errno = err ;
      return 0;
    }
    
    /* clean up, but do not close the master pty */
    master->fd = -1;
    destroy_dev (master);

    /*  turn off stdio buffering  */
    setbuf (fp, 0);

    if (child)
      *child = pid;
    return fp;
  } 

  /* child */
  child_detach_tty ();

  if ((slave = slave_pty (master)) == 0)
    exit (1);
  
  destroy_dev (master);
  update_line_discipline (slave->fd);

  dup2 (slave->fd, 0);
  dup2 (slave->fd, 1);
  dup2 (slave->fd, 2);

# ifdef DUMP_EXECVP
  {
    char *t, buf [10 * 1024] ;
    int n, i = 0 ;

    char  *u = buf + strlen (strcpy (buf, "execvp (")) ;
    int left = sizeof (buf) - 20;
    
    if ((n = strlen (t = (char*)file)) < left) {
      goto start;
      do {
	memcpy (u, ", ", 2);
	u    += 2 ;
	left -= 2 ;
      start:
	memcpy (u, t, n);
	u    += n ;
	left -= n ;
      } while ((t = (char*)argv [i ++]) != 0 && (n = strlen (t)) + 2 < left);
      memcpy (u, ")", 2);
#     ifdef HAVE_DEV_PTMX
      XERROR (("Executing on ptmx slave %s: %s.\n", slave->name, buf));
#     else
      XERROR (("Executing on %s: %s.\n", slave->name, buf));
#     endif
    }
  }
# endif

  /* clean up */
  if (slave->fd <= 2)
    slave->fd = -1;
  destroy_dev (slave);
  
  fcntl (1, F_SETFL, O_APPEND);
  setbuf (stdout, 0);

  /* explicitely gain controlling tty */
# ifdef TIOCSCTTY
  ioctl (0, TIOCSCTTY, 0);
# endif

  execvp (file, (char*const*)argv);
  
  if ((s = (char *)argv [1]) == 0) s = "NULL" ;
  XERROR (("Cannot execvp (%s, {%s, %s ...}): %s.\n", 
	   file, argv [0], s, strerror (errno)));
  exit (1);
}

#endif /* _WIN32 */
#endif /* NESSUSNT */

/* ------------------------------------------------------------------------- *
 *                  public functions: argv/arrc manangement                  *
 * ------------------------------------------------------------------------- */

ExtFunc
void /* define a logger function */
(*pty_logger(void(*f)(const char *, ...)))
     (const char *, ...)
{
  void (*g) (const char *, ...) = xlog ;
  xlog = f ;
  return g ;
}

ExtFunc
char **
append_argv
 (char **argv,
  char   *opt)
{
  int argc, n ;
  
  /* special case */
  if (opt == 0) {
    if (argv == 0) 
      (argv = XMALLOC (sizeof (char*))) [0] = 0 ;
    return argv ;
  }
  
  if (argv == 0) {
    argc = 1 ;
    argv = XMALLOC (2 * sizeof (char*));
#   ifdef _WIN32 /* here XMALLOC does not initialize */
    argv [1] = 0 ;
#   endif
  } else {

    /* calculate dim (argv) - 1 */
    argc = 0 ;
    while (argv [argc ++] != 0)
      ;

    /* append one more item */
    n = (++ argc) * sizeof (char*) ;
    argv = XREALLOC (argv, n) ;
    argv [-- argc] = 0 ;
  }

  /* append one duplicated item before the NULL entry */
  argv [-- argc] = strcpy (XMALLOC (strlen (opt) + 1), opt);
  return argv ;
}

ExtFunc
void
destroy_argv
 (char **argv)
{
  int argc ;
  if (argv == 0)
    return ;
  for (argc = 0; argv [argc] != 0; argc ++)
    XFREE (argv [argc]) ;
  XFREE (argv);
}
