xapi.shell.impl.ShellSessionDefault Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xapi-dev Show documentation
Show all versions of xapi-dev Show documentation
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();
}
}
}