Skip to content

Commit c0b8255

Browse files
committed
Fix detached sandbox exec stream handling
1 parent a9a1f09 commit c0b8255

3 files changed

Lines changed: 13 additions & 15 deletions

File tree

pkg/cmd/sandbox/common.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ func snapshotRef(id string) *sandboxv1.SnapshotRef {
5555
}
5656

5757
// consumeCommandEventStream drains a depot.sandbox.v1 CommandEvent stream
58-
// into stdout/stderr and returns the final exit code from Finished. The
59-
// stream shape mirrors RunCommand / RunCommandPipe / AttachCommand /
60-
// RunHook: Started -> Stdout/Stderr/Error/EvictedEarlyData* -> Finished.
58+
// into stdout/stderr and returns the final exit code from Finished. The stream
59+
// shape is generally Started -> Stdout/Stderr/Error/EvictedEarlyData* ->
60+
// Finished; detached RunCommand streams end cleanly after Started.
6161
//
6262
// EvictedEarlyData is reported on stderr as a single line so log consumers
6363
// see the gap; the stream continues afterward. Error frames are surfaced the
@@ -66,13 +66,16 @@ func snapshotRef(id string) *sandboxv1.SnapshotRef {
6666
func consumeCommandEventStream(
6767
stream *connect.ServerStreamForClient[sandboxv1.CommandEvent],
6868
stdout, stderr io.Writer,
69+
allowMissingFinished bool,
6970
) (exitCode int32, err error) {
7071
defer func() { _ = stream.Close() }()
72+
sawStarted := false
7173
for stream.Receive() {
7274
msg := stream.Msg()
7375
switch ev := msg.Event.(type) {
7476
case *sandboxv1.CommandEvent_Started_:
7577
// metadata only — nothing to print
78+
sawStarted = true
7679
case *sandboxv1.CommandEvent_Stdout:
7780
if ev.Stdout != nil && len(ev.Stdout.Data) > 0 {
7881
_, _ = stdout.Write(ev.Stdout.Data)
@@ -99,6 +102,9 @@ func consumeCommandEventStream(
99102
if err := stream.Err(); err != nil && !errors.Is(err, io.EOF) {
100103
return 0, fmt.Errorf("command stream: %w", err)
101104
}
105+
if allowMissingFinished && sawStarted {
106+
return 0, nil
107+
}
102108
// Stream closed without a Finished event. Treat this as an error rather
103109
// than silently reporting exit 0 — a clean disconnect mid-command is
104110
// indistinguishable from a real exit-0 completion to the caller otherwise.

pkg/cmd/sandbox/exec.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sandbox
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"connectrpc.com/connect"
89
"github.com/depot/cli/pkg/api"
@@ -88,7 +89,7 @@ parsing stops there.`,
8889
return fmt.Errorf("run command: %w", err)
8990
}
9091

91-
exit, err := consumeCommandEventStream(stream, os.Stdout, os.Stderr)
92+
exit, err := consumeCommandEventStream(stream, os.Stdout, os.Stderr, detached)
9293
if err != nil {
9394
return err
9495
}
@@ -119,20 +120,11 @@ func parseEnvSlice(in []string) (map[string]string, error) {
119120
}
120121
out := make(map[string]string, len(in))
121122
for _, e := range in {
122-
k, v, ok := splitKV(e)
123+
k, v, ok := strings.Cut(e, "=")
123124
if !ok {
124125
return nil, fmt.Errorf("invalid env format %q, expected KEY=VALUE", e)
125126
}
126127
out[k] = v
127128
}
128129
return out, nil
129130
}
130-
131-
func splitKV(s string) (string, string, bool) {
132-
for i := 0; i < len(s); i++ {
133-
if s[i] == '=' {
134-
return s[:i], s[i+1:], true
135-
}
136-
}
137-
return "", "", false
138-
}

pkg/cmd/sandbox/hooks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func runHook(ctx context.Context, client sandboxv1connect.SandboxServiceClient,
149149
return fmt.Errorf("exec: %w", err)
150150
}
151151

152-
exit, err := consumeCommandEventStream(stream, stdout, stderr)
152+
exit, err := consumeCommandEventStream(stream, stdout, stderr, false)
153153
if err != nil {
154154
return err
155155
}

0 commit comments

Comments
 (0)