Thursday, October 28, 2021

[SOLVED] Getting shell prompt twice in Java JSch Shell

Issue

Okay so, I'm having a problem where the response from the shell is very inconsistent. The welcome message for example, can come in 2 separate responses. And for some reason, when I use any command after that, the prompt will show up twice in my response.

I've noticed that these problems only occur when using my own piped input/output streams. When I use System.in and System.out, these problems don't occur at all.

Here's a picture where I use System.in and System.out: Used commands: "cd ../", "ls", "cd lib"

When using my own piped input/output streams: Used commands: "cd ../", "ls", "cd lib"

Below the related code.

SSHConnection.java

package com.displee.ssh;

import com.jcraft.jsch.*;

import java.util.function.Function;

public class SSHConnection {

    private static final JSch JSCH_INSTANCE = new JSch();

    private Session session;

    private ShellSession shellSession;

    public SSHConnection(String ip, String username, String password) {
        try {
            this.session = JSCH_INSTANCE.getSession(username, ip);
            this.session.setPassword(password);
            this.session.setConfig("StrictHostKeyChecking", "no");
        } catch(Exception e) {
            e.printStackTrace();
            this.session = null;
        }
    }

    public boolean connect() {
        try {
            session.connect();
            return true;
        } catch(Exception e) {
            e.printStackTrace();
            session.disconnect();
            return false;
        }
    }

    public boolean isConnected() {
        return session.isConnected();
    }

    public void disconnect() {
        session.disconnect();
    }

    public boolean startShellSession(Function<String, Void> callback) {
        try {
            shellSession = new ShellSession();
            shellSession.start(session, callback);
            return true;
        } catch(Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public void writeShellMessage(String message) {
        shellSession.write(message + "\r\n");
    }

}

ShellSession.java

package com.displee.ssh;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.Session;

import java.io.*;
import java.util.function.Function;

/**
 * A class representing a shell session.
 * @author Displee
 */
public class ShellSession {

    /**
     * If this session is running.
     */
    private boolean running = true;

    private PipedOutputStream poutWrapper;

    private PipedInputStream pin = new PipedInputStream(4096);

    private PipedInputStream pinWrapper = new PipedInputStream(4096);

    private PipedOutputStream pout;

    private String lastCommand;

    /**
     * Constructs a new {@code ShellSession} {@code Object}.
     */
    public ShellSession() {
        try {
            pout = new PipedOutputStream(pinWrapper);
            poutWrapper = new PipedOutputStream(pin);
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Start this shell session.
     * @param session The ssh session.
     */
    public void start(Session session, Function<String, Void> callback) {
        ChannelShell shellChannel = null;
        try {
            shellChannel = (ChannelShell) session.openChannel("shell");
            shellChannel.setInputStream(pin);
            shellChannel.setOutputStream(pout);
            shellChannel.connect();
        } catch(Exception e) {
            e.printStackTrace();
        }
        if (shellChannel == null) {
            return;
        }
        final Channel channel = shellChannel;
        Thread thread = new Thread(() -> {
            while (running) {
                try {
                    if (pinWrapper.available() != 0) {
                        String response = readResponse();
                        callback.apply(response);
                    }
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            channel.disconnect();
        });
        thread.setDaemon(true);
        thread.start();
    }

    /**
     * Stop this shell session.
     */
    public void stop() {
        running = false;
    }

    /**
     * Send a message to the shell.
     * @param message The message to send.
     */
    public void write(String message) {
        lastCommand = message;
        try {
            poutWrapper.write(message.getBytes());
        } catch(IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Read the response from {@code pinWrapper}.
     * @return The string.
     * @throws IOException If it could not read the piped stream.
     */
    private synchronized String readResponse() throws IOException {
        final StringBuilder s = new StringBuilder();
        while(pinWrapper.available() > 0) {
            s.append((char) pinWrapper.read());
        }
        return s.toString();
    }

}

My JavaFX controller:

package com.displee.ui;

import com.displee.ssh.SSHConnection;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;

import java.net.URL;
import java.util.ResourceBundle;

public class Controller implements Initializable {

    @FXML
    private TextArea console;

    @FXML
    private TextField commandInput;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        SSHConnection connection = new SSHConnection("host", "username", "password");
        connection.connect();
        connection.startShellSession(s -> {
            console.appendText(s);
            return null;
        });
        commandInput.setOnAction(x -> {
            connection.writeShellMessage(commandInput.getText());
            commandInput.clear();
        });
    }

}

Complete code: https://displee.com/upload/SSHProject.zip

I tried increasing the size of my inputstreams, but that didn't work.

This problem is really frustrating me. If anyone know how I can fix my problem, I'd really appreciate it.

Note: I've also noticed that it's pretty slow when using my piped streams. How can I speed this up?


Solution

Are you correctly processing CR (carriage return)?

The server probably sends the prompt twice in all cases. But if there's CR between the two prompts, when printed on System.out the second prompt will overwrite the first. While if your stream interprets the CR as a "new-line" (=line feed), you get two prompts.



Answered By - Martin Prikryl