diff --git a/.gitignore b/.gitignore index 27f6a34..a0ff50c 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ $RECYCLE.BIN/ build/ vendor/ +cpu_ctrl diff --git a/daemon/daemon.go b/daemon/daemon.go new file mode 100644 index 0000000..ae514cf --- /dev/null +++ b/daemon/daemon.go @@ -0,0 +1,14 @@ +package daemon + +import "github.com/coreos/go-systemd/daemon" + +const ( + DaemonStarted = notifyType("READY=1") + DaemonStopping = notifyType("STOPPING=1") +) + +type notifyType string + +func NotifyDaemon(status notifyType) { + daemon.SdNotify(false, string(status)) +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..04a83c6 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,109 @@ +package logger + +import ( + "io" + "os" + "path" + + "github.com/sirupsen/logrus" + "github.com/x-cray/logrus-prefixed-formatter" + "gopkg.in/natefinch/lumberjack.v2" +) + +var ( + logger = logrus.StandardLogger() + rotaters []*lumberjack.Logger + logDir string + formatter = prefixed.TextFormatter{} +) + +type Config struct { + FileName string + MaxSizeMb int + MaxBackup int + MaxDay int +} + +type Logger interface { + Debugf(format string, args ...interface{}) + Infof(format string, args ...interface{}) + Printf(format string, args ...interface{}) + Warnf(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Fatalf(format string, args ...interface{}) + Panicf(format string, args ...interface{}) + Debug(args ...interface{}) + Info(args ...interface{}) + Print(args ...interface{}) + Warn(args ...interface{}) + Warning(args ...interface{}) + Error(args ...interface{}) + Fatal(args ...interface{}) + Panic(args ...interface{}) + Debugln(args ...interface{}) + Infoln(args ...interface{}) + Println(args ...interface{}) + Warnln(args ...interface{}) + Warningln(args ...interface{}) + Errorln(args ...interface{}) + Fatalln(args ...interface{}) + Panicln(args ...interface{}) +} + +func init() { + logger.Formatter = &formatter +} +func LoggerIsStd() bool { + return !formatter.DisableColors +} +func InitLogger(verbose bool, logDirArg string, config *Config) { + logDir = logDirArg + colorSupport, writer := NewLogWriter(config) + formatter.DisableColors = !colorSupport + logger.Out = writer + if verbose { + logger.Level = logrus.DebugLevel + } else { + logger.Level = logrus.InfoLevel + } +} + +func NewLogger(prefix string) Logger { + return logrus.NewEntry(logger).WithField("prefix", prefix) +} +func NewLogWriter(config *Config) (bool, io.Writer) { + switch config.FileName { + case "Stdout": + return true, os.Stdout + case "Stderr": + return true, os.Stderr + default: + logpath := config.FileName + if logDir != "" { + logpath = path.Join(logDir, config.FileName) + } + logger.Info(" Attention!! log writes to ", config.FileName) + rotater := &lumberjack.Logger{ + Filename: logpath, + MaxSize: config.MaxSizeMb, // megabytes + MaxBackups: config.MaxBackup, + MaxAge: config.MaxDay, //days + LocalTime: true, + Compress: true, + } + rotaters = append(rotaters, rotater) + return false, rotater + } +} + +func RotateLogger() { + if len(rotaters) == 0 { + return + } + logger.Info("rotating logger") + for _, rotater := range rotaters { + rotater.Rotate() + } + logger.Info("rotated") +} diff --git a/main.go b/main.go index 07957f6..6cf4351 100644 --- a/main.go +++ b/main.go @@ -1,228 +1,89 @@ package main import ( - "context" "errors" - "fmt" - "io/ioutil" "os" - "os/exec" "os/signal" - "path" - "path/filepath" - "strconv" - "strings" - "sync" + "runtime" "syscall" "time" - "amuz.es/src/infra/cpu_ctrl/pid" - - "github.com/coreos/go-systemd/daemon" - "github.com/shirou/gopsutil/cpu" -) - -const ( - auto = 0x0 - min = 0x04 - max = 0x64 + "amuz.es/src/infra/cpu_ctrl/daemon" + "amuz.es/src/infra/cpu_ctrl/logger" + "amuz.es/src/infra/cpu_ctrl/processor" + "amuz.es/src/infra/cpu_ctrl/util" ) var ( - prefix = [...]int{0x3a, 0x01} - suffix = [...]int{auto, auto, auto, auto, auto, auto} + log = logger.NewLogger("cpu_ctrl") ) -type notifyType string - -const ( - DaemonStarted = notifyType("READY=1") - DaemonStopping = notifyType("STOPPING=1") -) - -func NotifyDaemon(status notifyType) { - daemon.SdNotify(false, string(status)) +func init() { + logger.InitLogger(true, "", &logger.Config{FileName: "Stderr"}) + setMaxProcs() } -func getProcessorCount() int { - var maxcpu int - stat, err := cpu.Info() - if err != nil { - panic(err) - } - for _, info := range stat { - physicalId, err := strconv.Atoi(info.PhysicalID) - if err != nil { - panic(err) - } else if maxcpu < physicalId { - maxcpu = physicalId - } - } - return maxcpu + 1 -} +func setMaxProcs() { + // TODO(vmarmol): Consider limiting if we have a CPU mask in effect. + // Allow as many threads as we have cores unless the user specified a value. + var numProcs int + // if *maxProcs < 1 { + numProcs = runtime.NumCPU() + // } else { + // numProcs = *maxProcs + // } + runtime.GOMAXPROCS(numProcs) -type Processor struct { - Id int - TempeturePath string -} - -type TempetureChange struct { - Id int - Tempeture float64 -} - -func getProcessorInfo(processorId int) (*Processor, error) { - if matches, err := filepath.Glob(fmt.Sprintf("/sys/devices/platform/coretemp.%d/hwmon/hwmon?", processorId)); err != nil { - return nil, err - } else if matches == nil { - return nil, errors.New("hwmon not found!") - } else { - return &Processor{ - Id: processorId, - TempeturePath: matches[0], - }, nil - } -} -func ReadTempeture(path string, senseChan chan<- float64, errorChan chan<- error, waiter *sync.WaitGroup) { - defer waiter.Done() - if dat, err := ioutil.ReadFile(path); err != nil { - errorChan <- err - } else if tempetureSense, err := strconv.Atoi(strings.TrimSpace(string(dat))); err != nil { - errorChan <- err - } else { - senseChan <- float64(tempetureSense) / 1000.0 - } -} - -func CpuTempetureMonitoring(info *Processor, sampleDuration time.Duration, notifier chan<- TempetureChange, errorChan chan<- error, ctx context.Context, waiter *sync.WaitGroup) { - defer waiter.Done() - tempeturePathGlob := path.Join(info.TempeturePath, "temp?_input") - ticker := time.Tick(sampleDuration) - for { - select { - case <-ticker: - matches, err := filepath.Glob(tempeturePathGlob) - if err != nil { - panic(err) - } - tempetureReadWaiter := &sync.WaitGroup{} - queue := make(chan float64, len(matches)) - // exclude package temp - for _, path := range matches[1:] { - tempetureReadWaiter.Add(1) - go ReadTempeture(path, queue, errorChan, tempetureReadWaiter) - } - tempetureReadWaiter.Wait() - close(queue) - var tempeture float64 - for sense := range queue { - if tempeture < sense { - tempeture = sense - } - } - notifier <- TempetureChange{ - Id: info.Id, - Tempeture: tempeture, - } - case <-ctx.Done(): - return - } - } -} -func CpuTempetureScraper(processorCount int, notifier <-chan TempetureChange, errorChan chan<- error, ctx context.Context, waiter *sync.WaitGroup) { - defer waiter.Done() - var ( - P, I, D = 1.5, 0.4, 2.0 - SetPoint = 38.0 - SampleTime = time.Second - maxNoob, minNoob = 0x64, 0x4 - WindupGuard = float64(maxNoob - minNoob) - ) - noobs := make([]int, processorCount) - controllers := make([]pid.Controller, 0, processorCount) - for i := 0; i < processorCount; i++ { - controller := pid.New(P, I, D) - controller.SetSetPoint(SetPoint) - controller.SetSampleTime(SampleTime) - controller.SetWindupGuard(WindupGuard) - controllers = append(controllers, controller) - } - - for { - select { - case change := <-notifier: - controller := controllers[change.Id] - adj_noob := int(-controller.Update(change.Tempeture)) - if adj_noob < minNoob { - adj_noob = minNoob - } else if adj_noob > maxNoob { - adj_noob = maxNoob - } - if noobs[change.Id] == adj_noob { - continue - } - noobs[change.Id] = adj_noob - fmt.Printf("cpu %d fan 0x%x\n", change.Id, adj_noob) - - args := make([]string, 0) - args = append(args, - "raw", - "0x3a", "0x01", - ) - for _, item := range noobs { - args = append(args, fmt.Sprintf("0x%x", item)) - } - args = append(args, - "0x0", "0x0", "0x0", "0x0", "0x0", "0x0", - ) - cmd := exec.Command("ipmitool", args...) - if err := cmd.Run(); err != nil { - errorChan <- err - return - } - case <-ctx.Done(): - return - } + // Check if the setting was successful. + actualNumProcs := runtime.GOMAXPROCS(0) + if actualNumProcs != numProcs { + log.Printf("Specified max procs of %v but using %v\n", numProcs, actualNumProcs) } } func main() { var ( - processorCount = getProcessorCount() - ctx, canceled = context.WithCancel(context.Background()) - waiter = &sync.WaitGroup{} - exitSignal = make(chan os.Signal, 1) - errorChan = make(chan error, 1) - tempetureChange = make(chan TempetureChange) - sampleDuration = time.Second + processorCount = processor.GetProcessorCount() + processors []processor.Processor + exitSignal = make(chan os.Signal, 1) + handler = util.NewHandler() + sampleDuration = time.Second ) + log.Info("Cpu fan controller v0.2") if processorCount == 0 { - errorChan <- errors.New("cpu not found!") + handler.NotifyError(errors.New("cpu not found!")) } + + processors = make([]processor.Processor, 0, processorCount) for i := 0; i < processorCount; i++ { - if info, err := getProcessorInfo(i); err != nil { - errorChan <- err + if info, err := processor.NewProcessorInfo(handler, i, sampleDuration, + 1.5, 0.4, 2.0, 38.0, 0x64, 0x4, + ); + err != nil { + handler.NotifyError(err) } else { - waiter.Add(1) - go CpuTempetureMonitoring(info, sampleDuration, tempetureChange, errorChan, ctx, waiter) + processors = append(processors, info) + handler.IncreaseWait() + go info.StartMonitoring() } } - waiter.Add(1) - go CpuTempetureScraper(processorCount, tempetureChange, errorChan, ctx, waiter) - defer waiter.Wait() + + handler.IncreaseWait() + go TempetureControl(processors, tempetureChange, errorChan, ctx, waiter) + signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - NotifyDaemon(DaemonStarted) - defer NotifyDaemon(DaemonStopping) + daemon.NotifyDaemon(daemon.DaemonStarted) + defer daemon.NotifyDaemon(daemon.DaemonStopping) + defer close(exitSignal) + defer handler.GracefullWait() select { - case <-ctx.Done(): - fmt.Println("Service request to close this application") - case err := <-errorChan: - canceled() - fmt.Printf("error! %s\n", err.Error()) + case <-handler.Done(): + log.Infoln("Service request to close this application") + case err := <-handler.Error(): + log.Errorf("%s", err) case sysSignal := <-exitSignal: - canceled() - fmt.Printf("SYSCALL! %s\n", sysSignal.String()) + log.Warnf("SYSCALL! %s", sysSignal.String()) } } diff --git a/processor/processor.go b/processor/processor.go new file mode 100644 index 0000000..e963846 --- /dev/null +++ b/processor/processor.go @@ -0,0 +1,193 @@ +package processor + +import ( + "github.com/shirou/gopsutil/cpu" + "strconv" + "path/filepath" + "fmt" + "errors" + "time" + "sync" + "path" + "amuz.es/src/infra/cpu_ctrl/logger" + "io/ioutil" + "strings" + "amuz.es/src/infra/cpu_ctrl/pid" + "amuz.es/src/infra/cpu_ctrl/util" +) + +type processor struct { + handler util.Handler + id int + tempeturePath string + tempeture float64 + tempetureChanged chan float64 + sampleDuration time.Duration + fanController pid.Controller + fanSpeed int + fanMaxSpeed int + fanMinSpeed int + fanSpeedChanged chan int +} +type Processor interface { + Id() int + Tempeture() float64 + FanSpeed() int + FanMaxSpeed() int + FanMinSpeed() int + StartMonitoring() + TempetureNotifier() <-chan float64 + FanSpeedNotifier() <-chan int + getMaxTempetureOnProcessor(string) (float64) + readTempeture(string, chan<- float64, *sync.WaitGroup) + normalizeFanspeed(float64) (int) +} + +var ( + log = logger.NewLogger("processor") +) + +func GetProcessorCount() (maxcpu int) { + stat, err := cpu.Info() + if err != nil { + panic(err) + } + for _, info := range stat { + physicalId, err := strconv.Atoi(info.PhysicalID) + if err != nil { + return + } else if maxcpu < physicalId { + maxcpu = physicalId + } + } + maxcpu += 1 + return +} + +func NewProcessorInfo( + handler util.Handler, + processorId int, + sampleDuration time.Duration, + P, I, D, + setPoint float64, + maxNoob, minNoob int, +) (Processor, error) { + + if matches, err := filepath.Glob(fmt.Sprintf("/sys/devices/platform/coretemp.%d/hwmon/hwmon?", processorId)); err != nil { + return nil, err + } else if matches == nil { + return nil, errors.New("hwmon not found!") + } else { + windupGuard := float64(maxNoob - minNoob) + + controller := pid.New(P, I, D) + controller.SetSetPoint(setPoint) + controller.SetSampleTime(sampleDuration) + controller.SetWindupGuard(windupGuard) + + return &processor{ + handler: handler, + id: processorId, + tempeturePath: matches[0], + tempetureChanged: make(chan float64), + sampleDuration: sampleDuration, + fanController: controller, + fanSpeedChanged: make(chan int), + fanMaxSpeed: maxNoob, + fanMinSpeed: minNoob, + }, nil + } +} +func (p *processor) Id() int { return p.id } +func (p *processor) Tempeture() float64 { return p.tempeture } +func (p *processor) FanSpeed() int { return p.fanSpeed } +func (p *processor) FanMaxSpeed() int { return p.fanMaxSpeed } +func (p *processor) FanMinSpeed() int { return p.fanMinSpeed } +func (p *processor) TempetureNotifier() <-chan float64 { return p.tempetureChanged } +func (p *processor) FanSpeedNotifier() <-chan int { return p.fanSpeedChanged } + +func (p *processor) StartMonitoring() { + defer p.handler.DecreaseWait() + defer func() { + if err := recover(); err != nil { + p.handler.NotifyError(err.(error)) + } + }() + + defer close(p.tempetureChanged) + defer close(p.fanSpeedChanged) + + tempeturePathGlob := path.Join(p.tempeturePath, "temp?_input") + ticker := time.Tick(p.sampleDuration) + log.Infof("Processor %d monitor started with %s", p.id, p.sampleDuration) + + for { + select { + case <-ticker: + var ( + highestTemp float64 + fanspeed int + ) + + highestTemp = p.getMaxTempetureOnProcessor(tempeturePathGlob) + if highestTemp != p.tempeture { + p.tempetureChanged <- highestTemp + p.tempeture = highestTemp + } + + fanspeed = p.normalizeFanspeed(p.fanController.Update(highestTemp)) + if fanspeed != p.fanSpeed { + p.fanSpeedChanged <- fanspeed + p.fanSpeed = fanspeed + } + + case <-p.handler.Done(): + return + } + } +} + +func (p *processor) getMaxTempetureOnProcessor(tempeturePathGlob string) (maxTempeture float64) { + matches, err := filepath.Glob(tempeturePathGlob) + if err != nil { + panic(err) + } + + tempetureReadWaiter := &sync.WaitGroup{} + queue := make(chan float64, len(matches)) + defer close(queue) + + // exclude package temp + for _, tempetureFilePath := range matches[1:] { + tempetureReadWaiter.Add(1) + go p.readTempeture(tempetureFilePath, queue, tempetureReadWaiter) + } + tempetureReadWaiter.Wait() + for sense := range queue { + if maxTempeture < sense { + maxTempeture = sense + } + } + return +} + +func (p *processor) readTempeture(path string, senseChan chan<- float64, waiter *sync.WaitGroup) { + defer waiter.Done() + if dat, err := ioutil.ReadFile(path); err != nil { + p.handler.NotifyError(err) + } else if tempetureSense, err := strconv.Atoi(strings.TrimSpace(string(dat))); err != nil { + p.handler.NotifyError(err) + } else { + senseChan <- float64(tempetureSense) / 1000.0 + } +} + +func (p *processor) normalizeFanspeed(response float64) (adjusted int) { + adjusted = int(-response) + if adjusted < p.fanMinSpeed { + adjusted = p.fanMinSpeed + } else if adjusted > p.fanMaxSpeed { + adjusted = p.fanMaxSpeed + } + return +} diff --git a/speed_ctrl/speed_controller.go b/speed_ctrl/speed_controller.go new file mode 100644 index 0000000..3664bd6 --- /dev/null +++ b/speed_ctrl/speed_controller.go @@ -0,0 +1,85 @@ +package speed_ctrl + +import ( + "errors" + "os" + "os/signal" + "runtime" + "syscall" + "time" + + "amuz.es/src/infra/cpu_ctrl/daemon" + "amuz.es/src/infra/cpu_ctrl/logger" + "amuz.es/src/infra/cpu_ctrl/processor" + "fmt" + "os/exec" + "amuz.es/src/infra/cpu_ctrl/pid" + "amuz.es/src/infra/cpu_ctrl/util" +) + +var ( + log = logger.NewLogger("speed_ctrl") +) + + +func TempetureControl( + handler util.Handler, + processors []processor.Processor, +) { + defer handler.DecreaseWait() + var ( + P, I, D = 1.5, 0.4, 2.0 + SetPoint = 38.0 + SampleTime = time.Second + maxNoob, minNoob = 0x64, 0x4 + WindupGuard = float64(maxNoob - minNoob) + ) + + noobs := make([]int, processorCount) + + controllers := make([]pid.Controller, 0, processorCount) + for i := 0; i < processorCount; i++ { + controller := pid.New(P, I, D) + controller.SetSetPoint(SetPoint) + controller.SetSampleTime(SampleTime) + controller.SetWindupGuard(WindupGuard) + controllers = append(controllers, controller) + } + + for { + select { + case change := <-notifier: + controller := controllers[change.Id()] + adj_noob := int(-controller.Update(change.Tempeture())) + if adj_noob < minNoob { + adj_noob = minNoob + } else if adj_noob > maxNoob { + adj_noob = maxNoob + } + if noobs[change.Id()] == adj_noob { + continue + } + noobs[change.Id()] = adj_noob + log.Printf("Processor %d fan 0x%x\n", change.Id, adj_noob) + + args := make([]string, 0) + args = append(args, + "raw", + "0x3a", "0x01", + ) + for _, item := range noobs { + args = append(args, fmt.Sprintf("0x%x", item)) + } + args = append(args, + "0x0", "0x0", "0x0", "0x0", "0x0", "0x0", + ) + cmd := exec.Command("ipmitool", args...) + if err := cmd.Run(); err != nil { + errorChan <- err + return + } + case <-ctx.Done(): + return + } + } +} diff --git a/util/handler.go b/util/handler.go new file mode 100644 index 0000000..76c18c2 --- /dev/null +++ b/util/handler.go @@ -0,0 +1,54 @@ +package util + +import ( + "sync" + "context" + "amuz.es/src/infra/cpu_ctrl/logger" +) + +var ( + log = logger.NewLogger("util") +) + +type handler struct { + errorChan chan error + ctx context.Context + canceler context.CancelFunc + waiter *sync.WaitGroup +} + +type Handler interface { + NotifyError(err error) + Error() <-chan error + Done() <-chan struct{} + GracefullWait() + IncreaseWait() + DecreaseWait() +} + +func NewHandler() Handler { + ctx, canceler := context.WithCancel(context.Background()) + return &handler{ + ctx: ctx, + canceler: canceler, + waiter: &sync.WaitGroup{}, + errorChan: make(chan error), + } +} + +func (h *handler) NotifyError(err error) { h.errorChan <- err } +func (h *handler) Error() <-chan error { return h.errorChan } +func (h *handler) Done() <-chan struct{} { return h.ctx.Done() } +func (h *handler) GracefullWait() { + if h.ctx.Err() == nil { + h.canceler() + } + h.waiter.Wait() + close(h.errorChan) + + for remainError := range h.errorChan { + log.Errorf("%s", remainError) + } +} +func (h *handler) IncreaseWait() { h.waiter.Add(1) } +func (h *handler) DecreaseWait() { h.waiter.Done() }