1// +build linux 2 3package host 4 5import ( 6 "bytes" 7 "context" 8 "encoding/binary" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "strconv" 16 "strings" 17 18 "github.com/shirou/gopsutil/v3/internal/common" 19 "golang.org/x/sys/unix" 20) 21 22type lsbStruct struct { 23 ID string 24 Release string 25 Codename string 26 Description string 27} 28 29// from utmp.h 30const ( 31 user_PROCESS = 7 32 33 hostTemperatureScale = 1000.0 34) 35 36func HostIDWithContext(ctx context.Context) (string, error) { 37 sysProductUUID := common.HostSys("class/dmi/id/product_uuid") 38 machineID := common.HostEtc("machine-id") 39 procSysKernelRandomBootID := common.HostProc("sys/kernel/random/boot_id") 40 switch { 41 // In order to read this file, needs to be supported by kernel/arch and run as root 42 // so having fallback is important 43 case common.PathExists(sysProductUUID): 44 lines, err := common.ReadLines(sysProductUUID) 45 if err == nil && len(lines) > 0 && lines[0] != "" { 46 return strings.ToLower(lines[0]), nil 47 } 48 fallthrough 49 // Fallback on GNU Linux systems with systemd, readable by everyone 50 case common.PathExists(machineID): 51 lines, err := common.ReadLines(machineID) 52 if err == nil && len(lines) > 0 && len(lines[0]) == 32 { 53 st := lines[0] 54 return fmt.Sprintf("%s-%s-%s-%s-%s", st[0:8], st[8:12], st[12:16], st[16:20], st[20:32]), nil 55 } 56 fallthrough 57 // Not stable between reboot, but better than nothing 58 default: 59 lines, err := common.ReadLines(procSysKernelRandomBootID) 60 if err == nil && len(lines) > 0 && lines[0] != "" { 61 return strings.ToLower(lines[0]), nil 62 } 63 } 64 65 return "", nil 66} 67 68func numProcs(ctx context.Context) (uint64, error) { 69 return common.NumProcs() 70} 71 72func BootTimeWithContext(ctx context.Context) (uint64, error) { 73 return common.BootTimeWithContext(ctx) 74} 75 76func UptimeWithContext(ctx context.Context) (uint64, error) { 77 sysinfo := &unix.Sysinfo_t{} 78 if err := unix.Sysinfo(sysinfo); err != nil { 79 return 0, err 80 } 81 return uint64(sysinfo.Uptime), nil 82} 83 84func UsersWithContext(ctx context.Context) ([]UserStat, error) { 85 utmpfile := common.HostVar("run/utmp") 86 87 file, err := os.Open(utmpfile) 88 if err != nil { 89 return nil, err 90 } 91 defer file.Close() 92 93 buf, err := ioutil.ReadAll(file) 94 if err != nil { 95 return nil, err 96 } 97 98 count := len(buf) / sizeOfUtmp 99 100 ret := make([]UserStat, 0, count) 101 102 for i := 0; i < count; i++ { 103 b := buf[i*sizeOfUtmp : (i+1)*sizeOfUtmp] 104 105 var u utmp 106 br := bytes.NewReader(b) 107 err := binary.Read(br, binary.LittleEndian, &u) 108 if err != nil { 109 continue 110 } 111 if u.Type != user_PROCESS { 112 continue 113 } 114 user := UserStat{ 115 User: common.IntToString(u.User[:]), 116 Terminal: common.IntToString(u.Line[:]), 117 Host: common.IntToString(u.Host[:]), 118 Started: int(u.Tv.Sec), 119 } 120 ret = append(ret, user) 121 } 122 123 return ret, nil 124 125} 126 127func getlsbStruct() (*lsbStruct, error) { 128 ret := &lsbStruct{} 129 if common.PathExists(common.HostEtc("lsb-release")) { 130 contents, err := common.ReadLines(common.HostEtc("lsb-release")) 131 if err != nil { 132 return ret, err // return empty 133 } 134 for _, line := range contents { 135 field := strings.Split(line, "=") 136 if len(field) < 2 { 137 continue 138 } 139 switch field[0] { 140 case "DISTRIB_ID": 141 ret.ID = field[1] 142 case "DISTRIB_RELEASE": 143 ret.Release = field[1] 144 case "DISTRIB_CODENAME": 145 ret.Codename = field[1] 146 case "DISTRIB_DESCRIPTION": 147 ret.Description = field[1] 148 } 149 } 150 } else if common.PathExists("/usr/bin/lsb_release") { 151 lsb_release, err := exec.LookPath("lsb_release") 152 if err != nil { 153 return ret, err 154 } 155 out, err := invoke.Command(lsb_release) 156 if err != nil { 157 return ret, err 158 } 159 for _, line := range strings.Split(string(out), "\n") { 160 field := strings.Split(line, ":") 161 if len(field) < 2 { 162 continue 163 } 164 switch field[0] { 165 case "Distributor ID": 166 ret.ID = field[1] 167 case "Release": 168 ret.Release = field[1] 169 case "Codename": 170 ret.Codename = field[1] 171 case "Description": 172 ret.Description = field[1] 173 } 174 } 175 176 } 177 178 return ret, nil 179} 180 181func PlatformInformationWithContext(ctx context.Context) (platform string, family string, version string, err error) { 182 lsb, err := getlsbStruct() 183 if err != nil { 184 lsb = &lsbStruct{} 185 } 186 187 if common.PathExists(common.HostEtc("oracle-release")) { 188 platform = "oracle" 189 contents, err := common.ReadLines(common.HostEtc("oracle-release")) 190 if err == nil { 191 version = getRedhatishVersion(contents) 192 } 193 194 } else if common.PathExists(common.HostEtc("enterprise-release")) { 195 platform = "oracle" 196 contents, err := common.ReadLines(common.HostEtc("enterprise-release")) 197 if err == nil { 198 version = getRedhatishVersion(contents) 199 } 200 } else if common.PathExists(common.HostEtc("slackware-version")) { 201 platform = "slackware" 202 contents, err := common.ReadLines(common.HostEtc("slackware-version")) 203 if err == nil { 204 version = getSlackwareVersion(contents) 205 } 206 } else if common.PathExists(common.HostEtc("debian_version")) { 207 if lsb.ID == "Ubuntu" { 208 platform = "ubuntu" 209 version = lsb.Release 210 } else if lsb.ID == "LinuxMint" { 211 platform = "linuxmint" 212 version = lsb.Release 213 } else { 214 if common.PathExists("/usr/bin/raspi-config") { 215 platform = "raspbian" 216 } else { 217 platform = "debian" 218 } 219 contents, err := common.ReadLines(common.HostEtc("debian_version")) 220 if err == nil && len(contents) > 0 && contents[0] != "" { 221 version = contents[0] 222 } 223 } 224 } else if common.PathExists(common.HostEtc("redhat-release")) { 225 contents, err := common.ReadLines(common.HostEtc("redhat-release")) 226 if err == nil { 227 version = getRedhatishVersion(contents) 228 platform = getRedhatishPlatform(contents) 229 } 230 } else if common.PathExists(common.HostEtc("system-release")) { 231 contents, err := common.ReadLines(common.HostEtc("system-release")) 232 if err == nil { 233 version = getRedhatishVersion(contents) 234 platform = getRedhatishPlatform(contents) 235 } 236 } else if common.PathExists(common.HostEtc("gentoo-release")) { 237 platform = "gentoo" 238 contents, err := common.ReadLines(common.HostEtc("gentoo-release")) 239 if err == nil { 240 version = getRedhatishVersion(contents) 241 } 242 } else if common.PathExists(common.HostEtc("SuSE-release")) { 243 contents, err := common.ReadLines(common.HostEtc("SuSE-release")) 244 if err == nil { 245 version = getSuseVersion(contents) 246 platform = getSusePlatform(contents) 247 } 248 // TODO: slackware detecion 249 } else if common.PathExists(common.HostEtc("arch-release")) { 250 platform = "arch" 251 version = lsb.Release 252 } else if common.PathExists(common.HostEtc("alpine-release")) { 253 platform = "alpine" 254 contents, err := common.ReadLines(common.HostEtc("alpine-release")) 255 if err == nil && len(contents) > 0 && contents[0] != "" { 256 version = contents[0] 257 } 258 } else if common.PathExists(common.HostEtc("os-release")) { 259 p, v, err := common.GetOSRelease() 260 if err == nil { 261 platform = p 262 version = v 263 } 264 } else if lsb.ID == "RedHat" { 265 platform = "redhat" 266 version = lsb.Release 267 } else if lsb.ID == "Amazon" { 268 platform = "amazon" 269 version = lsb.Release 270 } else if lsb.ID == "ScientificSL" { 271 platform = "scientific" 272 version = lsb.Release 273 } else if lsb.ID == "XenServer" { 274 platform = "xenserver" 275 version = lsb.Release 276 } else if lsb.ID != "" { 277 platform = strings.ToLower(lsb.ID) 278 version = lsb.Release 279 } 280 281 switch platform { 282 case "debian", "ubuntu", "linuxmint", "raspbian": 283 family = "debian" 284 case "fedora": 285 family = "fedora" 286 case "oracle", "centos", "redhat", "scientific", "enterpriseenterprise", "amazon", "xenserver", "cloudlinux", "ibm_powerkvm", "rocky": 287 family = "rhel" 288 case "suse", "opensuse", "opensuse-leap", "opensuse-tumbleweed", "opensuse-tumbleweed-kubic", "sles", "sled", "caasp": 289 family = "suse" 290 case "gentoo": 291 family = "gentoo" 292 case "slackware": 293 family = "slackware" 294 case "arch": 295 family = "arch" 296 case "exherbo": 297 family = "exherbo" 298 case "alpine": 299 family = "alpine" 300 case "coreos": 301 family = "coreos" 302 case "solus": 303 family = "solus" 304 } 305 306 return platform, family, version, nil 307 308} 309 310func KernelVersionWithContext(ctx context.Context) (version string, err error) { 311 var utsname unix.Utsname 312 err = unix.Uname(&utsname) 313 if err != nil { 314 return "", err 315 } 316 return string(utsname.Release[:bytes.IndexByte(utsname.Release[:], 0)]), nil 317} 318 319func getSlackwareVersion(contents []string) string { 320 c := strings.ToLower(strings.Join(contents, "")) 321 c = strings.Replace(c, "slackware ", "", 1) 322 return c 323} 324 325func getRedhatishVersion(contents []string) string { 326 c := strings.ToLower(strings.Join(contents, "")) 327 328 if strings.Contains(c, "rawhide") { 329 return "rawhide" 330 } 331 if matches := regexp.MustCompile(`release (\d[\d.]*)`).FindStringSubmatch(c); matches != nil { 332 return matches[1] 333 } 334 return "" 335} 336 337func getRedhatishPlatform(contents []string) string { 338 c := strings.ToLower(strings.Join(contents, "")) 339 340 if strings.Contains(c, "red hat") { 341 return "redhat" 342 } 343 f := strings.Split(c, " ") 344 345 return f[0] 346} 347 348func getSuseVersion(contents []string) string { 349 version := "" 350 for _, line := range contents { 351 if matches := regexp.MustCompile(`VERSION = ([\d.]+)`).FindStringSubmatch(line); matches != nil { 352 version = matches[1] 353 } else if matches := regexp.MustCompile(`PATCHLEVEL = ([\d]+)`).FindStringSubmatch(line); matches != nil { 354 version = version + "." + matches[1] 355 } 356 } 357 return version 358} 359 360func getSusePlatform(contents []string) string { 361 c := strings.ToLower(strings.Join(contents, "")) 362 if strings.Contains(c, "opensuse") { 363 return "opensuse" 364 } 365 return "suse" 366} 367 368func VirtualizationWithContext(ctx context.Context) (string, string, error) { 369 return common.VirtualizationWithContext(ctx) 370} 371 372func SensorsTemperaturesWithContext(ctx context.Context) ([]TemperatureStat, error) { 373 var err error 374 375 var files []string 376 377 temperatures := make([]TemperatureStat, 0) 378 379 // Only the temp*_input file provides current temperature 380 // value in millidegree Celsius as reported by the temperature to the device: 381 // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface 382 if files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/temp*_input")); err != nil { 383 return temperatures, err 384 } 385 386 if len(files) == 0 { 387 // CentOS has an intermediate /device directory: 388 // https://github.com/giampaolo/psutil/issues/971 389 if files, err = filepath.Glob(common.HostSys("/class/hwmon/hwmon*/device/temp*_input")); err != nil { 390 return temperatures, err 391 } 392 } 393 394 var warns Warnings 395 396 if len(files) == 0 { // handle distributions without hwmon, like raspbian #391, parse legacy thermal_zone files 397 files, err = filepath.Glob(common.HostSys("/class/thermal/thermal_zone*/")) 398 if err != nil { 399 return temperatures, err 400 } 401 for _, file := range files { 402 // Get the name of the temperature you are reading 403 name, err := ioutil.ReadFile(filepath.Join(file, "type")) 404 if err != nil { 405 warns.Add(err) 406 continue 407 } 408 // Get the temperature reading 409 current, err := ioutil.ReadFile(filepath.Join(file, "temp")) 410 if err != nil { 411 warns.Add(err) 412 continue 413 } 414 temperature, err := strconv.ParseInt(strings.TrimSpace(string(current)), 10, 64) 415 if err != nil { 416 warns.Add(err) 417 continue 418 } 419 420 temperatures = append(temperatures, TemperatureStat{ 421 SensorKey: strings.TrimSpace(string(name)), 422 Temperature: float64(temperature) / 1000.0, 423 }) 424 } 425 return temperatures, warns.Reference() 426 } 427 428 temperatures = make([]TemperatureStat, 0, len(files)) 429 430 // example directory 431 // device/ temp1_crit_alarm temp2_crit_alarm temp3_crit_alarm temp4_crit_alarm temp5_crit_alarm temp6_crit_alarm temp7_crit_alarm 432 // name temp1_input temp2_input temp3_input temp4_input temp5_input temp6_input temp7_input 433 // power/ temp1_label temp2_label temp3_label temp4_label temp5_label temp6_label temp7_label 434 // subsystem/ temp1_max temp2_max temp3_max temp4_max temp5_max temp6_max temp7_max 435 // temp1_crit temp2_crit temp3_crit temp4_crit temp5_crit temp6_crit temp7_crit uevent 436 for _, file := range files { 437 var raw []byte 438 439 var temperature float64 440 441 // Get the base directory location 442 directory := filepath.Dir(file) 443 444 // Get the base filename prefix like temp1 445 basename := strings.Split(filepath.Base(file), "_")[0] 446 447 // Get the base path like <dir>/temp1 448 basepath := filepath.Join(directory, basename) 449 450 // Get the label of the temperature you are reading 451 label := "" 452 453 if raw, _ = ioutil.ReadFile(basepath + "_label"); len(raw) != 0 { 454 // Format the label from "Core 0" to "core_0" 455 label = strings.Join(strings.Split(strings.TrimSpace(strings.ToLower(string(raw))), " "), "_") 456 } 457 458 // Get the name of the temperature you are reading 459 if raw, err = ioutil.ReadFile(filepath.Join(directory, "name")); err != nil { 460 warns.Add(err) 461 continue 462 } 463 464 name := strings.TrimSpace(string(raw)) 465 466 if label != "" { 467 name = name + "_" + label 468 } 469 470 // Get the temperature reading 471 if raw, err = ioutil.ReadFile(file); err != nil { 472 warns.Add(err) 473 continue 474 } 475 476 if temperature, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { 477 warns.Add(err) 478 continue 479 } 480 481 // Add discovered temperature sensor to the list 482 temperatures = append(temperatures, TemperatureStat{ 483 SensorKey: name, 484 Temperature: temperature / hostTemperatureScale, 485 High: optionalValueReadFromFile(basepath+"_max") / hostTemperatureScale, 486 Critical: optionalValueReadFromFile(basepath+"_crit") / hostTemperatureScale, 487 }) 488 } 489 490 return temperatures, warns.Reference() 491} 492 493func optionalValueReadFromFile(filename string) float64 { 494 var raw []byte 495 496 var err error 497 498 var value float64 499 500 // Check if file exists 501 if _, err := os.Stat(filename); os.IsNotExist(err) { 502 return 0 503 } 504 505 if raw, err = ioutil.ReadFile(filename); err != nil { 506 return 0 507 } 508 509 if value, err = strconv.ParseFloat(strings.TrimSpace(string(raw)), 64); err != nil { 510 return 0 511 } 512 513 return value 514} 515