yoohoo
This commit is contained in:
487
internal/system/detector.go
Normal file
487
internal/system/detector.go
Normal file
@@ -0,0 +1,487 @@
|
||||
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
|
||||
}
|
121
internal/system/detector_test.go
Normal file
121
internal/system/detector_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectPackageManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
distro string
|
||||
expected string
|
||||
}{
|
||||
{"ubuntu", "apt"},
|
||||
{"debian", "apt"},
|
||||
{"fedora", "dnf"},
|
||||
{"centos", "dnf"},
|
||||
{"arch", "pacman"},
|
||||
{"manjaro", "pacman"},
|
||||
{"opensuse", "zypper"},
|
||||
{"alpine", "apk"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := detectPackageManager(test.distro)
|
||||
if result != test.expected {
|
||||
t.Errorf("detectPackageManager(%q) = %q, want %q", test.distro, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
unknownResult := detectPackageManager("totally_unknown_distro")
|
||||
if unknownResult == "" {
|
||||
t.Error("detectPackageManager with unknown distro should return something")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSystem(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := DetectSystem(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("DetectSystem() failed: %v", err)
|
||||
}
|
||||
|
||||
if info.Distribution == "" {
|
||||
t.Error("Distribution should not be empty")
|
||||
}
|
||||
|
||||
if info.Architecture == "" {
|
||||
t.Error("Architecture should not be empty")
|
||||
}
|
||||
|
||||
if info.PackageManager == "" {
|
||||
t.Error("PackageManager should not be empty")
|
||||
}
|
||||
|
||||
// Architecture should match runtime.GOARCH
|
||||
if info.Architecture != runtime.GOARCH {
|
||||
t.Errorf("Architecture = %q, want %q", info.Architecture, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChassisType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
expected string
|
||||
}{
|
||||
{1, "Other"},
|
||||
{3, "Desktop"},
|
||||
{9, "Laptop"},
|
||||
{10, "Notebook"},
|
||||
{999, "Unknown"}, // Invalid type
|
||||
{0, "Unknown"}, // Invalid type
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := getChassisType(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("getChassisType(%d) = %q, want %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatherSystemInfo(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sysInfo, err := GatherSystemInfo(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GatherSystemInfo() failed: %v", err)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if sysInfo.Distribution.ID == "" {
|
||||
t.Error("Distribution ID should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.CPU.Architecture == "" {
|
||||
t.Error("CPU Architecture should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.Memory.Total <= 0 {
|
||||
t.Error("Memory Total should be greater than 0")
|
||||
}
|
||||
|
||||
// CPU should have at least 1 core (but may be 0 if /proc/cpuinfo is not accessible in test environment)
|
||||
if sysInfo.CPU.Cores < 0 {
|
||||
t.Error("CPU Cores should not be negative")
|
||||
}
|
||||
|
||||
// If we can read CPU info, we should have at least 1 core
|
||||
// In test environments, this might not be available, so we just log it
|
||||
if sysInfo.CPU.Cores == 0 {
|
||||
t.Logf("Warning: CPU cores is 0, possibly due to test environment limitations")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user