//-----------------------------------------------------------------------------
// Copyright © 2003 - Philip Howard - All rights reserved
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//-----------------------------------------------------------------------------
// package	libh/io
// homepage	http://libh.slashusr.org/
//-----------------------------------------------------------------------------
// author	Philip Howard
// email	libh at ipal dot org
// homepage	http://phil.ipal.org/
//-----------------------------------------------------------------------------
// This file is best viewed using a fixed spaced font such as Courier
// and in a display at least 120 columns wide.
//-----------------------------------------------------------------------------

#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

// #include <libh/map.h>

#include "io_lib.h"

#ifndef DEV_NULL
#define DEV_NULL	"/dev/null"
#endif

extern char **environ;


__PROTO_BEGIN__
//-----------------------------------------------------------------------------
// function	open_proc
//
// purpose	Open pipe(s) to a new process running the given command and
//		arguments.  The pipe must be closed via close_proc.
//
// arguments	1 (int *) pointer to store parent end of pipe to stdin
//		2 (int *) pointer to store parent end of pipe to stdout
//		3 (int *) pointer to store parent end of pipe to stderr
//		4 (int) file descriptor to use if no pipe for stdin
//		5 (int) file descriptor to use if no pipe for stdout
//		6 (int) file descriptor to use if no pipe for stderr
//		7 (int) highest descriptor to be closed in child
//		8 (int) 1 fork grandchild and wait for child now
//		9 (const char *) file name to execute
//		10 (const char * const *) array of arguments
//
// returns	(int)  < 0 : error
//		(int) == 0 : OK, child already exited, grandchild running
//		(int)  > 0 : OK, PID of child running, must wait later
//
// note		If the pointers given for stdout and stderr are the same,
//		then a single pipe is created and the child will have both
//		stdout and stderr sharing the write end of that pipe.
//
// note		Caller should keep track of the highest file descriptor it
//		has opened and pass it via argument 7 to ensure that the
//		child process closes everything but standard descriptors.
//
// note		If the caller needs to pass other file descriptors to the
//		child, then this function is not appropriate for use since it
//		closes all descriptors but the standard ones.  Most programs
//		executed via execve() only need to inherit standard files.
//-----------------------------------------------------------------------------
int
open_proc (
    int *		arg_fd_ptr_stdin
    ,
    int *		arg_fd_ptr_stdout
    ,
    int *		arg_fd_ptr_stderr
    ,
    int			arg_inh_fd_stdin
    ,
    int			arg_inh_fd_stdout
    ,
    int			arg_inh_fd_stderr
    ,
    int			arg_fd_high
    ,
    int			arg_wait_now
    ,
    const char *	arg_exec_name
    ,
    char * const *	arg_exec_args
    )
__PROTO_END__
{
    int			pipe_stdin	[2];
    int			pipe_stdout	[2];
    int			pipe_stderr	[2];
    int			fork_pid	;
    int			wait_pid	;
    int			wait_status	;


    //-----------------
    // Check arguments.
    //-----------------
    if ( ! arg_exec_name || ! * arg_exec_name ) return -1;
    if ( ! arg_exec_args || ! * arg_exec_args ) return -1;

    //---------------------------------------
    // Initialize pipe descriptors as closed.
    //---------------------------------------
    pipe_stdin[0]  = pipe_stdin[1]  = -1;
    pipe_stdout[0] = pipe_stdout[1] = -1;
    pipe_stderr[0] = pipe_stderr[1] = -1;

    //--------------------
    // Create the pipe(s).
    //--------------------
    if ( arg_fd_ptr_stdin &&
	 pipe( pipe_stdin ) < 0 ) goto cleanup_return_error;

    if ( arg_fd_ptr_stdout &&
	 pipe( pipe_stdout ) < 0 ) goto cleanup_return_error;

    if ( arg_fd_ptr_stderr &&
	 arg_fd_ptr_stderr != arg_fd_ptr_stdout &&
	 pipe( pipe_stderr ) < 0 ) goto cleanup_return_error;

    //------------------------------
    // Flush standard library files.
    //------------------------------
    fflush( stdout );
    fflush( stderr );

    //--------------------------
    // Create the child process.
    //--------------------------
    fork_pid = fork();
    if ( fork_pid < 0 ) goto cleanup_return_error;

    //--------------------
    // Handle child setup.
    //--------------------
    if ( fork_pid == 0 ) {
	int		fd_stdin	;
	int		fd_stdout	;
	int		fd_stderr	;

	//----------------------------------------------------------------------
	// All of this mess is required because of the possibility of just about
	// any arrangement of source file descriptors being in the range of 0..2
	// and being wrongly closed by dup2().  In extreme cases, even this code
	// cannot arrange the file descriptors correctly (e.g. too many open).
	//----------------------------------------------------------------------

	//------------------------------------------
	// The child must not have parent ends open.
	// This should free up enough descriptors.
	//------------------------------------------
	if ( pipe_stdin[1]  >= 0 ) close( pipe_stdin[1]  );
	if ( pipe_stdout[0] >= 0 ) close( pipe_stdout[0] );
	if ( pipe_stderr[0] >= 0 ) close( pipe_stderr[0] );

	//---------------------------------------------------------------
	// Choose the child descriptors to use, either pipe or inherited.
	//---------------------------------------------------------------
	fd_stdin  = ( pipe_stdin[0]  >= 0 ) ? pipe_stdin[0]  : arg_inh_fd_stdin;
	if ( fd_stdin  < 0 ) fd_stdin  = open( DEV_NULL, O_RDONLY );
	fd_stdout = ( pipe_stdout[1] >= 0 ) ? pipe_stdout[1] : arg_inh_fd_stdout;
	if ( fd_stdout < 0 ) fd_stdout = open( DEV_NULL, O_WRONLY );
	fd_stderr = ( pipe_stderr[1] >= 0 ) ? pipe_stderr[1] : arg_inh_fd_stderr;
	if ( fd_stderr < 0 ) fd_stderr = open( DEV_NULL, O_WRONLY );

	//---------------------------------------------------------------------
	// Move any misaligned descriptors out of standard range so they do not
	// get closed when realigning another one back into the standard range.
	//---------------------------------------------------------------------
	while ( fd_stdin  == 1 || fd_stdin  == 2 ) {
	    if ( ( fd_stdin = dup( fd_stdin  ) ) < 0 ) _exit( -1 );
	}
	while ( fd_stdout == 0 || fd_stdout == 2 ) {
	    if ( ( fd_stdout = dup( fd_stdout ) ) < 0 ) _exit( -1 );
	}
	while ( fd_stderr == 0 || fd_stderr == 1 ) {
	    if ( ( fd_stderr = dup( fd_stderr ) ) < 0 ) _exit( -1 );
	}

	//------------------------------------------------------------
	// Align descriptors to standard values, if not already there.
	//------------------------------------------------------------
	if ( fd_stdin  != 0 && dup2( fd_stdin,  0 ) < 0 ) _exit( -1 );
	if ( fd_stdout != 1 && dup2( fd_stdout, 1 ) < 0 ) _exit( -1 );
	if ( fd_stderr != 2 && dup2( fd_stderr, 2 ) < 0 ) _exit( -1 );

	//---------------------------------------------------------------
	// Raise the level of the highest descriptor to close, if needed.
	//---------------------------------------------------------------
	if ( arg_fd_high < pipe_stdin[0]  )	arg_fd_high = pipe_stdin[0];
	if ( arg_fd_high < pipe_stdout[1] )	arg_fd_high = pipe_stdout[1];
	if ( arg_fd_high < pipe_stderr[1] )	arg_fd_high = pipe_stderr[1];
	if ( arg_fd_high < fd_stdin  )		arg_fd_high = fd_stdin;
	if ( arg_fd_high < fd_stdout )		arg_fd_high = fd_stdout;
	if ( arg_fd_high < fd_stderr )		arg_fd_high = fd_stderr;

	//------------------------------------
	// Close all non-standard descriptors.
	//------------------------------------
	while ( arg_fd_high > 2 ) {
	    close( arg_fd_high );
	    -- arg_fd_high;
	}

	//-----------------------------------------------------
	// If caller requested fast wait now, fork a grandchild
	// so child can exit and parent can wakeup from wait.
	//-----------------------------------------------------
	if ( arg_wait_now ) {
	    fork_pid = fork();
	    if ( fork_pid < 0 ) _exit( -1 );
	    if ( fork_pid > 0 ) _exit( 0 );
	}

	//------------------------------------------------------------------
	// Execute the specified program in the child or grandchild process.
	//------------------------------------------------------------------
	execve( arg_exec_name, arg_exec_args, environ );
	_exit( -1 );
    }

    //------------------------------------------
    // The parent must not have child ends open.
    //------------------------------------------
    if ( pipe_stdin[0]  >= 0 ) close( pipe_stdin[0]  );
    if ( pipe_stdout[1] >= 0 ) close( pipe_stdout[1] );
    if ( pipe_stderr[1] >= 0 ) close( pipe_stderr[1] );

    //-----------------------------------
    // Store file descriptors for caller.
    //-----------------------------------
    if ( arg_fd_ptr_stdin  ) {
	* arg_fd_ptr_stdin  = pipe_stdin[1];
    }
    if ( arg_fd_ptr_stdout ) {
	* arg_fd_ptr_stdout = pipe_stdout[0];
    }
    if ( arg_fd_ptr_stderr ) {
	* arg_fd_ptr_stderr = ( arg_fd_ptr_stderr != arg_fd_ptr_stdout ) ? pipe_stderr[0] : pipe_stdout[0];
    }

    //-------------------------------------------------
    // If caller requested a fast wait, then do it now.
    //-------------------------------------------------
    if ( arg_wait_now ) {

	//------------------------------------------------------------------------
	// Wait until the child process exits, repeating after any signal wakeups.
	//------------------------------------------------------------------------
	do {
	    wait_pid = waitpid( fork_pid, & wait_status, 0 );
	} while ( wait_pid == -1 && errno == EINTR );

	//-------------------------------------------------
	// Return error if child status indicates an error.
	// Else return with no error.
	//-------------------------------------------------
	if ( WIFEXITED( wait_status ) == 0 ) return -1;
	wait_status = WEXITSTATUS( wait_status );
	if ( wait_status != 0 ) return -1;
	return 0;
    }

    //-----------------------------------
    // Return child process ID to caller.
    //-----------------------------------
    return fork_pid;

    //----------------------------------------------
    // Do error cleanup and return error indication.
    //----------------------------------------------
 cleanup_return_error:
    if ( pipe_stderr[0] >= 0 ) close( pipe_stderr[0] );
    if ( pipe_stderr[1] >= 0 ) close( pipe_stderr[1] );
    if ( pipe_stdout[0] >= 0 ) close( pipe_stdout[0] );
    if ( pipe_stdout[1] >= 0 ) close( pipe_stdout[1] );
    if ( pipe_stdin[0]  >= 0 ) close( pipe_stdin[0]  );
    if ( pipe_stdin[1]  >= 0 ) close( pipe_stdin[1]  );
    return -1;
}

