Skip to content
Open
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
18 changes: 15 additions & 3 deletions components/execd/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh

# Copyright 2025 Alibaba Group Holding Ltd.
#
Expand Down Expand Up @@ -45,13 +45,25 @@ elif [ $# -ge 1 ] && [ "$1" = "-c" ]; then
CMD="$*"
fi

SHELL_BIN="${BOOTSTRAP_SHELL:-}"
if [ -z "$SHELL_BIN" ]; then
if command -v bash >/dev/null 2>&1; then
SHELL_BIN="$(command -v bash)"
elif command -v sh >/dev/null 2>&1; then
SHELL_BIN="$(command -v sh)"
else
echo "error: neither bash nor sh found in PATH" >&2
exit 1
fi
fi

set -x
if [ "$CMD" != "" ]; then
exec bash -c "$CMD"
exec "$SHELL_BIN" -c "$CMD"
fi

if [ $# -eq 0 ]; then
exec bash
exec "$SHELL_BIN"
fi

exec "$@"
3 changes: 2 additions & 1 deletion components/execd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ package main
import (
"fmt"

"github.com/alibaba/opensandbox/internal/version"

_ "go.uber.org/automaxprocs/maxprocs"

"github.com/alibaba/opensandbox/execd/pkg/flag"
"github.com/alibaba/opensandbox/execd/pkg/log"
_ "github.com/alibaba/opensandbox/execd/pkg/util/safego"
"github.com/alibaba/opensandbox/execd/pkg/web"
"github.com/alibaba/opensandbox/execd/pkg/web/controller"
"github.com/alibaba/opensandbox/internal/version"
)

// main initializes and starts the execd server.
Expand Down
35 changes: 16 additions & 19 deletions components/execd/pkg/runtime/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func getShell() string {

func buildCredential(uid, gid *uint32) (*syscall.Credential, error) {
if uid == nil && gid == nil {
return nil, nil
return nil, nil //nolint:nilnil
}

cred := &syscall.Credential{}
Expand Down Expand Up @@ -105,9 +105,21 @@ func (c *Controller) runCommand(ctx context.Context, request *ExecuteCodeRequest
shell := getShell()
cmd := exec.CommandContext(ctx, shell, "-c", request.Code)

// Configure credentials and process group
cred, err := buildCredential(request.Uid, request.Gid)
if err != nil {
return fmt.Errorf("failed to build credential: %w", err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Credential: cred,
}

cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Env = mergeEnvs(os.Environ(), loadExtraEnvFromFile())
extraEnv := mergeExtraEnvs(loadExtraEnvFromFile(), request.Envs)
cmd.Env = mergeEnvs(os.Environ(), extraEnv)
cmd.Dir = request.Cwd

done := make(chan struct{}, 1)
var wg sync.WaitGroup
Expand All @@ -121,20 +133,7 @@ func (c *Controller) runCommand(ctx context.Context, request *ExecuteCodeRequest
c.tailStdPipe(stderrPath, request.Hooks.OnExecuteStderr, done)
})

cmd.Dir = request.Cwd

// Configure credentials and process group
cred, err := buildCredential(request.Uid, request.Gid)
if err != nil {
log.Error("failed to build credentials: %v", err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Credential: cred,
}

err = cmd.Start()

if err != nil {
request.Hooks.OnExecuteInit(session)
request.Hooks.OnExecuteError(&execute.ErrorOutput{EName: "CommandExecError", EValue: err.Error()})
Expand Down Expand Up @@ -230,9 +229,7 @@ func (c *Controller) runBackgroundCommand(ctx context.Context, cancel context.Ca
log.Info("received command: %v", request.Code)
shell := getShell()
cmd := exec.CommandContext(ctx, shell, "-c", request.Code)

cmd.Dir = request.Cwd

// Configure credentials and process group
cred, err := buildCredential(request.Uid, request.Gid)
if err != nil {
Expand All @@ -244,9 +241,9 @@ func (c *Controller) runBackgroundCommand(ctx context.Context, cancel context.Ca
}

cmd.Stdout = pipe

cmd.Stderr = pipe
cmd.Env = mergeEnvs(os.Environ(), loadExtraEnvFromFile())
extraEnv := mergeExtraEnvs(loadExtraEnvFromFile(), request.Envs)
cmd.Env = mergeEnvs(os.Environ(), extraEnv)

// use DevNull as stdin so interactive programs exit immediately.
cmd.Stdin = os.NewFile(uintptr(syscall.Stdin), os.DevNull)
Expand Down
86 changes: 25 additions & 61 deletions components/execd/pkg/runtime/command_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestGetCommandStatus_NotFound(t *testing.T) {
c := NewController("", "")

if _, err := c.GetCommandStatus("missing"); err == nil {
t.Fatalf("expected error for missing session")
}
_, err := c.GetCommandStatus("missing")
require.Error(t, err, "expected error for missing session")
}

func TestGetCommandStatus_Running(t *testing.T) {
Expand All @@ -45,12 +46,8 @@ func TestGetCommandStatus_Running(t *testing.T) {
}

ctx, cancel := context.WithCancel(context.Background())
if err := c.runBackgroundCommand(ctx, cancel, req); err != nil {
t.Fatalf("runBackgroundCommand error: %v", err)
}
if session == "" {
t.Fatalf("session should be set by OnExecuteInit")
}
require.NoError(t, c.runBackgroundCommand(ctx, cancel, req))
require.NotEmpty(t, session, "session should be set by OnExecuteInit")

// Poll until status is registered (runBackgroundCommand stores kernel asynchronously).
deadline := time.Now().Add(5 * time.Second)
Expand All @@ -67,24 +64,15 @@ func TestGetCommandStatus_Running(t *testing.T) {
time.Sleep(50 * time.Millisecond)
continue
}
t.Fatalf("GetCommandStatus unexpected error: %v", err)
}
if err != nil {
t.Fatalf("GetCommandStatus error after retry: %v", err)
require.NoError(t, err, "GetCommandStatus unexpected error")
}
require.NoError(t, err, "GetCommandStatus error after retry")

if status == nil || !status.Running {
t.Fatalf("expected running=true")
}
if status.ExitCode != nil {
t.Fatalf("expected exitCode to be nil while running")
}
if status.FinishedAt != nil {
t.Fatalf("expected finishedAt to be nil while running")
}
if status.StartedAt.IsZero() {
t.Fatalf("expected startedAt to be set")
}
require.NotNil(t, status)
require.True(t, status.Running, "expected running=true")
require.Nil(t, status.ExitCode, "expected exitCode to be nil while running")
require.Nil(t, status.FinishedAt, "expected finishedAt to be nil while running")
require.False(t, status.StartedAt.IsZero(), "expected startedAt to be set")
t.Log(status)
}

Expand All @@ -96,9 +84,7 @@ func TestSeekBackgroundCommandOutput_Completed(t *testing.T) {
stdoutPath := filepath.Join(tmpDir, session+".stdout")

stdoutContent := "hello stdout"
if err := os.WriteFile(stdoutPath, []byte(stdoutContent), 0o644); err != nil {
t.Fatalf("write stdout: %v", err)
}
require.NoError(t, os.WriteFile(stdoutPath, []byte(stdoutContent), 0o644))

started := time.Now().Add(-2 * time.Second)
finished := time.Now()
Expand All @@ -116,16 +102,10 @@ func TestSeekBackgroundCommandOutput_Completed(t *testing.T) {
c.storeCommandKernel(session, kernel)

output, cursor, err := c.SeekBackgroundCommandOutput(session, 0)
if err != nil {
t.Fatalf("GetCommandOutput error: %v", err)
}
require.NoError(t, err, "GetCommandOutput error")

if cursor <= 0 {
t.Fatalf("expected cursor>=0")
}
if string(output) != stdoutContent {
t.Fatalf("expected output=%s, got %s", stdoutContent, string(output))
}
require.Greater(t, cursor, int64(0), "expected cursor>=0")
require.Equal(t, stdoutContent, string(output))
}

func TestSeekBackgroundCommandOutput_WithRunBackgroundCommand(t *testing.T) {
Expand All @@ -144,12 +124,8 @@ func TestSeekBackgroundCommandOutput_WithRunBackgroundCommand(t *testing.T) {
}

ctx, cancel := context.WithCancel(context.Background())
if err := c.runBackgroundCommand(ctx, cancel, req); err != nil {
t.Fatalf("runBackgroundCommand error: %v", err)
}
if session == "" {
t.Fatalf("session should be set by OnExecuteInit")
}
require.NoError(t, c.runBackgroundCommand(ctx, cancel, req))
require.NotEmpty(t, session, "session should be set by OnExecuteInit")

var (
output []byte
Expand All @@ -165,25 +141,13 @@ func TestSeekBackgroundCommandOutput_WithRunBackgroundCommand(t *testing.T) {
}
time.Sleep(100 * time.Millisecond)
}
if err != nil {
t.Fatalf("SeekBackgroundCommandOutput error: %v", err)
}
if string(output) != expected {
t.Fatalf("unexpected output: %q", string(output))
}
if cursor < int64(len(expected)) {
t.Fatalf("cursor should advance to end of file, got %d", cursor)
}
require.NoError(t, err, "SeekBackgroundCommandOutput error")
require.Equal(t, expected, string(output))
require.GreaterOrEqual(t, cursor, int64(len(expected)), "cursor should advance to end of file")

// incremental seek from current cursor should return empty data and same-or-higher cursor
output2, cursor2, err := c.SeekBackgroundCommandOutput(session, cursor)
if err != nil {
t.Fatalf("SeekBackgroundCommandOutput (second call) error: %v", err)
}
if len(output2) != 0 {
t.Fatalf("expected no new output, got %q", string(output2))
}
if cursor2 < cursor {
t.Fatalf("cursor should not move backwards: got %d < %d", cursor2, cursor)
}
require.NoError(t, err, "SeekBackgroundCommandOutput (second call) error")
require.Empty(t, output2, "expected no new output")
require.GreaterOrEqual(t, cursor2, cursor, "cursor should not move backwards")
}
Loading
Loading