Skip to content

Commit b6fce7f

Browse files
committed
- Integrate Microsoft.Extensions.Logging in TestHelper and SshSession for improved debugging.
- Add `AnsiConsoleLogger` for console-based logging in tests. - Refactor `ThrowIfNotSuccessful` to include session-specific error context. - Enhance method preference handling in `SshSession` with deferred application and validation tests. - Improve testing coverage with additional scenarios and timeout validation.
1 parent dbde7b6 commit b6fce7f

File tree

6 files changed

+118
-22
lines changed

6 files changed

+118
-22
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.Extensions.Logging;
2+
using Spectre.Console;
3+
4+
namespace NullOpsDevs.LibSsh.Test;
5+
6+
public class AnsiConsoleLogger : ILogger
7+
{
8+
/// <inheritdoc />
9+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
10+
{
11+
throw new NotImplementedException();
12+
}
13+
14+
/// <inheritdoc />
15+
public bool IsEnabled(LogLevel logLevel) => true;
16+
17+
/// <inheritdoc />
18+
public void Log<TState>(LogLevel logLevel, EventId _, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
19+
{
20+
var logLevelColor = logLevel switch
21+
{
22+
LogLevel.Trace => "dim",
23+
LogLevel.Debug => "dim",
24+
LogLevel.Information => "white",
25+
LogLevel.Warning => "yellow",
26+
LogLevel.Error => "red",
27+
LogLevel.Critical => "red",
28+
_ => "white"
29+
};
30+
31+
var formatted = formatter(state, exception);
32+
AnsiConsole.MarkupLine($"[{logLevelColor}]{logLevel:G}[/] {Markup.Escape(formatted)}");
33+
}
34+
}

NullOpsDevs.LibSsh.Test/Program.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Text;
33
using NullOpsDevs.LibSsh.Core;
44
using NullOpsDevs.LibSsh.Credentials;
5+
using NullOpsDevs.LibSsh.Exceptions;
56
using NullOpsDevs.LibSsh.Terminal;
67
using Spectre.Console;
78

@@ -28,8 +29,6 @@ public static async Task<int> Main()
2829
return 255;
2930
}
3031

31-
// LibSsh2.GlobalLogger = msg => AnsiConsole.MarkupLine($"[grey]{Markup.Escape(msg)}[/]");
32-
3332
// Wait for Docker containers
3433
AnsiConsole.MarkupLine("[yellow]Waiting for Docker containers to be ready...[/]");
3534
try
@@ -548,16 +547,41 @@ private static async Task RunEdgeCaseTests()
548547
await RunTest("Command with large output", TestLargeOutput);
549548
await RunTest("Multiple sequential operations", TestMultipleOperations);
550549
await RunTest("Timeout test", TimeoutTest);
550+
await RunTest("Won't connect with empty methods", WontConnectWithEmptyMethods);
551+
}
552+
553+
private static Task<bool> WontConnectWithEmptyMethods()
554+
{
555+
var session = new SshSession();
556+
session.SetMethodPreferences(SshMethod.Kex, "diffie-hellman-group1-sha1");
557+
558+
try
559+
{
560+
session.Connect(TestConfig.Host, TestConfig.Port);
561+
}
562+
catch (SshException)
563+
{
564+
return Task.FromResult(true);
565+
}
566+
567+
return Task.FromResult(false);
551568
}
552569

553570
private static Task<bool> TimeoutTest()
554571
{
555572
var session = TestHelper.CreateConnectAndAuthenticate();
556573
session.SetSessionTimeout(TimeSpan.FromMilliseconds(1));
574+
575+
try
576+
{
577+
_ = session.ExecuteCommand("sleep 10");
578+
}
579+
catch (SshException e) when (e.Error == SshError.Timeout)
580+
{
581+
return Task.FromResult(true);
582+
}
557583

558-
var result = session.ExecuteCommand("sleep 10");
559-
560-
return Task.FromResult(result is { Successful: false });
584+
return Task.FromResult(false);
561585
}
562586

563587
private static Task<bool> TestEmptyFileTransfer()

NullOpsDevs.LibSsh.Test/TestHelper.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Security.Cryptography;
2+
using Microsoft.Extensions.Logging;
23
using NullOpsDevs.LibSsh.Credentials;
34

45
namespace NullOpsDevs.LibSsh.Test;
@@ -13,7 +14,12 @@ public static class TestHelper
1314
/// </summary>
1415
public static SshSession CreateAndConnect()
1516
{
16-
var session = new SshSession();
17+
ILogger? logger = null;
18+
19+
if (Environment.GetEnvironmentVariable("DEBUG_LOGGING") != null)
20+
logger = new AnsiConsoleLogger();
21+
22+
var session = new SshSession(logger);
1723
session.Connect(TestConfig.Host, TestConfig.Port);
1824
return session;
1925
}

NullOpsDevs.LibSsh/Exceptions/SshException.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ public class SshException(string message, SshError error, Exception? innerExcept
2121
/// Creates an <see cref="SshException"/> from the last error that occurred in the specified libssh2 session.
2222
/// </summary>
2323
/// <param name="session">Pointer to the libssh2 session.</param>
24+
/// <param name="additionalMessage">Additional message.</param>
2425
/// <returns>An <see cref="SshException"/> containing the error code and message from the session.</returns>
25-
internal static unsafe SshException FromLastSessionError(_LIBSSH2_SESSION* session)
26+
internal static unsafe SshException FromLastSessionError(_LIBSSH2_SESSION* session, string? additionalMessage = null)
2627
{
2728
sbyte* errorMsg = null;
2829
var errorMsgLen = 0;
2930
var errorCode = LibSshNative.libssh2_session_last_error(session, &errorMsg, &errorMsgLen, 0);
3031

31-
var errorText = errorMsg != null
32-
? Marshal.PtrToStringAnsi((IntPtr)errorMsg, errorMsgLen)
33-
: "Unknown error";
32+
var errorText = errorMsg != null ?
33+
Marshal.PtrToStringAnsi((IntPtr)errorMsg, errorMsgLen) :
34+
"Unknown error";
3435

35-
return new SshException($"[{(SshError)errorCode:G}] {errorText}", (SshError) errorCode);
36+
return new SshException($"{(additionalMessage != null ? $"{additionalMessage}: " : "")} [{(SshError)errorCode:G}] {errorText}", (SshError) errorCode);
3637
}
3738
}

NullOpsDevs.LibSsh/Extensions/LibSshExtensions.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,27 @@ internal static class LibSshExtensions
1212
/// Throws an <see cref="SshException"/> if the libssh2 return code indicates failure (negative value).
1313
/// </summary>
1414
/// <param name="return">The libssh2 function return code.</param>
15+
/// <param name="session">Libssh2 session.</param>
1516
/// <param name="message">Optional custom error message.</param>
1617
/// <param name="also">Optional action to execute before throwing the exception (e.g., cleanup).</param>
1718
/// <exception cref="SshException">Thrown when the return code is negative (indicates error).</exception>
18-
public static void ThrowIfNotSuccessful(this int @return, string? message = null, Action? also = null)
19+
public static unsafe void ThrowIfNotSuccessful(this int @return, SshSession session,
20+
string? message = null, Action? also = null)
1921
{
2022
if (@return >= 0)
2123
return;
2224

2325
also?.Invoke();
24-
throw new SshException(message ?? "Unhandled exception", (SshError)@return);
26+
27+
if (session.SessionPtr != null)
28+
{
29+
if (message != null)
30+
throw SshException.FromLastSessionError(session.SessionPtr, message);
31+
32+
throw SshException.FromLastSessionError(session.SessionPtr);
33+
}
34+
35+
throw new SshException(message ?? "Unknown error", (SshError)@return);
2536
}
2637

2738
/// <summary>

NullOpsDevs.LibSsh/SshSession.cs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public sealed class SshSession(ILogger? logger = null) : IDisposable
3636
public SshConnectionStatus ConnectionStatus { get; private set; }
3737

3838
internal unsafe _LIBSSH2_SESSION* SessionPtr { get; private set; }
39+
40+
private readonly Dictionary<SshMethod, string> methodPreferences = new();
3941

4042
private void EnsureInitialized()
4143
{
@@ -49,7 +51,7 @@ private void EnsureInitialized()
4951
var initResult = libssh2_init(0);
5052
logger?.LogDebug("libssh2_init returned: {InitResult}", initResult);
5153

52-
initResult.ThrowIfNotSuccessful("LibSSH2 initialization failed");
54+
initResult.ThrowIfNotSuccessful(this, "LibSSH2 initialization failed");
5355
libraryInitialized = true;
5456
}
5557
}
@@ -107,11 +109,29 @@ public unsafe void Connect(string host, int port)
107109
}
108110

109111
logger?.LogDebug("Connected to server: '{Host}:{Port}'", host, port);
112+
113+
logger?.LogDebug("Setting method preferences...");
114+
115+
foreach (var (method, pref) in methodPreferences)
116+
{
117+
logger?.LogDebug("Setting method preference for '{Method:G}' to '{Pref}'", method, pref);
118+
119+
using var prefBuffer = NativeBuffer.Allocate(pref);
120+
121+
var prefResult = libssh2_session_method_pref(newSession, (int) method, prefBuffer.AsPointer<sbyte>());
122+
123+
prefResult.ThrowIfNotSuccessful(this, $"Failed to set preference to '{method:G}' to '{pref}'", also: () =>
124+
{
125+
_ = libssh2_session_free(newSession);
126+
socket.Dispose();
127+
});
128+
}
129+
110130
logger?.LogDebug("Handshaking...");
111131

112132
var result = libssh2_session_handshake(newSession, (ulong)socket.Handle);
113133

114-
result.ThrowIfNotSuccessful("Failed to handshake with server", also: () =>
134+
result.ThrowIfNotSuccessful(this, "Failed to handshake with server", also: () =>
115135
{
116136
_ = libssh2_session_free(newSession);
117137
socket.Dispose();
@@ -191,15 +211,15 @@ public unsafe SshHostKey GetHostKey()
191211
/// This method must be called before connecting to the server. The session must be in <see cref="SshConnectionStatus.Disconnected"/> status.
192212
/// Use <see cref="SetSecureMethodPreferences"/> to apply a predefined set of secure defaults.
193213
/// </remarks>
194-
public unsafe void SetMethodPreferences(SshMethod method, string preferences)
214+
public void SetMethodPreferences(SshMethod method, string preferences)
195215
{
216+
if(string.IsNullOrWhiteSpace(preferences))
217+
throw new ArgumentException("Preferences cannot be empty", nameof(preferences));
218+
196219
EnsureInitialized();
197220
EnsureInStatus(SshConnectionStatus.Disconnected);
198221

199-
using var preferencesBuffer = NativeBuffer.Allocate(preferences);
200-
201-
var result = libssh2_session_method_pref(SessionPtr, (int) method, preferencesBuffer.AsPointer<sbyte>());
202-
result.ThrowIfNotSuccessful("Failed to set preferences");
222+
methodPreferences[method] = preferences;
203223
}
204224

205225
/// <summary>
@@ -368,7 +388,7 @@ public unsafe SshCommandResult ExecuteCommand(string command, CommandExecutionOp
368388
options.TerminalHeightPixels
369389
);
370390

371-
ptyResult.ThrowIfNotSuccessful("Failed to request PTY", also: () =>
391+
ptyResult.ThrowIfNotSuccessful(this, "Failed to request PTY", also: () =>
372392
{
373393
libssh2_channel_close(channel);
374394
libssh2_channel_wait_closed(channel);
@@ -389,7 +409,7 @@ public unsafe SshCommandResult ExecuteCommand(string command, CommandExecutionOp
389409
(uint)commandBytes.Length
390410
);
391411

392-
processStartupResult.ThrowIfNotSuccessful("Unable to execute command", also: () =>
412+
processStartupResult.ThrowIfNotSuccessful(this, "Unable to execute command", also: () =>
393413
{
394414
libssh2_channel_close(channel);
395415
libssh2_channel_wait_closed(channel);

0 commit comments

Comments
 (0)