182 lines
5.0 KiB
Go
182 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type cpuHandler struct {
|
|
corePaths []string
|
|
maxFreq int
|
|
minFreq int
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// readIntFromFile reads the given file until the end
|
|
// and converts the contained string into an integer
|
|
func readIntFromFile(path string) (int, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return strconv.Atoi(strings.TrimSpace(string(b)))
|
|
}
|
|
|
|
func enumerateCPUCores(logger *slog.Logger) ([]string, int, int, error) {
|
|
files, err := os.ReadDir("/sys/devices/system/cpu/")
|
|
if err != nil {
|
|
logger.Error("reading /sys/devices/system/cpu/", "err", err)
|
|
return nil, -1, -1, err
|
|
}
|
|
r, _ := regexp.Compile("cpu([0-9]+)")
|
|
|
|
freqPaths := make([]string, 0)
|
|
maxF := math.MaxInt
|
|
minF := math.MinInt
|
|
|
|
// we look for cpuN directories
|
|
for _, f := range files {
|
|
if !f.IsDir() || !r.MatchString(f.Name()) {
|
|
continue
|
|
}
|
|
|
|
// read files for min & max freqs
|
|
dir := "/sys/devices/system/cpu/" + f.Name() + "/cpufreq/"
|
|
minFreq, err := readIntFromFile(dir + "cpuinfo_min_freq")
|
|
if err != nil {
|
|
logger.Error("failed to read cpu min freq", "path", dir+"cpuinfo_min_freq", "error", err)
|
|
continue
|
|
}
|
|
maxFreq, err := readIntFromFile(dir + "cpuinfo_max_freq")
|
|
if err != nil {
|
|
logger.Error("failed to read cpu max freq", "path", dir+"cpuinfo_min_freq", "error", err)
|
|
continue
|
|
}
|
|
freqPaths = append(freqPaths, dir+"scaling_max_freq")
|
|
maxF = min(maxF, maxFreq)
|
|
minF = max(minF, minFreq)
|
|
}
|
|
return freqPaths, minF, maxF, nil
|
|
}
|
|
|
|
func (s *cpuHandler) handleMin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
writeResponse("invalid method", http.StatusBadRequest, w, s.logger)
|
|
return
|
|
}
|
|
writeResponse(strconv.Itoa(s.minFreq), http.StatusOK, w, s.logger)
|
|
}
|
|
|
|
func (s *cpuHandler) handleMax(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
writeResponse("invalid method", http.StatusBadRequest, w, s.logger)
|
|
return
|
|
}
|
|
writeResponse(strconv.Itoa(s.maxFreq), http.StatusOK, w, s.logger)
|
|
}
|
|
|
|
func (s *cpuHandler) handleCurrent(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case "GET":
|
|
maxFreq := math.MinInt
|
|
for _, core := range s.corePaths {
|
|
freq, err := readIntFromFile(core)
|
|
if err != nil {
|
|
s.logger.Error("error while reading core frequency", "core", core, "error", err)
|
|
writeResponse("internal server error", http.StatusInternalServerError, w, s.logger)
|
|
return
|
|
}
|
|
s.logger.Debug("core frequency", "core", core, "frequency", freq)
|
|
maxFreq = max(maxFreq, freq)
|
|
}
|
|
s.logger.Debug("reported frequency", "freq", maxFreq)
|
|
writeResponse(strconv.Itoa(maxFreq), http.StatusOK, w, s.logger)
|
|
case "PUT":
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
s.logger.Error("failed to read the request body", "err", err)
|
|
writeResponse("internal server error", http.StatusInternalServerError, w, s.logger)
|
|
}
|
|
bodyStr := strings.TrimSpace(string(body))
|
|
// Check if it is an integer
|
|
freqInt, err := strconv.Atoi(bodyStr)
|
|
if err != nil {
|
|
s.logger.Error("failed to convert body to int", "error", err)
|
|
writeResponse("bad request", http.StatusBadRequest, w, s.logger)
|
|
}
|
|
if freqInt < s.minFreq || freqInt > s.maxFreq {
|
|
s.logger.Warn("requested frequency is out-of-bounds", "freq", freqInt, "min", s.minFreq, "max", s.maxFreq)
|
|
freqInt = max(s.minFreq, min(freqInt, s.maxFreq))
|
|
bodyStr = strconv.Itoa(freqInt)
|
|
}
|
|
s.logger.Info("requested to set current frequency", "frequency", bodyStr)
|
|
success := true
|
|
for _, core := range s.corePaths {
|
|
err = os.WriteFile(core, []byte(bodyStr), 0)
|
|
if err != nil {
|
|
s.logger.Error("failed to set core frequency", "core", core, "error", err)
|
|
success = false
|
|
}
|
|
}
|
|
if success {
|
|
w.WriteHeader(http.StatusOK)
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
default:
|
|
s.logger.Error("unexpected method for current cpu", "method", r.Method)
|
|
writeResponse("invalid method", http.StatusBadRequest, w, s.logger)
|
|
}
|
|
}
|
|
|
|
func (s *cpuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
requestedPath := getStrippedRequestPath(r.URL, r.Pattern)
|
|
switch requestedPath {
|
|
case "frequency/min":
|
|
s.handleMin(w, r)
|
|
return
|
|
case "frequency/max":
|
|
s.handleMax(w, r)
|
|
return
|
|
case "frequency/current":
|
|
s.handleCurrent(w, r)
|
|
return
|
|
default:
|
|
s.logger.Error("unknown resource", "path", requestedPath)
|
|
writeResponse("not found", http.StatusNotFound, w, s.logger)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
appEnv := os.Getenv("AGENT_ENV")
|
|
loggerOpts := &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
}
|
|
var handler slog.Handler = slog.NewTextHandler(os.Stdout, loggerOpts)
|
|
if appEnv == "production" {
|
|
loggerOpts.Level = slog.LevelInfo
|
|
handler = slog.NewJSONHandler(os.Stdout, loggerOpts)
|
|
}
|
|
logger := slog.New(handler)
|
|
|
|
freqPaths, minF, maxF, err := enumerateCPUCores(logger)
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
|
|
cpuHandler := &cpuHandler{
|
|
logger: logger,
|
|
maxFreq: maxF,
|
|
minFreq: minF,
|
|
corePaths: freqPaths,
|
|
}
|
|
http.Handle("/cpu/", logRequests(logger, cpuHandler))
|
|
logger.Info("exit", "result", http.ListenAndServe(":8080", nil))
|
|
}
|