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