fix: retry ssh fallback ports for desktop paths
This commit is contained in:
parent
7884b1d71f
commit
fdef9df8af
@ -19,6 +19,7 @@
|
||||
- Fixed Code and WebVNC bridge websocket auth so upgraded brokers receive short-lived bridge tickets in the `Authorization` header instead of logging them in URL query strings, while preserving query fallback for older brokers.
|
||||
- Fixed managed AWS macOS desktop leases so readiness and WebVNC use a writable `ec2-user` work root, call `crabbox-ready` by absolute path, and read the generated Screen Sharing password via sudo.
|
||||
- Fixed coordinator-backed `crabbox list` so a stale admin token no longer blocks normal logged-in users; the CLI now falls back to active user-visible leases instead of failing with `401 unauthorized`.
|
||||
- Fixed desktop, screenshot, VNC, and WebVNC SSH helpers so they retry live fallback ports when a coordinator lease advertises an SSH port that is not ready yet.
|
||||
|
||||
## 0.6.0 - 2026-05-07
|
||||
|
||||
|
||||
@ -129,6 +129,9 @@ func (a App) resolveNetworkLeaseTarget(ctx context.Context, cfg Config, id strin
|
||||
return Server{}, SSHTarget{}, "", err
|
||||
}
|
||||
target = resolved.Target
|
||||
if target.Host != "" {
|
||||
_ = probeSSHTransport(ctx, &target, 4*time.Second)
|
||||
}
|
||||
if printFallback && resolved.FallbackReason != "" {
|
||||
fmt.Fprintf(a.Stderr, "network fallback %s\n", resolved.FallbackReason)
|
||||
}
|
||||
|
||||
@ -96,18 +96,32 @@ func captureDesktopScreenshot(ctx context.Context, target SSHTarget, outputPath
|
||||
|
||||
func runSSHToWriter(ctx context.Context, target SSHTarget, remote string, stdout io.Writer) error {
|
||||
remote = wrapRemoteForTarget(target, remote)
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
|
||||
cmd.Stdout = stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
message := strings.TrimSpace(stderr.String())
|
||||
if message != "" {
|
||||
return fmt.Errorf("%w: %s", err, message)
|
||||
var lastErr error
|
||||
var lastMessage string
|
||||
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
|
||||
probe := target
|
||||
probe.Port = port
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
|
||||
cmd.Stdout = stdout
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
lastErr = err
|
||||
lastMessage = strings.TrimSpace(stderr.String())
|
||||
if shouldRetrySSHPort(err) {
|
||||
continue
|
||||
}
|
||||
if lastMessage != "" {
|
||||
return fmt.Errorf("%w: %s", err, lastMessage)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
if lastMessage != "" {
|
||||
return fmt.Errorf("%w: %s", lastErr, lastMessage)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func screenshotRemoteCommand(target SSHTarget) string {
|
||||
|
||||
@ -233,27 +233,65 @@ func runSSHQuiet(ctx context.Context, target SSHTarget, remote string) error {
|
||||
|
||||
func runSSHQuietWithOptions(ctx context.Context, target SSHTarget, remote, connectTimeout, connectionAttempts string) error {
|
||||
remote = wrapRemoteForTarget(target, remote)
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgsWithOptions(target, remote, connectTimeout, connectionAttempts)...)
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
return cmd.Run()
|
||||
var lastErr error
|
||||
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
|
||||
probe := target
|
||||
probe.Port = port
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgsWithOptions(probe, remote, connectTimeout, connectionAttempts)...)
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
if !shouldRetrySSHPort(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func runSSHOutput(ctx context.Context, target SSHTarget, remote string) (string, error) {
|
||||
remote = wrapRemoteForTarget(target, remote)
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
var lastOut []byte
|
||||
var lastErr error
|
||||
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
|
||||
probe := target
|
||||
probe.Port = port
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
lastOut = out
|
||||
lastErr = err
|
||||
if !shouldRetrySSHPort(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
return strings.TrimSpace(string(lastOut)), lastErr
|
||||
}
|
||||
|
||||
func runSSHCombinedOutput(ctx context.Context, target SSHTarget, remote string) (string, error) {
|
||||
remote = wrapRemoteForTarget(target, remote)
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
return strings.TrimSpace(string(out)), err
|
||||
var lastOut []byte
|
||||
var lastErr error
|
||||
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
|
||||
probe := target
|
||||
probe.Port = port
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
lastOut = out
|
||||
lastErr = err
|
||||
if !shouldRetrySSHPort(err) {
|
||||
return strings.TrimSpace(string(out)), err
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(string(lastOut)), lastErr
|
||||
}
|
||||
|
||||
func runSSHInputQuiet(ctx context.Context, target SSHTarget, remote, input string) error {
|
||||
@ -262,11 +300,31 @@ func runSSHInputQuiet(ctx context.Context, target SSHTarget, remote, input strin
|
||||
|
||||
func runSSHInput(ctx context.Context, target SSHTarget, remote string, input io.Reader, stdout, stderr io.Writer) error {
|
||||
remote = wrapRemoteForTarget(target, remote)
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(target, remote)...)
|
||||
cmd.Stdin = input
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
return cmd.Run()
|
||||
if input == nil {
|
||||
input = strings.NewReader("")
|
||||
}
|
||||
data, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var lastErr error
|
||||
for _, port := range sshPortCandidates(target.Port, target.FallbackPorts) {
|
||||
probe := target
|
||||
probe.Port = port
|
||||
cmd := exec.CommandContext(ctx, "ssh", sshArgs(probe, remote)...)
|
||||
cmd.Stdin = bytes.NewReader(data)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
if !shouldRetrySSHPort(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func runSSHStream(ctx context.Context, target SSHTarget, remote string, stdout, stderr io.Writer) int {
|
||||
@ -291,6 +349,10 @@ func sshArgs(target SSHTarget, remote string) []string {
|
||||
return sshArgsWithOptions(target, remote, "10", "3")
|
||||
}
|
||||
|
||||
func shouldRetrySSHPort(err error) bool {
|
||||
return exitCode(err) == 255
|
||||
}
|
||||
|
||||
func sshArgsWithOptions(target SSHTarget, remote, connectTimeout, connectionAttempts string) []string {
|
||||
return append(sshBaseArgsWithOptions(target, connectTimeout, connectionAttempts),
|
||||
target.User+"@"+target.Host,
|
||||
|
||||
@ -270,6 +270,15 @@ func TestSSHArgsAuthSecretDisablesControlMaster(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRetrySSHPortOnlyForTransportExit(t *testing.T) {
|
||||
if !shouldRetrySSHPort(exec.Command("sh", "-c", "exit 255").Run()) {
|
||||
t.Fatal("ssh transport exit 255 should retry fallback ports")
|
||||
}
|
||||
if shouldRetrySSHPort(exec.Command("sh", "-c", "exit 7").Run()) {
|
||||
t.Fatal("remote command failure should not retry fallback ports")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCommandLineRedactsSecretAuthUser(t *testing.T) {
|
||||
target := SSHTarget{
|
||||
User: "tok_live_secret",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user