using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
namespace Renci.SshNet
{
///
/// Represents instance of the SSH shell object.
///
public class Shell : IDisposable
{
private readonly ISession _session;
private readonly string _terminalName;
private readonly uint _columns;
private readonly uint _rows;
private readonly uint _width;
private readonly uint _height;
private readonly IDictionary _terminalModes;
private readonly Stream _outputStream;
private readonly Stream _extendedOutputStream;
private readonly int _bufferSize;
private EventWaitHandle _dataReaderTaskCompleted;
private IChannelSession _channel;
private EventWaitHandle _channelClosedWaitHandle;
private Stream _input;
///
/// Gets a value indicating whether this shell is started.
///
///
/// true if started is started; otherwise, false.
///
public bool IsStarted { get; private set; }
///
/// Occurs when shell is starting.
///
public event EventHandler Starting;
///
/// Occurs when shell is started.
///
public event EventHandler Started;
///
/// Occurs when shell is stopping.
///
public event EventHandler Stopping;
///
/// Occurs when shell is stopped.
///
public event EventHandler Stopped;
///
/// Occurs when an error occurred.
///
public event EventHandler ErrorOccurred;
///
/// Initializes a new instance of the class.
///
/// The session.
/// The input.
/// The output.
/// The extended output.
/// Name of the terminal.
/// The columns.
/// The rows.
/// The width.
/// The height.
/// The terminal modes.
/// Size of the buffer for output stream.
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary terminalModes, int bufferSize)
{
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_terminalName = terminalName;
_columns = columns;
_rows = rows;
_width = width;
_height = height;
_terminalModes = terminalModes;
_bufferSize = bufferSize;
}
///
/// Starts this shell.
///
/// Shell is started.
public void Start()
{
if (IsStarted)
{
throw new SshException("Shell is started.");
}
Starting?.Invoke(this, EventArgs.Empty);
_channel = _session.CreateChannelSession();
_channel.DataReceived += Channel_DataReceived;
_channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
_channel.Closed += Channel_Closed;
_session.Disconnected += Session_Disconnected;
_session.ErrorOccured += Session_ErrorOccured;
_channel.Open();
_ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes);
_ = _channel.SendShellRequest();
_channelClosedWaitHandle = new AutoResetEvent(initialState: false);
// Start input stream listener
_dataReaderTaskCompleted = new ManualResetEvent(initialState: false);
ThreadAbstraction.ExecuteThread(() =>
{
try
{
var buffer = new byte[_bufferSize];
while (_channel.IsOpen)
{
var readTask = _input.ReadAsync(buffer, 0, buffer.Length);
var readWaitHandle = ((IAsyncResult) readTask).AsyncWaitHandle;
if (WaitHandle.WaitAny(new[] {readWaitHandle, _channelClosedWaitHandle}) == 0)
{
var read = readTask.GetAwaiter().GetResult();
_channel.SendData(buffer, 0, read);
continue;
}
break;
}
}
catch (Exception exp)
{
RaiseError(new ExceptionEventArgs(exp));
}
finally
{
_ = _dataReaderTaskCompleted.Set();
}
});
IsStarted = true;
Started?.Invoke(this, EventArgs.Empty);
}
///
/// Stops this shell.
///
/// Shell is not started.
public void Stop()
{
if (!IsStarted)
{
throw new SshException("Shell is not started.");
}
_channel?.Dispose();
}
private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
{
RaiseError(e);
}
private void RaiseError(ExceptionEventArgs e)
{
ErrorOccurred?.Invoke(this, e);
}
private void Session_Disconnected(object sender, EventArgs e)
{
Stop();
}
private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
{
_extendedOutputStream?.Write(e.Data, 0, e.Data.Length);
}
private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
{
_outputStream?.Write(e.Data, 0, e.Data.Length);
}
private void Channel_Closed(object sender, ChannelEventArgs e)
{
if (Stopping is not null)
{
// Handle event on different thread
ThreadAbstraction.ExecuteThread(() => Stopping(this, EventArgs.Empty));
}
_channel.Dispose();
_ = _channelClosedWaitHandle.Set();
_input.Dispose();
_input = null;
_ = _dataReaderTaskCompleted.WaitOne(_session.ConnectionInfo.Timeout);
_dataReaderTaskCompleted.Dispose();
_dataReaderTaskCompleted = null;
_channel.DataReceived -= Channel_DataReceived;
_channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
_channel.Closed -= Channel_Closed;
UnsubscribeFromSessionEvents(_session);
if (Stopped != null)
{
// Handle event on different thread
ThreadAbstraction.ExecuteThread(() => Stopped(this, EventArgs.Empty));
}
_channel = null;
}
///
/// Unsubscribes the current from session events.
///
/// The session.
///
/// Does nothing when is null.
///
private void UnsubscribeFromSessionEvents(ISession session)
{
if (session is null)
{
return;
}
session.Disconnected -= Session_Disconnected;
session.ErrorOccured -= Session_ErrorOccured;
}
private bool _disposed;
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
/// Releases unmanaged and - optionally - managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
UnsubscribeFromSessionEvents(_session);
var channelClosedWaitHandle = _channelClosedWaitHandle;
if (channelClosedWaitHandle is not null)
{
channelClosedWaitHandle.Dispose();
_channelClosedWaitHandle = null;
}
var channel = _channel;
if (channel is not null)
{
channel.Dispose();
_channel = null;
}
var dataReaderTaskCompleted = _dataReaderTaskCompleted;
if (dataReaderTaskCompleted is not null)
{
dataReaderTaskCompleted.Dispose();
_dataReaderTaskCompleted = null;
}
_disposed = true;
}
}
///
/// Finalizes an instance of the class.
///
~Shell()
{
Dispose(disposing: false);
}
}
}