Skip to content

Commit 9dc7db9

Browse files
scott-xuRob-Hague
andauthored
Support creating Shell(Stream) without PTY (#1419)
* Support creating Shell(Stream) without PTY Fixes #1418 * Add integration test for "PermitTTY no" * Fix Integration Test * Remove duplicate shell request * Put common operations in a shared constructor. Update xml doc comments. * Update comments and method overriding * Update per code review * Update integration tests * Renaming * Make `bufferSize` optional * Try fix the test * Update per code review * try agian * try again * docs * doc --------- Co-authored-by: Rob Hague <[email protected]>
1 parent 830e504 commit 9dc7db9

File tree

9 files changed

+397
-26
lines changed

9 files changed

+397
-26
lines changed

src/Renci.SshNet/IServiceFactory.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ ShellStream CreateShellStream(ISession session,
137137
IDictionary<TerminalModes, uint> terminalModeValues,
138138
int bufferSize);
139139

140+
/// <summary>
141+
/// Creates a shell stream without allocating a pseudo terminal.
142+
/// </summary>
143+
/// <param name="session">The SSH session.</param>
144+
/// <param name="bufferSize">Size of the buffer.</param>
145+
/// <returns>
146+
/// The created <see cref="ShellStream"/> instance.
147+
/// </returns>
148+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
149+
ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize);
150+
140151
/// <summary>
141152
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
142153
/// any embedded double quote with a backslash.

src/Renci.SshNet/ServiceFactory.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ public ShellStream CreateShellStream(ISession session, string terminalName, uint
206206
return new ShellStream(session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
207207
}
208208

209+
/// <inheritdoc/>
210+
public ShellStream CreateShellStreamNoTerminal(ISession session, int bufferSize)
211+
{
212+
return new ShellStream(session, bufferSize);
213+
}
214+
209215
/// <summary>
210216
/// Creates an <see cref="IRemotePathTransformation"/> that encloses a path in double quotes, and escapes
211217
/// any embedded double quote with a backslash.

src/Renci.SshNet/Shell.cs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Renci.SshNet
1414
/// </summary>
1515
public class Shell : IDisposable
1616
{
17+
private const int DefaultBufferSize = 1024;
18+
1719
private readonly ISession _session;
1820
private readonly string _terminalName;
1921
private readonly uint _columns;
@@ -24,6 +26,7 @@ public class Shell : IDisposable
2426
private readonly Stream _outputStream;
2527
private readonly Stream _extendedOutputStream;
2628
private readonly int _bufferSize;
29+
private readonly bool _noTerminal;
2730
private ManualResetEvent _dataReaderTaskCompleted;
2831
private IChannelSession _channel;
2932
private AutoResetEvent _channelClosedWaitHandle;
@@ -77,24 +80,66 @@ public class Shell : IDisposable
7780
/// <param name="terminalModes">The terminal modes.</param>
7881
/// <param name="bufferSize">Size of the buffer for output stream.</param>
7982
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)
83+
: this(session, input, output, extendedOutput, bufferSize, noTerminal: false)
8084
{
81-
_session = session;
82-
_input = input;
83-
_outputStream = output;
84-
_extendedOutputStream = extendedOutput;
8585
_terminalName = terminalName;
8686
_columns = columns;
8787
_rows = rows;
8888
_width = width;
8989
_height = height;
9090
_terminalModes = terminalModes;
91+
}
92+
93+
/// <summary>
94+
/// Initializes a new instance of the <see cref="Shell"/> class.
95+
/// </summary>
96+
/// <param name="session">The session.</param>
97+
/// <param name="input">The input.</param>
98+
/// <param name="output">The output.</param>
99+
/// <param name="extendedOutput">The extended output.</param>
100+
/// <param name="bufferSize">Size of the buffer for output stream.</param>
101+
internal Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize)
102+
: this(session, input, output, extendedOutput, bufferSize, noTerminal: true)
103+
{
104+
}
105+
106+
/// <summary>
107+
/// Initializes a new instance of the <see cref="Shell"/> class.
108+
/// </summary>
109+
/// <param name="session">The session.</param>
110+
/// <param name="input">The input.</param>
111+
/// <param name="output">The output.</param>
112+
/// <param name="extendedOutput">The extended output.</param>
113+
/// <param name="bufferSize">Size of the buffer for output stream.</param>
114+
/// <param name="noTerminal">Disables pseudo terminal allocation or not.</param>
115+
private Shell(ISession session, Stream input, Stream output, Stream extendedOutput, int bufferSize, bool noTerminal)
116+
{
117+
if (bufferSize == -1)
118+
{
119+
bufferSize = DefaultBufferSize;
120+
}
121+
#if NET8_0_OR_GREATER
122+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
123+
#else
124+
if (bufferSize <= 0)
125+
{
126+
throw new ArgumentOutOfRangeException(nameof(bufferSize));
127+
}
128+
#endif
129+
_session = session;
130+
_input = input;
131+
_outputStream = output;
132+
_extendedOutputStream = extendedOutput;
91133
_bufferSize = bufferSize;
134+
_noTerminal = noTerminal;
92135
}
93136

94137
/// <summary>
95138
/// Starts this shell.
96139
/// </summary>
97140
/// <exception cref="SshException">Shell is started.</exception>
141+
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
142+
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
98143
public void Start()
99144
{
100145
if (IsStarted)
@@ -112,8 +157,18 @@ public void Start()
112157
_session.ErrorOccured += Session_ErrorOccured;
113158

114159
_channel.Open();
115-
_ = _channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes);
116-
_ = _channel.SendShellRequest();
160+
if (!_noTerminal)
161+
{
162+
if (!_channel.SendPseudoTerminalRequest(_terminalName, _columns, _rows, _width, _height, _terminalModes))
163+
{
164+
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
165+
}
166+
}
167+
168+
if (!_channel.SendShellRequest())
169+
{
170+
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
171+
}
117172

118173
_channelClosedWaitHandle = new AutoResetEvent(initialState: false);
119174

src/Renci.SshNet/ShellStream.cs

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ namespace Renci.SshNet
2020
/// </summary>
2121
public class ShellStream : Stream
2222
{
23+
private const int DefaultBufferSize = 1024;
24+
2325
private readonly ISession _session;
2426
private readonly Encoding _encoding;
2527
private readonly IChannelSession _channel;
@@ -29,6 +31,7 @@ public class ShellStream : Stream
2931
private readonly object _sync = new object();
3032

3133
private readonly byte[] _writeBuffer;
34+
private readonly bool _noTerminal;
3235
private int _writeLength; // The length of the data in _writeBuffer.
3336

3437
private byte[] _readBuffer;
@@ -95,7 +98,68 @@ private void AssertValid()
9598
/// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
9699
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
97100
internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModeValues, int bufferSize)
101+
: this(session, bufferSize, noTerminal: false)
102+
{
103+
try
104+
{
105+
_channel.Open();
106+
107+
if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
108+
{
109+
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
110+
}
111+
112+
if (!_channel.SendShellRequest())
113+
{
114+
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
115+
}
116+
}
117+
catch
118+
{
119+
Dispose();
120+
throw;
121+
}
122+
}
123+
124+
/// <summary>
125+
/// Initializes a new instance of the <see cref="ShellStream"/> class.
126+
/// </summary>
127+
/// <param name="session">The SSH session.</param>
128+
/// <param name="bufferSize">The size of the buffer.</param>
129+
/// <exception cref="SshException">The channel could not be opened.</exception>
130+
/// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
131+
internal ShellStream(ISession session, int bufferSize)
132+
: this(session, bufferSize, noTerminal: true)
133+
{
134+
try
135+
{
136+
_channel.Open();
137+
138+
if (!_channel.SendShellRequest())
139+
{
140+
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
141+
}
142+
}
143+
catch
144+
{
145+
Dispose();
146+
throw;
147+
}
148+
}
149+
150+
/// <summary>
151+
/// Initializes a new instance of the <see cref="ShellStream"/> class.
152+
/// </summary>
153+
/// <param name="session">The SSH session.</param>
154+
/// <param name="bufferSize">The size of the buffer.</param>
155+
/// <param name="noTerminal">Disables pseudo terminal allocation or not.</param>
156+
/// <exception cref="SshException">The channel could not be opened.</exception>
157+
private ShellStream(ISession session, int bufferSize, bool noTerminal)
98158
{
159+
if (bufferSize == -1)
160+
{
161+
bufferSize = DefaultBufferSize;
162+
}
99163
#if NET8_0_OR_GREATER
100164
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
101165
#else
@@ -119,25 +183,7 @@ internal ShellStream(ISession session, string terminalName, uint columns, uint r
119183
_readBuffer = new byte[bufferSize];
120184
_writeBuffer = new byte[bufferSize];
121185

122-
try
123-
{
124-
_channel.Open();
125-
126-
if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
127-
{
128-
throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
129-
}
130-
131-
if (!_channel.SendShellRequest())
132-
{
133-
throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
134-
}
135-
}
136-
catch
137-
{
138-
Dispose();
139-
throw;
140-
}
186+
_noTerminal = noTerminal;
141187
}
142188

143189
/// <summary>
@@ -848,7 +894,9 @@ public override void Write(byte[] buffer, int offset, int count)
848894
/// <exception cref="ObjectDisposedException">The stream is closed.</exception>
849895
public void WriteLine(string line)
850896
{
851-
Write(line + "\r");
897+
// By default, the terminal driver translates carriage return to line feed on input.
898+
// See option ICRLF at https://www.man7.org/linux/man-pages/man3/termios.3.html.
899+
Write(line + (_noTerminal ? "\n" : "\r"));
852900
}
853901

854902
/// <inheritdoc/>

src/Renci.SshNet/SshClient.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,25 @@ public Shell CreateShell(Encoding encoding, string input, Stream output, Stream
391391
return CreateShell(encoding, input, output, extendedOutput, string.Empty, 0, 0, 0, 0, terminalModes: null, 1024);
392392
}
393393

394+
/// <summary>
395+
/// Creates the shell without allocating a pseudo terminal,
396+
/// similar to the <c>ssh -T</c> option.
397+
/// </summary>
398+
/// <param name="input">The input.</param>
399+
/// <param name="output">The output.</param>
400+
/// <param name="extendedOutput">The extended output.</param>
401+
/// <param name="bufferSize">Size of the internal read buffer.</param>
402+
/// <returns>
403+
/// Returns a representation of a <see cref="Shell" /> object.
404+
/// </returns>
405+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
406+
public Shell CreateShellNoTerminal(Stream input, Stream output, Stream extendedOutput, int bufferSize = -1)
407+
{
408+
EnsureSessionIsOpen();
409+
410+
return new Shell(Session, input, output, extendedOutput, bufferSize);
411+
}
412+
394413
/// <summary>
395414
/// Creates the shell stream.
396415
/// </summary>
@@ -450,6 +469,22 @@ public ShellStream CreateShellStream(string terminalName, uint columns, uint row
450469
return ServiceFactory.CreateShellStream(Session, terminalName, columns, rows, width, height, terminalModeValues, bufferSize);
451470
}
452471

472+
/// <summary>
473+
/// Creates the shell stream without allocating a pseudo terminal,
474+
/// similar to the <c>ssh -T</c> option.
475+
/// </summary>
476+
/// <param name="bufferSize">The size of the buffer.</param>
477+
/// <returns>
478+
/// The created <see cref="ShellStream"/> instance.
479+
/// </returns>
480+
/// <exception cref="SshConnectionException">Client is not connected.</exception>
481+
public ShellStream CreateShellStreamNoTerminal(int bufferSize = -1)
482+
{
483+
EnsureSessionIsOpen();
484+
485+
return ServiceFactory.CreateShellStreamNoTerminal(Session, bufferSize);
486+
}
487+
453488
/// <summary>
454489
/// Stops forwarded ports.
455490
/// </summary>

test/Renci.SshNet.IntegrationTests/RemoteSshdConfig.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ public RemoteSshdConfig PrintMotd(bool? value = true)
6969
return this;
7070
}
7171

72+
/// <summary>
73+
/// Specifies whether TTY is permitted.
74+
/// </summary>
75+
/// <param name="value"><see langword="true"/> to permit TTY.</param>
76+
/// <returns>
77+
/// The current <see cref="RemoteSshdConfig"/> instance.
78+
/// </returns>
79+
public RemoteSshdConfig PermitTTY(bool? value = true)
80+
{
81+
_config.PermitTTY = value;
82+
return this;
83+
}
84+
7285
/// <summary>
7386
/// Specifies whether TCP forwarding is permitted.
7487
/// </summary>

0 commit comments

Comments
 (0)