/*************************************************************************
** ^FILE: pgopen.c - pipe output to a pager for Unix Systems.
**
** ^DESCRIPTION:
**    The functions in this file implement a minimal paging system
**    using popen() & pclose() to invoke a specified paging program.
**
**    The following public functions are defined:
**
**       FILE *pgopen( FILE *fp, const char *pager_cmd )
**           -- attempt to open the pager, return the corresponding
**              file-pointer to write to (which will be <fp> if
**              popen(3S) fails).
**
**       int pgclose( FILE *pager_fp )
**           -- close the pager stream, return 0 on success
**
**       int pgactive( const FILE *pager_fp )
**           -- return TRUE if the pager is open.
**
** ^HISTORY:
**    12/03/90 	Brad Appleton 	<brad@ssd.csd.harris.com> 	Created
***^^**********************************************************************/

#ifdef  USE_PAGER


#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <useful.h>

/* get #defines for access() call */
#include <sys/file.h>
#ifndef X_OK
#  define X_OK  0x01
#endif

/*
**  Implement a state machine that tries first to use a pager
**  specified by the user, then as a second choice, /usr/ucb/more,
**  if all else fails use the given file-pointer (which we assume to
**  be writable)
**
**  The following defines the state machine.
**
*/

typedef enum  {
   PG_CLOSED,  /* pager not-yet-open */
   PG_NONE,    /* No pager used */
   PG_ENV,     /* the given pager name (or $PAGER) was used */
   PG_DFLT,    /* default pager (more) used */
   PG_FILE     /* file-pointer used */
}  pager_t;

#define  DEFAULT_PAGER   "/usr/ucb/more"

#ifndef TRUE
#  define  TRUE  1
#endif
#ifndef FALSE
#  define  FALSE 0
#endif

#define MAX_NAME_LEN  512
#define isTERMINAL(fd)   ( isatty(fd) ) ? TRUE : (errno = 0, FALSE)

extern  char *strcpy();
extern  int   strcmp();
extern  int   access();
extern  int   isatty();
extern  int   wait();
extern  int   sleep();

extern  int  errno;

static void  pg_error();       /* broken-pipe exception handler */
static jmp_buf pg_recover;     /* jump-buffer for exception handler */
static pager_t  pg_pathname();

static pager_t  Pager_Type = PG_CLOSED;
static FILE    *Pager_FP = (FILE *)NULL;


/***************************************************************************
** ^FUNCTION: pgactive - query pager status
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   int pgactive( pager_fp )
/*
** ^PARAMETERS:
*/
   FILE *pager_fp;
/*    -- the file-pointer returned by pgopen()
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Pgactive indicates whether or not the given file-pointer is in
**    use by the paging system.
**
** ^REQUIREMENTS:
**    pgopen() must first be called in order to obtain a valid
**    pager-file-pointer.
**
** ^SIDE-EFECTS:
**    None.
**
** ^RETURN-VALUE:
**    Zero if the pager is active; non-zero otherwise.
**
** ^ALGORITHM:
**    Trivial.
***^^**********************************************************************/
#ifdef __ANSI_C__
   int pgactive( const FILE *pager_fp )
#endif
{
   return  ( (Pager_Type != PG_CLOSED)  &&  (pager_fp) );
}


/***************************************************************************
** ^FUNCTION: pgclose - close the pager.
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   int pgclose( pager_fp )
/*
** ^PARAMETERS:
*/
   FILE *pager_fp;
/*    -- the file-pointer returned by pgopen()
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Pgclose will flush any remaining output and close the pager.
**
** ^REQUIREMENTS:
**    pgopen() must first be called in order to obtain a valid
**    pager-file-pointer.
**
** ^SIDE-EFECTS:
**    None.
**
** ^RETURN-VALUE:
**    Returns 0 if the pager was succesfully close; non-zero if it wasnt
**    in use to begin with.
**
** ^ALGORITHM:
**    - if ( pager-not-in-use )  return 1
**    - flush any remaining pager output
**    - if we used popen() to open this pager then
**       - call pclose to close the pager-program
**       - unset the SIGPIPE signal-handler
**       - wait for the pager-process to die
**      end-if
**    - reset the pager-file-pointer to NULL
**    - set the pager-state to closed
**    - return 0
***^^**********************************************************************/
#ifdef __ANSI_C__
   int pgclose( FILE *pager_fp )
#endif
{
   if ( Pager_Type == PG_CLOSED  ||  !Pager_FP  ||  !pager_fp )  return  1;

   fflush( Pager_FP );

   if ( Pager_Type == PG_ENV  ||  Pager_Type == PG_DFLT ) {
      signal( SIGPIPE, (void (*)())SIG_IGN );
      wait( (int *) 0 );
   }

   Pager_FP = (FILE *)NULL;
   Pager_Type = PG_CLOSED;

   return  0;
}


/***************************************************************************
** ^FUNCTION: pgopen - open the pager
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   FILE *pgopen( fp, pager_cmd )
/*
** ^PARAMETERS:
*/
   FILE *fp;
/*    -- the file pointer to use if popen() fails
*/
   char *pager_cmd;
/*    -- name of the pager-program to pass to popen()
*/
#endif  /* !__ANSI_C__ */

/* ^DESCRIPTION:
**    Pgopen will attempt to "redirect" output from the given file
**    pointer to the pager specified by <pager_cmd>.  If <fp> is NOT
**    connected to a terminal or <pager_cmd> cannot be succesfully
**    opened, then <fp> is resturned output is sent to it unpaginated.
**    Otherwise, pgopen will try to open <pager_cmd> for input. If it
**    cannot succeed it tries to open $PAGER for input. If this also
**    fails then /usr/ucb/more is tried. If all else fails, return <fp>.
**
** ^REQUIREMENTS:
**    pager_cmd should be the full-pathname name of a valid, executable
**    program which reads from standard input and writes (one screenful
**    at a time) to the terminal.
**
** ^SIDE-EFECTS:
**    If popen() succeeds, the SIGPIPE signal is trapped.
**
** ^RETURN-VALUE:
**    returns 0 upon success, non-zero if the pager is already in use.
**
** ^ALGORITHM:
**    - if the pager is already in use, return 1
**    - if <fp> is not connected to a terminal, use it for output and return 0
**    - set up recovery point for SIGPIPE signal
**    - if we can open the pager_cmd, check for a SIGPIPE error
**    - if either of the above fails, then try the same with $PAGER
**    - if either of the above fails, then try the same with /usr/ucb/more
**    - if either of the above fails, just use <fp> and return
***^^**********************************************************************/
#ifdef __ANSI_C__
   FILE *pgopen( FILE *fp, const char *pager_cmd )
#endif
{
   pager_t  pg_type;
   char     pg_name[ MAX_NAME_LEN ];

      /* if a pager is already open - ignore this call */
   if ( Pager_Type != PG_CLOSED  ||  Pager_FP )  return  fp;

      /*
      ** dont page output if it has been redirected
      */
   if ( !isTERMINAL(fileno(fp)) ) {
      Pager_Type = PG_FILE;
      Pager_FP = fp;
      return  fp;
   }

   *pg_name = '\0';
   if ( pager_cmd )  strcpy( pg_name, pager_cmd );
   pg_type = pg_pathname( pg_name );
   Pager_FP = (FILE *)NULL;

      /* jump here after pg_error fields SIGPIPE */
   if ( setjmp( pg_recover ) ) {
      (void) pclose( Pager_FP );
      Pager_FP = (FILE *)NULL;
      pg_type = ( pg_type == PG_ENV ) ? PG_DFLT : PG_NONE;
   }

      /* keep trying till we get a valid file-pointer */
   while ( !Pager_FP ) {
      switch( pg_type ) {
         case PG_DFLT:
               /* jump here if both $PAGER and DEFAULT-PAGER fail */
            if ( setjmp( pg_recover )) {
               (void) pclose( Pager_FP );
               Pager_FP = (FILE *)NULL;
               pg_type = PG_NONE;
               continue;
            }
               /* fall through to next case */

         case PG_ENV:
            signal( SIGPIPE, pg_error );
            Pager_FP = (FILE *) popen( pg_name, "w" );
            if ( !Pager_FP ) {
               if ( pg_type == PG_ENV ) {
                  Pager_FP = (FILE *)NULL;
                  pg_type = PG_DFLT;
               }
               else {
                  Pager_FP = fp;
                  pg_type = PG_NONE;
               }
            }
            else {
                  /*
                  ** Sleep for a bit, just so we block, and the child
                  ** process can get moving. Then attempt to write to
                  ** the pager pipeline. If the pager is bad then the
                  ** pipe will be broken and pg_error() will handle
                  ** the broken-pipe signal. Othwerwise, the write will
                  ** succeed and we'll reset the handler to ignore the
                  ** broken pipe signal.
                  */
               sleep( 1 );
               fputc( '\n', Pager_FP );
               fflush( Pager_FP );
               signal( SIGPIPE, (void (*)())SIG_IGN );
            }
            break;

         case PG_NONE:
         case PG_FILE:
            Pager_FP = fp;
            break;

         default:
            fprintf( stderr, "Unrecognized state [%d] in pgopen()\n",
                     pg_type );
            exit( -1 );
      }/*switch*/
   }/*while*/

   Pager_Type = pg_type;
   return  Pager_FP;
}


/***************************************************************************
** ^FUNCTION: pg_error - handle error when trying to open a pager
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   static void pg_error()
#endif
/*
** ^PARAMETERS:
**    None.
**
** ^DESCRIPTION:
**    Pgerror is called when the SIGPIPE signal is recieved. If SIGPIPE
**    is recieved, it means that popen succesfully forked the shell to
**    run the pager but the pager-command itself broke the read-end of
**    the pipe between us (i.e. it is an invalid pager-program or it crashed).
**
**    When SIGPIPE is recieved, this routine will reset the signal handler
**    to its previous value and jump to the recovery point set up by pgopen.
**
** ^REQUIREMENTS:
**    Pgopen must set up this function is the SIGPIPE handler function.
**
**    Pgopen must place the address of the desired recovery point in
**    pg_recover.
**
** ^SIDE-EFECTS:
**    Resets SIGPIPE signal-handler and performs a non-local goto.
**
** ^RETURN-VALUE:
**    None.
**
** ^ALGORITHM:
**    Trivial.
***^^**********************************************************************/
#ifdef __ANSI_C__
   static void pg_error( void )
#endif
{
   signal( SIGPIPE, (void (*)())SIG_IGN );
   longjmp( pg_recover, 1 );
}


/***************************************************************************
** ^FUNCTION: pg_pathname - get name of pager-program to use
**
** ^SYNOPSIS:
*/
#ifndef __ANSI_C__
   static pager_t pg_pathname( pager_cmd )
/*
** ^PARAMETERS:
*/
   char *pager_cmd;
/*    -- name of the pager-program to verify
*/
#endif  /* !__ANSI_C__ */
 
/* ^DESCRIPTION:
**    Pg_pathname will determine the name of the pager-program to attempt to
**    open based on the values of pager_cmd, and $PAGER.
**
** ^REQUIREMENTS:
**    pager_cmd must be non-null and be large enough to hold any of the
**    possible pager program-names to be opened.
**
** ^SIDE-EFECTS:
**    pager_cmd is over-written with the name of the pager-command to
**    try to open for output
**
** ^RETURN-VALUE:
**    Returns PG_DFLT if /usr/ucb/more is to be used.
**    Returns PG_ENV if $PAGER or <pager_cmd> is to be used.
**
** ^ALGORITHM:
**    - If pager_cmd is executable then compare it to /usr/ucb/more
**        return PG_ENV if not-equal, else return PG_DFLT
**    - Else
**      - pager_cmd = $PAGER
**      - If $PAGER is executable then compare it to /usr/ucb/more
**          return PG_ENV if not-equal, else return PG_DFLT
**      - Else
**          pager_cmd = /usr/ucb/more
**          return  PG_DFLT
**        End-if
**      End-if
***^^**********************************************************************/
#ifdef __ANSI_C__
   static pager_t  pg_pathname( char *pager_cmd )
#endif
{
   char      *pg_name = pager_cmd, *getenv();
   pager_t   pg_type;

   if ( Pager_Type != PG_NONE )   pg_type = Pager_Type;

      /* see if the given pager is okay */
   if ( !pg_name  ||  !*pg_name  ||  !access(pg_name, X_OK) ) {
      pg_name = getenv("PAGER");
   }
   else {
      pg_type = ( strcmp(pager_cmd, DEFAULT_PAGER) ) ? PG_ENV : PG_DFLT;
      return   pg_type;
   }

      /* see if $PAGER is ok */
   if ( !access(pg_name, X_OK) ) {
      pg_type = ( strcmp(pg_name, DEFAULT_PAGER) ) ? PG_ENV : PG_DFLT;
      strcpy( pager_cmd, pg_name );
   }
   else {
      pg_type = PG_DFLT;
      strcpy( pager_cmd, DEFAULT_PAGER );
   }

   return   pg_type;
}/* pg_pathname */


#endif  /* USE_PAGER */
