#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
///
/// Contains operation for working with SSH Shell.
///
public class ShellStream : Stream
{
private const int DefaultBufferSize = 1024;
private readonly ISession _session;
private readonly Encoding _encoding;
private readonly IChannelSession _channel;
private readonly byte[] _carriageReturnBytes;
private readonly byte[] _lineFeedBytes;
private readonly bool _noTerminal;
private readonly object _sync = new object();
private System.Net.ArrayBuffer _readBuffer;
private System.Net.ArrayBuffer _writeBuffer;
private bool _disposed;
///
/// Occurs when data was received.
///
public event EventHandler? DataReceived;
///
/// Occurs when an error occurred.
///
public event EventHandler? ErrorOccurred;
///
/// Occurs when the channel was closed.
///
public event EventHandler? Closed;
///
/// Gets a value indicating whether data is available on the to be read.
///
///
/// if data is available to be read; otherwise, .
///
public bool DataAvailable
{
get
{
lock (_sync)
{
return _readBuffer.ActiveLength > 0;
}
}
}
///
/// Initializes a new instance of the class.
///
/// The SSH session.
/// The TERM environment variable.
/// The terminal width in columns.
/// The terminal width in rows.
/// The terminal width in pixels.
/// The terminal height in pixels.
/// The terminal mode values.
/// The size of the buffer.
/// The channel could not be opened.
/// The pseudo-terminal request was not accepted by the server.
/// The request to start a shell was not accepted by the server.
internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary terminalModeValues, int bufferSize)
: this(session, bufferSize, noTerminal: false)
{
try
{
_channel.Open();
if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
{
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
}
if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}
}
catch
{
Dispose();
throw;
}
}
///
/// Initializes a new instance of the class.
///
/// The SSH session.
/// The size of the buffer.
/// The channel could not be opened.
/// The request to start a shell was not accepted by the server.
internal ShellStream(ISession session, int bufferSize)
: this(session, bufferSize, noTerminal: true)
{
try
{
_channel.Open();
if (!_channel.SendShellRequest())
{
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
}
}
catch
{
Dispose();
throw;
}
}
///
/// Initializes a new instance of the class.
///
/// The SSH session.
/// The size of the buffer.
/// Disables pseudo terminal allocation or not.
/// The channel could not be opened.
private ShellStream(ISession session, int bufferSize, bool noTerminal)
{
if (bufferSize == -1)
{
bufferSize = DefaultBufferSize;
}
#if NET
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
#else
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
#endif
_encoding = session.ConnectionInfo.Encoding;
_session = session;
_carriageReturnBytes = _encoding.GetBytes("\r");
_lineFeedBytes = _encoding.GetBytes("\n");
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.Closed += Channel_Closed;
_session.Disconnected += Session_Disconnected;
_session.ErrorOccured += Session_ErrorOccurred;
_readBuffer = new System.Net.ArrayBuffer(bufferSize);
_writeBuffer = new System.Net.ArrayBuffer(bufferSize);
_noTerminal = noTerminal;
}
///
/// Gets a value indicating whether the current stream supports reading.
///
///
/// .
///
///
/// It is safe to read from even after disposal.
///
public override bool CanRead
{
get { return true; }
}
///
/// Gets a value indicating whether the current stream supports seeking.
///
///
/// .
///
public override bool CanSeek
{
get { return false; }
}
///
/// Gets a value indicating whether the current stream supports writing.
///
///
/// if this stream has not been disposed and the underlying channel
/// is still open, otherwise .
///
///
/// A value of does not necessarily mean a write will succeed. It is possible
/// that the channel is closed and/or the stream is disposed by another thread between a call to
/// and the call to write.
///
public override bool CanWrite
{
get { return !_disposed; }
}
///
public override void Flush()
{
ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
if (_writeBuffer.ActiveLength > 0)
{
_channel.SendData(
_writeBuffer.DangerousGetUnderlyingBuffer(),
_writeBuffer.ActiveStartOffset,
_writeBuffer.ActiveLength);
_writeBuffer.Discard(_writeBuffer.ActiveLength);
}
}
///
/// Gets the number of bytes currently available for reading.
///
/// A value representing the length of the stream in bytes.
public override long Length
{
get
{
lock (_sync)
{
return _readBuffer.ActiveLength;
}
}
}
///
/// This property always returns 0, and throws
/// when calling the setter.
///
///
/// 0.
///
/// The setter is called.
#pragma warning disable SA1623 // The property's documentation should begin with 'Gets or sets'
public override long Position
#pragma warning restore SA1623 // The property's documentation should begin with 'Gets or sets'
{
get { return 0; }
set { throw new NotSupportedException(); }
}
///
/// This method always throws .
///
/// A byte offset relative to the parameter.
/// A value of type indicating the reference point used to obtain the new position.
/// Never.
/// Always.
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
///
/// This method always throws .
///
/// The desired length of the current stream in bytes.
/// Always.
public override void SetLength(long value)
{
throw new NotSupportedException();
}
///
/// Sends new dimensions of the window (terminal) to the server.
///
/// The terminal width in columns.
/// The terminal height in rows.
/// The terminal width in pixels.
/// The terminal height in pixels.
///
/// The column/row dimensions override the pixel dimensions (when nonzero). Pixel dimensions refer
/// to the drawable area of the window.
///
/// The stream is closed.
public void ChangeWindowSize(uint columns, uint rows, uint width, uint height)
{
ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
_channel.SendWindowChangeRequest(columns, rows, width, height);
}
///
/// Expects the specified expression and performs action when one is found.
///
/// The expected expressions and actions to perform.
public void Expect(params ExpectAction[] expectActions)
{
Expect(Timeout.InfiniteTimeSpan, expectActions);
}
///
/// Expects the specified expression and performs action when one is found.
///
/// Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The expected expressions and actions to perform, if the specified time elapsed and expected condition have not met, that method will exit without executing any action.
///
/// If a TimeSpan representing -1 millisecond is specified for the parameter,
/// this method blocks indefinitely until either the regex matches the data in the buffer, or the stream
/// is closed (via disposal or via the underlying channel closing).
///
public void Expect(TimeSpan timeout, params ExpectAction[] expectActions)
{
_ = ExpectRegex(timeout, lookback: -1, expectActions);
}
///
/// Expects the specified expression and performs action when one is found.
///
/// Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.
/// The expected expressions and actions to perform, if the specified time elapsed and expected condition have not met, that method will exit without executing any action.
///
///
/// If a TimeSpan representing -1 millisecond is specified for the parameter,
/// this method blocks indefinitely until either the regex matches the data in the buffer, or the stream
/// is closed (via disposal or via the underlying channel closing).
///
///
/// Use the parameter to constrain the search space to a fixed-size rolling window at the end of the buffer.
/// This can reduce the amount of work done in cases where lots of output from the shell is expected to be received before the matching expression is found.
///
///
/// Note: in situations with high volumes of data and a small value for , some data may not be searched through.
/// It is recommended to set to a large enough value to be able to search all data as it comes in,
/// but which still places a limit on the amount of work needed.
///
///
public void Expect(TimeSpan timeout, int lookback, params ExpectAction[] expectActions)
{
_ = ExpectRegex(timeout, lookback, expectActions);
}
///
/// Expects the expression specified by text.
///
/// The text to expect.
///
/// The text available in the shell up to and including the expected text,
/// or if the the stream is closed without a match.
///
public string? Expect(string text)
{
return Expect(text, Timeout.InfiniteTimeSpan);
}
///
/// Expects the expression specified by text.
///
/// The text to expect.
/// Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.
///
/// The text available in the shell up to and including the expected expression,
/// or if the specified time has elapsed or the stream is closed
/// without a match.
///
///
public string? Expect(string text, TimeSpan timeout, int lookback = -1)
{
ValidateTimeout(timeout);
ValidateLookback(lookback);
var timeoutTime = DateTime.Now.Add(timeout);
var expectBytes = _encoding.GetBytes(text);
lock (_sync)
{
while (true)
{
var searchHead = lookback == -1
? 0
: Math.Max(0, _readBuffer.ActiveLength - lookback);
var indexOfMatch = _readBuffer.ActiveReadOnlySpan.Slice(searchHead).IndexOf(expectBytes);
if (indexOfMatch >= 0)
{
var readLength = searchHead + indexOfMatch + expectBytes.Length;
var returnText = GetString(readLength);
_readBuffer.Discard(readLength);
return returnText;
}
if (_disposed)
{
return null;
}
if (timeout == Timeout.InfiniteTimeSpan)
{
_ = Monitor.Wait(_sync);
}
else
{
var waitTimeout = timeoutTime - DateTime.Now;
if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
{
return null;
}
}
}
}
}
///
/// Expects the expression specified by regular expression.
///
/// The regular expression to expect.
///
/// The text available in the shell up to and including the expected expression,
/// or if the stream is closed without a match.
///
public string? Expect(Regex regex)
{
return Expect(regex, Timeout.InfiniteTimeSpan);
}
///
/// Expects the expression specified by regular expression.
///
/// The regular expression to expect.
/// Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.
///
/// The text available in the shell up to and including the expected expression,
/// or if the specified timeout has elapsed or the stream
/// is closed without a match.
///
///
///
///
public string? Expect(Regex regex, TimeSpan timeout, int lookback = -1)
{
return ExpectRegex(timeout, lookback, [new ExpectAction(regex, s => { })]);
}
private string? ExpectRegex(TimeSpan timeout, int lookback, ExpectAction[] expectActions)
{
ValidateTimeout(timeout);
ValidateLookback(lookback);
var timeoutTime = DateTime.Now.Add(timeout);
lock (_sync)
{
while (true)
{
var bufferText = GetString(_readBuffer.ActiveLength);
var searchStart = lookback == -1
? 0
: Math.Max(bufferText.Length - lookback, 0);
foreach (var expectAction in expectActions)
{
#if NET
var matchEnumerator = expectAction.Expect.EnumerateMatches(bufferText.AsSpan(searchStart));
if (matchEnumerator.MoveNext())
{
var match = matchEnumerator.Current;
var returnText = bufferText.Substring(0, searchStart + match.Index + match.Length);
#else
var match = expectAction.Expect.Match(bufferText, searchStart);
if (match.Success)
{
var returnText = bufferText.Substring(0, match.Index + match.Length);
#endif
_readBuffer.Discard(_encoding.GetByteCount(returnText));
expectAction.Action(returnText);
return returnText;
}
}
if (_disposed)
{
return null;
}
if (timeout == Timeout.InfiniteTimeSpan)
{
Monitor.Wait(_sync);
}
else
{
var waitTimeout = timeoutTime - DateTime.Now;
if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
{
return null;
}
}
}
}
}
///
/// Begins the expect.
///
/// The expect actions.
///
/// An that references the asynchronous operation.
///
public IAsyncResult BeginExpect(params ExpectAction[] expectActions)
{
return BeginExpect(Timeout.InfiniteTimeSpan, callback: null, state: null, expectActions);
}
///
/// Begins the expect.
///
/// The callback.
/// The expect actions.
///
/// An that references the asynchronous operation.
///
public IAsyncResult BeginExpect(AsyncCallback? callback, params ExpectAction[] expectActions)
{
return BeginExpect(Timeout.InfiniteTimeSpan, callback, state: null, expectActions);
}
///
/// Begins the expect.
///
/// The callback.
/// The state.
/// The expect actions.
///
/// An that references the asynchronous operation.
///
public IAsyncResult BeginExpect(AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
{
return BeginExpect(Timeout.InfiniteTimeSpan, callback, state, expectActions);
}
///
/// Begins the expect.
///
/// The timeout. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The callback.
/// The state.
/// The expect actions.
///
/// An that references the asynchronous operation.
///
public IAsyncResult BeginExpect(TimeSpan timeout, AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
{
return BeginExpect(timeout, lookback: -1, callback, state, expectActions);
}
///
/// Begins the expect.
///
/// The timeout. Must non-negative or equal to -1 millisecond (for infinite timeout).
/// The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.
/// The callback.
/// The state.
/// The expect actions.
///
/// An that references the asynchronous operation.
///
public IAsyncResult BeginExpect(TimeSpan timeout, int lookback, AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
{
return TaskToAsyncResult.Begin(Task.Run(() => ExpectRegex(timeout, lookback, expectActions)), callback, state);
}
///
/// Ends the execute.
///
/// The async result.
///
/// The text available in the shell up to and including the expected expression.
///
public string? EndExpect(IAsyncResult asyncResult)
{
return TaskToAsyncResult.End(asyncResult);
}
///
/// Reads the next line from the shell. If a line is not available it will block and wait for a new line.
///
///
/// The line read from the shell.
///
///
///
/// This method blocks indefinitely until either a line is available in the buffer, or the stream is closed
/// (via disposal or via the underlying channel closing).
///
///
/// When the stream is closed and there are no more newlines in the buffer, this method returns the remaining data
/// (if any) and then indicating that no more data is in the buffer.
///
///
public string? ReadLine()
{
return ReadLine(Timeout.InfiniteTimeSpan);
}
///
/// Reads a line from the shell. If line is not available it will block the execution and will wait for new line.
///
/// Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).
///
/// The line read from the shell, or when no input is received for the specified timeout.
///
///
///
/// If a TimeSpan representing -1 millisecond is specified for the parameter, this method
/// blocks indefinitely until either a line is available in the buffer, or the stream is closed (via disposal or via
/// the underlying channel closing).
///
///
/// When the stream is closed and there are no more newlines in the buffer, this method returns the remaining data
/// (if any) and then indicating that no more data is in the buffer.
///
///
public string? ReadLine(TimeSpan timeout)
{
ValidateTimeout(timeout);
var timeoutTime = DateTime.Now.Add(timeout);
lock (_sync)
{
while (true)
{
var indexOfCr = _readBuffer.ActiveReadOnlySpan.IndexOf(_carriageReturnBytes);
if (indexOfCr >= 0)
{
// We have found \r. We only need to search for \n up to and just after the \r
// (in order to consume \r\n if we can).
var indexOfLf = indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length <= _readBuffer.ActiveLength
? _readBuffer.ActiveReadOnlySpan.Slice(0, indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length).IndexOf(_lineFeedBytes)
: _readBuffer.ActiveReadOnlySpan.Slice(0, indexOfCr).IndexOf(_lineFeedBytes);
if (indexOfLf >= 0 && indexOfLf < indexOfCr)
{
// If there is \n before the \r, then return up to the \n
var returnText = GetString(indexOfLf);
_readBuffer.Discard(indexOfLf + _lineFeedBytes.Length);
return returnText;
}
else if (indexOfLf == indexOfCr + _carriageReturnBytes.Length)
{
// If we have \r\n, then consume both
var returnText = GetString(indexOfCr);
_readBuffer.Discard(indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length);
return returnText;
}
else
{
// Return up to the \r
var returnText = GetString(indexOfCr);
_readBuffer.Discard(indexOfCr + _carriageReturnBytes.Length);
return returnText;
}
}
else
{
// There is no \r. What about \n?
var indexOfLf = _readBuffer.ActiveReadOnlySpan.IndexOf(_lineFeedBytes);
if (indexOfLf >= 0)
{
var returnText = GetString(indexOfLf);
_readBuffer.Discard(indexOfLf + _lineFeedBytes.Length);
return returnText;
}
}
if (_disposed)
{
var lastLine = _readBuffer.ActiveLength == 0
? null
: GetString(_readBuffer.ActiveLength);
_readBuffer.Discard(_readBuffer.ActiveLength);
return lastLine;
}
if (timeout == Timeout.InfiniteTimeSpan)
{
_ = Monitor.Wait(_sync);
}
else
{
var waitTimeout = timeoutTime - DateTime.Now;
if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
{
return null;
}
}
}
}
}
private static void ValidateTimeout(TimeSpan timeout)
{
if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(nameof(timeout), "Value must be non-negative or equal to -1 millisecond (for infinite timeout)");
}
}
private static void ValidateLookback(int lookback)
{
if (lookback is <= 0 and not -1)
{
throw new ArgumentOutOfRangeException(nameof(lookback), "Value must be positive or equal to -1 (for no window)");
}
}
///
/// Reads all of the text currently available in the shell.
///
///
/// The text available in the shell.
///
public string Read()
{
lock (_sync)
{
var text = GetString(_readBuffer.ActiveLength);
_readBuffer.Discard(_readBuffer.ActiveLength);
return text;
}
}
///
public override int Read(byte[] buffer, int offset, int count)
{
#if !NET
ThrowHelper.
#endif
ValidateBufferArguments(buffer, offset, count);
return Read(buffer.AsSpan(offset, count));
}
#if NET
///
public override int Read(Span buffer)
#else
private int Read(Span buffer)
#endif
{
lock (_sync)
{
while (_readBuffer.ActiveLength == 0 && !_disposed)
{
_ = Monitor.Wait(_sync);
}
var bytesRead = Math.Min(buffer.Length, _readBuffer.ActiveLength);
_readBuffer.ActiveReadOnlySpan.Slice(0, bytesRead).CopyTo(buffer);
_readBuffer.Discard(bytesRead);
return bytesRead;
}
}
#if NET
///
public override int ReadByte()
{
byte b = default;
var read = Read(new Span(ref b));
return read == 0 ? -1 : b;
}
#endif
private string GetString(int length)
{
Debug.Assert(Monitor.IsEntered(_sync));
Debug.Assert(length <= _readBuffer.ActiveLength);
return _encoding.GetString(
_readBuffer.DangerousGetUnderlyingBuffer(),
_readBuffer.ActiveStartOffset,
length);
}
///
/// Writes the specified text to the shell.
///
/// The text to be written to the shell.
///
/// If is , nothing is written.
/// Otherwise, is called after writing the data to the buffer.
///
/// The stream is closed.
public void Write(string? text)
{
if (text is null)
{
return;
}
Write(_encoding.GetBytes(text));
Flush();
}
///
public override void Write(byte[] buffer, int offset, int count)
{
#if !NET
ThrowHelper.
#endif
ValidateBufferArguments(buffer, offset, count);
Write(buffer.AsSpan(offset, count));
}
#if NET
///
public override void Write(ReadOnlySpan buffer)
#else
private void Write(ReadOnlySpan buffer)
#endif
{
ThrowHelper.ThrowObjectDisposedIf(_disposed, this);
while (!buffer.IsEmpty)
{
if (_writeBuffer.AvailableLength == 0)
{
Flush();
}
var bytesToCopy = Math.Min(buffer.Length, _writeBuffer.AvailableLength);
Debug.Assert(bytesToCopy > 0);
buffer.Slice(0, bytesToCopy).CopyTo(_writeBuffer.AvailableSpan);
_writeBuffer.Commit(bytesToCopy);
buffer = buffer.Slice(bytesToCopy);
}
}
///
public override void WriteByte(byte value)
{
Write([value]);
}
///
/// Writes the line to the shell.
///
/// The line to be written to the shell.
///
/// If is , only the line terminator is written.
/// is called once the data is written.
///
/// The stream is closed.
public void WriteLine(string line)
{
// By default, the terminal driver translates carriage return to line feed on input.
// See option ICRLF at https://www.man7.org/linux/man-pages/man3/termios.3.html.
Write(line + (_noTerminal ? "\n" : "\r"));
}
///
protected override void Dispose(bool disposing)
{
if (!disposing)
{
base.Dispose(disposing);
return;
}
lock (_sync)
{
if (_disposed)
{
return;
}
_disposed = true;
// Do not dispose _session (we don't own it)
_session.Disconnected -= Session_Disconnected;
_session.ErrorOccured -= Session_ErrorOccurred;
// But we do own _channel
_channel.DataReceived -= Channel_DataReceived;
_channel.Closed -= Channel_Closed;
_channel.Dispose();
Monitor.PulseAll(_sync);
}
base.Dispose(disposing);
}
private void Session_ErrorOccurred(object? sender, ExceptionEventArgs e)
{
ErrorOccurred?.Invoke(this, e);
}
private void Session_Disconnected(object? sender, EventArgs e)
{
Dispose();
}
private void Channel_Closed(object? sender, ChannelEventArgs e)
{
Dispose();
if (Closed != null)
{
// Handle event on different thread
ThreadAbstraction.ExecuteThread(() => Closed?.Invoke(this, EventArgs.Empty));
}
}
private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
{
lock (_sync)
{
_readBuffer.EnsureAvailableSpace(e.Data.Count);
e.Data.AsSpan().CopyTo(_readBuffer.AvailableSpan);
_readBuffer.Commit(e.Data.Count);
Monitor.PulseAll(_sync);
}
DataReceived?.Invoke(this, new ShellDataEventArgs(e.Data.ToArray()));
}
}
}