firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/sensors/lmsensors/scanner.go

Summary

Maintainability
C
7 hrs
Test Coverage
package lmsensors

import (
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
    "time"

    "github.com/netdata/netdata/go/plugins/logger"
)

// A filesystem is an interface to a filesystem, used for testing.
type filesystem interface {
    ReadFile(filename string) (string, error)
    Readlink(name string) (string, error)
    Stat(name string) (os.FileInfo, error)
    WalkDir(root string, walkFn fs.WalkDirFunc) error
}

// A Scanner scans for Devices, so data can be read from their Sensors.
type Scanner struct {
    *logger.Logger

    fs filesystem
}

// New creates a new Scanner.
func New() *Scanner {
    return &Scanner{
        fs: &systemFilesystem{},
    }
}

// Scan scans for Devices and their Sensors.
func (sc *Scanner) Scan() ([]*Device, error) {
    paths, err := sc.detectDevicePaths()
    if err != nil {
        return nil, err
    }

    sc.Debugf("sysfs scanner: found %d paths", len(paths))

    var devices []*Device

    for _, rootPath := range paths {
        sc.Debugf("sysfs scanner: scanning %s", rootPath)

        dev := &Device{}
        raw := make(map[string]map[string]string)

        // Walk filesystem paths to fetch devices and sensors
        err := sc.fs.WalkDir(rootPath, func(path string, de fs.DirEntry, err error) error {
            if err != nil {
                return err
            }

            if de.IsDir() || !de.Type().IsRegular() {
                if de.IsDir() && path != rootPath {
                    return fs.SkipDir
                }
                return nil
            }

            // Skip some files that can't be read or don't provide useful sensor information
            file := filepath.Base(path)
            if shouldSkip(file) {
                return nil
            }

            now := time.Now()
            s, err := sc.fs.ReadFile(path)
            if err != nil {
                return nil
            }
            sc.Debugf("sysfs scanner: reading file '%s' took %s", path, time.Since(now))

            if file == "name" {
                dev.Name = s
                return nil
            }

            // Sensor names in format "sensor#_foo", e.g. "temp1_input"
            parts := strings.SplitN(file, "_", 2)
            if len(parts) != 2 {
                return nil
            }

            if _, ok := raw[parts[0]]; !ok {
                raw[parts[0]] = make(map[string]string)
            }

            raw[parts[0]][parts[1]] = s

            return nil
        })
        if err != nil {
            return nil, err
        }

        sensors, err := parseSensors(raw)
        if err != nil {
            return nil, err
        }

        for _, sn := range sensors {
            sc.Debugf("sysfs scanner: found sensor %+v", sn)
        }

        dev.Sensors = sensors
        devices = append(devices, dev)
    }

    renameDevices(devices)

    return devices, nil
}

// renameDevices renames devices in place to prevent duplicate device names, and to number each device.
func renameDevices(devices []*Device) {
    nameCount := make(map[string]int)

    for i := range devices {
        name := devices[i].Name
        devices[i].Name = fmt.Sprintf("%s-%02d",
            name,
            nameCount[name],
        )
        nameCount[name]++
    }
}

// detectDevicePaths performs a filesystem walk to paths where devices may reside on Linux.
func (sc *Scanner) detectDevicePaths() ([]string, error) {
    const lookPath = "/sys/class/hwmon"

    var paths []string
    err := sc.fs.WalkDir(lookPath, func(path string, de os.DirEntry, err error) error {
        if err != nil {
            return err
        }

        if de.Type()&os.ModeSymlink == 0 {
            return nil
        }

        dest, err := sc.fs.Readlink(path)
        if err != nil {
            return err
        }

        dest = filepath.Join(lookPath, filepath.Clean(dest))

        // Symlink destination has a file called name, meaning a sensor exists here and data can be retrieved
        fi, err := sc.fs.Stat(filepath.Join(dest, "name"))
        if err != nil && !os.IsNotExist(err) {
            return err
        }
        if err == nil && fi.Mode().IsRegular() {
            paths = append(paths, dest)
            return nil
        }

        // Symlink destination has another symlink called device, which can be read and used to retrieve data
        device := filepath.Join(dest, "device")
        fi, err = sc.fs.Stat(device)
        if err != nil {
            if !os.IsNotExist(err) {
                return err
            }
            return nil
        }

        if fi.Mode()&os.ModeSymlink != 0 {
            return nil
        }

        device, err = sc.fs.Readlink(device)
        if err != nil {
            return err
        }

        dest = filepath.Join(dest, filepath.Clean(device))

        // Symlink destination has a file called name, meaning a sensor exists here and data can be retrieved
        if _, err := sc.fs.Stat(filepath.Join(dest, "name")); err != nil {
            if !os.IsNotExist(err) {
                return err
            }
            return nil
        }

        paths = append(paths, dest)

        return nil
    })

    return paths, err
}

// shouldSkip indicates if a given filename should be skipped during the filesystem walk operation.
func shouldSkip(file string) bool {
    if strings.HasPrefix(file, "runtime_") {
        return true
    }

    switch file {
    case "async":
    case "autosuspend_delay_ms":
    case "control":
    case "driver_override":
    case "modalias":
    case "uevent":
    default:
        return false
    }

    return true
}

var _ filesystem = &systemFilesystem{}

// A systemFilesystem is a filesystem which uses operations on the host filesystem.
type systemFilesystem struct{}

func (fs *systemFilesystem) ReadFile(filename string) (string, error) {
    b, err := os.ReadFile(filename)
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(b)), nil
}

func (fs *systemFilesystem) Readlink(name string) (string, error) {
    return os.Readlink(name)
}

func (fs *systemFilesystem) Stat(name string) (os.FileInfo, error) {
    return os.Stat(name)
}

func (fs *systemFilesystem) WalkDir(root string, walkFn fs.WalkDirFunc) error {
    return filepath.WalkDir(root, walkFn)
}