/*
Copyright (c) 1996-2001 The University of Texas
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.

The GNU Public License is available in the file LICENSE, or you
can write to the Free Software Foundation, Inc., 59 Temple Place -
Suite 330, Boston, MA 02111-1307, USA, or you can find it on the
World Wide Web at http://www.fsf.org.

Programmers: Dogu Arifler, Brian L. Evans, Jun Huang, and Saleem K. Marwat
Version: @(#)ServerThread.java	1.26  04/17/01
*/

/*
Summary:

This program creates threads when new requests for connection to a
particular simulator are received from the client through the
applet interface.  When it is running, it waits for input from the
simulators processes, which are child processes started by the thread. As
parent, this program controls the input and output streams into the
simulator.

One of the key ideas behind WEDS is that the same user interface can
be configured to support different types of digital signal processor
and microcontroller simulators.  To change the number and types of
simulators being supported, simply edit the data in lines 78-137 in
this file.  No application code changes when changing the simulators
that are supported.
*/

/*
Notes:

[1] The Simulator.java applet sends commands to the server using the
outputStream.writeUTF() method.  UTF stands for "Unicode Transmission
Format" which is a compact format to encode 16-bit Unicode characters
in 8-bit bytes. The writeUTF method sends two bytes before sending the
actual data.  When using the MC68HC11 simulator the ServerThread read
the two bytes from its input stream to the applet, processed them and
then read the remaining actual data and processed them.  Our server
was written so that it would ignore the first two bytes in every case,
hence we had a problem.  We assumed that the length of command sent t
the applet could not be larger than some large number. Hence we modified
the serverthread such that if the first byte is larger than 64
(65 is ASCII for 'A') then the two special bytes were already sent,
otherwise we simply strip them off and process.

[2] After running the web simulator, exiting it and starting it again,
it stalled. This was a problem related to the errorstream. We traced it
down to the errorstream by persistent debugging and when finally removed
the use of available() in getError, the simulator would restart properly
after exiting. The simulator would stall because it could not start the
actual simulator using Runtime.getRuntime().exec(pathname).  As a matter
of fact that's exactly where the simulator would stall.
     We tried to fix the problem by making the actual simulator send a
special character (a space in our case) to it's stderr stream after a
command was processed.  If we read a space on stderr, then we knew it was
empty; otherwise, we read the error to be returned and then read the
space character to clean the stream.  This approach, which was implemented
by the getErrorSpace method, failed.  We have gone back to using getError.
*/

package utexas.espl.weds.server;

import java.io.*;
import java.net.*;

public class ServerThread extends Thread {
  // Names of simulator commands to perform specific tasks which
  // will be passed to the Simulator applet
  private static int numSimulators = 4;
  private static String[]
    processorNames =        { "TMS320C30", "MC68HC11", "MC56L811", "MC56811" };
  private static String[]
    simulatorType =         { "instruction set architecture simulator",
                              "instruction set architecture simulator",
                              "board debugger",
                              "instruction set architecture simulator" };
  private static String[]
    commandsCommand =       { "help",     "help",      "helplist", "helplist"};
  private static String[]
    cycleCommand =          { "cycle",    "cycle",     "display",  "history"};
  private static String[]
    enableDebugCommand =    { "+dasm",    "+dasm",     "trace",    "trace"};
  private static String[]
    exitCommand =           { "exit",     "exit",      "exit",     "exit"};
  private static String[]
    loadCommand =           { "load",     "load",      "load",     "load"};
  private static String[]
    helpCommand =           { "help",     "help",      "help",     "help"};
  private static String[]
    procStateCommand =      { "c30state", "registers", "display",  "display"};
  private static String[]
    quitCommand =           { "quit",     "quit",      "quit",     "quit"};
  private static String[]
    simulatorStateCommand = { "simstate", "MC68state", "trace",    "trace"};
  private static String[]
    stepCommand =           { "step",     "step",      "step",     "step"};
  private static String[]
    versionCommand =        { "version",  "version",   "version",  "version"};

  // Sub-directory in which to put programs that the user downloads
  private static String
    UserProgramDirectory = "userprograms/";

  // Data for ServerThread to control simulator programs as processes
  private String[]
    processorPrompts =    { "c30>", "6811>",  "MC568>", "SIM568>"};
  private String[]
    processorProperties = { "env.C30SIM", "env.MC6811", "env.ADS568", "env.SIM568" };
  private String[]
    processorPathNames =  {
      "../simulator/tms320c30/bin.win/c30sim",
      "../simulator/m68hc11/bin.win/m68hc11sim",
      "../simulator/ads568/bin.win/ads56800",
      "../simulator/clas568/bin.win/sim56800" };
  private String[]
    processorArguments =  {
      "",
      "",
      "\"force s\"",
      "" };

  // Available simulator/debugger resources, where -1 means infinite
  // and a non-zero number indicates how many are available
  private static int[]
    availableBoards =           { -1, -1, 1, -1 };

  final static String NO_NEED_CONFIG = "donot send config";
  final static String CONFIG_REQUEST = "send config please";

  final int END_OF_STRING_MARKER = '`';
  final int NUM_APPLET_SKIP_CHARS = 2;                // FIXME: kludge

  // Max. no. of bytes that can be downloaded from a remote site.
  // 1. A TMS320C30 has a maximum of 6 kwords of on-chip memory plus
  //    up to 16 kwords of reserved memory.  The address space is 64
  //    kwords pages.  The C30 hex format takes about 14 characters
  //    per word: 14*22*1024 = 315392 (which is 308 Kb).
  // 2. An MC68HC11 has 64 kb of memory.  The HC11 S19 format takes
  //    2 characters per byte plus header information.  We'll use 3:
  //    3*64*1024 = 196608 bytes (192 Kb)
  // The larger size is for the TMS320C30 format.
  final int MAX_BYTES_TRANSFER = 16*14*1024;

  final String ERR_MSG = "Error: ";
  final String LOAD_CMD = "#LOAD";
  final String DELETE_CMD = "#DELETE";
  final String FILE_EXIST_CMD = "#EXIST";
  final String TERMINATE_CMD = "#BYE";

  // Socket through which the thread communicates with the applet
  private Socket socketHandle = null;
  private String[] args;

  // Global class variables
  final String IN_USE = "Boards Not Available";
  private boolean _DEBUG = false;
  private int simulatorId = -1;
  private String prompt = null;
  private Process simulatorProcess = null;
  private String userProgram = null;
  private String quitString = null;
  private String exitString = null;
  private boolean InUseFlag = false;
  private BufferedReader input = null;
  private OutputStream output = null;
  private String checkString;
  private boolean connectionValid = true;

  final String FILE_CMD = "file";
  final String SIMULATOR_CMD = "simulator";

  // Constructor
  ServerThread(Socket socket, String[] args) {
    super("ServerThread");
    socketHandle = socket;
    this.args = args;
    setDebuggingFlag();
  }

  public synchronized boolean resourceAllocate(int id) {
    if ((id < 0) || (id >= numSimulators)) {
      if (_DEBUG) {
        System.out.println("resourceAllocate: invalid simulator id " +
                           id + " which is not between 0 and " +
                           (numSimulators - 1) + " inclusive");
      }
      return false;
    }
    else if (availableBoards[id] == 0)
      return false;
    else if (availableBoards[id] == -1)
      return true;
    else {
      --availableBoards[id];
      return true;
    }
  }

  // Set the _DEBUG data member if debugging is enabled by checking
  // the setting of the DEBUG property passed from the command line
  private void setDebuggingFlag() {
    String debugProp = System.getProperty("DEBUG", "false");
    _DEBUG = debugProp.equalsIgnoreCase("true");
  }

  // Return the prompt for the simulator
  private String accessSimulatorInfo(String[] info, int simId) {
    if ((simId >= 0) && (simId < numSimulators)) {
      return new String(info[simId]);
    }
    return null;
  }

  // Return the path name of the simulator
  private String simulatorPathName(int simId) {
    if ((simId >= 0) && (simId < numSimulators)) {
      String simPath = System.getProperty(processorProperties[simId],
                                          processorPathNames[simId]);
      return new String(simPath);
    }
    return null;
  }

  // Return the path name of the simulator
  private String simulatorArguments(int simId) {
    if ((simId >= 0) && (simId < numSimulators)) {
      return new String(processorArguments[simId]);
    }
    return null;
  }

  // Read until we hit a specific character
  public String readStreamUntil(DataInputStream ins, char stopChar) {
    StringBuffer readString = new StringBuffer();
    if ( ins != null ) {
      try {
        byte aByteFromStdout = -1;                // end-of-stream return value
        while ( (aByteFromStdout = ins.readByte()) != -1 ) {
          // Ignore carriage returns but keep newlines
          char aCharFromStdout = (char)aByteFromStdout;
          if ( aCharFromStdout != '\r') {
            readString.append(aCharFromStdout);
          }
          if ( aCharFromStdout == stopChar ) break;
        }
      }
      catch(IOException e) {
        e.printStackTrace();
      }
    }
    return readString.toString();
  }

  // Read until we hit a prompt
  public String readStreamUntil(DataInputStream ins, String prompt) {
    StringBuffer readString = new StringBuffer();
    if ( ins != null ) {
      try {
        byte aByteFromStdout = -1;                // end-of-stream return value
        char lastPromptChar = prompt.charAt(prompt.length() - 1);

        // Read until a prompt or an end-of-stream is encountered
        while ( (aByteFromStdout = ins.readByte()) != -1 ) {
          // Ignore carriage returns but keep newlines
          char aCharFromStdout = (char)aByteFromStdout;
          if ( aCharFromStdout != '\r' ) {
            readString.append(aCharFromStdout);
          }
          if ( aCharFromStdout == lastPromptChar ) {
            int startIndex = readString.length() - prompt.length();
            if (startIndex >= 0) {
              String temp = readString.toString();
              if ( prompt.equals(temp.substring(startIndex)) ) {
                break;
              }
            }
          }
        }
      }
      catch(IOException e) {}
    }
    return readString.toString();
  }

  // Start the simulator as a child process
  private void initSimulator(int simId) {
    try {
      String simulatorPath =
          simulatorPathName(simId) + " "+ simulatorArguments(simId);
      simulatorProcess = Runtime.getRuntime().exec(simulatorPath);

      if (_DEBUG) {
        System.out.println("ServerThread: started the simulator process");
        System.out.println(simulatorPath);
      }

      if (_DEBUG) {
        System.out.println("ServerThread: read error stream from simulator");
      }

      // Read the first prompt from the process.  Do not close inputStream
      // as it will also close the input stream for the child process.
      DataInputStream inputStream =
        new DataInputStream(simulatorProcess.getInputStream());
      readStreamUntil(inputStream, prompt);
      if (_DEBUG) {
        System.out.println("ServerThread: read header from simulator");
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }

  public boolean terminateSimulatorCommand(String command) {
    boolean isTerminateCommand = false;
    if (!command.equals("")) {
      String trimmed = command.trim();
      isTerminateCommand =
        trimmed.equals(quitString) || trimmed.equals(exitString);
    }
    return isTerminateCommand;
  }

  // Run the thread which executes a read-evaluate loop
  // until exit is requested by the "parent" applet.
  public void run() {
    if (_DEBUG) {
      System.out.println("ServerThread: run() method invoked.");
    }

    try {
      input = new BufferedReader(new InputStreamReader(socketHandle.getInputStream()));
      output = socketHandle.getOutputStream();
      PrintStream outputStream = new PrintStream(output);

      // Applets inform server if it has the configuration information.
      // If it has, then send configuration; else read the simulator id
      String stringFromApplet = input.readLine();
      if ( stringFromApplet == null ) {
        if (_DEBUG) {
           System.out.println("ServerThread read null string from applet.");
        }
      }
      else if ( stringFromApplet.compareTo(CONFIG_REQUEST) == 0 ) {
        outputStream.println(ConfigInfo());
        outputStream.flush();
        connectionValid = false;
      }
      else {
        int simId = input.read();
        if ((simId < 0) || (simId >= numSimulators)) {
          System.err.println("ServerThread: invalid simulator id " +
                             simId + " read from applets: default to 0");
          simId = 0;
        }
        simulatorId = simId;

        if (resourceAllocate(simulatorId)) {
          // Send the ok message;
          outputStream.println("Boards Available, Happy");
          prompt = accessSimulatorInfo(processorPrompts, simulatorId);
          quitString = accessSimulatorInfo(quitCommand, simulatorId);
          exitString = accessSimulatorInfo(exitCommand, simulatorId);
          initSimulator(simulatorId);
          InUseFlag = false;
        }
        else {
          InUseFlag = true;
          connectionValid = false;
          outputStream.println(IN_USE);
        }
        outputStream.flush();
      }
    }
    catch (IOException e) {
      System.err.println("ServerThread: error with I/O stream to " +
                         "socketHandle " + e.getMessage());
    }

    try {
      // Declare input/output streams over the socket
      while (connectionValid) {
        if (input != null) {
          // Get string coming from the server
          StringBuffer commandFromApplet = new StringBuffer();

          // Read the string sent by the parent applet by writeUTF
          // line by line.  See [1] in the header of this file.
          // 1. Read until we hit a newline character or end of stream
          int charValue = -1;               // end-of-stream return value
          checkString = null;
          String FileOrSimulator = input.readLine();

          // Close the thread if the client is already disconnected
          if (FileOrSimulator == null) {
            connectionValid = false;
            break;
          }

          boolean fileCommand;
          if (FileOrSimulator.compareTo(FILE_CMD)==0)
            fileCommand = true;
          else
            fileCommand = false;
          String commandString = input.readLine();
          checkString = commandString;
          // checkString= cmdDecode(commandString);

          // Check the command from the applet.  The command can be either
          // 1. a file or terminate command, or
          // 2. a command to send to the simulator

          // Conditionally skip the first two bytes in the string by
          // by the applets. See [1] in the notes in the header.

          // If the string is not empty, then process the command.
          // The command will either be a file command or simulator command.
          if (checkString.length() > 0) {
            String resultFromSimulator = new String();

            // Load a file to the server
            if (checkString.startsWith(LOAD_CMD) && fileCommand) {
              String urlAddr =
                checkString.substring(LOAD_CMD.length()+1,
                                      checkString.length());
              String loadErrorString = loadFile( urlAddr );
              if (loadErrorString != null) {
                resultFromSimulator = new String(loadErrorString + "\n");
              }
              else {
                resultFromSimulator = new String("User program loaded\n");
              }
            }

            // Delete a file previously loaded by the user
            else if (checkString.startsWith(DELETE_CMD) && fileCommand) {
              deleteUserFile();
            }

            // Make sure file to be saved as does not exist before
            else if (checkString.startsWith(FILE_EXIST_CMD) && fileCommand) {
              resultFromSimulator = checkForFile(
                       checkString.substring(FILE_EXIST_CMD.length()+1,
                                             checkString.length()));
            }

            // Close the socket connection for this thread
            else if (checkString.startsWith(TERMINATE_CMD) && fileCommand) {
              if (_DEBUG) {
                System.out.println("ServerThread: terminate connection.");
              }
              killSimulatorProcess();
              connectionValid = false;
            }

            // Quit the simulator
            else if (terminateSimulatorCommand(checkString)) {
              if (_DEBUG) {
                System.out.println("ServerThread: terminate simulator.");
              }
              killSimulatorProcess();
              connectionValid = false;
            }

            // Send the command the simulator
            else {
              resultFromSimulator = callSimulator(checkString, prompt);
            }

            // Write the simulator response to the applet over the socket
            for (int i = 0; i < resultFromSimulator.length(); i++ ) {
              output.write(resultFromSimulator.charAt(i));
            }
            output.write((char)END_OF_STRING_MARKER);
            output.flush();
          }
        }
      }
    }
    catch (IOException e) {
      System.err.println("ServerThread: error with I/O stream to " +
                         "socketHandle " + e.getMessage());
    }
    // Now, the thread is in a dead state, so it should be automatically
    // garbage collected.
  }

  // Returns the response from the simulator not including the
  // simulator prompt.  A return of null is not acceptable.
  public synchronized String callSimulator(
                               String commandString, String prompt) {
    String returnString = new String();

    if (simulatorProcess == null) {
      returnString = new String("Error: The simulator is not running.");
    }
    else {
      // Find the input into the simulator process, and send the command
      // to the simulator, and make sure that it ends with a newline
      // Do not close the outputStream as it will also close the output stream
      // for the child process.
      PrintStream outputStream =
        new PrintStream(simulatorProcess.getOutputStream());

      // Make sure that the command sent is terminated by a newline
      int commandLength = commandString.length();
      if ( commandLength <= 0 ) {
        outputStream.print('\n');
      }
      else if (commandString.charAt(commandLength - 1) != '\n') {
        outputStream.println(commandString);
      }
      else {
        outputStream.print(commandString);
      }
      outputStream.flush();

      // Find the output from the simulator process, and read from it
      // it until we hit the prompt and then remove the prompt from
      // the end of the read string.
      // Do not close the inputStream as it will also close the input stream
      // for the child process.
      DataInputStream inputStream =
        new DataInputStream(simulatorProcess.getInputStream());

      String stringFromSimulator = readStreamUntil(inputStream, prompt);
      int endIndex = stringFromSimulator.length() - prompt.length();
      if (endIndex > 0) {
        returnString = stringFromSimulator.substring(0, endIndex);
      }

      String errFromSimulator = getError();
      if ( (returnString.length() == 0 ) &&
           (errFromSimulator.length() != 0) ) {
        returnString = new String( ERR_MSG + errFromSimulator + "\n" );
      }
    }

    notifyAll();

    return returnString;
  }

  // See [2] in the notes in the header
  public synchronized String getError() {
    String errFromSimulator = new String();
    try {
      if (simulatorProcess != null) {
        BufferedReader errorStream =
          new BufferedReader(new InputStreamReader(simulatorProcess.getErrorStream()));

        // We have to check if there are any bytes available
        // in the error stream
        if ( errorStream.ready() ) {
          errFromSimulator = errorStream.readLine();
        }
      }
    }
    catch (IOException e) {}
    notifyAll();
    return errFromSimulator;
  }

  public synchronized String checkForFile(String filename) {
    String status = null;
    try {
      File file = new File(filename);
      if (file.exists()) {
        status = new String("exists");
      }
      else {
        userProgram = new String(filename);
        status = new String("not_exists");
      }
    }
    catch (NullPointerException e) {
      status = new String("Error: file path is null");
    }
    notifyAll();
    return status;
  }

  public synchronized String loadFile(String urlAddr) {
    String error = null;

    try {
      URL remoteFile = new URL( urlAddr );

      try {
        DataOutputStream userFile =
            new DataOutputStream(new FileOutputStream(userProgram));
        URLConnection connection = remoteFile.openConnection();
        String buffer = new String(connection.getContent().toString());
        int fileLen = buffer.length();
        if (fileLen < MAX_BYTES_TRANSFER) {
          userFile.writeBytes(buffer);
        }
        else {
          error = new String( "Error: files are limited to " +
                              MAX_BYTES_TRANSFER +
                              " bytes in length." );
        }
        userFile.flush();
        userFile.close();
      }
      catch (IOException e) {
        error = new String("Error: input/output error");
      }
    }
    catch (MalformedURLException e) {
      System.err.println( "ServerThread: Malformed URL Exception" );
      error = new String( "Error: incorrect URL " + urlAddr );
    }
    notifyAll();
    return error;
  }

  // Clean up before exiting
  private void closeStreams() {
    try {
      if (simulatorProcess != null) {
        InputStream inputStream = simulatorProcess.getInputStream();
        if (inputStream != null) {
          inputStream.close();
        }
      }
    }
    catch (IOException e) {
      System.err.println( "ServerThread: " +
                          "Could not close input stream." );
    }
  }

  // Delete any files created by the user
  private void deleteUserFile() {
    if (userProgram != null) {
      File userFile = new File(userProgram);
      if (userFile.exists()) {
         userFile.delete();
      }
      userProgram = null;
    }
  }

  // Close files, delete temporary files, and close connection to child
  private void recoverChildProcessResources() {
    deleteUserFile();
    closeStreams();
  }

  // Terminate the simulator process
  private void killSimulatorProcess() {
    // FIXME: In Java Developer Kit 1.0.2, calling simulatorProcess.destroy()
    // will hang the applets and prevent new server threads from being created
    simulatorProcess.destroy();
    simulatorProcess = null;
  }

  // Close the socket connection to the applets
  private void closeSocket() {
    //System.out.println("resource retrieve.");
    // FIXME: "#BYE#" should probably be the terminate command
    if ((InUseFlag == false) || checkString.compareTo("#BYE#")!=0){
      resourceRetrieve(simulatorId);
    }
    try {
      if (socketHandle != null ) {
        socketHandle.close();
        socketHandle = null;
      }
    }
    catch (IOException e) {}
  }

  public synchronized void resourceRetrieve(int id){
    if ((id < 0) || (id >= numSimulators)) {
      if (_DEBUG) {
        System.out.println("resourceRetrieve: invalid simulator id " +
                           id + " which is not between 0 and " +
                           (numSimulators - 1) + " inclusive");
      }
      return;
    }
    if (availableBoards[id] == -1) {
      return;
    }
    ++availableBoards[id];
  }

  // Destructor
  protected void finalize() throws Throwable {
    if (_DEBUG) {
      System.out.println("ServerThread: finalize method called.");
    }
    if (InUseFlag == false) {
      recoverChildProcessResources();
      killSimulatorProcess();
    }
    closeSocket();
    super.finalize();
  }

  private static String ConfigInfo() {
    String info = Integer.toString(numSimulators);

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(processorNames[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(simulatorType[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(commandsCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(cycleCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(enableDebugCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(exitCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i=0;i<numSimulators;++i){
      info = info.concat(loadCommand[i] );
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(helpCommand[i] );
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(procStateCommand[i] );
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info =info.concat(quitCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info =info.concat(simulatorStateCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(stepCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    for (int i = 0; i < numSimulators; i++) {
      info = info.concat(versionCommand[i]);
      info = info.concat("#");
    }

    info = info.concat("\n");
    info = info.concat(UserProgramDirectory);
    info = info.concat("\n");

    return info;
  }
}
