/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.server.shell;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import org.apache.sshd.common.RuntimeSshException;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.common.util.threads.ThreadUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
/**
* A shell implementation that wraps an instance of {@link InvertedShell}
* as a {@link Command}. This is useful when using external
* processes.
* When starting the shell, this wrapper will also create a thread used
* to pump the streams and also to check if the shell is alive.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class InvertedShellWrapper extends AbstractLoggingBean implements Command, SessionAware {
/**
* Default buffer size for the I/O pumps.
*/
public static final int DEFAULT_BUFFER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
/**
* Value used to control the "busy-wait" sleep time (millis) on
* the pumping loop if nothing was pumped - must be <U>positive</U>
* @see #DEFAULT_PUMP_SLEEP_TIME
*/
public static final String PUMP_SLEEP_TIME = "inverted-shell-wrapper-pump-sleep";
/**
* Default value for {@link #PUMP_SLEEP_TIME} if none set
*/
public static final long DEFAULT_PUMP_SLEEP_TIME = 1L;
private final InvertedShell shell;
private final Executor executor;
private final int bufferSize;
private InputStream in;
private OutputStream out;
private OutputStream err;
private OutputStream shellIn;
private InputStream shellOut;
private InputStream shellErr;
private ExitCallback callback;
private boolean shutdownExecutor;
private long pumpSleepTime = DEFAULT_PUMP_SLEEP_TIME;
/**
* Auto-allocates an {@link Executor} in order to create the streams pump thread
* and uses the {@link #DEFAULT_BUFFER_SIZE}
*
* @param shell The {@link InvertedShell}
* @see #InvertedShellWrapper(InvertedShell, int)
*/
public InvertedShellWrapper(InvertedShell shell) {
this(shell, DEFAULT_BUFFER_SIZE);
}
/**
* Auto-allocates an {@link Executor} in order to create the streams pump thread
*
* @param shell The {@link InvertedShell}
* @param bufferSize Buffer size to use - must be above min. size ({@link Byte#SIZE})
* @see #InvertedShellWrapper(InvertedShell, Executor, boolean, int)
*/
public InvertedShellWrapper(InvertedShell shell, int bufferSize) {
this(shell, null, true, bufferSize);
}
/**
* @param shell The {@link InvertedShell}
* @param executor The {@link Executor} to use in order to create the streams pump thread.
* If {@code null} one is auto-allocated and shutdown when wrapper is {@link #destroy()}-ed.
* @param shutdownExecutor If {@code true} the executor is shut down when shell wrapper is {@link #destroy()}-ed.
* Ignored if executor service auto-allocated
* @param bufferSize Buffer size to use - must be above min. size ({@link Byte#SIZE})
*/
public InvertedShellWrapper(InvertedShell shell, Executor executor, boolean shutdownExecutor, int bufferSize) {
this.shell = Objects.requireNonNull(shell, "No shell");
this.executor = (executor == null) ? ThreadUtils.newSingleThreadExecutor("shell[0x" + Integer.toHexString(shell.hashCode()) + "]") : executor;
ValidateUtils.checkTrue(bufferSize > Byte.SIZE, "Copy buffer size too small: %d", bufferSize);
this.bufferSize = bufferSize;
this.shutdownExecutor = (executor == null) || shutdownExecutor;
}
@Override
public void setInputStream(InputStream in) {
this.in = in;
}
@Override
public void setOutputStream(OutputStream out) {
this.out = out;
}
@Override
public void setErrorStream(OutputStream err) {
this.err = err;
}
@Override
public void setExitCallback(ExitCallback callback) {
this.callback = callback;
}
@Override
public void setSession(ServerSession session) {
pumpSleepTime = session.getLongProperty(PUMP_SLEEP_TIME, DEFAULT_PUMP_SLEEP_TIME);
ValidateUtils.checkTrue(pumpSleepTime > 0L, "Invalid " + PUMP_SLEEP_TIME + ": %d", pumpSleepTime);
shell.setSession(session);
}
@Override
public synchronized void start(Environment env) throws IOException {
// TODO propagate the Environment itself and support signal sending.
shell.start(env);
shellIn = shell.getInputStream();
shellOut = shell.getOutputStream();
shellErr = shell.getErrorStream();
executor.execute(this::pumpStreams);
}
@Override
public synchronized void destroy() throws Exception {
Throwable err = null;
try {
shell.destroy();
} catch (Throwable e) {
log.warn("destroy({}) failed ({}) to destroy shell: {}",
this, e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) {
log.debug("destroy(" + this + ") shell destruction failure details", e);
}
err = GenericUtils.accumulateException(err, e);
}
if (shutdownExecutor && (executor instanceof ExecutorService)) {
try {
((ExecutorService) executor).shutdown();
} catch (Exception e) {
log.warn("destroy({}) failed ({}) to shut down executor: {}",
this, e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) {
log.debug("destroy(" + this + ") executor shutdown failure details", e);
}
err = GenericUtils.accumulateException(err, e);
}
}
if (err != null) {
if (err instanceof Exception) {
throw (Exception) err;
} else {
throw new RuntimeSshException(err);
}
}
}
protected void pumpStreams() {
try {
// Use a single thread to correctly sequence the output and error streams.
// If any bytes are available from the output stream, send them first, then
// check the error stream, or wait until more data is available.
for (byte[] buffer = new byte[bufferSize];;) {
if (pumpStream(in, shellIn, buffer)) {
continue;
}
if (pumpStream(shellOut, out, buffer)) {
continue;
}
if (pumpStream(shellErr, err, buffer)) {
continue;
}
/*
* Make sure we exhausted all data - the shell might be dead
* but some data may still be in transit via pumping
*/
if ((!shell.isAlive()) && (in.available() <= 0) && (shellOut.available() <= 0) && (shellErr.available() <= 0)) {
callback.onExit(shell.exitValue());
return;
}
// Sleep a bit. This is not very good, as it consumes CPU, but the
// input streams are not selectable for nio, and any other blocking
// method would consume at least two threads
Thread.sleep(pumpSleepTime);
}
} catch (Throwable e) {
try {
shell.destroy();
} catch (Throwable err) {
log.warn("pumpStreams({}) failed ({}) to destroy shell: {}",
this, e.getClass().getSimpleName(), e.getMessage());
if (log.isDebugEnabled()) {
log.debug("pumpStreams(" + this + ") shell destruction failure details", err);
}
}
int exitValue = shell.exitValue();
if (log.isDebugEnabled()) {
log.debug(e.getClass().getSimpleName() + " while pumping the streams (exit=" + exitValue + "): " + e.getMessage(), e);
}
callback.onExit(exitValue, e.getClass().getSimpleName());
}
}
protected boolean pumpStream(InputStream in, OutputStream out, byte[] buffer) throws IOException {
int available = in.available();
if (available > 0) {
int len = in.read(buffer);
if (len > 0) {
out.write(buffer, 0, len);
out.flush();
return true;
}
} else if (available == -1) {
out.close();
}
return false;
}
@Override
public String toString() {
return getClass().getSimpleName() + ": " + String.valueOf(shell);
}
}