Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support creating Shell(Stream) without PTY #1419

Merged
merged 17 commits into from
Jun 16, 2024
Merged
11 changes: 11 additions & 0 deletions src/Renci.SshNet/IServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ ShellStream CreateShellStream(ISession session,
IDictionary<TerminalModes, uint> terminalModeValues,
int bufferSize);

/// <summary>
/// Creates a shell stream.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">Size of the buffer.</param>
/// <returns>
/// The created <see cref="ShellStream"/> instance.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
ShellStream CreateShellStream(ISession session, int bufferSize);

/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
Expand Down
14 changes: 14 additions & 0 deletions src/Renci.SshNet/ServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,20 @@ public ShellStream CreateShellStream(ISession session, string terminalName, uint
return new ShellStream(session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}

/// <summary>
/// Creates a shell stream.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">The size of the buffer.</param>
/// <returns>
/// The created <see cref="ShellStream"/> instance.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public ShellStream CreateShellStream(ISession session, int bufferSize)
{
return new ShellStream(session, bufferSize);
}

/// <summary>
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
/// any embedded double quote with a backslash.
Expand Down
53 changes: 47 additions & 6 deletions src/Renci.SshNet/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class Shell : IDisposable
private readonly Stream _outputStream;
private readonly Stream _extendedOutputStream;
private readonly int _bufferSize;
private readonly bool _disablePTY;
private ManualResetEvent _dataReaderTaskCompleted;
private IChannelSession _channel;
private AutoResetEvent _channelClosedWaitHandle;
Expand Down Expand Up @@ -77,24 +78,54 @@ public class Shell : IDisposable
/// <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)
: this(session, input, output, extendedOutput, bufferSize, disablePTY: false)
{
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_terminalName = terminalName;
_columns = columns;
_rows = rows;
_width = width;
_height = height;
_terminalModes = terminalModes;
}

/// <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="bufferSize">Size of the buffer for output stream.</param>
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize)
: this(session, input, output, extendedOutput, bufferSize, disablePTY: true)
{
}

/// <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="bufferSize">Size of the buffer for output stream.</param>
/// <param name="disablePTY">Disables pseudo terminal allocation or not.</param>
private Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize, bool disablePTY)
{
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_bufferSize = bufferSize;
_disablePTY = disablePTY;
}

/// <summary>
/// Starts this shell.
/// </summary>
/// <exception cref="SshException">Shell is started.</exception>
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
public void Start()
{
if (IsStarted)
Expand All @@ -112,8 +143,18 @@ public void Start()
_session.ErrorOccured += Session_ErrorOccured;

_channel.Open();
_ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes);
_ = _channel.SendShellRequest();
if (!_disablePTY)
{
if (!_channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes))
{
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.");
}

_channelClosedWaitHandle = new AutoResetEvent(initialState: false);

Expand Down
80 changes: 60 additions & 20 deletions src/Renci.SshNet/ShellStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ShellStream : Stream
private readonly object _sync = new object();

private readonly byte[] _writeBuffer;
private readonly bool _disablePTY;
private int _writeLength; // The length of the data in _writeBuffer.

private byte[] _readBuffer;
Expand Down Expand Up @@ -95,6 +96,63 @@ private void AssertValid()
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModeValues, int bufferSize)
: this(session, bufferSize, disablePTY: 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;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ShellStream"/> class.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">The size of the buffer.</param>
/// <exception cref="SshException">The channel could not be opened.</exception>
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
internal ShellStream(ISession session, int bufferSize)
: this(session, bufferSize, disablePTY: 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;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ShellStream"/> class.
/// </summary>
/// <param name="session">The SSH session.</param>
/// <param name="bufferSize">The size of the buffer.</param>
/// <param name="disablePTY">Disables pseudo terminal allocation or not.</param>
/// <exception cref="SshException">The channel could not be opened.</exception>
private ShellStream(ISession session, int bufferSize, bool disablePTY)
{
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
Expand All @@ -119,25 +177,7 @@ internal ShellStream(ISession session, string terminalName, uint columns, uint r
_readBuffer = new byte[bufferSize];
_writeBuffer = new byte[bufferSize];

try
Rob-Hague marked this conversation as resolved.
Show resolved Hide resolved
{
_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;
}
_disablePTY = disablePTY;
}

/// <summary>
Expand Down Expand Up @@ -848,7 +888,7 @@ public override void Write(byte[] buffer, int offset, int count)
/// <exception cref="ObjectDisposedException">The stream is closed.</exception>
public void WriteLine(string line)
{
Write(line + "\r");
Write(line + (_disablePTY ? "\n" : "\r"));
scott-xu marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc/>
Expand Down
66 changes: 65 additions & 1 deletion src/Renci.SshNet/SshClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, str
return new Shell(Session, input, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, bufferSize);
}

/// <summary>
/// Creates the shell.
/// </summary>
/// <param name="input">The input.</param>
/// <param name="output">The output.</param>
/// <param name="extendedOutput">The extended output.</param>
/// <param name="bufferSize">Size of the internal read buffer.</param>
/// <returns>
/// Returns a representation of a <see cref="Shell" /> object.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, int bufferSize)
{
EnsureSessionIsOpen();

return new Shell(Session, input, output, extendedOutput, bufferSize);
}

/// <summary>
/// Creates the shell.
/// </summary>
Expand Down Expand Up @@ -313,7 +331,7 @@ public Shell CreateShell(Stream input, Stream output, Stream extendedOutput, str
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public Shell CreateShell(Stream input, Stream output, Stream extendedOutput)
{
return CreateShell(input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
return CreateShell(input, output, extendedOutput, 1024);
scott-xu marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand Down Expand Up @@ -353,6 +371,37 @@ public Shell CreateShell(Encoding encoding, string input, Stream output, Stream
return CreateShell(_inputStream, output, extendedOutput, terminalName, columns, rows, width, height, terminalModes, bufferSize);
}

/// <summary>
/// Creates the shell.
/// </summary>
/// <param name="encoding">The encoding to use to send the input.</param>
/// <param name="input">The input.</param>
/// <param name="output">The output.</param>
/// <param name="extendedOutput">The extended output.</param>
/// <param name="bufferSize">Size of the internal read buffer.</param>
/// <returns>
/// Returns a representation of a <see cref="Shell" /> object.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public Shell CreateShell(Encoding encoding, string input, Stream output, Stream extendedOutput, int bufferSize)
scott-xu marked this conversation as resolved.
Show resolved Hide resolved
{
/*
* TODO Issue #1224: let shell dispose of input stream when we own the stream!
*/

_inputStream = new MemoryStream();

using (var writer = new StreamWriter(_inputStream, encoding, bufferSize: 1024, leaveOpen: true))
{
writer.Write(input);
writer.Flush();
}

_ = _inputStream.Seek(0, SeekOrigin.Begin);

return CreateShell(_inputStream, output, extendedOutput, bufferSize);
}

/// <summary>
/// Creates the shell.
/// </summary>
Expand Down Expand Up @@ -450,6 +499,21 @@ public ShellStream CreateShellStream(string terminalName, uint columns, uint row
return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}

/// <summary>
/// Creates the shell stream.
/// </summary>
/// <param name="bufferSize">The size of the buffer.</param>
/// <returns>
/// The created <see cref="ShellStream"/> instance.
/// </returns>
/// <exception cref="SshConnectionException">Client is not connected.</exception>
public ShellStream CreateShellStream(int bufferSize)
scott-xu marked this conversation as resolved.
Show resolved Hide resolved
{
EnsureSessionIsOpen();

return ServiceFactory.CreateShellStream(Session, bufferSize);
}

/// <summary>
/// Stops forwarded ports.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ public RemoteSshdConfig PrintMotd(bool? value = true)
return this;
}

/// <summary>
/// Specifies whether TTY is permitted.
/// </summary>
/// <param name="value"><see langword="true"/> to permit TTY.</param>
/// <returns>
/// The current <see cref="RemoteSshdConfig"/> instance.
/// </returns>
public RemoteSshdConfig PermitTTY(bool? value = true)
{
_config.PermitTTY = value;
return this;
}

/// <summary>
/// Specifies whether TCP forwarding is permitted.
/// </summary>
Expand Down
Loading