From 4196b00771c2a04aa075c175be5936ae11ab1c3f Mon Sep 17 00:00:00 2001 From: Akshay Singla Date: Wed, 3 Jun 2026 04:47:18 +0000 Subject: [PATCH 1/2] lakebox: color "stopping" status yellow Mirrors the "creating" treatment so transient states are visually distinct from terminal ones in `list` and `status` output. Co-authored-by: Isaac --- cmd/lakebox/ui.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/lakebox/ui.go b/cmd/lakebox/ui.go index 82f2c45c313..45aa730f79e 100644 --- a/cmd/lakebox/ui.go +++ b/cmd/lakebox/ui.go @@ -65,6 +65,8 @@ func status(ctx context.Context, s string) string { return cmdio.Faint(ctx, "stopped") case "creating": return cmdio.Bold(ctx, cmdio.Cyan(ctx, "creating…")) + case "stopping": + return cmdio.Yellow(ctx, "stopping…") default: return cmdio.Faint(ctx, strings.ToLower(s)) } From 9519b7a88ab8817a47162a80c735df53cd08e088 Mon Sep 17 00:00:00 2001 From: Akshay Singla Date: Thu, 4 Jun 2026 01:01:29 +0000 Subject: [PATCH 2/2] lakebox: explicitly start stopped sandbox before ssh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the bare warning "Starting … (may take a few minutes)" with an explicit api.start + waitForRunning sequence before exec'ing ssh. The SSH gateway already auto-starts a stopped sandbox on connect, but that path is opaque — ssh just hangs for minutes with no progress — and races the cold-start timeout. Driving the start ourselves gives the user a visible spinner with elapsed time and a deterministic timeout (the same one `databricks lakebox start` uses). The new ensureRunning helper also handles already-transitioning states (Creating, Starting): it skips the start RPC and just polls. Co-authored-by: Isaac --- cmd/lakebox/ssh.go | 52 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/cmd/lakebox/ssh.go b/cmd/lakebox/ssh.go index 2e8670667b5..7ae644bbea9 100644 --- a/cmd/lakebox/ssh.go +++ b/cmd/lakebox/ssh.go @@ -1,6 +1,7 @@ package lakebox import ( + "context" "errors" "fmt" "io/fs" @@ -161,11 +162,21 @@ Examples: } } - // A stopped sandbox is implicitly started on connect, which - // can take minutes. Print an explicit notice so the user - // understands why the connect spinner is hanging. - if strings.EqualFold(sandboxStatus, "stopped") { - warn(ctx, "Starting "+cmdio.Bold(ctx, lakeboxID)+"… (may take a few minutes)") + // Explicitly start (and wait for) the sandbox if it isn't + // already Running. The gateway will auto-start a stopped + // sandbox on connect, but that path is opaque (ssh just + // hangs for minutes with no progress) and races the + // cold-start timeout. Driving the start ourselves gives + // the user a visible spinner with elapsed time and a + // deterministic timeout (see start.go). + if sandboxStatus != "" && !strings.EqualFold(sandboxStatus, "running") { + final, err := ensureRunning(ctx, api, lakeboxID, sandboxStatus) + if err != nil { + return err + } + if final.GatewayHost != "" { + sandboxGatewayHost = final.GatewayHost + } } // Resolution precedence: --gateway flag → fresh API response → @@ -205,6 +216,37 @@ Examples: return cmd } +// ensureRunning brings the named sandbox to Running before ssh hands +// off. Owns its own spinner lifecycle — caller must not have one open. +// Calls api.start when the sandbox is currently Stopped; falls through +// to a poll for already-transitioning states (Creating, Starting). +func ensureRunning(ctx context.Context, api *lakeboxAPI, id, currentStatus string) (*sandboxEntry, error) { + s := spin(ctx, "Starting "+cmdio.Bold(ctx, id)+"…") + defer s.Close() + + var sb *sandboxEntry + if strings.EqualFold(currentStatus, "stopped") { + updated, err := api.start(ctx, id) + if err != nil { + s.fail("Failed to start " + id) + return nil, fmt.Errorf("failed to start lakebox %s: %w", id, err) + } + sb = updated + } + + if sb == nil || !strings.EqualFold(sb.Status, "running") { + final, err := waitForRunning(ctx, api, s, id) + if err != nil { + s.fail("Failed to start " + id) + return nil, err + } + sb = final + } + + s.ok("Started " + cmdio.Bold(ctx, id)) + return sb, nil +} + // execSSHDirect replaces the CLI process with ssh (or simulates that on // Windows via execv). All options are passed on the command line, so no // ~/.ssh/config entry is required.