488 lines
12 KiB
Go
488 lines
12 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
|
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
|
)
|
|
|
|
type Info struct {
|
|
Distribution string
|
|
Version string
|
|
Architecture string
|
|
PackageManager string
|
|
}
|
|
|
|
func DetectSystem(ctx context.Context) (*Info, error) {
|
|
if runtime.GOOS != "linux" {
|
|
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
|
}
|
|
|
|
info := &Info{
|
|
Architecture: runtime.GOARCH,
|
|
}
|
|
|
|
if err := detectFromOSRelease(info); err != nil {
|
|
if err := detectFromLSBRelease(info); err != nil {
|
|
return nil, fmt.Errorf("failed to detect Linux distribution: %w", err)
|
|
}
|
|
}
|
|
|
|
info.PackageManager = detectPackageManager(info.Distribution)
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func detectFromOSRelease(info *Info) error {
|
|
const osReleasePath = "/etc/os-release"
|
|
|
|
if !utils.FileExists(osReleasePath) {
|
|
return fmt.Errorf("os-release file not found")
|
|
}
|
|
|
|
lines, err := utils.ReadFileLines(osReleasePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read os-release: %w", err)
|
|
}
|
|
|
|
osInfo := make(map[string]string)
|
|
for _, line := range lines {
|
|
if key, value, ok := utils.ParseKeyValue(line); ok {
|
|
osInfo[key] = value
|
|
}
|
|
}
|
|
|
|
if id, exists := osInfo["ID"]; exists {
|
|
info.Distribution = id
|
|
}
|
|
|
|
if version, exists := osInfo["VERSION_ID"]; exists {
|
|
info.Version = version
|
|
} else if version, exists := osInfo["VERSION"]; exists {
|
|
info.Version = version
|
|
}
|
|
|
|
if info.Distribution == "" {
|
|
return fmt.Errorf("could not determine distribution from os-release")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func detectFromLSBRelease(info *Info) error {
|
|
if !utils.CommandExists("lsb_release") {
|
|
return fmt.Errorf("lsb_release command not available")
|
|
}
|
|
|
|
distro, err := utils.RunCommand("lsb_release", "-si")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get distribution ID: %w", err)
|
|
}
|
|
info.Distribution = strings.ToLower(distro)
|
|
|
|
version, err := utils.RunCommand("lsb_release", "-sr")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get distribution version: %w", err)
|
|
}
|
|
info.Version = version
|
|
|
|
return nil
|
|
}
|
|
|
|
func detectPackageManager(distro string) string {
|
|
switch strings.ToLower(distro) {
|
|
case "ubuntu", "debian", "linuxmint", "elementary", "pop":
|
|
return "apt"
|
|
case "fedora", "rhel", "centos", "rocky", "almalinux":
|
|
return "dnf"
|
|
case "opensuse", "suse", "opensuse-leap", "opensuse-tumbleweed":
|
|
return "zypper"
|
|
case "arch", "manjaro", "endeavouros", "garuda":
|
|
return "pacman"
|
|
case "gentoo":
|
|
return "portage"
|
|
case "alpine":
|
|
return "apk"
|
|
default:
|
|
if utils.CommandExists("apt") {
|
|
return "apt"
|
|
} else if utils.CommandExists("dnf") {
|
|
return "dnf"
|
|
} else if utils.CommandExists("yum") {
|
|
return "yum"
|
|
} else if utils.CommandExists("zypper") {
|
|
return "zypper"
|
|
} else if utils.CommandExists("pacman") {
|
|
return "pacman"
|
|
} else if utils.CommandExists("apk") {
|
|
return "apk"
|
|
}
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error) {
|
|
sysInfo := &types.SystemInfo{}
|
|
|
|
basicInfo, err := DetectSystem(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect system: %w", err)
|
|
}
|
|
|
|
sysInfo.Distribution = types.DistributionInfo{
|
|
ID: basicInfo.Distribution,
|
|
Version: basicInfo.Version,
|
|
PackageManager: basicInfo.PackageManager,
|
|
}
|
|
|
|
if cpuInfo, err := gatherCPUInfo(); err == nil {
|
|
sysInfo.CPU = *cpuInfo
|
|
}
|
|
|
|
if memInfo, err := gatherMemoryInfo(); err == nil {
|
|
sysInfo.Memory = *memInfo
|
|
}
|
|
|
|
if batteryInfo, err := gatherBatteryInfo(); err == nil {
|
|
sysInfo.Battery = batteryInfo
|
|
}
|
|
|
|
if powerInfo, err := gatherPowerSupplyInfo(); err == nil {
|
|
sysInfo.PowerSupply = *powerInfo
|
|
}
|
|
|
|
if kernelInfo, err := gatherKernelInfo(); err == nil {
|
|
sysInfo.Kernel = *kernelInfo
|
|
}
|
|
|
|
if hwInfo, err := gatherHardwareInfo(); err == nil {
|
|
sysInfo.Hardware = *hwInfo
|
|
}
|
|
|
|
return sysInfo, nil
|
|
}
|
|
|
|
func gatherCPUInfo() (*types.CPUInfo, error) {
|
|
cpuInfo := &types.CPUInfo{}
|
|
|
|
if utils.FileExists("/proc/cpuinfo") {
|
|
lines, err := utils.ReadFileLines("/proc/cpuinfo")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, line := range lines {
|
|
if key, value, ok := utils.ParseKeyValue(line); ok {
|
|
switch strings.ToLower(key) {
|
|
case "model name":
|
|
if cpuInfo.Model == "" {
|
|
cpuInfo.Model = value
|
|
}
|
|
case "vendor_id":
|
|
if cpuInfo.Vendor == "" {
|
|
cpuInfo.Vendor = value
|
|
}
|
|
case "processor":
|
|
cpuInfo.Cores++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") {
|
|
governor, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
|
if err == nil {
|
|
cpuInfo.Governor = governor
|
|
}
|
|
}
|
|
|
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") {
|
|
maxFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
|
|
if err == nil {
|
|
cpuInfo.MaxFrequency = utils.ParseInt64(maxFreq) / 1000
|
|
}
|
|
}
|
|
|
|
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq") {
|
|
minFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")
|
|
if err == nil {
|
|
cpuInfo.MinFrequency = utils.ParseInt64(minFreq) / 1000
|
|
}
|
|
}
|
|
|
|
cpuInfo.Architecture = runtime.GOARCH
|
|
|
|
return cpuInfo, nil
|
|
}
|
|
|
|
func gatherMemoryInfo() (*types.MemoryInfo, error) {
|
|
memInfo := &types.MemoryInfo{}
|
|
|
|
if !utils.FileExists("/proc/meminfo") {
|
|
return nil, fmt.Errorf("/proc/meminfo not found")
|
|
}
|
|
|
|
lines, err := utils.ReadFileLines("/proc/meminfo")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, line := range lines {
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSuffix(parts[0], ":")
|
|
value := utils.ParseInt64(parts[1])
|
|
|
|
switch key {
|
|
case "MemTotal":
|
|
memInfo.Total = value / 1024
|
|
case "MemAvailable":
|
|
memInfo.Available = value / 1024
|
|
case "SwapTotal":
|
|
memInfo.SwapTotal = value / 1024
|
|
case "SwapFree":
|
|
swapFree := value / 1024
|
|
memInfo.SwapUsed = memInfo.SwapTotal - swapFree
|
|
}
|
|
}
|
|
|
|
memInfo.Used = memInfo.Total - memInfo.Available
|
|
|
|
return memInfo, nil
|
|
}
|
|
|
|
func gatherBatteryInfo() (*types.BatteryInfo, error) {
|
|
batteryPath := "/sys/class/power_supply/BAT0"
|
|
if !utils.FileExists(batteryPath) {
|
|
batteryPath = "/sys/class/power_supply/BAT1"
|
|
if !utils.FileExists(batteryPath) {
|
|
return nil, fmt.Errorf("no battery found")
|
|
}
|
|
}
|
|
|
|
batteryInfo := &types.BatteryInfo{Present: true}
|
|
|
|
properties := map[string]*string{
|
|
"status": &batteryInfo.Status,
|
|
"manufacturer": &batteryInfo.Manufacturer,
|
|
"model_name": &batteryInfo.Model,
|
|
"technology": &batteryInfo.Technology,
|
|
}
|
|
|
|
for prop, field := range properties {
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/" + prop); err == nil {
|
|
*field = value
|
|
}
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/capacity"); err == nil {
|
|
batteryInfo.Capacity = utils.ParseInt(value)
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full"); err == nil {
|
|
batteryInfo.EnergyFull = utils.ParseInt64(value) / 1000000
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_now"); err == nil {
|
|
batteryInfo.EnergyNow = utils.ParseInt64(value) / 1000000
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/power_now"); err == nil {
|
|
batteryInfo.PowerNow = utils.ParseInt64(value) / 1000000
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/cycle_count"); err == nil {
|
|
batteryInfo.CycleCount = utils.ParseInt(value)
|
|
}
|
|
|
|
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full_design"); err == nil {
|
|
batteryInfo.DesignCapacity = utils.ParseInt64(value) / 1000000
|
|
}
|
|
|
|
return batteryInfo, nil
|
|
}
|
|
|
|
func gatherPowerSupplyInfo() (*types.PowerSupplyInfo, error) {
|
|
powerInfo := &types.PowerSupplyInfo{}
|
|
|
|
acPaths := []string{
|
|
"/sys/class/power_supply/ADP0",
|
|
"/sys/class/power_supply/ADP1",
|
|
"/sys/class/power_supply/AC",
|
|
"/sys/class/power_supply/ACAD",
|
|
}
|
|
|
|
for _, path := range acPaths {
|
|
if utils.FileExists(path + "/online") {
|
|
if online, err := utils.ReadFirstLine(path + "/online"); err == nil {
|
|
powerInfo.ACConnected = online == "1"
|
|
powerInfo.Online = powerInfo.ACConnected
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
powerInfo.Type = "AC"
|
|
|
|
return powerInfo, nil
|
|
}
|
|
|
|
func gatherKernelInfo() (*types.KernelInfo, error) {
|
|
kernelInfo := &types.KernelInfo{
|
|
Parameters: make(map[string]string),
|
|
}
|
|
|
|
if version, err := utils.ReadFirstLine("/proc/version"); err == nil {
|
|
parts := strings.Fields(version)
|
|
if len(parts) >= 3 {
|
|
kernelInfo.Version = parts[2]
|
|
}
|
|
}
|
|
|
|
if release, err := utils.ReadFirstLine("/proc/sys/kernel/osrelease"); err == nil {
|
|
kernelInfo.Release = release
|
|
}
|
|
|
|
if cmdline, err := utils.ReadFirstLine("/proc/cmdline"); err == nil {
|
|
params := strings.Fields(cmdline)
|
|
for _, param := range params {
|
|
if key, value, ok := utils.ParseKeyValue(param); ok {
|
|
kernelInfo.Parameters[key] = value
|
|
} else {
|
|
kernelInfo.Parameters[param] = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
return kernelInfo, nil
|
|
}
|
|
|
|
func gatherHardwareInfo() (*types.HardwareInfo, error) {
|
|
hwInfo := &types.HardwareInfo{}
|
|
|
|
if utils.FileExists("/sys/class/dmi/id/chassis_type") {
|
|
if chassis, err := utils.ReadFirstLine("/sys/class/dmi/id/chassis_type"); err == nil {
|
|
hwInfo.Chassis = getChassisType(utils.ParseInt(chassis))
|
|
}
|
|
}
|
|
|
|
if utils.FileExists("/sys/class/dmi/id/sys_vendor") {
|
|
if vendor, err := utils.ReadFirstLine("/sys/class/dmi/id/sys_vendor"); err == nil {
|
|
hwInfo.Manufacturer = vendor
|
|
}
|
|
}
|
|
|
|
if utils.FileExists("/sys/class/dmi/id/product_name") {
|
|
if product, err := utils.ReadFirstLine("/sys/class/dmi/id/product_name"); err == nil {
|
|
hwInfo.ProductName = product
|
|
}
|
|
}
|
|
|
|
hwInfo.StorageDevices = gatherStorageInfo()
|
|
|
|
return hwInfo, nil
|
|
}
|
|
|
|
func getChassisType(chassisType int) string {
|
|
types := map[int]string{
|
|
1: "Other",
|
|
2: "Unknown",
|
|
3: "Desktop",
|
|
4: "Low Profile Desktop",
|
|
5: "Pizza Box",
|
|
6: "Mini Tower",
|
|
7: "Tower",
|
|
8: "Portable",
|
|
9: "Laptop",
|
|
10: "Notebook",
|
|
11: "Hand Held",
|
|
12: "Docking Station",
|
|
13: "All In One",
|
|
14: "Sub Notebook",
|
|
15: "Space-saving",
|
|
16: "Lunch Box",
|
|
17: "Main Server Chassis",
|
|
18: "Expansion Chassis",
|
|
19: "Sub Chassis",
|
|
20: "Bus Expansion Chassis",
|
|
21: "Peripheral Chassis",
|
|
22: "RAID Chassis",
|
|
23: "Rack Mount Chassis",
|
|
24: "Sealed-case PC",
|
|
25: "Multi-system",
|
|
26: "CompactPCI",
|
|
27: "AdvancedTCA",
|
|
28: "Blade",
|
|
29: "Blade Enclosing",
|
|
}
|
|
|
|
if desc, exists := types[chassisType]; exists {
|
|
return desc
|
|
}
|
|
return "Unknown"
|
|
}
|
|
|
|
func gatherStorageInfo() []types.StorageInfo {
|
|
var devices []types.StorageInfo
|
|
|
|
if !utils.FileExists("/proc/partitions") {
|
|
return devices
|
|
}
|
|
|
|
lines, err := utils.ReadFileLines("/proc/partitions")
|
|
if err != nil {
|
|
return devices
|
|
}
|
|
|
|
for _, line := range lines {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
|
|
deviceName := fields[3]
|
|
if strings.HasPrefix(line, "major") ||
|
|
strings.Contains(deviceName, "loop") ||
|
|
len(deviceName) > 3 && (deviceName[len(deviceName)-1] >= '0' && deviceName[len(deviceName)-1] <= '9') {
|
|
continue
|
|
}
|
|
|
|
device := types.StorageInfo{
|
|
Device: "/dev/" + deviceName,
|
|
Size: utils.ParseInt64(fields[2]) / 1024 / 1024,
|
|
}
|
|
|
|
rotationalPath := fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)
|
|
if utils.FileExists(rotationalPath) {
|
|
if rotational, err := utils.ReadFirstLine(rotationalPath); err == nil {
|
|
device.Rotational = rotational == "1"
|
|
if device.Rotational {
|
|
device.Type = "HDD"
|
|
} else if strings.HasPrefix(deviceName, "nvme") {
|
|
device.Type = "NVMe"
|
|
} else {
|
|
device.Type = "SSD"
|
|
}
|
|
}
|
|
}
|
|
|
|
modelPath := fmt.Sprintf("/sys/block/%s/device/model", deviceName)
|
|
if utils.FileExists(modelPath) {
|
|
if model, err := utils.ReadFirstLine(modelPath); err == nil {
|
|
device.Model = strings.TrimSpace(model)
|
|
}
|
|
}
|
|
|
|
devices = append(devices, device)
|
|
}
|
|
|
|
return devices
|
|
}
|