Skip to content

Commit ae176ac

Browse files
committed
- Revamp README.md with enhanced feature breakdown and detailed method mappings for clarity.
- Add `SendKeepAlive` and `ConfigureKeepAlive` methods to `SshSession` for connection keepalive configuration. - Strengthen precondition checks in `SshSession` methods with `EnsureInitialized` and session state validation. - Restrict `LibSshExtensions` methods visibility to `internal` for stricter encapsulation. - Refactor XML documentation across `SshSession` to improve readability and ensure consistent formatting.
1 parent b6fce7f commit ae176ac

File tree

3 files changed

+131
-41
lines changed

3 files changed

+131
-41
lines changed

NullOpsDevs.LibSsh/Extensions/LibSshExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal static class LibSshExtensions
1616
/// <param name="message">Optional custom error message.</param>
1717
/// <param name="also">Optional action to execute before throwing the exception (e.g., cleanup).</param>
1818
/// <exception cref="SshException">Thrown when the return code is negative (indicates error).</exception>
19-
public static unsafe void ThrowIfNotSuccessful(this int @return, SshSession session,
19+
internal static unsafe void ThrowIfNotSuccessful(this int @return, SshSession session,
2020
string? message = null, Action? also = null)
2121
{
2222
if (@return >= 0)
@@ -40,7 +40,7 @@ public static unsafe void ThrowIfNotSuccessful(this int @return, SshSession sess
4040
/// </summary>
4141
/// <param name="exception">The exception to convert.</param>
4242
/// <returns>An SshException wrapping the original exception.</returns>
43-
public static SshException AsSshException(this Exception exception)
43+
internal static SshException AsSshException(this Exception exception)
4444
{
4545
return new SshException(exception.Message, SshError.InnerException, exception);
4646
}

NullOpsDevs.LibSsh/SshSession.cs

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ private void EnsureInStatuses(params SshConnectionStatus[] statuses)
8080
/// <param name="port">The port number of the SSH server (typically 22).</param>
8181
/// <exception cref="SshException">Thrown when connection or SSH handshake fails.</exception>
8282
/// <remarks>
83-
/// This method establishes a TCP connection and performs the SSH protocol handshake.
84-
/// After successful connection, the session will be in <see cref="SshConnectionStatus.Connected"/> status.
85-
/// You must call <see cref="Authenticate"/> before executing commands.
83+
/// <para>This method establishes a TCP connection and performs the SSH protocol handshake.</para>
84+
/// <para>The session must be in <see cref="SshConnectionStatus.Disconnected"/> status before calling this method.</para>
85+
/// <para>After successful connection, the session will be in <see cref="SshConnectionStatus.Connected"/> status.</para>
86+
/// <para>You must call <see cref="Authenticate"/> before executing commands.</para>
8687
/// </remarks>
8788
public unsafe void Connect(string host, int port)
8889
{
@@ -252,17 +253,25 @@ public void SetSecureMethodPreferences()
252253
/// Disables the session timeout, allowing operations to wait indefinitely.
253254
/// </summary>
254255
/// <remarks>
255-
/// By default, libssh2 has no timeout. Use this method to explicitly disable any previously set timeout.
256+
/// <para>By default, libssh2 has no timeout. Use this method to explicitly disable any previously set timeout.</para>
257+
/// <para>The session must be in <see cref="SshConnectionStatus.Connected"/> or <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
256258
/// </remarks>
257-
public unsafe void DisableSessionTimeout() => libssh2_session_set_timeout(SessionPtr, 0);
259+
public unsafe void DisableSessionTimeout()
260+
{
261+
EnsureInitialized();
262+
EnsureInStatuses(SshConnectionStatus.Connected, SshConnectionStatus.LoggedIn);
263+
264+
libssh2_session_set_timeout(SessionPtr, 0);
265+
}
258266

259267
/// <summary>
260268
/// Sets the maximum time to wait for SSH operations to complete.
261269
/// </summary>
262270
/// <param name="timeout">The timeout duration. Must be greater than zero and less than <see cref="int.MaxValue"/> milliseconds.</param>
263271
/// <exception cref="ArgumentOutOfRangeException">Thrown if the timeout is negative or exceeds the maximum allowed value.</exception>
264272
/// <remarks>
265-
/// This timeout applies to blocking libssh2 operations. If an operation does not complete within the specified time, it will return an error.
273+
/// <para>This timeout applies to blocking libssh2 operations. If an operation does not complete within the specified time, it will return an error.</para>
274+
/// <para>The session must be in <see cref="SshConnectionStatus.Connected"/> or <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
266275
/// </remarks>
267276
public unsafe void SetSessionTimeout(TimeSpan timeout)
268277
{
@@ -271,10 +280,59 @@ public unsafe void SetSessionTimeout(TimeSpan timeout)
271280

272281
if(timeout.TotalMilliseconds > int.MaxValue)
273282
throw new ArgumentOutOfRangeException(nameof(timeout), timeout, "Timeout cannot be greater than ");
283+
284+
EnsureInitialized();
285+
EnsureInStatuses(SshConnectionStatus.Connected, SshConnectionStatus.LoggedIn);
274286

275287
libssh2_session_set_timeout(SessionPtr, (int) timeout.TotalMilliseconds);
276288
}
277289

290+
/// <summary>
291+
/// Sends a keepalive message to the remote SSH server.
292+
/// </summary>
293+
/// <returns>The number of seconds until the next keepalive should be sent, based on the configured interval.</returns>
294+
/// <exception cref="SshException">Thrown if the keepalive message cannot be sent or the session is not in the correct state.</exception>
295+
/// <remarks>
296+
/// <para>This method sends an SSH protocol keepalive message to prevent the connection from timing out due to inactivity.</para>
297+
/// <para>This method must be called after connecting to the server. The session must be in <see cref="SshConnectionStatus.Connected"/> or <see cref="SshConnectionStatus.LoggedIn"/> status.</para>
298+
/// <para>Use <see cref="ConfigureKeepAlive"/> to set the keepalive interval and whether the server should reply.</para>
299+
/// </remarks>
300+
public unsafe int SendKeepAlive()
301+
{
302+
EnsureInitialized();
303+
EnsureInStatuses(SshConnectionStatus.Connected, SshConnectionStatus.LoggedIn);
304+
305+
var untilNext = 0;
306+
var result = libssh2_keepalive_send(SessionPtr, &untilNext);
307+
308+
result.ThrowIfNotSuccessful(this, "Failed to send keepalive");
309+
310+
return untilNext;
311+
}
312+
313+
/// <summary>
314+
/// Configures the SSH session's keepalive behavior.
315+
/// </summary>
316+
/// <param name="wantReply">If true, the server will be requested to send a reply to keepalive messages. If false, keepalive messages are sent without expecting a reply.</param>
317+
/// <param name="interval">The interval between keepalive messages. Must be greater than or equal to zero.</param>
318+
/// <exception cref="ArgumentOutOfRangeException">Thrown if the interval is negative.</exception>
319+
/// <exception cref="SshException">Thrown if the session is not in the correct state.</exception>
320+
/// <remarks>
321+
/// <para>This method configures the keepalive settings for the SSH session. Keepalive messages help maintain connections that might otherwise timeout due to inactivity or be terminated by firewalls.</para>
322+
/// <para>This method must be called after connecting to the server. The session must be in <see cref="SshConnectionStatus.Connected"/> or <see cref="SshConnectionStatus.LoggedIn"/> status.</para>
323+
/// <para>Use <see cref="SendKeepAlive"/> to manually send keepalive messages once configured.</para>
324+
/// </remarks>
325+
public unsafe void ConfigureKeepAlive(bool wantReply, TimeSpan interval)
326+
{
327+
if (interval < TimeSpan.Zero)
328+
throw new ArgumentOutOfRangeException(nameof(interval), interval, "Interval must be greater than zero");
329+
330+
EnsureInitialized();
331+
EnsureInStatuses(SshConnectionStatus.Connected, SshConnectionStatus.LoggedIn);
332+
333+
libssh2_keepalive_config(SessionPtr, wantReply ? 1 : 0, (uint) interval.TotalSeconds);
334+
}
335+
278336
/// <summary>
279337
/// Computes a cryptographic hash of the server's host key for verification purposes.
280338
/// </summary>
@@ -318,9 +376,10 @@ public unsafe byte[] GetHostKeyHash(SshHashType keyHashType)
318376
/// <param name="cancellationToken">Optional cancellation token to cancel the operation.</param>
319377
/// <exception cref="SshException">Thrown when connection or SSH handshake fails.</exception>
320378
/// <remarks>
321-
/// This method offloads the blocking connection and handshake operations to a thread pool thread.
322-
/// After successful connection, the session will be in <see cref="SshConnectionStatus.Connected"/> status.
323-
/// You must call <see cref="AuthenticateAsync"/> or <see cref="Authenticate"/> before executing commands.
379+
/// <para>This method offloads the blocking connection and handshake operations to a thread pool thread.</para>
380+
/// <para>The session must be in <see cref="SshConnectionStatus.Disconnected"/> status before calling this method.</para>
381+
/// <para>After successful connection, the session will be in <see cref="SshConnectionStatus.Connected"/> status.</para>
382+
/// <para>You must call <see cref="AuthenticateAsync"/> or <see cref="Authenticate"/> before executing commands.</para>
324383
/// </remarks>
325384
public Task ConnectAsync(string host, int port, CancellationToken cancellationToken = default)
326385
{
@@ -335,8 +394,9 @@ public Task ConnectAsync(string host, int port, CancellationToken cancellationTo
335394
/// <param name="cancellationToken">Optional cancellation token to cancel the operation.</param>
336395
/// <returns>The result of the command execution including stdout and stderr output.</returns>
337396
/// <remarks>
338-
/// When <see cref="CommandExecutionOptions.RequestPty"/> is true, a pseudo-terminal (PTY) will be requested
339-
/// before executing the command. This enables terminal features like color output and interactive input.
397+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
398+
/// <para>When <see cref="CommandExecutionOptions.RequestPty"/> is true, a pseudo-terminal (PTY) will be requested
399+
/// before executing the command. This enables terminal features like color output and interactive input.</para>
340400
/// </remarks>
341401
public unsafe SshCommandResult ExecuteCommand(string command, CommandExecutionOptions? options = null, CancellationToken cancellationToken = default)
342402
{
@@ -468,9 +528,10 @@ public unsafe SshCommandResult ExecuteCommand(string command, CommandExecutionOp
468528
/// <param name="cancellationToken">Optional cancellation token to cancel the operation.</param>
469529
/// <returns>A task that represents the asynchronous operation, containing the result of the command execution including stdout and stderr output.</returns>
470530
/// <remarks>
471-
/// This method offloads the blocking command execution to a thread pool thread.
472-
/// When <see cref="CommandExecutionOptions.RequestPty"/> is true, a pseudo-terminal (PTY) will be requested
473-
/// before executing the command. This enables terminal features like color output and interactive input.
531+
/// <para>This method offloads the blocking command execution to a thread pool thread.</para>
532+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
533+
/// <para>When <see cref="CommandExecutionOptions.RequestPty"/> is true, a pseudo-terminal (PTY) will be requested
534+
/// before executing the command. This enables terminal features like color output and interactive input.</para>
474535
/// </remarks>
475536
public Task<SshCommandResult> ExecuteCommandAsync(string command, CommandExecutionOptions? options = null, CancellationToken cancellationToken = default)
476537
{
@@ -487,8 +548,9 @@ public Task<SshCommandResult> ExecuteCommandAsync(string command, CommandExecuti
487548
/// <returns>True if the entire file was successfully downloaded; false otherwise.</returns>
488549
/// <exception cref="SshException">Thrown when the SCP channel cannot be created or other SSH errors occur.</exception>
489550
/// <remarks>
490-
/// This method does not close the destination stream. The caller is responsible for managing the stream's lifecycle.
491-
/// The method uses SCP (Secure Copy Protocol) to transfer the file.
551+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
552+
/// <para>This method does not close the destination stream. The caller is responsible for managing the stream's lifecycle.</para>
553+
/// <para>The method uses SCP (Secure Copy Protocol) to transfer the file.</para>
492554
/// </remarks>
493555
public unsafe bool ReadFile(string path, Stream destination, int bufferSize = 32768, CancellationToken cancellationToken = default)
494556
{
@@ -545,8 +607,9 @@ public unsafe bool ReadFile(string path, Stream destination, int bufferSize = 32
545607
/// <returns>A task that represents the asynchronous operation, containing a boolean value indicating whether the entire file was successfully downloaded.</returns>
546608
/// <exception cref="SshException">Thrown when the SCP channel cannot be created or other SSH errors occur.</exception>
547609
/// <remarks>
548-
/// This method offloads the blocking SCP file transfer to a thread pool thread.
549-
/// The destination stream will not be closed by this method. The caller is responsible for managing the stream's lifecycle.
610+
/// <para>This method offloads the blocking SCP file transfer to a thread pool thread.</para>
611+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
612+
/// <para>The destination stream will not be closed by this method. The caller is responsible for managing the stream's lifecycle.</para>
550613
/// </remarks>
551614
public Task<bool> ReadFileAsync(string path, Stream destination, int bufferSize = 32768, CancellationToken cancellationToken = default)
552615
{
@@ -565,9 +628,10 @@ public Task<bool> ReadFileAsync(string path, Stream destination, int bufferSize
565628
/// <exception cref="ArgumentException">Thrown when the source stream is not readable or not seekable.</exception>
566629
/// <exception cref="SshException">Thrown when the SCP channel cannot be created or other SSH errors occur.</exception>
567630
/// <remarks>
568-
/// This method does not close the source stream. The caller is responsible for managing the stream's lifecycle.
569-
/// The method uses SCP (Secure Copy Protocol) to transfer the file.
570-
/// The source stream must be seekable because the total file size must be known before transmission begins.
631+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
632+
/// <para>This method does not close the source stream. The caller is responsible for managing the stream's lifecycle.</para>
633+
/// <para>The method uses SCP (Secure Copy Protocol) to transfer the file.</para>
634+
/// <para>The source stream must be seekable because the total file size must be known before transmission begins.</para>
571635
/// </remarks>
572636
public unsafe bool WriteFile(string path, Stream source, int mode = 420, int bufferSize = 32768, CancellationToken cancellationToken = default)
573637
{
@@ -635,9 +699,10 @@ public unsafe bool WriteFile(string path, Stream source, int mode = 420, int buf
635699
/// <exception cref="ArgumentException">Thrown when the source stream is not readable or not seekable.</exception>
636700
/// <exception cref="SshException">Thrown when the SCP channel cannot be created or other SSH errors occur.</exception>
637701
/// <remarks>
638-
/// This method offloads the blocking SCP file transfer to a thread pool thread.
639-
/// The source stream will not be closed by this method. The caller is responsible for managing the stream's lifecycle.
640-
/// The source stream must be seekable because the total file size must be known before transmission begins.
702+
/// <para>This method offloads the blocking SCP file transfer to a thread pool thread.</para>
703+
/// <para>The session must be in <see cref="SshConnectionStatus.LoggedIn"/> status before calling this method.</para>
704+
/// <para>The source stream will not be closed by this method. The caller is responsible for managing the stream's lifecycle.</para>
705+
/// <para>The source stream must be seekable because the total file size must be known before transmission begins.</para>
641706
/// </remarks>
642707
public Task<bool> WriteFileAsync(string path, Stream source, int mode = 420, int bufferSize = 32768, CancellationToken cancellationToken = default)
643708
{

0 commit comments

Comments
 (0)