| 751 | } |
| 752 | |
| 753 | func WaitForTCPPort(ctx context.Context, host string, port int, overallTimeout time.Duration) error { |
| 754 | address := net.JoinHostPort(host, strconv.Itoa(port)) |
| 755 | deadline := time.Now().Add(overallTimeout) |
| 756 | backoff := 1 * time.Second |
| 757 | maxBackoff := 10 * time.Second |
| 758 | attempt := 0 |
| 759 | |
| 760 | for { |
| 761 | attempt++ |
| 762 | if err := ctx.Err(); err != nil { |
| 763 | return fmt.Errorf("%w: TCP port check cancelled: %w", ErrSSHUnreachable, err) |
| 764 | } |
| 765 | if time.Now().After(deadline) { |
| 766 | return fmt.Errorf("%w: TCP port %s not available after %v", ErrSSHUnreachable, address, overallTimeout) |
| 767 | } |
| 768 | |
| 769 | remaining := time.Until(deadline) |
| 770 | attemptTimeout := remaining |
| 771 | if attemptTimeout > 5*time.Second { |
| 772 | attemptTimeout = 5 * time.Second |
| 773 | } |
| 774 | if attemptTimeout <= 0 { |
| 775 | return fmt.Errorf("%w: TCP port %s not available after %v", ErrSSHUnreachable, address, overallTimeout) |
| 776 | } |
| 777 | |
| 778 | dialer := &net.Dialer{ |
| 779 | Timeout: attemptTimeout, |
| 780 | } |
| 781 | |
| 782 | conn, err := dialer.DialContext(ctx, "tcp", address) |
| 783 | if err == nil { |
| 784 | conn.Close() |
| 785 | return nil |
| 786 | } |
| 787 | |
| 788 | // Only retry on connection-related errors |
| 789 | if !shouldRetryDial(err) { |
| 790 | return fmt.Errorf("%w: TCP port check failed: %w", ErrSSHUnreachable, err) |
| 791 | } |
| 792 | |
| 793 | // Exponential backoff with cap |
| 794 | if err := sleepWithContext(ctx, backoff); err != nil { |
| 795 | return fmt.Errorf("%w: TCP port check cancelled: %w", ErrSSHUnreachable, err) |
| 796 | } |
| 797 | backoff = minDuration(backoff*2, maxBackoff) |
| 798 | } |
| 799 | } |