This commit is contained in:
2025-09-16 14:27:34 +03:00
commit afeb139f5a
21 changed files with 4714 additions and 0 deletions

487
internal/system/detector.go Normal file
View 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
}