From 5d0c0ffcf36e64c3d69c61f8ccc8ccc0a5fb32c1 Mon Sep 17 00:00:00 2001 From: Vladislav Chmelyuk Date: Wed, 4 Feb 2026 14:17:28 +0300 Subject: [PATCH] refactor: update NewPingManager to accept a config file parameter - Modified the NewPingManager function to include a configFile argument for better SSH configuration management. - Updated all relevant tests to reflect the new function signature. - Enhanced ping functionality to support ProxyJump and ProxyCommand using an external SSH command. - Adjusted UI initialization to pass the config file to the PingManager. This change improves flexibility in managing SSH connections and enhances the overall functionality of the ping manager. --- internal/connectivity/ping.go | 74 +++++++++++++++++++++++++++--- internal/connectivity/ping_test.go | 40 ++++++++-------- internal/ui/tui.go | 2 +- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/internal/connectivity/ping.go b/internal/connectivity/ping.go index ddb60a9..c41aefd 100644 --- a/internal/connectivity/ping.go +++ b/internal/connectivity/ping.go @@ -2,12 +2,15 @@ package connectivity import ( "context" + "fmt" "net" - "github.com/Gu1llaum-3/sshm/internal/config" + "os/exec" "strings" "sync" "time" + "github.com/Gu1llaum-3/sshm/internal/config" + "golang.org/x/crypto/ssh" ) @@ -45,16 +48,18 @@ type HostPingResult struct { // PingManager manages SSH connectivity checks for multiple hosts type PingManager struct { - results map[string]*HostPingResult - mutex sync.RWMutex - timeout time.Duration + results map[string]*HostPingResult + mutex sync.RWMutex + timeout time.Duration + configFile string } // NewPingManager creates a new ping manager with the specified timeout -func NewPingManager(timeout time.Duration) *PingManager { +func NewPingManager(timeout time.Duration, configFile string) *PingManager { return &PingManager{ - results: make(map[string]*HostPingResult), - timeout: timeout, + results: make(map[string]*HostPingResult), + timeout: timeout, + configFile: configFile, } } @@ -98,6 +103,14 @@ func (pm *PingManager) PingHost(ctx context.Context, host config.SSHHost) *HostP // Mark as connecting pm.updateStatus(host.Name, StatusConnecting, nil, 0) + // If the host uses a ProxyJump or ProxyCommand, we need to use the external SSH command + // because implementing jump host support with pure Go ssh library requires + // handling authentication for the jump host, which is complex and requires + // access to the user's SSH agent or keys. + if host.ProxyJump != "" || host.ProxyCommand != "" { + return pm.pingWithExternalCommand(ctx, host, start) + } + // Determine the actual hostname and port hostname := host.Hostname if hostname == "" { @@ -159,6 +172,53 @@ func (pm *PingManager) PingHost(ctx context.Context, host config.SSHHost) *HostP } } +// pingWithExternalCommand pings a host using the external SSH command +func (pm *PingManager) pingWithExternalCommand(ctx context.Context, host config.SSHHost, start time.Time) *HostPingResult { + // Construct the SSH command + // ssh -q -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=5 host exit + args := []string{"-q", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no"} + + // Set timeout matching the manager's timeout + // Convert duration to seconds (rounding up to ensure we don't timeout too early in the command) + timeoutSec := int(pm.timeout.Seconds()) + if timeoutSec < 1 { + timeoutSec = 1 + } + args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", timeoutSec)) + + // If we have a specific config file, use it + if pm.configFile != "" { + args = append(args, "-F", pm.configFile) + } + + // Add the host name and the command to run (exit) + args = append(args, host.Name, "exit") + + // Create command with context for timeout cancellation + // Note: We used pm.timeout for the ssh command option, but we also respect the context deadline + cmd := exec.CommandContext(ctx, "ssh", args...) + + // Run the command + err := cmd.Run() + duration := time.Since(start) + + var status PingStatus + if err != nil { + // SSH returns non-zero exit code on connection failure + status = StatusOffline + } else { + status = StatusOnline + } + + pm.updateStatus(host.Name, status, err, duration) + return &HostPingResult{ + HostName: host.Name, + Status: status, + Error: err, + Duration: duration, + } +} + // PingAllHosts pings all hosts concurrently and returns a channel of results func (pm *PingManager) PingAllHosts(ctx context.Context, hosts []config.SSHHost) <-chan *HostPingResult { resultChan := make(chan *HostPingResult, len(hosts)) diff --git a/internal/connectivity/ping_test.go b/internal/connectivity/ping_test.go index d6a379f..dac8439 100644 --- a/internal/connectivity/ping_test.go +++ b/internal/connectivity/ping_test.go @@ -9,7 +9,7 @@ import ( ) func TestNewPingManager(t *testing.T) { - pm := NewPingManager(5 * time.Second) + pm := NewPingManager(5*time.Second, "") if pm == nil { t.Error("NewPingManager() returned nil") } @@ -19,16 +19,16 @@ func TestNewPingManager(t *testing.T) { } func TestPingManager_PingHost(t *testing.T) { - pm := NewPingManager(1 * time.Second) + pm := NewPingManager(1*time.Second, "") ctx := context.Background() - + // Test ping method exists and doesn't panic host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"} result := pm.PingHost(ctx, host) if result == nil { t.Error("Expected ping result to be returned") } - + // Test with invalid host invalidHost := config.SSHHost{Name: "invalid", Hostname: "invalid.host.12345", Port: "22"} result = pm.PingHost(ctx, invalidHost) @@ -38,14 +38,14 @@ func TestPingManager_PingHost(t *testing.T) { } func TestPingManager_GetStatus(t *testing.T) { - pm := NewPingManager(1 * time.Second) - + pm := NewPingManager(1*time.Second, "") + // Test unknown host status := pm.GetStatus("unknown.host") if status != StatusUnknown { t.Errorf("Expected StatusUnknown for unknown host, got %v", status) } - + // Test after ping ctx := context.Background() host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"} @@ -57,21 +57,21 @@ func TestPingManager_GetStatus(t *testing.T) { } func TestPingManager_PingMultipleHosts(t *testing.T) { - pm := NewPingManager(1 * time.Second) + pm := NewPingManager(1*time.Second, "") hosts := []config.SSHHost{ {Name: "localhost", Hostname: "127.0.0.1", Port: "22"}, {Name: "invalid", Hostname: "invalid.host.12345", Port: "22"}, } - + ctx := context.Background() - + // Ping each host individually for _, host := range hosts { result := pm.PingHost(ctx, host) if result == nil { t.Errorf("Expected ping result for host %s", host.Name) } - + // Check that status was set status := pm.GetStatus(host.Name) if status == StatusUnknown { @@ -81,19 +81,19 @@ func TestPingManager_PingMultipleHosts(t *testing.T) { } func TestPingManager_GetResult(t *testing.T) { - pm := NewPingManager(1 * time.Second) + pm := NewPingManager(1*time.Second, "") ctx := context.Background() - + // Test getting result for unknown host result, exists := pm.GetResult("unknown") if exists || result != nil { t.Error("Expected no result for unknown host") } - + // Test after ping host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"} pm.PingHost(ctx, host) - + result, exists = pm.GetResult("test") if !exists || result == nil { t.Error("Expected result to exist after ping") @@ -114,7 +114,7 @@ func TestPingStatus_String(t *testing.T) { {StatusOffline, "offline"}, {PingStatus(999), "unknown"}, // Invalid status } - + for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { if got := tt.status.String(); got != tt.expected { @@ -126,19 +126,19 @@ func TestPingStatus_String(t *testing.T) { func TestPingHost_Basic(t *testing.T) { // Test that the ping functionality exists - pm := NewPingManager(1 * time.Second) + pm := NewPingManager(1*time.Second, "") ctx := context.Background() host := config.SSHHost{Name: "test", Hostname: "127.0.0.1", Port: "22"} - + // Just ensure the function doesn't panic result := pm.PingHost(ctx, host) if result == nil { t.Error("Expected ping result to be returned") } - + // Test that status is set status := pm.GetStatus("test") if status == StatusUnknown { t.Error("Expected status to be set after ping attempt") } -} \ No newline at end of file +} diff --git a/internal/ui/tui.go b/internal/ui/tui.go index d8f5b1d..84e4581 100644 --- a/internal/ui/tui.go +++ b/internal/ui/tui.go @@ -38,7 +38,7 @@ func NewModel(hosts []config.SSHHost, configFile string, searchMode bool, curren styles := NewStyles(80) // Default width // Initialize ping manager with 5 second timeout - pingManager := connectivity.NewPingManager(5 * time.Second) + pingManager := connectivity.NewPingManager(5*time.Second, configFile) // Create the model with default sorting by name m := Model{