ShellStream.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.Text;
  8. using System.Text.RegularExpressions;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using Renci.SshNet.Abstractions;
  12. using Renci.SshNet.Channels;
  13. using Renci.SshNet.Common;
  14. namespace Renci.SshNet
  15. {
  16. /// <summary>
  17. /// Contains operation for working with SSH Shell.
  18. /// </summary>
  19. public class ShellStream : Stream
  20. {
  21. private const int DefaultBufferSize = 1024;
  22. private readonly ISession _session;
  23. private readonly Encoding _encoding;
  24. private readonly IChannelSession _channel;
  25. private readonly byte[] _carriageReturnBytes;
  26. private readonly byte[] _lineFeedBytes;
  27. private readonly object _sync = new object();
  28. private readonly byte[] _writeBuffer;
  29. private readonly bool _noTerminal;
  30. private int _writeLength; // The length of the data in _writeBuffer.
  31. private byte[] _readBuffer;
  32. private int _readHead; // The index from which the data starts in _readBuffer.
  33. private int _readTail; // The index at which to add new data into _readBuffer.
  34. private bool _disposed;
  35. /// <summary>
  36. /// Occurs when data was received.
  37. /// </summary>
  38. public event EventHandler<ShellDataEventArgs>? DataReceived;
  39. /// <summary>
  40. /// Occurs when an error occurred.
  41. /// </summary>
  42. public event EventHandler<ExceptionEventArgs>? ErrorOccurred;
  43. /// <summary>
  44. /// Occurs when the channel was closed.
  45. /// </summary>
  46. public event EventHandler<EventArgs>? Closed;
  47. /// <summary>
  48. /// Gets a value indicating whether data is available on the <see cref="ShellStream"/> to be read.
  49. /// </summary>
  50. /// <value>
  51. /// <see langword="true"/> if data is available to be read; otherwise, <see langword="false"/>.
  52. /// </value>
  53. public bool DataAvailable
  54. {
  55. get
  56. {
  57. lock (_sync)
  58. {
  59. AssertValid();
  60. return _readTail != _readHead;
  61. }
  62. }
  63. }
  64. [Conditional("DEBUG")]
  65. private void AssertValid()
  66. {
  67. Debug.Assert(Monitor.IsEntered(_sync), $"Should be in lock on {nameof(_sync)}");
  68. Debug.Assert(_readHead >= 0, $"{nameof(_readHead)} should be non-negative but is {_readHead.ToString(CultureInfo.InvariantCulture)}");
  69. Debug.Assert(_readTail >= 0, $"{nameof(_readTail)} should be non-negative but is {_readTail.ToString(CultureInfo.InvariantCulture)}");
  70. Debug.Assert(_readHead <= _readBuffer.Length, $"{nameof(_readHead)} should be <= {nameof(_readBuffer)}.Length but is {_readHead.ToString(CultureInfo.InvariantCulture)}");
  71. Debug.Assert(_readTail <= _readBuffer.Length, $"{nameof(_readTail)} should be <= {nameof(_readBuffer)}.Length but is {_readTail.ToString(CultureInfo.InvariantCulture)}");
  72. Debug.Assert(_readHead <= _readTail, $"Should have {nameof(_readHead)} <= {nameof(_readTail)} but have {_readHead.ToString(CultureInfo.InvariantCulture)} <= {_readTail.ToString(CultureInfo.InvariantCulture)}");
  73. }
  74. /// <summary>
  75. /// Initializes a new instance of the <see cref="ShellStream"/> class.
  76. /// </summary>
  77. /// <param name="session">The SSH session.</param>
  78. /// <param name="terminalName">The <c>TERM</c> environment variable.</param>
  79. /// <param name="columns">The terminal width in columns.</param>
  80. /// <param name="rows">The terminal width in rows.</param>
  81. /// <param name="width">The terminal width in pixels.</param>
  82. /// <param name="height">The terminal height in pixels.</param>
  83. /// <param name="terminalModeValues">The terminal mode values.</param>
  84. /// <param name="bufferSize">The size of the buffer.</param>
  85. /// <exception cref="SshException">The channel could not be opened.</exception>
  86. /// <exception cref="SshException">The pseudo-terminal request was not accepted by the server.</exception>
  87. /// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
  88. internal ShellStream(ISession session, string terminalName, uint columns, uint rows, uint width, uint height, IDictionary<TerminalModes, uint> terminalModeValues, int bufferSize)
  89. : this(session, bufferSize, noTerminal: false)
  90. {
  91. try
  92. {
  93. _channel.Open();
  94. if (!_channel.SendPseudoTerminalRequest(terminalName, columns, rows, width, height, terminalModeValues))
  95. {
  96. throw new SshException("The pseudo-terminal request was not accepted by the server. Consult the server log for more information.");
  97. }
  98. if (!_channel.SendShellRequest())
  99. {
  100. throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
  101. }
  102. }
  103. catch
  104. {
  105. Dispose();
  106. throw;
  107. }
  108. }
  109. /// <summary>
  110. /// Initializes a new instance of the <see cref="ShellStream"/> class.
  111. /// </summary>
  112. /// <param name="session">The SSH session.</param>
  113. /// <param name="bufferSize">The size of the buffer.</param>
  114. /// <exception cref="SshException">The channel could not be opened.</exception>
  115. /// <exception cref="SshException">The request to start a shell was not accepted by the server.</exception>
  116. internal ShellStream(ISession session, int bufferSize)
  117. : this(session, bufferSize, noTerminal: true)
  118. {
  119. try
  120. {
  121. _channel.Open();
  122. if (!_channel.SendShellRequest())
  123. {
  124. throw new SshException("The request to start a shell was not accepted by the server. Consult the server log for more information.");
  125. }
  126. }
  127. catch
  128. {
  129. Dispose();
  130. throw;
  131. }
  132. }
  133. /// <summary>
  134. /// Initializes a new instance of the <see cref="ShellStream"/> class.
  135. /// </summary>
  136. /// <param name="session">The SSH session.</param>
  137. /// <param name="bufferSize">The size of the buffer.</param>
  138. /// <param name="noTerminal">Disables pseudo terminal allocation or not.</param>
  139. /// <exception cref="SshException">The channel could not be opened.</exception>
  140. private ShellStream(ISession session, int bufferSize, bool noTerminal)
  141. {
  142. if (bufferSize == -1)
  143. {
  144. bufferSize = DefaultBufferSize;
  145. }
  146. #if NET8_0_OR_GREATER
  147. ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
  148. #else
  149. if (bufferSize <= 0)
  150. {
  151. throw new ArgumentOutOfRangeException(nameof(bufferSize));
  152. }
  153. #endif
  154. _encoding = session.ConnectionInfo.Encoding;
  155. _session = session;
  156. _carriageReturnBytes = _encoding.GetBytes("\r");
  157. _lineFeedBytes = _encoding.GetBytes("\n");
  158. _channel = _session.CreateChannelSession();
  159. _channel.DataReceived += Channel_DataReceived;
  160. _channel.Closed += Channel_Closed;
  161. _session.Disconnected += Session_Disconnected;
  162. _session.ErrorOccured += Session_ErrorOccured;
  163. _readBuffer = new byte[bufferSize];
  164. _writeBuffer = new byte[bufferSize];
  165. _noTerminal = noTerminal;
  166. }
  167. /// <summary>
  168. /// Gets a value indicating whether the current stream supports reading.
  169. /// </summary>
  170. /// <value>
  171. /// <see langword="true"/>.
  172. /// </value>
  173. /// <remarks>
  174. /// It is safe to read from <see cref="ShellStream"/> even after disposal.
  175. /// </remarks>
  176. public override bool CanRead
  177. {
  178. get { return true; }
  179. }
  180. /// <summary>
  181. /// Gets a value indicating whether the current stream supports seeking.
  182. /// </summary>
  183. /// <value>
  184. /// <see langword="false"/>.
  185. /// </value>
  186. public override bool CanSeek
  187. {
  188. get { return false; }
  189. }
  190. /// <summary>
  191. /// Gets a value indicating whether the current stream supports writing.
  192. /// </summary>
  193. /// <value>
  194. /// <see langword="true"/> if this stream has not been disposed and the underlying channel
  195. /// is still open, otherwise <see langword="false"/>.
  196. /// </value>
  197. /// <remarks>
  198. /// A value of <see langword="true"/> does not necessarily mean a write will succeed. It is possible
  199. /// that the channel is closed and/or the stream is disposed by another thread between a call to
  200. /// <see cref="CanWrite"/> and the call to write.
  201. /// </remarks>
  202. public override bool CanWrite
  203. {
  204. get { return !_disposed; }
  205. }
  206. /// <inheritdoc/>
  207. public override void Flush()
  208. {
  209. ThrowIfDisposed();
  210. Debug.Assert(_writeLength >= 0 && _writeLength <= _writeBuffer.Length);
  211. if (_writeLength > 0)
  212. {
  213. _channel.SendData(_writeBuffer, 0, _writeLength);
  214. _writeLength = 0;
  215. }
  216. }
  217. /// <summary>
  218. /// Gets the number of bytes currently available for reading.
  219. /// </summary>
  220. /// <value>A value representing the length of the stream in bytes.</value>
  221. public override long Length
  222. {
  223. get
  224. {
  225. lock (_sync)
  226. {
  227. AssertValid();
  228. return _readTail - _readHead;
  229. }
  230. }
  231. }
  232. /// <summary>
  233. /// This property always returns 0, and throws <see cref="NotSupportedException"/>
  234. /// when calling the setter.
  235. /// </summary>
  236. /// <value>
  237. /// 0.
  238. /// </value>
  239. /// <exception cref="NotSupportedException">The setter is called.</exception>
  240. #pragma warning disable SA1623 // The property's documentation should begin with 'Gets or sets'
  241. public override long Position
  242. #pragma warning restore SA1623 // The property's documentation should begin with 'Gets or sets'
  243. {
  244. get { return 0; }
  245. set { throw new NotSupportedException(); }
  246. }
  247. /// <summary>
  248. /// This method always throws <see cref="NotSupportedException"/>.
  249. /// </summary>
  250. /// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
  251. /// <param name="origin">A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
  252. /// <returns>Never.</returns>
  253. /// <exception cref="NotSupportedException">Always.</exception>
  254. public override long Seek(long offset, SeekOrigin origin)
  255. {
  256. throw new NotSupportedException();
  257. }
  258. /// <summary>
  259. /// This method always throws <see cref="NotSupportedException"/>.
  260. /// </summary>
  261. /// <param name="value">The desired length of the current stream in bytes.</param>
  262. /// <exception cref="NotSupportedException">Always.</exception>
  263. public override void SetLength(long value)
  264. {
  265. throw new NotSupportedException();
  266. }
  267. /// <summary>
  268. /// Expects the specified expression and performs action when one is found.
  269. /// </summary>
  270. /// <param name="expectActions">The expected expressions and actions to perform.</param>
  271. public void Expect(params ExpectAction[] expectActions)
  272. {
  273. Expect(Timeout.InfiniteTimeSpan, expectActions);
  274. }
  275. /// <summary>
  276. /// Expects the specified expression and performs action when one is found.
  277. /// </summary>
  278. /// <param name="timeout">Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  279. /// <param name="expectActions">The expected expressions and actions to perform, if the specified time elapsed and expected condition have not met, that method will exit without executing any action.</param>
  280. /// <remarks>
  281. /// If a TimeSpan representing -1 millisecond is specified for the <paramref name="timeout"/> parameter,
  282. /// this method blocks indefinitely until either the regex matches the data in the buffer, or the stream
  283. /// is closed (via disposal or via the underlying channel closing).
  284. /// </remarks>
  285. public void Expect(TimeSpan timeout, params ExpectAction[] expectActions)
  286. {
  287. _ = ExpectRegex(timeout, lookback: -1, expectActions);
  288. }
  289. /// <summary>
  290. /// Expects the specified expression and performs action when one is found.
  291. /// </summary>
  292. /// <param name="timeout">Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  293. /// <param name="lookback">The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.</param>
  294. /// <param name="expectActions">The expected expressions and actions to perform, if the specified time elapsed and expected condition have not met, that method will exit without executing any action.</param>
  295. /// <remarks>
  296. /// <para>
  297. /// If a TimeSpan representing -1 millisecond is specified for the <paramref name="timeout"/> parameter,
  298. /// this method blocks indefinitely until either the regex matches the data in the buffer, or the stream
  299. /// is closed (via disposal or via the underlying channel closing).
  300. /// </para>
  301. /// <para>
  302. /// Use the <paramref name="lookback"/> parameter to constrain the search space to a fixed-size rolling window at the end of the buffer.
  303. /// This can reduce the amount of work done in cases where lots of output from the shell is expected to be received before the matching expression is found.
  304. /// </para>
  305. /// <para>
  306. /// Note: in situations with high volumes of data and a small value for <paramref name="lookback"/>, some data may not be searched through.
  307. /// It is recommended to set <paramref name="lookback"/> to a large enough value to be able to search all data as it comes in,
  308. /// but which still places a limit on the amount of work needed.
  309. /// </para>
  310. /// </remarks>
  311. public void Expect(TimeSpan timeout, int lookback, params ExpectAction[] expectActions)
  312. {
  313. _ = ExpectRegex(timeout, lookback, expectActions);
  314. }
  315. /// <summary>
  316. /// Expects the expression specified by text.
  317. /// </summary>
  318. /// <param name="text">The text to expect.</param>
  319. /// <returns>
  320. /// The text available in the shell up to and including the expected text,
  321. /// or <see langword="null"/> if the the stream is closed without a match.
  322. /// </returns>
  323. public string? Expect(string text)
  324. {
  325. return Expect(text, Timeout.InfiniteTimeSpan);
  326. }
  327. /// <summary>
  328. /// Expects the expression specified by text.
  329. /// </summary>
  330. /// <param name="text">The text to expect.</param>
  331. /// <param name="timeout">Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  332. /// <param name="lookback">The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.</param>
  333. /// <returns>
  334. /// The text available in the shell up to and including the expected expression,
  335. /// or <see langword="null"/> if the specified time has elapsed or the stream is closed
  336. /// without a match.
  337. /// </returns>
  338. /// <remarks><inheritdoc cref="Expect(TimeSpan, int, ExpectAction[])"/></remarks>
  339. public string? Expect(string text, TimeSpan timeout, int lookback = -1)
  340. {
  341. ValidateTimeout(timeout);
  342. ValidateLookback(lookback);
  343. var timeoutTime = DateTime.Now.Add(timeout);
  344. var expectBytes = _encoding.GetBytes(text);
  345. lock (_sync)
  346. {
  347. while (true)
  348. {
  349. AssertValid();
  350. var searchHead = lookback == -1
  351. ? _readHead
  352. : Math.Max(_readTail - lookback, _readHead);
  353. Debug.Assert(_readHead <= searchHead && searchHead <= _readTail);
  354. var indexOfMatch = _readBuffer.AsSpan(searchHead, _readTail - searchHead).IndexOf(expectBytes);
  355. if (indexOfMatch >= 0)
  356. {
  357. var returnText = _encoding.GetString(_readBuffer, _readHead, searchHead - _readHead + indexOfMatch + expectBytes.Length);
  358. _readHead = searchHead + indexOfMatch + expectBytes.Length;
  359. AssertValid();
  360. return returnText;
  361. }
  362. if (_disposed)
  363. {
  364. return null;
  365. }
  366. if (timeout == Timeout.InfiniteTimeSpan)
  367. {
  368. _ = Monitor.Wait(_sync);
  369. }
  370. else
  371. {
  372. var waitTimeout = timeoutTime - DateTime.Now;
  373. if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
  374. {
  375. return null;
  376. }
  377. }
  378. }
  379. }
  380. }
  381. /// <summary>
  382. /// Expects the expression specified by regular expression.
  383. /// </summary>
  384. /// <param name="regex">The regular expression to expect.</param>
  385. /// <returns>
  386. /// The text available in the shell up to and including the expected expression,
  387. /// or <see langword="null"/> if the stream is closed without a match.
  388. /// </returns>
  389. public string? Expect(Regex regex)
  390. {
  391. return Expect(regex, Timeout.InfiniteTimeSpan);
  392. }
  393. /// <summary>
  394. /// Expects the expression specified by regular expression.
  395. /// </summary>
  396. /// <param name="regex">The regular expression to expect.</param>
  397. /// <param name="timeout">Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  398. /// <param name="lookback">The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.</param>
  399. /// <returns>
  400. /// The text available in the shell up to and including the expected expression,
  401. /// or <see langword="null"/> if the specified timeout has elapsed or the stream
  402. /// is closed without a match.
  403. /// </returns>
  404. /// <remarks>
  405. /// <inheritdoc cref="Expect(TimeSpan, int, ExpectAction[])"/>
  406. /// </remarks>
  407. public string? Expect(Regex regex, TimeSpan timeout, int lookback = -1)
  408. {
  409. return ExpectRegex(timeout, lookback, [new ExpectAction(regex, s => { })]);
  410. }
  411. private string? ExpectRegex(TimeSpan timeout, int lookback, ExpectAction[] expectActions)
  412. {
  413. ValidateTimeout(timeout);
  414. ValidateLookback(lookback);
  415. var timeoutTime = DateTime.Now.Add(timeout);
  416. lock (_sync)
  417. {
  418. while (true)
  419. {
  420. AssertValid();
  421. var bufferText = _encoding.GetString(_readBuffer, _readHead, _readTail - _readHead);
  422. var searchStart = lookback == -1
  423. ? 0
  424. : Math.Max(bufferText.Length - lookback, 0);
  425. foreach (var expectAction in expectActions)
  426. {
  427. #if NET7_0_OR_GREATER
  428. var matchEnumerator = expectAction.Expect.EnumerateMatches(bufferText.AsSpan(searchStart));
  429. if (matchEnumerator.MoveNext())
  430. {
  431. var match = matchEnumerator.Current;
  432. var returnText = bufferText.Substring(0, searchStart + match.Index + match.Length);
  433. #else
  434. var match = expectAction.Expect.Match(bufferText, searchStart);
  435. if (match.Success)
  436. {
  437. var returnText = bufferText.Substring(0, match.Index + match.Length);
  438. #endif
  439. _readHead += _encoding.GetByteCount(returnText);
  440. AssertValid();
  441. expectAction.Action(returnText);
  442. return returnText;
  443. }
  444. }
  445. if (_disposed)
  446. {
  447. return null;
  448. }
  449. if (timeout == Timeout.InfiniteTimeSpan)
  450. {
  451. Monitor.Wait(_sync);
  452. }
  453. else
  454. {
  455. var waitTimeout = timeoutTime - DateTime.Now;
  456. if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
  457. {
  458. return null;
  459. }
  460. }
  461. }
  462. }
  463. }
  464. /// <summary>
  465. /// Begins the expect.
  466. /// </summary>
  467. /// <param name="expectActions">The expect actions.</param>
  468. /// <returns>
  469. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  470. /// </returns>
  471. public IAsyncResult BeginExpect(params ExpectAction[] expectActions)
  472. {
  473. return BeginExpect(Timeout.InfiniteTimeSpan, callback: null, state: null, expectActions);
  474. }
  475. /// <summary>
  476. /// Begins the expect.
  477. /// </summary>
  478. /// <param name="callback">The callback.</param>
  479. /// <param name="expectActions">The expect actions.</param>
  480. /// <returns>
  481. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  482. /// </returns>
  483. public IAsyncResult BeginExpect(AsyncCallback? callback, params ExpectAction[] expectActions)
  484. {
  485. return BeginExpect(Timeout.InfiniteTimeSpan, callback, state: null, expectActions);
  486. }
  487. /// <summary>
  488. /// Begins the expect.
  489. /// </summary>
  490. /// <param name="callback">The callback.</param>
  491. /// <param name="state">The state.</param>
  492. /// <param name="expectActions">The expect actions.</param>
  493. /// <returns>
  494. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  495. /// </returns>
  496. public IAsyncResult BeginExpect(AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
  497. {
  498. return BeginExpect(Timeout.InfiniteTimeSpan, callback, state, expectActions);
  499. }
  500. /// <summary>
  501. /// Begins the expect.
  502. /// </summary>
  503. /// <param name="timeout">The timeout. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  504. /// <param name="callback">The callback.</param>
  505. /// <param name="state">The state.</param>
  506. /// <param name="expectActions">The expect actions.</param>
  507. /// <returns>
  508. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  509. /// </returns>
  510. public IAsyncResult BeginExpect(TimeSpan timeout, AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
  511. {
  512. return BeginExpect(timeout, lookback: -1, callback, state, expectActions);
  513. }
  514. /// <summary>
  515. /// Begins the expect.
  516. /// </summary>
  517. /// <param name="timeout">The timeout. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  518. /// <param name="lookback">The amount of data to search through from the most recent data in the buffer, or -1 to always search the entire buffer.</param>
  519. /// <param name="callback">The callback.</param>
  520. /// <param name="state">The state.</param>
  521. /// <param name="expectActions">The expect actions.</param>
  522. /// <returns>
  523. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  524. /// </returns>
  525. public IAsyncResult BeginExpect(TimeSpan timeout, int lookback, AsyncCallback? callback, object? state, params ExpectAction[] expectActions)
  526. {
  527. return TaskToAsyncResult.Begin(Task.Run(() => ExpectRegex(timeout, lookback, expectActions)), callback, state);
  528. }
  529. /// <summary>
  530. /// Ends the execute.
  531. /// </summary>
  532. /// <param name="asyncResult">The async result.</param>
  533. /// <returns>
  534. /// The text available in the shell up to and including the expected expression.
  535. /// </returns>
  536. public string? EndExpect(IAsyncResult asyncResult)
  537. {
  538. return TaskToAsyncResult.End<string?>(asyncResult);
  539. }
  540. /// <summary>
  541. /// Reads the next line from the shell. If a line is not available it will block and wait for a new line.
  542. /// </summary>
  543. /// <returns>
  544. /// The line read from the shell.
  545. /// </returns>
  546. /// <remarks>
  547. /// <para>
  548. /// This method blocks indefinitely until either a line is available in the buffer, or the stream is closed
  549. /// (via disposal or via the underlying channel closing).
  550. /// </para>
  551. /// <para>
  552. /// When the stream is closed and there are no more newlines in the buffer, this method returns the remaining data
  553. /// (if any) and then <see langword="null"/> indicating that no more data is in the buffer.
  554. /// </para>
  555. /// </remarks>
  556. public string? ReadLine()
  557. {
  558. return ReadLine(Timeout.InfiniteTimeSpan);
  559. }
  560. /// <summary>
  561. /// Reads a line from the shell. If line is not available it will block the execution and will wait for new line.
  562. /// </summary>
  563. /// <param name="timeout">Time to wait for input. Must non-negative or equal to -1 millisecond (for infinite timeout).</param>
  564. /// <returns>
  565. /// The line read from the shell, or <see langword="null"/> when no input is received for the specified timeout.
  566. /// </returns>
  567. /// <remarks>
  568. /// <para>
  569. /// If a TimeSpan representing -1 millisecond is specified for the <paramref name="timeout"/> parameter, this method
  570. /// blocks indefinitely until either a line is available in the buffer, or the stream is closed (via disposal or via
  571. /// the underlying channel closing).
  572. /// </para>
  573. /// <para>
  574. /// When the stream is closed and there are no more newlines in the buffer, this method returns the remaining data
  575. /// (if any) and then <see langword="null"/> indicating that no more data is in the buffer.
  576. /// </para>
  577. /// </remarks>
  578. public string? ReadLine(TimeSpan timeout)
  579. {
  580. ValidateTimeout(timeout);
  581. var timeoutTime = DateTime.Now.Add(timeout);
  582. lock (_sync)
  583. {
  584. while (true)
  585. {
  586. AssertValid();
  587. var indexOfCr = _readBuffer.AsSpan(_readHead, _readTail - _readHead).IndexOf(_carriageReturnBytes);
  588. if (indexOfCr >= 0)
  589. {
  590. // We have found \r. We only need to search for \n up to and just after the \r
  591. // (in order to consume \r\n if we can).
  592. var indexOfLf = indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length <= _readTail - _readHead
  593. ? _readBuffer.AsSpan(_readHead, indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length).IndexOf(_lineFeedBytes)
  594. : _readBuffer.AsSpan(_readHead, indexOfCr).IndexOf(_lineFeedBytes);
  595. if (indexOfLf >= 0 && indexOfLf < indexOfCr)
  596. {
  597. // If there is \n before the \r, then return up to the \n
  598. var returnText = _encoding.GetString(_readBuffer, _readHead, indexOfLf);
  599. _readHead += indexOfLf + _lineFeedBytes.Length;
  600. AssertValid();
  601. return returnText;
  602. }
  603. else if (indexOfLf == indexOfCr + _carriageReturnBytes.Length)
  604. {
  605. // If we have \r\n, then consume both
  606. var returnText = _encoding.GetString(_readBuffer, _readHead, indexOfCr);
  607. _readHead += indexOfCr + _carriageReturnBytes.Length + _lineFeedBytes.Length;
  608. AssertValid();
  609. return returnText;
  610. }
  611. else
  612. {
  613. // Return up to the \r
  614. var returnText = _encoding.GetString(_readBuffer, _readHead, indexOfCr);
  615. _readHead += indexOfCr + _carriageReturnBytes.Length;
  616. AssertValid();
  617. return returnText;
  618. }
  619. }
  620. else
  621. {
  622. // There is no \r. What about \n?
  623. var indexOfLf = _readBuffer.AsSpan(_readHead, _readTail - _readHead).IndexOf(_lineFeedBytes);
  624. if (indexOfLf >= 0)
  625. {
  626. var returnText = _encoding.GetString(_readBuffer, _readHead, indexOfLf);
  627. _readHead += indexOfLf + _lineFeedBytes.Length;
  628. AssertValid();
  629. return returnText;
  630. }
  631. }
  632. if (_disposed)
  633. {
  634. var lastLine = _readHead == _readTail
  635. ? null
  636. : _encoding.GetString(_readBuffer, _readHead, _readTail - _readHead);
  637. _readHead = _readTail = 0;
  638. return lastLine;
  639. }
  640. if (timeout == Timeout.InfiniteTimeSpan)
  641. {
  642. _ = Monitor.Wait(_sync);
  643. }
  644. else
  645. {
  646. var waitTimeout = timeoutTime - DateTime.Now;
  647. if (waitTimeout < TimeSpan.Zero || !Monitor.Wait(_sync, waitTimeout))
  648. {
  649. return null;
  650. }
  651. }
  652. }
  653. }
  654. }
  655. private static void ValidateTimeout(TimeSpan timeout)
  656. {
  657. if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
  658. {
  659. throw new ArgumentOutOfRangeException(nameof(timeout), "Value must be non-negative or equal to -1 millisecond (for infinite timeout)");
  660. }
  661. }
  662. private static void ValidateLookback(int lookback)
  663. {
  664. if (lookback is <= 0 and not -1)
  665. {
  666. throw new ArgumentOutOfRangeException(nameof(lookback), "Value must be positive or equal to -1 (for no window)");
  667. }
  668. }
  669. private void ThrowIfDisposed()
  670. {
  671. #if NET7_0_OR_GREATER
  672. ObjectDisposedException.ThrowIf(_disposed, this);
  673. #else
  674. if (_disposed)
  675. {
  676. throw new ObjectDisposedException(GetType().FullName);
  677. }
  678. #endif // NET7_0_OR_GREATER
  679. }
  680. /// <summary>
  681. /// Reads all of the text currently available in the shell.
  682. /// </summary>
  683. /// <returns>
  684. /// The text available in the shell.
  685. /// </returns>
  686. public string Read()
  687. {
  688. lock (_sync)
  689. {
  690. AssertValid();
  691. var text = _encoding.GetString(_readBuffer, _readHead, _readTail - _readHead);
  692. _readHead = _readTail = 0;
  693. return text;
  694. }
  695. }
  696. /// <inheritdoc/>
  697. public override int Read(byte[] buffer, int offset, int count)
  698. {
  699. lock (_sync)
  700. {
  701. while (_readHead == _readTail && !_disposed)
  702. {
  703. _ = Monitor.Wait(_sync);
  704. }
  705. AssertValid();
  706. var bytesRead = Math.Min(count, _readTail - _readHead);
  707. Buffer.BlockCopy(_readBuffer, _readHead, buffer, offset, bytesRead);
  708. _readHead += bytesRead;
  709. AssertValid();
  710. return bytesRead;
  711. }
  712. }
  713. /// <summary>
  714. /// Writes the specified text to the shell.
  715. /// </summary>
  716. /// <param name="text">The text to be written to the shell.</param>
  717. /// <remarks>
  718. /// If <paramref name="text"/> is <see langword="null"/>, nothing is written.
  719. /// Otherwise, <see cref="Flush"/> is called after writing the data to the buffer.
  720. /// </remarks>
  721. /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
  722. public void Write(string? text)
  723. {
  724. if (text is null)
  725. {
  726. return;
  727. }
  728. var data = _encoding.GetBytes(text);
  729. Write(data, 0, data.Length);
  730. Flush();
  731. }
  732. /// <inheritdoc/>
  733. public override void Write(byte[] buffer, int offset, int count)
  734. {
  735. ThrowIfDisposed();
  736. while (count > 0)
  737. {
  738. if (_writeLength == _writeBuffer.Length)
  739. {
  740. Flush();
  741. }
  742. var bytesToCopy = Math.Min(count, _writeBuffer.Length - _writeLength);
  743. Buffer.BlockCopy(buffer, offset, _writeBuffer, _writeLength, bytesToCopy);
  744. offset += bytesToCopy;
  745. count -= bytesToCopy;
  746. _writeLength += bytesToCopy;
  747. Debug.Assert(_writeLength >= 0 && _writeLength <= _writeBuffer.Length);
  748. }
  749. }
  750. /// <summary>
  751. /// Writes the line to the shell.
  752. /// </summary>
  753. /// <param name="line">The line to be written to the shell.</param>
  754. /// <remarks>
  755. /// If <paramref name="line"/> is <see langword="null"/>, only the line terminator is written.
  756. /// <see cref="Flush"/> is called once the data is written.
  757. /// </remarks>
  758. /// <exception cref="ObjectDisposedException">The stream is closed.</exception>
  759. public void WriteLine(string line)
  760. {
  761. // By default, the terminal driver translates carriage return to line feed on input.
  762. // See option ICRLF at https://www.man7.org/linux/man-pages/man3/termios.3.html.
  763. Write(line + (_noTerminal ? "\n" : "\r"));
  764. }
  765. /// <inheritdoc/>
  766. protected override void Dispose(bool disposing)
  767. {
  768. if (!disposing)
  769. {
  770. base.Dispose(disposing);
  771. return;
  772. }
  773. lock (_sync)
  774. {
  775. if (_disposed)
  776. {
  777. return;
  778. }
  779. _disposed = true;
  780. // Do not dispose _session (we don't own it)
  781. _session.Disconnected -= Session_Disconnected;
  782. _session.ErrorOccured -= Session_ErrorOccured;
  783. // But we do own _channel
  784. _channel.DataReceived -= Channel_DataReceived;
  785. _channel.Closed -= Channel_Closed;
  786. _channel.Dispose();
  787. Monitor.PulseAll(_sync);
  788. }
  789. base.Dispose(disposing);
  790. }
  791. private void Session_ErrorOccured(object? sender, ExceptionEventArgs e)
  792. {
  793. ErrorOccurred?.Invoke(this, e);
  794. }
  795. private void Session_Disconnected(object? sender, EventArgs e)
  796. {
  797. Dispose();
  798. }
  799. private void Channel_Closed(object? sender, ChannelEventArgs e)
  800. {
  801. Dispose();
  802. if (Closed != null)
  803. {
  804. // Handle event on different thread
  805. ThreadAbstraction.ExecuteThread(() => Closed?.Invoke(this, EventArgs.Empty));
  806. }
  807. }
  808. private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
  809. {
  810. lock (_sync)
  811. {
  812. AssertValid();
  813. // Ensure sufficient buffer space and copy the new data in.
  814. if (_readBuffer.Length - _readTail >= e.Data.Length)
  815. {
  816. // If there is enough space after _tail for the new data,
  817. // then copy the data there.
  818. Buffer.BlockCopy(e.Data, 0, _readBuffer, _readTail, e.Data.Length);
  819. _readTail += e.Data.Length;
  820. }
  821. else
  822. {
  823. // We can't fit the new data after _tail.
  824. var newLength = _readTail - _readHead + e.Data.Length;
  825. if (newLength <= _readBuffer.Length)
  826. {
  827. // If there is sufficient space at the start of the buffer,
  828. // then move the current data to the start of the buffer.
  829. Buffer.BlockCopy(_readBuffer, _readHead, _readBuffer, 0, _readTail - _readHead);
  830. }
  831. else
  832. {
  833. // Otherwise, we're gonna need a bigger buffer.
  834. var newBuffer = new byte[Math.Max(newLength, _readBuffer.Length * 2)];
  835. Buffer.BlockCopy(_readBuffer, _readHead, newBuffer, 0, _readTail - _readHead);
  836. _readBuffer = newBuffer;
  837. }
  838. // Copy the new data into the freed-up space.
  839. Buffer.BlockCopy(e.Data, 0, _readBuffer, _readTail - _readHead, e.Data.Length);
  840. _readHead = 0;
  841. _readTail = newLength;
  842. }
  843. AssertValid();
  844. Monitor.PulseAll(_sync);
  845. }
  846. DataReceived?.Invoke(this, new ShellDataEventArgs(e.Data));
  847. }
  848. }
  849. }