Java and SSH looking to execute multiple commands

2019-08-14 19:27发布

问题:

“my-thoughts” Provided the code below. but it’s not working 100%. 1) if I have more than one command, the program won’t run. 2) one command in the command file, the script won’t execute properly. for example with “ls” the log file will have the following "Last login: Tue Jun 9 14:30:11 2015 from localhost lsmyhost:~ myaccount$ ls "

JSSH class:

public class JSSH {
    private static final String user = "UID"; 
    private static final String password = "pass";
    public static void main(String args[]) throws JSchException,
        InterruptedException, IOException {

        JSSH jssh = new JSSH();
        JSch jsch = new JSch();
        for (String host : jssh.listOfhost()) {
            Session session = jsch.getSession(user, host, 22);
            session.setPassword(password);
            session.setConfig(getProperties());
            session.connect(10 * 1000);
            Channel channel = session.openChannel("shell");

            for(String command : jssh.listOfCommand()) {
                channel.setInputStream(new ByteArrayInputStream(command.getBytes()));
                channel.setOutputStream(new FileOutputStream(new File(OUTPUT_FILE)));
                channel.connect(15 * 1000);
                TimeUnit.SECONDS.sleep(3);
            }

            channel.disconnect();
            session.disconnect();
        }
    }

    private static Properties getProperties() {
        Properties properties = new Properties();
        properties.put("StrictHostKeyChecking", "no");
        return properties;
    }

    private List<String> listOfCommand() throws IOException {
        return new LineBuilder("command_file.txt").build();
    }

    private List<String> listOfhost() throws IOException {
        return new LineBuilder("host_file.txt").build();
    }
}  

LineBuilder Class:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class LineBuilder {

    private String fileName;

    public LineBuilder(String fileName) {
        this.fileName = fileName;
    }

    public List<String> build() throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)));
        List<String> lines = new ArrayList<String>();
        String line = null;
        try {
            while((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch(IOException e) {
            throw e;
        } finally {
            reader.close();
        }
        return lines;
    }
}

回答1:

Every time you call new FileOutputStream, you are overwriting the file. So you definitely should not be calling it for every command, as that would result in a file which contains only the output of the last command executed.

A FileOutputStream should almost always be wrapped in a BufferedOutputStream, as it often improves performance. This has been true for a very long time, since before Java even existed.

I'm guessing you want the output from all commands on all hosts to be in that log file. If that's the case, you want to create the OutputStream once, outside of any loops.

You probably should not create a new connection for every command. Move these two lines outside of your inner for-loop, so they're right after the creation of the Channel:

channel.setOutputStream(outputStream);
channel.connect(15 * 1000);

Note that the documentation for setOutputStream states that you should be calling it before you call Channel.connect(). Also, since we're using a single OutputStream for all commands from all hosts, you want to pass true as the second argument, so JSch won't close that OutputStream.

In fact, the documentation for setInputStream says the same thing: It has to be called before calling connect().

So how does one manage that? You'll need to create a background Thread that "feeds" lines to the InputStream through a pipe. This is accomplished with PipedInputStream and PipedOutputStream, which allow one thread to read from a stream fed by another thread.

So, the revised version might look like this:

JSSH jssh = new JSSH();
final String[] commands = jssh.listOfCommand();

// Local class for feeding commands to a pipe in a background thread.
class CommandSender
implements Runnable {
    private final OutputStream target;

    IOException exception;

    CommandSender(OutputStream target) {
        this.target = Objects.requireNonNull(target);
    }

    @Override
    public void run() {
        try {
            for (String command : commands) {
                target.write(command.getBytes());
                target.write(10);   // newline
                TimeUnit.SECONDS.sleep(3);
            }
        } catch (IOException e) {
            exception = e;
        } catch (InterruptedException e) {
            System.out.println("Interrupted, exiting prematurely.");
        }
    }
}

try (OutputStream log = new BufferedOutputStream(
    new FileOutputStream(OUTPUT_FILE))) {

    JSch jsch = new JSch();

    for (String host : jssh.listOfhost()) {
        Session session = jsch.getSession(user, host, 22);
        session.setPassword(password);
        session.setConfig(getProperties());
        session.connect(10 * 1000);

        Channel channel = session.openChannel("shell");
        channel.setOutputStream(log, true);

        try (PipedInputStream commandSource = new PipedInputStream();
             OutputStream commandSink = new PipedOutputStream(commandSource)) {

            CommandSender sender = new CommandSender(commandSink);
            Thread sendThread = new Thread(sender);
            sendThread.start();

            channel.setInputStream(commandSource);
            channel.connect(15 * 1000);

            sendThread.join();
            if (sender.exception != null) {
                throw sender.exception;
            }
        }

        channel.disconnect();
        session.disconnect();
    }
}

A word of caution: Calling String.getBytes() converts the String's characters to bytes using the platform's default charset. In Windows, this is usually UTF16-LE (two bytes per character), so if you are making ssh connections from Windows to Unix or Linux machines, which typically expect characters encoded in UTF-8, you may get a lot of failures. The easy solution is to specify an explicit charset, assuming you know the charset used by the target machines:

command.getBytes(StandardCharsets.UTF_8)


回答2:

If you are using the shell channel and sending raw command data (i.e., a ByteArrayInputStream in your case) you need to make sure to include a newline character \n after each command. Otherwise, the command will not execute, but just sit and wait for more input.

See my response to this question for a similar example.



标签: java ssh