views:

891

answers:

4

I have a Java program which runs in Tomcat and which needs to execute several ssh and scp commands, as well as a few simple commands such as ls on the local machine. I am having trouble with my current approach in that I am getting a time out every time I execute an ssh command. I can run the ssh command on the command line with no problems, but when it's executed from my Java program it times out. I am running the web application in which the ssh commands are executed as root (i.e. I start 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 configured on both the local and remote machines, at least I can perform the ssh commands at command line as root without having to enter a user name or password. I do not specify the user name or password in the ssh command that is being executed by my Java program since I assume that I can run the same ssh command in my Java code as I can execute at command line, but maybe this is a false assumption and is the cause of my trouble.

The Java code I have developed to perform command execution is as follows:

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's 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's 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's 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 execute the ssh commands in my Java program like so:

ProcessUtility.executeCommand("ssh " + physicalHostIpAddress + " virsh list \| grep " + newDomUName, false, false, 3600000)

Can anyone see what I am doing wrong? BTW 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 expert with concurrent programming so perhaps I am doing something boneheaded -- feel free to point it out if so.

Thanks a lot in advance for any suggestions, ideas, etc.

--James

Update: I have now taken the advice of the helpful folks who responded to my original question and have written a class which 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 are working in that they are both failing at the connect step, before I even get the chance to perform authentication. I expect that I am facing some sort of configuration issue on the servers where I am running the codes, although this is not obvious since I can perform ssh and scp commands on these servers with no problems when I issue the ssh or scp commands on the command line. The Solaris servers that I am testing my code on are showing the following as a result of ssh -V:

Sun_SSH_1.3, SSH protocols 1.5/2.0, OpenSSL 0x0090801f

Below is the Java code I have written 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 a lot in advance for your 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
+2  A: 

You might have better luck dealing with timeout and errors if you use jsch rather than try to shell out. There are usage examples here.

In most cases, the errors you'll get from JSch will be much more helpful in diagnosing if this is a connection issue or a logic problem.

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();

Without having to parse the output of ls. This will be much more portable.

sal
Thanks for your response, and for the good suggestions.
James Adams
I've also had good experience with jsch
Duncan McGregor
I get the following exception when making the connection to the host:JSchException: Algorithm negotiation failThis happens at the connect step: JSch jsch = new JSch(); session = jsch.getSession(user, remoteHost, 22); session.setPassword(password); session.connect();Can anyone comment as to what may be the cause of this error? (BTW I am also getting what I assume to be is a similar error at the connect step when using j2ssh, IOException: socket is EOF)
James Adams
what version of sshd/openssl is running on the server? Algorithm negotiation failure is likely due configuration error or a very old server stack. On the start of the connection, the client and server list supported encryption Algorithm and decide on which to use. There must be the same version on both client and server.
sal
I am using jsch version 0.1.31 and j2ssh-core 0.2.9. The two Solaris machines that I am running my codes on show the following as a result of a 'ssh -V' command:Sun_SSH_1.3, SSH protocols 1.5/2.0, OpenSSL 0x0090801fHow can I determine if this version of OpenSSL is compatible with either of these ssh Java libraries?
James Adams
are you able to ssh into the machine via putty or some other protocol? You might want to check if sshd is using ssh1 or ssh2. You can check this by doing a sshd -d and check the debug log for protocol version. Make sure the server is defaulting to ssh2
sal
A: 

This doesn't answer your question, but you can try sshtools lib that handles ssh nicely.

Some 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");
}
serg
I get an IOException when making the connection using this library:java.io.IOException: The socket is EOFThis happens at the connect() call: SshClient sshClient = new SshClient(); sshClient.setSocketTimeout(timeout); sshClient.connect(remoteHost, 22);Can anyone comment as to what this exception indicates?
James Adams
I added quick example. Again not sure how it is related to your problem, sorry.
serg
Thanks serg555. Please see my update to my original question which lists the code I am using to make the ssh command. My code is exactly as you have demonstrated above except I originally wasn't using an IgnoreHostKeyVerification in my connect step, but now I have added that and still it doesn't work, failing at the connect() call with this error:<br><br>SEVERE: The Transport Protocol thread failed<br>java.io.IOException: The socket is EOF
James Adams
Sorry, I have no idea. Maybe try asking on serverfault.com, they are server side specialists over there maybe they will have some clue...
serg
A: 

My guess: What user is tomcat running as? And how does SSH do key-management? I suspect the user that it's running as doesn't have proper keys configured.

Noon Silk
Thanks, silky. I run Tomcat as root. I don't think it really matters though since in my code I get a user and password and use those to authenticate, and I never even get to that step since the code is throwing an exception at the connect() step in both implementations (using sshtools/j2ssh and jsch), which occurs before the authentication step (see the update of my original message for the two implementations I've come up with using sshtools/j2ssh and jsch).
James Adams
A: 

I have abandoned my attempts at using ssh libraries for this and have instead used a bare-bones Runtime.exec() approach for issuing ssh and scp commands. Here's the code that I'm now using which is working 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 issues with this code, i.e. possible errors I'm not catching, etc. then please point them out. My original code was quite complicated and convoluted in order to address all of the issues pointed out in this article (http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html) but only recently was it pointed out to me that that article is almost 8 years old and that many of the issues it warns about are no longer applicable in current releases of Java. So I'm back to a basic solution which uses Runtime.exec() and all seems well.

Thanks again to everyone who tried to help me solve this problem.

James Adams