Move to typical go project layout, fix golangci-lint warnings, update deps
This commit is contained in:
15
internal/lib/constants.go
Normal file
15
internal/lib/constants.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package lib
|
||||
|
||||
const IOPRIO_CLASS_NONE = uint32(0) //nolint:revive,stylecheck
|
||||
const IOPRIO_CLASS_RT = uint32(1) //nolint:revive,stylecheck
|
||||
const IOPRIO_CLASS_BE = uint32(2) //nolint:revive,stylecheck
|
||||
const IOPRIO_CLASS_IDLE = uint32(3) //nolint:revive,stylecheck
|
||||
|
||||
const IOPRIO_WHO_PROCESS = 1 //nolint:revive,stylecheck
|
||||
const IOPRIO_WHO_PGRP = 2 //nolint:revive,stylecheck
|
||||
const IOPRIO_WHO_USER = 3 //nolint:revive,stylecheck
|
||||
|
||||
const IOPRIO_CLASS_SHIFT = uint32(13) //nolint:revive,stylecheck
|
||||
const IOPRIO_PRIO_MASK = ((uint32(1) << IOPRIO_CLASS_SHIFT) - 1) //nolint:revive,stylecheck
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
17
internal/lib/ionice.go
Normal file
17
internal/lib/ionice.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package lib
|
||||
|
||||
func PrioToClass(prio uint32) uint32 {
|
||||
return prio >> IOPRIO_CLASS_SHIFT
|
||||
}
|
||||
|
||||
func PrioToClassdata(prio uint32) uint32 {
|
||||
return prio & IOPRIO_PRIO_MASK
|
||||
}
|
||||
|
||||
func ClassAndClassdataToPrio(class uint32, classdata uint32) uint32 {
|
||||
return ((class) << IOPRIO_CLASS_SHIFT) | classdata
|
||||
}
|
||||
|
||||
func PrioToClassAndClassdata(prio uint32) (uint32, uint32) {
|
||||
return PrioToClass(prio), PrioToClassdata(prio)
|
||||
}
|
||||
80
internal/lib/ionice_linux.go
Normal file
80
internal/lib/ionice_linux.go
Normal file
@@ -0,0 +1,80 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"syscall"
|
||||
|
||||
proc "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
// GetIOPriority достаёт приоритет вводв-вывода для укаанного процесса.
|
||||
func GetIOPriority(which int, who int) (uint32, error) {
|
||||
var (
|
||||
err error
|
||||
r0, _, e1 = syscall.Syscall(syscall.SYS_IOPRIO_GET, uintptr(which), uintptr(who), 0)
|
||||
prio = uint32(r0)
|
||||
)
|
||||
|
||||
if e1 != 0 {
|
||||
err = fmt.Errorf("received error number %w", e1)
|
||||
}
|
||||
|
||||
return prio, err
|
||||
}
|
||||
|
||||
// GetIOPriority ставит приоритет вводв-вывода для укаанного процесса.
|
||||
func SetIOPriority(which int, who int, prio uint32) error {
|
||||
var (
|
||||
err error
|
||||
_, _, e1 = syscall.Syscall(syscall.SYS_IOPRIO_SET, uintptr(which), uintptr(who), uintptr(prio))
|
||||
)
|
||||
|
||||
if e1 != 0 {
|
||||
err = fmt.Errorf("received error number %w", e1)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IORenice выставляет процессу приоритет для операций ввода-вывода.
|
||||
func (cnf Config) IORenice(p *proc.Process, processName string) {
|
||||
if currentIOPrioLevel, err := GetIOPriority(IOPRIO_WHO_PROCESS, int(p.Pid)); err == nil {
|
||||
currentClass, currentClassdata := PrioToClassAndClassdata(currentIOPrioLevel)
|
||||
class := cnf.IoreniceClass[processName]
|
||||
classdata := cnf.IoreniceClassdata[processName]
|
||||
|
||||
if class == currentClass && classdata == currentClassdata {
|
||||
if cnf.Debug && currentIOPrioLevel != 0 { // 0 - это дефолтный уровень io niceness.
|
||||
log.Printf(
|
||||
"IONiceness for %s(%d) already set to %d %d",
|
||||
processName,
|
||||
p.Pid,
|
||||
currentClass,
|
||||
currentClassdata,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
iopriority := ClassAndClassdataToPrio(class, classdata)
|
||||
|
||||
_ = SetIOPriority(IOPRIO_WHO_PROCESS, int(p.Pid), iopriority)
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf(
|
||||
"Set ioniceness for %s(%d) to %d %d (was %d %d)",
|
||||
processName,
|
||||
p.Pid,
|
||||
class,
|
||||
classdata,
|
||||
currentClass,
|
||||
currentClassdata,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("Unable to get ioprio for %s(%d)", processName, p.Pid)
|
||||
}
|
||||
}
|
||||
16
internal/lib/ionice_other.go
Normal file
16
internal/lib/ionice_other.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package lib
|
||||
|
||||
import proc "github.com/shirou/gopsutil/v3/process"
|
||||
|
||||
func GetIOPriority(which int, who int) (prio uint32, err error) {
|
||||
return 0, NotImplementedError
|
||||
}
|
||||
|
||||
func SetIOPriority(which int, who int, prio uint32) (err error) {
|
||||
return NotImplementedError
|
||||
}
|
||||
|
||||
func IORenice(cnf Config, p *proc.Process, processName string) {}
|
||||
116
internal/lib/reniced.go
Normal file
116
internal/lib/reniced.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
proc "github.com/shirou/gopsutil/v3/process"
|
||||
)
|
||||
|
||||
// reniced основная логика.
|
||||
func (cnf Config) Reniced() {
|
||||
for {
|
||||
processList, err := proc.Processes()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get process list: %s", err)
|
||||
}
|
||||
|
||||
// Не самый эффективный, зато работающий способ - простой перебор массивов.
|
||||
for _, p := range processList {
|
||||
ok := cnf.Pool.TrySubmit(func() {
|
||||
if processName, err := p.Name(); err == nil {
|
||||
// Для каждого процесса извлекаем его текущий priority.
|
||||
if currentPrioLevel, err := syscall.Getpriority(syscall.PRIO_PROCESS, int(p.Pid)); err == nil {
|
||||
if niceLevel := cnf.Renice[processName]; niceLevel != 0 {
|
||||
prioLevel := niceLevel
|
||||
currentNiceLevel := currentPrioLevel
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
// Value of PR = 20 + (-20 to +19) is 0 to 39
|
||||
switch {
|
||||
case niceLevel > 0:
|
||||
prioLevel = 20 - niceLevel
|
||||
case niceLevel < 0:
|
||||
prioLevel = 20 + int(math.Abs(float64(niceLevel)))
|
||||
default:
|
||||
prioLevel = 20
|
||||
}
|
||||
|
||||
currentNiceLevel = 20 - currentPrioLevel
|
||||
}
|
||||
|
||||
if currentPrioLevel != prioLevel {
|
||||
// on Linux Setpriority actually operates niceness value.
|
||||
_ = syscall.Setpriority(syscall.PRIO_PROCESS, int(p.Pid), niceLevel)
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf(
|
||||
"Set niceness for %s(%d) to %d (was %d)",
|
||||
processName,
|
||||
p.Pid,
|
||||
niceLevel,
|
||||
currentNiceLevel,
|
||||
)
|
||||
}
|
||||
} else if cnf.Debug {
|
||||
log.Printf(
|
||||
"Niceness for %s(%d) already set to %d",
|
||||
processName,
|
||||
p.Pid,
|
||||
niceLevel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cnf.IORenice(p, processName)
|
||||
|
||||
// Посылаем процессу сигналы, если таковые есть в конфиге.
|
||||
if killSignal := cnf.KillSignal[processName]; killSignal != 0 {
|
||||
switch killSignal { //nolint:exhaustive
|
||||
case syscall.SIGSTOP:
|
||||
_ = p.SendSignal(killSignal)
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf(
|
||||
"Matching processName with killProcessname %s(%d), SIG%s sent",
|
||||
processName,
|
||||
p.Pid,
|
||||
strings.ToUpper(killSignal.String()),
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
_ = p.SendSignal(killSignal)
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf(
|
||||
"Matching processName with killProcessname %s(%d), SIG%s sent",
|
||||
processName,
|
||||
p.Pid,
|
||||
strings.ToUpper(killSignal.String()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if !ok {
|
||||
log.Printf("Unable to add task to pool. Increase nb_capacity?")
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * time.Duration(cnf.LoopDelay))
|
||||
}
|
||||
|
||||
// Мы сюда никогда не попадём.
|
||||
// pool.StopAndWait()
|
||||
} //nolint:wsl
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
30
internal/lib/sighandler.go
Normal file
30
internal/lib/sighandler.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sigHandler хэндлер сигналов. Работает на выходе приложения. Держит INT, TERM, QUIT.
|
||||
func (cnf Config) SigHandler() {
|
||||
for {
|
||||
var s = <-cnf.SigChan
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
log.Print("Got SIGINT, quitting")
|
||||
case syscall.SIGTERM:
|
||||
log.Print("Got SIGTERM, quitting")
|
||||
case syscall.SIGQUIT:
|
||||
log.Print("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
71
internal/lib/types.go
Normal file
71
internal/lib/types.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/alitto/pond"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Пул воркеров.
|
||||
Pool *pond.WorkerPool
|
||||
|
||||
KillSignal map[string]syscall.Signal
|
||||
Renice map[string]int
|
||||
IoreniceClass map[string]uint32
|
||||
IoreniceClassdata map[string]uint32
|
||||
ClassToString map[uint32]string
|
||||
StringToClass map[string]uint32
|
||||
|
||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||
SigChan chan os.Signal
|
||||
|
||||
NotImplementedError error
|
||||
|
||||
// Weather we fork to backgoround or stay as-is.
|
||||
Daemon bool `yaml:"daemon,omitempty"`
|
||||
|
||||
// Where to put pid-file.
|
||||
Pidfile string `yaml:"pidfile,omitempty"`
|
||||
|
||||
// LoopDelay in milliseconds - whole set of action delay period.
|
||||
LoopDelay int `yaml:"loop_delay,omitempty"`
|
||||
|
||||
// Whether to print debug info on stdout.
|
||||
Debug bool `yaml:"debug,omitempty"`
|
||||
|
||||
// Maximum workers in pool.
|
||||
MaxWorkers int `yaml:"max_workers,omitempty"`
|
||||
|
||||
// Capacity of non-blocking tasks. All tasks that exceeding this capacity will block submission of new tasks until
|
||||
// queue has vacant place for new task.
|
||||
NbCapacity int `yaml:"nb_capacity,omitempty"`
|
||||
|
||||
// В гошке yaml не позволяет сделать удобный парсер для конструкций, аналогичных json-овскому
|
||||
// {kill:{"stop":["proc1","proc2"], "term":["proc3","proc4"]}}
|
||||
// Поэтому мы просто возьмём массив и каждому элементу добавим тип сигнала.
|
||||
Kill Kill `yaml:"kill,omitempty"`
|
||||
|
||||
Prio Prio `yaml:"prio,omitempty"`
|
||||
|
||||
IOPrio IOPrio `yaml:"ioprio,omitempty"`
|
||||
}
|
||||
|
||||
type Prio []struct {
|
||||
Nice int `yaml:"nice,omitempty"`
|
||||
Name []string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
type Kill []struct {
|
||||
Sig string `yaml:"sig,omitempty"`
|
||||
Name []string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
type IOPrio []struct {
|
||||
Class uint32 `yaml:"class,omitempty"`
|
||||
Prio uint32 `yaml:"prio,omitempty"`
|
||||
Name []string `yaml:"name,omitempty"`
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
195
internal/lib/util.go
Normal file
195
internal/lib/util.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// readConf вычиляет, от какого пользователя запущен процесс и в зависимости от результата, скармливает readConfFile тот
|
||||
// или иной путь, по которому предположительно лежит конфиг.
|
||||
func (cnf *Config) ReadConf() error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
u, err := user.Current()
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get user info for current user: %s", err)
|
||||
}
|
||||
|
||||
// Для рута и для других пользователей конфиги находятся в разных местах.
|
||||
if u.Uid != "0" || runtime.GOOS == "darwin" {
|
||||
if os.Getenv("HOME") == "" {
|
||||
log.Fatalln("Unable to get HOME environment variable value")
|
||||
}
|
||||
|
||||
err = cnf.readConfFile(os.Getenv("HOME") + "/.reniced.yaml")
|
||||
} else {
|
||||
if runtime.GOOS == "freebsd" {
|
||||
err = cnf.readConfFile("/usr/local/etc/reniced.yaml")
|
||||
} else {
|
||||
err = cnf.readConfFile("/etc/reniced.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// readConfFile парсит даденный конфиг в глобальную структуру, с которой работает программа.
|
||||
func (cnf *Config) readConfFile(path string) error {
|
||||
fileInfo, err := os.Stat(path)
|
||||
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||
if fileInfo.Size() > 65535 {
|
||||
err = fmt.Errorf("config file %s is too long for config, skipping", path)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(path)
|
||||
|
||||
// Не удалось прочитать.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = yaml.Unmarshal(buf, &cnf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnf.LoopDelay == 0 {
|
||||
cnf.LoopDelay = 2000
|
||||
log.Printf("loop_delay set to %d milliseconds", cnf.LoopDelay)
|
||||
}
|
||||
|
||||
if cnf.Debug {
|
||||
log.Println("debug set to true")
|
||||
}
|
||||
|
||||
if cnf.MaxWorkers == 0 {
|
||||
cnf.MaxWorkers = 5
|
||||
log.Printf("max_workers set to %d", cnf.MaxWorkers)
|
||||
}
|
||||
|
||||
if cnf.NbCapacity == 0 {
|
||||
cnf.NbCapacity = 100
|
||||
log.Printf("nb_capacity set to %d", cnf.NbCapacity)
|
||||
}
|
||||
|
||||
if cnf.NbCapacity < cnf.MaxWorkers {
|
||||
cnf.NbCapacity = cnf.MaxWorkers
|
||||
log.Printf("nb_capacity set to same value as max_workers: %d", cnf.MaxWorkers)
|
||||
}
|
||||
|
||||
// Обычный юзер не может задрать nice больше 0, только руту такое можно.
|
||||
var maxNice int
|
||||
|
||||
if os.Getuid() == 0 {
|
||||
maxNice = -20
|
||||
}
|
||||
|
||||
for _, v := range cnf.Prio {
|
||||
switch {
|
||||
case len(v.Name) != 0 && (v.Nice < 20 && v.Nice > maxNice):
|
||||
for _, procName := range v.Name {
|
||||
cnf.Renice[procName] = v.Nice
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf("Add %s to list of nicelevel %d processes.", procName, v.Nice)
|
||||
}
|
||||
}
|
||||
case len(v.Name) == 0:
|
||||
log.Println("Skipping empty entry in prio config block")
|
||||
case v.Nice > 20 || v.Nice < maxNice:
|
||||
log.Printf("Niceness value out of range %d < nice < 20 in prio config block", maxNice)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range cnf.IOPrio {
|
||||
switch {
|
||||
case len(v.Name) == 0:
|
||||
log.Println("Skipping empty name entry in ioprio config block")
|
||||
case v.Class > 3:
|
||||
log.Printf("Skipping entry %s with class > 3 in ioprio config block", v.Name)
|
||||
case v.Class == 0 && v.Prio != 0:
|
||||
log.Printf("Skipping entry %s with class 0 and prio not 0 in ioprio config block", v.Name)
|
||||
case v.Class == 3 && v.Prio != 0:
|
||||
log.Printf("Skipping entry %s with class 3 and prio not 0 in ioprio config block", v.Name)
|
||||
case v.Class == 1 && v.Prio > 7:
|
||||
log.Printf("Skipping entry %s with class 1 and prio > 7 in ioprio config block", v.Name)
|
||||
case v.Class == 2 && v.Prio > 7:
|
||||
log.Printf("Skipping entry %s with class 2 and prio > 7 in ioprio config block", v.Name)
|
||||
default:
|
||||
for _, processName := range v.Name {
|
||||
cnf.IoreniceClass[processName] = v.Class
|
||||
cnf.IoreniceClassdata[processName] = v.Prio
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf("Add %s to list of ionice class %d processes.", processName, v.Class)
|
||||
log.Printf("Add %s to list of ionice prio %d processes.", processName, v.Prio)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range cnf.Kill {
|
||||
if len(v.Name) != 0 {
|
||||
switch v.Sig {
|
||||
case "kill", "stop", "term", "int", "quit", "abrt", "hup", "usr1", "usr2":
|
||||
for _, procName := range v.Name {
|
||||
switch v.Sig {
|
||||
case "kill":
|
||||
cnf.KillSignal[procName] = syscall.SIGKILL
|
||||
case "stop":
|
||||
cnf.KillSignal[procName] = syscall.SIGSTOP
|
||||
case "term":
|
||||
cnf.KillSignal[procName] = syscall.SIGTERM
|
||||
case "int":
|
||||
cnf.KillSignal[procName] = syscall.SIGINT
|
||||
case "quit":
|
||||
cnf.KillSignal[procName] = syscall.SIGQUIT
|
||||
case "abrt":
|
||||
cnf.KillSignal[procName] = syscall.SIGABRT
|
||||
case "hup":
|
||||
cnf.KillSignal[procName] = syscall.SIGHUP
|
||||
case "usr1":
|
||||
cnf.KillSignal[procName] = syscall.SIGUSR1
|
||||
case "usr2":
|
||||
cnf.KillSignal[procName] = syscall.SIGUSR2
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if cnf.Debug {
|
||||
log.Printf(
|
||||
"Add %s to list of processes that should be killed with SIG%s signal.",
|
||||
procName,
|
||||
strings.ToUpper(v.Sig),
|
||||
)
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Println("Skipping unsupported signal entry in kill config block")
|
||||
}
|
||||
} else {
|
||||
log.Println("Skipping empty entry in kill config block")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
Reference in New Issue
Block a user