Recently I’ve had the need from java to run a job on a remote server that can take a variable amount of time (anywhere from a few seconds to a few hours) and needed to detect when it is complete and get a file it generated when completed. This was a little challanging at first, as I had never used ssh from java, but I knew a little searching would yield results.

After a little searching, I found Java Secure Channel (JSch), a handy library with a BSD license and used in Ant and eclipse. The only sad thing is that there is zero documentation outside of a few examples, and my knowledge of streams (specifically PipedInputStream) was sorely behind as I hadn’t used them directly, since, well, college.

A little confusion abounded for awhile … how the heck am I supposed to detect the end of data coming over a pipe!? I mean, you can detect when it closes, but in my case I want to stay connected and issue more commands and close when I please, which means I cannot simply detect when the pipe is closed.

I struggled for a bit, then decided … I’ll just append ” && echo ” and read until I see that token, escape, remove the token, and return the data. So I wrote a small and dirty SshClient class that would allow me to send and recieve responses. The send method simply added the appropriate marker and saves the command so that it knows to strip it from the response:

public void send(String command) throws IOException {
		command += " && echo \"" + TERMINATOR + "\"\n";
		toServer.write(command.getBytes());
		lastCommand = new String(command);
	}

Getting the server response was a bit tricky. Since the command shows up when reading from the server, I need to detect the 2nd time it shows up and break out of the loop on that. After a little time I hacked up the following:

public String getServerResponse() throws IOException, InterruptedException {
		StringBuilder builder = new StringBuilder();
		int count = 0;
		String line = "";
		for (int i = 0; true; i++) {

			line = fromServer.readLine();
			builder.append(line).append("\n");
			if (line.contains(TERMINATOR) && (++count > 1)) {

				break;
			}

		}
		String result = builder.toString();

		int beginIndex = result.indexOf(TERMINATOR+"\"") + ((TERMINATOR+"\"").length());
		result = result.substring(beginIndex);
		return result.replaceAll(escape(TERMINATOR), "").trim();
	}

This makes so many assumptions, but I guess that as I keep working on it I can find a better way. Anyway, below is the final result of the SshClient class I came up with:

package jamesstuff.ssh;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.regex.Pattern;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;

public class SshClient {
	BufferedReader fromServer;
	Pattern alphaNumeric = Pattern.compile("([^a-zA-z0-9])");
	OutputStream toServer;
	String lastCommand = "";
	Channel channel;

	private static final String TERMINATOR = "zDonez";

	public void connect(String username, String password, String host, int port)
			throws JSchException, IOException {
		JSch shell = new JSch();
		Session session = shell.getSession(username, host, port);
		MyUserInfo ui = new MyUserInfo();
		ui.setPassword(password);
		session.setUserInfo(ui);
		session.connect();

		channel = session.openChannel("shell");
		fromServer = new BufferedReader(new InputStreamReader(channel.getInputStream()));
		toServer = channel.getOutputStream();
		channel.connect();
		if(isConnected()){
			send("echo \"\"");
		}
	}

	public boolean isConnected() {
		// TODO Auto-generated method stub
		return (channel != null && channel.isConnected());
	}

	public void disconnect() {
		if (isConnected()) {
			channel.disconnect();
		}

	}

	public void send(String command) throws IOException {
		command += "; echo \"" + TERMINATOR + "\"\n";
		toServer.write(command.getBytes());
		lastCommand = new String(command);
	}

	public String getServerResponse() throws IOException, InterruptedException {
		StringBuilder builder = new StringBuilder();
		int count = 0;
		String line = "";
		for (int i = 0; true; i++) {

			line = fromServer.readLine();
			builder.append(line).append("\n");
			if (line.contains(TERMINATOR) && (++count > 1)) {

				break;
			}

		}
		String result = builder.toString();

		int beginIndex = result.indexOf(TERMINATOR+"\"") + ((TERMINATOR+"\"").length());
		result = result.substring(beginIndex);
		return result.replaceAll(escape(TERMINATOR), "").trim();
	}
	private String escape(String subjectString){
		return alphaNumeric.matcher(subjectString).replaceAll("\\\\$1");
	}
	private static class MyUserInfo implements UserInfo {
		private String password;

		public void setPassword(String password) {
			this.password = password;

		}

		public String getPassphrase() {
			// TODO Auto-generated method stub
			return null;
		}

		public String getPassword() {
			// TODO Auto-generated method stub
			return password;
		}

		public boolean promptPassword(String arg0) {
			// TODO Auto-generated method stub
			return true;
		}

		public boolean promptPassphrase(String arg0) {
			// TODO Auto-generated method stub
			return true;
		}

		public boolean promptYesNo(String arg0) {
			// TODO Auto-generated method stub
			return true;
		}

		public void showMessage(String arg0) {
			// TODO Auto-generated method stub
			System.out.println(arg0);
		}

	}

}

If you're new here, you may want to subscribe to my RSS feed. Thanks for visiting!