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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/onsi/gomega v1.40.0
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.55.0
golang.org/x/sys v0.45.0
gopkg.in/yaml.v2 v2.4.0
)

Expand All @@ -33,7 +34,6 @@ require (
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect
Expand Down
6 changes: 6 additions & 0 deletions system/cmd_runner_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ type Command struct {
// Don't echo stdout/stderr
Quiet bool

// Run command with a lower scheduling priority than the parent process.
// On Unix: nice value is parent + 5, clamped at 19.
// On Windows: priority class is set to BelowNormal.
// If the parent is already at the minimum priority, the child will run at the same level.
SpawnWithLowerPriority bool

Stdin io.Reader

// Full stdout and stderr will be captured to memory
Expand Down
8 changes: 8 additions & 0 deletions system/exec_cmd_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func (r execCmdRunner) RunComplexCommand(cmd Command) (string, string, int, erro
return "", "", -1, err
}

if cmd.SpawnWithLowerPriority {
r.lowerProcessPriority(cmd.Name, process.cmd.Process.Pid) //nolint:errcheck
}
Comment thread
neddp marked this conversation as resolved.
Comment thread
neddp marked this conversation as resolved.

result := <-process.Wait()

return result.Stdout, result.Stderr, result.ExitStatus, result.Error
Expand All @@ -37,6 +41,10 @@ func (r execCmdRunner) RunComplexCommandAsync(cmd Command) (Process, error) {
return nil, err
}

if cmd.SpawnWithLowerPriority {
r.lowerProcessPriority(cmd.Name, process.cmd.Process.Pid) //nolint:errcheck
}
Comment thread
neddp marked this conversation as resolved.

return process, nil
}

Expand Down
115 changes: 115 additions & 0 deletions system/exec_cmd_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package system_test
import (
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -180,6 +182,66 @@ var _ = Describe("execCmdRunner", func() {
Expect(envVars).To(HaveKeyWithValue("ABC", "XYZ"))
Expect(envVars).To(HaveKeyWithValue("abc", "xyz"))
})

It("runs a command nicer than itself", func() {
// Write script that echos its nice value
// Sleep briefly to ensure parent has time to set priority
script := "#!/bin/bash\nsleep 0.1\nnice\n"
tmpFile, err := os.CreateTemp("", "tmp-script-*.sh")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(script)
Expect(err).ToNot(HaveOccurred())
err = tmpFile.Close()
Expect(err).ToNot(HaveOccurred())

niceOut, err := exec.Command("nice").Output()
Expect(err).ToNot(HaveOccurred())
parentNice, err := strconv.Atoi(strings.TrimSpace(string(niceOut)))
Expect(err).ToNot(HaveOccurred())
expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19))

// Run script with SpawnWithLowerPriority
cmd := Command{
Name: "bash",
Args: []string{tmpFile.Name()},
SpawnWithLowerPriority: true,
}
stdout, _, _, err := runner.RunComplexCommand(cmd)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expect(err).ToNot(HaveOccurred())
Expect(stdout).To(Equal(expectedOutput))
})

It("runs an async command nicer than itself", func() {
// Write script that echos its nice value
script := "#!/bin/bash\nsleep 0.1\nnice\n"
tmpFile, err := os.CreateTemp("", "tmp-script-*.sh")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(script)
Expect(err).ToNot(HaveOccurred())
err = tmpFile.Close()
Expect(err).ToNot(HaveOccurred())

niceOut, err := exec.Command("nice").Output()
Expect(err).ToNot(HaveOccurred())
parentNice, err := strconv.Atoi(strings.TrimSpace(string(niceOut)))
Expect(err).ToNot(HaveOccurred())
expectedOutput := fmt.Sprintf("%d\n", min(parentNice+5, 19))

cmd := Command{
Name: "bash",
Args: []string{tmpFile.Name()},
SpawnWithLowerPriority: true,
}
process, err := runner.RunComplexCommandAsync(cmd)
Expect(err).ToNot(HaveOccurred())

result := <-process.Wait()
Expect(result.Error).ToNot(HaveOccurred())
Expect(result.Stdout).To(Equal(expectedOutput))
})
})

Context("windows specific behavior", func() {
Expand Down Expand Up @@ -247,6 +309,59 @@ var _ = Describe("execCmdRunner", func() {
Expect(envVars).ToNot(HaveKey("_bar"))
Expect(envVars).To(HaveKeyWithValue("_BAR", "alpha=first"))
})

It("runs a command nicer than itself", func() {
// Write script that echos its priority class
// Sleep briefly to ensure parent has time to set priority
script := "Start-Sleep -Milliseconds 100\n$proc = Get-Process -Id $PID\nWrite-Output $proc.PriorityClass"

tmpFile, err := os.CreateTemp("", "tmp-script-*.ps1")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(script)
Expect(err).ToNot(HaveOccurred())
err = tmpFile.Close()
Expect(err).ToNot(HaveOccurred())
err = os.Chmod(tmpFile.Name(), 0700)
Expect(err).ToNot(HaveOccurred())

// Run script with SpawnWithLowerPriority
cmd := Command{
Name: "powershell",
Args: []string{"-ExecutionPolicy", "Bypass", "-File", tmpFile.Name()},
SpawnWithLowerPriority: true,
}
stdout, _, _, err := runner.RunComplexCommand(cmd)

Expect(err).ToNot(HaveOccurred())
Expect(stdout).To(Equal("BelowNormal\r\n"))
})
Comment thread
neddp marked this conversation as resolved.

It("runs an async command nicer than itself", func() {
script := "Start-Sleep -Milliseconds 100\n$proc = Get-Process -Id $PID\nWrite-Output $proc.PriorityClass"

tmpFile, err := os.CreateTemp("", "tmp-script-*.ps1")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(tmpFile.Name())
_, err = tmpFile.WriteString(script)
Expect(err).ToNot(HaveOccurred())
err = tmpFile.Close()
Expect(err).ToNot(HaveOccurred())
err = os.Chmod(tmpFile.Name(), 0700)
Expect(err).ToNot(HaveOccurred())

cmd := Command{
Name: "powershell",
Args: []string{"-ExecutionPolicy", "Bypass", "-File", tmpFile.Name()},
SpawnWithLowerPriority: true,
}
process, err := runner.RunComplexCommandAsync(cmd)
Expect(err).ToNot(HaveOccurred())

result := <-process.Wait()
Expect(result.Error).ToNot(HaveOccurred())
Expect(result.Stdout).To(Equal("BelowNormal\r\n"))
})
})

It("run complex command with stdin", func() {
Expand Down
48 changes: 48 additions & 0 deletions system/process_priority_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build !windows

// Inspired by github.com/hekmon/processpriority (MIT, Copyright 2024 Edouard Hur).
// Reimplemented inline to avoid the external dependency.

package system

import (
"os"
"syscall"
)

// getProcessPriority returns the nice value of the process with the given pid.
func getProcessPriority(pid int) (int, error) {
// syscall.Getpriority returns the "kernel nice" (20 - nice), so we convert.
// See https://linux.die.net/man/2/getpriority
knice, err := syscall.Getpriority(syscall.PRIO_PROCESS, pid)
if err != nil {
return 0, err
}
nice := (knice - 20) * -1
return nice, nil
}

// setProcessPriority sets the nice value of the process with the given pid.
func setProcessPriority(pid int, nice int) error {
return syscall.Setpriority(syscall.PRIO_PROCESS, pid, nice)
}

// lowerProcessPriority sets the child process nice value to parent + 5, clamped at 19.
func (r execCmdRunner) lowerProcessPriority(logTag string, processPid int) error {
parentPid := os.Getpid()

parentNice, err := getProcessPriority(parentPid)
if err != nil {
r.logger.Error(logTag, "Error getting priority of the current process (pid %d): %s", parentPid, err)
return err
}
r.logger.Debug(logTag, "Current process nice value is %d", parentNice)

childNice := min(parentNice+5, 19)
r.logger.Debug(logTag, "Setting child process (pid %d) nice value to %d", processPid, childNice)

if err = setProcessPriority(processPid, childNice); err != nil {
r.logger.Error(logTag, "Error setting priority on child process (pid %d): %s", processPid, err)
}
return err
}
66 changes: 66 additions & 0 deletions system/process_priority_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build windows

// Inspired by github.com/hekmon/processpriority (MIT, Copyright 2024 Edouard Hur).
// Reimplemented inline to avoid the external dependency.

package system

import (
"fmt"
"os"

"golang.org/x/sys/windows"
)

const (
// Windows process priority classes.
// https://learn.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
winPriorityBelowNormal = 0x4000 // BELOW_NORMAL_PRIORITY_CLASS
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// getProcessPriority returns the priority class of the process with the given pid.
func getProcessPriority(pid int) (int, error) {
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
if err != nil {
return 0, fmt.Errorf("failed to open process: %w", err)
}
defer windows.CloseHandle(handle) //nolint:errcheck

priority, err := windows.GetPriorityClass(handle)
if err != nil {
return 0, fmt.Errorf("failed to get priority class: %w", err)
}
return int(priority), nil
}

// setProcessPriority sets the priority class of the process with the given pid.
func setProcessPriority(pid int, priority int) error {
handle, err := windows.OpenProcess(windows.PROCESS_SET_INFORMATION, false, uint32(pid))
if err != nil {
return fmt.Errorf("failed to open process: %w", err)
}
defer windows.CloseHandle(handle) //nolint:errcheck

if err = windows.SetPriorityClass(handle, uint32(priority)); err != nil {
return fmt.Errorf("failed to set priority class: %w", err)
}
return nil
}

// lowerProcessPriority sets the child process priority class to BelowNormal.
func (r execCmdRunner) lowerProcessPriority(logTag string, processPid int) error {
parentPid := os.Getpid()

parentPriority, err := getProcessPriority(parentPid)
if err != nil {
r.logger.Error(logTag, "Error getting priority of the current process (pid %d): %s", parentPid, err)
return err
}
r.logger.Debug(logTag, "Current process priority class is %d", parentPriority)

r.logger.Debug(logTag, "Setting child process (pid %d) priority to BelowNormal", processPid)
if err = setProcessPriority(processPid, winPriorityBelowNormal); err != nil {
r.logger.Error(logTag, "Error setting priority on child process (pid %d): %s", processPid, err)
}
return err
}
13 changes: 13 additions & 0 deletions vendor/golang.org/x/sys/windows/aliases.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading