diff --git a/go.mod b/go.mod index 37edd3f6..702aed93 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stretchr/testify v1.11.1 golang.org/x/net v0.52.0 gopkg.in/yaml.v2 v2.4.0 + github.com/hekmon/processpriority v1.0.0 ) require ( diff --git a/go.sum b/go.sum index 8790387c..c2868345 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/hekmon/processpriority v1.0.0 h1:Qj0P8iEO25b+8wGQ1ZG7HzBLpHjGGSgbujr++dSBn68= +github.com/hekmon/processpriority v1.0.0/go.mod h1:pZm/iZaUiyJpxD7Ad+gGAP3jusrALui1hXL9t7twP6M= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= diff --git a/system/cmd_runner_interface.go b/system/cmd_runner_interface.go index 64a28c15..d251a4f5 100644 --- a/system/cmd_runner_interface.go +++ b/system/cmd_runner_interface.go @@ -18,6 +18,9 @@ type Command struct { // Don't echo stdout/stderr Quiet bool + // Run command with a priority lower than the parent process + SpawnWithLowerPriority bool + Stdin io.Reader // Full stdout and stderr will be captured to memory diff --git a/system/exec_cmd_runner.go b/system/exec_cmd_runner.go index d9ffadd0..ef269929 100644 --- a/system/exec_cmd_runner.go +++ b/system/exec_cmd_runner.go @@ -3,9 +3,11 @@ package system import ( "os" "os/exec" + "runtime" "strings" boshlog "github.com/cloudfoundry/bosh-utils/logger" + "github.com/hekmon/processpriority" ) type execCmdRunner struct { @@ -16,6 +18,35 @@ func NewExecCmdRunner(logger boshlog.Logger) CmdRunner { return execCmdRunner{logger} } +func (r execCmdRunner) LowerProcessPriority(processPid int) error { + parentName := os.Args[0] + parentPid := os.Getpid() + + parentPrio, rawParentPrio, err := processpriority.Get(parentPid) + if err != nil { + r.logger.Debug(parentName, "Error getting priority of the current process (%d): %s", parentPid, err) + return err + } + r.logger.Debug(parentName, "Current process priority is %s (%d)", parentPrio, rawParentPrio) + + if runtime.GOOS == "windows" { + r.logger.Debug(parentName, "Setting new child process priority to IDLE") + err = processpriority.Set(processPid, processpriority.Idle) + } else { + processPrio := rawParentPrio + 5 + if processPrio > 19 { + processPrio = 19 + } + r.logger.Debug(parentName, "Setting new child process priority to %d", processPrio) + err = processpriority.SetRAW(processPid, processPrio) + } + + if err != nil { + r.logger.Error(parentName, "Error setting priority on the command: %s", err) + } + return err +} + func (r execCmdRunner) RunComplexCommand(cmd Command) (string, string, int, error) { process := NewExecProcess(r.buildComplexCommand(cmd), cmd.KeepAttached, cmd.Quiet, r.logger) @@ -24,6 +55,12 @@ func (r execCmdRunner) RunComplexCommand(cmd Command) (string, string, int, erro return "", "", -1, err } + if cmd.SpawnWithLowerPriority { + if err := r.LowerProcessPriority(process.cmd.Process.Pid); err != nil { + r.logger.Error(cmd.Name, "Error setting process priority on %s", cmd.Name) + } + } + result := <-process.Wait() return result.Stdout, result.Stderr, result.ExitStatus, result.Error diff --git a/system/exec_cmd_runner_test.go b/system/exec_cmd_runner_test.go index 56217abe..1395238f 100644 --- a/system/exec_cmd_runner_test.go +++ b/system/exec_cmd_runner_test.go @@ -13,6 +13,8 @@ import ( "github.com/cloudfoundry/bosh-utils/logger/loggerfakes" . "github.com/cloudfoundry/bosh-utils/system" fakesys "github.com/cloudfoundry/bosh-utils/system/fakes" + + "github.com/hekmon/processpriority" ) const ErrExitCode = 14 @@ -180,6 +182,35 @@ 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 the its nice value + script := "#!/bin/bash\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()) + err = os.Chmod(tmpFile.Name(), 0700) + Expect(err).ToNot(HaveOccurred()) + + parentPid := os.Getpid() + _, rawParentPrio, err := processpriority.Get(parentPid) + Expect(err).ToNot(HaveOccurred()) + expectedOutput := fmt.Sprintf("%d\n", rawParentPrio+5) + + // Run script with SpawnWithLowerPriority + cmd := Command{ + Name: tmpFile.Name(), + SpawnWithLowerPriority: true, + } + stdout, _, _, err := runner.RunComplexCommand(cmd) + + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(Equal(expectedOutput)) + }) }) Context("windows specific behavior", func() { @@ -247,6 +278,32 @@ 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 the its nice value + script := "$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("Idle\r\n")) + }) }) It("run complex command with stdin", func() { diff --git a/vendor/github.com/hekmon/processpriority/.gitignore b/vendor/github.com/hekmon/processpriority/.gitignore new file mode 100644 index 00000000..6f6f5e6a --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/.gitignore @@ -0,0 +1,22 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum diff --git a/vendor/github.com/hekmon/processpriority/LICENSE b/vendor/github.com/hekmon/processpriority/LICENSE new file mode 100644 index 00000000..68127e96 --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Edouard Hur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/hekmon/processpriority/README.md b/vendor/github.com/hekmon/processpriority/README.md new file mode 100644 index 00000000..147e1a82 --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/README.md @@ -0,0 +1,85 @@ +# Process Priority + +[![Go Reference](https://pkg.go.dev/badge/github.com/hekmon/processpriority.svg)](https://pkg.go.dev/github.com/hekmon/processpriority) + +Easily get or set a process priority from a Golang program with transparent Linux/Windows support. + +Under the hood, it will uses pre-defined nice values for Linux and the priority classes for Windows. + +## Notes + +### Linux + +On Linux, a user can only decrease its priority (increase its nice value), not increase it. This means that even after lowering the priority, you won't be able to raise it to its original value. + +To be able to increase it, the user must be root or have the `CAP_SYS_NICE` capability. + +### Windows + +On Windows, the real time priority can only be set as an administrator. If you try to set it as a normal user, you will won't get an error but your process will be set at `High` instead. + +## Example + +```golang +package main + +import ( + "fmt" + "os/exec" + + "github.com/hekmon/processpriority" +) + +func main() { + // Run command + // cmd := exec.Command("sleep", "1") // linux binary + cmd := exec.Command("./sleep.exe") // windows binary + err := cmd.Start() + if err != nil { + panic(err) + } + // Get its current priority + prio, rawPrio, err := processpriority.Get(cmd.Process.Pid) + if err != nil { + panic(err) + } + fmt.Printf("Current process priority is %s (%d)\n", prio, rawPrio) + // Change its priority + newPriority := processpriority.BelowNormal + fmt.Printf("Changing process priority to %s\n", newPriority) + if err = processpriority.Set(cmd.Process.Pid, newPriority); err != nil { + panic(err) + } + // Verifying + if prio, rawPrio, err = processpriority.Get(cmd.Process.Pid); err != nil { + panic(err) + } + fmt.Printf("Current process priority is %s (%d)\n", prio, rawPrio) + // Wait for the cmd to end + if err = cmd.Wait(); err != nil { + panic(err) + } +} +``` + +### Linux output + +``` +Current process priority is Normal (0) +Changing process priority to Below Normal +Current process priority is Below Normal (5) +``` + +### Windows output + +``` +Current process priority is Normal (32) +Changing process priority to Below Normal +Current process priority is Below Normal (16384) +``` + +## Install + +```bash +go get -u github.com/hekmon/processpriority +``` diff --git a/vendor/github.com/hekmon/processpriority/priority.go b/vendor/github.com/hekmon/processpriority/priority.go new file mode 100644 index 00000000..97fb9b29 --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/priority.go @@ -0,0 +1,49 @@ +package processpriority + +// ProcessPriority is a universal type for process priorities. It used with the universal wrapper Set() to be platform agnostic. +type ProcessPriority int + +const ( + // PriorityOSSpecific is only used on Get(), it indicates that the current level is not a universal one from this package. + OSSpecific ProcessPriority = iota + Idle + BelowNormal + Normal + AboveNormal + High + RealTime +) + +// String implements the fmt.Stringer interface +func (pp ProcessPriority) String() string { + switch pp { + case OSSpecific: + return "OS Specific" + case Idle: + return "Idle" + case BelowNormal: + return "Below Normal" + case Normal: + return "Normal" + case AboveNormal: + return "Above Normal" + case High: + return "High" + case RealTime: + return "Real Time" + default: + return "" + } +} + +// Set is an universal wrapper for setting process priority. +// It uses OS specific convertion and calls OS specific implementation. +func Set(pid int, priority ProcessPriority) (err error) { + return setOS(pid, priority) +} + +// Get is an universal wrapper for getting process priority. +// It uses OS specific convertion and calls OS specific implementation. +func Get(pid int) (priority ProcessPriority, rawOSPriority int, err error) { + return getOS(pid) +} diff --git a/vendor/github.com/hekmon/processpriority/priority_unix.go b/vendor/github.com/hekmon/processpriority/priority_unix.go new file mode 100644 index 00000000..d9827def --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/priority_unix.go @@ -0,0 +1,78 @@ +//go:build !windows + +package processpriority + +import ( + "fmt" + "syscall" +) + +// Opiniated nice values +const ( + UnixPriorityIdle = 19 + UnixPriorityBelowNormal = 5 + UnixPriorityNormal = 0 + UnixPriorityAboveNormal = -5 + UnixPriorityHigh = -10 + UnixPriorityRealTime = -20 +) + +func getOS(pid int) (priority ProcessPriority, rawPriority int, err error) { + if rawPriority, err = GetRAW(pid); err != nil { + return + } + switch rawPriority { + case UnixPriorityIdle: + priority = Idle + case UnixPriorityBelowNormal: + priority = BelowNormal + case UnixPriorityNormal: + priority = Normal + case UnixPriorityAboveNormal: + priority = AboveNormal + case UnixPriorityHigh: + priority = High + case UnixPriorityRealTime: + priority = RealTime + default: + priority = OSSpecific + } + return +} + +func setOS(pid int, priority ProcessPriority) error { + var unixPriority int + switch priority { + case Idle: + unixPriority = UnixPriorityIdle + case BelowNormal: + unixPriority = UnixPriorityBelowNormal + case Normal: + unixPriority = UnixPriorityNormal + case AboveNormal: + unixPriority = UnixPriorityAboveNormal + case High: + unixPriority = UnixPriorityHigh + case RealTime: + unixPriority = UnixPriorityRealTime + default: + return fmt.Errorf("unknown universal priority: %d", priority) + } + return SetRAW(pid, unixPriority) +} + +// GetRAW is an OS specific function to get the priority of a process. +// As priority values are not the same on all OSes, you should use the universal function Get() instead to be platform agnostic. +func GetRAW(pid int) (priority int, err error) { + if priority, err = syscall.Getpriority(syscall.PRIO_PROCESS, pid); err != nil { + return + } + priority = (priority - 20) * -1 // Convert knice to unice, see notes on https://linux.die.net/man/2/getpriority + return +} + +// SetRAW is an OS specific function to set the priority of a process. +// As priority values are not the same on all OSes, you should use the universal function Set() instead to be platform agnostic. +func SetRAW(pid, priority int) (err error) { + return syscall.Setpriority(syscall.PRIO_PROCESS, pid, priority) // it seems there is no need to convert nice value to knice value here +} diff --git a/vendor/github.com/hekmon/processpriority/priority_win.go b/vendor/github.com/hekmon/processpriority/priority_win.go new file mode 100644 index 00000000..0c81779c --- /dev/null +++ b/vendor/github.com/hekmon/processpriority/priority_win.go @@ -0,0 +1,97 @@ +//go:build windows + +package processpriority + +import ( + "fmt" + + "golang.org/x/sys/windows" +) + +// https://learn.microsoft.com/fr-fr/dotnet/api/system.diagnostics.process.priorityclass?view=net-8.0#remarques +// https://learn.microsoft.com/fr-fr/dotnet/api/system.diagnostics.processpriorityclass?view=net-8.0#champs +const ( + WinPriorityIdle = 64 + WinPriorityBelowNormal = 16384 + WinPriorityNormal = 32 + WinPriorityAboveNormal = 32768 + WinPriorityHigh = 128 + WinPriorityRealTime = 256 +) + +func getOS(pid int) (priority ProcessPriority, rawPriority int, err error) { + if rawPriority, err = GetRAW(pid); err != nil { + return + } + switch rawPriority { + case WinPriorityIdle: + priority = Idle + case WinPriorityBelowNormal: + priority = BelowNormal + case WinPriorityNormal: + priority = Normal + case WinPriorityAboveNormal: + priority = AboveNormal + case WinPriorityHigh: + priority = High + case WinPriorityRealTime: + priority = RealTime + default: + priority = OSSpecific + } + return +} + +func setOS(pid int, priority ProcessPriority) error { + var winPriority int + switch priority { + case Idle: + winPriority = WinPriorityIdle + case BelowNormal: + winPriority = WinPriorityBelowNormal + case Normal: + winPriority = WinPriorityNormal + case AboveNormal: + winPriority = WinPriorityAboveNormal + case High: + winPriority = WinPriorityHigh + case RealTime: + winPriority = WinPriorityRealTime + default: + return fmt.Errorf("unknown universal priority: %d", priority) + } + return SetRAW(pid, winPriority) +} + +// https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights +const processAllAccess = windows.STANDARD_RIGHTS_REQUIRED | windows.SYNCHRONIZE | 0xffff + +// GetRAW is an OS specific function to get the priority of a process. +// As priority values are not the same on all OSes, you should use the universal function Get() instead to be platform agnostic. +func GetRAW(pid int) (priority int, err error) { + handle, err := windows.OpenProcess(processAllAccess, false, uint32(pid)) + if err != nil { + return 0, fmt.Errorf("failed to open process: %w", err) + } + defer windows.CloseHandle(handle) + rawPriority, err := windows.GetPriorityClass(handle) + if err != nil { + return 0, fmt.Errorf("failed to get priority class: %w", err) + } + priority = int(rawPriority) + return +} + +// SetRAW is an OS specific function to set the priority of a process. +// As priority values are not the same on all OSes, you should use the universal function Set() instead to be platform agnostic. +func SetRAW(pid, priority int) error { + handle, err := windows.OpenProcess(processAllAccess, false, uint32(pid)) + if err != nil { + return fmt.Errorf("failed to open process: %w", err) + } + defer windows.CloseHandle(handle) + if err = windows.SetPriorityClass(handle, uint32(priority)); err != nil { + return fmt.Errorf("failed to set priority class: %w", err) + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 60e6327e..bcddd03f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -37,6 +37,9 @@ github.com/google/go-cmp/cmp/internal/value # github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc ## explicit; go 1.24.0 github.com/google/pprof/profile +# github.com/hekmon/processpriority v1.0.0 +## explicit; go 1.22.4 +github.com/hekmon/processpriority # github.com/jessevdk/go-flags v1.6.1 ## explicit; go 1.20 github.com/jessevdk/go-flags