diff --git a/cmd/benchpod-cli/commands_register.go b/cmd/benchpod-cli/commands_register.go index c2393ef..6db1db1 100644 --- a/cmd/benchpod-cli/commands_register.go +++ b/cmd/benchpod-cli/commands_register.go @@ -204,9 +204,13 @@ func runRegister(g *globalFlags, opts registerOptions) error { return ctx.Err() } fmt.Fprint(os.Stderr, describeNotConnected(device.Name, host, opts.wait, status, pollErr)) - return fmt.Errorf("pod did not connect within %s", opts.wait) + return fmt.Errorf("%w within %s", errRegisteredNotConnected, opts.wait) } +// errRegisteredNotConnected: the registration stands and the pod keeps retrying on its own, it +// only did not reach the server in time (no cable yet, say). Setup carries on after it. +var errRegisteredNotConnected = errors.New("pod did not connect") + // cloudStatus is the part of the firmware's `cloud_status` reply that register reads. // LastError is nil on firmware that predates the field. type cloudStatus struct { diff --git a/cmd/benchpod-cli/errors.go b/cmd/benchpod-cli/errors.go index 1bde639..1d0cf65 100644 --- a/cmd/benchpod-cli/errors.go +++ b/cmd/benchpod-cli/errors.go @@ -146,6 +146,18 @@ func withRefusalHint(err error) error { return fmt.Errorf("%w (%s)", err, hint) } +// withSavedConnectionHint adds where the address came from when the pod did not answer at the +// saved default (saved, "" when the run did not use it): after a pod is unplugged or another pod +// takes its place, that default is stale and only `discover` / `set-connection` fix it. +func withSavedConnectionHint(err error, saved string) error { + if saved == "" || !errors.Is(err, tcpclient.ErrUnreachable) { + return err + } + return fmt.Errorf("%w\n%s is the saved default connection. If that pod is unplugged or you use another one, "+ + "run `benchpod discover` to list the pods, then `benchpod set-connection
>`, "+ + "or pass --connection", err, saved) +} + // ── exit codes ────────────────────────────────────────────────────────────── // The CLI's exit codes, so a script can tell a refusal from an unreachable pod. Documented in diff --git a/cmd/benchpod-cli/helpers.go b/cmd/benchpod-cli/helpers.go index 3207072..f8e1d11 100644 --- a/cmd/benchpod-cli/helpers.go +++ b/cmd/benchpod-cli/helpers.go @@ -46,9 +46,16 @@ func (g *globalFlags) rawConnection() (string, error) { } return "", fmt.Errorf("load config: %w", err) } - return strings.TrimSpace(cfg.Connection), nil + savedConnectionUsed = strings.TrimSpace(cfg.Connection) + return savedConnectionUsed, nil } +// savedConnectionUsed is the saved default this run fell back to ("" = none, or --connection / +// BENCHPOD_CONNECTION named the pod). An unreachable pod then gets a hint that the address came +// from the saved default: after a pod is swapped or moved it is a stale address, and the bare +// dial timeout does not say where it came from. +var savedConnectionUsed string + // resolveConnection classifies the effective connection (flag or stored default) // into a ConnSpec for a command that only works over the network or USB; errors when // no connection is set, the value is unusable, or it names a pod on embeddedci.com. diff --git a/cmd/benchpod-cli/root.go b/cmd/benchpod-cli/root.go index d2e827d..624352e 100644 --- a/cmd/benchpod-cli/root.go +++ b/cmd/benchpod-cli/root.go @@ -143,11 +143,12 @@ func Execute() int { // run runs the CLI with args and returns its exit code. func run(args []string) int { + savedConnectionUsed = "" root := newRootCmd() markUsageErrors(root) root.SetArgs(args) if err := root.Execute(); err != nil { - log.Printf("%v", withRefusalHint(err)) + log.Printf("%v", withSavedConnectionHint(withRefusalHint(err), savedConnectionUsed)) return exitCode(err) } return exitOK diff --git a/cmd/benchpod-cli/saved_connection_hint_test.go b/cmd/benchpod-cli/saved_connection_hint_test.go new file mode 100644 index 0000000..93a9477 --- /dev/null +++ b/cmd/benchpod-cli/saved_connection_hint_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/embeddedci-com/benchpod-cli/internal/tcpclient" +) + +// An unreachable pod at the saved default says the address is the saved one and how to change it; +// an address the user passed, or another error, is left as it is. +func TestSavedConnectionHint(t *testing.T) { + dial := fmt.Errorf("status: %w", tcpclient.ErrUnreachable) + got := withSavedConnectionHint(dial, "10.237.8.85:8080").Error() + for _, want := range []string{"pod unreachable", "10.237.8.85:8080 is the saved default connection", "benchpod discover", "set-connection"} { + if !strings.Contains(got, want) { + t.Errorf("missing %q in %q", want, got) + } + } + if !errors.Is(withSavedConnectionHint(dial, "x"), tcpclient.ErrUnreachable) { + t.Error("the hint must keep the error's identity (exit code)") + } + if got := withSavedConnectionHint(dial, ""); got != dial { + t.Errorf("no saved default used: %v", got) + } + other := errors.New("refused: busy") + if got := withSavedConnectionHint(other, "10.0.0.1:8080"); got != other { + t.Errorf("not unreachable: %v", got) + } +} diff --git a/cmd/benchpod-cli/setup.go b/cmd/benchpod-cli/setup.go index bcc33c6..719aaea 100644 --- a/cmd/benchpod-cli/setup.go +++ b/cmd/benchpod-cli/setup.go @@ -506,7 +506,12 @@ func setupCloud(env setupEnv, ui *setupUI, o setupOpts, p setupPod, mv int) (str } } if err := env.register(p.addr); err != nil { - return "", err + if !errors.Is(err, errRegisteredNotConnected) { + return "", err + } + // Registered; the pod connects once it has a network. Saving the connection (step 5) + // still matters, or the CLI keeps the previous default. + fmt.Fprintln(out, " Registered. It connects to embeddedci.com once it has a network; setup carries on.") } if c := env.cloudStatus(p.addr); c.known && c.DeviceID != "" { id := strings.TrimSpace(c.DeviceID) diff --git a/cmd/benchpod-cli/setup_test.go b/cmd/benchpod-cli/setup_test.go index 1e99cb1..9af765d 100644 --- a/cmd/benchpod-cli/setup_test.go +++ b/cmd/benchpod-cli/setup_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "errors" + "fmt" "strings" "testing" "time" @@ -25,6 +26,7 @@ type fakeSetupEnv struct { profileMV int // la_mv in the stored wiring profile, 0 = none saveErr error // what saveLaMV fails with latest string // latest firmware release, "" = unknown + regErr error // what register returns after registering calls []string wifiSSID string @@ -82,7 +84,7 @@ func (f *fakeSetupEnv) register(addr string) error { f.calls = append(f.calls, "register") f.registered = addr f.cloud = f.afterReg - return nil + return f.regErr } func (f *fakeSetupEnv) save(target string) error { f.calls = append(f.calls, "save") @@ -374,6 +376,32 @@ func TestSetupSkipsTheProfileWhenNotOnTheAccount(t *testing.T) { } } +// A pod registered but not yet on the network (no cable) does not stop setup: step 5 still saves +// the connection, or the CLI keeps aiming at the previous default. +func TestSetupCarriesOnWhenTheRegisteredPodIsNotConnectedYet(t *testing.T) { + unreg := cloudState{known: true} + env := &fakeSetupEnv{ + pods: []setupPod{{addr: "10.237.94.169:8080", cloud: unreg}}, cloud: unreg, laMV: 3300, who: "me@x.com", + afterReg: registeredMine, account: map[string]string{"dev-1": "bench-01"}, + regErr: fmt.Errorf("%w within 30s", errRegisteredNotConnected), + } + out, err := runSetupWith(t, env, "", setupOpts{yes: true}) + if err != nil { + t.Fatal(err) + } + if !env.did("save") || env.saved == "" || !strings.Contains(out, "embeddedci:bench-01") { + t.Fatalf("calls %v saved %q\n%s", env.calls, env.saved, out) + } + // Any other register error still stops it. + env = &fakeSetupEnv{ + pods: []setupPod{{addr: "10.0.0.5:8080", cloud: unreg}}, cloud: unreg, laMV: 3300, who: "me@x.com", + regErr: errors.New("register the pod: 401"), + } + if _, err := runSetupWith(t, env, "", setupOpts{yes: true}); err == nil || env.did("save") { + t.Fatalf("err %v calls %v", err, env.calls) + } +} + func TestSetupSavesTheVoltageAfterRegistering(t *testing.T) { unreg := cloudState{known: true} env := &fakeSetupEnv{