// SPDX-License-Identifier: BSD-3-Clause //go:build linux package host import ( "bytes" "context" "encoding/binary" "fmt" "io" "os" "regexp" "strings" "golang.org/x/sys/unix" "github.com/shirou/gopsutil/v4/internal/common" ) type lsbStruct struct { ID string Release string Codename string Description string } // from utmp.h const ( user_PROCESS = 7 ) func HostIDWithContext(ctx context.Context) (string, error) { sysProductUUID := common.HostSysWithContext(ctx, "class/dmi/id/product_uuid") machineID := common.HostEtcWithContext(ctx, "machine-id") procSysKernelRandomBootID := common.HostProcWithContext(ctx, "sys/kernel/random/boot_id") switch { // In order to read this file, needs to be supported by kernel/arch and run as root // so having fallback is important case common.PathExists(sysProductUUID): lines, err := common.ReadLines(sysProductUUID) if err == nil && len(lines) > 0 && lines[0] != "" { return strings.ToLower(lines[0]), nil } fallthrough // Fallback on GNU Linux systems with systemd, readable by everyone case common.PathExists(machineID): lines, err := common.ReadLines(machineID) if err == nil && len(lines) > 0 && len(lines[0]) == 32 { st := lines[0] return fmt.Sprintf("%s-%s-%s-%s-%s", st[0:8], st[8:12], st[12:16], st[16:20], st[20:32]), nil } fallthrough // Not stable between reboot, but better than nothing default: lines, err := common.ReadLines(procSysKernelRandomBootID) if err == nil && len(lines) > 0 && lines[0] != "" { return strings.ToLower(lines[0]), nil } } return "", nil } func numProcs(ctx context.Context) (uint64, error) { return common.NumProcsWithContext(ctx) } func BootTimeWithContext(ctx context.Context) (uint64, error) { return common.BootTimeWithContext(ctx, enableBootTimeCache) } func UptimeWithContext(ctx context.Context) (uint64, error) { sysinfo := &unix.Sysinfo_t{} if err := unix.Sysinfo(sysinfo); err != nil { return 0, err } return uint64(sysinfo.Uptime), nil } func UsersWithContext(ctx context.Context) ([]UserStat, error) { utmpfile := common.HostVarWithContext(ctx, "run/utmp") file, err := os.Open(utmpfile) if err != nil { return nil, err } defer file.Close() buf, err := io.ReadAll(file) if err != nil { return nil, err } count := len(buf) / sizeOfUtmp ret := make([]UserStat, 0, count) for i := 0; i < count; i++ { b := buf[i*sizeOfUtmp : (i+1)*sizeOfUtmp] var u utmp br := bytes.NewReader(b) err := binary.Read(br, binary.LittleEndian, &u) if err != nil { continue } if u.Type != user_PROCESS { continue } user := UserStat{ User: common.IntToString(u.User[:]), Terminal: common.IntToString(u.Line[:]), Host: common.IntToString(u.Host[:]), Started: int(u.Tv.Sec), } ret = append(ret, user) } return ret, nil } func getlsbStruct(ctx context.Context) (*lsbStruct, error) { ret := &lsbStruct{} if common.PathExists(common.HostEtcWithContext(ctx, "lsb-release")) { contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "lsb-release")) if err != nil { return ret, err // return empty } for _, line := range contents { field := strings.Split(line, "=") if len(field) < 2 { continue } switch field[0] { case "DISTRIB_ID": ret.ID = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_RELEASE": ret.Release = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_CODENAME": ret.Codename = strings.ReplaceAll(field[1], `"`, ``) case "DISTRIB_DESCRIPTION": ret.Description = strings.ReplaceAll(field[1], `"`, ``) } } } else if common.PathExists("/usr/bin/lsb_release") { out, err := invoke.Command("/usr/bin/lsb_release") if err != nil { return ret, err } for _, line := range strings.Split(string(out), "\n") { field := strings.Split(line, ":") if len(field) < 2 { continue } switch field[0] { case "Distributor ID": ret.ID = strings.ReplaceAll(field[1], `"`, ``) case "Release": ret.Release = strings.ReplaceAll(field[1], `"`, ``) case "Codename": ret.Codename = strings.ReplaceAll(field[1], `"`, ``) case "Description": ret.Description = strings.ReplaceAll(field[1], `"`, ``) } } } return ret, nil } func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) { lsb, err := getlsbStruct(ctx) if err != nil { lsb = &lsbStruct{} } if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "oracle-release")) { platform = "oracle" contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "oracle-release")) if err == nil { version = getRedhatishVersion(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "enterprise-release")) { platform = "oracle" contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "enterprise-release")) if err == nil { version = getRedhatishVersion(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "slackware-version")) { platform = "slackware" contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "slackware-version")) if err == nil { version = getSlackwareVersion(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "debian_version")) { if lsb.ID == "Ubuntu" { platform = "ubuntu" version = lsb.Release } else if lsb.ID == "LinuxMint" { platform = "linuxmint" version = lsb.Release } else if lsb.ID == "Kylin" { platform = "Kylin" version = lsb.Release } else if lsb.ID == `"Cumulus Linux"` { platform = "cumuluslinux" version = lsb.Release } else if lsb.ID == "uos" { platform = "uos" version = lsb.Release } else if lsb.ID == "Deepin" { platform = "Deepin" version = lsb.Release } else { if common.PathExistsWithContents("/usr/bin/raspi-config") { platform = "raspbian" } else { platform = "debian" } contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "debian_version")) if err == nil && len(contents) > 0 && contents[0] != "" { version = contents[0] } } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "neokylin-release")) { contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "neokylin-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "redhat-release")) { contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "redhat-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "system-release")) { contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "system-release")) if err == nil { version = getRedhatishVersion(contents) platform = getRedhatishPlatform(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "gentoo-release")) { platform = "gentoo" contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "gentoo-release")) if err == nil { version = getRedhatishVersion(contents) } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "SuSE-release")) { contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "SuSE-release")) if err == nil { version = getSuseVersion(contents) platform = getSusePlatform(contents) } // TODO: slackware detecion } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "arch-release")) { platform = "arch" version = lsb.Release } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "alpine-release")) { platform = "alpine" contents, err := common.ReadLines(common.HostEtcWithContext(ctx, "alpine-release")) if err == nil && len(contents) > 0 && contents[0] != "" { version = contents[0] } } else if common.PathExistsWithContents(common.HostEtcWithContext(ctx, "os-release")) { p, v, err := common.GetOSReleaseWithContext(ctx) if err == nil { platform = p version = v } } else if lsb.ID == "RedHat" { platform = "redhat" version = lsb.Release } else if lsb.ID == "Amazon" { platform = "amazon" version = lsb.Release } else if lsb.ID == "ScientificSL" { platform = "scientific" version = lsb.Release } else if lsb.ID == "XenServer" { platform = "xenserver" version = lsb.Release } else if lsb.ID != "" { platform = strings.ToLower(lsb.ID) version = lsb.Release } platform = strings.Trim(platform, `"`) switch platform { case "debian", "ubuntu", "linuxmint", "raspbian", "Kylin", "cumuluslinux", "uos", "Deepin": family = "debian" case "fedora": family = "fedora" case "oracle", "centos", "redhat", "scientific", "enterpriseenterprise", "amazon", "xenserver", "cloudlinux", "ibm_powerkvm", "rocky", "almalinux": family = "rhel" case "suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed", "opensuse-tumbleweed-kubic", "sles", "sled", "caasp": family = "suse" case "gentoo": family = "gentoo" case "slackware": family = "slackware" case "arch": family = "arch" case "exherbo": family = "exherbo" case "alpine": family = "alpine" case "coreos": family = "coreos" case "solus": family = "solus" case "neokylin": family = "neokylin" case "anolis": family = "anolis" } return platform, family, version, nil } func KernelVersionWithContext(ctx context.Context) (version string, err error) { var utsname unix.Utsname err = unix.Uname(&utsname) if err != nil { return "", err } return unix.ByteSliceToString(utsname.Release[:]), nil } func getSlackwareVersion(contents []string) string { c := strings.ToLower(strings.Join(contents, "")) c = strings.Replace(c, "slackware ", "", 1) return c } var redhatishReleaseMatch = regexp.MustCompile(`release (\w[\d.]*)`) func getRedhatishVersion(contents []string) string { c := strings.ToLower(strings.Join(contents, "")) if strings.Contains(c, "rawhide") { return "rawhide" } if matches := redhatishReleaseMatch.FindStringSubmatch(c); matches != nil { return matches[1] } return "" } func getRedhatishPlatform(contents []string) string { c := strings.ToLower(strings.Join(contents, "")) if strings.Contains(c, "red hat") { return "redhat" } f := strings.Split(c, " ") return f[0] } var ( suseVersionMatch = regexp.MustCompile(`VERSION = ([\d.]+)`) susePatchLevelMatch = regexp.MustCompile(`PATCHLEVEL = (\d+)`) ) func getSuseVersion(contents []string) string { version := "" for _, line := range contents { if matches := suseVersionMatch.FindStringSubmatch(line); matches != nil { version = matches[1] } else if matches = susePatchLevelMatch.FindStringSubmatch(line); matches != nil { version = version + "." + matches[1] } } return version } func getSusePlatform(contents []string) string { c := strings.ToLower(strings.Join(contents, "")) if strings.Contains(c, "opensuse") { return "opensuse" } return "suse" } func VirtualizationWithContext(ctx context.Context) (string, string, error) { return common.VirtualizationWithContext(ctx) }