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.
This commit is contained in:
Vladislav Chmelyuk
2026-02-04 14:17:28 +03:00
parent 58a9e6f40f
commit 5d0c0ffcf3
3 changed files with 88 additions and 28 deletions

View File

@@ -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))

View File

@@ -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")
}
}
}