/*
 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
 *
 *  This file is licensed to You under the Common Public License (CPL);
 *  You may not use this file except in compliance with the License. You
 *  may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/cpl1.0.php
 *
 *  See the COPYRIGHT.txt file distributed with this work for information
 *  regarding copyright ownership.
 */
package org.jikesrvm.scheduler.greenthreads;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.jikesrvm.VM;
import org.vmmagic.pragma.Uninterruptible;

/**
 * Jikes RVM implementation of <code>java.lang.Process</code>.
 */
public class VMProcess extends java.lang.Process {

  static {
    System.loadLibrary("rvmexec");
  }

  // Processor from which the child process was created.
  // Linux forces us to use the same pthread when we wait
  // for the child process to exit.
  GreenProcessor creatingProcessor;

  // Child's Unix process id
  private int pid;

  // File descriptors and streams connecting VM to child process.
  private int inputDescriptor;
  private int outputDescriptor;
  private int errorDescriptor;
  private OutputStream outputStream; // connected to process's stdin
  private InputStream inputStream;   // connected to process's stdout
  private InputStream errorStream;   // connected to process's stderr

  // Data members for getting the process's exit status.
  // exitStatusLock must be held to access or modify.
  private Object exitStatusLock = new Object();
  private boolean waiting = false;
  private boolean hasExited = false;
  private int exitStatus;

  /**
   * Constructor.
   * @param program name of program being executed
   * @param args array of program arguments
   * @param env array of environment variable bindings (optional)
   * @param dirPath name of directory to use as working directory (optional)
   */
  public VMProcess(String program, String[] args, String[] env, String dirPath) {
    pid = exec4(program, args, env, dirPath);
    creatingProcessor = GreenProcessor.getCurrentProcessor();
    // FIXME: check for error creating process

    // initialize file descriptors
    FileSystem.onCreateFileDescriptor(inputDescriptor, false);
    FileSystem.onCreateFileDescriptor(outputDescriptor, false);
    FileSystem.onCreateFileDescriptor(errorDescriptor, false);
  }

  /**
   * Get the <code>Processor</code> that the child process was
   * created from.
   */
  @Uninterruptible
  public GreenProcessor getCreatingProcessor() {
    return creatingProcessor;
  }

  /**
   * Get the pid of this process.
   */
  public int getPid() {
    return pid;
  }

  /**
   * Get the exit value of the process.
   * @throws IllegalThreadStateException if the process hasn't exited yet
   */
  public int exitValue() {
    synchronized (exitStatusLock) {
      if (!hasExited) {
        // If someone else is waiting for the process to exit,
        // then it obviously hasn't exited.  Otherwise, poll to see if
        // the process has exited.
        if (waiting || !isDead()) {
          throw new IllegalThreadStateException("I'm not dead yet!");
        }

        // Process is dead, and we've recorded its exit status,
        // so let anyone waiting for its status know that
        // we've got it.
        hasExited = true;
        exitStatusLock.notifyAll();
      }
      return exitStatus;
    }
  }

  /**
   * Wait for the process to exit.
   * @return the process's exit value
   */
  public int waitFor() throws InterruptedException {
    synchronized (exitStatusLock) {
      // Make sure no other thread is waiting
      // for the process to exit.
      while (waiting) {
        exitStatusLock.wait();
      }

      // Maybe the process has exited already?
      if (hasExited) {
        return exitStatus;
      }

      // We know that the process hasn't exited, and that no one
      // else is waiting for it.  Set waiting = true, to
      // mark the fact that WE are now doing the wait.
      waiting = true;
    }

    // exitStatusLock is now not held, so other threads can find out
    // that we're waiting without themselves being blocked,
    // i.e., threads that are calling exitValue().

    // Try to wait for the process to exit.
    // We may be interrupted.
    int code;
    try {
      code = waitForInternal();
    } catch (InterruptedException e) {
      // We were interrupted, so end the wait
      synchronized (exitStatusLock) {
        waiting = false;
        exitStatusLock.notifyAll();
      }
      throw e;
    }

    // Process has exited!  Broadcast the exit status.
    synchronized (exitStatusLock) {
      waiting = false;
      hasExited = true;
      exitStatus = code;
      exitStatusLock.notifyAll();
    }

    return code;
  }

  /**
   * Kill the process.
   */
  public void destroy() {
    // We only attempt to kill the process if
    // we think it's still alive.
    synchronized (exitStatusLock) {
      if (!hasExited) {
        destroyInternal();
      }
    }
  }

  /**
   * Get an <code>InputStream</code> to read process's stderr.
   */
  public InputStream getErrorStream() {
    if (errorStream == null) errorStream = getInputStream(errorDescriptor);

    return errorStream;
  }

  /**
   * Get an <code>InputStream</code> to read process's stdout.
   */
  public InputStream getInputStream() {
    if (inputStream == null) inputStream = getInputStream(outputDescriptor);

    return inputStream;
  }

  /**
   * Get an <code>OutputStream</code> to write process's stdin.
   */
  public OutputStream getOutputStream() {
    if (outputStream == null) outputStream = getOutputStream(inputDescriptor);

    return outputStream;
  }

  /**
   * Poll to see if process is dead.
   * <code>exitStatusLock</code> must be held.
   */
  private boolean isDead() {
    try {
      // Using a timeout of 1 second should give the
      // Processor a couple chances to poll to see if the
      // process has exited.
      ThreadProcessWaitData waitData = Wait.processWait(this, 1.0d);
      boolean finished = waitData.finished;
      if (finished) {
        exitStatus = waitData.exitStatus;
      }
      return finished;
    } catch (InterruptedException e) {
      // The thread can get interrupted while
      // on the process wait queue.  However,
      // this is not a blocking call,
      // so we don't want to actually throw an exception.
      java.lang.Thread.currentThread().interrupt();
      return false;
    }
  }

  /**
   * Blocking wait to see if process is dead.
   * Returns exit status.  Should be called with
   * <code>exitStatusLock</code> NOT held, but <code>waiting</code>
   * set to true so no other thread tries to wait
   * at the same time.  (Unix semantics require us to
   * do only a single <code>waitpid()</code> for the same process).
   */
  private int waitForInternal() throws InterruptedException {
    ThreadProcessWaitData waitData = Wait.processWait(this, ThreadEventConstants.WAIT_INFINITE);
    // InterruptedException may be thrown,
    // in which case we definitely did NOT do the waitpid()

    if (VM.VerifyAssertions) VM._assert(waitData.finished);
    return waitData.exitStatus;
  }

  /**
   * Call fork() and execvp() to spawn the process.
   * @return the process id
   */
  private native int exec4(String program, String[] args, String[] env, String dirPath);

  /**
   * Send the process a kill signal.
   */
  private native void destroyInternal();

  private InputStream getInputStream(final int fd) {
    return new InputStream() {
      public int available() throws IOException {
        return FileSystem.bytesAvailable(fd);
      }

      public void close() throws IOException {
        // stream closed implicitly when process exits
      }

      public int read() throws IOException {
        return FileSystem.readByte(fd);
      }

      public int read(byte[] buffer) throws IOException {
        return FileSystem.readBytes(fd, buffer, 0, buffer.length);
      }

      public int read(byte[] buf, int off, int len) throws IOException {
        return FileSystem.readBytes(fd, buf, off, len);
      }
    };
  }

  private OutputStream getOutputStream(final int fd) {
    return new OutputStream() {
      public void write(int b) throws IOException {
        FileSystem.writeByte(fd, b);
      }

      public void write(byte[] b) throws IOException {
        FileSystem.writeBytes(fd, b, 0, b.length);
      }

      public void write(byte[] b, int off, int len) throws IOException {
        FileSystem.writeBytes(fd, b, off, len);
      }

      public void flush() throws IOException {
        FileSystem.sync(fd);
      }

      public void close() throws IOException {
        // stream closed implicitly when process exits
      }
    };
  }
}

