SshCommand.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. #nullable enable
  2. using System;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Renci.SshNet.Channels;
  9. using Renci.SshNet.Common;
  10. using Renci.SshNet.Messages.Connection;
  11. using Renci.SshNet.Messages.Transport;
  12. namespace Renci.SshNet
  13. {
  14. /// <summary>
  15. /// Represents an SSH command that can be executed.
  16. /// </summary>
  17. public class SshCommand : IDisposable
  18. {
  19. private readonly ISession _session;
  20. private readonly Encoding _encoding;
  21. private IChannelSession _channel;
  22. private TaskCompletionSource<object>? _tcs;
  23. private CancellationTokenSource? _cts;
  24. private CancellationTokenRegistration _tokenRegistration;
  25. private string? _stdOut;
  26. private string? _stdErr;
  27. private bool _hasError;
  28. private bool _isDisposed;
  29. private ChannelInputStream? _inputStream;
  30. private TimeSpan _commandTimeout;
  31. /// <summary>
  32. /// The token supplied as an argument to <see cref="ExecuteAsync(CancellationToken)"/>.
  33. /// </summary>
  34. private CancellationToken _userToken;
  35. /// <summary>
  36. /// Whether <see cref="CancelAsync(bool, int)"/> has been called
  37. /// (either by a token or manually).
  38. /// </summary>
  39. private bool _cancellationRequested;
  40. private int _exitStatus;
  41. private volatile bool _haveExitStatus; // volatile to prevent re-ordering of reads/writes of _exitStatus.
  42. /// <summary>
  43. /// Gets the command text.
  44. /// </summary>
  45. public string CommandText { get; private set; }
  46. /// <summary>
  47. /// Gets or sets the command timeout.
  48. /// </summary>
  49. /// <value>
  50. /// The command timeout.
  51. /// </value>
  52. public TimeSpan CommandTimeout
  53. {
  54. get
  55. {
  56. return _commandTimeout;
  57. }
  58. set
  59. {
  60. value.EnsureValidTimeout(nameof(CommandTimeout));
  61. _commandTimeout = value;
  62. }
  63. }
  64. /// <summary>
  65. /// Gets the number representing the exit status of the command, if applicable,
  66. /// otherwise <see langword="null"/>.
  67. /// </summary>
  68. /// <remarks>
  69. /// The value is not <see langword="null"/> when an exit status code has been returned
  70. /// from the server. If the command terminated due to a signal, <see cref="ExitSignal"/>
  71. /// may be not <see langword="null"/> instead.
  72. /// </remarks>
  73. /// <seealso cref="ExitSignal"/>
  74. public int? ExitStatus
  75. {
  76. get
  77. {
  78. return _haveExitStatus ? _exitStatus : null;
  79. }
  80. }
  81. /// <summary>
  82. /// Gets the name of the signal due to which the command
  83. /// terminated violently, if applicable, otherwise <see langword="null"/>.
  84. /// </summary>
  85. /// <remarks>
  86. /// The value (if it exists) is supplied by the server and is usually one of the
  87. /// following, as described in https://datatracker.ietf.org/doc/html/rfc4254#section-6.10:
  88. /// ABRT, ALRM, FPE, HUP, ILL, INT, KILL, PIPE, QUIT, SEGV, TER, USR1, USR2.
  89. /// </remarks>
  90. public string? ExitSignal { get; private set; }
  91. /// <summary>
  92. /// Gets the output stream.
  93. /// </summary>
  94. public Stream OutputStream { get; private set; }
  95. /// <summary>
  96. /// Gets the extended output stream.
  97. /// </summary>
  98. public Stream ExtendedOutputStream { get; private set; }
  99. /// <summary>
  100. /// Creates and returns the input stream for the command.
  101. /// </summary>
  102. /// <returns>
  103. /// The stream that can be used to transfer data to the command's input stream.
  104. /// </returns>
  105. /// <remarks>
  106. /// Callers should ensure that <see cref="Stream.Dispose()"/> is called on the
  107. /// returned instance in order to notify the command that no more data will be sent.
  108. /// Failure to do so may result in the command executing indefinitely.
  109. /// </remarks>
  110. /// <example>
  111. /// This example shows how to stream some data to 'cat' and have the server echo it back.
  112. /// <code>
  113. /// using (SshCommand command = mySshClient.CreateCommand("cat"))
  114. /// {
  115. /// Task executeTask = command.ExecuteAsync(CancellationToken.None);
  116. ///
  117. /// using (Stream inputStream = command.CreateInputStream())
  118. /// {
  119. /// inputStream.Write("Hello World!"u8);
  120. /// }
  121. ///
  122. /// await executeTask;
  123. ///
  124. /// Console.WriteLine(command.ExitStatus); // 0
  125. /// Console.WriteLine(command.Result); // "Hello World!"
  126. /// }
  127. /// </code>
  128. /// </example>
  129. public Stream CreateInputStream()
  130. {
  131. if (!_channel.IsOpen)
  132. {
  133. throw new InvalidOperationException("The input stream can be used only during execution.");
  134. }
  135. if (_inputStream != null)
  136. {
  137. throw new InvalidOperationException("The input stream already exists.");
  138. }
  139. _inputStream = new ChannelInputStream(_channel);
  140. return _inputStream;
  141. }
  142. /// <summary>
  143. /// Gets the standard output of the command by reading <see cref="OutputStream"/>.
  144. /// </summary>
  145. public string Result
  146. {
  147. get
  148. {
  149. if (_stdOut is not null)
  150. {
  151. return _stdOut;
  152. }
  153. if (_tcs is null)
  154. {
  155. return string.Empty;
  156. }
  157. using (var sr = new StreamReader(OutputStream, _encoding))
  158. {
  159. return _stdOut = sr.ReadToEnd();
  160. }
  161. }
  162. }
  163. /// <summary>
  164. /// Gets the standard error of the command by reading <see cref="ExtendedOutputStream"/>,
  165. /// when extended data has been sent which has been marked as stderr.
  166. /// </summary>
  167. public string Error
  168. {
  169. get
  170. {
  171. if (_stdErr is not null)
  172. {
  173. return _stdErr;
  174. }
  175. if (_tcs is null || !_hasError)
  176. {
  177. return string.Empty;
  178. }
  179. using (var sr = new StreamReader(ExtendedOutputStream, _encoding))
  180. {
  181. return _stdErr = sr.ReadToEnd();
  182. }
  183. }
  184. }
  185. /// <summary>
  186. /// Initializes a new instance of the <see cref="SshCommand"/> class.
  187. /// </summary>
  188. /// <param name="session">The session.</param>
  189. /// <param name="commandText">The command text.</param>
  190. /// <param name="encoding">The encoding to use for the results.</param>
  191. /// <exception cref="ArgumentNullException">Either <paramref name="session"/>, <paramref name="commandText"/> is <see langword="null"/>.</exception>
  192. internal SshCommand(ISession session, string commandText, Encoding encoding)
  193. {
  194. ThrowHelper.ThrowIfNull(session);
  195. ThrowHelper.ThrowIfNull(commandText);
  196. ThrowHelper.ThrowIfNull(encoding);
  197. _session = session;
  198. CommandText = commandText;
  199. _encoding = encoding;
  200. CommandTimeout = Timeout.InfiniteTimeSpan;
  201. OutputStream = new PipeStream();
  202. ExtendedOutputStream = new PipeStream();
  203. _session.Disconnected += Session_Disconnected;
  204. _session.ErrorOccured += Session_ErrorOccured;
  205. _channel = _session.CreateChannelSession();
  206. }
  207. /// <summary>
  208. /// Executes the command asynchronously.
  209. /// </summary>
  210. /// <param name="cancellationToken">
  211. /// The <see cref="CancellationToken"/>. When triggered, attempts to terminate the
  212. /// remote command by sending a signal.
  213. /// </param>
  214. /// <returns>A <see cref="Task"/> representing the lifetime of the command.</returns>
  215. /// <exception cref="InvalidOperationException">Command is already executing. Thrown synchronously.</exception>
  216. /// <exception cref="ObjectDisposedException">Instance has been disposed. Thrown synchronously.</exception>
  217. /// <exception cref="OperationCanceledException">The <see cref="Task"/> has been cancelled.</exception>
  218. /// <exception cref="SshOperationTimeoutException">The command timed out according to <see cref="CommandTimeout"/>.</exception>
  219. #pragma warning disable CA1849 // Call async methods when in an async method; PipeStream.DisposeAsync would complete synchronously anyway.
  220. public Task ExecuteAsync(CancellationToken cancellationToken = default)
  221. {
  222. ThrowHelper.ThrowObjectDisposedIf(_isDisposed, this);
  223. if (cancellationToken.IsCancellationRequested)
  224. {
  225. return Task.FromCanceled(cancellationToken);
  226. }
  227. if (_tcs is not null)
  228. {
  229. if (!_tcs.Task.IsCompleted)
  230. {
  231. throw new InvalidOperationException("Asynchronous operation is already in progress.");
  232. }
  233. UnsubscribeFromChannelEvents(dispose: true);
  234. OutputStream.Dispose();
  235. ExtendedOutputStream.Dispose();
  236. // Initialize output streams. We already initialised them for the first
  237. // execution in the constructor (to allow passing them around before execution)
  238. // so we just need to reinitialise them for subsequent executions.
  239. OutputStream = new PipeStream();
  240. ExtendedOutputStream = new PipeStream();
  241. _channel = _session.CreateChannelSession();
  242. }
  243. _exitStatus = default;
  244. _haveExitStatus = false;
  245. ExitSignal = null;
  246. _stdOut = null;
  247. _stdErr = null;
  248. _hasError = false;
  249. _tokenRegistration.Dispose();
  250. _tokenRegistration = default;
  251. _cts?.Dispose();
  252. _cts = null;
  253. _cancellationRequested = false;
  254. _tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  255. _userToken = cancellationToken;
  256. _channel.DataReceived += Channel_DataReceived;
  257. _channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
  258. _channel.RequestReceived += Channel_RequestReceived;
  259. _channel.Closed += Channel_Closed;
  260. _channel.Open();
  261. _ = _channel.SendExecRequest(CommandText);
  262. if (CommandTimeout != Timeout.InfiniteTimeSpan)
  263. {
  264. _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  265. _cts.CancelAfter(CommandTimeout);
  266. cancellationToken = _cts.Token;
  267. }
  268. if (cancellationToken.CanBeCanceled)
  269. {
  270. _tokenRegistration = cancellationToken.Register(static cmd =>
  271. {
  272. try
  273. {
  274. ((SshCommand)cmd!).CancelAsync();
  275. }
  276. catch
  277. {
  278. // Swallow exceptions which would otherwise be unhandled.
  279. }
  280. },
  281. this);
  282. }
  283. return _tcs.Task;
  284. }
  285. #pragma warning restore CA1849
  286. /// <summary>
  287. /// Begins an asynchronous command execution.
  288. /// </summary>
  289. /// <returns>
  290. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  291. /// </returns>
  292. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  293. /// <exception cref="SshException">Invalid operation.</exception>
  294. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  295. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  296. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  297. public IAsyncResult BeginExecute()
  298. {
  299. return BeginExecute(callback: null, state: null);
  300. }
  301. /// <summary>
  302. /// Begins an asynchronous command execution.
  303. /// </summary>
  304. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  305. /// <returns>
  306. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  307. /// </returns>
  308. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  309. /// <exception cref="SshException">Invalid operation.</exception>
  310. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  311. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  312. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  313. public IAsyncResult BeginExecute(AsyncCallback? callback)
  314. {
  315. return BeginExecute(callback, state: null);
  316. }
  317. /// <summary>
  318. /// Begins an asynchronous command execution.
  319. /// </summary>
  320. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  321. /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
  322. /// <returns>
  323. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  324. /// </returns>
  325. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  326. /// <exception cref="SshException">Invalid operation.</exception>
  327. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  328. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  329. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  330. public IAsyncResult BeginExecute(AsyncCallback? callback, object? state)
  331. {
  332. return TaskToAsyncResult.Begin(ExecuteAsync(), callback, state);
  333. }
  334. /// <summary>
  335. /// Begins an asynchronous command execution.
  336. /// </summary>
  337. /// <param name="commandText">The command text.</param>
  338. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  339. /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
  340. /// <returns>
  341. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  342. /// </returns>
  343. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  344. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  345. public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state)
  346. {
  347. ThrowHelper.ThrowIfNull(commandText);
  348. CommandText = commandText;
  349. return BeginExecute(callback, state);
  350. }
  351. /// <summary>
  352. /// Waits for the pending asynchronous command execution to complete.
  353. /// </summary>
  354. /// <param name="asyncResult">The reference to the pending asynchronous request to finish.</param>
  355. /// <returns><see cref="Result"/>.</returns>
  356. /// <exception cref="ArgumentException"><paramref name="asyncResult"/> does not correspond to the currently executing command.</exception>
  357. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  358. public string EndExecute(IAsyncResult asyncResult)
  359. {
  360. var executeTask = TaskToAsyncResult.Unwrap(asyncResult);
  361. if (executeTask != _tcs?.Task)
  362. {
  363. throw new ArgumentException("Argument does not correspond to the currently executing command.", nameof(asyncResult));
  364. }
  365. executeTask.GetAwaiter().GetResult();
  366. return Result;
  367. }
  368. /// <summary>
  369. /// Cancels a running command by sending a signal to the remote process.
  370. /// </summary>
  371. /// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
  372. /// <param name="millisecondsTimeout">Time to wait for the server to reply.</param>
  373. /// <remarks>
  374. /// <para>
  375. /// This method stops the command running on the server by sending a SIGTERM
  376. /// (or SIGKILL, depending on <paramref name="forceKill"/>) signal to the remote
  377. /// process. When the server implements signals, it will send a response which
  378. /// populates <see cref="ExitSignal"/> with the signal with which the command terminated.
  379. /// </para>
  380. /// <para>
  381. /// When the server does not implement signals, it may send no response. As a fallback,
  382. /// this method waits up to <paramref name="millisecondsTimeout"/> for a response
  383. /// and then completes the <see cref="SshCommand"/> object anyway if there was none.
  384. /// </para>
  385. /// <para>
  386. /// If the command has already finished (with or without cancellation), this method does
  387. /// nothing.
  388. /// </para>
  389. /// </remarks>
  390. /// <exception cref="InvalidOperationException">Command has not been started.</exception>
  391. public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
  392. {
  393. if (_tcs is null)
  394. {
  395. throw new InvalidOperationException("Command has not been started.");
  396. }
  397. if (_tcs.Task.IsCompleted)
  398. {
  399. return;
  400. }
  401. _cancellationRequested = true;
  402. Interlocked.MemoryBarrier(); // ensure fresh read in SetAsyncComplete (possibly unnecessary)
  403. try
  404. {
  405. // Try to send the cancellation signal.
  406. if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
  407. {
  408. // Command has completed (in the meantime since the last check).
  409. return;
  410. }
  411. // Having sent the "signal" message, we expect to receive "exit-signal"
  412. // and then a close message. But since a server may not implement signals,
  413. // we can't guarantee that, so we wait a short time for that to happen and
  414. // if it doesn't, just complete the task ourselves to unblock waiters.
  415. _ = _tcs.Task.Wait(millisecondsTimeout);
  416. }
  417. catch (AggregateException)
  418. {
  419. // We expect to be here from the call to Wait if the server implements signals.
  420. // But we don't want to propagate the exception on the task from here.
  421. }
  422. finally
  423. {
  424. SetAsyncComplete();
  425. }
  426. }
  427. /// <summary>
  428. /// Executes the command specified by <see cref="CommandText"/>.
  429. /// </summary>
  430. /// <returns><see cref="Result"/>.</returns>
  431. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  432. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  433. public string Execute()
  434. {
  435. ExecuteAsync().GetAwaiter().GetResult();
  436. return Result;
  437. }
  438. /// <summary>
  439. /// Executes the specified command.
  440. /// </summary>
  441. /// <param name="commandText">The command text.</param>
  442. /// <returns><see cref="Result"/>.</returns>
  443. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  444. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  445. public string Execute(string commandText)
  446. {
  447. CommandText = commandText;
  448. return Execute();
  449. }
  450. private void Session_Disconnected(object? sender, EventArgs e)
  451. {
  452. _ = _tcs?.TrySetException(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
  453. SetAsyncComplete(setResult: false);
  454. }
  455. private void Session_ErrorOccured(object? sender, ExceptionEventArgs e)
  456. {
  457. _ = _tcs?.TrySetException(e.Exception);
  458. SetAsyncComplete(setResult: false);
  459. }
  460. private void SetAsyncComplete(bool setResult = true)
  461. {
  462. Interlocked.MemoryBarrier(); // ensure fresh read of _cancellationRequested (possibly unnecessary)
  463. if (setResult)
  464. {
  465. Debug.Assert(_tcs is not null, "Should only be completing the task if we've started one.");
  466. if (_userToken.IsCancellationRequested)
  467. {
  468. _ = _tcs.TrySetCanceled(_userToken);
  469. }
  470. else if (_cts?.Token.IsCancellationRequested == true)
  471. {
  472. _ = _tcs.TrySetException(new SshOperationTimeoutException($"Command '{CommandText}' timed out. ({nameof(CommandTimeout)}: {CommandTimeout})."));
  473. }
  474. else if (_cancellationRequested)
  475. {
  476. _ = _tcs.TrySetCanceled();
  477. }
  478. else
  479. {
  480. _ = _tcs.TrySetResult(null!);
  481. }
  482. }
  483. // We don't dispose the channel here to avoid a race condition
  484. // where SSH_MSG_CHANNEL_CLOSE arrives before _channel starts
  485. // waiting for a response in _channel.SendExecRequest().
  486. UnsubscribeFromChannelEvents(dispose: false);
  487. OutputStream.Dispose();
  488. ExtendedOutputStream.Dispose();
  489. }
  490. private void Channel_Closed(object? sender, ChannelEventArgs e)
  491. {
  492. SetAsyncComplete();
  493. }
  494. private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
  495. {
  496. if (e.Info is ExitStatusRequestInfo exitStatusInfo)
  497. {
  498. _exitStatus = (int)exitStatusInfo.ExitStatus;
  499. _haveExitStatus = true;
  500. Debug.Assert(!exitStatusInfo.WantReply, "exit-status is want_reply := false by definition.");
  501. }
  502. else if (e.Info is ExitSignalRequestInfo exitSignalInfo)
  503. {
  504. ExitSignal = exitSignalInfo.SignalName;
  505. Debug.Assert(!exitSignalInfo.WantReply, "exit-signal is want_reply := false by definition.");
  506. }
  507. else if (e.Info.WantReply && sender is IChannel { RemoteChannelNumber: uint remoteChannelNumber })
  508. {
  509. var replyMessage = new ChannelFailureMessage(remoteChannelNumber);
  510. _session.SendMessage(replyMessage);
  511. }
  512. }
  513. private void Channel_ExtendedDataReceived(object? sender, ChannelExtendedDataEventArgs e)
  514. {
  515. ExtendedOutputStream.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  516. if (e.DataTypeCode == 1)
  517. {
  518. _hasError = true;
  519. }
  520. }
  521. private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
  522. {
  523. OutputStream.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
  524. }
  525. /// <summary>
  526. /// Unsubscribes the current <see cref="SshCommand"/> from channel events, and optionally,
  527. /// disposes <see cref="_channel"/>.
  528. /// </summary>
  529. private void UnsubscribeFromChannelEvents(bool dispose)
  530. {
  531. var channel = _channel;
  532. // unsubscribe from events as we do not want to be signaled should these get fired
  533. // during the dispose of the channel
  534. channel.DataReceived -= Channel_DataReceived;
  535. channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
  536. channel.RequestReceived -= Channel_RequestReceived;
  537. channel.Closed -= Channel_Closed;
  538. if (dispose)
  539. {
  540. channel.Dispose();
  541. }
  542. }
  543. /// <summary>
  544. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  545. /// </summary>
  546. public void Dispose()
  547. {
  548. Dispose(disposing: true);
  549. GC.SuppressFinalize(this);
  550. }
  551. /// <summary>
  552. /// Releases unmanaged and - optionally - managed resources.
  553. /// </summary>
  554. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
  555. protected virtual void Dispose(bool disposing)
  556. {
  557. if (_isDisposed)
  558. {
  559. return;
  560. }
  561. if (disposing)
  562. {
  563. // unsubscribe from session events to ensure other objects that we're going to dispose
  564. // are not accessed while disposing
  565. _session.Disconnected -= Session_Disconnected;
  566. _session.ErrorOccured -= Session_ErrorOccured;
  567. // unsubscribe from channel events to ensure other objects that we're going to dispose
  568. // are not accessed while disposing
  569. UnsubscribeFromChannelEvents(dispose: true);
  570. _inputStream?.Dispose();
  571. _inputStream = null;
  572. OutputStream.Dispose();
  573. ExtendedOutputStream.Dispose();
  574. _tokenRegistration.Dispose();
  575. _tokenRegistration = default;
  576. _cts?.Dispose();
  577. _cts = null;
  578. if (_tcs is { Task.IsCompleted: false } tcs)
  579. {
  580. // In case an operation is still running, try to complete it with an ObjectDisposedException.
  581. _ = tcs.TrySetException(new ObjectDisposedException(GetType().FullName));
  582. }
  583. _isDisposed = true;
  584. }
  585. }
  586. }
  587. }