In Executing Commands we demonstrated how to execute commands using the SSH "exec" mechanism. This API allows you to execute a single command line on the server and return the exit code and output of the command(s).
When programming with the API developers are often trying to mimic user input and in practice, the mechanism we have just discussed is not widely used even by Administrations and end-users and so it's difficult to match the behavior of a user by executing commands in that way. The default behavior of most SSH clients is to log users directly into a shell allowing them to execute command after command, interactively, with the environment maintaining state between. And it's this usage that most developers want to try to replicate with the API.
This presents the developer with a problem. We can use the ShellTask to start a shell, but how do we separate the output of each command executed? When do we know the shell is at the prompt so we can execute a command?
ssh.runTask(new ShellTask(ssh) {
@Override protected void onOpenSession(SessionChannelNG session)
throws IOException, SshException, ShellTimeoutException {
try {
int b;
byte[] buf = new byte[1024];
while((b = session.getInputStream().read(buf)) > -1) {
// Use the buffer as needed
}
} catch (IOException e) { }
}
});
There is no in-built mechanism to separate the output of each command executed. It's just all part of the same stream. To a user that understands this its easy for them to use, but to control it programmatically this is more problematic.
Executing commands requires that we write commands to an OutputStream, as we see in the example below:
session.getOutputStream().write("ls -l\r".getBytes());
Note how we have to pass an EOL character to ensure the command gets input into the shell. On Windows, this would need to be \r\n. By default, the ShellTask allocates a dumb pseudo-terminal. If you don't allocate a pseudo-terminal, then the EOL character changes, and you need to use \n on *nix type platforms.
Expect Shell
In order to address this, we have developed the ExpectShell utility to allow developers to execute commands within the shell, capturing the output of just the command itself. To sue this you just create an instance of ExpectShell and pass through the current session.
ssh.runTask(new ShellTask(ssh) {
@Override protected void onOpenSession(SessionChannelNG session)
throws IOException, SshException, ShellTimeoutException {
ExpectShell shell = new ExpectShell(this);
...
}
});
ExpectShell uses the echo command to insert markers before and after command execution. On platforms where it is possible (*nix), we can also capture the exit command of the process. It also performs some startup detection to auto-configure itself.
We can now execute a command on the session and get a ShellProcess object which has its own InputStream for us to read the output of the individual command. When the InputStream returns EOF the command has completed in the session and we can return to execute another.
ShellProcess process = shell.executeCommand("ls -l");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String line;
while((line = reader.readLine())!=null) {
System.out.println(line);
}
We can capture the exit code of the command once it has completed.
int exitCode = process.getExitCode();
Whether the exit code is available or note is dependent upon the platofrm you have connected to. You should check the return value for ExpectShell.EXIT_CODE_UNKNOWN and Shell.EXIT_CODE_PROCESS_ACTIVE constants to ensure you have a valid code.
If you do not want to deal with the InputStream and instead grab all the output at the end we can use the ShellProcess' drain method.
ShellProcess process = shell.executeCommand("ls -l");
process.drain();
String output = process.getCommandOutput();
If we want more interactivity with the command, we can use the ShellProcessController to wrap the process. This has expect methods that allow you to evaluate the output of the command, waiting for specific output so you can respond accordingly. In this example we remove a file, forcing it to confirm and we type 'y' to answer the prompt when it appears.
ShellProcessController controller = new ShellProcessController(
shell.executeCommand("rm -i file.txt"));
if(controller.expect("remove")) {
controller.typeAndReturn("y");
}
controller.getProcess().drain();
Switching Users
One of the powerful features of most shells is the ability to switch user within the shell and start a fresh session with a new user. The ExpectShell supports this through its su methods. These return an entirely new ExpectShell instance, that lasts for the duration of the new user's shell. In the following example, we su from the master shell to one user, exit this shell, and again from the master shell, we su into another user's shell. Each time we exit from a child shell we can return to the parent shell to execute more commands.
ExpectShell shell = new ExpectShell(this);
ExpectShell user1 = shell.su("user1");
System.out.println(user1.executeCommand("whoami").drain().getCommandOutput());
user1.exit();
ExpectShell user2 = shell.su("user2");
System.out.println(user1.executeCommand("whoami").drain().getCommandOutput());
user2.exit();
shell.exit();
You may need to supply a password to the su command.
shell.su("user1", "xxxxx");
If the password prompt is not "Password:" you can also pass this as an additional argument.
shell.su("user1", "xxxxx", "Secret:");
Sudo
There are other times you cannot switch to a users shell, for example, you have logged in as a standard user with the ability to sudo as a privileged user. This is also supported through the ExpectShell's sudo methods. In the case of these methods, a ShellProcess is returned as it is exected in the context of the current session.
It's important to note here, that the methods to do prepend sudo command, you have to supply the exact command including sudo and its arguments.
If a password is required by the user to perform sudo you should force sudo to prompt for the users' password using the -k switch to remove cached credentials. The methods cannot optionally capture the password output, it either needs to be without, or with a password.
shell.sudo("sudo -k systemctl start haproxy", "xxxxxx");
These methods return a ShellProcess so all techniques above can be used on the returned process.