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

347
internal/ai/client.go Normal file
View File

@@ -0,0 +1,347 @@
package ai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
)
type Client struct {
config *types.AIConfig
httpClient *http.Client
logger *utils.Logger
}
func NewClient(config *types.AIConfig, logger *utils.Logger) *Client {
return &Client{
config: config,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
logger: logger.WithComponent("ai"),
}
}
func ConfigureClient(logger *utils.Logger) (*Client, error) {
fmt.Println("\n" + strings.Repeat("=", 50))
fmt.Println("AI SERVICE CONFIGURATION")
fmt.Println(strings.Repeat("=", 50))
config := &types.AIConfig{}
fmt.Println("\nAvailable AI providers:")
fmt.Println("1. Groq (Fast inference)")
fmt.Println("2. OpenRouter (Multiple models)")
fmt.Println("3. Gemini (Google)")
fmt.Println("4. Custom (OpenAI-compatible)")
choice := utils.GetUserInput("Select provider (1-4)", "1")
switch choice {
case "1":
config.Provider = types.AIProviderGroq
config.Endpoint = "https://api.groq.com/openai/v1/chat/completions"
config.Model = "openai/gpt-oss-20b"
case "2":
config.Provider = types.AIProviderOpenRouter
config.Endpoint = "https://openrouter.ai/api/v1/chat/completions"
config.Model = utils.GetUserInput("Model name", "meta-llama/llama-3.1-8b-instruct:free")
case "3":
config.Provider = types.AIProviderGemini
config.Endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
config.Model = "gemini-pro"
case "4":
config.Provider = types.AIProviderCustom
config.Endpoint = utils.GetUserInput("API endpoint", "")
config.Model = utils.GetUserInput("Model name", "")
default:
return nil, fmt.Errorf("invalid provider selection")
}
fmt.Printf("\nEnter your API key for %s: ", config.Provider)
var apiKey string
fmt.Scanln(&apiKey)
config.APIKey = strings.TrimSpace(apiKey)
if config.APIKey == "" {
return nil, fmt.Errorf("API key is required")
}
config.MaxTokens = 1500
config.Temperature = 0.3
client := NewClient(config, logger)
if err := client.validateConnection(context.Background()); err != nil {
return nil, fmt.Errorf("failed to validate AI connection: %w", err)
}
fmt.Println("✓ AI service configured successfully!")
return client, nil
}
func (c *Client) GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
c.logger.Info("Generating TLP configuration using AI", "provider", c.config.Provider, "model", c.config.Model)
prompt := c.buildPrompt(sysInfo, preferences)
response, err := c.makeRequest(ctx, prompt)
if err != nil {
return nil, fmt.Errorf("AI request failed: %w", err)
}
config, err := c.parseResponse(response, sysInfo, preferences)
if err != nil {
return nil, fmt.Errorf("failed to parse AI response: %w", err)
}
c.logger.Info("TLP configuration generated successfully", "settings_count", len(config.Settings))
return config, nil
}
func (c *Client) buildPrompt(sysInfo *types.SystemInfo, preferences *types.UserPreferences) string {
var prompt strings.Builder
prompt.WriteString("Generate TLP configuration in JSON format.\n\n")
prompt.WriteString("System: ")
if sysInfo.Battery != nil && sysInfo.Battery.Present {
prompt.WriteString("Laptop")
} else {
prompt.WriteString("Desktop")
}
prompt.WriteString(fmt.Sprintf(", %s, %d cores\n", sysInfo.Distribution.ID, sysInfo.CPU.Cores))
prompt.WriteString(fmt.Sprintf("Profile: %s, Use: %s, Mode: %s\n\n", preferences.PowerProfile, preferences.UseCase, preferences.PerformanceMode))
prompt.WriteString("Return JSON with this structure:\n")
prompt.WriteString(`{"description": "Config description", "settings": {"TLP_ENABLE": "1", "CPU_SCALING_GOVERNOR_ON_AC": "performance", "CPU_SCALING_GOVERNOR_ON_BAT": "powersave", "DISK_APM_LEVEL_ON_AC": "254", "DISK_APM_LEVEL_ON_BAT": "128", "WIFI_PWR_ON_AC": "off", "WIFI_PWR_ON_BAT": "on", "USB_AUTOSUSPEND": "1"}, "rationale": {"CPU_SCALING_GOVERNOR_ON_AC": "Max performance on AC"}, "warnings": ["Monitor temperatures"]}` + "\n\n")
prompt.WriteString("Generate 8-10 TLP settings for this system.")
return prompt.String()
}
func (c *Client) makeRequest(ctx context.Context, prompt string) (string, error) {
c.logger.Debug("Making AI request", "prompt_length", len(prompt))
c.logger.Debug("Full prompt being sent", "prompt", prompt)
var requestBody interface{}
var endpoint string
switch c.config.Provider {
case types.AIProviderGemini:
requestBody = map[string]interface{}{
"contents": []map[string]interface{}{
{
"parts": []map[string]string{
{"text": prompt},
},
},
},
"generationConfig": map[string]interface{}{
"temperature": c.config.Temperature,
"maxOutputTokens": c.config.MaxTokens,
},
}
endpoint = c.config.Endpoint + "?key=" + c.config.APIKey
default:
requestBody = map[string]interface{}{
"model": c.config.Model,
"messages": []map[string]interface{}{
{
"role": "user",
"content": prompt,
},
},
"max_tokens": c.config.MaxTokens,
"temperature": c.config.Temperature,
"response_format": map[string]interface{}{
"type": "json_object",
},
}
endpoint = c.config.Endpoint
}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
c.logger.Debug("Request body", "json", string(jsonBody))
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonBody))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if c.config.Provider != types.AIProviderGemini {
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
return c.extractContent(body)
}
func (c *Client) extractContent(responseBody []byte) (string, error) {
var response map[string]interface{}
if err := json.Unmarshal(responseBody, &response); err != nil {
return "", fmt.Errorf("failed to parse response JSON: %w", err)
}
switch c.config.Provider {
case types.AIProviderGemini:
if candidates, ok := response["candidates"].([]interface{}); ok && len(candidates) > 0 {
if candidate, ok := candidates[0].(map[string]interface{}); ok {
if content, ok := candidate["content"].(map[string]interface{}); ok {
if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 {
if part, ok := parts[0].(map[string]interface{}); ok {
if text, ok := part["text"].(string); ok {
return text, nil
}
}
}
}
}
}
default:
if choices, ok := response["choices"].([]interface{}); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]interface{}); ok {
if message, ok := choice["message"].(map[string]interface{}); ok {
if content, ok := message["content"].(string); ok {
return content, nil
}
}
}
}
}
return "", fmt.Errorf("unexpected response format")
}
func (c *Client) parseResponse(response string, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
c.logger.Debug("Raw AI response", "response", response)
start := strings.Index(response, "{")
end := strings.LastIndex(response, "}") + 1
if start == -1 || end == 0 {
c.logger.Error("No JSON found in AI response", "response_length", len(response), "response_preview", response[:min(200, len(response))])
c.logger.Info("Generating fallback TLP configuration")
return c.generateFallbackConfig(sysInfo, preferences), nil
}
jsonStr := response[start:end]
var aiResponse struct {
Description string `json:"description"`
Settings map[string]string `json:"settings"`
Rationale map[string]string `json:"rationale"`
Warnings []string `json:"warnings"`
}
if err := json.Unmarshal([]byte(jsonStr), &aiResponse); err != nil {
c.logger.Error("Failed to parse AI JSON response", "error", err, "json_str", jsonStr)
c.logger.Info("Generating fallback TLP configuration due to JSON parse error")
return c.generateFallbackConfig(sysInfo, preferences), nil
}
if len(aiResponse.Settings) == 0 {
return nil, fmt.Errorf("AI response contains no settings")
}
config := &types.TLPConfiguration{
Settings: aiResponse.Settings,
Description: aiResponse.Description,
Rationale: aiResponse.Rationale,
Warnings: aiResponse.Warnings,
Generated: time.Now(),
SystemInfo: sysInfo,
Preferences: preferences,
}
if _, exists := config.Settings["TLP_ENABLE"]; !exists {
config.Settings["TLP_ENABLE"] = "1"
}
if config.Rationale == nil {
config.Rationale = make(map[string]string)
}
return config, nil
}
func (c *Client) validateConnection(ctx context.Context) error {
testPrompt := "Respond with a JSON object containing 'status': 'OK' to confirm the connection is working."
response, err := c.makeRequest(ctx, testPrompt)
if err != nil {
return fmt.Errorf("connection test failed: %w", err)
}
if !strings.Contains(strings.ToUpper(response), "OK") {
return fmt.Errorf("unexpected response from AI service: %s", response)
}
return nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func (c *Client) generateFallbackConfig(sysInfo *types.SystemInfo, preferences *types.UserPreferences) *types.TLPConfiguration {
c.logger.Info("Generating fallback configuration based on user preferences")
settings := config.GetRecommendedSettings(preferences)
rationale := make(map[string]string)
rationale["TLP_ENABLE"] = "Enable TLP power management"
rationale["CPU_SCALING_GOVERNOR_ON_AC"] = "CPU governor for AC power based on user preferences"
rationale["CPU_SCALING_GOVERNOR_ON_BAT"] = "CPU governor for battery power based on user preferences"
warnings := []string{
"This is a fallback configuration generated when AI service was unavailable",
"Configuration is based on user preferences and common best practices",
"Consider running WiseTLP again when AI service is available for optimized settings",
}
description := fmt.Sprintf("Fallback TLP configuration for %s use case with %s power profile",
preferences.UseCase, preferences.PowerProfile)
return &types.TLPConfiguration{
Settings: settings,
Description: description,
Rationale: rationale,
Warnings: warnings,
Generated: time.Now(),
SystemInfo: sysInfo,
Preferences: preferences,
}
}

View File

@@ -0,0 +1,346 @@
package config
import (
"fmt"
"strings"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
)
// Config represents the application configuration
type Config struct {
// Application-wide configuration settings
}
// New creates a new configuration instance
func New() *Config {
return &Config{}
}
// GatherUserPreferences interactively collects user preferences
func GatherUserPreferences() (*types.UserPreferences, error) {
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("USER PREFERENCES CONFIGURATION")
fmt.Println(strings.Repeat("=", 60))
preferences := &types.UserPreferences{
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
// Power Profile
fmt.Println("\n1. Power Profile Selection")
fmt.Println("Choose your preferred power management approach:")
fmt.Println(" a) Balanced - Good compromise between performance and power saving")
fmt.Println(" b) Performance - Maximum performance, higher power consumption")
fmt.Println(" c) Power Saving - Maximum battery life, reduced performance")
fmt.Println(" d) Custom - I'll specify custom requirements")
profileChoice := utils.GetUserInput("Select power profile (a/b/c/d)", "a")
switch strings.ToLower(profileChoice) {
case "a", "balanced":
preferences.PowerProfile = types.PowerProfileBalanced
case "b", "performance":
preferences.PowerProfile = types.PowerProfilePerformance
case "c", "power saving", "powersaving":
preferences.PowerProfile = types.PowerProfilePowerSaving
case "d", "custom":
preferences.PowerProfile = types.PowerProfileCustom
default:
preferences.PowerProfile = types.PowerProfileBalanced
}
// Use Case
fmt.Println("\n2. Primary Use Case")
fmt.Println("What do you primarily use this system for?")
fmt.Println(" a) General use - Web browsing, office work, media consumption")
fmt.Println(" b) Development - Programming, compiling, development tools")
fmt.Println(" c) Gaming - Gaming and graphics-intensive applications")
fmt.Println(" d) Server - Server workloads, always-on services")
fmt.Println(" e) Multimedia - Video editing, rendering, content creation")
fmt.Println(" f) Office - Primarily office applications and productivity")
useCaseChoice := utils.GetUserInput("Select use case (a/b/c/d/e/f)", "a")
switch strings.ToLower(useCaseChoice) {
case "a", "general":
preferences.UseCase = types.UseCaseGeneral
case "b", "development", "dev":
preferences.UseCase = types.UseCaseDevelopment
case "c", "gaming":
preferences.UseCase = types.UseCaseGaming
case "d", "server":
preferences.UseCase = types.UseCaseServer
case "e", "multimedia", "media":
preferences.UseCase = types.UseCaseMultimedia
case "f", "office":
preferences.UseCase = types.UseCaseOffice
default:
preferences.UseCase = types.UseCaseGeneral
}
// Battery Priority (only for systems with batteries)
fmt.Println("\n3. Battery Optimization Priority")
fmt.Println("How would you like to optimize battery usage?")
fmt.Println(" a) Balanced - Balance between battery life and longevity")
fmt.Println(" b) Runtime - Maximize single-charge runtime")
fmt.Println(" c) Longevity - Maximize battery lifespan over years")
batteryChoice := utils.GetUserInput("Select battery priority (a/b/c)", "a")
switch strings.ToLower(batteryChoice) {
case "a", "balanced":
preferences.BatteryPriority = types.BatteryPriorityBalanced
case "b", "runtime":
preferences.BatteryPriority = types.BatteryPriorityRuntime
case "c", "longevity":
preferences.BatteryPriority = types.BatteryPriorityLongevity
default:
preferences.BatteryPriority = types.BatteryPriorityBalanced
}
// Performance Mode
fmt.Println("\n4. Performance Mode")
fmt.Println("How should the system handle performance scaling?")
fmt.Println(" a) Adaptive - Automatically adjust based on workload")
fmt.Println(" b) Maximum - Always prefer maximum performance")
fmt.Println(" c) Efficient - Always prefer energy efficiency")
perfChoice := utils.GetUserInput("Select performance mode (a/b/c)", "a")
switch strings.ToLower(perfChoice) {
case "a", "adaptive":
preferences.PerformanceMode = types.PerformanceModeAdaptive
case "b", "maximum", "max":
preferences.PerformanceMode = types.PerformanceModeMaximum
case "c", "efficient", "efficiency":
preferences.PerformanceMode = types.PerformanceModeEfficient
default:
preferences.PerformanceMode = types.PerformanceModeAdaptive
}
// Special Requirements
fmt.Println("\n5. Special Requirements")
fmt.Println("Do you have any special requirements? (Select all that apply)")
fmt.Println(" a) Minimize fan noise")
fmt.Println(" b) Prevent thermal throttling")
fmt.Println(" c) Optimize for external displays")
fmt.Println(" d) Gaming performance priority")
fmt.Println(" e) Maximum WiFi performance")
fmt.Println(" f) Minimize disk wear")
fmt.Println(" g) Fast system wake/sleep")
fmt.Println(" h) None of the above")
requirements := utils.GetUserInput("Enter letters separated by spaces (e.g., 'a c f')", "h")
if strings.ToLower(requirements) != "h" {
reqMap := map[string]string{
"a": "Minimize fan noise",
"b": "Prevent thermal throttling",
"c": "Optimize for external displays",
"d": "Gaming performance priority",
"e": "Maximum WiFi performance",
"f": "Minimize disk wear",
"g": "Fast system wake/sleep",
}
reqLetters := strings.Fields(strings.ToLower(requirements))
for _, letter := range reqLetters {
if req, exists := reqMap[letter]; exists {
preferences.SpecialRequirements = append(preferences.SpecialRequirements, req)
}
}
}
// Custom Settings (for advanced users)
if preferences.PowerProfile == types.PowerProfileCustom {
fmt.Println("\n6. Custom Settings")
fmt.Println("You selected custom power profile. You can specify additional settings.")
if utils.GetUserConfirmation("Do you want to specify custom CPU governor settings?") {
acGovernor := utils.GetUserInput("CPU governor on AC power (performance/powersave/ondemand/conservative/schedutil)", "performance")
batGovernor := utils.GetUserInput("CPU governor on battery (performance/powersave/ondemand/conservative/schedutil)", "powersave")
preferences.CustomSettings["cpu_governor_ac"] = acGovernor
preferences.CustomSettings["cpu_governor_battery"] = batGovernor
}
if utils.GetUserConfirmation("Do you want to specify custom disk settings?") {
diskAPMAC := utils.GetUserInput("Disk APM level on AC (1-255, higher = more aggressive)", "254")
diskAPMBat := utils.GetUserInput("Disk APM level on battery (1-255, higher = more aggressive)", "128")
preferences.CustomSettings["disk_apm_ac"] = diskAPMAC
preferences.CustomSettings["disk_apm_battery"] = diskAPMBat
}
}
// Summary
fmt.Println("\n" + strings.Repeat("-", 60))
fmt.Println("PREFERENCES SUMMARY")
fmt.Println(strings.Repeat("-", 60))
fmt.Printf("Power Profile: %s\n", preferences.PowerProfile)
fmt.Printf("Use Case: %s\n", preferences.UseCase)
fmt.Printf("Battery Priority: %s\n", preferences.BatteryPriority)
fmt.Printf("Performance Mode: %s\n", preferences.PerformanceMode)
if len(preferences.SpecialRequirements) > 0 {
fmt.Printf("Special Requirements: %s\n", strings.Join(preferences.SpecialRequirements, ", "))
}
if len(preferences.CustomSettings) > 0 {
fmt.Println("Custom Settings:")
for key, value := range preferences.CustomSettings {
fmt.Printf(" %s: %v\n", key, value)
}
}
fmt.Println(strings.Repeat("-", 60))
if !utils.GetUserConfirmation("Are these preferences correct?") {
fmt.Println("Let's try again...")
return GatherUserPreferences()
}
return preferences, nil
}
// ValidatePreferences validates user preferences for consistency
func ValidatePreferences(preferences *types.UserPreferences) error {
if preferences == nil {
return fmt.Errorf("preferences cannot be nil")
}
// Validate power profile
validProfiles := []types.PowerProfile{
types.PowerProfileBalanced,
types.PowerProfilePerformance,
types.PowerProfilePowerSaving,
types.PowerProfileCustom,
}
validProfile := false
for _, profile := range validProfiles {
if preferences.PowerProfile == profile {
validProfile = true
break
}
}
if !validProfile {
return fmt.Errorf("invalid power profile: %s", preferences.PowerProfile)
}
// Validate use case
validUseCases := []types.UseCase{
types.UseCaseGeneral,
types.UseCaseDevelopment,
types.UseCaseGaming,
types.UseCaseServer,
types.UseCaseMultimedia,
types.UseCaseOffice,
}
validUseCase := false
for _, useCase := range validUseCases {
if preferences.UseCase == useCase {
validUseCase = true
break
}
}
if !validUseCase {
return fmt.Errorf("invalid use case: %s", preferences.UseCase)
}
// Check for conflicting preferences
if preferences.PowerProfile == types.PowerProfilePowerSaving &&
preferences.PerformanceMode == types.PerformanceModeMaximum {
return fmt.Errorf("conflicting preferences: power saving profile with maximum performance mode")
}
if preferences.UseCase == types.UseCaseGaming &&
preferences.PowerProfile == types.PowerProfilePowerSaving {
fmt.Println("⚠️ Warning: Gaming use case with power saving profile may impact performance")
}
return nil
}
// GetRecommendedSettings provides recommended settings based on preferences
func GetRecommendedSettings(preferences *types.UserPreferences) map[string]string {
settings := make(map[string]string)
// Base settings
settings["TLP_ENABLE"] = "1"
settings["TLP_WARN_LEVEL"] = "3"
// CPU settings based on power profile and use case
switch preferences.PowerProfile {
case types.PowerProfilePerformance:
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
case types.PowerProfilePowerSaving:
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "powersave"
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
default: // Balanced
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
}
// Adjust for use case
switch preferences.UseCase {
case types.UseCaseGaming:
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
settings["PLATFORM_PROFILE_ON_BAT"] = "balanced"
case types.UseCaseServer:
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
settings["PLATFORM_PROFILE_ON_BAT"] = "performance"
case types.UseCaseDevelopment:
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "ondemand"
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
}
// Disk settings based on battery priority
switch preferences.BatteryPriority {
case types.BatteryPriorityRuntime:
settings["DISK_APM_LEVEL_ON_BAT"] = "1"
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "60"
case types.BatteryPriorityLongevity:
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "0"
default: // Balanced
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "120"
}
settings["DISK_APM_LEVEL_ON_AC"] = "254"
settings["DISK_SPINDOWN_TIMEOUT_ON_AC"] = "0"
// Network settings
if utils.Contains(preferences.SpecialRequirements, "Maximum WiFi performance") {
settings["WIFI_PWR_ON_AC"] = "off"
settings["WIFI_PWR_ON_BAT"] = "off"
} else {
settings["WIFI_PWR_ON_AC"] = "off"
settings["WIFI_PWR_ON_BAT"] = "on"
}
// USB settings
settings["USB_AUTOSUSPEND"] = "1"
// Apply custom settings
for key, value := range preferences.CustomSettings {
switch key {
case "cpu_governor_ac":
settings["CPU_SCALING_GOVERNOR_ON_AC"] = fmt.Sprintf("%v", value)
case "cpu_governor_battery":
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = fmt.Sprintf("%v", value)
case "disk_apm_ac":
settings["DISK_APM_LEVEL_ON_AC"] = fmt.Sprintf("%v", value)
case "disk_apm_battery":
settings["DISK_APM_LEVEL_ON_BAT"] = fmt.Sprintf("%v", value)
}
}
return settings
}

View File

@@ -0,0 +1,162 @@
package config
import (
"testing"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
)
func TestValidatePreferences(t *testing.T) {
validPrefs := &types.UserPreferences{
PowerProfile: types.PowerProfileBalanced,
UseCase: types.UseCaseGeneral,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeAdaptive,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
if err := ValidatePreferences(validPrefs); err != nil {
t.Errorf("ValidatePreferences() with valid preferences failed: %v", err)
}
if err := ValidatePreferences(nil); err == nil {
t.Error("ValidatePreferences() with nil preferences should fail")
}
invalidPrefs := &types.UserPreferences{
PowerProfile: "invalid_profile",
UseCase: types.UseCaseGeneral,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeAdaptive,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
if err := ValidatePreferences(invalidPrefs); err == nil {
t.Error("ValidatePreferences() with invalid power profile should fail")
}
invalidPrefs2 := &types.UserPreferences{
PowerProfile: types.PowerProfileBalanced,
UseCase: "invalid_usecase",
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeAdaptive,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
if err := ValidatePreferences(invalidPrefs2); err == nil {
t.Error("ValidatePreferences() with invalid use case should fail")
}
// Test conflicting preferences
conflictingPrefs := &types.UserPreferences{
PowerProfile: types.PowerProfilePowerSaving,
UseCase: types.UseCaseGeneral,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeMaximum,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
if err := ValidatePreferences(conflictingPrefs); err == nil {
t.Error("ValidatePreferences() with conflicting preferences should fail")
}
}
func TestGetRecommendedSettings(t *testing.T) {
// Test performance profile
perfPrefs := &types.UserPreferences{
PowerProfile: types.PowerProfilePerformance,
UseCase: types.UseCaseGaming,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeMaximum,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
settings := GetRecommendedSettings(perfPrefs)
// Should have TLP_ENABLE
if settings["TLP_ENABLE"] != "1" {
t.Error("TLP_ENABLE should be set to '1'")
}
// Performance profile should use performance governor
if settings["CPU_SCALING_GOVERNOR_ON_AC"] != "performance" {
t.Error("Performance profile should use performance governor on AC")
}
// Test power saving profile
powerSavePrefs := &types.UserPreferences{
PowerProfile: types.PowerProfilePowerSaving,
UseCase: types.UseCaseOffice,
BatteryPriority: types.BatteryPriorityRuntime,
PerformanceMode: types.PerformanceModeEfficient,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{},
}
settings2 := GetRecommendedSettings(powerSavePrefs)
// Power saving should use powersave governor
if settings2["CPU_SCALING_GOVERNOR_ON_AC"] != "powersave" {
t.Error("Power saving profile should use powersave governor on AC")
}
if settings2["CPU_SCALING_GOVERNOR_ON_BAT"] != "powersave" {
t.Error("Power saving profile should use powersave governor on battery")
}
// Test custom settings
customPrefs := &types.UserPreferences{
PowerProfile: types.PowerProfileCustom,
UseCase: types.UseCaseGeneral,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeAdaptive,
CustomSettings: map[string]interface{}{
"cpu_governor_ac": "ondemand",
"cpu_governor_battery": "conservative",
"disk_apm_ac": "200",
"disk_apm_battery": "100",
},
SpecialRequirements: []string{},
}
settings3 := GetRecommendedSettings(customPrefs)
// Custom settings should override defaults
if settings3["CPU_SCALING_GOVERNOR_ON_AC"] != "ondemand" {
t.Error("Custom CPU governor AC setting should be applied")
}
if settings3["CPU_SCALING_GOVERNOR_ON_BAT"] != "conservative" {
t.Error("Custom CPU governor battery setting should be applied")
}
if settings3["DISK_APM_LEVEL_ON_AC"] != "200" {
t.Error("Custom disk APM AC setting should be applied")
}
if settings3["DISK_APM_LEVEL_ON_BAT"] != "100" {
t.Error("Custom disk APM battery setting should be applied")
}
// Test special requirements
wifiPrefs := &types.UserPreferences{
PowerProfile: types.PowerProfileBalanced,
UseCase: types.UseCaseGeneral,
BatteryPriority: types.BatteryPriorityBalanced,
PerformanceMode: types.PerformanceModeAdaptive,
CustomSettings: make(map[string]interface{}),
SpecialRequirements: []string{"Maximum WiFi performance"},
}
settings4 := GetRecommendedSettings(wifiPrefs)
// WiFi performance requirement should disable power saving
if settings4["WIFI_PWR_ON_AC"] != "off" || settings4["WIFI_PWR_ON_BAT"] != "off" {
t.Error("Maximum WiFi performance should disable WiFi power saving")
}
}

View File

@@ -0,0 +1,267 @@
package security
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"golang.org/x/term"
)
// KeyStore handles secure storage and retrieval of API keys
type KeyStore struct {
configDir string
}
// NewKeyStore creates a new KeyStore instance
func NewKeyStore() (*KeyStore, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
}
configDir := filepath.Join(homeDir, ".config", "autotlp")
if err := os.MkdirAll(configDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create config directory: %w", err)
}
return &KeyStore{
configDir: configDir,
}, nil
}
// StoreAPIKey securely stores an API key
func (ks *KeyStore) StoreAPIKey(provider, apiKey string) error {
if apiKey == "" {
return fmt.Errorf("API key cannot be empty")
}
// Get or create master password
masterPassword, err := ks.getMasterPassword()
if err != nil {
return fmt.Errorf("failed to get master password: %w", err)
}
// Encrypt the API key
encryptedKey, err := ks.encrypt(apiKey, masterPassword)
if err != nil {
return fmt.Errorf("failed to encrypt API key: %w", err)
}
// Store encrypted key
keyFile := filepath.Join(ks.configDir, provider+".key")
if err := os.WriteFile(keyFile, []byte(encryptedKey), 0600); err != nil {
return fmt.Errorf("failed to write key file: %w", err)
}
return nil
}
// RetrieveAPIKey retrieves and decrypts an API key
func (ks *KeyStore) RetrieveAPIKey(provider string) (string, error) {
keyFile := filepath.Join(ks.configDir, provider+".key")
// Check if key file exists
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
return "", fmt.Errorf("API key not found for provider: %s", provider)
}
// Read encrypted key
encryptedKey, err := os.ReadFile(keyFile)
if err != nil {
return "", fmt.Errorf("failed to read key file: %w", err)
}
// Get master password
masterPassword, err := ks.getMasterPassword()
if err != nil {
return "", fmt.Errorf("failed to get master password: %w", err)
}
// Decrypt the API key
apiKey, err := ks.decrypt(string(encryptedKey), masterPassword)
if err != nil {
return "", fmt.Errorf("failed to decrypt API key: %w", err)
}
return apiKey, nil
}
// DeleteAPIKey removes a stored API key
func (ks *KeyStore) DeleteAPIKey(provider string) error {
keyFile := filepath.Join(ks.configDir, provider+".key")
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to delete key file: %w", err)
}
return nil
}
// ListProviders returns a list of providers with stored keys
func (ks *KeyStore) ListProviders() ([]string, error) {
files, err := os.ReadDir(ks.configDir)
if err != nil {
return nil, fmt.Errorf("failed to read config directory: %w", err)
}
var providers []string
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".key" {
provider := file.Name()[:len(file.Name())-4] // Remove .key extension
providers = append(providers, provider)
}
}
return providers, nil
}
// getMasterPassword gets or creates a master password for encryption
func (ks *KeyStore) getMasterPassword() (string, error) {
passwordFile := filepath.Join(ks.configDir, ".master")
// Check if master password file exists
if _, err := os.Stat(passwordFile); os.IsNotExist(err) {
// Create new master password
fmt.Print("Create a master password to secure your API keys: ")
password, err := ks.readPassword()
if err != nil {
return "", fmt.Errorf("failed to read password: %w", err)
}
fmt.Print("Confirm master password: ")
confirmPassword, err := ks.readPassword()
if err != nil {
return "", fmt.Errorf("failed to read confirmation password: %w", err)
}
if password != confirmPassword {
return "", fmt.Errorf("passwords do not match")
}
// Hash and store the password
hashedPassword := ks.hashPassword(password)
if err := os.WriteFile(passwordFile, []byte(hashedPassword), 0600); err != nil {
return "", fmt.Errorf("failed to store master password: %w", err)
}
return password, nil
}
// Master password exists, prompt for it
fmt.Print("Enter master password: ")
password, err := ks.readPassword()
if err != nil {
return "", fmt.Errorf("failed to read password: %w", err)
}
// Verify password
storedHash, err := os.ReadFile(passwordFile)
if err != nil {
return "", fmt.Errorf("failed to read stored password hash: %w", err)
}
if ks.hashPassword(password) != string(storedHash) {
return "", fmt.Errorf("incorrect master password")
}
return password, nil
}
// readPassword reads a password from stdin without echoing
func (ks *KeyStore) readPassword() (string, error) {
password, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", err
}
fmt.Println() // Add newline after password input
return string(password), nil
}
// hashPassword creates a hash of the password for verification
func (ks *KeyStore) hashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return base64.StdEncoding.EncodeToString(hash[:])
}
// encrypt encrypts data using AES-GCM
func (ks *KeyStore) encrypt(plaintext, password string) (string, error) {
// Create cipher
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
// Create GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Generate nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// Encrypt
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// decrypt decrypts data using AES-GCM
func (ks *KeyStore) decrypt(ciphertext, password string) (string, error) {
// Decode base64
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
// Create cipher
key := sha256.Sum256([]byte(password))
block, err := aes.NewCipher(key[:])
if err != nil {
return "", err
}
// Create GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// Extract nonce
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", fmt.Errorf("ciphertext too short")
}
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
// Decrypt
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// SecureInput prompts for secure input without echoing
func SecureInput(prompt string) (string, error) {
fmt.Print(prompt)
input, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", err
}
fmt.Println() // Add newline
return string(input), nil
}

View File

@@ -0,0 +1,249 @@
package security
import (
"fmt"
"os"
"os/exec"
"syscall"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
)
// PrivilegeManager handles privilege escalation and security operations
type PrivilegeManager struct {
logger *utils.Logger
}
// NewPrivilegeManager creates a new privilege manager
func NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager {
return &PrivilegeManager{
logger: logger.WithComponent("security"),
}
}
// RequireRoot ensures the current operation has root privileges
func (pm *PrivilegeManager) RequireRoot(operation string) error {
if os.Geteuid() == 0 {
return nil
}
pm.logger.Info("Root privileges required", "operation", operation)
return fmt.Errorf("operation '%s' requires root privileges", operation)
}
// EscalateIfNeeded escalates privileges if not already running as root
func (pm *PrivilegeManager) EscalateIfNeeded(args []string, operation string) error {
if os.Geteuid() == 0 {
return nil
}
pm.logger.Info("Escalating privileges", "operation", operation)
// Check if sudo is available
if !utils.CommandExists("sudo") {
return fmt.Errorf("sudo is required for privilege escalation but not available")
}
// Inform user about privilege escalation
fmt.Printf("\nPrivilege escalation required for: %s\n", operation)
fmt.Println("This operation needs administrative privileges to:")
switch operation {
case "install_tlp":
fmt.Println("- Install TLP packages using system package manager")
fmt.Println("- Enable and start TLP systemd service")
fmt.Println("- Mask conflicting power management services")
case "apply_config":
fmt.Println("- Write TLP configuration to system directories")
fmt.Println("- Reload TLP service with new configuration")
case "system_info":
fmt.Println("- Access hardware information from system files")
fmt.Println("- Read power management settings")
default:
fmt.Printf("- Perform system operation: %s\n", operation)
}
if !utils.GetUserConfirmation("Continue with privilege escalation?") {
return fmt.Errorf("privilege escalation declined by user")
}
// Execute with sudo
return pm.executeSudo(args)
}
// executeSudo executes the current program with sudo
func (pm *PrivilegeManager) executeSudo(args []string) error {
// Get current executable path
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Prepare sudo command
sudoArgs := append([]string{executable}, args...)
cmd := exec.Command("sudo", sudoArgs...)
// Preserve environment variables that might be needed
cmd.Env = os.Environ()
// Connect stdio
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
pm.logger.Info("Executing with sudo", "command", cmd.String())
// Execute the command
err = cmd.Run()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// Get exit code from the elevated process
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
os.Exit(status.ExitStatus())
}
}
return fmt.Errorf("sudo execution failed: %w", err)
}
// If we reach here, the elevated process completed successfully
os.Exit(0)
return nil // This line will never be reached
}
// CheckSudoAccess verifies that the user can use sudo
func (pm *PrivilegeManager) CheckSudoAccess() error {
if os.Geteuid() == 0 {
return nil // Already root
}
if !utils.CommandExists("sudo") {
return fmt.Errorf("sudo command not available")
}
// Test sudo access with a harmless command
cmd := exec.Command("sudo", "-n", "true")
if err := cmd.Run(); err != nil {
// -n flag failed, user needs to authenticate
pm.logger.Debug("Sudo authentication required")
return nil // This is expected for most users
}
pm.logger.Debug("Sudo access confirmed without authentication")
return nil
}
// DropPrivileges drops root privileges if running as root
func (pm *PrivilegeManager) DropPrivileges() error {
if os.Geteuid() != 0 {
return nil // Not running as root
}
// Get the original user info from environment
sudoUID := os.Getenv("SUDO_UID")
sudoGID := os.Getenv("SUDO_GID")
if sudoUID == "" || sudoGID == "" {
pm.logger.Warn("Cannot drop privileges: SUDO_UID/SUDO_GID not set")
return nil // Don't fail, just log warning
}
pm.logger.Info("Dropping root privileges", "uid", sudoUID, "gid", sudoGID)
// This is a placeholder - actual privilege dropping requires careful handling
// and is typically done at the start of the program, not mid-execution
pm.logger.Debug("Privilege dropping not implemented for mid-execution")
return nil
}
// ValidateSystemAccess validates that the program has necessary system access
func (pm *PrivilegeManager) ValidateSystemAccess() error {
// Check read access to system information
systemPaths := []string{
"/proc/cpuinfo",
"/proc/meminfo",
"/sys/class/power_supply",
"/sys/devices/system/cpu",
}
for _, path := range systemPaths {
if utils.FileExists(path) {
// Try to read the file/directory
if file, err := os.Open(path); err != nil {
pm.logger.Warn("Limited access to system path", "path", path, "error", err)
} else {
file.Close()
}
}
}
return nil
}
// SecureFilePermissions sets secure permissions on configuration files
func (pm *PrivilegeManager) SecureFilePermissions(filePath string) error {
// Set file permissions to be readable only by owner and group
if err := os.Chmod(filePath, 0640); err != nil {
return fmt.Errorf("failed to set secure permissions on %s: %w", filePath, err)
}
pm.logger.Debug("Set secure file permissions", "file", filePath, "mode", "0640")
return nil
}
// ValidateInput performs basic input validation for security
func ValidateInput(input string, maxLength int, allowedChars string) error {
if len(input) > maxLength {
return fmt.Errorf("input too long: %d characters (max %d)", len(input), maxLength)
}
if len(input) == 0 {
return fmt.Errorf("input cannot be empty")
}
// Basic validation against null bytes and control characters
for i, r := range input {
if r == 0 {
return fmt.Errorf("null byte at position %d", i)
}
if r < 32 && r != 9 && r != 10 && r != 13 { // Allow tab, LF, CR
return fmt.Errorf("control character at position %d", i)
}
}
return nil
}
// SanitizeFilePath sanitizes file paths to prevent directory traversal
func SanitizeFilePath(path string) (string, error) {
// Basic path validation
if path == "" {
return "", fmt.Errorf("empty path")
}
// Check for directory traversal attempts
if len(path) > 1 && (path[0] == '/' || path[1] == ':') {
// Absolute path - this might be intentional, so we allow it
// but log it for security awareness
}
// Check for dangerous patterns
dangerousPatterns := []string{
"../",
"..\\",
"./",
".\\",
}
for _, pattern := range dangerousPatterns {
if len(path) >= len(pattern) {
for i := 0; i <= len(path)-len(pattern); i++ {
if path[i:i+len(pattern)] == pattern {
return "", fmt.Errorf("potentially dangerous path pattern: %s", pattern)
}
}
}
}
return path, nil
}

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
}

View 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")
}
}

358
internal/tlp/manager.go Normal file
View File

@@ -0,0 +1,358 @@
package tlp
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
)
type Manager struct {
logger *utils.Logger
}
func NewManager(logger *utils.Logger) *Manager {
return &Manager{
logger: logger.WithComponent("tlp"),
}
}
func (m *Manager) GetStatus(ctx context.Context) (*types.TLPStatus, error) {
status := &types.TLPStatus{
CurrentConfig: make(map[string]string),
}
if utils.CommandExists("tlp") {
status.Installed = true
if version, err := utils.RunCommand("tlp", "--version"); err == nil {
parts := strings.Fields(version)
if len(parts) >= 2 {
status.Version = parts[1]
}
}
if output, err := utils.RunCommand("systemctl", "is-active", "tlp"); err == nil {
status.Active = strings.TrimSpace(output) == "active"
}
}
configPaths := []string{
"/etc/tlp.conf",
"/etc/default/tlp",
}
for _, path := range configPaths {
if utils.FileExists(path) {
status.ConfigPath = path
status.ConfigExists = true
if info, err := os.Stat(path); err == nil {
modTime := info.ModTime()
status.LastModified = &modTime
}
if config, err := m.readConfig(path); err == nil {
status.CurrentConfig = config
}
break
}
}
return status, nil
}
func (m *Manager) Install(ctx context.Context, sysInfo *system.Info) error {
m.logger.Info("Installing TLP", "distro", sysInfo.Distribution, "package_manager", sysInfo.PackageManager)
if utils.CommandExists("tlp") {
return fmt.Errorf("TLP is already installed")
}
if !utils.IsRoot() {
return fmt.Errorf("root privileges required for TLP installation")
}
var installCmd []string
var updateCmd []string
switch sysInfo.PackageManager {
case "apt":
updateCmd = []string{"apt", "update"}
installCmd = []string{"apt", "install", "-y", "tlp", "tlp-rdw"}
case "dnf":
installCmd = []string{"dnf", "install", "-y", "tlp", "tlp-rdw"}
case "yum":
installCmd = []string{"yum", "install", "-y", "tlp", "tlp-rdw"}
case "zypper":
installCmd = []string{"zypper", "install", "-y", "tlp", "tlp-rdw"}
case "pacman":
updateCmd = []string{"pacman", "-Sy"}
installCmd = []string{"pacman", "-S", "--noconfirm", "tlp", "tlp-rdw"}
case "apk":
installCmd = []string{"apk", "add", "tlp"}
default:
return fmt.Errorf("unsupported package manager: %s", sysInfo.PackageManager)
}
if len(updateCmd) > 0 {
m.logger.Info("Updating package lists")
if _, err := utils.RunCommand(updateCmd[0], updateCmd[1:]...); err != nil {
m.logger.Warn("Failed to update package lists", "error", err)
}
}
m.logger.Info("Installing TLP packages", "command", strings.Join(installCmd, " "))
if _, err := utils.RunCommand(installCmd[0], installCmd[1:]...); err != nil {
return fmt.Errorf("failed to install TLP: %w", err)
}
m.logger.Info("Enabling TLP service")
if _, err := utils.RunCommand("systemctl", "enable", "tlp"); err != nil {
m.logger.Warn("Failed to enable TLP service", "error", err)
}
if _, err := utils.RunCommand("systemctl", "start", "tlp"); err != nil {
m.logger.Warn("Failed to start TLP service", "error", err)
}
if utils.CommandExists("systemctl") {
if _, err := utils.RunCommand("systemctl", "mask", "power-profiles-daemon"); err != nil {
m.logger.Debug("power-profiles-daemon not found or already masked")
}
}
m.logger.Info("TLP installation completed successfully")
return nil
}
func (m *Manager) ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error {
m.logger.Info("Applying TLP configuration")
if err := m.ValidateConfig(config); err != nil {
return fmt.Errorf("configuration validation failed: %w", err)
}
configPath := "/etc/tlp.conf"
if !utils.FileExists(configPath) {
configPath = "/etc/default/tlp"
}
if utils.FileExists(configPath) {
backupPath := fmt.Sprintf("%s.backup.%d", configPath, time.Now().Unix())
if err := m.backupConfig(configPath, backupPath); err != nil {
m.logger.Warn("Failed to backup existing configuration", "error", err)
} else {
m.logger.Info("Backed up existing configuration", "backup", backupPath)
}
}
if err := m.writeConfig(configPath, config); err != nil {
return fmt.Errorf("failed to write configuration: %w", err)
}
m.logger.Info("Reloading TLP configuration")
if _, err := utils.RunCommand("tlp", "start"); err != nil {
m.logger.Warn("Failed to reload TLP configuration", "error", err)
}
m.logger.Info("TLP configuration applied successfully")
return nil
}
func (m *Manager) ValidateConfig(config *types.TLPConfiguration) error {
if config == nil {
return fmt.Errorf("configuration is nil")
}
if len(config.Settings) == 0 {
return fmt.Errorf("configuration has no settings")
}
for key, value := range config.Settings {
if key == "" {
return fmt.Errorf("empty setting key found")
}
switch key {
case "TLP_ENABLE":
if value != "1" && value != "0" {
return fmt.Errorf("TLP_ENABLE must be 0 or 1, got: %s", value)
}
case "CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT":
validGovernors := []string{"performance", "powersave", "ondemand", "conservative", "schedutil"}
if !utils.Contains(validGovernors, value) {
return fmt.Errorf("invalid CPU governor: %s", value)
}
}
}
return nil
}
func (m *Manager) readConfig(configPath string) (map[string]string, error) {
config := make(map[string]string)
lines, err := utils.ReadFileLines(configPath)
if err != nil {
return nil, err
}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if key, value, ok := utils.ParseKeyValue(line); ok {
config[key] = value
}
}
return config, nil
}
func (m *Manager) writeConfig(configPath string, config *types.TLPConfiguration) error {
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open config file: %w", err)
}
defer file.Close()
fmt.Fprintf(file, "# TLP Configuration File\n")
fmt.Fprintf(file, "# Generated by WiseTLP on %s\n", time.Now().Format("2006-01-02 15:04:05"))
fmt.Fprintf(file, "# %s\n\n", config.Description)
for key, value := range config.Settings {
if rationale, exists := config.Rationale[key]; exists {
fmt.Fprintf(file, "# %s\n", rationale)
}
fmt.Fprintf(file, "%s=%s\n\n", key, value)
}
if len(config.Warnings) > 0 {
fmt.Fprintf(file, "# WARNINGS:\n")
for _, warning := range config.Warnings {
fmt.Fprintf(file, "# - %s\n", warning)
}
}
return nil
}
func (m *Manager) backupConfig(configPath, backupPath string) error {
input, err := os.ReadFile(configPath)
if err != nil {
return err
}
return os.WriteFile(backupPath, input, 0644)
}
type Configuration struct {
*types.TLPConfiguration
}
func (c *Configuration) Present() error {
fmt.Println("\n" + strings.Repeat("=", 60))
fmt.Println("GENERATED TLP CONFIGURATION")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("\nDescription: %s\n", c.Description)
if len(c.Warnings) > 0 {
fmt.Println("\n⚠ WARNINGS:")
for _, warning := range c.Warnings {
fmt.Printf(" - %s\n", warning)
}
}
fmt.Println("\nConfiguration Settings:")
fmt.Println(strings.Repeat("-", 40))
// Group settings by category for better presentation
categories := map[string][]string{
"General": {"TLP_ENABLE", "TLP_WARN_LEVEL", "TLP_DEBUG"},
"CPU": {"CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT",
"CPU_SCALING_MIN_FREQ_ON_AC", "CPU_SCALING_MAX_FREQ_ON_AC",
"CPU_SCALING_MIN_FREQ_ON_BAT", "CPU_SCALING_MAX_FREQ_ON_BAT"},
"Platform": {"PLATFORM_PROFILE_ON_AC", "PLATFORM_PROFILE_ON_BAT"},
"Disk": {"DISK_APM_LEVEL_ON_AC", "DISK_APM_LEVEL_ON_BAT",
"DISK_SPINDOWN_TIMEOUT_ON_AC", "DISK_SPINDOWN_TIMEOUT_ON_BAT"},
"Graphics": {"RADEON_DPM_STATE_ON_AC", "RADEON_DPM_STATE_ON_BAT"},
"Network": {"WIFI_PWR_ON_AC", "WIFI_PWR_ON_BAT"},
"USB": {"USB_AUTOSUSPEND", "USB_BLACKLIST"},
}
for category, keys := range categories {
hasSettings := false
var categorySettings []string
for _, key := range keys {
if value, exists := c.Settings[key]; exists {
if !hasSettings {
categorySettings = append(categorySettings, fmt.Sprintf("\n%s:", category))
hasSettings = true
}
rationale := ""
if r, exists := c.Rationale[key]; exists {
rationale = fmt.Sprintf(" (%s)", r)
}
categorySettings = append(categorySettings, fmt.Sprintf(" %s = %s%s", key, value, rationale))
}
}
if hasSettings {
for _, setting := range categorySettings {
fmt.Println(setting)
}
}
}
// Show any remaining settings not in categories
fmt.Println("\nOther Settings:")
for key, value := range c.Settings {
found := false
for _, keys := range categories {
if utils.Contains(keys, key) {
found = true
break
}
}
if !found {
rationale := ""
if r, exists := c.Rationale[key]; exists {
rationale = fmt.Sprintf(" (%s)", r)
}
fmt.Printf(" %s = %s%s\n", key, value, rationale)
}
}
fmt.Println(strings.Repeat("=", 60))
// Get user approval
fmt.Print("\nDo you want to apply this configuration? (y/N): ")
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response != "y" && response != "yes" {
return fmt.Errorf("configuration rejected by user")
}
return nil
}