|
|
|
|
| package agent
|
|
|
| import (
|
| "os"
|
| "path/filepath"
|
| "strconv"
|
| "strings"
|
|
|
| "github.com/henrygd/beszel/agent/utils"
|
| "github.com/henrygd/beszel/internal/entities/smart"
|
| )
|
|
|
|
|
| var emmcSysfsRoot = "/sys"
|
|
|
| type emmcHealth struct {
|
| model string
|
| serial string
|
| revision string
|
| capacity uint64
|
| preEOL uint8
|
| lifeA uint8
|
| lifeB uint8
|
| }
|
|
|
| func scanEmmcDevices() []*DeviceInfo {
|
| blockDir := filepath.Join(emmcSysfsRoot, "class", "block")
|
| entries, err := os.ReadDir(blockDir)
|
| if err != nil {
|
| return nil
|
| }
|
|
|
| devices := make([]*DeviceInfo, 0, 2)
|
| for _, ent := range entries {
|
| name := ent.Name()
|
| if !isEmmcBlockName(name) {
|
| continue
|
| }
|
|
|
| deviceDir := filepath.Join(blockDir, name, "device")
|
| if !hasEmmcHealthFiles(deviceDir) {
|
| continue
|
| }
|
|
|
| devPath := filepath.Join("/dev", name)
|
| devices = append(devices, &DeviceInfo{
|
| Name: devPath,
|
| Type: "emmc",
|
| InfoName: devPath + " [eMMC]",
|
| Protocol: "MMC",
|
| })
|
| }
|
|
|
| return devices
|
| }
|
|
|
| func (sm *SmartManager) collectEmmcHealth(deviceInfo *DeviceInfo) (bool, error) {
|
| if deviceInfo == nil || deviceInfo.Name == "" {
|
| return false, nil
|
| }
|
|
|
| base := filepath.Base(deviceInfo.Name)
|
| if !isEmmcBlockName(base) && !strings.EqualFold(deviceInfo.Type, "emmc") && !strings.EqualFold(deviceInfo.Type, "mmc") {
|
| return false, nil
|
| }
|
|
|
| health, ok := readEmmcHealth(base)
|
| if !ok {
|
| return false, nil
|
| }
|
|
|
|
|
| deviceInfo.Type = "emmc"
|
|
|
| key := health.serial
|
| if key == "" {
|
| key = filepath.Join("/dev", base)
|
| }
|
|
|
| status := emmcSmartStatus(health.preEOL)
|
|
|
| attrs := []*smart.SmartAttribute{
|
| {
|
| Name: "PreEOLInfo",
|
| RawValue: uint64(health.preEOL),
|
| RawString: emmcPreEOLString(health.preEOL),
|
| },
|
| {
|
| Name: "DeviceLifeTimeEstA",
|
| RawValue: uint64(health.lifeA),
|
| RawString: emmcLifeTimeString(health.lifeA),
|
| },
|
| {
|
| Name: "DeviceLifeTimeEstB",
|
| RawValue: uint64(health.lifeB),
|
| RawString: emmcLifeTimeString(health.lifeB),
|
| },
|
| }
|
|
|
| sm.Lock()
|
| defer sm.Unlock()
|
|
|
| if _, exists := sm.SmartDataMap[key]; !exists {
|
| sm.SmartDataMap[key] = &smart.SmartData{}
|
| }
|
|
|
| data := sm.SmartDataMap[key]
|
| data.ModelName = health.model
|
| data.SerialNumber = health.serial
|
| data.FirmwareVersion = health.revision
|
| data.Capacity = health.capacity
|
| data.Temperature = 0
|
| data.SmartStatus = status
|
| data.DiskName = filepath.Join("/dev", base)
|
| data.DiskType = "emmc"
|
| data.Attributes = attrs
|
|
|
| return true, nil
|
| }
|
|
|
| func readEmmcHealth(blockName string) (emmcHealth, bool) {
|
| var out emmcHealth
|
|
|
| if !isEmmcBlockName(blockName) {
|
| return out, false
|
| }
|
|
|
| deviceDir := filepath.Join(emmcSysfsRoot, "class", "block", blockName, "device")
|
| preEOL, okPre := readHexByteFile(filepath.Join(deviceDir, "pre_eol_info"))
|
|
|
|
|
|
|
| lifeA, lifeB, okLife := readLifeTime(deviceDir)
|
|
|
| if !okPre && !okLife {
|
| return out, false
|
| }
|
|
|
| out.preEOL = preEOL
|
| out.lifeA = lifeA
|
| out.lifeB = lifeB
|
|
|
| out.model = utils.ReadStringFile(filepath.Join(deviceDir, "name"))
|
| out.serial = utils.ReadStringFile(filepath.Join(deviceDir, "serial"))
|
| out.revision = utils.ReadStringFile(filepath.Join(deviceDir, "prv"))
|
|
|
| if capBytes, ok := readBlockCapacityBytes(blockName); ok {
|
| out.capacity = capBytes
|
| }
|
|
|
| return out, true
|
| }
|
|
|
| func readLifeTime(deviceDir string) (uint8, uint8, bool) {
|
| if content, ok := utils.ReadStringFileOK(filepath.Join(deviceDir, "life_time")); ok {
|
| a, b, ok := parseHexBytePair(content)
|
| return a, b, ok
|
| }
|
|
|
| a, okA := readHexByteFile(filepath.Join(deviceDir, "device_life_time_est_typ_a"))
|
| b, okB := readHexByteFile(filepath.Join(deviceDir, "device_life_time_est_typ_b"))
|
| if okA || okB {
|
| return a, b, true
|
| }
|
| return 0, 0, false
|
| }
|
|
|
| func readBlockCapacityBytes(blockName string) (uint64, bool) {
|
| sizePath := filepath.Join(emmcSysfsRoot, "class", "block", blockName, "size")
|
| lbsPath := filepath.Join(emmcSysfsRoot, "class", "block", blockName, "queue", "logical_block_size")
|
|
|
| sizeStr, ok := utils.ReadStringFileOK(sizePath)
|
| if !ok {
|
| return 0, false
|
| }
|
| sectors, err := strconv.ParseUint(sizeStr, 10, 64)
|
| if err != nil || sectors == 0 {
|
| return 0, false
|
| }
|
|
|
| lbsStr, ok := utils.ReadStringFileOK(lbsPath)
|
| logicalBlockSize := uint64(512)
|
| if ok {
|
| if parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {
|
| logicalBlockSize = parsed
|
| }
|
| }
|
|
|
| return sectors * logicalBlockSize, true
|
| }
|
|
|
| func readHexByteFile(path string) (uint8, bool) {
|
| content, ok := utils.ReadStringFileOK(path)
|
| if !ok {
|
| return 0, false
|
| }
|
| b, ok := parseHexOrDecByte(content)
|
| return b, ok
|
| }
|
|
|
| func hasEmmcHealthFiles(deviceDir string) bool {
|
| entries, err := os.ReadDir(deviceDir)
|
| if err != nil {
|
| return false
|
| }
|
| for _, ent := range entries {
|
| switch ent.Name() {
|
| case "pre_eol_info", "life_time", "device_life_time_est_typ_a", "device_life_time_est_typ_b":
|
| return true
|
| }
|
| }
|
| return false
|
| }
|
|
|