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
22 changes: 22 additions & 0 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ func TestServerExecHelloWorld(t *testing.T) {
}
}

func TestServerExecBackgroundChildDoesNotPinSession(t *testing.T) {
t.Parallel()
ctx, conn := dialTestServer(t)

var stdout bytes.Buffer
start := time.Now()
exit, err := client.Run(ctx, conn, []string{"sh", "-c", "sleep 6 & echo started"}, nil, nil, &stdout, io.Discard)
if err != nil {
t.Fatalf("client run: %v", err)
}
if exit != 0 {
t.Errorf("exit = %d, want 0", exit)
}
if got := strings.TrimSpace(stdout.String()); got != "started" {
t.Errorf("stdout = %q, want \"started\"", got)
}
// Without WaitDelay the session is pinned until the background sleep exits.
if elapsed := time.Since(start); elapsed > 4*time.Second {
t.Errorf("session took %v, want < 4s (pinned by background child)", elapsed)
}
}

func TestServerPropagatesNonZeroExit(t *testing.T) {
t.Parallel()
ctx, conn := dialTestServer(t)
Expand Down
10 changes: 10 additions & 0 deletions agent/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import (
"os"
"os/exec"
"strings"
"time"
)

// execWaitDelay bounds how long Wait blocks on the child's stdout/stderr
// pipes after exit: a daemonized grandchild inherits them and would pin
// the session (and its conn) until it dies.
const execWaitDelay = 2 * time.Second

// processController hooks platform-specific child-lifecycle steps into
// runExec: AfterStart runs once cmd.Process exists (Windows assigns the
// child to its Job Object); Close releases any held kernel resource.
Expand Down Expand Up @@ -46,6 +52,7 @@ func runExec(parentCtx context.Context, argv []string, env map[string]string, st
defer cancel()

cmd := exec.CommandContext(ctx, argv[0], argv[1:]...) //nolint:gosec // argv from trusted vsock peer
cmd.WaitDelay = execWaitDelay
procCtl, err := setupProcess(cmd)
if err != nil {
return enc.SendErrorf("exec: setup process %s: %v", argv[0], err)
Expand Down Expand Up @@ -115,6 +122,9 @@ func runExec(parentCtx context.Context, argv []string, env map[string]string, st
case waitErr == nil:
case errors.As(waitErr, &exitErr):
exitCode = exitErr.ExitCode()
case errors.Is(waitErr, exec.ErrWaitDelay):
// Pipes were abandoned to a background child; the command itself exited.
exitCode = cmd.ProcessState.ExitCode()
default:
return enc.SendErrorf("exec: wait %s: %v", argv[0], waitErr)
}
Expand Down