Timeout when ssh command is executed from my Java program
I have a Java program that runs in Tomcat that needs to execute a few ssh and scp commands as well as a few simple commands like ls on the local machine. I am having problems with my current approach in that I get a timeout every time I execute the ssh command. I can run the ssh command on the command line without issue, but when executed from my Java program, it times out. I am running a web application where ssh commands are executed as root (i.e. I am running Tomcat as root user, with my web application code deployed as a WAR file) and as far as I know, the correct certification keys are set up as local and remote machines, at least I can execute ssh commands on the command line as root without having to enter a username or password.I am not specifying the username or password in the ssh command that my Java program is executing as I am assuming that I can run the same ssh command in my Java code as on the command line, but perhaps this is a false assumption and is the cause of my problem.
The Java code I developed to execute the command looks like this:
public class ProcessUtility
{
static Log log = LogFactory.getLog(ProcessUtility.class);
/**
* Thread class to be used as a worker
*/
private static class Worker
extends Thread
{
private final Process process;
private volatile Integer exitValue;
Worker(final Process process)
{
this.process = process;
}
public Integer getExitValue()
{
return exitValue;
}
@Override
public void run()
{
try
{
exitValue = process.waitFor();
}
catch (InterruptedException ignore)
{
return;
}
}
}
/**
* Executes a command.
*
* @param args command + arguments
*/
public static void execCommand(final String[] args)
{
try
{
Runtime.getRuntime().exec(args);
}
catch (IOException e)
{
// swallow it
}
}
/**
* Executes a command.
*
* @param command
* @param printOutput
* @param printError
* @param timeOut
* @return
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
public static int executeCommand(final String command,
final boolean printOutput,
final boolean printError,
final long timeOut)
{
return executeCommandWithWorker(command, printOutput, printError, timeOut);
}
/**
* Executes a command and returns its output or error stream.
*
* @param command
* @return the command resulting output or error stream
*/
public static String executeCommandReceiveOutput(final String command)
{
try
{
// create the process which will run the command
Runtime runtime = Runtime.getRuntime();
final Process process = runtime.exec(command);
try
{
// consume the error and output streams
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT", false);
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR", false);
outputGobbler.start();
errorGobbler.start();
// execute the command
if (process.waitFor() == 0)
{
return outputGobbler.getInput();
}
return errorGobbler.getInput();
}
finally
{
process.destroy();
}
}
catch (InterruptedException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an unexpected interruption.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
catch (IOException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an IO error.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
}
/**
* Executes a command.
*
* @param command
* @param printOutput
* @param printError
* @param timeOut
* @return
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
@SuppressWarnings("unused")
private static int executeCommandWithExecutors(final String command,
final boolean printOutput,
final boolean printError,
final long timeOut)
{
// validate the system and command line and get a system-appropriate command line
String massagedCommand = validateSystemAndMassageCommand(command);
try
{
// create the process which will run the command
Runtime runtime = Runtime.getRuntime();
final Process process = runtime.exec(massagedCommand);
// consume and display the error and output streams
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT", printOutput);
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR", printError);
outputGobbler.start();
errorGobbler.start();
// create a Callable for the command Process which can be called by an Executor
Callable<Integer> call = new Callable<Integer>()
{
public Integer call()
throws Exception
{
process.waitFor();
return process.exitValue();
}
};
// submit the command call via an Executor and get the result from a Future
ExecutorService executorService = Executors.newSingleThreadExecutor();
try
{
Future<Integer> futureResultOfCall = executorService.submit(call);
int exitValue = futureResultOfCall.get(timeOut, TimeUnit.MILLISECONDS);
return exitValue;
}
catch (TimeoutException ex)
{
String errorMessage = "The command [" + command + "] timed out.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
catch (ExecutionException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an execution error.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
finally
{
executorService.shutdown();
process.destroy();
}
}
catch (InterruptedException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an unexpected interruption.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
catch (IOException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an IO error.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
}
/**
* Executes a command.
*
* @param command
* @param printOutput
* @param printError
* @param timeOut
* @return
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
private static int executeCommandWithWorker(final String command,
final boolean printOutput,
final boolean printError,
final long timeOut)
{
// validate the system and command line and get a system-appropriate command line
String massagedCommand = validateSystemAndMassageCommand(command);
try
{
// create the process which will run the command
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(massagedCommand);
// consume and display the error and output streams
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT", printOutput);
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR", printError);
outputGobbler.start();
errorGobbler.start();
// create and start a Worker thread which this thread will join for the timeout period
Worker worker = new Worker(process);
worker.start();
try
{
worker.join(timeOut);
Integer exitValue = worker.getExitValue();
if (exitValue != null)
{
// the worker thread completed within the timeout period
// stop the output and error stream gobblers
outputGobbler.stopGobbling();
errorGobbler.stopGobbling();
return exitValue;
}
// if we get this far then we never got an exit value from the worker thread as a result of a timeout
String errorMessage = "The command [" + command + "] timed out.";
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
catch (InterruptedException ex)
{
worker.interrupt();
Thread.currentThread().interrupt();
throw ex;
}
finally
{
process.destroy();
}
}
catch (InterruptedException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an unexpected interruption.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
catch (IOException ex)
{
String errorMessage = "The command [" + command + "] did not complete due to an IO error.";
log.error(errorMessage, ex);
throw new RuntimeException(errorMessage, ex);
}
}
/**
* Validates that the system is running a supported OS and returns a system-appropriate command line.
*
* @param originalCommand
* @return
*/
private static String validateSystemAndMassageCommand(final String originalCommand)
{
// make sure that we have a command
if (originalCommand.isEmpty() || (originalCommand.length() < 1))
{
String errorMessage = "Missing or empty command line parameter.";
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
// make sure that we are running on a supported system, and if so set the command line appropriately
String massagedCommand;
String osName = System.getProperty("os.name");
if (osName.equals("Windows XP"))
{
massagedCommand = "cmd.exe /C " + originalCommand;
}
else if (osName.equals("Solaris") || osName.equals("SunOS") || osName.equals("Linux"))
{
massagedCommand = originalCommand;
}
else
{
String errorMessage = "Unable to run on this system which is not Solaris, Linux, or Windows XP (actual OS type: \'" +
osName + "\').";
log.error(errorMessage);
throw new RuntimeException(errorMessage);
}
return massagedCommand;
}
}
class StreamGobbler
extends Thread
{
static private Log log = LogFactory.getLog(StreamGobbler.class);
private InputStream inputStream;
private String streamType;
private boolean displayStreamOutput;
private final StringBuffer inputBuffer = new StringBuffer();
private boolean keepGobbling = true;
/**
* Constructor.
*
* @param inputStream the InputStream to be consumed
* @param streamType the stream type (should be OUTPUT or ERROR)
* @param displayStreamOutput whether or not to display the output of the stream being consumed
*/
StreamGobbler(final InputStream inputStream,
final String streamType,
final boolean displayStreamOutput)
{
this.inputStream = inputStream;
this.streamType = streamType;
this.displayStreamOutput = displayStreamOutput;
}
/**
* Returns the output stream of the
*
* @return
*/
public String getInput()
{
return inputBuffer.toString();
}
/**
* Consumes the output from the input stream and displays the lines consumed if configured to do so.
*/
@Override
public void run()
{
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
try
{
String line = null;
while (keepGobbling && inputStreamReader.ready() && ((line = bufferedReader.readLine()) != null))
{
inputBuffer.append(line);
if (displayStreamOutput)
{
System.out.println(streamType + ">" + line);
}
}
}
catch (IOException ex)
{
log.error("Failed to successfully consume and display the input stream of type " + streamType + ".", ex);
ex.printStackTrace();
}
finally
{
try
{
bufferedReader.close();
inputStreamReader.close();
}
catch (IOException e)
{
// swallow it
}
}
}
public void stopGobbling()
{
keepGobbling = false;
}
}
I am executing ssh commands in my Java program like this:
ProcessUtility.executeCommand ("ssh" + physicalHostIpAddress + "virsh list \\ grep" + newDomUName, false, false, 3600000)
Can anyone see what I am doing wrong? By the way, the above code was developed using this article as a guide: http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html . I am not very good at concurrent programming, so maybe I am doing something shameless - feel free to point out if this is the case.
Thanks a lot for any suggestions, ideas, etc.
- James
Update . I have now consulted some helpful people who answered my original question and wrote a class that provides methods for making ssh and scp calls, implemented using two Java ssh libraries jsch (jsch-0.1.31) and sshtools (j2ssh- core-0.2.9). However, neither of these implementations work in that they both fail in the connect phase before I can authenticate. I expect I am having some kind of configuration problem on the servers where I am running the codes, although this is not obvious since I can execute ssh and scp commands on these servers without issue when I issue ssh or scp commands to the command line ... The Solaris servers I am testing my code on show the following as a result of ssh -V:
Sun_SSH_1.3, SSH 1.5 / 2.0 protocols, OpenSSL 0x0090801f
Below is the Java code I wrote for this purpose - if anyone can see what I am doing wrong at the Java code level, then please let me know, and if so, thanks for the help for the help.
public class SecureCommandUtility
{
static Log log = LogFactory.getLog(SecureCommandUtility.class);
/**
* Performs a secure copy of a single file (using scp).
*
* @param localFilePathName
* @param username
* @param password
* @param remoteHost
* @param remoteFilePathName
* @param timeout
*/
public static void secureCopySingleFile(final String localFilePathName,
final String username,
final String password,
final String remoteHost,
final String remoteFilePathName,
final int timeout)
{
// basic validation of the parameters
if ((localFilePathName == null) || localFilePathName.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied local file path name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((username == null) || username.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied user name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((password == null) || password.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied password parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteHost == null) || remoteHost.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied remote host parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteFilePathName == null) || remoteFilePathName.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied remote file path name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if (timeout < 1000)
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied timeout parameter is less than one second.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
//secureCopySingleFileJSch(localFilePathName, username, password, remoteHost, remoteFilePathName);
secureCopySingleFileJ2Ssh(localFilePathName, username, password, remoteHost, remoteFilePathName, timeout);
}
/**
*
* @param user
* @param password
* @param remoteHost
* @param command
* @return exit status of the command
*/
public static int secureShellCommand(final String user,
final String password,
final String remoteHost,
final String command,
final int timeout)
{
// basic validation of the parameters
if ((user == null) || user.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied user name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((password == null) || password.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied password parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteHost == null) || remoteHost.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied remote host parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((command == null) || command.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command: the supplied command parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if (timeout < 1000)
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied timeout parameter is less than one second.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
//return secureShellCommandJsch(user, password, remoteHost, command, timeout);
return secureShellCommandJ2Ssh(user, password, remoteHost, command, timeout);
}
/**
* Performs a secure copy of a single file (using scp).
*
* @param localFilePathName
* @param username
* @param password
* @param remoteHost
* @param remoteFilePathName
* @param timeout
*/
private static void secureCopySingleFileJ2Ssh(final String localFilePathName,
final String username,
final String password,
final String remoteHost,
final String remoteFilePathName,
final int timeout)
{
SshClient sshClient = null;
try
{
// create and connect client
sshClient = new SshClient();
sshClient.setSocketTimeout(timeout);
sshClient.connect(remoteHost, 22, new IgnoreHostKeyVerification());
// perform password-based authentication
PasswordAuthenticationClient passwordAuthenticationClient = new PasswordAuthenticationClient();
passwordAuthenticationClient.setUsername(username);
passwordAuthenticationClient.setPassword(password);
if (sshClient.authenticate(passwordAuthenticationClient) != AuthenticationProtocolState.COMPLETE)
{
// log the error and throw an exception
String errorMessage = "Failed to copy \'" + localFilePathName + "\' to \'" + remoteHost + ":" +
remoteFilePathName + "\' -- failed to authenticate using username/password \'" +
username + "\'/\'" + password + "\'.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
// perform the copy
sshClient.openScpClient().put(localFilePathName, remoteFilePathName, false);
}
catch (Exception ex)
{
// log the error and throw an exception
String errorMessage = "Failed to copy \'" + localFilePathName + "\' to \'" + remoteHost + ":" +
remoteFilePathName + "\'.";
log.error(errorMessage, ex);
throw new LifecycleException(errorMessage, ex);
}
finally
{
if ((sshClient != null) && sshClient.isConnected())
{
sshClient.disconnect();
}
}
}
/**
* Performs a secure copy of a single file (using scp).
*
* @param localFilePathName
* @param user
* @param password
* @param remoteHost
* @param remoteFilePathName
*/
@SuppressWarnings("unused")
private static void secureCopySingleFileJSch(final String localFilePathName,
final String user,
final String password,
final String remoteHost,
final String remoteFilePathName)
{
Session session = null;
Channel channel = null;
FileInputStream fileInputStream = null;
try
{
// create and connect Jsch session
JSch jsch = new JSch();
session = jsch.getSession(user, remoteHost, 22);
session.setPassword(password);
session.connect();
// exec 'scp -p -t remoteFilePathName' remotely
String command = "scp -p -t " + remoteFilePathName;
channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(command);
// get the I/O streams for the remote scp
OutputStream outputStream = channel.getOutputStream();
InputStream inputStream = channel.getInputStream();
// connect the channel
channel.connect();
int ackCheck = checkAck(inputStream);
if (checkAck(inputStream) != 0)
{
// log the error and throw an exception
String errorMessage = "The scp command failed -- input stream ACK check failed with the following result: " +
ackCheck;
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
// send "C0644 filesize filename", where filename should not include '/'
long filesize = (new File(localFilePathName)).length();
command = "C0644 " + filesize + " ";
if (localFilePathName.lastIndexOf('/') > 0)
{
command += localFilePathName.substring(localFilePathName.lastInde
source to share
I gave up my attempts to use the ssh libraries for this and instead used the bare bones Runtime.exec () method to issue ssh and scp commands. Here's the code I'm currently using that works well:
public static void executeSecureCommand(final String user,
final String remoteHost,
final String command)
{
// basic validation of the parameters
if ((user == null) || user.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied user name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteHost == null) || remoteHost.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command \'" + command +
"\': the supplied remote host parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((command == null) || command.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the ssh command: the supplied command parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
// create and execute a corresponding ssh command
String sshCommand = "ssh " + user + "@" + remoteHost + " " + command;
try
{
executeShellCommand(sshCommand);
}
catch (Exception ex)
{
// log the error and throw an exception
String errorMessage = "Error executing the secure shell command \'" + sshCommand + "\'";
log.error(errorMessage, ex);
throw new LifecycleException(errorMessage, ex);
}
}
public static void executeSecureFileCopy(final String localFilePathName,
final String user,
final String remoteHost,
final String remoteFilePathName)
{
// basic validation of the parameters
if ((localFilePathName == null) || localFilePathName.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied local file path name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((user == null) || user.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied user name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteHost == null) || remoteHost.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied remote host parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
if ((remoteFilePathName == null) || remoteFilePathName.isEmpty())
{
// log the error and throw an exception
String errorMessage = "Error executing the secure copy -- the supplied remote file path name parameter is null or empty.";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
try
{
// create an scp command we'll use to perform the secure file copy
String scpCommand = "scp -B -C -q " + localFilePathName + " " + user + "@" + remoteHost + ":" +
remoteFilePathName;
// execute the scp command
executeShellCommand(scpCommand);
}
catch (Exception ex)
{
// log the error and throw an exception
String errorMessage = "Failed to copy local file \'" + localFilePathName + "\' to remote host:file \'" +
remoteHost + ":" + remoteFilePathName + "\'.";
log.error(errorMessage, ex);
throw new LifecycleException(errorMessage, ex);
}
}
public static void executeShellCommand(final String command)
{
try
{
// create and execute a runtime process which runs the command
Process process = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", command });
// gobble the input stream
InputStream processInputStream = process.getInputStream();
BufferedReader processInputStreamReader = new BufferedReader(new InputStreamReader(processInputStream));
String inputStreamLine = processInputStreamReader.readLine();
while (inputStreamLine != null)
{
inputStreamLine = processInputStreamReader.readLine();
}
// capture the error stream
InputStream processErrorStream = process.getErrorStream();
BufferedReader processErrorStreamReader = new BufferedReader(new InputStreamReader(processErrorStream));
String errorStreamLine = processErrorStreamReader.readLine();
StringBuffer errorBuffer = new StringBuffer();
while (errorStreamLine != null)
{
errorBuffer.append(errorStreamLine);
errorStreamLine = processErrorStreamReader.readLine();
}
// close the streams
processInputStream.close();
processErrorStream.close();
// wait for the process to finish and return the exit code
process.waitFor();
if (process.exitValue() != 0)
{
// log the error and throw an exception
String errorMessage = "Failed to execute the shell command \'" + command + "\' -- Error: \'" +
errorBuffer.toString() + "\'";
log.error(errorMessage);
throw new LifecycleException(errorMessage);
}
}
catch (Exception ex)
{
// log the error and throw an exception
String errorMessage = "Failed to execute the shell command \'" + command + "\'.";
log.error(errorMessage, ex);
throw new LifecycleException(errorMessage, ex);
}
}
If anyone sees problems with this code, i.e. there are possible errors that I am not catching, etc., please point them out. My original code was quite complex and convoluted to address all of the issues mentioned in this article ( http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html ) but only recently was he pointed out to me that this article is almost 8 years old and that many of the problems it warns about no longer apply in current Java releases. So I'll go back to a basic solution that uses Runtime.exec () and everything looks good.
Thanks to everyone who tried to help me solve this problem.
source to share
You might be able to handle timeouts and errors if you are using jsch instead of trying to disconnect. Examples of use here .
In most cases, the errors you get from JSch will be much more helpful in diagnosing if it is a connection issue or a logic issue.
Also, not sure why you need to use ls
this way. You can get an array of files this way
File dir = new File("directory");
String[] children = dir.list();
No parsing of the output ls
. It will be much more portable.
source to share
This doesn't answer your question, but you can try the sshtools lib which does a great job with ssh.
Example:
SshClient ssh = = new SshClient();
ssh.connect(server, port, new IgnoreHostKeyVerification());
PasswordAuthenticationClient pwd = new PasswordAuthenticationClient();
pwd.setUsername(uid);
pwd.setPassword(password);
int result = ssh.authenticate(pwd);
if (result == AuthenticationProtocolState.COMPLETE) {
SessionChannelClient session = ssh.openSessionChannel();
session.executeCommand("sh test.sh");
}
source to share