From 90281bd9d00f812606d0ee2279a0bb113dbcc1e0 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 23 Oct 2024 23:45:00 +0000 Subject: [PATCH 01/17] feat: add agentexec pkg --- agent/agentexec/cli.go | 75 ++++++++++++++++++++ agent/agentexec/cmdtest/main.go | 17 +++++ agent/agentexec/exec.go | 41 +++++++++++ cli/agentexec.go | 122 ++++++++++++++++++++++++++++++++ cli/agentexec_test.go | 49 +++++++++++++ cli/root.go | 1 + 6 files changed, 305 insertions(+) create mode 100644 agent/agentexec/cli.go create mode 100644 agent/agentexec/cmdtest/main.go create mode 100644 agent/agentexec/exec.go create mode 100644 cli/agentexec.go create mode 100644 cli/agentexec_test.go diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli.go new file mode 100644 index 0000000000000..138d35ee4aaae --- /dev/null +++ b/agent/agentexec/cli.go @@ -0,0 +1,75 @@ +package agentexec + +import ( + "context" + "fmt" + "os" + "os/exec" + "runtime" + "slices" + "strconv" + "strings" + "syscall" + + "golang.org/x/xerrors" +) + +const ( + EnvProcOOMScore = "CODER_PROC_OOM_SCORE" + EnvProcNiceScore = "CODER_PROC_NICE_SCORE" +) + +// CLI runs the agent-exec command. It should only be called by the cli package. +func CLI(ctx context.Context, args []string, environ []string) error { + if runtime.GOOS != "linux" { + return xerrors.Errorf("agent-exec is only supported on Linux") + } + + pid := os.Getpid() + + oomScore, ok := envVal(environ, EnvProcOOMScore) + if !ok { + return xerrors.Errorf("missing %q", EnvProcOOMScore) + } + + niceScore, ok := envVal(environ, EnvProcNiceScore) + if !ok { + return xerrors.Errorf("missing %q", EnvProcNiceScore) + } + + score, err := strconv.Atoi(niceScore) + if err != nil { + return xerrors.Errorf("invalid nice score: %w", err) + } + + err = syscall.Setpriority(syscall.PRIO_PROCESS, pid, score) + if err != nil { + return xerrors.Errorf("set nice score: %w", err) + } + + oomPath := fmt.Sprintf("/proc/%d/oom_score_adj", pid) + err = os.WriteFile(oomPath, []byte(oomScore), 0o600) + if err != nil { + return xerrors.Errorf("set oom score: %w", err) + } + + path, err := exec.LookPath(args[0]) + if err != nil { + return xerrors.Errorf("look path: %w", err) + } + + env := slices.DeleteFunc(environ, func(env string) bool { + return strings.HasPrefix(env, EnvProcOOMScore) || strings.HasPrefix(env, EnvProcNiceScore) + }) + + return syscall.Exec(path, args, env) +} + +func envVal(environ []string, key string) (string, bool) { + for _, env := range environ { + if strings.HasPrefix(env, key+"=") { + return strings.TrimPrefix(env, key+"="), true + } + } + return "", false +} diff --git a/agent/agentexec/cmdtest/main.go b/agent/agentexec/cmdtest/main.go new file mode 100644 index 0000000000000..3ed90958816f3 --- /dev/null +++ b/agent/agentexec/cmdtest/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/coder/coder/v2/agent/agentexec" +) + +func main() { + err := agentexec.CLI(context.Background(), os.Args, os.Environ()) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/agent/agentexec/exec.go b/agent/agentexec/exec.go new file mode 100644 index 0000000000000..ed3d82a9ffccd --- /dev/null +++ b/agent/agentexec/exec.go @@ -0,0 +1,41 @@ +package agentexec + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "runtime" + + "golang.org/x/xerrors" +) + +const ( + // EnvProcPrioMgmt is the environment variable that determines whether + // we attempt to manage process CPU and OOM Killer priority. + EnvProcPrioMgmt = "CODER_PROC_PRIO_MGMT" +) + +// CommandContext returns an exec.Cmd that calls "coder agent-exec" prior to exec'ing +// the provided command if CODER_PROC_PRIO_MGMT is set, otherwise a normal exec.Cmd +// is returned. All instances of exec.Cmd should flow through this function to ensure +// proper resource constraints are applied to the child process. +func CommandContext(ctx context.Context, cmd string, args []string) (*exec.Cmd, error) { + _, enabled := envVal(os.Environ(), EnvProcPrioMgmt) + if runtime.GOOS != "linux" || !enabled { + return exec.CommandContext(ctx, cmd, args...), nil + } + + executable, err := os.Executable() + if err != nil { + return nil, xerrors.Errorf("get executable: %w", err) + } + + bin, err := filepath.EvalSymlinks(executable) + if err != nil { + return nil, xerrors.Errorf("eval symlinks: %w", err) + } + + args = append([]string{"agent-exec", cmd}, args...) + return exec.CommandContext(ctx, bin, args...), nil +} diff --git a/cli/agentexec.go b/cli/agentexec.go new file mode 100644 index 0000000000000..fd6bb92de3070 --- /dev/null +++ b/cli/agentexec.go @@ -0,0 +1,122 @@ +package cli + +import ( + "context" + "fmt" + "os" + "os/exec" + "runtime" + "slices" + "strconv" + "strings" + "syscall" + + "golang.org/x/xerrors" + + "github.com/spf13/afero" + + "github.com/coder/serpent" +) + +const EnvProcOOMScore = "CODER_PROC_OOM_SCORE" +const EnvProcNiceScore = "CODER_PROC_NICE_SCORE" + +func (*RootCmd) agentExec() *serpent.Command { + return &serpent.Command{ + Use: "agent-exec", + Hidden: true, + RawArgs: true, + Handler: func(inv *serpent.Invocation) error { + if runtime.GOOS != "linux" { + return xerrors.Errorf("agent-exec is only supported on Linux") + } + + var ( + pid = os.Getpid() + args = inv.Args + oomScore = inv.Environ.Get(EnvProcOOMScore) + niceScore = inv.Environ.Get(EnvProcNiceScore) + + fs = fsFromContext(inv.Context()) + syscaller = syscallerFromContext(inv.Context()) + ) + + score, err := strconv.Atoi(niceScore) + if err != nil { + return xerrors.Errorf("invalid nice score: %w", err) + } + + err = syscaller.Setpriority(syscall.PRIO_PROCESS, pid, score) + if err != nil { + return xerrors.Errorf("set nice score: %w", err) + } + + oomPath := fmt.Sprintf("/proc/%d/oom_score_adj", pid) + err = afero.WriteFile(fs, oomPath, []byte(oomScore), 0o600) + if err != nil { + return xerrors.Errorf("set oom score: %w", err) + } + + path, err := exec.LookPath(args[0]) + if err != nil { + return xerrors.Errorf("look path: %w", err) + } + + env := slices.DeleteFunc(inv.Environ.ToOS(), excludeKeys(EnvProcOOMScore, EnvProcNiceScore)) + + return syscall.Exec(path, args, env) + }, + } +} + +func excludeKeys(keys ...string) func(env string) bool { + return func(env string) bool { + for _, key := range keys { + if strings.HasPrefix(env, key+"=") { + return true + } + } + return false + } +} + +type Syscaller interface { + Setpriority(int, int, int) error + Exec(string, []string, []string) error +} + +type linuxSyscaller struct{} + +func (linuxSyscaller) Setpriority(which, pid, nice int) error { + return syscall.Setpriority(which, pid, nice) +} + +func (linuxSyscaller) Exec(path string, args, env []string) error { + return syscall.Exec(path, args, env) +} + +type syscallerKey struct{} + +func WithSyscaller(ctx context.Context, syscaller Syscaller) context.Context { + return context.WithValue(ctx, syscallerKey{}, syscaller) +} + +func syscallerFromContext(ctx context.Context) Syscaller { + if syscaller, ok := ctx.Value(syscallerKey{}).(Syscaller); ok { + return syscaller + } + return linuxSyscaller{} +} + +type fsKey struct{} + +func WithFS(ctx context.Context, fs afero.Fs) context.Context { + return context.WithValue(ctx, fsKey{}, fs) +} + +func fsFromContext(ctx context.Context) afero.Fs { + if fs, ok := ctx.Value(fsKey{}).(afero.Fs); ok { + return fs + } + return afero.NewOsFs() +} diff --git a/cli/agentexec_test.go b/cli/agentexec_test.go new file mode 100644 index 0000000000000..999733f1952d1 --- /dev/null +++ b/cli/agentexec_test.go @@ -0,0 +1,49 @@ +package cli_test + +import ( + "bytes" + "io" + "runtime" + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli" + "github.com/coder/coder/v2/cli/clitest" +) + +func TestAgentExec(t *testing.T) { + t.Parallel() + + if runtime.GOOS != "linux" { + t.Skip("agent-exec is only supported on Linux") + } + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + inv, _ := clitest.New(t, "agent-exec", "echo", "hello") + inv.Environ.Set(cli.EnvProcOOMScore, "1000") + inv.Environ.Set(cli.EnvProcNiceScore, "10") + var buf bytes.Buffer + wr := &syncWriter{W: &buf} + inv.Stdout = wr + inv.Stderr = wr + clitest.Start(t, inv) + + require.Equal(t, "hello\n", buf.String()) + }) + +} + +type syncWriter struct { + W io.Writer + mu sync.Mutex +} + +func (w *syncWriter) Write(p []byte) (n int, err error) { + w.mu.Lock() + defer w.mu.Unlock() + return w.W.Write(p) +} diff --git a/cli/root.go b/cli/root.go index 9f9028c072423..892df16e4ff4f 100644 --- a/cli/root.go +++ b/cli/root.go @@ -122,6 +122,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { r.whoami(), // Hidden + r.agentExec(), r.expCmd(), r.gitssh(), r.support(), From 538c6c3c9335c48fd1a79b46445230f6a1d2fcce Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Mon, 18 Nov 2024 21:21:11 +0000 Subject: [PATCH 02/17] remove CLI references --- cli/agentexec.go | 122 ------------------------------------------ cli/agentexec_test.go | 49 ----------------- 2 files changed, 171 deletions(-) delete mode 100644 cli/agentexec.go delete mode 100644 cli/agentexec_test.go diff --git a/cli/agentexec.go b/cli/agentexec.go deleted file mode 100644 index fd6bb92de3070..0000000000000 --- a/cli/agentexec.go +++ /dev/null @@ -1,122 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "os" - "os/exec" - "runtime" - "slices" - "strconv" - "strings" - "syscall" - - "golang.org/x/xerrors" - - "github.com/spf13/afero" - - "github.com/coder/serpent" -) - -const EnvProcOOMScore = "CODER_PROC_OOM_SCORE" -const EnvProcNiceScore = "CODER_PROC_NICE_SCORE" - -func (*RootCmd) agentExec() *serpent.Command { - return &serpent.Command{ - Use: "agent-exec", - Hidden: true, - RawArgs: true, - Handler: func(inv *serpent.Invocation) error { - if runtime.GOOS != "linux" { - return xerrors.Errorf("agent-exec is only supported on Linux") - } - - var ( - pid = os.Getpid() - args = inv.Args - oomScore = inv.Environ.Get(EnvProcOOMScore) - niceScore = inv.Environ.Get(EnvProcNiceScore) - - fs = fsFromContext(inv.Context()) - syscaller = syscallerFromContext(inv.Context()) - ) - - score, err := strconv.Atoi(niceScore) - if err != nil { - return xerrors.Errorf("invalid nice score: %w", err) - } - - err = syscaller.Setpriority(syscall.PRIO_PROCESS, pid, score) - if err != nil { - return xerrors.Errorf("set nice score: %w", err) - } - - oomPath := fmt.Sprintf("/proc/%d/oom_score_adj", pid) - err = afero.WriteFile(fs, oomPath, []byte(oomScore), 0o600) - if err != nil { - return xerrors.Errorf("set oom score: %w", err) - } - - path, err := exec.LookPath(args[0]) - if err != nil { - return xerrors.Errorf("look path: %w", err) - } - - env := slices.DeleteFunc(inv.Environ.ToOS(), excludeKeys(EnvProcOOMScore, EnvProcNiceScore)) - - return syscall.Exec(path, args, env) - }, - } -} - -func excludeKeys(keys ...string) func(env string) bool { - return func(env string) bool { - for _, key := range keys { - if strings.HasPrefix(env, key+"=") { - return true - } - } - return false - } -} - -type Syscaller interface { - Setpriority(int, int, int) error - Exec(string, []string, []string) error -} - -type linuxSyscaller struct{} - -func (linuxSyscaller) Setpriority(which, pid, nice int) error { - return syscall.Setpriority(which, pid, nice) -} - -func (linuxSyscaller) Exec(path string, args, env []string) error { - return syscall.Exec(path, args, env) -} - -type syscallerKey struct{} - -func WithSyscaller(ctx context.Context, syscaller Syscaller) context.Context { - return context.WithValue(ctx, syscallerKey{}, syscaller) -} - -func syscallerFromContext(ctx context.Context) Syscaller { - if syscaller, ok := ctx.Value(syscallerKey{}).(Syscaller); ok { - return syscaller - } - return linuxSyscaller{} -} - -type fsKey struct{} - -func WithFS(ctx context.Context, fs afero.Fs) context.Context { - return context.WithValue(ctx, fsKey{}, fs) -} - -func fsFromContext(ctx context.Context) afero.Fs { - if fs, ok := ctx.Value(fsKey{}).(afero.Fs); ok { - return fs - } - return afero.NewOsFs() -} diff --git a/cli/agentexec_test.go b/cli/agentexec_test.go deleted file mode 100644 index 999733f1952d1..0000000000000 --- a/cli/agentexec_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package cli_test - -import ( - "bytes" - "io" - "runtime" - "sync" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/cli" - "github.com/coder/coder/v2/cli/clitest" -) - -func TestAgentExec(t *testing.T) { - t.Parallel() - - if runtime.GOOS != "linux" { - t.Skip("agent-exec is only supported on Linux") - } - - t.Run("OK", func(t *testing.T) { - t.Parallel() - - inv, _ := clitest.New(t, "agent-exec", "echo", "hello") - inv.Environ.Set(cli.EnvProcOOMScore, "1000") - inv.Environ.Set(cli.EnvProcNiceScore, "10") - var buf bytes.Buffer - wr := &syncWriter{W: &buf} - inv.Stdout = wr - inv.Stderr = wr - clitest.Start(t, inv) - - require.Equal(t, "hello\n", buf.String()) - }) - -} - -type syncWriter struct { - W io.Writer - mu sync.Mutex -} - -func (w *syncWriter) Write(p []byte) (n int, err error) { - w.mu.Lock() - defer w.mu.Unlock() - return w.W.Write(p) -} From 00c9cd71721842df30f4c52015b54dd11b80336f Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Mon, 18 Nov 2024 22:23:53 +0000 Subject: [PATCH 03/17] add basic test --- agent/agentexec/cli.go | 3 +- agent/agentexec/cli_test.go | 90 +++++++++++++++++++++++++++++++++ agent/agentexec/cmdtest/main.go | 3 +- agent/agentexec/main_test.go | 27 ++++++++++ 4 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 agent/agentexec/cli_test.go create mode 100644 agent/agentexec/main_test.go diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli.go index 138d35ee4aaae..931997190630e 100644 --- a/agent/agentexec/cli.go +++ b/agent/agentexec/cli.go @@ -1,7 +1,6 @@ package agentexec import ( - "context" "fmt" "os" "os/exec" @@ -20,7 +19,7 @@ const ( ) // CLI runs the agent-exec command. It should only be called by the cli package. -func CLI(ctx context.Context, args []string, environ []string) error { +func CLI(args []string, environ []string) error { if runtime.GOOS != "linux" { return xerrors.Errorf("agent-exec is only supported on Linux") } diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_test.go new file mode 100644 index 0000000000000..5f70d1a454dc2 --- /dev/null +++ b/agent/agentexec/cli_test.go @@ -0,0 +1,90 @@ +package agentexec_test + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" + + "github.com/coder/coder/v2/testutil" +) + +func TestCLI(t *testing.T) { + t.Parallel() + t.Run("OK", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + cmd, dir := cmd(t, ctx) + cmd.Env = append(cmd.Env, "CODER_PROC_NICE_SCORE=10") + cmd.Env = append(cmd.Env, "CODER_PROC_OOM_SCORE=123") + err := cmd.Start() + require.NoError(t, err) + + waitForSentinel(t, ctx, cmd, dir) + requireOOMScore(t, cmd.Process.Pid, 123) + requireNiceScore(t, cmd.Process.Pid, 10) + }) +} + +func requireNiceScore(t *testing.T, pid int, score int) { + t.Helper() + + nice, err := unix.Getpriority(0, pid) + require.NoError(t, err) + require.Equal(t, score, nice) +} + +func requireOOMScore(t *testing.T, pid int, expected int) { + t.Helper() + + actual, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid)) + require.NoError(t, err) + score := strings.TrimSpace(string(actual)) + require.Equal(t, strconv.Itoa(expected), score) +} + +func waitForSentinel(t *testing.T, ctx context.Context, cmd *exec.Cmd, dir string) { + t.Helper() + + require.Eventually(t, func() bool { + // Check if the process is still running. + err := cmd.Process.Signal(syscall.Signal(0)) + require.NoError(t, err) + + _, err = os.Stat(dir) + return err == nil && ctx.Err() == nil + }, testutil.WaitLong, testutil.IntervalFast) +} + +func cmd(t *testing.T, ctx context.Context, args ...string) (*exec.Cmd, string) { + dir := "" + cmd := exec.Command(TestBin, args...) + if len(args) == 0 { + dir = t.TempDir() + //nolint:gosec + cmd = exec.CommandContext(ctx, TestBin, "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", dir)) + } + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + t.Cleanup(func() { + // Print output of a command if the test fails. + if t.Failed() { + t.Logf("cmd %q output: %s", cmd.Args, buf.String()) + } + + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + }) + return cmd, dir +} diff --git a/agent/agentexec/cmdtest/main.go b/agent/agentexec/cmdtest/main.go index 3ed90958816f3..fbe5939e527ee 100644 --- a/agent/agentexec/cmdtest/main.go +++ b/agent/agentexec/cmdtest/main.go @@ -1,7 +1,6 @@ package main import ( - "context" "fmt" "os" @@ -9,7 +8,7 @@ import ( ) func main() { - err := agentexec.CLI(context.Background(), os.Args, os.Environ()) + err := agentexec.CLI(os.Args, os.Environ()) if err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/agent/agentexec/main_test.go b/agent/agentexec/main_test.go new file mode 100644 index 0000000000000..a70f9f7eadbdb --- /dev/null +++ b/agent/agentexec/main_test.go @@ -0,0 +1,27 @@ +package agentexec_test + +import ( + "fmt" + "os" + "os/exec" + "testing" +) + +const TestBin = "/tmp/agent-test" + +func TestMain(m *testing.M) { + buildBinary() + + os.Exit(m.Run()) +} + +func buildBinary() { + out, err := exec.Command("go", "build", "-o", TestBin, "./cmdtest").CombinedOutput() + mustf(err, "build binary: %s", out) +} + +func mustf(err error, msg string, args ...any) { + if err != nil { + panic(fmt.Sprintf(msg, args...)) + } +} From 196c8a90797324439de3d5d8368130fc49d18077 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Mon, 18 Nov 2024 23:02:26 +0000 Subject: [PATCH 04/17] fixup some test stuff --- agent/agentexec/cli.go | 6 +++++ agent/agentexec/cli_test.go | 46 ++++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli.go index 931997190630e..643218da58b23 100644 --- a/agent/agentexec/cli.go +++ b/agent/agentexec/cli.go @@ -24,6 +24,12 @@ func CLI(args []string, environ []string) error { return xerrors.Errorf("agent-exec is only supported on Linux") } + if len(args) < 2 { + return xerrors.Errorf("malformed command %q", args) + } + + args = args[2:] + pid := os.Getpid() oomScore, ok := envVal(environ, EnvProcOOMScore) diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_test.go index 5f70d1a454dc2..99b69aca998fe 100644 --- a/agent/agentexec/cli_test.go +++ b/agent/agentexec/cli_test.go @@ -6,10 +6,12 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strconv" "strings" "syscall" "testing" + "time" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" @@ -23,13 +25,14 @@ func TestCLI(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) - cmd, dir := cmd(t, ctx) + cmd, path := cmd(ctx, t) cmd.Env = append(cmd.Env, "CODER_PROC_NICE_SCORE=10") cmd.Env = append(cmd.Env, "CODER_PROC_OOM_SCORE=123") err := cmd.Start() require.NoError(t, err) + go cmd.Wait() - waitForSentinel(t, ctx, cmd, dir) + waitForSentinel(t, ctx, cmd, path) requireOOMScore(t, cmd.Process.Pid, 123) requireNiceScore(t, cmd.Process.Pid, 10) }) @@ -52,27 +55,40 @@ func requireOOMScore(t *testing.T, pid int, expected int) { require.Equal(t, strconv.Itoa(expected), score) } -func waitForSentinel(t *testing.T, ctx context.Context, cmd *exec.Cmd, dir string) { +func waitForSentinel(t *testing.T, ctx context.Context, cmd *exec.Cmd, path string) { t.Helper() - require.Eventually(t, func() bool { - // Check if the process is still running. + ticker := time.NewTicker(testutil.IntervalFast) + defer ticker.Stop() + + // RequireEventually doesn't work well with require.NoError or similar require functions. + for { err := cmd.Process.Signal(syscall.Signal(0)) require.NoError(t, err) - _, err = os.Stat(dir) - return err == nil && ctx.Err() == nil - }, testutil.WaitLong, testutil.IntervalFast) + _, err = os.Stat(path) + if err == nil { + return + } + + select { + case <-ticker.C: + case <-ctx.Done(): + return + } + } } -func cmd(t *testing.T, ctx context.Context, args ...string) (*exec.Cmd, string) { - dir := "" +func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) { + file := "" cmd := exec.Command(TestBin, args...) if len(args) == 0 { - dir = t.TempDir() + dir := t.TempDir() + file = filepath.Join(dir, uniqueFile(t)) //nolint:gosec - cmd = exec.CommandContext(ctx, TestBin, "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", dir)) + cmd = exec.CommandContext(ctx, TestBin, "agent-exec", "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file)) } + cmd.Env = os.Environ() var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf @@ -86,5 +102,9 @@ func cmd(t *testing.T, ctx context.Context, args ...string) (*exec.Cmd, string) _ = cmd.Process.Kill() } }) - return cmd, dir + return cmd, file +} + +func uniqueFile(t *testing.T) string { + return fmt.Sprintf("%s-%d", strings.ReplaceAll(t.Name(), "/", "_"), time.Now().UnixNano()) } From 97e68f410dc8f09dbba8917f0d79ca380c2b361f Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 02:16:42 +0000 Subject: [PATCH 05/17] idk --- agent/agentexec/cli.go | 107 ++++++++++++++++++++++++++++++------ agent/agentexec/cli_test.go | 72 ++++++++++++++++++++---- 2 files changed, 152 insertions(+), 27 deletions(-) diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli.go index 643218da58b23..d3914ed0e43a6 100644 --- a/agent/agentexec/cli.go +++ b/agent/agentexec/cli.go @@ -10,6 +10,7 @@ import ( "strings" "syscall" + "golang.org/x/sys/unix" "golang.org/x/xerrors" ) @@ -28,32 +29,37 @@ func CLI(args []string, environ []string) error { return xerrors.Errorf("malformed command %q", args) } + // Slice off 'coder agent-exec' args = args[2:] pid := os.Getpid() - oomScore, ok := envVal(environ, EnvProcOOMScore) + var err error + nice, ok := envValInt(environ, EnvProcNiceScore) if !ok { - return xerrors.Errorf("missing %q", EnvProcOOMScore) + // If an explicit nice score isn't set, we use the default. + nice, err = defaultNiceScore() + if err != nil { + return xerrors.Errorf("get default nice score: %w", err) + } + fmt.Println("nice score", nice, "pid", pid) } - niceScore, ok := envVal(environ, EnvProcNiceScore) + oomscore, ok := envValInt(environ, EnvProcOOMScore) if !ok { - return xerrors.Errorf("missing %q", EnvProcNiceScore) - } - - score, err := strconv.Atoi(niceScore) - if err != nil { - return xerrors.Errorf("invalid nice score: %w", err) + // If an explicit oom score isn't set, we use the default. + oomscore, err = defaultOOMScore() + if err != nil { + return xerrors.Errorf("get default oom score: %w", err) + } } - err = syscall.Setpriority(syscall.PRIO_PROCESS, pid, score) + err = unix.Setpriority(unix.PRIO_PROCESS, pid, nice) if err != nil { return xerrors.Errorf("set nice score: %w", err) } - oomPath := fmt.Sprintf("/proc/%d/oom_score_adj", pid) - err = os.WriteFile(oomPath, []byte(oomScore), 0o600) + err = writeOOMScoreAdj(pid, oomscore) if err != nil { return xerrors.Errorf("set oom score: %w", err) } @@ -63,6 +69,7 @@ func CLI(args []string, environ []string) error { return xerrors.Errorf("look path: %w", err) } + // Remove environments variables specifically set for the agent-exec command. env := slices.DeleteFunc(environ, func(env string) bool { return strings.HasPrefix(env, EnvProcOOMScore) || strings.HasPrefix(env, EnvProcNiceScore) }) @@ -70,10 +77,78 @@ func CLI(args []string, environ []string) error { return syscall.Exec(path, args, env) } -func envVal(environ []string, key string) (string, bool) { - for _, env := range environ { - if strings.HasPrefix(env, key+"=") { - return strings.TrimPrefix(env, key+"="), true +func defaultNiceScore() (int, error) { + score, err := unix.Getpriority(unix.PRIO_PROCESS, os.Getpid()) + if err != nil { + return 0, xerrors.Errorf("get nice score: %w", err) + } + // Priority is niceness + 20. + score -= 20 + + score += 5 + if score > 19 { + return 19, nil + } + return score, nil +} + +func defaultOOMScore() (int, error) { + score, err := oomScoreAdj(os.Getpid()) + if err != nil { + return 0, xerrors.Errorf("get oom score: %w", err) + } + + // If the agent has a negative oom_score_adj, we set the child to 0 + // so it's treated like every other process. + if score < 0 { + return 0, nil + } + + // If the agent is already almost at the maximum then set it to the max. + if score >= 998 { + return 1000, nil + } + + // If the agent oom_score_adj is >=0, we set the child to slightly + // less than the maximum. If users want a different score they set it + // directly. + return 998, nil +} + +func oomScoreAdj(pid int) (int, error) { + scoreStr, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid)) + if err != nil { + return 0, xerrors.Errorf("read oom_score_adj: %w", err) + } + return strconv.Atoi(strings.TrimSpace(string(scoreStr))) +} + +func writeOOMScoreAdj(pid int, score int) error { + return os.WriteFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), []byte(fmt.Sprintf("%d", score)), 0o600) +} + +// envValInt searches for a key in a list of environment variables and parses it to an int. +// If the key is not found or cannot be parsed, returns 0 and false. +func envValInt(env []string, key string) (int, bool) { + val, ok := envVal(env, key) + if !ok { + return 0, false + } + + i, err := strconv.Atoi(val) + if err != nil { + return 0, false + } + return i, true +} + +// envVal searches for a key in a list of environment variables and returns its value. +// If the key is not found, returns empty string and false. +func envVal(env []string, key string) (string, bool) { + prefix := key + "=" + for _, e := range env { + if strings.HasPrefix(e, prefix) { + return strings.TrimPrefix(e, prefix), true } } return "", false diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_test.go index 99b69aca998fe..675a86d477177 100644 --- a/agent/agentexec/cli_test.go +++ b/agent/agentexec/cli_test.go @@ -32,16 +32,34 @@ func TestCLI(t *testing.T) { require.NoError(t, err) go cmd.Wait() - waitForSentinel(t, ctx, cmd, path) + waitForSentinel(ctx, t, cmd, path) requireOOMScore(t, cmd.Process.Pid, 123) requireNiceScore(t, cmd.Process.Pid, 10) }) + + t.Run("Defaults", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + cmd, path := cmd(ctx, t) + err := cmd.Start() + require.NoError(t, err) + go cmd.Wait() + + waitForSentinel(ctx, t, cmd, path) + + expectedNice := expectedNiceScore(t) + expectedOOM := expectedOOMScore(t) + fmt.Println("expected nice", expectedNice, "expected oom", expectedOOM) + requireOOMScore(t, cmd.Process.Pid, expectedOOM) + requireNiceScore(t, cmd.Process.Pid, expectedNice) + }) } func requireNiceScore(t *testing.T, pid int, score int) { t.Helper() - nice, err := unix.Getpriority(0, pid) + nice, err := unix.Getpriority(unix.PRIO_PROCESS, pid) require.NoError(t, err) require.Equal(t, score, nice) } @@ -55,7 +73,7 @@ func requireOOMScore(t *testing.T, pid int, expected int) { require.Equal(t, strconv.Itoa(expected), score) } -func waitForSentinel(t *testing.T, ctx context.Context, cmd *exec.Cmd, path string) { +func waitForSentinel(ctx context.Context, t *testing.T, cmd *exec.Cmd, path string) { t.Helper() ticker := time.NewTicker(testutil.IntervalFast) @@ -74,17 +92,20 @@ func waitForSentinel(t *testing.T, ctx context.Context, cmd *exec.Cmd, path stri select { case <-ticker.C: case <-ctx.Done(): - return + require.NoError(t, ctx.Err()) } } } func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) { file := "" - cmd := exec.Command(TestBin, args...) + //nolint:gosec + cmd := exec.Command(TestBin, append([]string{"agent-exec"}, args...)...) if len(args) == 0 { + // Generate a unique path that we can touch to indicate that we've progressed past the + // syscall.Exec. dir := t.TempDir() - file = filepath.Join(dir, uniqueFile(t)) + file = filepath.Join(dir, "sentinel") //nolint:gosec cmd = exec.CommandContext(ctx, TestBin, "agent-exec", "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file)) } @@ -98,13 +119,42 @@ func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) t.Logf("cmd %q output: %s", cmd.Args, buf.String()) } - if cmd.Process != nil { - _ = cmd.Process.Kill() - } + // if cmd.Process != nil { + // _ = cmd.Process.Kill() + // } }) return cmd, file } -func uniqueFile(t *testing.T) string { - return fmt.Sprintf("%s-%d", strings.ReplaceAll(t.Name(), "/", "_"), time.Now().UnixNano()) +func expectedOOMScore(t *testing.T) int { + t.Helper() + + score, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", os.Getpid())) + require.NoError(t, err) + + scoreInt, err := strconv.Atoi(strings.TrimSpace(string(score))) + require.NoError(t, err) + + if scoreInt < 0 { + return 0 + } + if scoreInt >= 998 { + return 1000 + } + return 998 +} + +func expectedNiceScore(t *testing.T) int { + t.Helper() + + score, err := unix.Getpriority(unix.PRIO_PROCESS, os.Getpid()) + require.NoError(t, err) + + // Priority is niceness + 20. + score -= 20 + score += 5 + if score > 19 { + return 19 + } + return score } From 712b328fde102aa0cf098f8ee8e9f77dba5a7615 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 06:57:52 +0000 Subject: [PATCH 06/17] more tests --- agent/agentexec/cli.go | 9 +++--- agent/agentexec/cli_test.go | 22 +++++++------ agent/agentexec/exec.go | 5 +-- agent/agentexec/exec_test.go | 61 ++++++++++++++++++++++++++++++++++++ cli/root.go | 2 -- 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 agent/agentexec/exec_test.go diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli.go index d3914ed0e43a6..5596872b6f744 100644 --- a/agent/agentexec/cli.go +++ b/agent/agentexec/cli.go @@ -21,6 +21,8 @@ const ( // CLI runs the agent-exec command. It should only be called by the cli package. func CLI(args []string, environ []string) error { + runtime.LockOSThread() + if runtime.GOOS != "linux" { return xerrors.Errorf("agent-exec is only supported on Linux") } @@ -42,7 +44,6 @@ func CLI(args []string, environ []string) error { if err != nil { return xerrors.Errorf("get default nice score: %w", err) } - fmt.Println("nice score", nice, "pid", pid) } oomscore, ok := envValInt(environ, EnvProcOOMScore) @@ -54,7 +55,7 @@ func CLI(args []string, environ []string) error { } } - err = unix.Setpriority(unix.PRIO_PROCESS, pid, nice) + err = syscall.Setpriority(syscall.PRIO_PROCESS, 0, nice) if err != nil { return xerrors.Errorf("set nice score: %w", err) } @@ -82,8 +83,8 @@ func defaultNiceScore() (int, error) { if err != nil { return 0, xerrors.Errorf("get nice score: %w", err) } - // Priority is niceness + 20. - score -= 20 + // See https://linux.die.net/man/2/setpriority#Notes + score = 20 - score score += 5 if score > 19 { diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_test.go index 675a86d477177..26d26e9e65a58 100644 --- a/agent/agentexec/cli_test.go +++ b/agent/agentexec/cli_test.go @@ -26,7 +26,7 @@ func TestCLI(t *testing.T) { ctx := testutil.Context(t, testutil.WaitMedium) cmd, path := cmd(ctx, t) - cmd.Env = append(cmd.Env, "CODER_PROC_NICE_SCORE=10") + cmd.Env = append(cmd.Env, "CODER_PROC_NICE_SCORE=12") cmd.Env = append(cmd.Env, "CODER_PROC_OOM_SCORE=123") err := cmd.Start() require.NoError(t, err) @@ -34,7 +34,7 @@ func TestCLI(t *testing.T) { waitForSentinel(ctx, t, cmd, path) requireOOMScore(t, cmd.Process.Pid, 123) - requireNiceScore(t, cmd.Process.Pid, 10) + requireNiceScore(t, cmd.Process.Pid, 12) }) t.Run("Defaults", func(t *testing.T) { @@ -50,7 +50,6 @@ func TestCLI(t *testing.T) { expectedNice := expectedNiceScore(t) expectedOOM := expectedOOMScore(t) - fmt.Println("expected nice", expectedNice, "expected oom", expectedOOM) requireOOMScore(t, cmd.Process.Pid, expectedOOM) requireNiceScore(t, cmd.Process.Pid, expectedNice) }) @@ -61,7 +60,8 @@ func requireNiceScore(t *testing.T, pid int, score int) { nice, err := unix.Getpriority(unix.PRIO_PROCESS, pid) require.NoError(t, err) - require.Equal(t, score, nice) + // See https://linux.die.net/man/2/setpriority#Notes + require.Equal(t, score, 20-nice) } func requireOOMScore(t *testing.T, pid int, expected int) { @@ -108,6 +108,10 @@ func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) file = filepath.Join(dir, "sentinel") //nolint:gosec cmd = exec.CommandContext(ctx, TestBin, "agent-exec", "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file)) + // We set this so we can also easily kill the sleep process the shell spawns. + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } } cmd.Env = os.Environ() var buf bytes.Buffer @@ -118,10 +122,10 @@ func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) if t.Failed() { t.Logf("cmd %q output: %s", cmd.Args, buf.String()) } - - // if cmd.Process != nil { - // _ = cmd.Process.Kill() - // } + if cmd.Process != nil { + // We use -cmd.Process.Pid to kill the whole process group. + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) + } }) return cmd, file } @@ -151,7 +155,7 @@ func expectedNiceScore(t *testing.T) int { require.NoError(t, err) // Priority is niceness + 20. - score -= 20 + score = 20 - score score += 5 if score > 19 { return 19 diff --git a/agent/agentexec/exec.go b/agent/agentexec/exec.go index ed3d82a9ffccd..9e54cc9808cc4 100644 --- a/agent/agentexec/exec.go +++ b/agent/agentexec/exec.go @@ -20,8 +20,9 @@ const ( // the provided command if CODER_PROC_PRIO_MGMT is set, otherwise a normal exec.Cmd // is returned. All instances of exec.Cmd should flow through this function to ensure // proper resource constraints are applied to the child process. -func CommandContext(ctx context.Context, cmd string, args []string) (*exec.Cmd, error) { - _, enabled := envVal(os.Environ(), EnvProcPrioMgmt) +func CommandContext(ctx context.Context, cmd string, args ...string) (*exec.Cmd, error) { + environ := os.Environ() + _, enabled := envVal(environ, EnvProcPrioMgmt) if runtime.GOOS != "linux" || !enabled { return exec.CommandContext(ctx, cmd, args...), nil } diff --git a/agent/agentexec/exec_test.go b/agent/agentexec/exec_test.go new file mode 100644 index 0000000000000..7b294ae95f6ba --- /dev/null +++ b/agent/agentexec/exec_test.go @@ -0,0 +1,61 @@ +package agentexec_test + +import ( + "context" + "os" + "os/exec" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/agentexec" +) + +func TestExec(t *testing.T) { + t.Run("NonLinux", func(t *testing.T) { + + t.Setenv(agentexec.EnvProcPrioMgmt, "true") + + if runtime.GOOS == "linux" { + t.Skip("skipping on linux") + } + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + require.Equal(t, "sh", cmd.Path) + require.Equal(t, []string{"-c", "sleep"}, cmd.Args[1:]) + }) + + t.Run("Linux", func(t *testing.T) { + + t.Run("Disabled", func(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("skipping on linux") + } + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + path, err := exec.LookPath("sh") + require.NoError(t, err) + require.Equal(t, path, cmd.Path) + require.Equal(t, []string{"sh", "-c", "sleep"}, cmd.Args) + }) + + t.Run("Enabled", func(t *testing.T) { + t.Setenv(agentexec.EnvProcPrioMgmt, "hello") + + if runtime.GOOS != "linux" { + t.Skip("skipping on linux") + } + + executable, err := os.Executable() + require.NoError(t, err) + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + require.Equal(t, executable, cmd.Path) + require.Equal(t, []string{executable, "agent-exec", "sh", "-c", "sleep"}, cmd.Args) + }) + }) +} diff --git a/cli/root.go b/cli/root.go index 892df16e4ff4f..f162b9b2cd22b 100644 --- a/cli/root.go +++ b/cli/root.go @@ -121,8 +121,6 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { r.update(), r.whoami(), - // Hidden - r.agentExec(), r.expCmd(), r.gitssh(), r.support(), From 5f633e16712b4dd21f7fa9fd9ade8ffdc350e3dd Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 07:07:13 +0000 Subject: [PATCH 07/17] lint --- agent/agentexec/exec_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/agent/agentexec/exec_test.go b/agent/agentexec/exec_test.go index 7b294ae95f6ba..41b3c9ad5be5d 100644 --- a/agent/agentexec/exec_test.go +++ b/agent/agentexec/exec_test.go @@ -12,9 +12,10 @@ import ( "github.com/coder/coder/v2/agent/agentexec" ) +//nolint:paralleltest // we need to test environment variables func TestExec(t *testing.T) { + //nolint:paralleltest // we need to test environment variables t.Run("NonLinux", func(t *testing.T) { - t.Setenv(agentexec.EnvProcPrioMgmt, "true") if runtime.GOOS == "linux" { @@ -23,12 +24,16 @@ func TestExec(t *testing.T) { cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") require.NoError(t, err) - require.Equal(t, "sh", cmd.Path) - require.Equal(t, []string{"-c", "sleep"}, cmd.Args[1:]) + + path, err := exec.LookPath("sh") + require.NoError(t, err) + require.Equal(t, path, cmd.Path) + require.Equal(t, []string{"sh", "-c", "sleep"}, cmd.Args) }) + //nolint:paralleltest // we need to test environment variables t.Run("Linux", func(t *testing.T) { - + //nolint:paralleltest // we need to test environment variables t.Run("Disabled", func(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("skipping on linux") @@ -42,6 +47,7 @@ func TestExec(t *testing.T) { require.Equal(t, []string{"sh", "-c", "sleep"}, cmd.Args) }) + //nolint:paralleltest // we need to test environment variables t.Run("Enabled", func(t *testing.T) { t.Setenv(agentexec.EnvProcPrioMgmt, "hello") From 05702c3e2cf2919f8a9c2c821b719e4e2c5f5300 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 07:14:57 +0000 Subject: [PATCH 08/17] skip nonlinux --- agent/agentexec/cli_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_test.go index 26d26e9e65a58..7b456abe2d1e1 100644 --- a/agent/agentexec/cli_test.go +++ b/agent/agentexec/cli_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -21,6 +22,11 @@ import ( func TestCLI(t *testing.T) { t.Parallel() + + if runtime.GOOS != "linux" { + t.Skip("tests only valid on linux") + } + t.Run("OK", func(t *testing.T) { t.Parallel() From 0132fb3d36269b8a5f6769d8bce680284190a80e Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 19:00:38 +0000 Subject: [PATCH 09/17] build files --- agent/agentexec/cli_other.go | 10 +++++++ agent/agentexec/{cli.go => cli_unix.go} | 35 +++---------------------- agent/agentexec/exec.go | 33 ++++++++++++++++++++++- 3 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 agent/agentexec/cli_other.go rename agent/agentexec/{cli.go => cli_unix.go} (78%) diff --git a/agent/agentexec/cli_other.go b/agent/agentexec/cli_other.go new file mode 100644 index 0000000000000..79e98957734fe --- /dev/null +++ b/agent/agentexec/cli_other.go @@ -0,0 +1,10 @@ +//go:build !linux +// +build !linux + +package agentexec + +import "golang.org/x/xerrors" + +func CLI(args []string, environ []string) error { + return xerrors.Errorf("agent-exec is only supported on Linux") +} diff --git a/agent/agentexec/cli.go b/agent/agentexec/cli_unix.go similarity index 78% rename from agent/agentexec/cli.go rename to agent/agentexec/cli_unix.go index 5596872b6f744..fe4a824f45d6c 100644 --- a/agent/agentexec/cli.go +++ b/agent/agentexec/cli_unix.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package agentexec import ( @@ -14,11 +17,6 @@ import ( "golang.org/x/xerrors" ) -const ( - EnvProcOOMScore = "CODER_PROC_OOM_SCORE" - EnvProcNiceScore = "CODER_PROC_NICE_SCORE" -) - // CLI runs the agent-exec command. It should only be called by the cli package. func CLI(args []string, environ []string) error { runtime.LockOSThread() @@ -127,30 +125,3 @@ func oomScoreAdj(pid int) (int, error) { func writeOOMScoreAdj(pid int, score int) error { return os.WriteFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), []byte(fmt.Sprintf("%d", score)), 0o600) } - -// envValInt searches for a key in a list of environment variables and parses it to an int. -// If the key is not found or cannot be parsed, returns 0 and false. -func envValInt(env []string, key string) (int, bool) { - val, ok := envVal(env, key) - if !ok { - return 0, false - } - - i, err := strconv.Atoi(val) - if err != nil { - return 0, false - } - return i, true -} - -// envVal searches for a key in a list of environment variables and returns its value. -// If the key is not found, returns empty string and false. -func envVal(env []string, key string) (string, bool) { - prefix := key + "=" - for _, e := range env { - if strings.HasPrefix(e, prefix) { - return strings.TrimPrefix(e, prefix), true - } - } - return "", false -} diff --git a/agent/agentexec/exec.go b/agent/agentexec/exec.go index 9e54cc9808cc4..57616dd29b203 100644 --- a/agent/agentexec/exec.go +++ b/agent/agentexec/exec.go @@ -6,6 +6,8 @@ import ( "os/exec" "path/filepath" "runtime" + "strconv" + "strings" "golang.org/x/xerrors" ) @@ -13,7 +15,9 @@ import ( const ( // EnvProcPrioMgmt is the environment variable that determines whether // we attempt to manage process CPU and OOM Killer priority. - EnvProcPrioMgmt = "CODER_PROC_PRIO_MGMT" + EnvProcPrioMgmt = "CODER_PROC_PRIO_MGMT" + EnvProcOOMScore = "CODER_PROC_OOM_SCORE" + EnvProcNiceScore = "CODER_PROC_NICE_SCORE" ) // CommandContext returns an exec.Cmd that calls "coder agent-exec" prior to exec'ing @@ -40,3 +44,30 @@ func CommandContext(ctx context.Context, cmd string, args ...string) (*exec.Cmd, args = append([]string{"agent-exec", cmd}, args...) return exec.CommandContext(ctx, bin, args...), nil } + +// envValInt searches for a key in a list of environment variables and parses it to an int. +// If the key is not found or cannot be parsed, returns 0 and false. +func envValInt(env []string, key string) (int, bool) { + val, ok := envVal(env, key) + if !ok { + return 0, false + } + + i, err := strconv.Atoi(val) + if err != nil { + return 0, false + } + return i, true +} + +// envVal searches for a key in a list of environment variables and returns its value. +// If the key is not found, returns empty string and false. +func envVal(env []string, key string) (string, bool) { + prefix := key + "=" + for _, e := range env { + if strings.HasPrefix(e, prefix) { + return strings.TrimPrefix(e, prefix), true + } + } + return "", false +} From ae306438337aec61a76d3e8d917b1cd30eec9c2f Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 19:01:33 +0000 Subject: [PATCH 10/17] test file only for linux --- agent/agentexec/cli_unix.go | 2 +- agent/agentexec/{cli_test.go => cli_unix_test.go} | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) rename agent/agentexec/{cli_test.go => cli_unix_test.go} (97%) diff --git a/agent/agentexec/cli_unix.go b/agent/agentexec/cli_unix.go index fe4a824f45d6c..9d7d528f8ff36 100644 --- a/agent/agentexec/cli_unix.go +++ b/agent/agentexec/cli_unix.go @@ -53,7 +53,7 @@ func CLI(args []string, environ []string) error { } } - err = syscall.Setpriority(syscall.PRIO_PROCESS, 0, nice) + err = unix.Setpriority(unix.PRIO_PROCESS, 0, nice) if err != nil { return xerrors.Errorf("set nice score: %w", err) } diff --git a/agent/agentexec/cli_test.go b/agent/agentexec/cli_unix_test.go similarity index 97% rename from agent/agentexec/cli_test.go rename to agent/agentexec/cli_unix_test.go index 7b456abe2d1e1..0de127f4224aa 100644 --- a/agent/agentexec/cli_test.go +++ b/agent/agentexec/cli_unix_test.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package agentexec_test import ( @@ -7,7 +10,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strconv" "strings" "syscall" @@ -23,10 +25,6 @@ import ( func TestCLI(t *testing.T) { t.Parallel() - if runtime.GOOS != "linux" { - t.Skip("tests only valid on linux") - } - t.Run("OK", func(t *testing.T) { t.Parallel() From 986e18e73f087e060d67f613147197df6b706502 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 21:07:34 +0000 Subject: [PATCH 11/17] use flag parsing instead --- agent/agentexec/cli_unix.go | 24 ++++++++++------ agent/agentexec/exec.go | 44 +++++++++++++++++------------ agent/agentexec/exec_test.go | 54 +++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 26 deletions(-) diff --git a/agent/agentexec/cli_unix.go b/agent/agentexec/cli_unix.go index 9d7d528f8ff36..637855950005b 100644 --- a/agent/agentexec/cli_unix.go +++ b/agent/agentexec/cli_unix.go @@ -4,6 +4,7 @@ package agentexec import ( + "flag" "fmt" "os" "os/exec" @@ -19,8 +20,15 @@ import ( // CLI runs the agent-exec command. It should only be called by the cli package. func CLI(args []string, environ []string) error { + // We lock the OS thread here to avoid a race conditino where the nice priority + // we get is on a different thread from the one we set it on. runtime.LockOSThread() + nice := flag.Int(niceArg, 0, "") + oom := flag.Int(oomArg, 0, "") + + flag.Parse() + if runtime.GOOS != "linux" { return xerrors.Errorf("agent-exec is only supported on Linux") } @@ -35,30 +43,30 @@ func CLI(args []string, environ []string) error { pid := os.Getpid() var err error - nice, ok := envValInt(environ, EnvProcNiceScore) - if !ok { + if nice == nil { // If an explicit nice score isn't set, we use the default. - nice, err = defaultNiceScore() + n, err := defaultNiceScore() if err != nil { return xerrors.Errorf("get default nice score: %w", err) } + nice = &n } - oomscore, ok := envValInt(environ, EnvProcOOMScore) - if !ok { + if oom == nil { // If an explicit oom score isn't set, we use the default. - oomscore, err = defaultOOMScore() + o, err := defaultOOMScore() if err != nil { return xerrors.Errorf("get default oom score: %w", err) } + oom = &o } - err = unix.Setpriority(unix.PRIO_PROCESS, 0, nice) + err = unix.Setpriority(unix.PRIO_PROCESS, 0, *nice) if err != nil { return xerrors.Errorf("set nice score: %w", err) } - err = writeOOMScoreAdj(pid, oomscore) + err = writeOOMScoreAdj(pid, *oom) if err != nil { return xerrors.Errorf("set oom score: %w", err) } diff --git a/agent/agentexec/exec.go b/agent/agentexec/exec.go index 57616dd29b203..09bc071084e49 100644 --- a/agent/agentexec/exec.go +++ b/agent/agentexec/exec.go @@ -2,12 +2,12 @@ package agentexec import ( "context" + "fmt" "os" "os/exec" "path/filepath" "runtime" "strconv" - "strings" "golang.org/x/xerrors" ) @@ -25,8 +25,7 @@ const ( // is returned. All instances of exec.Cmd should flow through this function to ensure // proper resource constraints are applied to the child process. func CommandContext(ctx context.Context, cmd string, args ...string) (*exec.Cmd, error) { - environ := os.Environ() - _, enabled := envVal(environ, EnvProcPrioMgmt) + _, enabled := os.LookupEnv(EnvProcPrioMgmt) if runtime.GOOS != "linux" || !enabled { return exec.CommandContext(ctx, cmd, args...), nil } @@ -41,14 +40,24 @@ func CommandContext(ctx context.Context, cmd string, args ...string) (*exec.Cmd, return nil, xerrors.Errorf("eval symlinks: %w", err) } - args = append([]string{"agent-exec", cmd}, args...) - return exec.CommandContext(ctx, bin, args...), nil + execArgs := []string{"agent-exec"} + if score, ok := envValInt(EnvProcOOMScore); ok { + execArgs = append(execArgs, oomScoreArg(score)) + } + + if score, ok := envValInt(EnvProcNiceScore); ok { + execArgs = append(execArgs, niceScoreArg(score)) + } + execArgs = append(execArgs, "--", cmd) + execArgs = append(execArgs, args...) + + return exec.CommandContext(ctx, bin, execArgs...), nil } // envValInt searches for a key in a list of environment variables and parses it to an int. // If the key is not found or cannot be parsed, returns 0 and false. -func envValInt(env []string, key string) (int, bool) { - val, ok := envVal(env, key) +func envValInt(key string) (int, bool) { + val, ok := os.LookupEnv(key) if !ok { return 0, false } @@ -60,14 +69,15 @@ func envValInt(env []string, key string) (int, bool) { return i, true } -// envVal searches for a key in a list of environment variables and returns its value. -// If the key is not found, returns empty string and false. -func envVal(env []string, key string) (string, bool) { - prefix := key + "=" - for _, e := range env { - if strings.HasPrefix(e, prefix) { - return strings.TrimPrefix(e, prefix), true - } - } - return "", false +const ( + niceArg = "coder-nice" + oomArg = "coder-oom" +) + +func niceScoreArg(score int) string { + return fmt.Sprintf("--%s=%d", niceArg, score) +} + +func oomScoreArg(score int) string { + return fmt.Sprintf("--%s=%d", oomArg, score) } diff --git a/agent/agentexec/exec_test.go b/agent/agentexec/exec_test.go index 41b3c9ad5be5d..26fcde259eea4 100644 --- a/agent/agentexec/exec_test.go +++ b/agent/agentexec/exec_test.go @@ -61,7 +61,59 @@ func TestExec(t *testing.T) { cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") require.NoError(t, err) require.Equal(t, executable, cmd.Path) - require.Equal(t, []string{executable, "agent-exec", "sh", "-c", "sleep"}, cmd.Args) + require.Equal(t, []string{executable, "agent-exec", "--", "sh", "-c", "sleep"}, cmd.Args) + }) + + t.Run("Nice", func(t *testing.T) { + t.Setenv(agentexec.EnvProcPrioMgmt, "hello") + t.Setenv(agentexec.EnvProcNiceScore, "10") + + if runtime.GOOS != "linux" { + t.Skip("skipping on linux") + } + + executable, err := os.Executable() + require.NoError(t, err) + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + require.Equal(t, executable, cmd.Path) + require.Equal(t, []string{executable, "agent-exec", "--coder-nice=10", "--", "sh", "-c", "sleep"}, cmd.Args) + }) + + t.Run("OOM", func(t *testing.T) { + t.Setenv(agentexec.EnvProcPrioMgmt, "hello") + t.Setenv(agentexec.EnvProcOOMScore, "123") + + if runtime.GOOS != "linux" { + t.Skip("skipping on linux") + } + + executable, err := os.Executable() + require.NoError(t, err) + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + require.Equal(t, executable, cmd.Path) + require.Equal(t, []string{executable, "agent-exec", "--coder-oom=123", "--", "sh", "-c", "sleep"}, cmd.Args) + }) + + t.Run("Both", func(t *testing.T) { + t.Setenv(agentexec.EnvProcPrioMgmt, "hello") + t.Setenv(agentexec.EnvProcOOMScore, "432") + t.Setenv(agentexec.EnvProcNiceScore, "14") + + if runtime.GOOS != "linux" { + t.Skip("skipping on linux") + } + + executable, err := os.Executable() + require.NoError(t, err) + + cmd, err := agentexec.CommandContext(context.Background(), "sh", "-c", "sleep") + require.NoError(t, err) + require.Equal(t, executable, cmd.Path) + require.Equal(t, []string{executable, "agent-exec", "--coder-oom=432", "--coder-nice=14", "--", "sh", "-c", "sleep"}, cmd.Args) }) }) } From b1589197aed3117e39bb74fcd614b0f49753c376 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 22:07:53 +0000 Subject: [PATCH 12/17] flag parsing --- agent/agentexec/cli_unix.go | 60 ++++++++++++++++++++------------ agent/agentexec/cli_unix_test.go | 46 ++++++++++++++---------- agent/agentexec/cmdtest/main.go | 2 +- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/agent/agentexec/cli_unix.go b/agent/agentexec/cli_unix.go index 637855950005b..ba1b446bef1e6 100644 --- a/agent/agentexec/cli_unix.go +++ b/agent/agentexec/cli_unix.go @@ -9,7 +9,6 @@ import ( "os" "os/exec" "runtime" - "slices" "strconv" "strings" "syscall" @@ -18,47 +17,57 @@ import ( "golang.org/x/xerrors" ) +// unset is set to an invalid value for nice and oom scores. +const unset = -2000 + // CLI runs the agent-exec command. It should only be called by the cli package. -func CLI(args []string, environ []string) error { +func CLI() error { // We lock the OS thread here to avoid a race conditino where the nice priority // we get is on a different thread from the one we set it on. runtime.LockOSThread() - nice := flag.Int(niceArg, 0, "") - oom := flag.Int(oomArg, 0, "") + var ( + fs = flag.NewFlagSet("agent-exec", flag.ExitOnError) + nice = fs.Int("coder-nice", unset, "") + oom = fs.Int("coder-oom", unset, "") + ) + + if len(os.Args) < 3 { + return xerrors.Errorf("malformed command %+v", os.Args) + } - flag.Parse() + // Parse everything after "coder agent-exec". + err := fs.Parse(os.Args[2:]) + if err != nil { + return xerrors.Errorf("parse flags: %w", err) + } if runtime.GOOS != "linux" { return xerrors.Errorf("agent-exec is only supported on Linux") } - if len(args) < 2 { - return xerrors.Errorf("malformed command %q", args) + // Get everything after "coder agent-exec --" + args := execArgs(os.Args) + if len(args) == 0 { + return xerrors.Errorf("no exec command provided %+v", os.Args) } - // Slice off 'coder agent-exec' - args = args[2:] - pid := os.Getpid() - var err error - if nice == nil { + if *nice == unset { // If an explicit nice score isn't set, we use the default. - n, err := defaultNiceScore() + *nice, err = defaultNiceScore() if err != nil { return xerrors.Errorf("get default nice score: %w", err) } - nice = &n } - if oom == nil { + if *oom == unset { // If an explicit oom score isn't set, we use the default. - o, err := defaultOOMScore() + *oom, err = defaultOOMScore() if err != nil { return xerrors.Errorf("get default oom score: %w", err) } - oom = &o } err = unix.Setpriority(unix.PRIO_PROCESS, 0, *nice) @@ -76,12 +85,7 @@ func CLI(args []string, environ []string) error { return xerrors.Errorf("look path: %w", err) } - // Remove environments variables specifically set for the agent-exec command. - env := slices.DeleteFunc(environ, func(env string) bool { - return strings.HasPrefix(env, EnvProcOOMScore) || strings.HasPrefix(env, EnvProcNiceScore) - }) - - return syscall.Exec(path, args, env) + return syscall.Exec(path, args, os.Environ()) } func defaultNiceScore() (int, error) { @@ -133,3 +137,13 @@ func oomScoreAdj(pid int) (int, error) { func writeOOMScoreAdj(pid int, score int) error { return os.WriteFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), []byte(fmt.Sprintf("%d", score)), 0o600) } + +// execArgs returns the arguments to pass to syscall.Exec after the "--" delimiter. +func execArgs(args []string) []string { + for i, arg := range args { + if arg == "--" { + return args[i+1:] + } + } + return nil +} diff --git a/agent/agentexec/cli_unix_test.go b/agent/agentexec/cli_unix_test.go index 0de127f4224aa..6a5345971616d 100644 --- a/agent/agentexec/cli_unix_test.go +++ b/agent/agentexec/cli_unix_test.go @@ -29,9 +29,7 @@ func TestCLI(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) - cmd, path := cmd(ctx, t) - cmd.Env = append(cmd.Env, "CODER_PROC_NICE_SCORE=12") - cmd.Env = append(cmd.Env, "CODER_PROC_OOM_SCORE=123") + cmd, path := cmd(ctx, t, 123, 12) err := cmd.Start() require.NoError(t, err) go cmd.Wait() @@ -45,7 +43,7 @@ func TestCLI(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) - cmd, path := cmd(ctx, t) + cmd, path := cmd(ctx, t, 0, 0) err := cmd.Start() require.NoError(t, err) go cmd.Wait() @@ -101,22 +99,22 @@ func waitForSentinel(ctx context.Context, t *testing.T, cmd *exec.Cmd, path stri } } -func cmd(ctx context.Context, t *testing.T, args ...string) (*exec.Cmd, string) { - file := "" - //nolint:gosec - cmd := exec.Command(TestBin, append([]string{"agent-exec"}, args...)...) - if len(args) == 0 { - // Generate a unique path that we can touch to indicate that we've progressed past the - // syscall.Exec. - dir := t.TempDir() +func cmd(ctx context.Context, t *testing.T, oom, nice int) (*exec.Cmd, string) { + var ( + args = execArgs(oom, nice) + dir = t.TempDir() file = filepath.Join(dir, "sentinel") - //nolint:gosec - cmd = exec.CommandContext(ctx, TestBin, "agent-exec", "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file)) - // We set this so we can also easily kill the sleep process the shell spawns. - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } + ) + + args = append(args, "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file)) + //nolint:gosec + cmd := exec.CommandContext(ctx, TestBin, args...) + + // We set this so we can also easily kill the sleep process the shell spawns. + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, } + cmd.Env = os.Environ() var buf bytes.Buffer cmd.Stdout = &buf @@ -166,3 +164,15 @@ func expectedNiceScore(t *testing.T) int { } return score } + +func execArgs(oom int, nice int) []string { + execArgs := []string{"agent-exec"} + if oom != 0 { + execArgs = append(execArgs, fmt.Sprintf("--coder-oom=%d", oom)) + } + if nice != 0 { + execArgs = append(execArgs, fmt.Sprintf("--coder-nice=%d", nice)) + } + execArgs = append(execArgs, "--") + return execArgs +} diff --git a/agent/agentexec/cmdtest/main.go b/agent/agentexec/cmdtest/main.go index fbe5939e527ee..e96c05eb73c15 100644 --- a/agent/agentexec/cmdtest/main.go +++ b/agent/agentexec/cmdtest/main.go @@ -8,7 +8,7 @@ import ( ) func main() { - err := agentexec.CLI(os.Args, os.Environ()) + err := agentexec.CLI() if err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) From 7235bfb54e555b77498728fc1a03aec0d3d79b93 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 22:19:59 +0000 Subject: [PATCH 13/17] add comment for the use of flags --- agent/agentexec/exec.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/agent/agentexec/exec.go b/agent/agentexec/exec.go index 09bc071084e49..253671aeebe86 100644 --- a/agent/agentexec/exec.go +++ b/agent/agentexec/exec.go @@ -69,15 +69,18 @@ func envValInt(key string) (int, bool) { return i, true } +// The following are flags used by the agent-exec command. We use flags instead of +// environment variables to avoid having to deal with a caller overriding the +// environment variables. const ( - niceArg = "coder-nice" - oomArg = "coder-oom" + niceFlag = "coder-nice" + oomFlag = "coder-oom" ) func niceScoreArg(score int) string { - return fmt.Sprintf("--%s=%d", niceArg, score) + return fmt.Sprintf("--%s=%d", niceFlag, score) } func oomScoreArg(score int) string { - return fmt.Sprintf("--%s=%d", oomArg, score) + return fmt.Sprintf("--%s=%d", oomFlag, score) } From 521956f0f278e87cb15135c3404bee5926413df9 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 22:21:41 +0000 Subject: [PATCH 14/17] update cmdtest for other oses --- agent/agentexec/cli_other.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agentexec/cli_other.go b/agent/agentexec/cli_other.go index 79e98957734fe..2f109e2f0670c 100644 --- a/agent/agentexec/cli_other.go +++ b/agent/agentexec/cli_other.go @@ -5,6 +5,6 @@ package agentexec import "golang.org/x/xerrors" -func CLI(args []string, environ []string) error { +func CLI() error { return xerrors.Errorf("agent-exec is only supported on Linux") } From 471ea8d6d599d3237ee6d1d9ed5dfcff6902a053 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 20 Nov 2024 22:26:11 +0000 Subject: [PATCH 15/17] stray deletion --- cli/root.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/root.go b/cli/root.go index f162b9b2cd22b..9f9028c072423 100644 --- a/cli/root.go +++ b/cli/root.go @@ -121,6 +121,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { r.update(), r.whoami(), + // Hidden r.expCmd(), r.gitssh(), r.support(), From 5076cf08d3c342c74c44ace2bfe89db78469d467 Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Thu, 21 Nov 2024 21:08:12 +0000 Subject: [PATCH 16/17] pr changes --- agent/agentexec/{cli_unix.go => cli_linux.go} | 20 ++++----- .../{cli_unix_test.go => cli_linux_test.go} | 0 agent/agentexec/cli_other.go | 2 +- .../cmdtest/{main.go => main_linux.go} | 3 ++ agent/agentexec/main_linux_test.go | 43 +++++++++++++++++++ agent/agentexec/main_test.go | 27 ------------ 6 files changed, 55 insertions(+), 40 deletions(-) rename agent/agentexec/{cli_unix.go => cli_linux.go} (86%) rename agent/agentexec/{cli_unix_test.go => cli_linux_test.go} (100%) rename agent/agentexec/cmdtest/{main.go => main_linux.go} (85%) create mode 100644 agent/agentexec/main_linux_test.go delete mode 100644 agent/agentexec/main_test.go diff --git a/agent/agentexec/cli_unix.go b/agent/agentexec/cli_linux.go similarity index 86% rename from agent/agentexec/cli_unix.go rename to agent/agentexec/cli_linux.go index ba1b446bef1e6..f5881dff967cb 100644 --- a/agent/agentexec/cli_unix.go +++ b/agent/agentexec/cli_linux.go @@ -25,6 +25,8 @@ func CLI() error { // We lock the OS thread here to avoid a race conditino where the nice priority // we get is on a different thread from the one we set it on. runtime.LockOSThread() + // Nop on success but we do it anyway in case of an error. + defer runtime.UnlockOSThread() var ( fs = flag.NewFlagSet("agent-exec", flag.ExitOnError) @@ -42,18 +44,12 @@ func CLI() error { return xerrors.Errorf("parse flags: %w", err) } - if runtime.GOOS != "linux" { - return xerrors.Errorf("agent-exec is only supported on Linux") - } - // Get everything after "coder agent-exec --" args := execArgs(os.Args) if len(args) == 0 { return xerrors.Errorf("no exec command provided %+v", os.Args) } - pid := os.Getpid() - if *nice == unset { // If an explicit nice score isn't set, we use the default. *nice, err = defaultNiceScore() @@ -75,7 +71,7 @@ func CLI() error { return xerrors.Errorf("set nice score: %w", err) } - err = writeOOMScoreAdj(pid, *oom) + err = writeOOMScoreAdj(*oom) if err != nil { return xerrors.Errorf("set oom score: %w", err) } @@ -104,7 +100,7 @@ func defaultNiceScore() (int, error) { } func defaultOOMScore() (int, error) { - score, err := oomScoreAdj(os.Getpid()) + score, err := oomScoreAdj() if err != nil { return 0, xerrors.Errorf("get oom score: %w", err) } @@ -126,16 +122,16 @@ func defaultOOMScore() (int, error) { return 998, nil } -func oomScoreAdj(pid int) (int, error) { - scoreStr, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid)) +func oomScoreAdj() (int, error) { + scoreStr, err := os.ReadFile("/proc/self/oom_score_adj") if err != nil { return 0, xerrors.Errorf("read oom_score_adj: %w", err) } return strconv.Atoi(strings.TrimSpace(string(scoreStr))) } -func writeOOMScoreAdj(pid int, score int) error { - return os.WriteFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), []byte(fmt.Sprintf("%d", score)), 0o600) +func writeOOMScoreAdj(score int) error { + return os.WriteFile("/proc/self/oom_score_adj", []byte(fmt.Sprintf("%d", score)), 0o600) } // execArgs returns the arguments to pass to syscall.Exec after the "--" delimiter. diff --git a/agent/agentexec/cli_unix_test.go b/agent/agentexec/cli_linux_test.go similarity index 100% rename from agent/agentexec/cli_unix_test.go rename to agent/agentexec/cli_linux_test.go diff --git a/agent/agentexec/cli_other.go b/agent/agentexec/cli_other.go index 2f109e2f0670c..67fe7d1eede2b 100644 --- a/agent/agentexec/cli_other.go +++ b/agent/agentexec/cli_other.go @@ -6,5 +6,5 @@ package agentexec import "golang.org/x/xerrors" func CLI() error { - return xerrors.Errorf("agent-exec is only supported on Linux") + return xerrors.New("agent-exec is only supported on Linux") } diff --git a/agent/agentexec/cmdtest/main.go b/agent/agentexec/cmdtest/main_linux.go similarity index 85% rename from agent/agentexec/cmdtest/main.go rename to agent/agentexec/cmdtest/main_linux.go index e96c05eb73c15..8cd48f0b21812 100644 --- a/agent/agentexec/cmdtest/main.go +++ b/agent/agentexec/cmdtest/main_linux.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package main import ( diff --git a/agent/agentexec/main_linux_test.go b/agent/agentexec/main_linux_test.go new file mode 100644 index 0000000000000..096c7fbd80aea --- /dev/null +++ b/agent/agentexec/main_linux_test.go @@ -0,0 +1,43 @@ +package agentexec_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" +) + +var TestBin string + +func TestMain(m *testing.M) { + code := func() int { + // We generate a unique directory per test invocation to avoid collisions between two + // processes attempting to create the same temp file. + dir := genDir() + defer os.RemoveAll(dir) + TestBin = buildBinary(dir) + return m.Run() + }() + + os.Exit(code) +} + +func buildBinary(dir string) string { + path := filepath.Join(dir, "agent-test") + out, err := exec.Command("go", "build", "-o", path, "./cmdtest").CombinedOutput() + mustf(err, "build binary: %s", out) + return path +} + +func mustf(err error, msg string, args ...any) { + if err != nil { + panic(fmt.Sprintf(msg, args...)) + } +} + +func genDir() string { + dir, err := os.MkdirTemp(os.TempDir(), "agentexec") + mustf(err, "create temp dir: %v", err) + return dir +} diff --git a/agent/agentexec/main_test.go b/agent/agentexec/main_test.go deleted file mode 100644 index a70f9f7eadbdb..0000000000000 --- a/agent/agentexec/main_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package agentexec_test - -import ( - "fmt" - "os" - "os/exec" - "testing" -) - -const TestBin = "/tmp/agent-test" - -func TestMain(m *testing.M) { - buildBinary() - - os.Exit(m.Run()) -} - -func buildBinary() { - out, err := exec.Command("go", "build", "-o", TestBin, "./cmdtest").CombinedOutput() - mustf(err, "build binary: %s", out) -} - -func mustf(err error, msg string, args ...any) { - if err != nil { - panic(fmt.Sprintf(msg, args...)) - } -} From b8725f25e46544a6963b2393e4954fa9f256ff2d Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Fri, 22 Nov 2024 15:47:42 +0000 Subject: [PATCH 17/17] overlooked changes --- agent/agentexec/cli_linux.go | 4 ++-- agent/agentexec/main_linux_test.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/agent/agentexec/cli_linux.go b/agent/agentexec/cli_linux.go index f5881dff967cb..4081882712a40 100644 --- a/agent/agentexec/cli_linux.go +++ b/agent/agentexec/cli_linux.go @@ -22,7 +22,7 @@ const unset = -2000 // CLI runs the agent-exec command. It should only be called by the cli package. func CLI() error { - // We lock the OS thread here to avoid a race conditino where the nice priority + // We lock the OS thread here to avoid a race condition where the nice priority // we get is on a different thread from the one we set it on. runtime.LockOSThread() // Nop on success but we do it anyway in case of an error. @@ -85,7 +85,7 @@ func CLI() error { } func defaultNiceScore() (int, error) { - score, err := unix.Getpriority(unix.PRIO_PROCESS, os.Getpid()) + score, err := unix.Getpriority(unix.PRIO_PROCESS, 0) if err != nil { return 0, xerrors.Errorf("get nice score: %w", err) } diff --git a/agent/agentexec/main_linux_test.go b/agent/agentexec/main_linux_test.go index 096c7fbd80aea..8b5df84d60372 100644 --- a/agent/agentexec/main_linux_test.go +++ b/agent/agentexec/main_linux_test.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package agentexec_test import (