Skip to content

Support creating Shell(Stream) without PTY #1419

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

Merged
merged 17 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 without allocating a pseudo terminal.
/// </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 CreateShellStreamNoTerminal(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
6 changes: 6 additions & 0 deletions src/Renci.SshNet/ServiceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ public ShellStream CreateShellStream(ISession session, string terminalName, uint
return new ShellStream(session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
}

/// <inheritdoc/>
public ShellStream CreateShellStreamNoTerminal(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
67 changes: 61 additions & 6 deletions src/Renci.SshNet/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Renci.SshNet
/// </summary>
public class Shell : IDisposable
{
private const int DefaultBufferSize = 1024;

private readonly ISession _session;
private readonly string _terminalName;
private readonly uint _columns;
Expand All @@ -24,6 +26,7 @@ public class Shell : IDisposable
private readonly Stream _outputStream;
private readonly Stream _extendedOutputStream;
private readonly int _bufferSize;
private readonly bool _noTerminal;
private ManualResetEvent _dataReaderTaskCompleted;
private IChannelSession _channel;
private AutoResetEvent _channelClosedWaitHandle;
Expand Down Expand Up @@ -77,24 +80,66 @@ 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, noTerminal: 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, noTerminal: 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="noTerminal">Disables pseudo terminal allocation or not.</param>
private Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize, bool noTerminal)
{
if (bufferSize == -1)
{
bufferSize = DefaultBufferSize;
}
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
#else
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
#endif
_session = session;
_input = input;
_outputStream = output;
_extendedOutputStream = extendedOutput;
_bufferSize = bufferSize;
_noTerminal = noTerminal;
}

/// <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 +157,18 @@ public void Start()
_session.ErrorOccured += Session_ErrorOccured;

_channel.Open();
_ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes);
_ = _channel.SendShellRequest();
if (!_noTerminal)
{
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
88 changes: 68 additions & 20 deletions src/Renci.SshNet/ShellStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace Renci.SshNet
/// </summary>
public class ShellStream : Stream
{
private const int DefaultBufferSize = 1024;

private readonly ISession _session;
private readonly Encoding _encoding;
private readonly IChannelSession _channel;
Expand All @@ -29,6 +31,7 @@ public class ShellStream : Stream
private readonly object _sync = new object();

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

private byte[] _readBuffer;
Expand Down Expand Up @@ -95,7 +98,68 @@ 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, 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;
}
}

/// <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, 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;
}
}

/// <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="noTerminal">Disables pseudo terminal allocation or not.</param>
/// <exception cref="SshException">The channel could not be opened.</exception>
private ShellStream(ISession session, int bufferSize, bool noTerminal)
{
if (bufferSize == -1)
{
bufferSize = DefaultBufferSize;
}
#if NET8_0_OR_GREATER
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
#else
Expand All @@ -119,25 +183,7 @@ internal ShellStream(ISession session, string terminalName, uint columns, uint r
_readBuffer = new byte[bufferSize];
_writeBuffer = new byte[bufferSize];

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;
}
_noTerminal = noTerminal;
}

/// <summary>
Expand Down Expand Up @@ -848,7 +894,9 @@ 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");
// 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"));
}

/// <inheritdoc/>
Expand Down
35 changes: 35 additions & 0 deletions src/Renci.SshNet/SshClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,25 @@ public Shell CreateShell(Encoding encoding, string input, Stream output, Stream
return CreateShell(encoding, input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
}

/// <summary>
/// Creates the shell without allocating a pseudo terminal,
/// similar to the <c>ssh -T</c> option.
/// </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 CreateShellNoTerminal(Stream input, Stream output, Stream extendedOutput, int bufferSize = -1)
{
EnsureSessionIsOpen();

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

/// <summary>
/// Creates the shell stream.
/// </summary>
Expand Down Expand Up @@ -450,6 +469,22 @@ 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 without allocating a pseudo terminal,
/// similar to the <c>ssh -T</c> option.
/// </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 CreateShellStreamNoTerminal(int bufferSize = -1)
{
EnsureSessionIsOpen();

return ServiceFactory.CreateShellStreamNoTerminal(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