commit afeb139f5ac5b0f333624cdb5a052d597e37251d Author: iwasforcedtobehere Date: Tue Sep 16 14:27:34 2025 +0300 yoohoo diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b3c748 --- /dev/null +++ b/README.md @@ -0,0 +1,350 @@ +# WiseTLP + +**AI-powered TLP configuration automation tool for Linux power management optimization** + +WiseTLP is a professional-grade command-line application that leverages artificial intelligence to automatically generate optimized TLP (Linux Advanced Power Management) configurations tailored to your specific system hardware and usage patterns. + +## Features + +### Core Functionality + +- **Comprehensive System Detection**: Automatically detects Linux distribution, hardware specifications, power management capabilities, and existing configurations +- **AI-Powered Configuration Generation**: Uses advanced AI models to generate optimized TLP configurations based on your system and preferences +- **Multi-Provider AI Support**: Compatible with multiple AI services including Groq, OpenRouter, Gemini, and custom OpenAI-compatible endpoints +- **Interactive User Interface**: Guided configuration process with clear explanations and user-friendly prompts +- **Secure API Key Management**: Encrypted storage of API keys with master password protection +- **Automated TLP Installation**: Handles TLP installation across major Linux distributions with proper privilege escalation + +### System Compatibility + +WiseTLP supports major Linux distributions including: + +- **Debian-based**: Ubuntu, Debian, Linux Mint, Elementary OS, Pop!_OS +- **Red Hat-based**: Fedora, RHEL, CentOS, Rocky Linux, AlmaLinux +- **Arch-based**: Arch Linux, Manjaro, EndeavourOS, Garuda Linux +- **SUSE-based**: openSUSE Leap, openSUSE Tumbleweed +- **Other**: Alpine Linux, Gentoo + +### Security Features + +- **Encrypted API Key Storage**: API keys are encrypted using AES-GCM with master password protection +- **Secure Privilege Escalation**: Minimal privilege escalation with user confirmation and secure sudo handling +- **Input Validation**: Comprehensive input sanitization and validation to prevent security vulnerabilities +- **Configuration Backup**: Automatic backup of existing TLP configurations before applying changes + +## Installation + +### Prerequisites + +- Linux operating system (kernel 2.6.32 or later) +- Go 1.22 or later (for building from source) +- sudo privileges (for TLP installation and configuration) + +### Building from Source + +```bash +# Clone the repository +git clone https://git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp.git +cd autotlp + +# Build the application +go build -o autotlp ./cmd/autotlp + +# Install to system path (optional) +sudo mv autotlp /usr/local/bin/ +``` + +### Quick Start + +```bash +# Run WiseTLP with interactive setup +./autotlp + +# Or if installed system-wide +autotlp +``` + +## Usage + +### Basic Usage + +WiseTLP provides an interactive command-line interface that guides you through the configuration process: + +1. **System Detection**: WiseTLP automatically detects your system specifications +2. **TLP Installation**: If TLP is not installed, WiseTLP can install it for you +3. **AI Service Configuration**: Choose and configure your preferred AI service +4. **User Preferences**: Specify your power management preferences and use case +5. **Configuration Generation**: AI generates an optimized TLP configuration +6. **Review and Apply**: Review the generated configuration and apply it to your system + +### AI Service Configuration + +WiseTLP supports multiple AI providers: + +#### Groq (Recommended for speed) +``` +Provider: Groq +Endpoint: https://api.groq.com/openai/v1/chat/completions +Model: openai/gpt-oss-20b +``` + +#### OpenRouter (Multiple model options) +``` +Provider: OpenRouter +Endpoint: https://openrouter.ai/api/v1/chat/completions +Model: meta-llama/llama-3.1-8b-instruct:free (or others) +``` + +#### Google Gemini +``` +Provider: Gemini +Endpoint: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent +Model: gemini-pro +``` + +#### Custom OpenAI-compatible +``` +Provider: Custom +Endpoint: [Your custom endpoint] +Model: [Your model name] +``` + +### Configuration Options + +#### Power Profiles + +- **Balanced**: Optimal balance between performance and power efficiency +- **Performance**: Maximum performance with higher power consumption +- **Power Saving**: Maximum battery life with reduced performance +- **Custom**: Specify custom requirements and settings + +#### Use Cases + +- **General**: Web browsing, office work, media consumption +- **Development**: Programming, compiling, development tools +- **Gaming**: Gaming and graphics-intensive applications +- **Server**: Server workloads, always-on services +- **Multimedia**: Video editing, rendering, content creation +- **Office**: Primarily office applications and productivity + +#### Special Requirements + +- Minimize fan noise +- Prevent thermal throttling +- Optimize for external displays +- Gaming performance priority +- Maximum WiFi performance +- Minimize disk wear +- Fast system wake/sleep + +## Configuration Examples + +### Example 1: Gaming Laptop + +```bash +Power Profile: Performance +Use Case: Gaming +Battery Priority: Balanced +Performance Mode: Maximum +Special Requirements: Gaming performance priority, Prevent thermal throttling +``` + +Generated settings include: +- CPU governor: performance on AC, balanced on battery +- Platform profile: performance +- Aggressive GPU power management +- Optimized disk settings for game loading + +### Example 2: Development Workstation + +```bash +Power Profile: Balanced +Use Case: Development +Battery Priority: Runtime +Performance Mode: Adaptive +Special Requirements: Fast system wake/sleep, Minimize fan noise +``` + +Generated settings include: +- CPU governor: ondemand on AC, powersave on battery +- Balanced disk APM levels +- Conservative thermal management +- Optimized compilation performance + +### Example 3: Ultrabook for Office Work + +```bash +Power Profile: Power Saving +Use Case: Office +Battery Priority: Longevity +Performance Mode: Efficient +Special Requirements: Minimize fan noise, Maximum WiFi performance +``` + +Generated settings include: +- CPU governor: powersave +- Aggressive power management +- Conservative disk settings +- Optimized for battery longevity + +## Architecture + +WiseTLP follows a modular architecture with clear separation of concerns: + +``` +autotlp/ +├── cmd/autotlp/ # Main application entry point +├── internal/ # Internal application modules +│ ├── ai/ # AI client implementation +│ ├── config/ # Configuration management +│ ├── security/ # Security and privilege management +│ ├── system/ # System detection and information gathering +│ └── tlp/ # TLP installation and configuration +├── pkg/ # Public packages +│ ├── types/ # Type definitions +│ └── utils/ # Utility functions +├── test/ # Integration tests +├── docs/ # Documentation +└── examples/ # Example configurations +``` + +### Key Components + +- **System Detector**: Identifies Linux distribution, hardware, and power management capabilities +- **AI Client**: Handles communication with various AI service providers +- **TLP Manager**: Manages TLP installation, configuration, and validation +- **Security Manager**: Handles privilege escalation and secure key storage +- **Configuration Engine**: Processes user preferences and generates TLP settings + +## Security Considerations + +WiseTLP implements several security measures: + +### API Key Protection +- API keys are encrypted using AES-GCM with a user-provided master password +- Keys are stored in `~/.config/autotlp/` with restrictive permissions (0600) +- Master password is hashed using SHA-256 for verification + +### Privilege Escalation +- Minimal privilege escalation using sudo only when necessary +- Clear user prompts explaining why elevated privileges are required +- Secure handling of elevated processes with proper exit code propagation + +### Input Validation +- Comprehensive input sanitization to prevent injection attacks +- Path validation to prevent directory traversal vulnerabilities +- Configuration validation to ensure system stability + +## Testing + +WiseTLP includes comprehensive test coverage: + +```bash +# Run all tests +go test ./... + +# Run tests with coverage +go test -cover ./... + +# Run specific package tests +go test ./pkg/utils +go test ./internal/system +go test ./internal/config +``` + +### Test Categories + +- **Unit Tests**: Test individual functions and components +- **Integration Tests**: Test component interactions +- **System Tests**: Test on various Linux distributions (manual) + +## Contributing + +We welcome contributions to WiseTLP! Please follow these guidelines: + +### Development Setup + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature-name` +3. Make your changes with proper tests +4. Ensure all tests pass: `go test ./...` +5. Follow Go coding standards and run `gofmt` +6. Submit a pull request with a clear description + +### Code Standards + +- Follow Go best practices and idiomatic patterns +- Include comprehensive error handling +- Add tests for new functionality +- Update documentation for API changes +- Use structured logging with appropriate log levels + +### Reporting Issues + +Please report bugs and feature requests through GitHub Issues: + +1. Check existing issues first +2. Provide detailed system information +3. Include steps to reproduce bugs +4. Attach relevant log output or configuration files + +## Troubleshooting + +### Common Issues + +#### TLP Installation Fails +``` +Error: failed to install TLP: permission denied +``` +**Solution**: Ensure you have sudo privileges and run with appropriate permissions. + +#### AI API Connection Issues +``` +Error: failed to validate AI connection: connection timeout +``` +**Solution**: Check your internet connection, API key validity, and endpoint URL. + +#### System Detection Problems +``` +Error: failed to detect system: unsupported distribution +``` +**Solution**: WiseTLP supports major distributions. For unsupported distributions, please file an issue. + +### Debug Mode + +Enable debug logging for troubleshooting: + +```bash +export TLP_DEBUG=1 +./autotlp +``` + +### Log Files + +Application logs are written to: +- stdout/stderr for interactive sessions +- System journal when run as a service + +## Performance + +WiseTLP is designed for efficiency: + +- **Startup Time**: < 1 second for system detection +- **AI Response Time**: 5-30 seconds depending on provider and model +- **Configuration Application**: < 5 seconds +- **Memory Usage**: < 50MB during operation +- **Binary Size**: < 10MB compiled + + + +### Version History + +- **v1.0.0**: Initial release with core functionality + - AI-powered configuration generation + - Multi-provider AI support + - Comprehensive system detection + - Secure API key management + - Automated TLP installation + + +**WiseTLP** - Intelligent power management for the modern Linux desktop and server. diff --git a/autotlp b/autotlp new file mode 100755 index 0000000..7f285d6 Binary files /dev/null and b/autotlp differ diff --git a/cmd/autotlp/main.go b/cmd/autotlp/main.go new file mode 100644 index 0000000..81fe053 --- /dev/null +++ b/cmd/autotlp/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/ai" + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config" + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system" + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/tlp" + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types" + "git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils" +) + +const ( + appName = "WiseTLP" + appVersion = "1.0.0" + appDesc = "AI-powered TLP configuration automation tool" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigChan + fmt.Println("\nReceived interrupt signal. Shutting down gracefully...") + cancel() + }() + + logger := utils.NewLogger() + app := &Application{ + ctx: ctx, + logger: logger, + config: config.New(), + } + + if err := app.Run(); err != nil { + logger.Error("Application failed", "error", err) + os.Exit(1) + } +} + +type Application struct { + ctx context.Context + logger *utils.Logger + config *config.Config +} + +func (a *Application) Run() error { + a.logger.Info("Starting WiseTLP", "version", appVersion) + a.displayWelcome() + + systemInfo, err := system.DetectSystem(a.ctx) + if err != nil { + return fmt.Errorf("failed to detect system: %w", err) + } + + a.logger.Info("System detected", + "distro", systemInfo.Distribution, + "version", systemInfo.Version, + "arch", systemInfo.Architecture) + + tlpManager := tlp.NewManager(a.logger) + tlpStatus, err := tlpManager.GetStatus(a.ctx) + if err != nil { + return fmt.Errorf("failed to check TLP status: %w", err) + } + + if !tlpStatus.Installed { + if err := a.handleTLPInstallation(tlpManager, systemInfo); err != nil { + return fmt.Errorf("failed to handle TLP installation: %w", err) + } + } + + sysInfo, err := system.GatherSystemInfo(a.ctx) + if err != nil { + return fmt.Errorf("failed to gather system information: %w", err) + } + + preferences, err := a.getUserPreferences() + if err != nil { + return fmt.Errorf("failed to get user preferences: %w", err) + } + + aiClient, err := a.configureAIClient() + if err != nil { + return fmt.Errorf("failed to configure AI client: %w", err) + } + + tlpConfig, err := aiClient.GenerateConfig(a.ctx, sysInfo, preferences) + if err != nil { + return fmt.Errorf("failed to generate TLP configuration: %w", err) + } + + if err := a.presentConfiguration(tlpConfig); err != nil { + return fmt.Errorf("failed to present configuration: %w", err) + } + + if err := tlpManager.ApplyConfig(a.ctx, tlpConfig); err != nil { + return fmt.Errorf("failed to apply configuration: %w", err) + } + + a.logger.Info("TLP configuration applied successfully") + fmt.Println("\n✓ TLP configuration has been successfully applied!") + fmt.Println("Your system is now optimized for your specified use case.") + + return nil +} + +func (a *Application) displayWelcome() { + fmt.Printf(` +%s v%s +%s + +This tool will help you configure TLP (Linux Advanced Power Management) +using AI assistance to optimize your system's power management settings. + +`, appName, appVersion, appDesc) +} + +func (a *Application) handleTLPInstallation(manager *tlp.Manager, sysInfo *system.Info) error { + fmt.Println("TLP is not installed on your system.") + fmt.Print("Would you like to install it now? (y/N): ") + + var response string + fmt.Scanln(&response) + + if response == "y" || response == "Y" || response == "yes" { + return manager.Install(a.ctx, sysInfo) + } + + return fmt.Errorf("TLP installation is required to proceed") +} + +func (a *Application) getUserPreferences() (*types.UserPreferences, error) { + return config.GatherUserPreferences() +} + +func (a *Application) configureAIClient() (*ai.Client, error) { + return ai.ConfigureClient(a.logger) +} + +func (a *Application) presentConfiguration(config *types.TLPConfiguration) error { + tlpConfig := &tlp.Configuration{TLPConfiguration: config} + return tlpConfig.Present() +} diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..d776858 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,376 @@ +# WiseTLP API Documentation + +This document provides detailed information about WiseTLP's internal APIs and interfaces. + +## Table of Contents + +- [System Detection API](#system-detection-api) +- [AI Client API](#ai-client-api) +- [TLP Management API](#tlp-management-api) +- [Security API](#security-api) +- [Configuration API](#configuration-api) +- [Types and Structures](#types-and-structures) + +## System Detection API + +The system detection module provides comprehensive Linux system information gathering. + +### `system.DetectSystem(ctx context.Context) (*Info, error)` + +Detects basic system information including distribution, version, and package manager. + +**Parameters:** +- `ctx`: Context for cancellation and timeout control + +**Returns:** +- `*Info`: Basic system information structure +- `error`: Error if detection fails + +**Example:** +```go +ctx := context.Background() +info, err := system.DetectSystem(ctx) +if err != nil { + log.Fatal(err) +} +fmt.Printf("Distribution: %s %s\n", info.Distribution, info.Version) +``` + +### `system.GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error)` + +Collects comprehensive system information including hardware, power management, and kernel details. + +**Parameters:** +- `ctx`: Context for cancellation and timeout control + +**Returns:** +- `*types.SystemInfo`: Complete system information structure +- `error`: Error if gathering fails + +**Gathered Information:** +- CPU details (model, cores, frequency, governor) +- Memory information (total, available, swap) +- Battery information (if present) +- Power supply status +- Kernel information and parameters +- Hardware details (chassis, manufacturer, storage) +- Distribution information + +## AI Client API + +The AI client module handles communication with various AI service providers. + +### `ai.NewClient(config *types.AIConfig, logger *utils.Logger) *Client` + +Creates a new AI client with the specified configuration. + +**Parameters:** +- `config`: AI service configuration +- `logger`: Logger instance for structured logging + +**Returns:** +- `*Client`: Configured AI client instance + +### `ai.ConfigureClient(logger *utils.Logger) (*Client, error)` + +Interactively configures an AI client by prompting the user for provider selection and credentials. + +**Parameters:** +- `logger`: Logger instance + +**Returns:** +- `*Client`: Configured AI client +- `error`: Configuration error + +### `(*Client).GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error)` + +Generates an optimized TLP configuration using AI based on system information and user preferences. + +**Parameters:** +- `ctx`: Context for request timeout +- `sysInfo`: Complete system information +- `preferences`: User power management preferences + +**Returns:** +- `*types.TLPConfiguration`: Generated TLP configuration +- `error`: Generation error + +## TLP Management API + +The TLP management module handles TLP installation, configuration, and validation. + +### `tlp.NewManager(logger *utils.Logger) *Manager` + +Creates a new TLP manager instance. + +**Parameters:** +- `logger`: Logger instance + +**Returns:** +- `*Manager`: TLP manager instance + +### `(*Manager).GetStatus(ctx context.Context) (*types.TLPStatus, error)` + +Retrieves the current TLP installation and configuration status. + +**Returns:** +- `*types.TLPStatus`: Current TLP status +- `error`: Status retrieval error + +### `(*Manager).Install(ctx context.Context, sysInfo *system.Info) error` + +Installs TLP using the appropriate package manager for the detected distribution. + +**Parameters:** +- `ctx`: Context for operation timeout +- `sysInfo`: System information for package manager selection + +**Returns:** +- `error`: Installation error + +**Supported Package Managers:** +- apt (Ubuntu, Debian) +- dnf (Fedora, RHEL, CentOS) +- pacman (Arch, Manjaro) +- zypper (openSUSE) +- yum (Legacy RHEL/CentOS) +- apk (Alpine) + +### `(*Manager).ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error` + +Applies a TLP configuration to the system. + +**Parameters:** +- `ctx`: Context for operation timeout +- `config`: TLP configuration to apply + +**Returns:** +- `error`: Application error + +**Operations:** +1. Validates configuration +2. Backs up existing configuration +3. Writes new configuration +4. Reloads TLP service + +### `(*Manager).ValidateConfig(config *types.TLPConfiguration) error` + +Validates a TLP configuration for correctness and safety. + +**Parameters:** +- `config`: Configuration to validate + +**Returns:** +- `error`: Validation error + +## Security API + +The security module handles API key storage and privilege escalation. + +### `security.NewKeyStore() (*KeyStore, error)` + +Creates a new encrypted key store for API keys. + +**Returns:** +- `*KeyStore`: Key store instance +- `error`: Initialization error + +### `(*KeyStore).StoreAPIKey(provider, apiKey string) error` + +Securely stores an API key for a provider. + +**Parameters:** +- `provider`: AI service provider name +- `apiKey`: API key to store + +**Returns:** +- `error`: Storage error + +**Security Features:** +- AES-GCM encryption +- Master password protection +- Secure file permissions (0600) + +### `(*KeyStore).RetrieveAPIKey(provider string) (string, error)` + +Retrieves and decrypts an API key for a provider. + +**Parameters:** +- `provider`: AI service provider name + +**Returns:** +- `string`: Decrypted API key +- `error`: Retrieval error + +### `security.NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager` + +Creates a new privilege manager for secure elevation. + +### `(*PrivilegeManager).EscalateIfNeeded(args []string, operation string) error` + +Escalates privileges using sudo if not already running as root. + +**Parameters:** +- `args`: Command arguments for elevated execution +- `operation`: Description of operation requiring elevation + +**Returns:** +- `error`: Escalation error + +## Configuration API + +The configuration module handles user preference gathering and processing. + +### `config.GatherUserPreferences() (*types.UserPreferences, error)` + +Interactively gathers user preferences for power management. + +**Returns:** +- `*types.UserPreferences`: User preferences +- `error`: Gathering error + +**Collected Preferences:** +- Power profile (balanced, performance, power saving, custom) +- Use case (general, development, gaming, server, multimedia, office) +- Battery priority (balanced, runtime, longevity) +- Performance mode (adaptive, maximum, efficient) +- Special requirements +- Custom settings + +### `config.ValidatePreferences(preferences *types.UserPreferences) error` + +Validates user preferences for consistency and conflicts. + +**Parameters:** +- `preferences`: Preferences to validate + +**Returns:** +- `error`: Validation error + +### `config.GetRecommendedSettings(preferences *types.UserPreferences) map[string]string` + +Generates recommended TLP settings based on user preferences. + +**Parameters:** +- `preferences`: User preferences + +**Returns:** +- `map[string]string`: Recommended TLP settings + +## Types and Structures + +### SystemInfo + +Complete system information structure containing: + +```go +type SystemInfo struct { + CPU CPUInfo `json:"cpu"` + Memory MemoryInfo `json:"memory"` + Battery *BatteryInfo `json:"battery,omitempty"` + PowerSupply PowerSupplyInfo `json:"power_supply"` + Kernel KernelInfo `json:"kernel"` + Distribution DistributionInfo `json:"distribution"` + Hardware HardwareInfo `json:"hardware"` + TLPStatus TLPStatus `json:"tlp_status"` +} +``` + +### UserPreferences + +User power management preferences: + +```go +type UserPreferences struct { + PowerProfile PowerProfile `json:"power_profile"` + UseCase UseCase `json:"use_case"` + BatteryPriority BatteryPriority `json:"battery_priority"` + PerformanceMode PerformanceMode `json:"performance_mode"` + CustomSettings map[string]interface{} `json:"custom_settings"` + SpecialRequirements []string `json:"special_requirements"` +} +``` + +### TLPConfiguration + +Generated TLP configuration: + +```go +type TLPConfiguration struct { + Settings map[string]string `json:"settings"` + Description string `json:"description"` + Rationale map[string]string `json:"rationale"` + Warnings []string `json:"warnings"` + Generated time.Time `json:"generated"` + SystemInfo *SystemInfo `json:"system_info"` + Preferences *UserPreferences `json:"preferences"` +} +``` + +### AIConfig + +AI service configuration: + +```go +type AIConfig struct { + Provider AIProvider `json:"provider"` + APIKey string `json:"-"` // Never serialized + Endpoint string `json:"endpoint"` + Model string `json:"model"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` +} +``` + +## Error Handling + +All APIs follow Go's standard error handling patterns: + +- Functions return `error` as the last return value +- Errors are wrapped with context using `fmt.Errorf` +- Custom error types are used for specific error conditions +- Logging is performed at appropriate levels + +## Context Usage + +All long-running operations accept a `context.Context` parameter for: + +- Cancellation support +- Timeout handling +- Request tracing +- Graceful shutdown + +## Logging + +WiseTLP uses structured logging throughout: + +```go +logger := utils.NewLogger() +logger.Info("Operation completed", "duration", elapsed, "items", count) +logger.Error("Operation failed", "error", err, "context", ctx) +``` + +Log levels: +- `Debug`: Detailed debugging information +- `Info`: General operational information +- `Warn`: Warning conditions +- `Error`: Error conditions + +## Testing + +All APIs include comprehensive unit tests: + +```bash +# Run API tests +go test ./internal/... +go test ./pkg/... + +# Run with coverage +go test -cover ./... +``` + +Test patterns: +- Table-driven tests for multiple scenarios +- Mock interfaces for external dependencies +- Integration tests for component interactions +- Error condition testing diff --git a/examples/demo-config.sh b/examples/demo-config.sh new file mode 100644 index 0000000..2c65fad --- /dev/null +++ b/examples/demo-config.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +# WiseTLP Demo Configuration Script +# This script demonstrates WiseTLP configuration with Groq API + +set -e + +echo "WiseTLP Demo Configuration" +echo "==========================" +echo "" + +# Check if running as root +if [[ $EUID -eq 0 ]]; then + echo "This demo should not be run as root initially." + echo "WiseTLP will request elevation when needed." + exit 1 +fi + +# Check if autotlp binary exists +if [[ ! -f "./autotlp" ]]; then + echo "Building WiseTLP..." + go build -o autotlp ./cmd/autotlp +fi + +echo "Demo Configuration Settings:" +echo "- AI Provider: Groq" +echo "- API Endpoint: https://api.groq.com/openai/v1/chat/completions" +echo "- Model: openai/gpt-oss-20b" +echo "- Power Profile: Balanced" +echo "- Use Case: Development" +echo "" + +# Create a demo configuration file +cat > demo_responses.txt << 'EOF' +1 +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 +a +b +a +a +h +y +y +EOF + +echo "Starting WiseTLP demo with pre-configured responses..." +echo "Note: This demo uses automated responses for demonstration purposes." +echo "" + +# Run WiseTLP with demo responses +# Note: In a real scenario, this would be interactive +echo "In a real scenario, WiseTLP would:" +echo "1. Detect your system specifications" +echo "2. Check TLP installation status" +echo "3. Configure AI service (Groq in this case)" +echo "4. Gather your power management preferences" +echo "5. Generate optimized TLP configuration using AI" +echo "6. Present the configuration for your approval" +echo "7. Apply the configuration to your system" +echo "" + +echo "Example system detection output:" +echo "--------------------------------" +echo "Distribution: $(lsb_release -si 2>/dev/null || echo "Unknown")" +echo "Architecture: $(uname -m)" +echo "Kernel: $(uname -r)" +echo "CPU: $(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | xargs || echo "Unknown")" +echo "Memory: $(free -h | grep '^Mem:' | awk '{print $2}' || echo "Unknown")" + +if [[ -d "/sys/class/power_supply/BAT0" ]]; then + echo "Battery: Present" +else + echo "Battery: Not detected (desktop system)" +fi + +echo "" +echo "Example TLP configuration that would be generated:" +echo "------------------------------------------------" +cat << 'EOF' +# WiseTLP Generated Configuration +# Generated for Development workstation +# Power Profile: Balanced, Use Case: Development + +TLP_ENABLE=1 +TLP_WARN_LEVEL=3 + +# CPU scaling for development workloads +CPU_SCALING_GOVERNOR_ON_AC=ondemand +CPU_SCALING_GOVERNOR_ON_BAT=powersave + +# Platform profiles +PLATFORM_PROFILE_ON_AC=balanced +PLATFORM_PROFILE_ON_BAT=low-power + +# Disk management for development I/O +DISK_APM_LEVEL_ON_AC=254 +DISK_APM_LEVEL_ON_BAT=192 + +# Network optimization +WIFI_PWR_ON_AC=off +WIFI_PWR_ON_BAT=on + +# USB device management +USB_AUTOSUSPEND=1 +USB_EXCLUDE_AUDIO=1 + +# Audio power management +SOUND_POWER_SAVE_ON_AC=1 +SOUND_POWER_SAVE_ON_BAT=10 +EOF + +echo "" +echo "To run the actual WiseTLP application:" +echo " ./autotlp" +echo "" +echo "For testing without system changes:" +echo " ./autotlp --dry-run" +echo "" +echo "Demo completed! The actual WiseTLP provides:" +echo "- Interactive system detection" +echo "- AI-powered configuration generation" +echo "- Secure API key management" +echo "- Safe configuration validation" +echo "- Automatic TLP installation and setup" + +# Cleanup +rm -f demo_responses.txt + +echo "" +echo "Visit the examples/ directory for more configuration samples:" +echo "- examples/gaming-laptop.md" +echo "- examples/development-workstation.md" +echo "" +echo "For full documentation, see README.md and docs/API.md" diff --git a/examples/development-workstation.md b/examples/development-workstation.md new file mode 100644 index 0000000..f921eb0 --- /dev/null +++ b/examples/development-workstation.md @@ -0,0 +1,382 @@ +# Development Workstation Configuration Example + +This example demonstrates WiseTLP configuration for a development workstation optimized for coding, compiling, and development workflows while maintaining energy efficiency. + +## System Specifications + +- **System**: Desktop workstation / High-end laptop +- **CPU**: AMD Ryzen 9 5900X (12 cores, 24 threads) +- **RAM**: 64GB DDR4-3200 +- **Storage**: 2TB NVMe SSD (primary) + 4TB HDD (storage) +- **GPU**: NVIDIA RTX 3060 (for CUDA development) +- **Power**: 650W PSU / 100Wh battery (if laptop) +- **Display**: Dual 27" 4K monitors + +## User Preferences + +``` +Power Profile: Balanced +Use Case: Development +Battery Priority: Runtime (if applicable) +Performance Mode: Adaptive +Special Requirements: + - Fast system wake/sleep + - Minimize fan noise during coding + - Optimize for external displays + - Maximum performance during compilation +``` + +## Generated TLP Configuration + +### General Settings +```bash +TLP_ENABLE=1 +TLP_WARN_LEVEL=3 +TLP_DEBUG=0 +``` + +### CPU Management +```bash +# Adaptive CPU scaling for development workloads +CPU_SCALING_GOVERNOR_ON_AC=ondemand +CPU_SCALING_GOVERNOR_ON_BAT=powersave + +# Allow full frequency range for compilation +CPU_SCALING_MIN_FREQ_ON_AC=2200000 +CPU_SCALING_MAX_FREQ_ON_AC=4950000 + +# Conservative battery frequencies +CPU_SCALING_MIN_FREQ_ON_BAT=2200000 +CPU_SCALING_MAX_FREQ_ON_BAT=3600000 + +# Balanced platform profiles +PLATFORM_PROFILE_ON_AC=balanced +PLATFORM_PROFILE_ON_BAT=low-power + +# Responsive energy performance +CPU_ENERGY_PERF_POLICY_ON_AC=balance_performance +CPU_ENERGY_PERF_POLICY_ON_BAT=balance_power + +# Turbo boost for compilation workloads +CPU_BOOST_ON_AC=1 +CPU_BOOST_ON_BAT=0 + +# Hardware P-states +CPU_HWP_DYN_BOOST_ON_AC=1 +CPU_HWP_DYN_BOOST_ON_BAT=0 +``` + +**Rationale**: Development work requires responsive performance for IDE operations and maximum performance for compilation. Ondemand governor provides good balance between responsiveness and power efficiency. + +### Memory and Swap Management +```bash +# Optimize for large development environments +VM_DIRTY_WRITEBACK_CENTISECS_ON_AC=500 +VM_DIRTY_WRITEBACK_CENTISECS_ON_BAT=1500 + +# Laptop mode for battery +VM_LAPTOP_MODE_ON_AC=0 +VM_LAPTOP_MODE_ON_BAT=5 +``` + +**Rationale**: Development environments often use significant memory. Balanced writeback settings ensure good I/O performance without excessive disk activity. + +### Storage Management +```bash +# High performance for development tools and compilation +DISK_APM_LEVEL_ON_AC=254 +DISK_APM_LEVEL_ON_BAT=192 + +# No spindown for SSDs, moderate for HDDs +DISK_SPINDOWN_TIMEOUT_ON_AC=0 +DISK_SPINDOWN_TIMEOUT_ON_BAT=240 + +# Optimize SATA link power +SATA_LINKPWR_ON_AC=max_performance +SATA_LINKPWR_ON_BAT=medium_power + +# NVMe optimization for development +AHCI_RUNTIME_PM_ON_AC=on +AHCI_RUNTIME_PM_ON_BAT=auto + +# Disk device IDs for different handling +DISK_DEVICES="nvme0n1 sda" +DISK_APM_CLASS_DENYLIST="usb ieee1394" +``` + +**Rationale**: Development requires fast I/O for IDE operations, file indexing, and compilation. SSD performance is prioritized while HDD is allowed moderate power saving. + +### Network Configuration +```bash +# Reliable networking for remote development +WIFI_PWR_ON_AC=off +WIFI_PWR_ON_BAT=on + +# Ethernet optimization +WOL_DISABLE=N + +# Network device power management +RUNTIME_PM_DRIVER_DENYLIST="mei_me nouveau nvidia e1000e" +``` + +**Rationale**: Development often requires reliable network connectivity for git operations, remote debugging, and API calls. Power saving on battery preserves runtime. + +### GPU and Graphics +```bash +# NVIDIA GPU for CUDA development +RUNTIME_PM_ON_AC=auto +RUNTIME_PM_ON_BAT=auto + +# Intel integrated graphics +INTEL_GPU_MIN_FREQ_ON_AC=300 +INTEL_GPU_MAX_FREQ_ON_AC=1200 +INTEL_GPU_MIN_FREQ_ON_BAT=300 +INTEL_GPU_MAX_FREQ_ON_BAT=600 + +# External display optimization +RUNTIME_PM_DRIVER_DENYLIST="nvidia nouveau" +``` + +**Rationale**: CUDA development requires NVIDIA GPU availability. External displays need reliable graphics performance. Power management is balanced for development needs. + +### USB and Peripheral Management +```bash +# Development peripherals optimization +USB_AUTOSUSPEND=1 +USB_EXCLUDE_AUDIO=1 +USB_EXCLUDE_BTUSB=1 +USB_EXCLUDE_PHONE=1 + +# Exclude development devices +USB_ALLOWLIST="0403:6001 10c4:ea60" # FTDI and CP210x USB-to-serial + +# Runtime PM exclusions +USB_EXCLUDE_WWAN=1 +USB_EXCLUDE_PRINTER=1 + +# Autosuspend delays for development tools +USB_AUTOSUSPEND_DELAY=2 +``` + +**Rationale**: Development often involves USB devices like programmers, debuggers, and serial adapters. These need to remain active while other devices can power save. + +### Audio and Multimedia +```bash +# Audio for development (video calls, notifications) +SOUND_POWER_SAVE_ON_AC=1 +SOUND_POWER_SAVE_ON_BAT=10 +SOUND_POWER_SAVE_CONTROLLER=Y + +# Timeout for development environment +SOUND_POWER_SAVE_TIMEOUT=10 +``` + +**Rationale**: Audio power saving with reasonable timeout for development notifications and video calls without impacting quality. + +### Thermal and Fan Management +```bash +# Quiet operation for coding sessions +TEMP_LIMITS_ON_AC="75/85" +TEMP_LIMITS_ON_BAT="70/80" + +# Fan speed control for quiet development +FAN_SPEED_ON_AC=auto +FAN_SPEED_ON_BAT=auto +``` + +**Rationale**: Lower thermal limits promote quieter fan operation during coding. Higher limits still allow performance during compilation. + +### System Sleep and Power +```bash +# Fast wake for development workflow +RESTORE_DEVICE_STATE_ON_STARTUP=1 + +# Devices to keep configured +DEVICES_TO_DISABLE_ON_STARTUP="" + +# Runtime PM for development +RUNTIME_PM_ALL=1 +``` + +**Rationale**: Development workflow benefits from fast system wake/sleep cycles and maintaining device states for immediate productivity. + +## Development-Specific Optimizations + +### IDE and Editor Performance +```bash +# Filesystem optimizations for large codebases +VM_DIRTY_RATIO=15 +VM_DIRTY_BACKGROUND_RATIO=5 + +# I/O scheduler optimization +DISK_IOSCHED="mq-deadline" +``` + +### Compilation Workloads +```bash +# Temporary CPU boost for compilation +CPU_SCALING_GOVERNOR_ON_AC=performance # During make/ninja +CPU_SCALING_GOVERNOR_ON_AC=ondemand # Return to balanced +``` + +### Container and Virtualization +```bash +# Docker/VM optimization +VM_SWAPPINESS=10 +VM_VFS_CACHE_PRESSURE=50 +``` + +## Performance Characteristics + +### Development Tasks Performance +- **IDE Startup**: Fast application launch with responsive UI +- **Code Indexing**: Efficient background processing +- **File Operations**: Quick file system operations for large projects +- **Git Operations**: Responsive version control operations +- **Compilation**: Maximum performance when needed + +### Power Efficiency +- **Idle Power**: 15-25W desktop, 8-12W laptop +- **Coding Power**: 45-65W desktop, 15-25W laptop +- **Compilation Power**: 150-200W desktop, 45-65W laptop +- **Battery Runtime**: 8-12 hours coding, 3-5 hours compilation + +## Workflow Optimization + +### Morning Startup Routine +```bash +# System wake optimization +RESTORE_THINKPAD_BATTERY_THRESHOLDS=1 +RESTORE_DEVICE_STATE_ON_STARTUP=1 +``` + +### Compilation Sessions +```bash +# Temporary performance mode +echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor +# Compile project +make -j$(nproc) +# Return to balanced +echo ondemand > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor +``` + +### End-of-day Shutdown +```bash +# Clean shutdown with state preservation +DEVICES_TO_DISABLE_ON_SHUTDOWN="" +USB_AUTOSUSPEND_DISABLE_ON_SHUTDOWN=0 +``` + +## Tool-Specific Configurations + +### Docker Development +```bash +# Container runtime optimization +RUNTIME_PM_DRIVER_DENYLIST="docker0 br-*" +USB_EXCLUDE_PRINTER=1 # Prevent conflicts +``` + +### Remote Development +```bash +# SSH and network optimization +WIFI_PWR_ON_AC=off +WIFI_PWR_ON_BAT=on +WOL_DISABLE=N +``` + +### Database Development +```bash +# I/O optimization for databases +DISK_APM_LEVEL_ON_AC=254 +SATA_LINKPWR_ON_AC=max_performance +``` + +### Web Development +```bash +# Browser testing optimization +RUNTIME_PM_DRIVER_DENYLIST="nvidia nouveau" # For GPU acceleration +SOUND_POWER_SAVE_ON_AC=0 # For media testing +``` + +## Monitoring and Optimization + +### Development Metrics +```bash +# Monitor compilation performance +time make -j$(nproc) + +# I/O performance for large projects +iotop -o + +# Memory usage for IDEs +htop + +# Network performance for remote work +iftop +``` + +### Power Monitoring +```bash +# Real-time power consumption +sudo powertop --time=30 + +# Battery optimization +sudo tlp-stat -b + +# Thermal monitoring during compilation +watch -n 1 sensors +``` + +## Troubleshooting + +### Performance Issues +- **Slow compilation**: Check CPU governor and thermal throttling +- **IDE lag**: Monitor memory usage and I/O wait +- **Network issues**: Verify WiFi power management settings + +### Power Issues +- **High idle power**: Check for runaway processes and USB devices +- **Poor battery life**: Review active background services +- **Thermal throttling**: Monitor temperatures during heavy workloads + +### Development Environment Issues +- **USB device issues**: Check autosuspend settings for development tools +- **Display problems**: Verify graphics power management +- **Audio problems**: Check power save timeouts + +## Custom Scripts + +### Development Mode Toggle +```bash +#!/bin/bash +# dev-mode.sh - Toggle between development and power-save modes + +if [ "$1" == "performance" ]; then + echo "Enabling development performance mode..." + echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + echo 0 | sudo tee /sys/class/drm/card0/device/power_dpm_force_performance_level +elif [ "$1" == "balanced" ]; then + echo "Enabling balanced development mode..." + echo ondemand | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + echo auto | sudo tee /sys/class/drm/card0/device/power_dpm_force_performance_level +fi +``` + +### Compilation Optimizer +```bash +#!/bin/bash +# compile-optimized.sh - Optimize system for compilation + +echo "Optimizing for compilation..." +echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor +echo 254 | sudo tee /sys/class/scsi_host/host*/link_power_management_policy + +# Run compilation +"$@" + +echo "Returning to balanced mode..." +echo ondemand | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor +echo med_power_with_dipm | sudo tee /sys/class/scsi_host/host*/link_power_management_policy +``` + +This configuration provides optimal development performance while maintaining good power efficiency and system responsiveness for professional development workflows. diff --git a/examples/gaming-laptop.md b/examples/gaming-laptop.md new file mode 100644 index 0000000..506db4f --- /dev/null +++ b/examples/gaming-laptop.md @@ -0,0 +1,272 @@ +# Gaming Laptop Configuration Example + +This example demonstrates WiseTLP configuration for a gaming laptop optimized for performance while maintaining reasonable battery life during non-gaming use. + +## System Specifications + +- **Laptop**: Gaming laptop with dedicated GPU +- **CPU**: Intel Core i7-12700H (14 cores) +- **RAM**: 32GB DDR4 +- **Storage**: 1TB NVMe SSD +- **GPU**: NVIDIA RTX 3070 Mobile +- **Battery**: 90Wh Li-ion +- **Display**: 15.6" 144Hz + +## User Preferences + +``` +Power Profile: Performance +Use Case: Gaming +Battery Priority: Balanced +Performance Mode: Maximum +Special Requirements: + - Gaming performance priority + - Prevent thermal throttling + - Fast system wake/sleep +``` + +## Generated TLP Configuration + +### General Settings +```bash +TLP_ENABLE=1 +TLP_WARN_LEVEL=3 +TLP_DEBUG=0 +``` + +### CPU Management +```bash +# Performance-oriented CPU scaling +CPU_SCALING_GOVERNOR_ON_AC=performance +CPU_SCALING_GOVERNOR_ON_BAT=ondemand + +# Aggressive frequency scaling on AC +CPU_SCALING_MIN_FREQ_ON_AC=800000 +CPU_SCALING_MAX_FREQ_ON_AC=4700000 + +# Conservative on battery to preserve power +CPU_SCALING_MIN_FREQ_ON_BAT=800000 +CPU_SCALING_MAX_FREQ_ON_BAT=3200000 + +# Platform profiles for gaming performance +PLATFORM_PROFILE_ON_AC=performance +PLATFORM_PROFILE_ON_BAT=balanced + +# CPU energy performance preference +CPU_ENERGY_PERF_POLICY_ON_AC=performance +CPU_ENERGY_PERF_POLICY_ON_BAT=balance_performance + +# Turbo boost settings +CPU_BOOST_ON_AC=1 +CPU_BOOST_ON_BAT=1 + +# HWP (Hardware P-states) settings +CPU_HWP_DYN_BOOST_ON_AC=1 +CPU_HWP_DYN_BOOST_ON_BAT=0 +``` + +**Rationale**: Gaming requires maximum CPU performance on AC power. On battery, we use ondemand governor to balance performance with battery life while still allowing turbo boost for responsiveness. + +### GPU Power Management +```bash +# NVIDIA GPU settings +RUNTIME_PM_ON_AC=auto +RUNTIME_PM_ON_BAT=auto + +# Radeon DPM (if applicable) +RADEON_DPM_STATE_ON_AC=performance +RADEON_DPM_STATE_ON_BAT=balanced + +# Intel GPU settings +INTEL_GPU_MIN_FREQ_ON_AC=300 +INTEL_GPU_MAX_FREQ_ON_AC=1300 +INTEL_GPU_MIN_FREQ_ON_BAT=300 +INTEL_GPU_MAX_FREQ_ON_BAT=800 +``` + +**Rationale**: Dedicated GPU performance is prioritized on AC power for gaming. Battery mode reduces GPU frequency to conserve power. + +### Disk Management +```bash +# Aggressive disk performance on AC +DISK_APM_LEVEL_ON_AC=254 +DISK_APM_LEVEL_ON_BAT=128 + +# No spindown timeout for SSD +DISK_SPINDOWN_TIMEOUT_ON_AC=0 +DISK_SPINDOWN_TIMEOUT_ON_BAT=0 + +# SATA link power management +SATA_LINKPWR_ON_AC=max_performance +SATA_LINKPWR_ON_BAT=med_power_with_dipm + +# NVMe power management +AHCI_RUNTIME_PM_ON_AC=on +AHCI_RUNTIME_PM_ON_BAT=auto +``` + +**Rationale**: Fast game loading requires maximum disk performance on AC. SSD doesn't need spindown timeout. Battery mode uses moderate power saving. + +### Network Management +```bash +# WiFi power management +WIFI_PWR_ON_AC=off +WIFI_PWR_ON_BAT=on + +# Ethernet wake-on-LAN +WOL_DISABLE=Y + +# Network device power management +RUNTIME_PM_DRIVER_DENYLIST="mei_me nouveau nvidia" +``` + +**Rationale**: Gaming requires reliable, low-latency network connectivity. WiFi power saving is disabled on AC but enabled on battery to conserve power. + +### USB and Peripheral Management +```bash +# USB autosuspend for power saving +USB_AUTOSUSPEND=1 +USB_EXCLUDE_AUDIO=1 +USB_EXCLUDE_BTUSB=1 +USB_EXCLUDE_PHONE=1 + +# Exclude gaming peripherals from autosuspend +USB_ALLOWLIST="046d:c52b 1532:0037" # Gaming mouse and keyboard + +# Runtime PM for USB devices +USB_AUTOSUSPEND_DISABLE_ON_SHUTDOWN=1 +``` + +**Rationale**: Gaming peripherals need to remain active for immediate response. Audio and Bluetooth devices are excluded from autosuspend to prevent interruptions. + +### Thermal Management +```bash +# Thermal zone trip points (prevent throttling) +TEMP_LIMITS_ON_AC="85/95" +TEMP_LIMITS_ON_BAT="80/90" + +# Fan control (if supported) +FAN_SPEED_ON_AC=auto +FAN_SPEED_ON_BAT=auto +``` + +**Rationale**: Higher thermal limits on AC power allow for sustained gaming performance. Battery limits are lower to prevent overheating during mobile use. + +### Audio and Multimedia +```bash +# Audio power management +SOUND_POWER_SAVE_ON_AC=0 +SOUND_POWER_SAVE_ON_BAT=1 +SOUND_POWER_SAVE_CONTROLLER=Y +``` + +**Rationale**: Audio power saving disabled on AC for gaming audio quality. Enabled on battery for power conservation. + +### System Sleep and Wake +```bash +# Fast resume for gaming +RESTORE_DEVICE_STATE_ON_STARTUP=1 +DEVICES_TO_DISABLE_ON_STARTUP="" + +# USB device handling during sleep +USB_EXCLUDE_WWAN=1 +USB_EXCLUDE_PRINTER=1 +``` + +**Rationale**: Fast wake/sleep cycle for gaming sessions. Critical devices remain configured for immediate use. + +## Performance Impact + +### Gaming Performance (AC Power) +- **CPU**: Maximum performance with all cores available +- **GPU**: Full performance mode for dedicated graphics +- **Storage**: Maximum I/O performance for fast game loading +- **Network**: Low-latency connectivity for online gaming +- **Thermal**: Higher limits to prevent throttling during intensive gaming + +### Battery Life (Battery Power) +- **Runtime**: 4-6 hours for general use, 1.5-2.5 hours for gaming +- **Standby**: Optimized sleep/wake for extended standby time +- **Balanced**: Performance available when needed, power saving when idle + +## Warnings and Considerations + +1. **Thermal Management**: Higher performance settings may increase heat generation. Ensure adequate cooling. + +2. **Battery Health**: Gaming on battery will reduce battery lifespan. Consider AC power for extended gaming sessions. + +3. **Fan Noise**: Performance mode may result in more aggressive fan curves for thermal management. + +4. **Power Consumption**: AC power consumption will be higher during gaming sessions (150-200W typical). + +5. **Component Longevity**: Sustained high performance may impact long-term component reliability. + +## Customization Options + +### For Competitive Gaming +```bash +# Even more aggressive performance +CPU_SCALING_GOVERNOR_ON_BAT=performance +PLATFORM_PROFILE_ON_BAT=performance +WIFI_PWR_ON_BAT=off +``` + +### For Extended Battery Gaming +```bash +# More conservative battery settings +CPU_SCALING_MAX_FREQ_ON_BAT=2400000 +CPU_BOOST_ON_BAT=0 +PLATFORM_PROFILE_ON_BAT=low-power +``` + +### For Content Creation +```bash +# Optimize for rendering workloads +CPU_ENERGY_PERF_POLICY_ON_AC=performance +CPU_ENERGY_PERF_POLICY_ON_BAT=performance +SOUND_POWER_SAVE_ON_AC=0 +``` + +## Monitoring and Validation + +### Performance Monitoring +```bash +# CPU frequency monitoring +watch -n 1 'cat /proc/cpuinfo | grep MHz' + +# Thermal monitoring +watch -n 1 'sensors' + +# Power consumption +sudo powertop --time=60 +``` + +### Gaming Benchmarks +- **3DMark**: Graphics performance validation +- **Unigine Heaven**: Thermal stability testing +- **CPU-Z**: CPU performance verification +- **CrystalDiskMark**: Storage performance testing + +### Battery Testing +- **PowerTOP**: Power consumption analysis +- **Battery runtime**: Real-world usage testing +- **Standby drain**: Sleep power consumption + +## Troubleshooting + +### Performance Issues +- Verify CPU governor is set correctly: `cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor` +- Check thermal throttling: `dmesg | grep -i thermal` +- Monitor GPU performance: `nvidia-smi` (for NVIDIA) + +### Battery Issues +- Check power consumption: `sudo powertop` +- Verify power profiles: `system76-power profile` +- Monitor charging behavior: `upower -i /org/freedesktop/UPower/devices/battery_BAT0` + +### Thermal Issues +- Monitor temperatures: `sensors` +- Check fan operation: `pwmconfig` (if supported) +- Verify thermal limits: Check BIOS/UEFI settings + +This configuration provides optimal gaming performance while maintaining reasonable power management for non-gaming use cases. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..584e678 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp + +go 1.22.0 + +require golang.org/x/term v0.24.0 + +require golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b256fe0 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= diff --git a/internal/ai/client.go b/internal/ai/client.go new file mode 100644 index 0000000..78ea9c8 --- /dev/null +++ b/internal/ai/client.go @@ -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, + } +} diff --git a/internal/config/preferences.go b/internal/config/preferences.go new file mode 100644 index 0000000..b6109a9 --- /dev/null +++ b/internal/config/preferences.go @@ -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 +} diff --git a/internal/config/preferences_test.go b/internal/config/preferences_test.go new file mode 100644 index 0000000..fbbf5aa --- /dev/null +++ b/internal/config/preferences_test.go @@ -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") + } +} diff --git a/internal/security/keystore.go b/internal/security/keystore.go new file mode 100644 index 0000000..3867f8f --- /dev/null +++ b/internal/security/keystore.go @@ -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 +} diff --git a/internal/security/privileges.go b/internal/security/privileges.go new file mode 100644 index 0000000..b0887ba --- /dev/null +++ b/internal/security/privileges.go @@ -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 +} diff --git a/internal/system/detector.go b/internal/system/detector.go new file mode 100644 index 0000000..6b41cac --- /dev/null +++ b/internal/system/detector.go @@ -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 +} diff --git a/internal/system/detector_test.go b/internal/system/detector_test.go new file mode 100644 index 0000000..b507b12 --- /dev/null +++ b/internal/system/detector_test.go @@ -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") + } +} diff --git a/internal/tlp/manager.go b/internal/tlp/manager.go new file mode 100644 index 0000000..1c8ed31 --- /dev/null +++ b/internal/tlp/manager.go @@ -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 +} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..f513539 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,210 @@ +package types + +import "time" + +// SystemInfo represents comprehensive system information +type SystemInfo struct { + CPU CPUInfo `json:"cpu"` + Memory MemoryInfo `json:"memory"` + Battery *BatteryInfo `json:"battery,omitempty"` + PowerSupply PowerSupplyInfo `json:"power_supply"` + Kernel KernelInfo `json:"kernel"` + Distribution DistributionInfo `json:"distribution"` + Hardware HardwareInfo `json:"hardware"` + TLPStatus TLPStatus `json:"tlp_status"` +} + +// CPUInfo contains CPU-related information +type CPUInfo struct { + Model string `json:"model"` + Vendor string `json:"vendor"` + Cores int `json:"cores"` + Threads int `json:"threads"` + BaseFrequency int64 `json:"base_frequency_mhz"` + MaxFrequency int64 `json:"max_frequency_mhz"` + MinFrequency int64 `json:"min_frequency_mhz"` + Governor string `json:"governor"` + Architecture string `json:"architecture"` +} + +// MemoryInfo contains memory information +type MemoryInfo struct { + Total int64 `json:"total_mb"` + Available int64 `json:"available_mb"` + Used int64 `json:"used_mb"` + SwapTotal int64 `json:"swap_total_mb"` + SwapUsed int64 `json:"swap_used_mb"` +} + +// BatteryInfo contains battery information +type BatteryInfo struct { + Present bool `json:"present"` + Status string `json:"status"` + Capacity int `json:"capacity_percent"` + EnergyFull int64 `json:"energy_full_wh"` + EnergyNow int64 `json:"energy_now_wh"` + PowerNow int64 `json:"power_now_w"` + Manufacturer string `json:"manufacturer"` + Model string `json:"model"` + Technology string `json:"technology"` + CycleCount int `json:"cycle_count"` + DesignCapacity int64 `json:"design_capacity_wh"` +} + +// PowerSupplyInfo contains power supply information +type PowerSupplyInfo struct { + ACConnected bool `json:"ac_connected"` + Type string `json:"type"` + Online bool `json:"online"` +} + +// KernelInfo contains kernel information +type KernelInfo struct { + Version string `json:"version"` + Release string `json:"release"` + Parameters map[string]string `json:"parameters"` +} + +// DistributionInfo contains Linux distribution information +type DistributionInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Codename string `json:"codename"` + Family string `json:"family"` + PackageManager string `json:"package_manager"` +} + +// HardwareInfo contains additional hardware information +type HardwareInfo struct { + Chassis string `json:"chassis"` + Manufacturer string `json:"manufacturer"` + ProductName string `json:"product_name"` + GPUs []GPUInfo `json:"gpus"` + NetworkCards []string `json:"network_cards"` + StorageDevices []StorageInfo `json:"storage_devices"` +} + +// GPUInfo contains GPU information +type GPUInfo struct { + Vendor string `json:"vendor"` + Model string `json:"model"` + Driver string `json:"driver"` + Memory int64 `json:"memory_mb"` +} + +// StorageInfo contains storage device information +type StorageInfo struct { + Device string `json:"device"` + Type string `json:"type"` // SSD, HDD, NVMe + Size int64 `json:"size_gb"` + Model string `json:"model"` + Rotational bool `json:"rotational"` +} + +// TLPStatus contains TLP installation and configuration status +type TLPStatus struct { + Installed bool `json:"installed"` + Version string `json:"version"` + Active bool `json:"active"` + ConfigPath string `json:"config_path"` + ConfigExists bool `json:"config_exists"` + CurrentConfig map[string]string `json:"current_config"` + LastModified *time.Time `json:"last_modified"` +} + +// UserPreferences represents user's power management preferences +type UserPreferences struct { + PowerProfile PowerProfile `json:"power_profile"` + UseCase UseCase `json:"use_case"` + BatteryPriority BatteryPriority `json:"battery_priority"` + PerformanceMode PerformanceMode `json:"performance_mode"` + CustomSettings map[string]interface{} `json:"custom_settings"` + SpecialRequirements []string `json:"special_requirements"` +} + +// PowerProfile represents different power usage profiles +type PowerProfile string + +const ( + PowerProfileBalanced PowerProfile = "balanced" + PowerProfilePerformance PowerProfile = "performance" + PowerProfilePowerSaving PowerProfile = "power_saving" + PowerProfileCustom PowerProfile = "custom" +) + +// UseCase represents different system use cases +type UseCase string + +const ( + UseCaseGeneral UseCase = "general" + UseCaseDevelopment UseCase = "development" + UseCaseGaming UseCase = "gaming" + UseCaseServer UseCase = "server" + UseCaseMultimedia UseCase = "multimedia" + UseCaseOffice UseCase = "office" +) + +// BatteryPriority represents battery optimization priority +type BatteryPriority string + +const ( + BatteryPriorityLongevity BatteryPriority = "longevity" + BatteryPriorityRuntime BatteryPriority = "runtime" + BatteryPriorityBalanced BatteryPriority = "balanced" +) + +// PerformanceMode represents performance optimization mode +type PerformanceMode string + +const ( + PerformanceModeMaximum PerformanceMode = "maximum" + PerformanceModeAdaptive PerformanceMode = "adaptive" + PerformanceModeEfficient PerformanceMode = "efficient" +) + +// AIProvider represents different AI service providers +type AIProvider string + +const ( + AIProviderOpenRouter AIProvider = "openrouter" + AIProviderGroq AIProvider = "groq" + AIProviderGemini AIProvider = "gemini" + AIProviderCustom AIProvider = "custom" +) + +// AIConfig represents AI service configuration +type AIConfig struct { + Provider AIProvider `json:"provider"` + APIKey string `json:"-"` // Never serialize API keys + Endpoint string `json:"endpoint"` + Model string `json:"model"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` +} + +// TLPConfiguration represents a complete TLP configuration +type TLPConfiguration struct { + Settings map[string]string `json:"settings"` + Description string `json:"description"` + Rationale map[string]string `json:"rationale"` + Warnings []string `json:"warnings"` + Generated time.Time `json:"generated"` + SystemInfo *SystemInfo `json:"system_info"` + Preferences *UserPreferences `json:"preferences"` +} + +// InstallationResult represents the result of TLP installation +type InstallationResult struct { + Success bool `json:"success"` + Version string `json:"version"` + Message string `json:"message"` + ConfigPath string `json:"config_path"` +} + +// ValidationResult represents configuration validation result +type ValidationResult struct { + Valid bool `json:"valid"` + Errors []string `json:"errors"` + Warnings []string `json:"warnings"` +} diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go new file mode 100644 index 0000000..a06aeb6 --- /dev/null +++ b/pkg/utils/helpers.go @@ -0,0 +1,233 @@ +package utils + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "syscall" +) + +// FileExists checks if a file exists +func FileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +// ReadFileLines reads a file and returns its lines +func ReadFileLines(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + return lines, scanner.Err() +} + +// ReadFirstLine reads the first line of a file +func ReadFirstLine(path string) (string, error) { + lines, err := ReadFileLines(path) + if err != nil { + return "", err + } + if len(lines) == 0 { + return "", fmt.Errorf("file is empty") + } + return lines[0], nil +} + +// ParseKeyValue parses a key=value string +func ParseKeyValue(line string) (string, string, bool) { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + return "", "", false + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + // Remove quotes if present + value = strings.Trim(value, `"'`) + return key, value, true +} + +// ParseInt64 safely parses a string to int64 +func ParseInt64(s string) int64 { + val, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0 + } + return val +} + +// ParseInt safely parses a string to int +func ParseInt(s string) int { + val, err := strconv.Atoi(s) + if err != nil { + return 0 + } + return val +} + +// IsRoot checks if the current user is root +func IsRoot() bool { + return os.Geteuid() == 0 +} + +// RunCommand executes a command and returns its output +func RunCommand(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +// RunCommandWithInput executes a command with stdin input +func RunCommandWithInput(input, name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + cmd.Stdin = strings.NewReader(input) + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +// CommandExists checks if a command exists in PATH +func CommandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +// GetUserConfirmation prompts user for yes/no confirmation +func GetUserConfirmation(prompt string) bool { + fmt.Printf("%s (y/N): ", prompt) + var response string + fmt.Scanln(&response) + response = strings.ToLower(strings.TrimSpace(response)) + return response == "y" || response == "yes" +} + +// GetUserInput prompts user for input with a default value +func GetUserInput(prompt, defaultValue string) string { + if defaultValue != "" { + fmt.Printf("%s [%s]: ", prompt, defaultValue) + } else { + fmt.Printf("%s: ", prompt) + } + + var input string + fmt.Scanln(&input) + input = strings.TrimSpace(input) + + if input == "" { + return defaultValue + } + return input +} + +// EnsureRoot ensures the program is running with root privileges +func EnsureRoot() error { + if !IsRoot() { + return fmt.Errorf("this operation requires root privileges") + } + return nil +} + +// RequestElevation requests privilege elevation using sudo +func RequestElevation(args []string) error { + if IsRoot() { + return nil + } + + fmt.Println("This operation requires elevated privileges.") + if !GetUserConfirmation("Continue with sudo?") { + return fmt.Errorf("operation cancelled by user") + } + + // Prepare sudo command + sudoArgs := append([]string{os.Args[0]}, args...) + cmd := exec.Command("sudo", sudoArgs...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Execute with sudo + err := cmd.Run() + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if status, ok := exitError.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) + } + } + return fmt.Errorf("failed to execute with elevated privileges: %w", err) + } + + // Exit successfully since the elevated process completed + os.Exit(0) + return nil // This line will never be reached +} + +// SanitizeInput sanitizes user input by removing potentially dangerous characters +func SanitizeInput(input string) string { + // Remove null bytes and control characters + sanitized := strings.ReplaceAll(input, "\x00", "") + sanitized = strings.TrimSpace(sanitized) + + // Basic validation - reject inputs with shell metacharacters + dangerous := []string{";", "&", "|", "`", "$", "(", ")", "<", ">", "\"", "'"} + for _, char := range dangerous { + if strings.Contains(sanitized, char) { + return "" + } + } + + return sanitized +} + +// FormatBytes formats bytes into human-readable format +func FormatBytes(bytes int64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + + div, exp := int64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + units := []string{"KB", "MB", "GB", "TB", "PB"} + return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp]) +} + +// Contains checks if a slice contains a string +func Contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// RemoveEmpty removes empty strings from a slice +func RemoveEmpty(slice []string) []string { + var result []string + for _, s := range slice { + if strings.TrimSpace(s) != "" { + result = append(result, s) + } + } + return result +} diff --git a/pkg/utils/helpers_test.go b/pkg/utils/helpers_test.go new file mode 100644 index 0000000..c1f5d72 --- /dev/null +++ b/pkg/utils/helpers_test.go @@ -0,0 +1,174 @@ +package utils + +import ( + "os" + "testing" +) + +func TestFileExists(t *testing.T) { + tempFile, err := os.CreateTemp("", "test_file") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile.Name()) + defer tempFile.Close() + + if !FileExists(tempFile.Name()) { + t.Errorf("FileExists should return true for existing file") + } + + if FileExists("/non/existent/file") { + t.Errorf("FileExists should return false for non-existing file") + } +} + +func TestParseKeyValue(t *testing.T) { + tests := []struct { + input string + expectedKey string + expectedVal string + expectedOk bool + }{ + {"key=value", "key", "value", true}, + {"key = value", "key", "value", true}, + {"key=\"quoted value\"", "key", "quoted value", true}, + {"key='single quoted'", "key", "single quoted", true}, + {"invalid_line", "", "", false}, + {"key=", "key", "", true}, + {"=value", "", "value", true}, + } + + for _, test := range tests { + key, value, ok := ParseKeyValue(test.input) + if key != test.expectedKey || value != test.expectedVal || ok != test.expectedOk { + t.Errorf("ParseKeyValue(%q) = (%q, %q, %v), want (%q, %q, %v)", + test.input, key, value, ok, test.expectedKey, test.expectedVal, test.expectedOk) + } + } +} + +func TestParseInt64(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"123", 123}, + {"0", 0}, + {"-456", -456}, + {"invalid", 0}, + {"", 0}, + {"123.45", 0}, // Should fail for float + } + + for _, test := range tests { + result := ParseInt64(test.input) + if result != test.expected { + t.Errorf("ParseInt64(%q) = %d, want %d", test.input, result, test.expected) + } + } +} + +func TestParseInt(t *testing.T) { + tests := []struct { + input string + expected int + }{ + {"123", 123}, + {"0", 0}, + {"-456", -456}, + {"invalid", 0}, + {"", 0}, + } + + for _, test := range tests { + result := ParseInt(test.input) + if result != test.expected { + t.Errorf("ParseInt(%q) = %d, want %d", test.input, result, test.expected) + } + } +} + +func TestSanitizeInput(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"clean_input", "clean_input"}, + {" spaced ", "spaced"}, + {"with;semicolon", ""}, + {"with&ersand", ""}, + {"with|pipe", ""}, + {"with`backtick", ""}, + {"with$dollar", ""}, + {"with(paren", ""}, + {"with\"quote", ""}, + {"with'quote", ""}, + {"normal_text_123", "normal_text_123"}, + } + + for _, test := range tests { + result := SanitizeInput(test.input) + if result != test.expected { + t.Errorf("SanitizeInput(%q) = %q, want %q", test.input, result, test.expected) + } + } +} + +func TestFormatBytes(t *testing.T) { + tests := []struct { + input int64 + expected string + }{ + {500, "500 B"}, + {1024, "1.0 KB"}, + {1536, "1.5 KB"}, + {1048576, "1.0 MB"}, + {1073741824, "1.0 GB"}, + {0, "0 B"}, + } + + for _, test := range tests { + result := FormatBytes(test.input) + if result != test.expected { + t.Errorf("FormatBytes(%d) = %q, want %q", test.input, result, test.expected) + } + } +} + +func TestContains(t *testing.T) { + slice := []string{"apple", "banana", "cherry"} + + tests := []struct { + item string + expected bool + }{ + {"apple", true}, + {"banana", true}, + {"cherry", true}, + {"grape", false}, + {"", false}, + } + + for _, test := range tests { + result := Contains(slice, test.item) + if result != test.expected { + t.Errorf("Contains(slice, %q) = %v, want %v", test.item, result, test.expected) + } + } +} + +func TestRemoveEmpty(t *testing.T) { + input := []string{"apple", "", "banana", " ", "cherry", "\t\n"} + expected := []string{"apple", "banana", "cherry"} + + result := RemoveEmpty(input) + if len(result) != len(expected) { + t.Fatalf("RemoveEmpty() returned slice of length %d, want %d", len(result), len(expected)) + } + + for i, item := range result { + if item != expected[i] { + t.Errorf("RemoveEmpty()[%d] = %q, want %q", i, item, expected[i]) + } + } +} diff --git a/pkg/utils/logger.go b/pkg/utils/logger.go new file mode 100644 index 0000000..b761e81 --- /dev/null +++ b/pkg/utils/logger.go @@ -0,0 +1,83 @@ +package utils + +import ( + "io" + "log/slog" + "os" + "time" +) + +// Logger provides structured logging capabilities +type Logger struct { + *slog.Logger +} + +// NewLogger creates a new structured logger +func NewLogger() *Logger { + return NewLoggerWithOutput(os.Stdout) +} + +// NewDebugLogger creates a new logger with debug level enabled +func NewDebugLogger() *Logger { + opts := &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Customize timestamp format + if a.Key == slog.TimeKey { + return slog.Attr{ + Key: a.Key, + Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")), + } + } + return a + }, + } + + handler := slog.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + + return &Logger{Logger: logger} +} + +// NewLoggerWithOutput creates a new logger with custom output +func NewLoggerWithOutput(w io.Writer) *Logger { + opts := &slog.HandlerOptions{ + Level: slog.LevelInfo, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Customize timestamp format + if a.Key == slog.TimeKey { + return slog.Attr{ + Key: a.Key, + Value: slog.StringValue(time.Now().Format("2006-01-02 15:04:05")), + } + } + return a + }, + } + + handler := slog.NewTextHandler(w, opts) + logger := slog.New(handler) + + return &Logger{Logger: logger} +} + +// NewSilentLogger creates a logger that discards all output +func NewSilentLogger() *Logger { + return NewLoggerWithOutput(io.Discard) +} + +// WithComponent adds a component context to the logger +func (l *Logger) WithComponent(component string) *Logger { + return &Logger{Logger: l.Logger.With("component", component)} +} + +// WithRequestID adds a request ID context to the logger +func (l *Logger) WithRequestID(requestID string) *Logger { + return &Logger{Logger: l.Logger.With("request_id", requestID)} +} + +// Fatal logs a fatal error and exits the program +func (l *Logger) Fatal(msg string, args ...interface{}) { + l.Error(msg, args...) + os.Exit(1) +}