ProcessStream.java

/*
** Module   : ProcessStream.java
** Abstract : represents a child process' stdio input or output pipes
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
** 001 GES 20050718   @21744 Created initial version which provides
**                           input and output semantics for the STDIO
**                           pipes (either input or both output pipes
**                           combined) of a child process.
** 002 GES 20050801   @22247 New assign() interface for support of shared
**                           streams.
** 003 GES 20050824   @22213 Minor addition to make multiple calls to
**                           close() more safe.
** 004 GES 20060207   @24327 Translate InterruptedException into a
**                           StopConditionException.
** 005 GES 20060217   @24672 Added writeByte().
** 006 GES 20060225   @24740 Added call to cleanup() in close();
** 007 ECF 20060330   @25260 Throw EOFException from readLn() when no data
**                           was read in the read loop.
** 008 GES 20060518   @26344 Modifications to bypass J2SE process
**                           launching to match requirements in
**                           ProcessDaemon.
** 009 GES 20060811   @28550 Resolve case where output after the last
**                           newline was never seen in the child process
**                           because the STDIN of a pty or tty is line
**                           buffered. An explicit send of an EOF
**                           character just before we close the stream
**                           resolves the problem because it notifies
**                           the child process to flush the stream without
**                           adding any additional real data that will be
**                           seen in the output.
** 010 GES 20060817   @28661 Removed H009 EOF processing because a more
**                           reliable approach to pty configuration has
**                           been taken.  In particular, a full RAW
**                           (non-canonical) mode has been implemented.
** 011 EVL 20070609   @34005 Renaming reserved in Java 6 enum identifier
**                           to be able to compile for Java 6
** 012 NVS 20070725   @34688 Implemented methods isIn() and isOut(), which
**                           are newly added to the base class as abstract.
** 013 SIY 20070928   @35316 Fix for PUT/DISPLAY handling regressions.
**                           Ref: #35314
** 014 GES 20080412   @38019 Support standard character conversion (just
**                           the default behavior, no user-definable
**                           source and/or target codepages yet). This
**                           allows other languages (and the upper ASCII
**                           range in the latin codepages) to be properly
**                           handled. Added support for the no-convert
**                           option as well, to disable this new support.
** 015 SVL 20090612   @42670 If an output error has occurred, any further
**                           output will be silently suppressed until the
**                           buffer size limit will be reached.
** 016 SIY 20090622   @42775 Added method to trigger broken mode externally.
** 017 SVL 20090707   @43085 Some refactoring due to changes in the 
**                           BufferSizeManager.
** 018 SVL 20110403          Added write(byte[]) function to ProcessStream,
**                           IOHelper, InReader, InStream, OutWriter
**                           and OutStream.
** 019 GES 20111006          Use launch manager directly instead of thin client.
** 020 EVL 20130425          Modifying Waiter class to have only one instance if we have to wait
**                           for process with both redirected input and output. The example is
**                           INPUT-OUTPUT THROUGH implementation. The method alreadyInWait() was
**                           added to check for another waiting for given pid.
** 021 CA  20140805          Removed ThinClient import.
** 022 OM  20170622          Implemented peekCh() method.
** 023 ECF 20171026          Added write(byte[], int, int) method.
** 024 EVL 20220325          Base refactoring for Stream based classes getting single byte and array of bytes.
** 025 TT  20230804          Added readByteWorker().
** 026 ES  20250424          Handle StopConditionException through ErrorManager.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.io.*;

/**
 * A stream class supporting input and output semantics for the 
 * <code>STDIO</code> pipes of a child process.  This class can be used for
 * either the input pipe (<code>STDIN</code>) or for both output pipes
 * (<code>STDOUT and STDERR</code>) combined.
 * <p>
 * Due to the (Progress-driven) requirement that a single read return
 * any available data from both the standard output pipes, both pipes are
 * checked and read in series.  The <code>STDOUT</code> pipe is always
 * checked before <code>STDERR</code>.  This artificially makes the two
 * pipes appear as a single stream, however the approach is inherently
 * limited.  If an entire line needs to be read it is critical that the
 * {@link #readLn} is used, since this will be guaranteed to read the whole
 * line from a single stream.  The {@link #readCh} will always read from 
 * <code>STDOUT</code> as long as there are bytes available.  However this
 * means that there is a natural race condition with <code>STDERR</code>.
 * If at one moment <code>STDOUT</code> has no bytes available for reading,
 * then a read will occur from <code>STDERR</code>.  If bytes arrive on
 * <code>STDOUT</code> then the very next call to <code>readCh</code> will
 * first read from <code>STDOUT</code> even if there are bytes that arrived
 * previously on <code>STDERR</code>.  This may make the output appear in
 * a different order than expected.
 * <p>
 * A polling approach is used for all reading to implement the semantic of
 * combining the two pipes.
 * <p>
 * This class does not support getting/setting the length or read/write
 * position of this stream.
 */
public class ProcessStream
extends Stream
{
   /** Standard message for unsupported operations. */
   private static final String UNSUPPORTED = "Not allowed in process STDIO.";
   
   /** Determines if there is any explicit charset to honor. */
   private String cset = Utils.getCharsetOverride();
   
   /** The <code>STDOUT</code> from the child process. */
   private IOHelper out = null;
   
   /** The <code>STDERR</code> from the child process. */
   private IOHelper err = null;
   
   /** The <code>STDIN</code> from the child process. */
   private IOHelper in = null;
   
   /** The child process babysitter. */
   private Waiter child = null;

   /**
    * Determines whether an output error has occurred. If true, any further 
    * output will be silently suppressed.
    */
   private boolean broken = false;

   /**
    * Exception that was raised when the stream become broken. 
    */
   private IOException storedException = null;

   /**
    * Default constructor which creates an instance that implements the
    * standard character conversion.
    */
   public ProcessStream()
   {
      // nothing to do
   }
   
   /**
    * Connects the <code>STDIN</code> of the child process to this stream.
    *
    * @param    pid
    *           The ID of the child process that is reading our input.
    * @param    in
    *           The child process' <code>STDIN</code> stream.
    */
   public void connectIn(int pid, OutputStream in)
   {
      startWaiter(pid);
      this.in = convert ? new OutWriter(in) : new OutStream(in);
   }
   
   /**
    * Connects the <code>STDOUT and STDERR</code> of the child process to
    * this stream.
    *
    * @param    pid
    *           The ID of the child process that is generating output.
    * @param    out
    *           The child process' <code>STDOUT</code> stream.
    * @param    err
    *           The child process' <code>STDERR</code> stream.    
    */
   public void connectOut(int pid, InputStream out, InputStream err)
   {
      startWaiter(pid);
      this.out = convert ? new InReader(out) : new InStream(out); 
      this.err = convert ? new InReader(err) : new InStream(err);
   }
   
   /**
    * Mark stream as broken and it will silently ignore I/O errors.
    */
   public void setBroken()
   {
      // also set special error mode
      makeBroken(new IOException());
   }
   
   /**
    * The number of bytes available to be immediately read without blocking.
    *
    * @return   The number of available bytes.
    * 
    * @throws   IOException 
    */
   public long available()
   throws IOException
   {
      int onum = out.available(); 
      return onum > 0 ? onum : err.available();
   }
   
   /**
    * The 0-based offset into the stream at which the next read or write will 
    * occur.  This does not work for "streams" that require sequential
    * access.
    *
    * @return   The current position in the stream.
    * 
    * @throws   UnsupportedOperationException 
    * @throws   IOException 
    */
   public long getPos()
   throws UnsupportedOperationException,   
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }
   
   /**
    * Moves the current read/write position to the specified absolute 0-based
    * offset.  
    *
    * @param    pos
    *           The new read/write position in the stream.
    *           
    * @throws   UnsupportedOperationException 
    * @throws   IOException 
    */
   public void setPos(long pos)
   throws UnsupportedOperationException,   
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }
   
   /**
    * The length of the stream in bytes.  This does not work for "streams"
    * that require sequential access.
    *
    * @return   The length of the stream.
    *           
    * @throws   UnsupportedOperationException 
    * @throws   IOException 
    */
   public long getLen()
   throws UnsupportedOperationException,   
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }
   
   /**
    * Truncates or extends the stream to the specified length if this stream
    * supports such an operation. If truncation is requested, all data
    * located after this point in the file is discarded.  If extending the
    * file is requested, the values of the data in the extended portion of
    * the file is undefined. 
    *
    * @param    len
    *           The new length of the file.
    *           
    * @throws   UnsupportedOperationException 
    * @throws   IOException 
    */
   public void setLen(long len)
   throws UnsupportedOperationException,   
          IOException
   {
      throw new UnsupportedOperationException(UNSUPPORTED);
   }
   
   /**
    * Write the given character to the output stream.
    *
    * @param    ch
    *           The character to be written.
    *           
    * @throws   IOException 
    */
   public void writeCh(char ch)
   throws IOException
   {
      in.writeCh(ch);
   }
   
   /**
    * Write the given byte to the output stream.
    *
    * @param    b
    *           The byte to be written.
    *           
    * @throws   IOException 
    */
   public void writeByte(byte b)
   throws IOException
   {
      writeCh((char) b);
   }
   
   /**
    * Write the given string to the output stream.
    *
    * @param    data
    *           The data to be written.
    *           
    * @throws   IOException 
    */
   public void write(String data)
   throws IOException
   {
      in.write(data);
   }

   /**
    * Write the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data)
   throws IOException
   {
      in.write(data);
   }
   
   /**
    * Write the specified range of bytes from the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    * @param    off
    *           Starting offset in data from which to read bytes to be written. Must be
    *           non-negative and {@code &lt; data.length}.
    * @param    len
    *           Length of data to be written. Must be non-negative and {@code &lt;= (data.length
    *           - offset)}.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data, int off, int len)
   throws IOException
   {
      in.write(data, off, len);
   }
   
   /**
    * Peeks at the character from the current read position in the stream (reads a character from
    * the current read position in the stream without incrementing stream read position. The next
    * {@code peekCh()} and {@code readCh()} will return the same value).
    * <p>
    * The underlying stream subclass determines the content of the result. Byte oriented streams
    * such as pipes or files will return a byte while streams that generate keystrokes or
    * characters may return a DBCS or Unicode character.
    *
    * @return  The next character read from the stream, -1 on any failure and -2 upon an
    *          {@code EOF}.
    *          
    * @throws  StopConditionException
    *          If any interruption occurs during I/O processing.
    */
   public int peekCh()
   {
      return -1; // TODO: implement some caching of the read value (?)
   }
   
   /**
    * Read the raw integer value of a character from the current read
    * position in the stream. This method DOES NOT convert the value read
    * from the stream's character set into Unicode.
    * <p>
    * At this time, only a single byte will be read no matter what encoding
    * is used by the underlying stream.  This means that DBCS or Unicode
    * encodings will not be properly handled.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   @Override
   public int readCh()
   throws StopConditionException
   {
      return readChWorker(convert);
   }
   
   /**
    * Read the raw integer value of a character from the current read
    * position in the stream. This method DOES NOT convert the value read
    * from the stream's character set into Unicode.
    * <p>
    * At this time, only a single byte will be read no matter what encoding
    * is used by the underlying stream.  This means that DBCS or Unicode
    * encodings will not be properly handled.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   @Override
   public int readByte()
   throws StopConditionException
   {
      // we actually read a byte here
      int res = readByteWorker();

      return res >= 0 ? res & 0x000000FF : res;
   }
   
   /**
    * Read all characters from the current read position in the stream to the
    * next line separator (as determined by the <code>File.separator</code>
    * or to the <code>EOF</code>. Any line separator character(s) and the
    * <code>EOF</code> character are not returned.
    *
    * @return   The next line read from the stream.
    * 
    * @throws   EOFException 
    * @throws   IOException 
    * @throws   InterruptedException 
    */
   public String readLn()
   throws EOFException,   
          IOException,
          InterruptedException
   {
      StringBuilder sb = new StringBuilder();
      
      // set the initial number of bytes available
      int onum = out.available();
      int errnum = err.available();
      
      // loop until some input is detected
      while (child.isAlive() || onum > 0 || errnum > 0)
      {
         if (onum > 0)
         {
            while (sb.indexOf(NEWLINE) < 0 && 
                   (out.available() > 0 || child.isAlive()))
            {
               sb.append((char)out.read());
            }
            break;
         }
         else
         {
            if (errnum > 0)
            {
               while (sb.indexOf(NEWLINE) < 0 && 
                      (err.available() > 0 || child.isAlive()))
               {
                  sb.append((char)err.read());
               }
               break;
            }
         }
         
         // short pause before checking again
         Thread.sleep(increment);
         
         // reset num of available bytes
         onum = out.available();
         errnum = err.available();
      }
      
      return readLineCleanup(sb);
   }
   
   /**
    * Closes the input stream and releases OS resources associated with it.
    */
   public void closeIn()
   {
      try
      {
         cleanup(true, false);
         
         if (out != null)
         {
            out.close();
            out = null;
         }
         
         if (err != null)
         {
            err.close();
            err = null;
         }

      }
      
      catch (InterruptedIOException iioe)
      {
         ErrorManager.handleStopException(new StopConditionException(iioe));
      }
      
      catch (IOException ioe)
      {
         // nothing to do
      }
   }

   /**
    * Closes the output stream and releases OS resources associated with it.
    */
   public void closeOut()
   {
      try
      {
         endOutput();
         cleanup(false, true);
         
         if (in != null)
         {
            in.close();
            in = null;
         }
      }
      
      catch (InterruptedIOException iioe)
      {
         ErrorManager.handleStopException(new StopConditionException(iioe));
      }
      
      catch (IOException ioe)
      {
         // nothing to do
      }

      try
      {
         raiseStopOnCloseIfNecessary();
      }
      finally
      {
         BufferSizeManager.getInstance().notifyCloseStream(this);
      }
   }
   
   /**
    * Closes the stream and releases OS resources associated with it.
    */ 
   public void close()
   {
      closeIn();
      closeOut();
   }
   
   /**
    * State of the input side of the stream.
    *
    * @return   <code>true</code> if the input side of the stream is active.
    */
   public boolean isIn()
   {
      return (out != null);
   }
   
   /**
    * State of the output side of the stream.
    *
    * @return   <code>true</code> if the output side of the stream is active.
    */
   public boolean isOut()
   {
      return (in != null);
   }   
   
   /**
    * Assigns the internal stream reference to the given reference.
    *
    * @param    stream
    *           The new internal stream reference to use for all operations.
    *           
    * @throws   UnsupportedOperationException 
    */
   public void assign(Stream stream)
   throws UnsupportedOperationException
   {
      throw new UnsupportedOperationException("Cannot handle this.");
   }

   /**
    * Starts the child process monitor.
    *
    * @param    pid
    *           The ID of the child process that is generating output.
    */
   private void startWaiter(int pid)
   {
      // In case of INPUT-OUTPUT THROUGH statement we need only one waiter per process
      if (this.child != null && this.child.alreadyInWait(pid))
      {
         return;
      }
      this.child = new Waiter(pid);
      
      Thread t1 = new Thread(this.child);
      t1.start();
   }

   /**
    * Check whether we have exceeded the buffer size limit for the flush operations
    * and, if it is true, re-throw the exception that was raised when the stream
    * become broken.
    *
    * @throws IOException
    *         if we have exceeded the buffer size limit for the flush operations.
    */
   private void raiseExceptionOnFlushIfNecessary()
   throws IOException
   {
      if (storedException == null)
      {
         return;
      }

      BufferSizeManager manager = BufferSizeManager.getInstance();
      
      if (manager.shouldRaiseStop(false, this))
      {
         manager.notifyBroken(this);
         IOException e = storedException;
         storedException = null;
         throw e;
      }
   }

   /**
    * Check whether we have exceeded the buffer size limit for the close operations
    * and, if it is true, display the error message and raise the STOP condition.
    *
    * @throws StopConditionException
    *         if we have exceeded the buffer size limit for the close operations.
    */
   private void raiseStopOnCloseIfNecessary()
   throws StopConditionException
   {
      if (storedException == null)
      {
         return;
      }

      BufferSizeManager manager = BufferSizeManager.getInstance();
      if (manager.shouldRaiseStop(true, this))
      {
         manager.notifyBroken(this);
         storedException = null;
         ErrorManager.showErrorAndAbend(140, PIPE_BROKEN_MESSAGE);
      }
   }

   /**
    * Mark the stream as broken and notify BufferSizeManager about it.
    *
    * @param exc
    *        Exception because of which the stream became broken.   
    */
   private void makeBroken(IOException exc)
   {
      broken = true;
      storedException = exc;
      BufferSizeManager.getInstance().notifyOutputError(this);
   }

   /**
    * Read the raw integer value of a character from the current read
    * position in the stream. This method DOES NOT convert the value read
    * from the stream's character set into Unicode.
    * <p>
    * At this time, only a single byte will be read no matter what encoding
    * is used by the underlying stream.  This means that DBCS or Unicode
    * encodings will not be properly handled.
    *
    * @param    isConvert
    *           Flag to control if the charset conversion is required.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   private int readChWorker(boolean isConvert)
   throws StopConditionException
   {
      int ch = readByteWorker();

      // convert back to the raw value if needed since the out or err stream
      // handled translation to a Unicode char for us
      if (isConvert && ch >= 0)
      {
         ch = Utils.toInt((char) ch, cset);
      }

      return ch;
   }

   /**
    * Read the raw integer value of a byte from the current read
    * position in the stream.
    * <p>
    * At this time, only a single byte will be read.
    *
    * @return   The next character read from the stream, -1 on any failure
    *           and -2 upon an <code>EOF</code>.
    *
    * @throws   StopConditionException
    *           If any interruption occurs during I/O processing.
    */
   private int readByteWorker()
   throws StopConditionException
   {
      try
      {
         // set the initial number of bytes available
         int onum = out.available();
         int errnum = err.available();

         // loop until some input is detected
         while (child.isAlive() || onum > 0 || errnum > 0)
         {
            if (onum > 0)
            {
               return out.read();
            }
            else if (errnum > 0)
            {
               return err.read();
            }
            Thread.sleep(increment); // short pause before checking again

            // reset the number of bytes available
            onum = out.available();
            errnum = err.available();
         }
      }
      catch (EOFException ignored)
      {
         //  The expected return -2 is coalesced with the case when the child is no longer alive and data
         //  from both its stream is no longer available (that is when the while loop exits naturally).
      }
      catch (InterruptedException | InterruptedIOException exception)
      {
         ErrorManager.handleStopException(new StopConditionException(exception));
      }
      catch (IOException ioe)
      {
         return -1;
      }

      return -2;
   }

   /**
    * Provides a simple, common interface to hide the differences between
    * streams and readers.
    */
   private interface IOHelper
   {
      /**
       * The number of bytes available to be immediately read without
       * blocking.
       *
       * @return   The number of available bytes.
       * 
       * @throws   IOException 
       */
      public int available()
      throws IOException;
      
      /**
       * Write the given character to the output destination.
       *
       * @param    ch
       *           The character to be written.
       *           
       * @throws   IOException 
       */
      public void writeCh(char ch)
      throws IOException;
      
      /**
       * Write the given string to the output destination.
       *
       * @param    data
       *           The data to be written.
       *           
       * @throws   IOException 
       */
      public void write(String data)
      throws IOException;
      
      /**
       * Write the given byte array to the output destination.
       *
       * @param    data
       *           The data to be written.
       *
       * @throws   IOException
       */
      public void write(byte[] data)
      throws IOException;
      
      /**
       * Write the specified range of bytes from the given byte array to the output stream.
       *
       * @param    data
       *           The data to be written.
       * @param    off
       *           Starting offset in data from which to read bytes to be written. Must be
       *           non-negative and {@code &lt; data.length}.
       * @param    len
       *           Length of data to be written. Must be non-negative and {@code &lt;=
       *           (data.length - offset)}.
       *
       * @throws   IOException
       *           If an I/O error occurs.
       */
      public void write(byte[] data, int off, int len)
      throws IOException;
      
      /**
       * Read a character from the current read position in the stream.
       * <p>
       * The underlying stream subclass determines the content of the result. 
       * Byte oriented streams such as pipes or files will return a byte
       * while readers that generate keystrokes or characters may return a
       * Unicode character.
       *
       * @return   The next character read from the stream.
       * 
       * @throws   IOException
       */
      public int read()
      throws IOException;
      
      /**
       * Close and release any OS resources.
       *           
       * @throws   IOException 
       */
      public void close()
      throws IOException;
   }
   
   /**
    * Provides low level stream input support.
    */
   private class InStream
   implements IOHelper
   {
      /** The stream from which to read. */
      private InputStream in = null;
      
      /**
       * Construct an instance with the given stream as the source.
       *
       * @param    in
       *           The stream source.
       */
      InStream(InputStream in)
      {
         this.in = in;
      }
      
      /**
       * The number of bytes available to be immediately read without
       * blocking.
       *
       * @return   The number of available bytes.
       *           
       * @throws   IOException 
       */
      public int available()
      throws IOException
      {
         return in.available();
      }
      
      /**
       * Write the given character to the output destination.
       *
       * @param    ch
       *           The character to be written.
       */
      public void writeCh(char ch)
      {
         throw new UnsupportedOperationException("Input stream only.");
      }
      
      /**
       * Write the given string to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(String data)
      {
         throw new UnsupportedOperationException("Input stream only.");
      }
      
      /**
       * Write the given byte array to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(byte[] data)
      {
         throw new UnsupportedOperationException("Input stream only.");
      }
      
      /**
       * Write the specified range of bytes from the given byte array to the output stream.
       *
       * @param    data
       *           The data to be written.
       * @param    off
       *           Starting offset in data from which to read bytes to be written. Must be
       *           non-negative and {@code &lt; data.length}.
       * @param    len
       *           Length of data to be written. Must be non-negative and {@code &lt;=
       *           (data.length - offset)}.
       *
       * @throws   IOException
       *           If an I/O error occurs.
       */
      public void write(byte[] data, int off, int len)
      throws IOException
      {
         throw new UnsupportedOperationException("Input stream only.");
      }
      
      /**
       * Read a character from the current read position in the stream.
       * <p>
       * The underlying stream subclass determines the content of the result. 
       * Byte oriented streams such as pipes or files will return a byte
       * while readers that generate keystrokes or characters may return a
       * Unicode character.
       *
       * @return   The next character read from the stream.
       *           
       * @throws   IOException 
       */
      public int read()
      throws IOException
      {
         return in.read();
      }
      
      /**
       * Close and release any OS resources.
       *           
       * @throws   IOException 
       */
      public void close()
      throws IOException
      {
         in.close();
      }
   }
   
   /**
    * Provides low level stream output support.
    */
   private class OutStream
   implements IOHelper
   {
      /** The stream to which to write. */
      private OutputStream out = null;
      
      /**
       * Construct an instance with the given stream as the source.
       *
       * @param    out
       *           The stream source.
       */
      OutStream(OutputStream out)
      {
         this.out = out;
      }
      
      /**
       * The number of bytes available to be immediately read without
       * blocking.
       *
       * @return   The number of available bytes.
       */
      public int available()
      {
         throw new UnsupportedOperationException("Output stream only.");
      }
      
      /**
       * Write the given character to the output destination.
       *
       * @param    ch
       *           The character to be written.
       *           
       * @throws   IOException 
       */
      public void writeCh(char ch)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }

         try
         {
            out.write(ch);
            out.flush();
         }
         catch (IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }

      /**
       * Write the given string to the output destination.
       *
       * @param    data
       *           The data to be written.
       *           
       * @throws   IOException 
       */
      public void write(String data)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }

         try
         {
            out.write(data.getBytes());
            out.flush();
         }
         catch (IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }

      /**
       * Write the given byte array to the output destination.
       *
       * @param    data
       *           The data to be written.
       *
       * @throws   IOException
       */
      public void write(byte[] data)
      throws IOException
      {
         write(data, 0, data.length);
      }
      
      /**
       * Write the specified range of bytes from the given byte array to the output stream.
       *
       * @param    data
       *           The data to be written.
       * @param    off
       *           Starting offset in data from which to read bytes to be written. Must be
       *           non-negative and {@code &lt; data.length}.
       * @param    len
       *           Length of data to be written. Must be non-negative and {@code &lt;=
       *           (data.length - offset)}.
       *
       * @throws   IOException
       *           If an I/O error occurs.
       */
      public void write(byte[] data, int off, int len)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }
         
         try
         {
            out.write(data, off, len);
            out.flush();
         }
         catch (IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }
      
      /**
       * Read a character from the current read position in the stream.
       * <p>
       * The underlying stream subclass determines the content of the result.
       * Byte oriented streams such as pipes or files will return a byte
       * while readers that generate keystrokes or characters may return a
       * Unicode character.
       *
       * @return   The next character read from the stream.
       */
      public int read()
      {
         throw new UnsupportedOperationException("Output stream only.");
      }
      
      /**
       * Close and release any OS resources.
       *           
       * @throws   IOException 
       */
      public void close()
      throws IOException
      {
         try
         {
            out.close();
         }
         catch(IOException e)
         {
            // suppress exception if the stream is broken
            if (!broken)
               throw e;
         }
      }
   }
   
   /**
    * Provides low level reader input support.
    */
   private class InReader
   implements IOHelper
   {
      /** The reader from which to read. */
      private Reader in = null;
      
      /**
       * Construct an instance with the given stream as the source.
       *
       * @param    in
       *           The stream source.
       */
      InReader(InputStream in)
      {
         try
         {
            this.in = (cset == null) ? new InputStreamReader(in)
                                     : new InputStreamReader(in, cset);
         }
         
         catch (UnsupportedEncodingException uee)
         {
            // fall back to the default
            this.in = new InputStreamReader(in);
         }
      }
      
      /**
       * The number of bytes available to be immediately read without
       * blocking.
       *
       * @return   1 if there is at least 1 (maybe more) bytes ready to read
       *           or 0 if there is no data ready to be read.
       *           
       * @throws   IOException 
       */
      public int available()
      throws IOException
      {
         return in.ready() ? 1 : 0;
      }
      
      /**
       * Write the given character to the output destination.
       *
       * @param    ch
       *           The character to be written.
       */
      public void writeCh(char ch)
      {
         throw new UnsupportedOperationException("Input reader only.");
      }
      
      /**
       * Write the given string to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(String data)
      {
         throw new UnsupportedOperationException("Input reader only.");
      }
      
      /**
       * Write the given byte array to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(byte[] data)
      {
         throw new UnsupportedOperationException("Input reader only.");
      }
      
      /**
       * Write the specified range of bytes from the given byte array to the output stream.
       *
       * @param    data
       *           The data to be written.
       * @param    off
       *           Starting offset in data from which to read bytes to be written. Must be
       *           non-negative and {@code &lt; data.length}.
       * @param    len
       *           Length of data to be written. Must be non-negative and {@code &lt;=
       *           (data.length - offset)}.
       *
       * @throws   IOException
       *           If an I/O error occurs.
       */
      public void write(byte[] data, int off, int len)
      throws IOException
      {
         throw new UnsupportedOperationException("Input reader only.");
      }
      
      /**
       * Read a character from the current read position in the stream.
       * <p>
       * The underlying stream subclass determines the content of the result. 
       * Byte oriented streams such as pipes or files will return a byte
       * while readers that generate keystrokes or characters may return a
       * Unicode character.
       *
       * @return   The next character read from the stream.
       *           
       * @throws   IOException 
       */
      public int read()
      throws IOException
      {
         return in.read();
      }
      
      /**
       * Close and release any OS resources.
       *           
       * @throws   IOException 
       */
      public void close()
      throws IOException
      {
         in.close();
      }
   }
   
   /**
    * Provides low level writer output support.
    */
   private class OutWriter
   implements IOHelper
   {
      /** The stream to which to write. */
      private OutputStreamWriter out = null;

      /**
       * Construct an instance with the given stream as the source.
       *
       * @param    out
       *           The stream source.  
       */
      OutWriter(OutputStream out)
      {
         try
         {
            this.out = (cset == null) ? new OutputStreamWriter(out)
                                      : new OutputStreamWriter(out, cset);
         }
         
         catch (UnsupportedEncodingException uee)
         {
            // fall back to the default
            this.out = new OutputStreamWriter(out);
         }
      }
      
      /**
       * The number of bytes available to be immediately read without
       * blocking.
       *
       * @return   The number of available bytes.
       */
      public int available()
      {
         throw new UnsupportedOperationException("Output writer only.");
      }
      
      /**
       * Write the given character to the output destination.
       *
       * @param    ch
       *           The character to be written.
       */
      public void writeCh(char ch)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }

         try
         {
            out.write(ch);
            out.flush();
         }
         catch (IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }
      
      /**
       * Write the given string to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(String data)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }

         try
         {
            out.write(data, 0, data.length());
            out.flush();
         }
         catch(IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }

      /**
       * Write the given byte array to the output destination.
       *
       * @param    data
       *           The data to be written.
       */
      public void write(byte[] data)
      throws IOException
      {
         write(data, 0, data.length);
      }
      
      /**
       * Write the specified range of bytes from the given byte array to the output stream.
       *
       * @param    data
       *           The data to be written.
       * @param    off
       *           Starting offset in data from which to read bytes to be written. Must be
       *           non-negative and {@code &lt; data.length}.
       * @param    len
       *           Length of data to be written. Must be non-negative and {@code &lt;=
       *           (data.length - offset)}.
       *
       * @throws   IOException
       *           If an I/O error occurs.
       */
      public void write(byte[] data, int off, int len)
      throws IOException
      {
         if (broken)
         {
            raiseExceptionOnFlushIfNecessary();
            return;
         }
         
         char[] cbuf = new char[len];
         for (int i = off; i < len; i++)
         {
            cbuf[i] = (char) data[i];
         }
         
         try
         {
            out.write(cbuf);
            out.flush();
         }
         catch(IOException e)
         {
            makeBroken(e);
            raiseExceptionOnFlushIfNecessary();
         }
      }
      
      /**
       * Read a character from the current read position in the stream.
       * <p>
       * The underlying stream subclass determines the content of the result. 
       * Byte oriented streams such as pipes or files will return a byte
       * while readers that generate keystrokes or characters may return a
       * Unicode character.
       *
       * @return   The next character read from the stream.
       */
      public int read()   
      {
         throw new UnsupportedOperationException("Output writer only.");
      }
      
      /**
       * Close and release any OS resources.
       * 
       * @throws   IOException
       */
      public void close()
      throws IOException
      {
         try
         {
            out.close();
         }
         catch(IOException e)
         {
            // suppress exception if the stream is broken
            if (!broken)
               throw e;
         }
      }
   }
   
   /**
    * Provides a simple mechanism to wait for the exit of a process on an
    * asynchronous basis (on a new thread dedicated to this wait).
    */
   private class Waiter
   implements Runnable
   {
      /** The child process for which to wait. */
      private int pid = -1;
      
      /** Status of the child process. */
      private boolean alive = true;
      
      /**
       * Constructs an instance that has the data needed to wait for the
       * child process and report on that process' state.
       *
       * @param    pid
       *           The child process for which to wait.
       */
      public Waiter(int pid)
      {
         this.pid = pid;
      }
      
      /**
       * Accesses the status of the child process.
       *
       * @return   If <code>true</code>, the process is still running.
       */
      public synchronized boolean isAlive()
      {
         return alive;
      }

      /**
       * Verify if the given process already waited to be done.
       *
       * @return   If <code>true</code>, the process is under waiting for complete.
       */
      public synchronized boolean alreadyInWait(int pid)
      {
         return alive && this.pid == pid;
      }
      
      /**
      * Waits for the child process to die and then sets the 
      * <code>alive</code> status to <code>false</code>.
       */
      public void run()
      {
         // wait for the child process to die, then record that fact
         
         try
         {
            // block until the child process exits
            LaunchManager.pseudoTerminalWait(pid);
         }
         
         finally
         {
            synchronized (this)
            {
               alive = false;
            }
         }
      }
   }
}