// Copyright (C) 2003-2009 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the CPL Common Public License version 1.0.

package fitnesse.testsystems;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import util.TimeMeasurement;

public class CommandRunner {
  private static final Logger LOG = Logger.getLogger(CommandRunner.class.getName());

  private Process process;
  private String input = "";
  protected List<Throwable> exceptions = new ArrayList<Throwable>();
  protected StringBuffer outputBuffer = new StringBuffer();
  protected StringBuffer errorBuffer = new StringBuffer();
  protected int exitCode = -1;
  private TimeMeasurement timeMeasurement = new TimeMeasurement();
  private String command = "";
  private Map<String, String> environmentVariables;

  public CommandRunner(String command, String input, Map<String, String> environmentVariables) {
    this.command = command;
    this.input = input;
    this.environmentVariables = environmentVariables;
  }

  protected CommandRunner(String command, String input, int exitCode) {
    this(command, input, null);
    this.exitCode = exitCode;
  }

  public void asynchronousStart() throws IOException {
    Runtime rt = Runtime.getRuntime();
    timeMeasurement.start();
    String[] environmentVariables = determineEnvironment();
    process = rt.exec(command, environmentVariables);
    OutputStream stdin = process.getOutputStream();
    InputStream stdout = process.getInputStream();
    InputStream stderr = process.getErrorStream();

    new Thread(new OutputReadingRunnable(stdout, outputBuffer), "CommandRunner stdout").start();
    new Thread(new OutputReadingRunnable(stderr, errorBuffer), "CommandRunner error").start();

    sendInput(stdin);
  }

  private String[] determineEnvironment() {
    if (environmentVariables == null) {
      return null;
    }
    Map<String, String> systemVariables = new HashMap<String, String>(System.getenv());
    systemVariables.putAll(environmentVariables);
    List<String> systemVariableAssignments = new ArrayList<String>();
    for (Map.Entry<String, String> entry : systemVariables.entrySet()) {
      systemVariableAssignments.add(entry.getKey() + "=" + entry.getValue());
    }
    return systemVariableAssignments.toArray(new String[systemVariableAssignments.size()]);
  }

  public void run() throws IOException {
    asynchronousStart();
    join();
  }

  public void join() {
    waitForDeathOf(process);
    timeMeasurement.stop();
    if (isDead(process)) {
      exitCode = process.exitValue();
    }
  }

  private void waitForDeathOf(Process process) {
    int timeStep = 100;
    int maxDelay = 2000;
    try {
      for (int delayed = 0; delayed < maxDelay; delayed += timeStep) {
        if (isDead(process)) {
          return;
        }
        Thread.sleep(timeStep);
      }
    } catch (InterruptedException e) {
      LOG.log(Level.FINE, "Wait for death of process " + process + " interrupted", e);
    }
    LOG.warning("Could not detect death of command line test runner.");
  }

  private boolean isDead(Process process) {
    try {
      process.exitValue();
      return true;
    } catch (IllegalThreadStateException e) {
      return false;
    }
  }

  public void kill() {
    if (process != null) {
      process.destroy();
      join();
    }
  }

  protected void setCommand(String command) {
    this.command = command;
  }

  public String getCommand() {
    return command;
  }

  public String getOutput() {
    return outputBuffer.toString();
  }

  public String getError() {
    return errorBuffer.toString();
  }

  public List<Throwable> getExceptions() {
    return exceptions;
  }

  public boolean hasExceptions() {
    return exceptions.size() > 0;
  }

  public boolean wroteToErrorStream() {
    return errorBuffer.length() > 0;
  }

  public boolean wroteToOutputStream() {
    return outputBuffer.length() > 0;
  }

  public int getExitCode() {
    return exitCode;
  }

  // Used to catch exceptions thrown from the read and write threads.
  public void exceptionOccurred(Exception e) {
    exceptions.add(e);
  }

  public long getExecutionTime() {
    return timeMeasurement.elapsed();
  }

  protected void sendInput(OutputStream stdin) throws IOException {
    try {
      stdin.write(input.getBytes("UTF-8"));
      stdin.flush();
    } finally {
      try {
        stdin.close();
      } catch (IOException e) {
        LOG.log(Level.FINE, "Failed to close output stream", e);
      }
    }
  }

  private class OutputReadingRunnable implements Runnable {
    public InputStream input;
    public StringBuffer buffer;

    public OutputReadingRunnable(InputStream input, StringBuffer buffer) {
      this.input = new BufferedInputStream(input);
      this.buffer = buffer;
    }

    public void run() {
      try {
        int c;
        while ((c = input.read()) != -1)
          buffer.append((char) c);
      } catch (Exception e) {
        exceptionOccurred(e);
      }
    }

  }

  public int waitForCommandToFinish() throws InterruptedException {
    return process.waitFor();
  }
}
@@ -193,27 +193,22 @@
 Buffered
-InputStream
+Reader
 ;%0Aimport
@@ -272,28 +272,107 @@
 java.io.
-OutputStream
+InputStreamReader;%0Aimport java.io.OutputStream;%0Aimport java.io.UnsupportedEncodingException
 ;%0Aimport
@@ -589,16 +589,78 @@
 unner %7B%0A
+  private static final String DEFAULT_CHARSET_NAME = %22UTF-8%22;%0A
   privat
@@ -4671,23 +4671,36 @@
 etBytes(
-%22UTF-8%22
+DEFAULT_CHARSET_NAME
 ));%0A    
@@ -4947,38 +4947,8 @@
 e %7B%0A
-    public InputStream input;%0A
     
@@ -4974,16 +4974,51 @@
  buffer;
+%0A    private BufferedReader reader;
 %0A%0A    pu
@@ -5097,51 +5097,188 @@
    t
-his.input = new BufferedInputStream(input);
+ry %7B%0A        reader = new BufferedReader(new InputStreamReader(input, DEFAULT_CHARSET_NAME));%0A      %7D catch (UnsupportedEncodingException e) %7B%0A        exceptionOccurred(e);%0A      %7D
 %0A   
@@ -5380,21 +5380,22 @@
 e ((c = 
-input
+reader
 .read())

Use UTF-8 encoding for the captured Standard Output and Standard Error on the ErrorLogs ("Output Captured") Pages