netfetch/internal/collector/os.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)
}