mirror of
https://github.com/Alexander-D-Karpov/netfetch.git
synced 2026-03-16 22:07:03 +03:00
485 lines
11 KiB
Go
485 lines
11 KiB
Go
package collector
|
|
|
|
import (
|
|
"bufio"
|
|
"netfetch/internal/model"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
func (c *Collector) collectOS() {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
hostname, _ := os.Hostname()
|
|
user := os.Getenv("USER")
|
|
if user == "" {
|
|
user = os.Getenv("USERNAME")
|
|
}
|
|
|
|
c.info.OS = &model.OSInfo{
|
|
Arch: getArchitecture(),
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
detectLinuxOS(c.info)
|
|
case "darwin":
|
|
detectDarwinOS(c.info)
|
|
case "windows":
|
|
detectWindowsOS(c.info)
|
|
case "freebsd", "openbsd", "netbsd":
|
|
detectBSDOS(c.info)
|
|
}
|
|
|
|
c.info.Host = hostname
|
|
c.info.User = user
|
|
c.info.Kernel = getKernelVersion()
|
|
|
|
if c.info.OS.Distro == "" {
|
|
c.info.OS.Distro = runtime.GOOS
|
|
}
|
|
}
|
|
|
|
func detectLinuxOS(info *model.SystemInfo) {
|
|
osInfo := parseOSRelease()
|
|
|
|
if osInfo != nil {
|
|
info.OS.Name = getOrDefault(osInfo, "NAME", "Linux")
|
|
info.OS.PrettyName = getOrDefault(osInfo, "PRETTY_NAME", "Linux")
|
|
info.OS.Distro = getOrDefault(osInfo, "ID", "linux")
|
|
info.OS.IDLike = getOrDefault(osInfo, "ID_LIKE", "")
|
|
info.OS.Version = getOrDefault(osInfo, "VERSION", "")
|
|
info.OS.VersionID = getOrDefault(osInfo, "VERSION_ID", "")
|
|
info.OS.Codename = getOrDefault(osInfo, "VERSION_CODENAME", "")
|
|
info.OS.BuildID = getOrDefault(osInfo, "BUILD_ID", "")
|
|
info.OS.Variant = getOrDefault(osInfo, "VARIANT", "")
|
|
info.OS.VariantID = getOrDefault(osInfo, "VARIANT_ID", "")
|
|
}
|
|
|
|
if info.OS.Distro == "ubuntu" || info.OS.IDLike == "ubuntu" {
|
|
detectUbuntuFlavor(info)
|
|
}
|
|
|
|
detectContainerEnvironment(info)
|
|
detectWSL(info)
|
|
}
|
|
|
|
func parseOSRelease() map[string]string {
|
|
paths := []string{
|
|
"/etc/os-release",
|
|
"/usr/lib/os-release",
|
|
"/etc/lsb-release",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if info := parseKeyValueFile(path); info != nil && len(info) > 0 {
|
|
if path == "/etc/lsb-release" {
|
|
return normalizeLSBRelease(info)
|
|
}
|
|
return info
|
|
}
|
|
}
|
|
|
|
return tryLSBReleaseCommand()
|
|
}
|
|
|
|
func normalizeLSBRelease(info map[string]string) map[string]string {
|
|
normalized := make(map[string]string)
|
|
normalized["NAME"] = getOrDefault(info, "DISTRIB_ID", "")
|
|
normalized["VERSION"] = getOrDefault(info, "DISTRIB_RELEASE", "")
|
|
normalized["ID"] = strings.ToLower(getOrDefault(info, "DISTRIB_ID", ""))
|
|
normalized["VERSION_CODENAME"] = getOrDefault(info, "DISTRIB_CODENAME", "")
|
|
normalized["PRETTY_NAME"] = getOrDefault(info, "DISTRIB_DESCRIPTION", "")
|
|
return normalized
|
|
}
|
|
|
|
func tryLSBReleaseCommand() map[string]string {
|
|
out, err := exec.Command("lsb_release", "-a").Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]string)
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
parts := strings.SplitN(line, ":", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch key {
|
|
case "Distributor ID":
|
|
result["NAME"] = value
|
|
result["ID"] = strings.ToLower(value)
|
|
case "Release":
|
|
result["VERSION_ID"] = value
|
|
case "Codename":
|
|
result["VERSION_CODENAME"] = value
|
|
case "Description":
|
|
result["PRETTY_NAME"] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func detectUbuntuFlavor(info *model.SystemInfo) {
|
|
xdgConfigDirs := os.Getenv("XDG_CONFIG_DIRS")
|
|
xdgCurrentDesktop := strings.ToLower(os.Getenv("XDG_CURRENT_DESKTOP"))
|
|
xdgSessionDesktop := strings.ToLower(os.Getenv("XDG_SESSION_DESKTOP"))
|
|
|
|
combined := xdgConfigDirs + " " + xdgCurrentDesktop + " " + xdgSessionDesktop
|
|
|
|
flavorMap := map[string]struct {
|
|
name string
|
|
id string
|
|
}{
|
|
"kde": {"Kubuntu", "kubuntu"},
|
|
"plasma": {"Kubuntu", "kubuntu"},
|
|
"kubuntu": {"Kubuntu", "kubuntu"},
|
|
"xfce": {"Xubuntu", "xubuntu"},
|
|
"xubuntu": {"Xubuntu", "xubuntu"},
|
|
"lxqt": {"Lubuntu", "lubuntu"},
|
|
"lubuntu": {"Lubuntu", "lubuntu"},
|
|
"budgie": {"Ubuntu Budgie", "ubuntu-budgie"},
|
|
"mate": {"Ubuntu MATE", "ubuntu-mate"},
|
|
"cinnamon": {"Ubuntu Cinnamon", "ubuntu-cinnamon"},
|
|
"unity": {"Ubuntu Unity", "ubuntu-unity"},
|
|
"pantheon": {"elementary OS", "elementary"},
|
|
"gnome": {"Ubuntu", "ubuntu"},
|
|
"ubuntu:gnome": {"Ubuntu", "ubuntu"},
|
|
}
|
|
|
|
for key, flavor := range flavorMap {
|
|
if strings.Contains(combined, key) {
|
|
if key != "gnome" && key != "ubuntu:gnome" {
|
|
info.OS.Name = flavor.name
|
|
info.OS.Distro = flavor.id
|
|
info.OS.IDLike = "ubuntu debian"
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectContainerEnvironment(info *model.SystemInfo) {
|
|
containerEnv := os.Getenv("container")
|
|
if containerEnv != "" {
|
|
info.OS.Variant = "Container (" + containerEnv + ")"
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat("/.dockerenv"); err == nil {
|
|
info.OS.Variant = "Container (Docker)"
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat("/run/.containerenv"); err == nil {
|
|
info.OS.Variant = "Container (Podman)"
|
|
return
|
|
}
|
|
|
|
cgroupData, err := os.ReadFile("/proc/1/cgroup")
|
|
if err == nil {
|
|
content := string(cgroupData)
|
|
if strings.Contains(content, "docker") {
|
|
info.OS.Variant = "Container (Docker)"
|
|
return
|
|
}
|
|
if strings.Contains(content, "lxc") {
|
|
info.OS.Variant = "Container (LXC)"
|
|
return
|
|
}
|
|
if strings.Contains(content, "kubepods") {
|
|
info.OS.Variant = "Container (Kubernetes)"
|
|
return
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat("/proc/vz"); err == nil {
|
|
if _, err := os.Stat("/proc/bc"); os.IsNotExist(err) {
|
|
info.OS.Variant = "Container (OpenVZ)"
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectWSL(info *model.SystemInfo) {
|
|
kernelVersion := getKernelVersion()
|
|
kernelLower := strings.ToLower(kernelVersion)
|
|
|
|
if strings.Contains(kernelLower, "microsoft") || strings.Contains(kernelLower, "wsl") {
|
|
info.OS.Variant = "WSL"
|
|
|
|
if _, err := os.Stat("/proc/sys/fs/binfmt_misc/WSLInterop"); err == nil {
|
|
info.OS.Variant = "WSL2"
|
|
} else if strings.Contains(kernelLower, "wsl2") {
|
|
info.OS.Variant = "WSL2"
|
|
}
|
|
return
|
|
}
|
|
|
|
if data, err := os.ReadFile("/proc/version"); err == nil {
|
|
content := strings.ToLower(string(data))
|
|
if strings.Contains(content, "microsoft") {
|
|
info.OS.Variant = "WSL"
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectDarwinOS(info *model.SystemInfo) {
|
|
info.OS.Distro = "macos"
|
|
info.OS.Name = "macOS"
|
|
|
|
out, err := exec.Command("sw_vers", "-productVersion").Output()
|
|
if err == nil {
|
|
info.OS.VersionID = strings.TrimSpace(string(out))
|
|
info.OS.Version = info.OS.VersionID
|
|
}
|
|
|
|
out, err = exec.Command("sw_vers", "-productName").Output()
|
|
if err == nil {
|
|
info.OS.Name = strings.TrimSpace(string(out))
|
|
}
|
|
|
|
out, err = exec.Command("sw_vers", "-buildVersion").Output()
|
|
if err == nil {
|
|
info.OS.BuildID = strings.TrimSpace(string(out))
|
|
}
|
|
|
|
info.OS.PrettyName = info.OS.Name + " " + info.OS.Version
|
|
|
|
versionCodenames := map[string]string{
|
|
"15": "Sequoia",
|
|
"14": "Sonoma",
|
|
"13": "Ventura",
|
|
"12": "Monterey",
|
|
"11": "Big Sur",
|
|
"10.15": "Catalina",
|
|
"10.14": "Mojave",
|
|
}
|
|
|
|
for prefix, codename := range versionCodenames {
|
|
if strings.HasPrefix(info.OS.VersionID, prefix) {
|
|
info.OS.Codename = codename
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectWindowsOS(info *model.SystemInfo) {
|
|
info.OS.Distro = "windows"
|
|
info.OS.Name = "Windows"
|
|
|
|
out, err := exec.Command("cmd", "/c", "ver").Output()
|
|
if err == nil {
|
|
info.OS.Version = strings.TrimSpace(string(out))
|
|
}
|
|
|
|
out, err = exec.Command("wmic", "os", "get", "Caption", "/value").Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "Caption=") {
|
|
info.OS.PrettyName = strings.TrimSpace(strings.TrimPrefix(line, "Caption="))
|
|
info.OS.Name = info.OS.PrettyName
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
out, err = exec.Command("wmic", "os", "get", "BuildNumber", "/value").Output()
|
|
if err == nil {
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "BuildNumber=") {
|
|
info.OS.BuildID = strings.TrimSpace(strings.TrimPrefix(line, "BuildNumber="))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func detectBSDOS(info *model.SystemInfo) {
|
|
info.OS.Distro = runtime.GOOS
|
|
info.OS.Name = strings.Title(runtime.GOOS)
|
|
|
|
out, err := exec.Command("uname", "-r").Output()
|
|
if err == nil {
|
|
info.OS.VersionID = strings.TrimSpace(string(out))
|
|
info.OS.Version = info.OS.VersionID
|
|
}
|
|
|
|
info.OS.PrettyName = info.OS.Name + " " + info.OS.Version
|
|
}
|
|
|
|
func parseKeyValueFile(path string) map[string]string {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer file.Close()
|
|
|
|
result := make(map[string]string)
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
idx := strings.Index(line, "=")
|
|
if idx == -1 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(line[:idx])
|
|
value := strings.TrimSpace(line[idx+1:])
|
|
value = strings.Trim(value, `"'`)
|
|
|
|
result[key] = value
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getKernelVersion() string {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
if data, err := os.ReadFile("/proc/version"); err == nil {
|
|
fields := strings.Fields(string(data))
|
|
if len(fields) >= 3 {
|
|
return fields[2]
|
|
}
|
|
}
|
|
out, err := exec.Command("uname", "-r").Output()
|
|
if err != nil {
|
|
return "Unknown"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
case "darwin":
|
|
out, err := exec.Command("uname", "-r").Output()
|
|
if err != nil {
|
|
return "Unknown"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
case "windows":
|
|
return getWindowsKernelVersion()
|
|
case "freebsd", "openbsd", "netbsd":
|
|
out, err := exec.Command("uname", "-r").Output()
|
|
if err != nil {
|
|
return "Unknown"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|
|
|
|
func getWindowsKernelVersion() string {
|
|
out, err := exec.Command("wmic", "os", "get", "Version", "/value").Output()
|
|
if err != nil {
|
|
return "Unknown"
|
|
}
|
|
lines := strings.Split(string(out), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "Version=") {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "Version="))
|
|
}
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func getArchitecture() string {
|
|
arch := runtime.GOARCH
|
|
switch arch {
|
|
case "amd64":
|
|
return "x86_64"
|
|
case "386":
|
|
return "i686"
|
|
case "arm64":
|
|
return "aarch64"
|
|
case "arm":
|
|
if data, err := os.ReadFile("/proc/cpuinfo"); err == nil {
|
|
content := string(data)
|
|
if strings.Contains(content, "ARMv7") {
|
|
return "armv7l"
|
|
}
|
|
if strings.Contains(content, "ARMv6") {
|
|
return "armv6l"
|
|
}
|
|
}
|
|
return "armv7l"
|
|
case "riscv64":
|
|
return "riscv64"
|
|
case "ppc64le":
|
|
return "ppc64le"
|
|
case "s390x":
|
|
return "s390x"
|
|
case "mips64le":
|
|
return "mips64el"
|
|
default:
|
|
return arch
|
|
}
|
|
}
|
|
|
|
func getOrDefault(m map[string]string, key, defaultValue string) string {
|
|
if val, ok := m[key]; ok && val != "" {
|
|
return val
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func pathExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func getUserShellPath() string {
|
|
shell := os.Getenv("SHELL")
|
|
if shell != "" {
|
|
return shell
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
return os.Getenv("COMSPEC")
|
|
}
|
|
|
|
passwdPath := "/etc/passwd"
|
|
user := os.Getenv("USER")
|
|
if user == "" {
|
|
return ""
|
|
}
|
|
|
|
data, err := os.ReadFile(passwdPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, user+":") {
|
|
fields := strings.Split(line, ":")
|
|
if len(fields) >= 7 {
|
|
return fields[6]
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func getUserShell() string {
|
|
shellPath := getUserShellPath()
|
|
if shellPath == "" {
|
|
return "Unknown"
|
|
}
|
|
return filepath.Base(shellPath)
|
|
}
|