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