You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
11 KiB
C#
346 lines
11 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using Renci.SshNet.Channels;
|
|
using Renci.SshNet.Common;
|
|
using System.Collections.Generic;
|
|
using Renci.SshNet.Abstractions;
|
|
|
|
namespace Renci.SshNet
|
|
{
|
|
/// <summary>
|
|
/// Represents instance of the SSH shell object
|
|
/// </summary>
|
|
public class Shell : IDisposable
|
|
{
|
|
private readonly ISession _session;
|
|
private IChannelSession _channel;
|
|
private EventWaitHandle _channelClosedWaitHandle;
|
|
private Stream _input;
|
|
private readonly string _terminalName;
|
|
private readonly uint _columns;
|
|
private readonly uint _rows;
|
|
private readonly uint _width;
|
|
private readonly uint _height;
|
|
private readonly IDictionary<TerminalModes, uint> _terminalModes;
|
|
private EventWaitHandle _dataReaderTaskCompleted;
|
|
private readonly Stream _outputStream;
|
|
private readonly Stream _extendedOutputStream;
|
|
private readonly int _bufferSize;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this shell is started.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if started is started; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public bool IsStarted { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Occurs when shell is starting.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> Starting;
|
|
|
|
/// <summary>
|
|
/// Occurs when shell is started.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> Started;
|
|
|
|
/// <summary>
|
|
/// Occurs when shell is stopping.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> Stopping;
|
|
|
|
/// <summary>
|
|
/// Occurs when shell is stopped.
|
|
/// </summary>
|
|
public event EventHandler<EventArgs> Stopped;
|
|
|
|
/// <summary>
|
|
/// Occurs when an error occurred.
|
|
/// </summary>
|
|
public event EventHandler<ExceptionEventArgs> ErrorOccurred;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Shell"/> class.
|
|
/// </summary>
|
|
/// <param name="session">The session.</param>
|
|
/// <param name="input">The input.</param>
|
|
/// <param name="output">The output.</param>
|
|
/// <param name="extendedOutput">The extended output.</param>
|
|
/// <param name="terminalName">Name of the terminal.</param>
|
|
/// <param name="columns">The columns.</param>
|
|
/// <param name="rows">The rows.</param>
|
|
/// <param name="width">The width.</param>
|
|
/// <param name="height">The height.</param>
|
|
/// <param name="terminalModes">The terminal modes.</param>
|
|
/// <param name="bufferSize">Size of the buffer for output stream.</param>
|
|
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts this shell.
|
|
/// </summary>
|
|
/// <exception cref="SshException">Shell is started.</exception>
|
|
public void Start()
|
|
{
|
|
if (IsStarted)
|
|
{
|
|
throw new SshException("Shell is started.");
|
|
}
|
|
|
|
if (Starting != null)
|
|
{
|
|
Starting(this, new EventArgs());
|
|
}
|
|
|
|
_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(false);
|
|
|
|
// Start input stream listener
|
|
_dataReaderTaskCompleted = new ManualResetEvent(false);
|
|
ThreadAbstraction.ExecuteThread(() =>
|
|
{
|
|
try
|
|
{
|
|
var buffer = new byte[_bufferSize];
|
|
|
|
while (_channel.IsOpen)
|
|
{
|
|
#if FEATURE_STREAM_TAP
|
|
var readTask = _input.ReadAsync(buffer, 0, buffer.Length);
|
|
var readWaitHandle = ((IAsyncResult) readTask).AsyncWaitHandle;
|
|
|
|
if (WaitHandle.WaitAny(new[] {readWaitHandle, _channelClosedWaitHandle}) == 0)
|
|
{
|
|
var read = readTask.Result;
|
|
_channel.SendData(buffer, 0, read);
|
|
continue;
|
|
}
|
|
#elif FEATURE_STREAM_APM
|
|
var asyncResult = _input.BeginRead(buffer, 0, buffer.Length, result =>
|
|
{
|
|
// If input stream is closed and disposed already don't finish reading the stream
|
|
if (_input == null)
|
|
return;
|
|
|
|
var read = _input.EndRead(result);
|
|
_channel.SendData(buffer, 0, read);
|
|
}, null);
|
|
|
|
WaitHandle.WaitAny(new[] { asyncResult.AsyncWaitHandle, _channelClosedWaitHandle });
|
|
|
|
if (asyncResult.IsCompleted)
|
|
continue;
|
|
#else
|
|
#error Async receive is not implemented.
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception exp)
|
|
{
|
|
RaiseError(new ExceptionEventArgs(exp));
|
|
}
|
|
finally
|
|
{
|
|
_dataReaderTaskCompleted.Set();
|
|
}
|
|
});
|
|
|
|
IsStarted = true;
|
|
|
|
if (Started != null)
|
|
{
|
|
Started(this, new EventArgs());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops this shell.
|
|
/// </summary>
|
|
/// <exception cref="SshException">Shell is not started.</exception>
|
|
public void Stop()
|
|
{
|
|
if (!IsStarted)
|
|
{
|
|
throw new SshException("Shell is not started.");
|
|
}
|
|
|
|
if (_channel != null)
|
|
{
|
|
_channel.Dispose();
|
|
}
|
|
}
|
|
|
|
private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
|
|
{
|
|
RaiseError(e);
|
|
}
|
|
|
|
private void RaiseError(ExceptionEventArgs e)
|
|
{
|
|
var handler = ErrorOccurred;
|
|
if (handler != null)
|
|
{
|
|
handler(this, e);
|
|
}
|
|
}
|
|
|
|
private void Session_Disconnected(object sender, EventArgs e)
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
|
|
{
|
|
if (_extendedOutputStream != null)
|
|
{
|
|
_extendedOutputStream.Write(e.Data, 0, e.Data.Length);
|
|
}
|
|
}
|
|
|
|
private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
|
|
{
|
|
if (_outputStream != null)
|
|
{
|
|
_outputStream.Write(e.Data, 0, e.Data.Length);
|
|
}
|
|
}
|
|
|
|
private void Channel_Closed(object sender, ChannelEventArgs e)
|
|
{
|
|
if (Stopping != null)
|
|
{
|
|
// Handle event on different thread
|
|
ThreadAbstraction.ExecuteThread(() => Stopping(this, new EventArgs()));
|
|
}
|
|
|
|
_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, new EventArgs()));
|
|
}
|
|
|
|
_channel = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes the current <see cref="Shell"/> from session events.
|
|
/// </summary>
|
|
/// <param name="session">The session.</param>
|
|
/// <remarks>
|
|
/// Does nothing when <paramref name="session"/> is <c>null</c>.
|
|
/// </remarks>
|
|
private void UnsubscribeFromSessionEvents(ISession session)
|
|
{
|
|
if (session == null)
|
|
return;
|
|
|
|
session.Disconnected -= Session_Disconnected;
|
|
session.ErrorOccured -= Session_ErrorOccured;
|
|
}
|
|
|
|
#region IDisposable Members
|
|
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases unmanaged and - optionally - managed resources
|
|
/// </summary>
|
|
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
if (disposing)
|
|
{
|
|
UnsubscribeFromSessionEvents(_session);
|
|
|
|
var channelClosedWaitHandle = _channelClosedWaitHandle;
|
|
if (channelClosedWaitHandle != null)
|
|
{
|
|
channelClosedWaitHandle.Dispose();
|
|
_channelClosedWaitHandle = null;
|
|
}
|
|
|
|
var channel = _channel;
|
|
if (channel != null)
|
|
{
|
|
channel.Dispose();
|
|
_channel = null;
|
|
}
|
|
|
|
var dataReaderTaskCompleted = _dataReaderTaskCompleted;
|
|
if (dataReaderTaskCompleted != null)
|
|
{
|
|
dataReaderTaskCompleted.Dispose();
|
|
_dataReaderTaskCompleted = null;
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases unmanaged resources and performs other cleanup operations before the
|
|
/// <see cref="Shell"/> is reclaimed by garbage collection.
|
|
/// </summary>
|
|
~Shell()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|