# TWSSH Tutorials

Comprehensive tutorials and examples for the TWSSH SSH library.

## Table of Contents

1. [Interactive Shell](#interactive-shell)
2. [SFTP File Transfer](#sftp-file-transfer)
3. [SCP File Transfer](#scp-file-transfer)
4. [Port Forwarding](#port-forwarding)
5. [Executing Sudo Commands](#executing-sudo-commands)
6. [Batch Command Execution](#batch-command-execution)
7. [Connection Management](#connection-management)
8. [Host Key Verification](#host-key-verification)
9. [Working with Environment Variables](#working-with-environment-variables)
10. [File Operations via Commands](#file-operations-via-commands)
11. [Real-World Examples](#real-world-examples)
12. [Error Handling Best Practices](#error-handling-best-practices)
13. [Connection Pooling](#connection-pooling)

---

## Interactive Shell

The interactive shell provides a PTY (pseudo-terminal) for running commands that require terminal interaction.

### Basic Shell Session

```csharp
using TwSsh.Client;
using TwSsh.Authentication;

await using var client = new SshClient(settings);
await client.ConnectAndAuthenticateAsync();

// Create shell with default PTY settings
await using var shell = await client.CreateShellAsync();

// Wait for initial prompt
await shell.ExpectAsync("$", TimeSpan.FromSeconds(5));

// Execute commands
shell.WriteLine("hostname");
var hostname = await shell.ExpectAsync("$", TimeSpan.FromSeconds(5));
Console.WriteLine($"Hostname: {hostname}");

shell.WriteLine("uptime");
var uptime = await shell.ExpectAsync("$", TimeSpan.FromSeconds(5));
Console.WriteLine($"Uptime: {uptime}");
```

### Custom PTY Settings

```csharp
using TwSsh.Connection;  // For PtyRequest

var ptyRequest = new PtyRequest
{
    TerminalType = "xterm-256color",
    Columns = 120,      // or WidthChars = 120 (they're aliases)
    Rows = 40,          // or HeightRows = 40 (they're aliases)
    WidthPixels = 0,
    HeightPixels = 0
};

await using var shell = await client.CreateShellAsync(ptyRequest);
```

**PtyRequest Property Aliases:**
- `Columns` and `WidthChars` are equivalent (terminal width in characters)
- `Rows` and `HeightRows` are equivalent (terminal height in rows)

Both naming conventions are valid - use whichever feels more natural.

### Pattern Matching with Regex

```csharp
using System.Text.RegularExpressions;

await using var shell = await client.CreateShellAsync();

// Wait for username@hostname prompt
var promptPattern = new Regex(@"\w+@\w+[:\$#]\s*$");
await shell.ExpectAsync(promptPattern, TimeSpan.FromSeconds(5));

shell.WriteLine("cat /etc/os-release");

// Wait for output containing PRETTY_NAME
var osPattern = new Regex(@"PRETTY_NAME=""([^""]+)""");
var output = await shell.ExpectAsync(osPattern, TimeSpan.FromSeconds(5));

// ExpectAsync returns the matched text as string, use regex to extract groups
if (output != null)
{
    var match = osPattern.Match(output);
    if (match.Success)
    {
        Console.WriteLine($"OS: {match.Groups[1].Value}");
    }
}
```

### Handling ANSI Escape Codes in Prompts

Shell prompts often include ANSI escape sequences for colors and formatting. These sequences can cause simple string matching to fail.

```csharp
// Problem: Simple string patterns may not match colored prompts
await shell.ExpectAsync("$");  // May timeout if prompt includes colors!

// Solution 1: Use regex that matches the end of the prompt
var promptPattern = new Regex(@"[\$#>]\s*$");
await shell.ExpectAsync(promptPattern, TimeSpan.FromSeconds(5));

// Solution 2: Use a more robust pattern for common shells
var robustPrompt = new Regex(@"(?:\x1b\[[0-9;]*m)*[$#>]\s*$");
await shell.ExpectAsync(robustPrompt, TimeSpan.FromSeconds(5));

// Solution 3: Wait for specific command output instead of prompt
shell.WriteLine("echo COMMAND_DONE");
await shell.ExpectAsync("COMMAND_DONE", TimeSpan.FromSeconds(10));
```

### Timeout Handling for Expect

```csharp
try
{
    // Always specify an appropriate timeout
    await shell.ExpectAsync("$", TimeSpan.FromSeconds(10));
}
catch (TimeoutException)
{
    Console.WriteLine("Prompt not received within timeout");
    // Read whatever data is available
    if (shell.DataAvailable)
    {
        var buffer = new byte[4096];
        int bytesRead = await shell.ReadAsync(buffer, 0, buffer.Length);
        Console.WriteLine($"Received: {Encoding.UTF8.GetString(buffer, 0, bytesRead)}");
    }
}
```

### Handling Menu-Based Applications

```csharp
await using var shell = await client.CreateShellAsync();

// Launch a menu application
shell.WriteLine("top -b -n 1");

// Wait for header
await shell.ExpectAsync("load average", TimeSpan.FromSeconds(5));

// Read some output
var buffer = new byte[4096];
int bytesRead = await shell.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));

// Send quit command (Write accepts bytes, use WriteLine for strings with newline)
shell.Write(Encoding.UTF8.GetBytes("q"));
```

### Window Size Changes

```csharp
await using var shell = await client.CreateShellAsync();

// Initial size
await shell.SendWindowChangeAsync(80, 24);

// Later, resize
await shell.SendWindowChangeAsync(120, 40);
```

---

## SFTP File Transfer

TWSSH provides a complete, high-level SFTP client for secure file transfers. The `SftpClient` class offers a rich API for uploading, downloading, and managing remote files and directories.

**Note:** SFTP requires a Team or Enterprise license.

### Creating an SFTP Client

```csharp
using TwSsh.Client;
using TwSsh.Sftp;

await using var sshClient = new SshClient(settings);
await sshClient.ConnectAndAuthenticateAsync();

// Create SFTP client from authenticated SSH connection
await using var sftp = await SftpClient.ConnectAsync(sshClient);

Console.WriteLine($"SFTP Protocol Version: {sftp.ProtocolVersion}");
Console.WriteLine($"Connected: {sftp.IsConnected}");
```

### Uploading Files

The `UploadFileAsync` method has several overloads for different scenarios:

```csharp
// Overload 1: Simple upload (localPath, remotePath)
await sftp.UploadFileAsync("C:/local/document.pdf", "/home/user/document.pdf");

// Overload 2: Upload with progress (localPath, remotePath, progress)
var progress = new Progress<SftpTransferProgress>(p =>
{
    Console.WriteLine($"{p.FileName}: {p.PercentComplete:F1}% - {p.BytesPerSecond / 1024:F1} KB/s");
});
await sftp.UploadFileAsync("C:/local/large-file.zip", "/home/user/large-file.zip", progress);

// Overload 3: Upload with options and progress (localPath, remotePath, options, progress)
var options = new SftpTransferOptions { BufferSize = 65536, PreserveTimestamps = true };
await sftp.UploadFileAsync("C:/local/file.zip", "/home/user/file.zip", options, progress);
```

**Method Signatures:**
```csharp
// Basic upload
ValueTask UploadFileAsync(string localPath, string remotePath, CancellationToken ct = default);

// With progress only
ValueTask UploadFileAsync(string localPath, string remotePath, IProgress<SftpTransferProgress> progress, CancellationToken ct = default);

// With options and optional progress
ValueTask UploadFileAsync(string localPath, string remotePath, SftpTransferOptions options, IProgress<SftpTransferProgress>? progress = null, CancellationToken ct = default);
```

### Downloading Files

```csharp
// Simple download
await sftp.DownloadFileAsync("/home/user/report.csv", "C:/local/report.csv");

// Download with progress
var progress = new Progress<SftpTransferProgress>(p =>
{
    var eta = p.EstimatedRemaining?.ToString(@"mm\:ss") ?? "calculating...";
    Console.WriteLine($"Downloading: {p.PercentComplete:F1}% - ETA: {eta}");
});

await sftp.DownloadFileAsync("/home/user/backup.tar.gz", "C:/local/backup.tar.gz", progress);
```

### Resume Interrupted Transfers

```csharp
using TwSsh.Sftp;

// Resume a download that was interrupted
var options = SftpTransferOptions.ResumeDownload(existingFileSize);
await sftp.DownloadFileAsync("/remote/large-file.iso", "C:/local/large-file.iso", options);

// Resume an upload
var uploadOptions = SftpTransferOptions.ResumeUpload(existingRemoteSize);
await sftp.UploadFileAsync("C:/local/large-file.iso", "/remote/large-file.iso", uploadOptions);
```

### Directory Operations

```csharp
// Create directories
await sftp.CreateDirectoryAsync("/home/user/new-folder");
await sftp.CreateDirectoryAsync("/home/user/project/src", permissions: 0755);

// Delete directories
await sftp.DeleteDirectoryAsync("/home/user/old-folder");

// List directory contents
var entries = await sftp.ListDirectoryAsync("/home/user");
foreach (var entry in entries)
{
    var type = entry.IsDirectory ? "DIR " : "FILE";
    Console.WriteLine($"{type} {entry.Size,10} {entry.FileName}");
}

// Enumerate with async stream (memory efficient for large directories)
await foreach (var entry in sftp.EnumerateDirectoryAsync("/var/log"))
{
    if (entry.FileName.EndsWith(".log"))
    {
        Console.WriteLine($"Log file: {entry.FileName}");
    }
}
```

### File Operations

```csharp
// Check if file/directory exists
if (await sftp.ExistsAsync("/home/user/config.json"))
{
    Console.WriteLine("Config file exists");
}

// Get file information
var info = await sftp.GetFileInfoAsync("/home/user/document.pdf");
Console.WriteLine($"Size: {info.Length} bytes");
Console.WriteLine($"Modified: {info.LastWriteTime}");
Console.WriteLine($"Permissions: {info.Permissions}");

// Rename/move files
await sftp.RenameAsync("/home/user/old-name.txt", "/home/user/new-name.txt");

// Delete files
await sftp.DeleteFileAsync("/home/user/temp-file.txt");

// Read file directly to memory
var data = await sftp.ReadAllBytesAsync("/home/user/small-config.json");
var json = Encoding.UTF8.GetString(data);

// Write data directly
var content = Encoding.UTF8.GetBytes("Hello, World!");
await sftp.WriteAllBytesAsync("/home/user/greeting.txt", content);
```

### Symbolic Links

```csharp
// Create symbolic link
await sftp.CreateSymbolicLinkAsync("/home/user/link", "/home/user/target");

// Read link target
var target = await sftp.ReadSymbolicLinkAsync("/home/user/link");
Console.WriteLine($"Link points to: {target}");
```

### Stream-Based Access

```csharp
// Open file for reading as a Stream
await using var readStream = await sftp.OpenReadAsync("/home/user/data.bin");
var buffer = new byte[4096];
int bytesRead = await readStream.ReadAsync(buffer, 0, buffer.Length);

// Open file for writing
await using var writeStream = await sftp.OpenWriteAsync("/home/user/output.bin");
await writeStream.WriteAsync(data, 0, data.Length);

// Full control over file mode and access
await using var stream = await sftp.OpenFileAsync(
    "/home/user/file.dat",
    FileMode.OpenOrCreate,
    FileAccess.ReadWrite);

stream.Seek(100, SeekOrigin.Begin);
await stream.WriteAsync(newData, 0, newData.Length);
```

### Batch Operations

Upload or download multiple files with a single call:

```csharp
// Progress handler for batch operations (reports per-file progress)
var progress = new Progress<SftpTransferProgress>(p =>
{
    Console.WriteLine($"[{p.FileName}] {p.PercentComplete:F1}% - {p.BytesPerSecond / 1024:F1} KB/s");
});

// Upload multiple specific files to a directory
var filesToUpload = new[] { "report.pdf", "data.csv", "image.png" }
    .Select(f => Path.Combine("C:/local/exports", f));

await sftp.UploadFilesAsync(filesToUpload, "/home/user/uploads/", progress);

// Upload all files matching a pattern from a directory
await sftp.UploadFilesAsync(
    localDirectory: "C:/local/logs",
    remoteDirectory: "/var/backups/logs",
    pattern: "*.log",           // Only .log files
    recursive: false,           // Don't include subdirectories
    progress: progress);

// Upload entire directory tree recursively
await sftp.UploadFilesAsync(
    localDirectory: "C:/local/website",
    remoteDirectory: "/var/www/html",
    pattern: "*",               // All files
    recursive: true,            // Include all subdirectories
    progress: progress);

// Download multiple specific files
var filesToDownload = new[] { "/home/user/doc1.pdf", "/home/user/doc2.pdf" };
await sftp.DownloadFilesAsync(filesToDownload, "C:/local/downloads/", progress);

// Download all files from a remote directory
await sftp.DownloadFilesAsync(
    remoteDirectory: "/var/log/app",
    localDirectory: "C:/local/log-backup",
    pattern: "*.log",
    recursive: true,
    progress: progress);
```

### Directory Synchronization

```csharp
// Sync local directory to remote (like rsync)
await sftp.SyncToRemoteAsync(
    "C:/local/project",
    "/home/user/project",
    deleteExtra: true,  // Remove remote files not in local
    progress);
```

### Filesystem Information

```csharp
// Get filesystem statistics (requires statvfs@openssh.com extension)
var fsInfo = await sftp.GetFilesystemInfoAsync("/home/user");
if (fsInfo != null)
{
    Console.WriteLine($"Total: {fsInfo.TotalSize / 1024 / 1024 / 1024} GB");
    Console.WriteLine($"Free: {fsInfo.FreeSize / 1024 / 1024 / 1024} GB");
    Console.WriteLine($"Usage: {fsInfo.UsagePercent:F1}%");
}
```

### Feature Detection

```csharp
// Check server capabilities
var caps = sftp.Capabilities;

Console.WriteLine($"Protocol Version: {caps.ProtocolVersion}");
Console.WriteLine($"Supports Symlinks: {caps.SupportsSymlinks}");
Console.WriteLine($"Supports POSIX Rename: {caps.SupportsPosixRename}");
Console.WriteLine($"Supports StatVfs: {caps.SupportsStatVfs}");
Console.WriteLine($"Supports Hard Links: {caps.SupportsHardLink}");

// Check specific extension
if (caps.SupportsExtension("posix-rename@openssh.com"))
{
    await sftp.PosixRenameAsync("/old/path", "/new/path");
}
```

### Advanced Transfer Options

```csharp
var options = new SftpTransferOptions
{
    BufferSize = 65536,          // 64 KB buffer
    Resume = true,               // Resume interrupted transfers
    PreserveTimestamps = true,   // Keep original file timestamps
    Overwrite = true,            // Overwrite existing files
    VerifySize = true            // Verify file size after transfer
};

await sftp.UploadFileAsync("local.zip", "/remote/file.zip", options, progress);
```

---

## SCP File Transfer

SCP (Secure Copy Protocol) provides simple file copy operations over SSH. While SFTP is recommended for most use cases due to its richer feature set, SCP can be useful for:
- Legacy system compatibility
- Simple one-off file transfers
- Environments where SFTP is disabled

### Basic SCP Operations

```csharp
using TwSsh.Client;
using TwSsh.Scp;

await using var client = new SshClient(settings);
await client.ConnectAndAuthenticateAsync();

// Create SCP client from authenticated SSH connection
await using var scp = await ScpClient.ConnectAsync(client);

// Upload a file
await scp.UploadAsync("C:/local/file.txt", "/remote/path/file.txt");

// Download a file
await scp.DownloadAsync("/remote/path/file.txt", "C:/local/downloaded.txt");

// Upload with progress
var progress = new Progress<ScpTransferProgress>(p =>
    Console.WriteLine($"{p.FileName}: {p.BytesTransferred}/{p.TotalBytes}"));

await scp.UploadAsync("large-file.zip", "/remote/large-file.zip", progress);
```

### SCP vs SFTP

| Feature | SCP | SFTP |
|---------|-----|------|
| File upload/download | Yes | Yes |
| Directory listing | No | Yes |
| File attributes | Limited | Full |
| Resume transfers | No | Yes |
| Rename/delete files | No | Yes |
| Directory operations | No | Yes |
| Random access | No | Yes |
| Progress reporting | Yes | Yes |

**Recommendation:** Use SFTP unless you have a specific reason to use SCP. SFTP provides a more complete file management API.

---

## Port Forwarding

Port forwarding creates secure tunnels through SSH.

### Local Port Forwarding (SSH -L)

Forward a local port to a remote destination. Useful for accessing services behind firewalls.

```csharp
// Scenario: Access remote MySQL through SSH tunnel
// Equivalent to: ssh -L 3307:127.0.0.1:3306 user@jumphost

await using var client = new SshClient(settings);
await client.ConnectAndAuthenticateAsync();

// Forward local port 3307 to remote MySQL port 3306
await using var tunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 3307,    // Local endpoint
    "127.0.0.1", 3306);   // Remote endpoint

Console.WriteLine($"Tunnel active: localhost:{tunnel.LocalPort} -> remote:{tunnel.RemotePort}");

// Now connect to MySQL at localhost:3307
var connectionString = "Server=127.0.0.1;Port=3307;Database=mydb;User=root;Password=secret;";

using var mysqlConnection = new MySqlConnection(connectionString);
await mysqlConnection.OpenAsync();
// ... use MySQL
```

### Forwarding to Different Host

```csharp
// Access a database server through SSH jump host
// Equivalent to: ssh -L 5432:db-server.internal:5432 user@jumphost

await using var tunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 5432,           // Local endpoint
    "db-server.internal", 5432); // Remote endpoint (internal network)

// PostgreSQL is now accessible at localhost:5432
```

### Multiple Tunnels

```csharp
await using var client = new SshClient(settings);
await client.ConnectAndAuthenticateAsync();

// Create multiple tunnels
await using var mysqlTunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 3307, "127.0.0.1", 3306);

await using var redisTunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 6380, "127.0.0.1", 6379);

await using var httpTunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 8080, "127.0.0.1", 80);

Console.WriteLine("All tunnels active:");
Console.WriteLine($"  MySQL: localhost:3307");
Console.WriteLine($"  Redis: localhost:6380");
Console.WriteLine($"  HTTP:  localhost:8080");

// Keep tunnels alive
Console.WriteLine("Press Enter to close tunnels...");
Console.ReadLine();
```

### Remote Port Forwarding (SSH -R)

Allow remote server to access local services.

```csharp
// Scenario: Expose local development server to remote
// Equivalent to: ssh -R 8080:127.0.0.1:3000 user@server

await using var client = new SshClient(settings);
await client.ConnectAndAuthenticateAsync();

// Remote port 8080 will forward to local port 3000
await using var tunnel = await client.ForwardRemotePortAsync(
    "0.0.0.0", 8080,     // Remote endpoint (listen on all interfaces)
    "127.0.0.1", 3000);  // Local endpoint

Console.WriteLine($"Remote port {tunnel.RemotePort} forwarded to local port {tunnel.LocalPort}");
Console.WriteLine($"Access at: http://server-address:8080");

// Keep tunnel alive
Console.ReadLine();
```

### Error Handling for Tunnels

```csharp
var tunnel = await client.ForwardLocalPortAsync(
    "127.0.0.1", 8080, "127.0.0.1", 80);

tunnel.ErrorOccurred += (sender, ex) =>
{
    Console.WriteLine($"Tunnel error: {ex.Message}");
};

// Use tunnel...
```

---

## Executing Sudo Commands

Running commands with elevated privileges.

### Method 1: Echo Password to Sudo

```csharp
var password = "user_password";

// Use sudo -S to read password from stdin
var result = await client.RunCommandAsync(
    $"echo '{password}' | sudo -S whoami 2>/dev/null");

Console.WriteLine($"Running as: {result.Output.Trim()}"); // Should print "root"
```

### Method 2: Using Shell with Sudo

```csharp
await using var shell = await client.CreateShellAsync();

// Wait for initial prompt
await shell.ExpectAsync("$");

// Start sudo command
shell.WriteLine("sudo -S cat /etc/shadow");

// Wait for password prompt
await shell.ExpectAsync("password", TimeSpan.FromSeconds(5));

// Send password
shell.WriteLine(password);

// Wait for result
var output = await shell.ExpectAsync("$", TimeSpan.FromSeconds(5));
Console.WriteLine(output);
```

### Method 3: Pre-configured Passwordless Sudo

If the remote user has NOPASSWD sudo access:

```csharp
// No password needed
var result = await client.RunCommandAsync("sudo apt update");
Console.WriteLine(result.Output);
```

### Checking Sudo Access

```csharp
// Check if user has sudo access
var result = await client.RunCommandAsync("groups");

if (result.Output.Contains("sudo") || result.Output.Contains("wheel"))
{
    Console.WriteLine("User has sudo access");
}
```

---

## Batch Command Execution

Efficiently execute multiple commands.

### Sequential Execution

```csharp
var commands = new[]
{
    "cd /var/log",
    "ls -la",
    "wc -l *.log",
    "df -h"
};

var results = new List<SshCommandResult>();

foreach (var cmd in commands)
{
    var result = await client.RunCommandAsync(cmd);
    results.Add(result);

    if (!result.IsSuccess)
    {
        Console.WriteLine($"Command '{cmd}' failed with code {result.ExitCode}");
        break;
    }
}
```

### Compound Commands

```csharp
// Execute multiple commands in a single call
var script = @"
    cd /var/log &&
    echo '=== System Logs ===' &&
    tail -5 syslog &&
    echo '=== Auth Logs ===' &&
    tail -5 auth.log
";

var result = await client.RunCommandAsync(script.Replace("\n", " "));
Console.WriteLine(result.Output);
```

### Parallel Execution

```csharp
// Execute commands in parallel on same connection
// Note: RunCommandAsync returns ValueTask, so we convert to Task for Task.WhenAll
var tasks = new[]
{
    client.RunCommandAsync("hostname").AsTask(),
    client.RunCommandAsync("uptime").AsTask(),
    client.RunCommandAsync("free -h").AsTask(),
    client.RunCommandAsync("df -h /").AsTask()
};

var results = await Task.WhenAll(tasks);

Console.WriteLine($"Hostname: {results[0].Output.Trim()}");
Console.WriteLine($"Uptime: {results[1].Output.Trim()}");
Console.WriteLine($"Memory: {results[2].Output.Trim()}");
Console.WriteLine($"Disk: {results[3].Output.Trim()}");
```

**Note:** Most async methods in TWSSH return `ValueTask` rather than `Task` for performance reasons. When using `Task.WhenAll` or other Task-based combinators, use `.AsTask()` to convert. Standard .NET methods like `NetworkStream.ReadAsync` typically return `Task<int>` directly, so `.AsTask()` is not needed for those.

### Script Execution

```csharp
// Execute a bash script
var script = @"#!/bin/bash
for i in {1..5}; do
    echo ""Iteration $i""
    sleep 1
done
echo ""Done!""
";

// Write script and execute
var base64Script = Convert.ToBase64String(Encoding.UTF8.GetBytes(script));
var result = await client.RunCommandAsync(
    $"echo '{base64Script}' | base64 -d | bash");

Console.WriteLine(result.Output);
```

---

## Connection Management

### Keep-Alive Configuration

```csharp
var settings = new SshClientSettings
{
    Host = "example.com",
    Credential = credential,
    KeepAliveInterval = TimeSpan.FromSeconds(30),
    ConnectionTimeout = TimeSpan.FromSeconds(30),
    OperationTimeout = TimeSpan.FromMinutes(5)
};
```

### Handling Disconnection

```csharp
var client = new SshClient(settings);

client.Disconnected += (sender, args) =>
{
    Console.WriteLine($"Disconnected: {args.Reason}");

    // Attempt reconnection if appropriate
    if (args.Reason != SshDisconnectReason.ByApplication)
    {
        // Schedule reconnection
        _ = Task.Run(async () =>
        {
            await Task.Delay(5000);
            // Reconnect logic...
        });
    }
};

await client.ConnectAndAuthenticateAsync();
```

### Connection State Monitoring

```csharp
void MonitorConnection(SshClient client)
{
    Console.WriteLine($"State: {client.State}");
    Console.WriteLine($"Connected: {client.IsConnected}");
    Console.WriteLine($"Authenticated: {client.IsAuthenticated}");

    if (client.ConnectionInfo != null)
    {
        var info = client.ConnectionInfo;
        Console.WriteLine($"Server: {info.ServerVersion}");
        Console.WriteLine($"User: {info.Username}");
        Console.WriteLine($"Algorithms: {info.Algorithms}");
    }
}
```

---

## Host Key Verification

### Strict Verification with Known Hosts

```csharp
var knownHosts = new Dictionary<string, string>
{
    ["192.168.1.100"] = "SHA256:AbCdEfGhIjKlMnOpQrStUvWxYz1234567890",
    ["server.example.com"] = "SHA256:XyZaBcDeFgHiJkLmNoPqRsTuVwXyZ098765"
};

var settings = new SshClientSettings
{
    Host = "192.168.1.100",
    Credential = credential,
    HostKeyVerification = HostKeyVerificationMode.Callback,
    HostKeyCallback = (args) =>
    {
        if (knownHosts.TryGetValue(args.Host, out var expectedFingerprint))
        {
            return args.Fingerprint == expectedFingerprint;
        }

        // Unknown host - prompt user
        Console.WriteLine($"Unknown host: {args.Host}");
        Console.WriteLine($"Key type: {args.KeyType}");
        Console.WriteLine($"Fingerprint: {args.Fingerprint}");
        Console.Write("Trust this host? (yes/no): ");

        var response = Console.ReadLine();
        if (response?.ToLower() == "yes")
        {
            // Add to known hosts
            knownHosts[args.Host] = args.Fingerprint;
            SaveKnownHosts(knownHosts);
            return true;
        }

        return false;
    }
};
```

### Trust on First Use (TOFU)

```csharp
var settings = new SshClientSettings
{
    Host = "example.com",
    Credential = credential,
    HostKeyVerification = HostKeyVerificationMode.AutoAdd
};

// First connection: automatically accepts and stores key
// Subsequent connections: verifies against stored key
```

### Disable Verification (Not Recommended)

```csharp
// SECURITY WARNING: Only use for testing!
var settings = new SshClientSettings
{
    Host = "example.com",
    Credential = credential,
    HostKeyVerification = HostKeyVerificationMode.None
};
```

---

## Working with Environment Variables

### Reading Environment Variables

```csharp
// Get specific variable
var result = await client.RunCommandAsync("echo $HOME");
Console.WriteLine($"Home: {result.Output.Trim()}");

// Get all environment variables
result = await client.RunCommandAsync("env");
var envVars = result.Output
    .Split('\n')
    .Where(line => line.Contains('='))
    .Select(line => line.Split('=', 2))
    .ToDictionary(parts => parts[0], parts => parts[1]);

foreach (var (key, value) in envVars.Take(5))
{
    Console.WriteLine($"{key}={value}");
}
```

### Setting Environment Variables

```csharp
// Set for single command
var result = await client.RunCommandAsync(
    "MY_VAR='custom_value' && echo $MY_VAR");

// Using export (persists in shell session)
await using var shell = await client.CreateShellAsync();
await shell.ExpectAsync("$");

shell.WriteLine("export MY_APP_MODE='production'");
await shell.ExpectAsync("$");

shell.WriteLine("echo $MY_APP_MODE");
var output = await shell.ExpectAsync("$");
Console.WriteLine(output); // production
```

---

## File Operations via Commands

### Check File Existence

```csharp
var result = await client.RunCommandAsync("test -f /etc/passwd && echo 'exists'");
bool exists = result.Output.Contains("exists");
```

### Get File Information

```csharp
var result = await client.RunCommandAsync("stat /etc/passwd");
Console.WriteLine(result.Output);
```

### Read File Content

```csharp
var result = await client.RunCommandAsync("cat /etc/hostname");
Console.WriteLine($"Hostname: {result.Output.Trim()}");

// Read with line numbers
result = await client.RunCommandAsync("cat -n /etc/hosts");
Console.WriteLine(result.Output);
```

### Write File Content

```csharp
var content = "Hello from TWSSH!";

// Simple write
await client.RunCommandAsync($"echo '{content}' > /tmp/test.txt");

// Append
await client.RunCommandAsync($"echo 'Additional line' >> /tmp/test.txt");

// Multi-line content using heredoc
var multiline = @"Line 1
Line 2
Line 3";

var escaped = multiline.Replace("'", "'\"'\"'");
await client.RunCommandAsync($"cat << 'EOF' > /tmp/multi.txt\n{multiline}\nEOF");
```

### File Permissions

```csharp
// Change permissions
await client.RunCommandAsync("chmod 755 /tmp/script.sh");

// Change owner
await client.RunCommandAsync("sudo chown user:group /tmp/file.txt");

// Check permissions
var result = await client.RunCommandAsync("ls -la /tmp/script.sh");
Console.WriteLine(result.Output);
```

### Directory Operations

```csharp
// Create directory
await client.RunCommandAsync("mkdir -p /tmp/myapp/logs");

// List directory
var result = await client.RunCommandAsync("ls -la /tmp/myapp");
Console.WriteLine(result.Output);

// Remove directory
await client.RunCommandAsync("rm -rf /tmp/myapp");
```

---

## Real-World Examples

### Example 1: Server Health Check

```csharp
public class ServerHealthCheck
{
    private readonly SshClient _client;

    public ServerHealthCheck(SshClient client)
    {
        _client = client;
    }

    public async Task<HealthReport> CheckAsync()
    {
        var report = new HealthReport();

        // CPU usage
        var cpuResult = await _client.RunCommandAsync(
            "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1");
        report.CpuUsage = double.Parse(cpuResult.Output.Trim());

        // Memory usage
        var memResult = await _client.RunCommandAsync(
            "free | grep Mem | awk '{print $3/$2 * 100.0}'");
        report.MemoryUsage = double.Parse(memResult.Output.Trim());

        // Disk usage
        var diskResult = await _client.RunCommandAsync(
            "df / | tail -1 | awk '{print $5}' | tr -d '%'");
        report.DiskUsage = double.Parse(diskResult.Output.Trim());

        // Uptime
        var uptimeResult = await _client.RunCommandAsync(
            "uptime -p");
        report.Uptime = uptimeResult.Output.Trim();

        // Process count
        var procResult = await _client.RunCommandAsync(
            "ps aux | wc -l");
        report.ProcessCount = int.Parse(procResult.Output.Trim());

        return report;
    }
}

public class HealthReport
{
    public double CpuUsage { get; set; }
    public double MemoryUsage { get; set; }
    public double DiskUsage { get; set; }
    public string Uptime { get; set; }
    public int ProcessCount { get; set; }
}
```

### Example 2: Log Monitoring

```csharp
public class LogMonitor
{
    public async Task MonitorLogsAsync(SshClient client, string logFile,
        Action<string> onLogLine, CancellationToken ct)
    {
        await using var shell = await client.CreateShellAsync();

        // Start tail -f
        shell.WriteLine($"tail -f {logFile}");

        var buffer = new byte[4096];

        while (!ct.IsCancellationRequested)
        {
            if (shell.DataAvailable)
            {
                int bytesRead = await shell.ReadAsync(buffer, 0, buffer.Length, ct);
                var text = Encoding.UTF8.GetString(buffer, 0, bytesRead);

                foreach (var line in text.Split('\n', StringSplitOptions.RemoveEmptyEntries))
                {
                    onLogLine(line);
                }
            }
            else
            {
                await Task.Delay(100, ct);
            }
        }
    }
}

// Usage
var monitor = new LogMonitor();
await monitor.MonitorLogsAsync(client, "/var/log/syslog",
    line => Console.WriteLine($"[LOG] {line}"),
    cancellationToken);
```

### Example 3: Deployment Script

```csharp
public class Deployer
{
    private readonly SshClient _client;
    private readonly string _password;

    public Deployer(SshClient client, string sudoPassword)
    {
        _client = client;
        _password = sudoPassword;
    }

    public async Task DeployAsync(string appPath, string version)
    {
        Console.WriteLine($"Deploying version {version}...");

        // Stop service
        await RunSudoAsync("systemctl stop myapp");
        Console.WriteLine("  Service stopped");

        // Backup current version
        await _client.RunCommandAsync(
            $"cp -r {appPath} {appPath}.backup.$(date +%Y%m%d_%H%M%S)");
        Console.WriteLine("  Backup created");

        // Deploy new version (assuming files are uploaded)
        // In real scenario, use SFTP or SCP for file transfer
        Console.WriteLine("  Files deployed");

        // Set permissions
        await RunSudoAsync($"chown -R www-data:www-data {appPath}");
        Console.WriteLine("  Permissions set");

        // Start service
        await RunSudoAsync("systemctl start myapp");
        Console.WriteLine("  Service started");

        // Verify
        var result = await _client.RunCommandAsync("systemctl is-active myapp");
        if (result.Output.Trim() == "active")
        {
            Console.WriteLine($"Deployment of version {version} successful!");
        }
        else
        {
            throw new Exception("Service failed to start");
        }
    }

    private async Task<SshCommandResult> RunSudoAsync(string command)
    {
        return await _client.RunCommandAsync(
            $"echo '{_password}' | sudo -S {command} 2>/dev/null");
    }
}
```

### Example 4: Database Backup via SSH Tunnel

```csharp
public async Task BackupDatabaseAsync()
{
    var settings = new SshClientSettings
    {
        Host = "jump-host.example.com",
        Credential = new PasswordCredential("user", "password")
    };

    await using var client = new SshClient(settings);
    await client.ConnectAndAuthenticateAsync();

    // Create tunnel to database server
    await using var tunnel = await client.ForwardLocalPortAsync(
        "127.0.0.1", 3307,
        "db.internal", 3306);

    Console.WriteLine("Tunnel established, starting backup...");

    // Now use mysqldump through the tunnel
    var process = new ProcessStartInfo
    {
        FileName = "mysqldump",
        Arguments = "-h 127.0.0.1 -P 3307 -u dbuser -p'dbpassword' mydb",
        RedirectStandardOutput = true,
        UseShellExecute = false
    };

    using var proc = Process.Start(process);
    var backup = await proc.StandardOutput.ReadToEndAsync();

    await File.WriteAllTextAsync($"backup_{DateTime.Now:yyyyMMdd}.sql", backup);
    Console.WriteLine("Backup completed!");
}
```

---

## Best Practices

### 1. Always Dispose Clients

```csharp
// Use await using for automatic disposal
await using var client = new SshClient(settings);

// Or explicit disposal
var client = new SshClient(settings);
try
{
    // Use client
}
finally
{
    await client.DisposeAsync();
}
```

### 2. Use Cancellation Tokens

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));

try
{
    await client.ConnectAndAuthenticateAsync(cts.Token);
    var result = await client.RunCommandAsync("long_command", cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled");
}
```

### 3. Handle Credentials Securely

```csharp
// Use secure string for passwords where possible
// Clear sensitive data when done
var credential = new PasswordCredential("user", password);

try
{
    // Use credential
}
finally
{
    // Clear password from memory if using custom implementation
}
```

### 4. Validate Inputs

```csharp
// Escape shell arguments to prevent injection
public static string EscapeShellArg(string arg)
{
    return "'" + arg.Replace("'", "'\"'\"'") + "'";
}

var filename = EscapeShellArg(userInput);
var result = await client.RunCommandAsync($"cat {filename}");
```

---

## Error Handling Best Practices

Proper error handling is essential for building robust SSH applications. This section covers common error scenarios and recommended handling strategies.

### Exception Hierarchy

TWSSH uses a clear exception hierarchy:

```
Exception
├── SshException (base for all SSH errors)
│   ├── SshConnectionException  - Connection/network errors
│   ├── SshAuthenticationException - Auth failures
│   ├── SshChannelException - Channel operation errors
│   └── SftpException - SFTP-specific errors
└── LicenseException - License errors
```

### Comprehensive Error Handling

```csharp
using TwSsh.Client;
using TwSsh.Authentication;
using TwSsh.Common;
using TwSsh.Licensing;

public async Task<bool> ConnectWithRetry(SshClientSettings settings, int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            await using var client = new SshClient(settings);
            await client.ConnectAndAuthenticateAsync();

            // Connection successful
            return true;
        }
        catch (SshAuthenticationException ex)
        {
            // Authentication failed - don't retry (credentials are wrong)
            Console.WriteLine($"Authentication failed: {ex.Message}");
            Console.WriteLine($"Allowed methods: {string.Join(", ", ex.AllowedMethods ?? Array.Empty<string>())}");
            return false;
        }
        catch (SshConnectionException ex) when (attempt < maxRetries)
        {
            // Network error - retry with exponential backoff
            Console.WriteLine($"Connection attempt {attempt} failed: {ex.Message}");
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
        catch (SshChannelException ex)
        {
            // Channel error - may indicate server issue
            Console.WriteLine($"Channel error: {ex.Message}");
            Console.WriteLine($"Channel ID: {ex.ChannelId}, Reason: {ex.ReasonCode}");
            return false;
        }
        catch (LicenseException ex)
        {
            // License error - cannot proceed
            Console.WriteLine($"License error: {ex.Message}");
            throw; // Re-throw - this is a configuration issue
        }
        catch (TimeoutException)
        {
            Console.WriteLine($"Connection attempt {attempt} timed out");
            if (attempt == maxRetries) return false;
        }
    }

    return false;
}
```

### Handling Transient Failures

For long-running applications, implement reconnection logic:

```csharp
public class ResilientSshConnection
{
    private SshClient? _client;
    private readonly SshClientSettings _settings;
    private readonly SemaphoreSlim _lock = new(1, 1);

    public ResilientSshConnection(SshClientSettings settings)
    {
        _settings = settings;
    }

    public async Task<SshClient> GetConnectionAsync()
    {
        await _lock.WaitAsync();
        try
        {
            if (_client == null || !_client.IsConnected)
            {
                _client?.Dispose();
                _client = new SshClient(_settings);
                await _client.ConnectAndAuthenticateAsync();
            }
            return _client;
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task<SshCommandResult> RunCommandSafeAsync(string command)
    {
        try
        {
            var client = await GetConnectionAsync();
            return await client.RunCommandAsync(command);
        }
        catch (SshConnectionException)
        {
            // Connection lost - reconnect and retry once
            _client?.Dispose();
            _client = null;
            var client = await GetConnectionAsync();
            return await client.RunCommandAsync(command);
        }
    }
}
```

### SFTP Error Handling

```csharp
using TwSsh.Sftp;

public async Task SafeUploadAsync(SftpClient sftp, string local, string remote)
{
    try
    {
        // Check if remote directory exists
        var remoteDir = Path.GetDirectoryName(remote)?.Replace("\\", "/");
        if (!string.IsNullOrEmpty(remoteDir))
        {
            if (!await sftp.ExistsAsync(remoteDir))
            {
                await sftp.CreateDirectoryAsync(remoteDir);
            }
        }

        await sftp.UploadFileAsync(local, remote);
    }
    catch (SftpException ex) when (ex.StatusCode == SftpStatusCode.NoSuchFile)
    {
        Console.WriteLine($"Path not found: {ex.Message}");
        throw;
    }
    catch (SftpException ex) when (ex.StatusCode == SftpStatusCode.PermissionDenied)
    {
        Console.WriteLine($"Permission denied: {ex.Message}");
        throw;
    }
    catch (SftpException ex)
    {
        Console.WriteLine($"SFTP error ({ex.StatusCode}): {ex.Message}");
        throw;
    }
}
```

### Timeout Handling

```csharp
public async Task<string> RunCommandWithTimeout(
    SshClient client,
    string command,
    TimeSpan timeout)
{
    using var cts = new CancellationTokenSource(timeout);

    try
    {
        var result = await client.RunCommandAsync(command, cts.Token);
        return result.Output;
    }
    catch (OperationCanceledException) when (cts.IsCancellationRequested)
    {
        throw new TimeoutException($"Command '{command}' timed out after {timeout}");
    }
}
```

### Graceful Disconnection

```csharp
public async Task SafeDisconnectAsync(SshClient client)
{
    try
    {
        if (client.IsConnected)
        {
            await client.DisconnectAsync();
        }
    }
    catch (Exception ex)
    {
        // Log but don't throw on disconnect errors
        Console.WriteLine($"Warning during disconnect: {ex.Message}");
    }
    finally
    {
        await client.DisposeAsync();
    }
}
```

### Common Error Scenarios

| Error | Cause | Solution |
|-------|-------|----------|
| `Connection timed out` | Network issue or firewall | Check connectivity, increase timeout |
| `Connection refused` | SSH not running or wrong port | Verify server and port |
| `Host key verification failed` | Key changed or not trusted | Verify fingerprint, update known hosts |
| `Authentication failed` | Wrong credentials | Check username/password/key |
| `Permission denied (publickey)` | Key not authorized | Add key to authorized_keys |
| `Channel open failed` | Server resource limit | Wait and retry, check server limits |
| `License not initialized` | Missing license call | Call `LicenseManager.Instance.Initialize()` |
| `Feature not available` | License tier too low | Upgrade license or check feature flags |

---

## Connection Pooling

Connection pooling allows you to reuse SSH connections for high-throughput scenarios, reducing the overhead of establishing new connections.

**Note:** Connection pooling requires a Team or Enterprise license.

### Basic Connection Pool Usage

```csharp
using TwSsh.Client;
using TwSsh.Authentication;
using System.Collections.Concurrent;

public class SshConnectionPool : IAsyncDisposable
{
    private readonly SshClientSettings _settings;
    private readonly ConcurrentBag<SshClient> _available = new();
    private readonly int _maxConnections;
    private int _currentCount;

    public SshConnectionPool(SshClientSettings settings, int maxConnections = 10)
    {
        _settings = settings;
        _maxConnections = maxConnections;
    }

    public async ValueTask<SshClient> GetConnectionAsync(CancellationToken ct = default)
    {
        // Try to get an existing connection
        if (_available.TryTake(out var client) && client.IsConnected)
        {
            return client;
        }

        // Create new connection if under limit
        if (Interlocked.Increment(ref _currentCount) <= _maxConnections)
        {
            var newClient = new SshClient(_settings);
            await newClient.ConnectAndAuthenticateAsync(ct);
            return newClient;
        }

        Interlocked.Decrement(ref _currentCount);
        throw new InvalidOperationException("Connection pool exhausted");
    }

    public void ReturnConnection(SshClient client)
    {
        if (client.IsConnected)
        {
            _available.Add(client);
        }
        else
        {
            Interlocked.Decrement(ref _currentCount);
            _ = client.DisposeAsync();
        }
    }

    public async ValueTask DisposeAsync()
    {
        while (_available.TryTake(out var client))
        {
            await client.DisposeAsync();
        }
    }
}
```

### Using the Connection Pool

```csharp
var settings = new SshClientSettings
{
    Host = "example.com",
    Credential = new PasswordCredential("user", "password"),
    KeepAliveInterval = TimeSpan.FromSeconds(30)
};

await using var pool = new SshConnectionPool(settings, maxConnections: 5);

// Execute multiple commands in parallel
var tasks = Enumerable.Range(0, 10).Select(async i =>
{
    var client = await pool.GetConnectionAsync();
    try
    {
        var result = await client.RunCommandAsync($"echo 'Task {i}'");
        return result.Output;
    }
    finally
    {
        pool.ReturnConnection(client);
    }
});

var results = await Task.WhenAll(tasks);
```

### Best Practices for Connection Pooling

1. **Set Keep-Alive**: Use `KeepAliveInterval` to prevent idle connections from timing out
2. **Handle Disconnects**: Always check `IsConnected` before reusing a connection
3. **Limit Pool Size**: Match pool size to your server's `MaxSessions` setting
4. **Dispose Properly**: Always dispose the pool when done to clean up connections

---

For more information, see:
- [API Reference](API-REFERENCE.md)
- [Getting Started](GETTING-STARTED.md)
- [Licensing Guide](LICENSING.md)

---

Copyright 2025 Terminalworks Ltd. All rights reserved.
