Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions cmd/lakebox/ssh.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lakebox

import (
"context"
"errors"
"fmt"
"io/fs"
Expand Down Expand Up @@ -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 →
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions cmd/lakebox/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
Loading