2
0

SshCommand.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  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 == null)
  132. {
  133. throw new InvalidOperationException($"The input stream can be used only after calling BeginExecute and before calling EndExecute.");
  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. }
  206. /// <summary>
  207. /// Executes the command asynchronously.
  208. /// </summary>
  209. /// <param name="cancellationToken">
  210. /// The <see cref="CancellationToken"/>. When triggered, attempts to terminate the
  211. /// remote command by sending a signal.
  212. /// </param>
  213. /// <returns>A <see cref="Task"/> representing the lifetime of the command.</returns>
  214. /// <exception cref="InvalidOperationException">Command is already executing. Thrown synchronously.</exception>
  215. /// <exception cref="ObjectDisposedException">Instance has been disposed. Thrown synchronously.</exception>
  216. /// <exception cref="OperationCanceledException">The <see cref="Task"/> has been cancelled.</exception>
  217. /// <exception cref="SshOperationTimeoutException">The command timed out according to <see cref="CommandTimeout"/>.</exception>
  218. #pragma warning disable CA1849 // Call async methods when in an async method; PipeStream.DisposeAsync would complete synchronously anyway.
  219. public Task ExecuteAsync(CancellationToken cancellationToken = default)
  220. {
  221. ThrowHelper.ThrowObjectDisposedIf(_isDisposed, this);
  222. if (cancellationToken.IsCancellationRequested)
  223. {
  224. return Task.FromCanceled(cancellationToken);
  225. }
  226. if (_tcs is not null)
  227. {
  228. if (!_tcs.Task.IsCompleted)
  229. {
  230. throw new InvalidOperationException("Asynchronous operation is already in progress.");
  231. }
  232. OutputStream.Dispose();
  233. ExtendedOutputStream.Dispose();
  234. // Initialize output streams. We already initialised them for the first
  235. // execution in the constructor (to allow passing them around before execution)
  236. // so we just need to reinitialise them for subsequent executions.
  237. OutputStream = new PipeStream();
  238. ExtendedOutputStream = new PipeStream();
  239. }
  240. _exitStatus = default;
  241. _haveExitStatus = false;
  242. ExitSignal = null;
  243. _stdOut = null;
  244. _stdErr = null;
  245. _hasError = false;
  246. _tokenRegistration.Dispose();
  247. _tokenRegistration = default;
  248. _cts?.Dispose();
  249. _cts = null;
  250. _cancellationRequested = false;
  251. _tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
  252. _userToken = cancellationToken;
  253. _channel = _session.CreateChannelSession();
  254. _channel.DataReceived += Channel_DataReceived;
  255. _channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
  256. _channel.RequestReceived += Channel_RequestReceived;
  257. _channel.Closed += Channel_Closed;
  258. _channel.Open();
  259. _ = _channel.SendExecRequest(CommandText);
  260. if (CommandTimeout != Timeout.InfiniteTimeSpan)
  261. {
  262. _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
  263. _cts.CancelAfter(CommandTimeout);
  264. cancellationToken = _cts.Token;
  265. }
  266. if (cancellationToken.CanBeCanceled)
  267. {
  268. _tokenRegistration = cancellationToken.Register(static cmd => ((SshCommand)cmd!).CancelAsync(), this);
  269. }
  270. return _tcs.Task;
  271. }
  272. #pragma warning restore CA1849
  273. /// <summary>
  274. /// Begins an asynchronous command execution.
  275. /// </summary>
  276. /// <returns>
  277. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  278. /// </returns>
  279. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  280. /// <exception cref="SshException">Invalid operation.</exception>
  281. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  282. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  283. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  284. public IAsyncResult BeginExecute()
  285. {
  286. return BeginExecute(callback: null, state: null);
  287. }
  288. /// <summary>
  289. /// Begins an asynchronous command execution.
  290. /// </summary>
  291. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  292. /// <returns>
  293. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  294. /// </returns>
  295. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  296. /// <exception cref="SshException">Invalid operation.</exception>
  297. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  298. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  299. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  300. public IAsyncResult BeginExecute(AsyncCallback? callback)
  301. {
  302. return BeginExecute(callback, state: null);
  303. }
  304. /// <summary>
  305. /// Begins an asynchronous command execution.
  306. /// </summary>
  307. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  308. /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
  309. /// <returns>
  310. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  311. /// </returns>
  312. /// <exception cref="InvalidOperationException">Asynchronous operation is already in progress.</exception>
  313. /// <exception cref="SshException">Invalid operation.</exception>
  314. /// <exception cref="ArgumentException">CommandText property is empty.</exception>
  315. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  316. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  317. public IAsyncResult BeginExecute(AsyncCallback? callback, object? state)
  318. {
  319. return TaskToAsyncResult.Begin(ExecuteAsync(), callback, state);
  320. }
  321. /// <summary>
  322. /// Begins an asynchronous command execution.
  323. /// </summary>
  324. /// <param name="commandText">The command text.</param>
  325. /// <param name="callback">An optional asynchronous callback, to be called when the command execution is complete.</param>
  326. /// <param name="state">A user-provided object that distinguishes this particular asynchronous read request from other requests.</param>
  327. /// <returns>
  328. /// An <see cref="IAsyncResult" /> that represents the asynchronous command execution, which could still be pending.
  329. /// </returns>
  330. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  331. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  332. public IAsyncResult BeginExecute(string commandText, AsyncCallback? callback, object? state)
  333. {
  334. ThrowHelper.ThrowIfNull(commandText);
  335. CommandText = commandText;
  336. return BeginExecute(callback, state);
  337. }
  338. /// <summary>
  339. /// Waits for the pending asynchronous command execution to complete.
  340. /// </summary>
  341. /// <param name="asyncResult">The reference to the pending asynchronous request to finish.</param>
  342. /// <returns><see cref="Result"/>.</returns>
  343. /// <exception cref="ArgumentException"><paramref name="asyncResult"/> does not correspond to the currently executing command.</exception>
  344. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  345. public string EndExecute(IAsyncResult asyncResult)
  346. {
  347. var executeTask = TaskToAsyncResult.Unwrap(asyncResult);
  348. if (executeTask != _tcs?.Task)
  349. {
  350. throw new ArgumentException("Argument does not correspond to the currently executing command.", nameof(asyncResult));
  351. }
  352. executeTask.GetAwaiter().GetResult();
  353. return Result;
  354. }
  355. /// <summary>
  356. /// Cancels a running command by sending a signal to the remote process.
  357. /// </summary>
  358. /// <param name="forceKill">if true send SIGKILL instead of SIGTERM.</param>
  359. /// <param name="millisecondsTimeout">Time to wait for the server to reply.</param>
  360. /// <remarks>
  361. /// <para>
  362. /// This method stops the command running on the server by sending a SIGTERM
  363. /// (or SIGKILL, depending on <paramref name="forceKill"/>) signal to the remote
  364. /// process. When the server implements signals, it will send a response which
  365. /// populates <see cref="ExitSignal"/> with the signal with which the command terminated.
  366. /// </para>
  367. /// <para>
  368. /// When the server does not implement signals, it may send no response. As a fallback,
  369. /// this method waits up to <paramref name="millisecondsTimeout"/> for a response
  370. /// and then completes the <see cref="SshCommand"/> object anyway if there was none.
  371. /// </para>
  372. /// <para>
  373. /// If the command has already finished (with or without cancellation), this method does
  374. /// nothing.
  375. /// </para>
  376. /// </remarks>
  377. /// <exception cref="InvalidOperationException">Command has not been started.</exception>
  378. public void CancelAsync(bool forceKill = false, int millisecondsTimeout = 500)
  379. {
  380. if (_tcs is null)
  381. {
  382. throw new InvalidOperationException("Command has not been started.");
  383. }
  384. if (_tcs.Task.IsCompleted)
  385. {
  386. return;
  387. }
  388. _cancellationRequested = true;
  389. Interlocked.MemoryBarrier(); // ensure fresh read in SetAsyncComplete (possibly unnecessary)
  390. // Try to send the cancellation signal.
  391. if (_channel?.SendSignalRequest(forceKill ? "KILL" : "TERM") is null)
  392. {
  393. // Command has completed (in the meantime since the last check).
  394. return;
  395. }
  396. // Having sent the "signal" message, we expect to receive "exit-signal"
  397. // and then a close message. But since a server may not implement signals,
  398. // we can't guarantee that, so we wait a short time for that to happen and
  399. // if it doesn't, just complete the task ourselves to unblock waiters.
  400. try
  401. {
  402. if (_tcs.Task.Wait(millisecondsTimeout))
  403. {
  404. return;
  405. }
  406. }
  407. catch (AggregateException)
  408. {
  409. // We expect to be here if the server implements signals.
  410. // But we don't want to propagate the exception on the task from here.
  411. return;
  412. }
  413. SetAsyncComplete();
  414. }
  415. /// <summary>
  416. /// Executes the command specified by <see cref="CommandText"/>.
  417. /// </summary>
  418. /// <returns><see cref="Result"/>.</returns>
  419. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  420. /// <exception cref="SshOperationTimeoutException">Operation has timed out.</exception>
  421. public string Execute()
  422. {
  423. ExecuteAsync().GetAwaiter().GetResult();
  424. return Result;
  425. }
  426. /// <summary>
  427. /// Executes the specified command.
  428. /// </summary>
  429. /// <param name="commandText">The command text.</param>
  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(string commandText)
  434. {
  435. CommandText = commandText;
  436. return Execute();
  437. }
  438. private void Session_Disconnected(object? sender, EventArgs e)
  439. {
  440. _ = _tcs?.TrySetException(new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost));
  441. SetAsyncComplete(setResult: false);
  442. }
  443. private void Session_ErrorOccured(object? sender, ExceptionEventArgs e)
  444. {
  445. _ = _tcs?.TrySetException(e.Exception);
  446. SetAsyncComplete(setResult: false);
  447. }
  448. private void SetAsyncComplete(bool setResult = true)
  449. {
  450. Interlocked.MemoryBarrier(); // ensure fresh read of _cancellationRequested (possibly unnecessary)
  451. if (setResult)
  452. {
  453. Debug.Assert(_tcs is not null, "Should only be completing the task if we've started one.");
  454. if (_userToken.IsCancellationRequested)
  455. {
  456. _ = _tcs.TrySetCanceled(_userToken);
  457. }
  458. else if (_cts?.Token.IsCancellationRequested == true)
  459. {
  460. _ = _tcs.TrySetException(new SshOperationTimeoutException($"Command '{CommandText}' timed out. ({nameof(CommandTimeout)}: {CommandTimeout})."));
  461. }
  462. else if (_cancellationRequested)
  463. {
  464. _ = _tcs.TrySetCanceled();
  465. }
  466. else
  467. {
  468. _ = _tcs.TrySetResult(null!);
  469. }
  470. }
  471. UnsubscribeFromEventsAndDisposeChannel();
  472. OutputStream.Dispose();
  473. ExtendedOutputStream.Dispose();
  474. }
  475. private void Channel_Closed(object? sender, ChannelEventArgs e)
  476. {
  477. SetAsyncComplete();
  478. }
  479. private void Channel_RequestReceived(object? sender, ChannelRequestEventArgs e)
  480. {
  481. if (e.Info is ExitStatusRequestInfo exitStatusInfo)
  482. {
  483. _exitStatus = (int)exitStatusInfo.ExitStatus;
  484. _haveExitStatus = true;
  485. Debug.Assert(!exitStatusInfo.WantReply, "exit-status is want_reply := false by definition.");
  486. }
  487. else if (e.Info is ExitSignalRequestInfo exitSignalInfo)
  488. {
  489. ExitSignal = exitSignalInfo.SignalName;
  490. Debug.Assert(!exitSignalInfo.WantReply, "exit-signal is want_reply := false by definition.");
  491. }
  492. else if (e.Info.WantReply && _channel?.RemoteChannelNumber is uint remoteChannelNumber)
  493. {
  494. var replyMessage = new ChannelFailureMessage(remoteChannelNumber);
  495. _session.SendMessage(replyMessage);
  496. }
  497. }
  498. private void Channel_ExtendedDataReceived(object? sender, ChannelExtendedDataEventArgs e)
  499. {
  500. ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
  501. if (e.DataTypeCode == 1)
  502. {
  503. _hasError = true;
  504. }
  505. }
  506. private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
  507. {
  508. OutputStream.Write(e.Data, 0, e.Data.Length);
  509. }
  510. /// <summary>
  511. /// Unsubscribes the current <see cref="SshCommand"/> from channel events, and disposes
  512. /// the <see cref="_channel"/>.
  513. /// </summary>
  514. private void UnsubscribeFromEventsAndDisposeChannel()
  515. {
  516. var channel = _channel;
  517. if (channel is null)
  518. {
  519. return;
  520. }
  521. _channel = null;
  522. // unsubscribe from events as we do not want to be signaled should these get fired
  523. // during the dispose of the channel
  524. channel.DataReceived -= Channel_DataReceived;
  525. channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
  526. channel.RequestReceived -= Channel_RequestReceived;
  527. channel.Closed -= Channel_Closed;
  528. // actually dispose the channel
  529. channel.Dispose();
  530. }
  531. /// <summary>
  532. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  533. /// </summary>
  534. public void Dispose()
  535. {
  536. Dispose(disposing: true);
  537. GC.SuppressFinalize(this);
  538. }
  539. /// <summary>
  540. /// Releases unmanaged and - optionally - managed resources.
  541. /// </summary>
  542. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
  543. protected virtual void Dispose(bool disposing)
  544. {
  545. if (_isDisposed)
  546. {
  547. return;
  548. }
  549. if (disposing)
  550. {
  551. // unsubscribe from session events to ensure other objects that we're going to dispose
  552. // are not accessed while disposing
  553. _session.Disconnected -= Session_Disconnected;
  554. _session.ErrorOccured -= Session_ErrorOccured;
  555. // unsubscribe from channel events to ensure other objects that we're going to dispose
  556. // are not accessed while disposing
  557. UnsubscribeFromEventsAndDisposeChannel();
  558. _inputStream?.Dispose();
  559. _inputStream = null;
  560. OutputStream.Dispose();
  561. ExtendedOutputStream.Dispose();
  562. _tokenRegistration.Dispose();
  563. _tokenRegistration = default;
  564. _cts?.Dispose();
  565. _cts = null;
  566. if (_tcs is { Task.IsCompleted: false } tcs)
  567. {
  568. // In case an operation is still running, try to complete it with an ObjectDisposedException.
  569. _ = tcs.TrySetException(new ObjectDisposedException(GetType().FullName));
  570. }
  571. _isDisposed = true;
  572. }
  573. }
  574. }
  575. }