Executing Commands within an Interactive Shell

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. In practice, the mechanism we have just discussed is not widely used by Administrators and end-users, so it isn’t easy to match a user’s behaviour by executing commands in that way. The default behaviour 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(ShellTask.ShellTaskBuilder.create().
	.withClient(ssh)
	.onTask((t, session) -> {
      try {     
          int b;     
          byte[] buf = new byte[1024];     
          while((b = session.getInputStream().read(buf)) > -1) {         
              // Use the buffer as needed     
          } 
      } catch (IOException e) { } 
						
      })
.build());

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 who understands this, it’s easy 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

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 use this, you create an instance of ExpectShell and pass through the current session.

ssh.runTask(ShellTask.ShellTaskBuilder.create().
	.withClient(ssh)
	.onTask((t, session) -> {
      ExpectShell shell = new ExpectShell(t);

.build());

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 start-up detection to auto-configure itself. 

We can now execute a command on the session and get a ShellProcess object with its own InputStream to read the output of the individual command. When the InputStream returns EOF, the command has been completed, and we can return to the shell 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 command’s exit code once it has been completed.

int exitCode = process.getExitCode();

Whether the exit code is available depends on the platform 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 command output, 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 users 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 until the new user’s shell ends. 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(t);
 
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 user’s 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 ExpectShell’s sudo methods. In the case of these methods, a ShellProcess is returned as it is expected in the context of the current session.

It’s important to note that these methods do not prepend the sudo command; you must supply the exact command, including sudo and its arguments.

It does not matter if the command prompts for a password; the implementation will capture the prompt if presented, and it can be used multiple times throughout the lifetime of your ExpectShell object. <Java>

shell.sudo("sudo systemctl start haproxy", "xxxxxx");

These methods return a ShellProcess, so all techniques above can be used on the returned process.