I'm working on a Putty-like user-interactive SSH UI using C# and SSH.NET, an open-source library for making SSH connections. The issue I've been having is that the way all the examples I've been able to find work, they appear to create a new connection, or execute in non-interactive mode, which means nothing executed in a prior command carries over to following commands.
I'm basically new to using streams - there are some methods in the SSH.NET library that appear to open a shell using streams, such that you can assign streams to stdin, stdout, and stderr, but in trying to use them, I can't find good information on how to setup the streams to feed them into the method calls.
- Do I feed in raw streams?
- Do I use
MemoryStream
orTextStream
?
Here's the short definition of the CreateShell
method in the SshClient
class in SSH.NET:
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput)
{
return CreateShell(input, output, extendedOutput, string.Empty, 0, 0, 0, 0, null, 1024);
}
And the long override version that one uses:
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModes, int bufferSize)
{
EnsureSessionIsOpen();
return new Shell(Session, input, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, bufferSize);
}
And after setting up the correct streams to attach, how do I read and write to them? Do I need to setup a StreamReader
and/or StreamWriter
to access the stream(s)? I have already done this in order to read the StdErr
after using the existing SshCommand
object with the .Execute()
method, but I'm not sure how to tie this to the pre-existing streams that I create.
ShellStream Class
Finally, there is another option which seems to make the connection with a single Stream
object - but I'm unsure if this is just being used for Unit Testing, or if it is viable for "normal" operations:
public ShellStream CreateShellStream(string terminalName, uint columns, uint rows, uint width, uint height, int bufferSize, IDictionary<TerminalModes, uint> terminalModeValues)
{
EnsureSessionIsOpen();
return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}
The ShellStream : Stream
class does look like a good candidate as well, with overrides to the base Stream
class for CanRead
, CanSeek
, CanWrite
, Flush
, Length
, Read
, Write
, etc.
Here is the base definition of the ShellStream
class, with properties and the constructor:
public class ShellStream : Stream
{
private const string CrLf = "\r\n";
private readonly ISession _session;
private readonly Encoding _encoding;
private readonly int _bufferSize;
private readonly Queue<byte> _incoming;
private readonly Queue<byte> _outgoing;
private IChannelSession _channel;
private AutoResetEvent _dataReceived = new AutoResetEvent(false);
private bool _isDisposed;
/// <summary>
/// Occurs when data was received.
/// </summary>
public event EventHandler<ShellDataEventArgs> DataReceived;
/// <summary>
/// Occurs when an error occurred.
/// </summary>
public event EventHandler<ExceptionEventArgs> ErrorOccurred;
/// <summary>
/// Gets a value that indicates whether data is available on the <see cref="ShellStream"/> to be read.
/// </summary>
/// <value>
/// <c>true</c> if data is available to be read; otherwise, <c>false</c>.
/// </value>
public bool DataAvailable
{
get
{
lock (_incoming)
{
return _incoming.Count > 0;
}
}
}
/// <summary>
/// Gets the number of bytes that will be written to the internal buffer.
/// </summary>
/// <value>
/// The number of bytes that will be written to the internal buffer.
/// </value>
internal int BufferSize
{
get { return _bufferSize; }
}
/// <summary>
/// Initializes a new <see cref="ShellStream"/> instance.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="terminalName">The <c>TERM</c> environment variable.</param>
/// <param name="columns">The terminal width in columns.</param>
/// <param name="rows">The terminal width in rows.</param>
/// <param name="width">The terminal height in pixels.</param>
/// <param name="height">The terminal height in pixels.</param>
/// <param name="terminalModeValues">The terminal mode values.</param>
/// <param name="bufferSize">The size of the buffer.</param>
internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModeValues, int bufferSize)
{
_encoding = session.ConnectionInfo.Encoding;
_session = session;
_bufferSize = bufferSize;
_incoming = new Queue<byte>();
_outgoing = new Queue<byte>();
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.Closed += Channel_Closed;
_session.Disconnected += Session_Disconnected;
_session.ErrorOccured += Session_ErrorOccured;
_channel.Open();
_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues);
_channel.SendShellRequest();
}