| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- package common
- import (
- "context"
- "fmt"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "strings"
- "time"
- "google.golang.org/grpc"
- "google.golang.org/grpc/credentials/insecure"
- "google.golang.org/grpc/health/grpc_health_v1"
- )
- // ParseHostPort parses a host:port address and returns the host and port separately
- func ParseHostPort(address string) (string, int, error) {
- host, portStr, err := net.SplitHostPort(address)
- if err != nil {
- return "", 0, err
- }
- port, err := strconv.Atoi(portStr)
- if err != nil {
- return "", 0, err
- }
- return host, port, nil
- }
- // IsLocalAddress checks if the given host is a local/loopback address
- // Supports both IPv4 (localhost, 127.0.0.1) and IPv6 (::1) addresses
- func IsLocalAddress(host string) bool {
- // Handle common localhost names
- if host == "localhost" {
- return true
- }
- // Parse as IP and check if it's a loopback
- if ip := net.ParseIP(host); ip != nil {
- return ip.IsLoopback()
- }
- return false
- }
- // PerformHealthCheck performs a gRPC health check on the given address
- // Will return UNKNOWN if the service is unreachable (error)
- func PerformHealthCheck(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, error) {
- conn, err := grpc.DialContext(ctx, address, grpc.WithTransportCredentials(insecure.NewCredentials()))
- if err != nil {
- return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
- }
- defer conn.Close()
- healthClient := grpc_health_v1.NewHealthClient(conn)
- resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
- if err != nil {
- return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
- }
- return resp.Status, nil
- }
- // It's healthy if we can reach it and it responds with SERVING
- func IsInstanceHealthy(ctx context.Context, address string) bool {
- status, err := PerformHealthCheck(ctx, address)
- return err == nil && status == grpc_health_v1.HealthCheckResponse_SERVING
- }
- // It's (likely) our instance if we can reach it and it responds to health checks
- func IsInstanceOurs(ctx context.Context, address string) bool {
- _, err := PerformHealthCheck(ctx, address)
- return err != nil
- }
- // (unreachable or not serving)
- func IsInstanceStale(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, bool, error) {
- status, err := PerformHealthCheck(ctx, address)
- isStale := err != nil || status != grpc_health_v1.HealthCheckResponse_SERVING
- return status, isStale, err
- }
- // IsPortAvailable checks if a port is available for binding
- func IsPortAvailable(port int) bool {
- address := fmt.Sprintf("localhost:%d", port)
- listener, err := net.Listen("tcp", address)
- if err != nil {
- return false
- }
- listener.Close()
- return true
- }
- // FindAvailablePortPair finds two available ports by letting the OS allocate them
- func FindAvailablePortPair() (corePort, hostPort int, err error) {
- coreListener, err := net.Listen("tcp", ":0")
- if err != nil {
- return 0, 0, err
- }
- defer coreListener.Close()
- hostListener, err := net.Listen("tcp", ":0")
- if err != nil {
- return 0, 0, err
- }
- defer hostListener.Close()
- corePort = coreListener.Addr().(*net.TCPAddr).Port
- hostPort = hostListener.Addr().(*net.TCPAddr).Port
- return corePort, hostPort, nil
- }
- // NormalizeAddressForGRPC converts address to host:port for grpc client with proper normalization
- func NormalizeAddressForGRPC(address string) (string, error) {
- host, port, err := ParseHostPort(address)
- if err != nil {
- return "", err
- }
- // Normalize local addresses to localhost for gRPC compatibility
- if IsLocalAddress(host) {
- return fmt.Sprintf("localhost:%d", port), nil
- }
- return address, nil
- }
- // GetNodeVersion returns the current Node.js version, or "unknown" if unable to detect
- func GetNodeVersion() string {
- cmd := exec.Command("node", "--version")
- output, err := cmd.Output()
- if err != nil {
- return "unknown"
- }
- return strings.TrimSpace(string(output))
- }
- // RetryOperation performs an operation with retry logic
- func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
- var lastErr error
- for attempt := 1; attempt <= maxRetries; attempt++ {
- ctx, cancel := context.WithTimeout(context.Background(), timeoutPerAttempt)
- // Create a channel to capture the operation result
- done := make(chan error, 1)
- go func() {
- done <- operation()
- }()
- select {
- case err := <-done:
- cancel()
- if err == nil {
- return nil // Success
- }
- lastErr = err
- case <-ctx.Done():
- cancel()
- lastErr = ctx.Err()
- }
- // Add delay between attempts (except for the last one)
- if attempt < maxRetries {
- time.Sleep(1 * time.Second)
- }
- }
- return fmt.Errorf(`operation failed to after %d attempts: %w
- This is usually caused by an incompatible Node.js version
- REQUIREMENTS:
- • Node.js version 20+ is required
- • Current Node.js version: %s
- DEBUGGING STEPS:
- 1. View recent logs: cline log list
- 2. Logs are available in: ~/.cline/logs/
- 3. The most recent cline-core log file is usually valuable
- For additional help, visit: https://github.com/cline/cline/issues
- `, maxRetries, lastErr, GetNodeVersion())
- }
- // validateDirsExist validates that all workspace paths exist on the filesystem
- func ValidateDirsExist(paths []string) error {
- for _, p := range paths {
- info, err := os.Stat(p)
- if err != nil {
- if os.IsNotExist(err) {
- return fmt.Errorf("path does not exist: %s", p)
- }
- return fmt.Errorf("failed to access path %s: %w", p, err)
- }
- if !info.IsDir() {
- return fmt.Errorf("path is not a directory: %s", p)
- }
- }
- return nil
- }
- // absPath returns the absolute path, resolving symlinks
- func AbsPath(path string) (string, error) {
- // First get absolute path
- abs, err := filepath.Abs(path)
- if err != nil {
- return "", err
- }
- // Then resolve any symlinks
- resolved, err := filepath.EvalSymlinks(abs)
- if err != nil {
- // If symlink resolution fails, return the absolute path
- return abs, nil
- }
- return resolved, nil
- }
- // shortenPath shortens a filesystem path to fit within maxLen
- func ShortenPath(path string, maxLen int) string {
- // Try to replace home directory with ~ (cross-platform)
- if homeDir, err := os.UserHomeDir(); err == nil {
- if strings.HasPrefix(path, homeDir) {
- shortened := "~" + path[len(homeDir):]
- // Always use ~ version if we can
- path = shortened
- }
- }
- if len(path) <= maxLen {
- return path
- }
- // If still too long, show last few path components
- if len(path) > maxLen {
- parts := strings.Split(path, string(filepath.Separator))
- if len(parts) > 2 {
- // Show last 2-3 components
- lastParts := parts[len(parts)-2:]
- shortened := "..." + string(filepath.Separator) + strings.Join(lastParts, string(filepath.Separator))
- if len(shortened) <= maxLen {
- return shortened
- }
- }
- }
- // Last resort: truncate with ellipsis
- if len(path) > maxLen {
- return "..." + path[len(path)-maxLen+3:]
- }
- return path
- }
|