All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xapi.shell.impl.ShellSessionDefault Maven / Gradle / Ivy

Go to download

Everything needed to run a comprehensive dev environment. Just type X_ and pick a service from autocomplete; new dev modules will be added as they are built. The only dev service not included in the uber jar is xapi-dev-maven, as it includes all runtime dependencies of maven, adding ~4 seconds to build time, and 6 megabytes to the final output jar size (without xapi-dev-maven, it's ~1MB).

The newest version!
package xapi.shell.impl;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import xapi.collect.X_Collect;
import xapi.collect.api.Fifo;
import xapi.io.X_IO;
import xapi.io.api.HasLiveness;
import xapi.io.api.LineReader;
import xapi.io.api.StringReader;
import xapi.log.X_Log;
import xapi.log.api.LogLevel;
import xapi.process.X_Process;
import xapi.shell.api.ArgumentProcessor;
import xapi.shell.api.ShellCommand;
import xapi.shell.api.ShellSession;
import xapi.time.X_Time;
import xapi.time.api.Moment;
import xapi.time.impl.RunOnce;
import xapi.util.X_Debug;
import xapi.util.api.ErrorHandler;
import xapi.util.api.Pointer;
import xapi.util.api.RemovalHandler;
import xapi.util.api.SuccessHandler;

class ShellSessionDefault implements ShellSession, Runnable {

  Process process;
  public boolean finished;
  private final ShellCommandDefault command;
  private final StringReader onStdErr = new StringReader();
  private final StringReader onStdOut = new StringReader();
  private final Fifo stdIns = X_Collect.newFifo();
  private final Fifo clears = X_Collect.newFifo();
  private final SuccessHandler callback;
  private final ErrorHandler err;
  private final ArgumentProcessor processor;
  private final Moment birth = X_Time.now();
  private final RunOnce once = new RunOnce();

  private boolean normalCompletion;
  PipeOut out;
  private Integer status;

  public ShellSessionDefault(final ShellCommandDefault cmd,
      final ArgumentProcessor argProcessor, final SuccessHandler onSuccess, final ErrorHandler onError) {
    this.command = cmd;
    this.callback = onSuccess;
    this.err = onError;
    this.processor = argProcessor;
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Override
  public void run() {
    final InputStream stdOut;
    final InputStream stdErr;
    synchronized (this) {
      if (process == null) {
        InputStream o = null;
        InputStream e = null;
        try {
          process = command.doRun(processor);
          o = process.getInputStream();
          e = process.getErrorStream();
        } catch (final Throwable ex) {
          X_Log.error(getClass(), "Could not start command " + command.commands(), ex);
          err.onError(ex);
        }
        stdOut = o;
        stdErr = e;
      } else {
        stdOut = null;
        stdErr = null;
        X_Log.warn(getClass(), "Shell command " + command.commands() + " has already been started.");
      }
      notifyAll();
    }
    if (stdOut != null) {
      onStdOut.onStart();
      onStdErr.onStart();
      final HasLiveness check = new HasLiveness() {
        @Override
        public boolean isAlive() {
          return !finished;
        }
      };
      X_IO.drain(LogLevel.TRACE, stdOut, onStdOut, check);
      X_IO.drain(LogLevel.ERROR, stdErr, onStdErr, check);
    }
    join();
    drainStreams();
    if (status != 0) {
      if (callback instanceof ErrorHandler) {
        ((ErrorHandler)callback).onError(new RuntimeException("Exit status "+status+" for "+command.commands));
      }
      X_Log.error("Exit status",status,"for ",command.commands);
    }
    destroy();
    synchronized (this) {
      notifyAll();
    }
  }

  @Override
  public double birth() {
    return birth.millis();
  }

  @Override
  public ShellCommand parent() {
    return command;
  }

  @Override
  public int pid() {
    return 0;
  }

  @Override
  public int block(final int i, final TimeUnit seconds) {
    final Thread waiting = Thread.currentThread();
    X_Process.newThread(new Runnable() {
      @Override
      public void run() {
        synchronized (ShellSessionDefault.this) {
          try {
            ShellSessionDefault.this.wait(seconds.toMillis(i), 0);
          } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
            waiting.interrupt();
            return;
          }
        }
        if (status == null) {
          waiting.interrupt();
        }
      }
    }).start();
    return join();
  }

  @Override
  public int join() {
    if (status != null) {
      return status;
    }
    try {
      if (process == null) {
        synchronized (this) {
          if (process == null) {
            wait(10000);
          }
        }
        if (status != null) {
          return status;
        }
      }
      if (process == null) {
        X_Log.warn(getClass(),"Process failed to start after "+X_Time.difference(birth));
      } else {
        X_Log.trace(getClass(), "Joining process",process, "after",X_Time.difference(birth), "uptime");
        X_Log.debug(getClass(), "Joining from",new Throwable());
        return (status = process.waitFor());
      }
    } catch (final InterruptedException e) {
      X_Log.info(getClass(), "Interrupted while joining process",process);
      finished = true;
      try {
        if (normalCompletion) {
          return (status = 0);
        }
      status = -1;
      } finally {
        destroy();
      }
      err.onError(e);
    } finally {
      X_Log.trace(getClass(), "Joined process",process,"after", X_Time.difference(birth)," uptime");
      if (status == null) {
        if (process == null) {
          status = ShellCommand.STATUS_FAILED;
        } else {
          status = process.exitValue();
        }
        X_Log.warn(getClass(), "Process did not exit normally; status:",status);
      }
      if (status == 126) {
        // The scripts need chmod +x
        X_Log.warn(getClass(), "The script you are trying to run requires chmod +x\n",command.commands);
        X_Log.info(getClass(), "Attempting to make files executable");
        for (final String command : this.command.commands.forEach()) {
          final File f = new File(command);
          if (f.exists()) {
            if (!f.canExecute()) {
              X_Log.info(getClass(), "Setting file",f,"to be executable.  Result: ", f.setExecutable(true, false));
            }
          }
        }
      }
      finished = true;
      drainStreams();
    }
    return status;
  }

  @Override
  public void destroy() {
    if (status == null) {// don't clobber a real exit status
      status = ShellCommandDefault.STATUS_DESTROYED;
    }
    finished = true;
    // Don't block to notify stdErr and stdOut
    X_Time.runLater(new Runnable() {
      @Override
      public void run() {
        onStdOut.onEnd();
        onStdErr.onEnd();
      }
    });
    finish();
  }

  protected void drainStreams() {
    try {
      X_Log.trace(getClass(), "Process ended; Waiting for stdErr");
      onStdErr.waitToEnd();
      X_Log.trace(getClass(), "Blocking on stdOut");
      onStdOut.waitToEnd();
      X_Log.trace(getClass(), "Done");
    } catch (final InterruptedException e) {
      Thread.interrupted();
      throw X_Debug.rethrow(e);
    }
  }

  protected void finish () {
    boolean shouldRun = false;
    synchronized (once) {
      for (final RemovalHandler clear : clears.forEach()) {
        clear.remove();
      }
      clears.clear();
      shouldRun = status == 0 && once.shouldRun(false);
    }
    if (shouldRun) {
      if (callback != null) {
        callback.onSuccess(this);
      }
    }
  }

  @Override
  public boolean isRunning() {
    return command == null ? false : status == null;
  }

  @Override
  public Future exitStatus() {
    return new FutureCommand() {
      @Override
      protected Integer getValue() {
        return join();
      }
    };
  }

  @Override
  public ShellSessionDefault stdOut(final LineReader stdReader) {
    onStdOut.forwardTo(stdReader);
    return this;
  }

  @Override
  public ShellSessionDefault stdErr(final LineReader errReader) {
    onStdErr.forwardTo(errReader);
    return this;
  }

  @Override
  public boolean stdIn(final String string) {
    if (!isRunning()) {
      throw new IllegalStateException("The command "+command.commands()+" is not running to receive " +
            "your input of "+string);
    }
    final boolean immediate = stdIns.isEmpty();
    stdIns.give(string);
    if (immediate) {
      // maybe have to init
      synchronized (stdIns) {
        // don't want to init twice!
        if (out == null) {
          out = new PipeOut();
          X_Process.newThread(out).start();
        } else {
          out.ping();
        }
      }

    } else {
      if (out == null) {
        X_Log.error(getClass(), "Attempting to send message to closed process, ",string,"will be ignored");
      } else {
        out.ping();
      }
    }
    return immediate;
  }
  class PipeOut implements Runnable{
    private final Pointer blocking = new Pointer(false);// we start out with content to push.
    private long timeout = 50;
    public PipeOut() {
    }
    void ping(){
      // Called when more stdIn shows up.  If we're blocking now, don't bother.
      if (blocking.get()) {
        return;
      }
      synchronized (blocking) {
        blocking.notify();
      }
    }
    OutputStream os;
    @Override
    public void run() {
      X_Log.info(getClass(), "Running process", command.commands);
      try {
      while(isRunning()) {
        if (stdIns.isEmpty() || process == null) {
          X_Log.debug(getClass(), "Waiting until process finishes");
          // go to sleep
          synchronized (blocking) {
            if ((timeout+=50) > 5000) {
              timeout = 2000;
            }
            blocking.set(false);
            try {
              blocking.wait(timeout);
            } catch (final InterruptedException e) {
              Thread.currentThread().interrupt();
              X_Log.error(getClass(), "Shell command $" +
                    command.commands()+" thread interrupted; bailing now.");
              return;
            }
          }
        } else {
          timeout = 50;
          try {
            blocking.set(true);
            final String line = stdIns.take();
            X_Log.trace(getClass(), "Sending command to process stdIn",line);
            try {
              if (os == null){
                os = process.getOutputStream();
              }
              if (os == null){
                X_Log.warn(getClass(), "Null output stream  for "+command.commands);
              }
              else {
                os.write((line+"\n").getBytes());
                os.flush();
              }
            } catch (final IOException e) {
              X_Log.warn(getClass(), "Command ",command.commands()," received IO error sending ",line,"\n", e);
              // TODO perhaps put command back on stack; though recursion sickness would suck
            }
          }finally {
            blocking.set(false);
          }

        }
      }
      if (!stdIns.isEmpty()){
        X_Log.warn(getClass(), "Ended command "+command.commands()+" while stdIn still had data in the buffer:");
        X_Log.warn(stdIns.join(" -- "));
        destroy();
      }
      } finally {
        out = null;
        X_Log.info(getClass(), "Finished process", command.commands);
      }
      status = process.exitValue();
    };
  }

  abstract class FutureCommand implements Future, RemovalHandler {
    @Override
    public T get() throws InterruptedException, ExecutionException {
      join();
      return getValue();
    }

    @Override
    public void remove() {
      if (waiting != null && isRunning()) {
        waiting.interrupt();
        waiting = null;
        clears.remove(this);
      }
    }

    Thread waiting;

    @Override
    public T get(final long timeout, final TimeUnit unit) throws InterruptedException,
        ExecutionException, TimeoutException {
      assert waiting == null || waiting == Thread.currentThread() : "Should not make more than"
          + " one thread wait on a process at once.";
      waiting = Thread.currentThread();
      clears.give(this);
      X_Process.runTimeout(new Runnable() {
        @Override
        public void run() {
          remove();
        }
      }, (int) unit.toMillis(timeout));
      join();
      return getValue();
    }

    protected abstract T getValue();

    @Override
    public boolean cancel(final boolean mayInterruptIfRunning) {
      try {
        destroy();
      } finally {
        if (waiting != null) {
          waiting.interrupt();
          return false;
        }
      }
      return true;
    }

    @Override
    public boolean isCancelled() {
      return ShellCommandDefault.STATUS_DESTROYED.equals(status);
    }

    @Override
    public boolean isDone() {
      return !isRunning();
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy