/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.util;

//JDK imports
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.FileReader;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.lang.IllegalArgumentException;
/**
	@author  Kedar
	@version 1.0
*/

public class ProcessExecutor
{
    public static final long kDefaultTimeoutMillis = 600000;
    public static final long kSleepTime = 2000;
    private static final long DEFAULT_TIMEOUT_SEC = 600;
    private static final String NEWLINE = System.getProperty("line.separator");
    private long mTimeoutMilliseconds = 0;
    protected String[] mCmdStrings = null;
    protected File mOutFile = null;
    protected File mErrFile = null;
    private OutputStream mOutStream = null;
    private OutputStream mErrStream = null;

    private File mWorkingDir = null; //working directory    
    private String[] mEnv = null; //environment
    private String[] mInputLines = null; // strings to set in process's InputStream (like from redirection)
    private int mExitValue = -1;
    private Process mSubProcess=null; // used to get handle to child process for ProcessManager funtionality
    private boolean mVerboseMode = false;
    private boolean retainExecutionLogs	= false;
    private String lastExecutionOutputString = null;
    private String lastExecutionErrorString = null;
    private static boolean bDebug=false;

    /** 
		Creates new ProcessExecutor
	*/
	public ProcessExecutor(String[] cmd)
	{
		this(cmd, DEFAULT_TIMEOUT_SEC, null);
	}
	
	/** 
		Creates new ProcessExecutor
	*/
	public ProcessExecutor(String[] cmd, String[] inputLines)
	{
		this(cmd, DEFAULT_TIMEOUT_SEC, inputLines);
	}

	/** 
		Creates new ProcessExecutor
	*/
        public ProcessExecutor(String[] cmd, long timeoutSeconds)
	{
		this(cmd, timeoutSeconds, null);
        }
    
        public ProcessExecutor(String[] cmd, long timeoutSeconds, String[] inputLines)
        {
            this(cmd, timeoutSeconds, inputLines, null, null);
        }
        
	/**
		Creates a new <code> ProcessExecutor </code> that executes the given
		command.
		@param cmd String that has command name and its command line arguments
		@param timeoutSeconds long integer timeout to be applied in seconds. After this time
		if the process to execute does not end, it will be destroyed.
	*/
	
        public ProcessExecutor(String[] cmd, long timeoutSeconds, String[] inputLines, 
            String[] env, File workingDir)
	{
            mCmdStrings				= cmd;
            mInputLines             = inputLines;
            mEnv                    = env;
            mWorkingDir             = workingDir;
            char fwdSlashChar       = '/';
            char backSlashChar      = '\\';
            
            if (System.getProperty("Debug") != null) {
                // turn on debug, this option was added to help developers
                // debug the their code
                bDebug=true;
            }                        
            
            for(int i=0; i<mCmdStrings.length; i++)
            {
                if (OS.isUnix())
                {
                    mCmdStrings[i] = mCmdStrings[i].replace(backSlashChar, fwdSlashChar);
                }
                else
                {
                    mCmdStrings[i] = mCmdStrings[i].replace(fwdSlashChar, backSlashChar);
                }
            }
            mTimeoutMilliseconds = (long) timeoutSeconds * 1000;
        }
	
	/** This is the setting after the fact that an instance of ProcessExecutor
	 * is created. This is to be used in case the output and error of the last
	 * execute call has to be retained for latter analysis.
	 * @param s boolean representing whether to retain, true means the buffers
	 * will be retained, false otherwise.
	 */
	public void setExecutionRetentionFlag(final boolean s) {
		this.retainExecutionLogs = s;
	}
	public boolean getExecutionRetentionFlag() {
		return ( this.retainExecutionLogs );
	}
	/** Returns the last 4096 bytes in the error stream of last execution as a String, if
	 * the ProcessExecutor was configured properly. It may return null if the
	 * retentionFlag is set to false.
	 */
	public String getLastExecutionError() {
		return ( this.lastExecutionErrorString );
	}
	/** Returns the last 4096 bytes in the output stream of last execution as a String, if
	 * the ProcessExecutor was configured properly. It may return null if the
	 * retentionFlag is set to false.
	 */
	public String getLastExecutionOutput() {
		return ( this.lastExecutionOutputString );
	}
	private void init() throws ExecException
	{
	  try{
		mOutFile = File.createTempFile("stdout", null);
		mOutFile.deleteOnExit();
		mErrFile = File.createTempFile("stderr", null);
		mErrFile.deleteOnExit();
	  }
	  catch (IllegalArgumentException iae){
		deleteTempFiles();
		throw new ExecException("Internal error (util.ProcessExecutor.init()): "+iae.getMessage());
	  }
	  catch (IOException ioe){
		deleteTempFiles();
		throw new ExecException(cannotCreateTempFiles());
	  }
	}

    private final static String cannotCreateTempFiles(){
	return "Could not create temporary files - check "
	+ System.getProperty("java.io.tmpdir")
	+ " to see if its writeable and not-full";
    }


    private void deleteTempFiles(){
        if (mOutStream != null) {
            try {
                mOutStream.flush();
                mOutStream.close();
            } catch (IOException ioe) {
                // Ignore
            }
        }
        
        if (mErrStream != null) {
            try {
                mErrStream.flush();
                mErrStream.close();
            } catch (IOException ioe) {
                // Ignore
            }
        }
	if (mOutFile != null) mOutFile.delete();
	if (mErrFile != null) mErrFile.delete();
    }
  
    public void execute() throws ExecException
    {
        execute(false);
    }

    
	/*
		Executes the command. Redirects the standard output and error streams
		safely to files. This makes the subprocess NOT block or wait on 
		buffers getting flushed. This is done in a threaded manner.
		Note that the subprocess will be killed if it does not end in given
		timeout.
	 
		@throws ExecException if anything goes wrong in subprocess, or subprocess
		terminates abruptly.
	*/
	
	public String[] execute(boolean bReturnOutputLines) throws ExecException
	{
        return execute(bReturnOutputLines, true);
    }
    
    
    /**
     * Allows a subclass to control the error message returned when a non-zero exit code is 
     * returned from a failed execution
     * @return
     */    
    protected String getExceptionMessage() 
    {
        /* read the error message from error file */
        String errorMessage = getFileBuffer(mErrFile);                                
        if (errorMessage.length() == 0) {
            errorMessage = "The Process Output: " + getLatestOutput(mOutFile);
        }
        return "abnormal subprocess termination: Detailed Message:" + errorMessage;
    }
    
    /*
		Executes the command. Redirects the standard output and error streams
		safely to files. This makes the subprocess NOT block or wait on 
		buffers getting flushed. This is done in a threaded manner.
		Note that the subprocess will be killed if it does not end in given
		timeout.
	 
		@throws ExecException if anything goes wrong in subprocess, or subprocess
		terminates abruptly.
	*/
	
	public String[] execute(boolean bReturnOutputLines, boolean bStartUpTimeLimit) throws ExecException
        {
            init();
            InputStream inputStream = null;
            try
            {
                
                if (bDebug) {
                    System.out.println("\n**** Executing command:");
                    for(int ii=0; ii < mCmdStrings.length; ii++) {
                        System.out.println(mCmdStrings[ii]);
                    }
                }
                
                mSubProcess = Runtime.getRuntime().exec(mCmdStrings, mEnv, mWorkingDir);
                if(mInputLines != null)
                    addInputLinesToProcessInput(mSubProcess);
                if(!bReturnOutputLines)
                    mOutStream = redirectProcessOutput(mSubProcess);
                else
                    inputStream = mSubProcess.getInputStream(); //attach to input stream for later reading
                mErrStream = redirectProcessError(mSubProcess);
                
                // see if process should startup in a limited ammount of time
                // processes used by ProcessManager don't return
                if (bStartUpTimeLimit) {
                    long	timeBefore = System.currentTimeMillis();
                    boolean timeoutReached		= false;
                    boolean isSubProcessFinished	= false;
                    boolean shouldBeDone		= false;
                    while (! shouldBeDone)
                    {
                        sleep(kSleepTime);
                        long timeAfter = System.currentTimeMillis();
                        timeoutReached = (timeAfter - timeBefore) >= mTimeoutMilliseconds;
                        try
                        {
                            mExitValue          	= mSubProcess.exitValue();
                            isSubProcessFinished	= true;
                        }
                        catch(IllegalThreadStateException itse)
                        {
                            isSubProcessFinished = false;
                            //ignore exception
                        }
                        shouldBeDone = timeoutReached || isSubProcessFinished;
                    }
                    if (!isSubProcessFinished)
                    {
                        mSubProcess.destroy();
                        mExitValue = -255;
                        throw new ExecException("Subprocess timed out after "+mTimeoutMilliseconds +"mS");
                    }
                    else
                    {
                        mExitValue = mSubProcess.exitValue();
                        if (debug()) {
                            System.out.println("Subprocess command line = " + a2s(mCmdStrings));
                            System.out.println("Subprocess exit value = " + mExitValue);
                        }
                        if (mExitValue != 0)
                        {
                            mExitValue = mSubProcess.exitValue();
                            if (mExitValue != 0)
                            {   
                                throw new ExecException(getExceptionMessage());
                            }
                        }
                    }
                }
            }
            catch(SecurityException se)
            {
                throw new ExecException(se.getMessage());
            }
            catch(IOException ioe)
            {
            	throw new ExecException(ioe.getMessage());
            }
            finally {

                // retain buffers before deleting them
                retainBuffers();

                // only delete files if the time is limited
                // for processes that don't return, the temp files will remain
                if (bStartUpTimeLimit) {
                    deleteTempFiles();
                }
            }

            if(bReturnOutputLines) {
                return getInputStrings(inputStream);
            } else {
                return null;
            }
            
        }

    /**
     * Get the exit value of the process executed. If this method is called
     * before process execution is complete (i.e. before execute() method has
     * returned, it will return -1. If sub process is terminated at timeout,
     * the method will return -255
     */
    public int getProcessExitValue() {
        return mExitValue;
    }

    private void addInputLinesToProcessInput(Process subProcess) throws ExecException {
	if(mInputLines==null)
            return;
        
        PrintWriter out = null;
        try 
        {
            out = new PrintWriter(new BufferedWriter(
                new OutputStreamWriter(subProcess.getOutputStream())));

            for(int i=0; i<mInputLines.length; i++)
            {
                if(bDebug) {
                    System.out.println("InputLine ->" + mInputLines[i] + "<-");
                }
                out.println(mInputLines[i]);
            }
            out.flush();
        }
        catch (Exception e)
        {
            throw new ExecException(e.getMessage());
        }
        finally
        {
            try
            {
                out.close();
            }
            catch (Throwable t)
            {
        }
        }
    }

    private String[] getInputStrings(InputStream inputStream) throws ExecException
	{
		if(inputStream==null)
            return null;
        BufferedReader in = null;
        ArrayList list = new ArrayList();
        String    str;
        try
		{
			in = new BufferedReader( new InputStreamReader(inputStream));
			while((str=in.readLine())!=null)
                list.add(str);
            if(list.size()<1)
                return null;
            return (String[])list.toArray(new String[list.size()]); 
            
		}
		catch (Exception e)
		{
			throw new ExecException(e.getMessage());
		}
        finally
        {
            try
            {
                in.close();
            }
            catch (Throwable t)
            {
            }
        }
	}

    private OutputStream redirectProcessOutput(Process subProcess) throws ExecException
	{
		OutputStream out = null;
                try
		{
			InputStream		in	= subProcess.getInputStream();
            // Redirect stderr for verbose mode
            if(mVerboseMode) {
                // send output to stderr
                out=System.err;
            } else {
                // send to temp file
                out=new FileOutputStream(mOutFile);
            }
            
			new FlusherThread(in, out).start();
		}
		catch (Exception e)
		{
			throw new ExecException(e.getMessage());
		}
                return out;
	}
	
	private OutputStream redirectProcessError(Process subProcess) throws ExecException
	{
                OutputStream out = null;
		try
		{
			InputStream		in	= subProcess.getErrorStream();
            // Redirect stderr for verbose mode
            if(mVerboseMode) {
                // send output to stderr
                out=System.err;
            } else {
                // send to temp file
                out=new FileOutputStream(mErrFile);
            }
			new FlusherThread(in, out).start();
		}
		catch (Exception e)
		{
			throw new ExecException(e.getMessage());
		}
                return out;
	}

    public void setVerbose(boolean verbose) {
        mVerboseMode=verbose;
    }
    
	private void sleep (long millis)
	{
		try
		{
			Thread.sleep(millis);
		}
		catch(InterruptedException ie)
		{
			//ignore exception
		}
	}
    
    /** Returns the contents of a file as a String. It never returns a null. If
     * the file is empty, an empty string is returned.
     * @param file the file to read
    */
    protected String getFileBuffer(File file) {
        final StringBuffer sb = new StringBuffer();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(file));
            String line = null;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
                sb.append(NEWLINE);
            }
        }
        catch(Exception e) {
            //squelch the exception
        }
        finally {
            try {
                reader.close();
            }
            catch(Exception e) {}
        }
        return ( sb.toString() );
    }
	protected String getLatestOutput(final File f) {
		return ( new RAFileReader(f).readLastBytesAsString() );
	}
	
	public void retainBuffers() {
		if (this.retainExecutionLogs) {
			this.lastExecutionErrorString	= this.getLatestOutput(this.mErrFile);
			this.lastExecutionOutputString	= this.getLatestOutput(this.mOutFile);
		}
	}
	
	private boolean debug() {
		final String td = System.getProperty("java.io.tmpdir");
		final String n = "as_debug_process_executor"; // a debug hook
		final File f = new File(td, n);
		return ( f.exists() );
	}
	private String a2s(String[] a) {
		final StringBuffer s = new StringBuffer();
		if (a != null) {
			for (int i = 0 ; i < a.length ; i++) {
				s.append(a[i]);
				s.append(" ");
			}
		}
		return ( s.toString() );
	}
	private static class RAFileReader {
		final File file;
		final int LAST_BYTES	= 4096;
		final String RMODE		= "r"; //read

		RAFileReader(final File file) {
			this.file= file;
		}

		String readLastBytesAsString() {
			final int n				= getNumberOfBytes(LAST_BYTES);
			final StringBuffer sb	= new StringBuffer();
			final long ln			= file.length(); //if SecurityManager is not present, this is safe.
			if (ln == 0)
				return ( sb.toString() ); //nothing to read, file may not exist, is protected, is a directory etc.
			assert (n <= ln) : ("Asked to read number of bytes more than size of file");
			final long s	= ln - n;
			return ( readWithoutCheck(s) );
		}

		private String readWithoutCheck(final long seekPos) {
			final StringBuffer sb	= new StringBuffer();
			RandomAccessFile rf		= null;
			long ln		= 0L;
			int lines	= 0;
			try {
				rf = new RandomAccessFile(file, RMODE);
				ln = rf.length();
				rf.seek(seekPos);
				String tmp = rf.readLine();
				while (tmp != null) {
					lines++;
					sb.append(tmp);
					//sb.append(Character.LINE_SEPARATOR);
					sb.append('\n'); // adding a newline character is going to add one extra byte
					tmp = rf.readLine();
				}
			}
			catch (Exception e) {
				//e.printStackTrace(); //ignore
			}
			finally {
				try {
					if (rf != null)
						rf.close();
				}
				catch(Exception e) {}//ignore;
			}
			//System.out.println("ln-seekPos = " + (ln - seekPos) );
			//System.out.println("bytes = " + sb.toString().getBytes().length);
			//System.out.println("lines = " + lines);
			//assert ((ln - seekPos) == (sb.toString().getBytes().length + lines)) : "Wrong number of bytes read";
			return ( sb.toString() );
		}

		private int getNumberOfBytes(final int max) {
			final long ln	= file.length();
			return ( max >= ln ? (int)ln : max );
		}
	}    

    // used for ProcessManager to watchdog subProcess
    public Process getSubProcess() {
        return mSubProcess;
    }

	public static void main(String args[])
    {
        testProcessError();
    }
    
    /* This method tests the condition of process throwing an error. 
     * On Unixlike systems this throws an error in error file. On non-unixlike
     * Systems it will throw IOException for CreateProcess, which is desired */
    private static void testProcessError() {
        ProcessExecutor executor = new ProcessExecutor(
            new String[]{"/usr/bin/ls", "-wrongPARAMS123"});
        try {
            executor.execute();
        }
        catch (ExecException ee) {
            System.out.println(ee.getMessage());
        }
    }
}


    /**
     * inner class to flush runtime.exec process so it doesn't hang
     */
    class FlusherThread extends Thread {
        InputStream mInStream	= null;
        OutputStream mOutStream	= null;

        public static final int kSize = 1024;

        FlusherThread(InputStream in, OutputStream out) {
            mInStream = in;
            mOutStream = out;
        }

        public void run() {
        // check for null stream
        if (mInStream == null) return;

        // transfer bytes from input to output stream
        try {
            int byteCnt=0;
            byte[] buffer=new byte[4096];
            while ((byteCnt=mInStream.read(buffer)) != -1) {
                if (mOutStream != null && byteCnt > 0) {
                mOutStream.write(buffer, 0, byteCnt);
                mOutStream.flush();
            }
            yield();
            }
        } catch (IOException e) {
            // ignore
        } finally {
            try {
                mOutStream.close();
            } catch (IOException ioe) {
            // ignore
            }
        }        
    }
}
