package main import ( "context" "errors" "fmt" "io/ioutil" "os" "os/exec" "os/signal" "path" "path/filepath" "strconv" "strings" "sync" "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 ) var ( prefix = [...]int{0x3a, 0x01} suffix = [...]int{auto, auto, auto, auto, auto, auto} ) type notifyType string const ( DaemonStarted = notifyType("READY=1") DaemonStopping = notifyType("STOPPING=1") ) func NotifyDaemon(status notifyType) { daemon.SdNotify(false, string(status)) } 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 } 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 = 37.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 } } } 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 ) if processorCount == 0 { errorChan <- errors.New("cpu not found!") } for i := 0; i < processorCount; i++ { if info, err := getProcessorInfo(i); err != nil { errorChan <- err } else { waiter.Add(1) go CpuTempetureMonitoring(info, sampleDuration, tempetureChange, errorChan, ctx, waiter) } } waiter.Add(1) go CpuTempetureScraper(processorCount, tempetureChange, errorChan, ctx, waiter) defer waiter.Wait() signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) NotifyDaemon(DaemonStarted) defer NotifyDaemon(DaemonStopping) select { case <-ctx.Done(): fmt.Println("Service request to close this application") case err := <-errorChan: canceled() fmt.Printf("error! %s\n", err.Error()) case sysSignal := <-exitSignal: canceled() fmt.Printf("SYSCALL! %s\n", sysSignal.String()) } }