yoohoo
This commit is contained in:
350
README.md
Normal file
350
README.md
Normal file
@@ -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.
|
152
cmd/autotlp/main.go
Normal file
152
cmd/autotlp/main.go
Normal file
@@ -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()
|
||||
}
|
376
docs/API.md
Normal file
376
docs/API.md
Normal file
@@ -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
|
134
examples/demo-config.sh
Normal file
134
examples/demo-config.sh
Normal file
@@ -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"
|
382
examples/development-workstation.md
Normal file
382
examples/development-workstation.md
Normal file
@@ -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.
|
272
examples/gaming-laptop.md
Normal file
272
examples/gaming-laptop.md
Normal file
@@ -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.
|
7
go.mod
Normal file
7
go.mod
Normal file
@@ -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
|
4
go.sum
Normal file
4
go.sum
Normal file
@@ -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=
|
347
internal/ai/client.go
Normal file
347
internal/ai/client.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/config"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
config *types.AIConfig
|
||||
httpClient *http.Client
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
func NewClient(config *types.AIConfig, logger *utils.Logger) *Client {
|
||||
return &Client{
|
||||
config: config,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
logger: logger.WithComponent("ai"),
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigureClient(logger *utils.Logger) (*Client, error) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 50))
|
||||
fmt.Println("AI SERVICE CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
config := &types.AIConfig{}
|
||||
|
||||
fmt.Println("\nAvailable AI providers:")
|
||||
fmt.Println("1. Groq (Fast inference)")
|
||||
fmt.Println("2. OpenRouter (Multiple models)")
|
||||
fmt.Println("3. Gemini (Google)")
|
||||
fmt.Println("4. Custom (OpenAI-compatible)")
|
||||
|
||||
choice := utils.GetUserInput("Select provider (1-4)", "1")
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
config.Provider = types.AIProviderGroq
|
||||
config.Endpoint = "https://api.groq.com/openai/v1/chat/completions"
|
||||
config.Model = "openai/gpt-oss-20b"
|
||||
case "2":
|
||||
config.Provider = types.AIProviderOpenRouter
|
||||
config.Endpoint = "https://openrouter.ai/api/v1/chat/completions"
|
||||
config.Model = utils.GetUserInput("Model name", "meta-llama/llama-3.1-8b-instruct:free")
|
||||
case "3":
|
||||
config.Provider = types.AIProviderGemini
|
||||
config.Endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"
|
||||
config.Model = "gemini-pro"
|
||||
case "4":
|
||||
config.Provider = types.AIProviderCustom
|
||||
config.Endpoint = utils.GetUserInput("API endpoint", "")
|
||||
config.Model = utils.GetUserInput("Model name", "")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid provider selection")
|
||||
}
|
||||
|
||||
fmt.Printf("\nEnter your API key for %s: ", config.Provider)
|
||||
var apiKey string
|
||||
fmt.Scanln(&apiKey)
|
||||
config.APIKey = strings.TrimSpace(apiKey)
|
||||
|
||||
if config.APIKey == "" {
|
||||
return nil, fmt.Errorf("API key is required")
|
||||
}
|
||||
|
||||
config.MaxTokens = 1500
|
||||
config.Temperature = 0.3
|
||||
|
||||
client := NewClient(config, logger)
|
||||
if err := client.validateConnection(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate AI connection: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ AI service configured successfully!")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) GenerateConfig(ctx context.Context, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||
c.logger.Info("Generating TLP configuration using AI", "provider", c.config.Provider, "model", c.config.Model)
|
||||
|
||||
prompt := c.buildPrompt(sysInfo, preferences)
|
||||
response, err := c.makeRequest(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI request failed: %w", err)
|
||||
}
|
||||
|
||||
config, err := c.parseResponse(response, sysInfo, preferences)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse AI response: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("TLP configuration generated successfully", "settings_count", len(config.Settings))
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *Client) buildPrompt(sysInfo *types.SystemInfo, preferences *types.UserPreferences) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("Generate TLP configuration in JSON format.\n\n")
|
||||
|
||||
prompt.WriteString("System: ")
|
||||
if sysInfo.Battery != nil && sysInfo.Battery.Present {
|
||||
prompt.WriteString("Laptop")
|
||||
} else {
|
||||
prompt.WriteString("Desktop")
|
||||
}
|
||||
prompt.WriteString(fmt.Sprintf(", %s, %d cores\n", sysInfo.Distribution.ID, sysInfo.CPU.Cores))
|
||||
|
||||
prompt.WriteString(fmt.Sprintf("Profile: %s, Use: %s, Mode: %s\n\n", preferences.PowerProfile, preferences.UseCase, preferences.PerformanceMode))
|
||||
|
||||
prompt.WriteString("Return JSON with this structure:\n")
|
||||
prompt.WriteString(`{"description": "Config description", "settings": {"TLP_ENABLE": "1", "CPU_SCALING_GOVERNOR_ON_AC": "performance", "CPU_SCALING_GOVERNOR_ON_BAT": "powersave", "DISK_APM_LEVEL_ON_AC": "254", "DISK_APM_LEVEL_ON_BAT": "128", "WIFI_PWR_ON_AC": "off", "WIFI_PWR_ON_BAT": "on", "USB_AUTOSUSPEND": "1"}, "rationale": {"CPU_SCALING_GOVERNOR_ON_AC": "Max performance on AC"}, "warnings": ["Monitor temperatures"]}` + "\n\n")
|
||||
|
||||
prompt.WriteString("Generate 8-10 TLP settings for this system.")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
|
||||
func (c *Client) makeRequest(ctx context.Context, prompt string) (string, error) {
|
||||
c.logger.Debug("Making AI request", "prompt_length", len(prompt))
|
||||
c.logger.Debug("Full prompt being sent", "prompt", prompt)
|
||||
|
||||
var requestBody interface{}
|
||||
var endpoint string
|
||||
|
||||
switch c.config.Provider {
|
||||
case types.AIProviderGemini:
|
||||
requestBody = map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{
|
||||
"parts": []map[string]string{
|
||||
{"text": prompt},
|
||||
},
|
||||
},
|
||||
},
|
||||
"generationConfig": map[string]interface{}{
|
||||
"temperature": c.config.Temperature,
|
||||
"maxOutputTokens": c.config.MaxTokens,
|
||||
},
|
||||
}
|
||||
endpoint = c.config.Endpoint + "?key=" + c.config.APIKey
|
||||
default:
|
||||
requestBody = map[string]interface{}{
|
||||
"model": c.config.Model,
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
"max_tokens": c.config.MaxTokens,
|
||||
"temperature": c.config.Temperature,
|
||||
"response_format": map[string]interface{}{
|
||||
"type": "json_object",
|
||||
},
|
||||
}
|
||||
endpoint = c.config.Endpoint
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("Request body", "json", string(jsonBody))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonBody))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if c.config.Provider != types.AIProviderGemini {
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return c.extractContent(body)
|
||||
}
|
||||
|
||||
func (c *Client) extractContent(responseBody []byte) (string, error) {
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(responseBody, &response); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response JSON: %w", err)
|
||||
}
|
||||
|
||||
switch c.config.Provider {
|
||||
case types.AIProviderGemini:
|
||||
if candidates, ok := response["candidates"].([]interface{}); ok && len(candidates) > 0 {
|
||||
if candidate, ok := candidates[0].(map[string]interface{}); ok {
|
||||
if content, ok := candidate["content"].(map[string]interface{}); ok {
|
||||
if parts, ok := content["parts"].([]interface{}); ok && len(parts) > 0 {
|
||||
if part, ok := parts[0].(map[string]interface{}); ok {
|
||||
if text, ok := part["text"].(string); ok {
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if choices, ok := response["choices"].([]interface{}); ok && len(choices) > 0 {
|
||||
if choice, ok := choices[0].(map[string]interface{}); ok {
|
||||
if message, ok := choice["message"].(map[string]interface{}); ok {
|
||||
if content, ok := message["content"].(string); ok {
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unexpected response format")
|
||||
}
|
||||
|
||||
func (c *Client) parseResponse(response string, sysInfo *types.SystemInfo, preferences *types.UserPreferences) (*types.TLPConfiguration, error) {
|
||||
c.logger.Debug("Raw AI response", "response", response)
|
||||
|
||||
start := strings.Index(response, "{")
|
||||
end := strings.LastIndex(response, "}") + 1
|
||||
|
||||
if start == -1 || end == 0 {
|
||||
c.logger.Error("No JSON found in AI response", "response_length", len(response), "response_preview", response[:min(200, len(response))])
|
||||
c.logger.Info("Generating fallback TLP configuration")
|
||||
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||
}
|
||||
|
||||
jsonStr := response[start:end]
|
||||
|
||||
var aiResponse struct {
|
||||
Description string `json:"description"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
Rationale map[string]string `json:"rationale"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &aiResponse); err != nil {
|
||||
c.logger.Error("Failed to parse AI JSON response", "error", err, "json_str", jsonStr)
|
||||
c.logger.Info("Generating fallback TLP configuration due to JSON parse error")
|
||||
return c.generateFallbackConfig(sysInfo, preferences), nil
|
||||
}
|
||||
|
||||
if len(aiResponse.Settings) == 0 {
|
||||
return nil, fmt.Errorf("AI response contains no settings")
|
||||
}
|
||||
|
||||
config := &types.TLPConfiguration{
|
||||
Settings: aiResponse.Settings,
|
||||
Description: aiResponse.Description,
|
||||
Rationale: aiResponse.Rationale,
|
||||
Warnings: aiResponse.Warnings,
|
||||
Generated: time.Now(),
|
||||
SystemInfo: sysInfo,
|
||||
Preferences: preferences,
|
||||
}
|
||||
|
||||
if _, exists := config.Settings["TLP_ENABLE"]; !exists {
|
||||
config.Settings["TLP_ENABLE"] = "1"
|
||||
}
|
||||
|
||||
if config.Rationale == nil {
|
||||
config.Rationale = make(map[string]string)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *Client) validateConnection(ctx context.Context) error {
|
||||
testPrompt := "Respond with a JSON object containing 'status': 'OK' to confirm the connection is working."
|
||||
|
||||
response, err := c.makeRequest(ctx, testPrompt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connection test failed: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.ToUpper(response), "OK") {
|
||||
return fmt.Errorf("unexpected response from AI service: %s", response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (c *Client) generateFallbackConfig(sysInfo *types.SystemInfo, preferences *types.UserPreferences) *types.TLPConfiguration {
|
||||
c.logger.Info("Generating fallback configuration based on user preferences")
|
||||
|
||||
settings := config.GetRecommendedSettings(preferences)
|
||||
|
||||
rationale := make(map[string]string)
|
||||
rationale["TLP_ENABLE"] = "Enable TLP power management"
|
||||
rationale["CPU_SCALING_GOVERNOR_ON_AC"] = "CPU governor for AC power based on user preferences"
|
||||
rationale["CPU_SCALING_GOVERNOR_ON_BAT"] = "CPU governor for battery power based on user preferences"
|
||||
|
||||
warnings := []string{
|
||||
"This is a fallback configuration generated when AI service was unavailable",
|
||||
"Configuration is based on user preferences and common best practices",
|
||||
"Consider running WiseTLP again when AI service is available for optimized settings",
|
||||
}
|
||||
|
||||
description := fmt.Sprintf("Fallback TLP configuration for %s use case with %s power profile",
|
||||
preferences.UseCase, preferences.PowerProfile)
|
||||
|
||||
return &types.TLPConfiguration{
|
||||
Settings: settings,
|
||||
Description: description,
|
||||
Rationale: rationale,
|
||||
Warnings: warnings,
|
||||
Generated: time.Now(),
|
||||
SystemInfo: sysInfo,
|
||||
Preferences: preferences,
|
||||
}
|
||||
}
|
346
internal/config/preferences.go
Normal file
346
internal/config/preferences.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
// Application-wide configuration settings
|
||||
}
|
||||
|
||||
// New creates a new configuration instance
|
||||
func New() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// GatherUserPreferences interactively collects user preferences
|
||||
func GatherUserPreferences() (*types.UserPreferences, error) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("USER PREFERENCES CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
preferences := &types.UserPreferences{
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
// Power Profile
|
||||
fmt.Println("\n1. Power Profile Selection")
|
||||
fmt.Println("Choose your preferred power management approach:")
|
||||
fmt.Println(" a) Balanced - Good compromise between performance and power saving")
|
||||
fmt.Println(" b) Performance - Maximum performance, higher power consumption")
|
||||
fmt.Println(" c) Power Saving - Maximum battery life, reduced performance")
|
||||
fmt.Println(" d) Custom - I'll specify custom requirements")
|
||||
|
||||
profileChoice := utils.GetUserInput("Select power profile (a/b/c/d)", "a")
|
||||
switch strings.ToLower(profileChoice) {
|
||||
case "a", "balanced":
|
||||
preferences.PowerProfile = types.PowerProfileBalanced
|
||||
case "b", "performance":
|
||||
preferences.PowerProfile = types.PowerProfilePerformance
|
||||
case "c", "power saving", "powersaving":
|
||||
preferences.PowerProfile = types.PowerProfilePowerSaving
|
||||
case "d", "custom":
|
||||
preferences.PowerProfile = types.PowerProfileCustom
|
||||
default:
|
||||
preferences.PowerProfile = types.PowerProfileBalanced
|
||||
}
|
||||
|
||||
// Use Case
|
||||
fmt.Println("\n2. Primary Use Case")
|
||||
fmt.Println("What do you primarily use this system for?")
|
||||
fmt.Println(" a) General use - Web browsing, office work, media consumption")
|
||||
fmt.Println(" b) Development - Programming, compiling, development tools")
|
||||
fmt.Println(" c) Gaming - Gaming and graphics-intensive applications")
|
||||
fmt.Println(" d) Server - Server workloads, always-on services")
|
||||
fmt.Println(" e) Multimedia - Video editing, rendering, content creation")
|
||||
fmt.Println(" f) Office - Primarily office applications and productivity")
|
||||
|
||||
useCaseChoice := utils.GetUserInput("Select use case (a/b/c/d/e/f)", "a")
|
||||
switch strings.ToLower(useCaseChoice) {
|
||||
case "a", "general":
|
||||
preferences.UseCase = types.UseCaseGeneral
|
||||
case "b", "development", "dev":
|
||||
preferences.UseCase = types.UseCaseDevelopment
|
||||
case "c", "gaming":
|
||||
preferences.UseCase = types.UseCaseGaming
|
||||
case "d", "server":
|
||||
preferences.UseCase = types.UseCaseServer
|
||||
case "e", "multimedia", "media":
|
||||
preferences.UseCase = types.UseCaseMultimedia
|
||||
case "f", "office":
|
||||
preferences.UseCase = types.UseCaseOffice
|
||||
default:
|
||||
preferences.UseCase = types.UseCaseGeneral
|
||||
}
|
||||
|
||||
// Battery Priority (only for systems with batteries)
|
||||
fmt.Println("\n3. Battery Optimization Priority")
|
||||
fmt.Println("How would you like to optimize battery usage?")
|
||||
fmt.Println(" a) Balanced - Balance between battery life and longevity")
|
||||
fmt.Println(" b) Runtime - Maximize single-charge runtime")
|
||||
fmt.Println(" c) Longevity - Maximize battery lifespan over years")
|
||||
|
||||
batteryChoice := utils.GetUserInput("Select battery priority (a/b/c)", "a")
|
||||
switch strings.ToLower(batteryChoice) {
|
||||
case "a", "balanced":
|
||||
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||
case "b", "runtime":
|
||||
preferences.BatteryPriority = types.BatteryPriorityRuntime
|
||||
case "c", "longevity":
|
||||
preferences.BatteryPriority = types.BatteryPriorityLongevity
|
||||
default:
|
||||
preferences.BatteryPriority = types.BatteryPriorityBalanced
|
||||
}
|
||||
|
||||
// Performance Mode
|
||||
fmt.Println("\n4. Performance Mode")
|
||||
fmt.Println("How should the system handle performance scaling?")
|
||||
fmt.Println(" a) Adaptive - Automatically adjust based on workload")
|
||||
fmt.Println(" b) Maximum - Always prefer maximum performance")
|
||||
fmt.Println(" c) Efficient - Always prefer energy efficiency")
|
||||
|
||||
perfChoice := utils.GetUserInput("Select performance mode (a/b/c)", "a")
|
||||
switch strings.ToLower(perfChoice) {
|
||||
case "a", "adaptive":
|
||||
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||
case "b", "maximum", "max":
|
||||
preferences.PerformanceMode = types.PerformanceModeMaximum
|
||||
case "c", "efficient", "efficiency":
|
||||
preferences.PerformanceMode = types.PerformanceModeEfficient
|
||||
default:
|
||||
preferences.PerformanceMode = types.PerformanceModeAdaptive
|
||||
}
|
||||
|
||||
// Special Requirements
|
||||
fmt.Println("\n5. Special Requirements")
|
||||
fmt.Println("Do you have any special requirements? (Select all that apply)")
|
||||
fmt.Println(" a) Minimize fan noise")
|
||||
fmt.Println(" b) Prevent thermal throttling")
|
||||
fmt.Println(" c) Optimize for external displays")
|
||||
fmt.Println(" d) Gaming performance priority")
|
||||
fmt.Println(" e) Maximum WiFi performance")
|
||||
fmt.Println(" f) Minimize disk wear")
|
||||
fmt.Println(" g) Fast system wake/sleep")
|
||||
fmt.Println(" h) None of the above")
|
||||
|
||||
requirements := utils.GetUserInput("Enter letters separated by spaces (e.g., 'a c f')", "h")
|
||||
if strings.ToLower(requirements) != "h" {
|
||||
reqMap := map[string]string{
|
||||
"a": "Minimize fan noise",
|
||||
"b": "Prevent thermal throttling",
|
||||
"c": "Optimize for external displays",
|
||||
"d": "Gaming performance priority",
|
||||
"e": "Maximum WiFi performance",
|
||||
"f": "Minimize disk wear",
|
||||
"g": "Fast system wake/sleep",
|
||||
}
|
||||
|
||||
reqLetters := strings.Fields(strings.ToLower(requirements))
|
||||
for _, letter := range reqLetters {
|
||||
if req, exists := reqMap[letter]; exists {
|
||||
preferences.SpecialRequirements = append(preferences.SpecialRequirements, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Settings (for advanced users)
|
||||
if preferences.PowerProfile == types.PowerProfileCustom {
|
||||
fmt.Println("\n6. Custom Settings")
|
||||
fmt.Println("You selected custom power profile. You can specify additional settings.")
|
||||
|
||||
if utils.GetUserConfirmation("Do you want to specify custom CPU governor settings?") {
|
||||
acGovernor := utils.GetUserInput("CPU governor on AC power (performance/powersave/ondemand/conservative/schedutil)", "performance")
|
||||
batGovernor := utils.GetUserInput("CPU governor on battery (performance/powersave/ondemand/conservative/schedutil)", "powersave")
|
||||
|
||||
preferences.CustomSettings["cpu_governor_ac"] = acGovernor
|
||||
preferences.CustomSettings["cpu_governor_battery"] = batGovernor
|
||||
}
|
||||
|
||||
if utils.GetUserConfirmation("Do you want to specify custom disk settings?") {
|
||||
diskAPMAC := utils.GetUserInput("Disk APM level on AC (1-255, higher = more aggressive)", "254")
|
||||
diskAPMBat := utils.GetUserInput("Disk APM level on battery (1-255, higher = more aggressive)", "128")
|
||||
|
||||
preferences.CustomSettings["disk_apm_ac"] = diskAPMAC
|
||||
preferences.CustomSettings["disk_apm_battery"] = diskAPMBat
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
fmt.Println("\n" + strings.Repeat("-", 60))
|
||||
fmt.Println("PREFERENCES SUMMARY")
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
fmt.Printf("Power Profile: %s\n", preferences.PowerProfile)
|
||||
fmt.Printf("Use Case: %s\n", preferences.UseCase)
|
||||
fmt.Printf("Battery Priority: %s\n", preferences.BatteryPriority)
|
||||
fmt.Printf("Performance Mode: %s\n", preferences.PerformanceMode)
|
||||
|
||||
if len(preferences.SpecialRequirements) > 0 {
|
||||
fmt.Printf("Special Requirements: %s\n", strings.Join(preferences.SpecialRequirements, ", "))
|
||||
}
|
||||
|
||||
if len(preferences.CustomSettings) > 0 {
|
||||
fmt.Println("Custom Settings:")
|
||||
for key, value := range preferences.CustomSettings {
|
||||
fmt.Printf(" %s: %v\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
|
||||
if !utils.GetUserConfirmation("Are these preferences correct?") {
|
||||
fmt.Println("Let's try again...")
|
||||
return GatherUserPreferences()
|
||||
}
|
||||
|
||||
return preferences, nil
|
||||
}
|
||||
|
||||
// ValidatePreferences validates user preferences for consistency
|
||||
func ValidatePreferences(preferences *types.UserPreferences) error {
|
||||
if preferences == nil {
|
||||
return fmt.Errorf("preferences cannot be nil")
|
||||
}
|
||||
|
||||
// Validate power profile
|
||||
validProfiles := []types.PowerProfile{
|
||||
types.PowerProfileBalanced,
|
||||
types.PowerProfilePerformance,
|
||||
types.PowerProfilePowerSaving,
|
||||
types.PowerProfileCustom,
|
||||
}
|
||||
|
||||
validProfile := false
|
||||
for _, profile := range validProfiles {
|
||||
if preferences.PowerProfile == profile {
|
||||
validProfile = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validProfile {
|
||||
return fmt.Errorf("invalid power profile: %s", preferences.PowerProfile)
|
||||
}
|
||||
|
||||
// Validate use case
|
||||
validUseCases := []types.UseCase{
|
||||
types.UseCaseGeneral,
|
||||
types.UseCaseDevelopment,
|
||||
types.UseCaseGaming,
|
||||
types.UseCaseServer,
|
||||
types.UseCaseMultimedia,
|
||||
types.UseCaseOffice,
|
||||
}
|
||||
|
||||
validUseCase := false
|
||||
for _, useCase := range validUseCases {
|
||||
if preferences.UseCase == useCase {
|
||||
validUseCase = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validUseCase {
|
||||
return fmt.Errorf("invalid use case: %s", preferences.UseCase)
|
||||
}
|
||||
|
||||
// Check for conflicting preferences
|
||||
if preferences.PowerProfile == types.PowerProfilePowerSaving &&
|
||||
preferences.PerformanceMode == types.PerformanceModeMaximum {
|
||||
return fmt.Errorf("conflicting preferences: power saving profile with maximum performance mode")
|
||||
}
|
||||
|
||||
if preferences.UseCase == types.UseCaseGaming &&
|
||||
preferences.PowerProfile == types.PowerProfilePowerSaving {
|
||||
fmt.Println("⚠️ Warning: Gaming use case with power saving profile may impact performance")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecommendedSettings provides recommended settings based on preferences
|
||||
func GetRecommendedSettings(preferences *types.UserPreferences) map[string]string {
|
||||
settings := make(map[string]string)
|
||||
|
||||
// Base settings
|
||||
settings["TLP_ENABLE"] = "1"
|
||||
settings["TLP_WARN_LEVEL"] = "3"
|
||||
|
||||
// CPU settings based on power profile and use case
|
||||
switch preferences.PowerProfile {
|
||||
case types.PowerProfilePerformance:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||
case types.PowerProfilePowerSaving:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "powersave"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
default: // Balanced
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
}
|
||||
|
||||
// Adjust for use case
|
||||
switch preferences.UseCase {
|
||||
case types.UseCaseGaming:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_BAT"] = "balanced"
|
||||
case types.UseCaseServer:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "performance"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_AC"] = "performance"
|
||||
settings["PLATFORM_PROFILE_ON_BAT"] = "performance"
|
||||
case types.UseCaseDevelopment:
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = "ondemand"
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = "powersave"
|
||||
}
|
||||
|
||||
// Disk settings based on battery priority
|
||||
switch preferences.BatteryPriority {
|
||||
case types.BatteryPriorityRuntime:
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "1"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "60"
|
||||
case types.BatteryPriorityLongevity:
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "0"
|
||||
default: // Balanced
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = "128"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_BAT"] = "120"
|
||||
}
|
||||
|
||||
settings["DISK_APM_LEVEL_ON_AC"] = "254"
|
||||
settings["DISK_SPINDOWN_TIMEOUT_ON_AC"] = "0"
|
||||
|
||||
// Network settings
|
||||
if utils.Contains(preferences.SpecialRequirements, "Maximum WiFi performance") {
|
||||
settings["WIFI_PWR_ON_AC"] = "off"
|
||||
settings["WIFI_PWR_ON_BAT"] = "off"
|
||||
} else {
|
||||
settings["WIFI_PWR_ON_AC"] = "off"
|
||||
settings["WIFI_PWR_ON_BAT"] = "on"
|
||||
}
|
||||
|
||||
// USB settings
|
||||
settings["USB_AUTOSUSPEND"] = "1"
|
||||
|
||||
// Apply custom settings
|
||||
for key, value := range preferences.CustomSettings {
|
||||
switch key {
|
||||
case "cpu_governor_ac":
|
||||
settings["CPU_SCALING_GOVERNOR_ON_AC"] = fmt.Sprintf("%v", value)
|
||||
case "cpu_governor_battery":
|
||||
settings["CPU_SCALING_GOVERNOR_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||
case "disk_apm_ac":
|
||||
settings["DISK_APM_LEVEL_ON_AC"] = fmt.Sprintf("%v", value)
|
||||
case "disk_apm_battery":
|
||||
settings["DISK_APM_LEVEL_ON_BAT"] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
162
internal/config/preferences_test.go
Normal file
162
internal/config/preferences_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
)
|
||||
|
||||
func TestValidatePreferences(t *testing.T) {
|
||||
validPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(validPrefs); err != nil {
|
||||
t.Errorf("ValidatePreferences() with valid preferences failed: %v", err)
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(nil); err == nil {
|
||||
t.Error("ValidatePreferences() with nil preferences should fail")
|
||||
}
|
||||
|
||||
invalidPrefs := &types.UserPreferences{
|
||||
PowerProfile: "invalid_profile",
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(invalidPrefs); err == nil {
|
||||
t.Error("ValidatePreferences() with invalid power profile should fail")
|
||||
}
|
||||
|
||||
invalidPrefs2 := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: "invalid_usecase",
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(invalidPrefs2); err == nil {
|
||||
t.Error("ValidatePreferences() with invalid use case should fail")
|
||||
}
|
||||
|
||||
// Test conflicting preferences
|
||||
conflictingPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePowerSaving,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeMaximum,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
if err := ValidatePreferences(conflictingPrefs); err == nil {
|
||||
t.Error("ValidatePreferences() with conflicting preferences should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRecommendedSettings(t *testing.T) {
|
||||
// Test performance profile
|
||||
perfPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePerformance,
|
||||
UseCase: types.UseCaseGaming,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeMaximum,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings := GetRecommendedSettings(perfPrefs)
|
||||
|
||||
// Should have TLP_ENABLE
|
||||
if settings["TLP_ENABLE"] != "1" {
|
||||
t.Error("TLP_ENABLE should be set to '1'")
|
||||
}
|
||||
|
||||
// Performance profile should use performance governor
|
||||
if settings["CPU_SCALING_GOVERNOR_ON_AC"] != "performance" {
|
||||
t.Error("Performance profile should use performance governor on AC")
|
||||
}
|
||||
|
||||
// Test power saving profile
|
||||
powerSavePrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfilePowerSaving,
|
||||
UseCase: types.UseCaseOffice,
|
||||
BatteryPriority: types.BatteryPriorityRuntime,
|
||||
PerformanceMode: types.PerformanceModeEfficient,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings2 := GetRecommendedSettings(powerSavePrefs)
|
||||
|
||||
// Power saving should use powersave governor
|
||||
if settings2["CPU_SCALING_GOVERNOR_ON_AC"] != "powersave" {
|
||||
t.Error("Power saving profile should use powersave governor on AC")
|
||||
}
|
||||
|
||||
if settings2["CPU_SCALING_GOVERNOR_ON_BAT"] != "powersave" {
|
||||
t.Error("Power saving profile should use powersave governor on battery")
|
||||
}
|
||||
|
||||
// Test custom settings
|
||||
customPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileCustom,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: map[string]interface{}{
|
||||
"cpu_governor_ac": "ondemand",
|
||||
"cpu_governor_battery": "conservative",
|
||||
"disk_apm_ac": "200",
|
||||
"disk_apm_battery": "100",
|
||||
},
|
||||
SpecialRequirements: []string{},
|
||||
}
|
||||
|
||||
settings3 := GetRecommendedSettings(customPrefs)
|
||||
|
||||
// Custom settings should override defaults
|
||||
if settings3["CPU_SCALING_GOVERNOR_ON_AC"] != "ondemand" {
|
||||
t.Error("Custom CPU governor AC setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["CPU_SCALING_GOVERNOR_ON_BAT"] != "conservative" {
|
||||
t.Error("Custom CPU governor battery setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["DISK_APM_LEVEL_ON_AC"] != "200" {
|
||||
t.Error("Custom disk APM AC setting should be applied")
|
||||
}
|
||||
|
||||
if settings3["DISK_APM_LEVEL_ON_BAT"] != "100" {
|
||||
t.Error("Custom disk APM battery setting should be applied")
|
||||
}
|
||||
|
||||
// Test special requirements
|
||||
wifiPrefs := &types.UserPreferences{
|
||||
PowerProfile: types.PowerProfileBalanced,
|
||||
UseCase: types.UseCaseGeneral,
|
||||
BatteryPriority: types.BatteryPriorityBalanced,
|
||||
PerformanceMode: types.PerformanceModeAdaptive,
|
||||
CustomSettings: make(map[string]interface{}),
|
||||
SpecialRequirements: []string{"Maximum WiFi performance"},
|
||||
}
|
||||
|
||||
settings4 := GetRecommendedSettings(wifiPrefs)
|
||||
|
||||
// WiFi performance requirement should disable power saving
|
||||
if settings4["WIFI_PWR_ON_AC"] != "off" || settings4["WIFI_PWR_ON_BAT"] != "off" {
|
||||
t.Error("Maximum WiFi performance should disable WiFi power saving")
|
||||
}
|
||||
}
|
267
internal/security/keystore.go
Normal file
267
internal/security/keystore.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// KeyStore handles secure storage and retrieval of API keys
|
||||
type KeyStore struct {
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewKeyStore creates a new KeyStore instance
|
||||
func NewKeyStore() (*KeyStore, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(homeDir, ".config", "autotlp")
|
||||
if err := os.MkdirAll(configDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
return &KeyStore{
|
||||
configDir: configDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StoreAPIKey securely stores an API key
|
||||
func (ks *KeyStore) StoreAPIKey(provider, apiKey string) error {
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("API key cannot be empty")
|
||||
}
|
||||
|
||||
// Get or create master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt the API key
|
||||
encryptedKey, err := ks.encrypt(apiKey, masterPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt API key: %w", err)
|
||||
}
|
||||
|
||||
// Store encrypted key
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
if err := os.WriteFile(keyFile, []byte(encryptedKey), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetrieveAPIKey retrieves and decrypts an API key
|
||||
func (ks *KeyStore) RetrieveAPIKey(provider string) (string, error) {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
// Check if key file exists
|
||||
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("API key not found for provider: %s", provider)
|
||||
}
|
||||
|
||||
// Read encrypted key
|
||||
encryptedKey, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read key file: %w", err)
|
||||
}
|
||||
|
||||
// Get master password
|
||||
masterPassword, err := ks.getMasterPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get master password: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the API key
|
||||
apiKey, err := ks.decrypt(string(encryptedKey), masterPassword)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt API key: %w", err)
|
||||
}
|
||||
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
// DeleteAPIKey removes a stored API key
|
||||
func (ks *KeyStore) DeleteAPIKey(provider string) error {
|
||||
keyFile := filepath.Join(ks.configDir, provider+".key")
|
||||
|
||||
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete key file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListProviders returns a list of providers with stored keys
|
||||
func (ks *KeyStore) ListProviders() ([]string, error) {
|
||||
files, err := os.ReadDir(ks.configDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config directory: %w", err)
|
||||
}
|
||||
|
||||
var providers []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".key" {
|
||||
provider := file.Name()[:len(file.Name())-4] // Remove .key extension
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getMasterPassword gets or creates a master password for encryption
|
||||
func (ks *KeyStore) getMasterPassword() (string, error) {
|
||||
passwordFile := filepath.Join(ks.configDir, ".master")
|
||||
|
||||
// Check if master password file exists
|
||||
if _, err := os.Stat(passwordFile); os.IsNotExist(err) {
|
||||
// Create new master password
|
||||
fmt.Print("Create a master password to secure your API keys: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print("Confirm master password: ")
|
||||
confirmPassword, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read confirmation password: %w", err)
|
||||
}
|
||||
|
||||
if password != confirmPassword {
|
||||
return "", fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
// Hash and store the password
|
||||
hashedPassword := ks.hashPassword(password)
|
||||
if err := os.WriteFile(passwordFile, []byte(hashedPassword), 0600); err != nil {
|
||||
return "", fmt.Errorf("failed to store master password: %w", err)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// Master password exists, prompt for it
|
||||
fmt.Print("Enter master password: ")
|
||||
password, err := ks.readPassword()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
|
||||
// Verify password
|
||||
storedHash, err := os.ReadFile(passwordFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read stored password hash: %w", err)
|
||||
}
|
||||
|
||||
if ks.hashPassword(password) != string(storedHash) {
|
||||
return "", fmt.Errorf("incorrect master password")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// readPassword reads a password from stdin without echoing
|
||||
func (ks *KeyStore) readPassword() (string, error) {
|
||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline after password input
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
// hashPassword creates a hash of the password for verification
|
||||
func (ks *KeyStore) hashPassword(password string) string {
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
return base64.StdEncoding.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// encrypt encrypts data using AES-GCM
|
||||
func (ks *KeyStore) encrypt(plaintext, password string) (string, error) {
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encrypt
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// decrypt decrypts data using AES-GCM
|
||||
func (ks *KeyStore) decrypt(ciphertext, password string) (string, error) {
|
||||
// Decode base64
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create cipher
|
||||
key := sha256.Sum256([]byte(password))
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create GCM
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract nonce
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
// Decrypt
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// SecureInput prompts for secure input without echoing
|
||||
func SecureInput(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
input, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println() // Add newline
|
||||
return string(input), nil
|
||||
}
|
249
internal/security/privileges.go
Normal file
249
internal/security/privileges.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
// PrivilegeManager handles privilege escalation and security operations
|
||||
type PrivilegeManager struct {
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
// NewPrivilegeManager creates a new privilege manager
|
||||
func NewPrivilegeManager(logger *utils.Logger) *PrivilegeManager {
|
||||
return &PrivilegeManager{
|
||||
logger: logger.WithComponent("security"),
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRoot ensures the current operation has root privileges
|
||||
func (pm *PrivilegeManager) RequireRoot(operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Root privileges required", "operation", operation)
|
||||
return fmt.Errorf("operation '%s' requires root privileges", operation)
|
||||
}
|
||||
|
||||
// EscalateIfNeeded escalates privileges if not already running as root
|
||||
func (pm *PrivilegeManager) EscalateIfNeeded(args []string, operation string) error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pm.logger.Info("Escalating privileges", "operation", operation)
|
||||
|
||||
// Check if sudo is available
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo is required for privilege escalation but not available")
|
||||
}
|
||||
|
||||
// Inform user about privilege escalation
|
||||
fmt.Printf("\nPrivilege escalation required for: %s\n", operation)
|
||||
fmt.Println("This operation needs administrative privileges to:")
|
||||
|
||||
switch operation {
|
||||
case "install_tlp":
|
||||
fmt.Println("- Install TLP packages using system package manager")
|
||||
fmt.Println("- Enable and start TLP systemd service")
|
||||
fmt.Println("- Mask conflicting power management services")
|
||||
case "apply_config":
|
||||
fmt.Println("- Write TLP configuration to system directories")
|
||||
fmt.Println("- Reload TLP service with new configuration")
|
||||
case "system_info":
|
||||
fmt.Println("- Access hardware information from system files")
|
||||
fmt.Println("- Read power management settings")
|
||||
default:
|
||||
fmt.Printf("- Perform system operation: %s\n", operation)
|
||||
}
|
||||
|
||||
if !utils.GetUserConfirmation("Continue with privilege escalation?") {
|
||||
return fmt.Errorf("privilege escalation declined by user")
|
||||
}
|
||||
|
||||
// Execute with sudo
|
||||
return pm.executeSudo(args)
|
||||
}
|
||||
|
||||
// executeSudo executes the current program with sudo
|
||||
func (pm *PrivilegeManager) executeSudo(args []string) error {
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Prepare sudo command
|
||||
sudoArgs := append([]string{executable}, args...)
|
||||
cmd := exec.Command("sudo", sudoArgs...)
|
||||
|
||||
// Preserve environment variables that might be needed
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Connect stdio
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
pm.logger.Info("Executing with sudo", "command", cmd.String())
|
||||
|
||||
// Execute the command
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
// Get exit code from the elevated process
|
||||
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(status.ExitStatus())
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("sudo execution failed: %w", err)
|
||||
}
|
||||
|
||||
// If we reach here, the elevated process completed successfully
|
||||
os.Exit(0)
|
||||
return nil // This line will never be reached
|
||||
}
|
||||
|
||||
// CheckSudoAccess verifies that the user can use sudo
|
||||
func (pm *PrivilegeManager) CheckSudoAccess() error {
|
||||
if os.Geteuid() == 0 {
|
||||
return nil // Already root
|
||||
}
|
||||
|
||||
if !utils.CommandExists("sudo") {
|
||||
return fmt.Errorf("sudo command not available")
|
||||
}
|
||||
|
||||
// Test sudo access with a harmless command
|
||||
cmd := exec.Command("sudo", "-n", "true")
|
||||
if err := cmd.Run(); err != nil {
|
||||
// -n flag failed, user needs to authenticate
|
||||
pm.logger.Debug("Sudo authentication required")
|
||||
return nil // This is expected for most users
|
||||
}
|
||||
|
||||
pm.logger.Debug("Sudo access confirmed without authentication")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropPrivileges drops root privileges if running as root
|
||||
func (pm *PrivilegeManager) DropPrivileges() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return nil // Not running as root
|
||||
}
|
||||
|
||||
// Get the original user info from environment
|
||||
sudoUID := os.Getenv("SUDO_UID")
|
||||
sudoGID := os.Getenv("SUDO_GID")
|
||||
|
||||
if sudoUID == "" || sudoGID == "" {
|
||||
pm.logger.Warn("Cannot drop privileges: SUDO_UID/SUDO_GID not set")
|
||||
return nil // Don't fail, just log warning
|
||||
}
|
||||
|
||||
pm.logger.Info("Dropping root privileges", "uid", sudoUID, "gid", sudoGID)
|
||||
|
||||
// This is a placeholder - actual privilege dropping requires careful handling
|
||||
// and is typically done at the start of the program, not mid-execution
|
||||
pm.logger.Debug("Privilege dropping not implemented for mid-execution")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSystemAccess validates that the program has necessary system access
|
||||
func (pm *PrivilegeManager) ValidateSystemAccess() error {
|
||||
// Check read access to system information
|
||||
systemPaths := []string{
|
||||
"/proc/cpuinfo",
|
||||
"/proc/meminfo",
|
||||
"/sys/class/power_supply",
|
||||
"/sys/devices/system/cpu",
|
||||
}
|
||||
|
||||
for _, path := range systemPaths {
|
||||
if utils.FileExists(path) {
|
||||
// Try to read the file/directory
|
||||
if file, err := os.Open(path); err != nil {
|
||||
pm.logger.Warn("Limited access to system path", "path", path, "error", err)
|
||||
} else {
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SecureFilePermissions sets secure permissions on configuration files
|
||||
func (pm *PrivilegeManager) SecureFilePermissions(filePath string) error {
|
||||
// Set file permissions to be readable only by owner and group
|
||||
if err := os.Chmod(filePath, 0640); err != nil {
|
||||
return fmt.Errorf("failed to set secure permissions on %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
pm.logger.Debug("Set secure file permissions", "file", filePath, "mode", "0640")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateInput performs basic input validation for security
|
||||
func ValidateInput(input string, maxLength int, allowedChars string) error {
|
||||
if len(input) > maxLength {
|
||||
return fmt.Errorf("input too long: %d characters (max %d)", len(input), maxLength)
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
return fmt.Errorf("input cannot be empty")
|
||||
}
|
||||
|
||||
// Basic validation against null bytes and control characters
|
||||
for i, r := range input {
|
||||
if r == 0 {
|
||||
return fmt.Errorf("null byte at position %d", i)
|
||||
}
|
||||
if r < 32 && r != 9 && r != 10 && r != 13 { // Allow tab, LF, CR
|
||||
return fmt.Errorf("control character at position %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeFilePath sanitizes file paths to prevent directory traversal
|
||||
func SanitizeFilePath(path string) (string, error) {
|
||||
// Basic path validation
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
// Check for directory traversal attempts
|
||||
if len(path) > 1 && (path[0] == '/' || path[1] == ':') {
|
||||
// Absolute path - this might be intentional, so we allow it
|
||||
// but log it for security awareness
|
||||
}
|
||||
|
||||
// Check for dangerous patterns
|
||||
dangerousPatterns := []string{
|
||||
"../",
|
||||
"..\\",
|
||||
"./",
|
||||
".\\",
|
||||
}
|
||||
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if len(path) >= len(pattern) {
|
||||
for i := 0; i <= len(path)-len(pattern); i++ {
|
||||
if path[i:i+len(pattern)] == pattern {
|
||||
return "", fmt.Errorf("potentially dangerous path pattern: %s", pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
487
internal/system/detector.go
Normal file
487
internal/system/detector.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
Distribution string
|
||||
Version string
|
||||
Architecture string
|
||||
PackageManager string
|
||||
}
|
||||
|
||||
func DetectSystem(ctx context.Context) (*Info, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
info := &Info{
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
|
||||
if err := detectFromOSRelease(info); err != nil {
|
||||
if err := detectFromLSBRelease(info); err != nil {
|
||||
return nil, fmt.Errorf("failed to detect Linux distribution: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
info.PackageManager = detectPackageManager(info.Distribution)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func detectFromOSRelease(info *Info) error {
|
||||
const osReleasePath = "/etc/os-release"
|
||||
|
||||
if !utils.FileExists(osReleasePath) {
|
||||
return fmt.Errorf("os-release file not found")
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines(osReleasePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read os-release: %w", err)
|
||||
}
|
||||
|
||||
osInfo := make(map[string]string)
|
||||
for _, line := range lines {
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
osInfo[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if id, exists := osInfo["ID"]; exists {
|
||||
info.Distribution = id
|
||||
}
|
||||
|
||||
if version, exists := osInfo["VERSION_ID"]; exists {
|
||||
info.Version = version
|
||||
} else if version, exists := osInfo["VERSION"]; exists {
|
||||
info.Version = version
|
||||
}
|
||||
|
||||
if info.Distribution == "" {
|
||||
return fmt.Errorf("could not determine distribution from os-release")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectFromLSBRelease(info *Info) error {
|
||||
if !utils.CommandExists("lsb_release") {
|
||||
return fmt.Errorf("lsb_release command not available")
|
||||
}
|
||||
|
||||
distro, err := utils.RunCommand("lsb_release", "-si")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get distribution ID: %w", err)
|
||||
}
|
||||
info.Distribution = strings.ToLower(distro)
|
||||
|
||||
version, err := utils.RunCommand("lsb_release", "-sr")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get distribution version: %w", err)
|
||||
}
|
||||
info.Version = version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectPackageManager(distro string) string {
|
||||
switch strings.ToLower(distro) {
|
||||
case "ubuntu", "debian", "linuxmint", "elementary", "pop":
|
||||
return "apt"
|
||||
case "fedora", "rhel", "centos", "rocky", "almalinux":
|
||||
return "dnf"
|
||||
case "opensuse", "suse", "opensuse-leap", "opensuse-tumbleweed":
|
||||
return "zypper"
|
||||
case "arch", "manjaro", "endeavouros", "garuda":
|
||||
return "pacman"
|
||||
case "gentoo":
|
||||
return "portage"
|
||||
case "alpine":
|
||||
return "apk"
|
||||
default:
|
||||
if utils.CommandExists("apt") {
|
||||
return "apt"
|
||||
} else if utils.CommandExists("dnf") {
|
||||
return "dnf"
|
||||
} else if utils.CommandExists("yum") {
|
||||
return "yum"
|
||||
} else if utils.CommandExists("zypper") {
|
||||
return "zypper"
|
||||
} else if utils.CommandExists("pacman") {
|
||||
return "pacman"
|
||||
} else if utils.CommandExists("apk") {
|
||||
return "apk"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func GatherSystemInfo(ctx context.Context) (*types.SystemInfo, error) {
|
||||
sysInfo := &types.SystemInfo{}
|
||||
|
||||
basicInfo, err := DetectSystem(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect system: %w", err)
|
||||
}
|
||||
|
||||
sysInfo.Distribution = types.DistributionInfo{
|
||||
ID: basicInfo.Distribution,
|
||||
Version: basicInfo.Version,
|
||||
PackageManager: basicInfo.PackageManager,
|
||||
}
|
||||
|
||||
if cpuInfo, err := gatherCPUInfo(); err == nil {
|
||||
sysInfo.CPU = *cpuInfo
|
||||
}
|
||||
|
||||
if memInfo, err := gatherMemoryInfo(); err == nil {
|
||||
sysInfo.Memory = *memInfo
|
||||
}
|
||||
|
||||
if batteryInfo, err := gatherBatteryInfo(); err == nil {
|
||||
sysInfo.Battery = batteryInfo
|
||||
}
|
||||
|
||||
if powerInfo, err := gatherPowerSupplyInfo(); err == nil {
|
||||
sysInfo.PowerSupply = *powerInfo
|
||||
}
|
||||
|
||||
if kernelInfo, err := gatherKernelInfo(); err == nil {
|
||||
sysInfo.Kernel = *kernelInfo
|
||||
}
|
||||
|
||||
if hwInfo, err := gatherHardwareInfo(); err == nil {
|
||||
sysInfo.Hardware = *hwInfo
|
||||
}
|
||||
|
||||
return sysInfo, nil
|
||||
}
|
||||
|
||||
func gatherCPUInfo() (*types.CPUInfo, error) {
|
||||
cpuInfo := &types.CPUInfo{}
|
||||
|
||||
if utils.FileExists("/proc/cpuinfo") {
|
||||
lines, err := utils.ReadFileLines("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
switch strings.ToLower(key) {
|
||||
case "model name":
|
||||
if cpuInfo.Model == "" {
|
||||
cpuInfo.Model = value
|
||||
}
|
||||
case "vendor_id":
|
||||
if cpuInfo.Vendor == "" {
|
||||
cpuInfo.Vendor = value
|
||||
}
|
||||
case "processor":
|
||||
cpuInfo.Cores++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") {
|
||||
governor, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
||||
if err == nil {
|
||||
cpuInfo.Governor = governor
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") {
|
||||
maxFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq")
|
||||
if err == nil {
|
||||
cpuInfo.MaxFrequency = utils.ParseInt64(maxFreq) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq") {
|
||||
minFreq, err := utils.ReadFirstLine("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq")
|
||||
if err == nil {
|
||||
cpuInfo.MinFrequency = utils.ParseInt64(minFreq) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
cpuInfo.Architecture = runtime.GOARCH
|
||||
|
||||
return cpuInfo, nil
|
||||
}
|
||||
|
||||
func gatherMemoryInfo() (*types.MemoryInfo, error) {
|
||||
memInfo := &types.MemoryInfo{}
|
||||
|
||||
if !utils.FileExists("/proc/meminfo") {
|
||||
return nil, fmt.Errorf("/proc/meminfo not found")
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines("/proc/meminfo")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSuffix(parts[0], ":")
|
||||
value := utils.ParseInt64(parts[1])
|
||||
|
||||
switch key {
|
||||
case "MemTotal":
|
||||
memInfo.Total = value / 1024
|
||||
case "MemAvailable":
|
||||
memInfo.Available = value / 1024
|
||||
case "SwapTotal":
|
||||
memInfo.SwapTotal = value / 1024
|
||||
case "SwapFree":
|
||||
swapFree := value / 1024
|
||||
memInfo.SwapUsed = memInfo.SwapTotal - swapFree
|
||||
}
|
||||
}
|
||||
|
||||
memInfo.Used = memInfo.Total - memInfo.Available
|
||||
|
||||
return memInfo, nil
|
||||
}
|
||||
|
||||
func gatherBatteryInfo() (*types.BatteryInfo, error) {
|
||||
batteryPath := "/sys/class/power_supply/BAT0"
|
||||
if !utils.FileExists(batteryPath) {
|
||||
batteryPath = "/sys/class/power_supply/BAT1"
|
||||
if !utils.FileExists(batteryPath) {
|
||||
return nil, fmt.Errorf("no battery found")
|
||||
}
|
||||
}
|
||||
|
||||
batteryInfo := &types.BatteryInfo{Present: true}
|
||||
|
||||
properties := map[string]*string{
|
||||
"status": &batteryInfo.Status,
|
||||
"manufacturer": &batteryInfo.Manufacturer,
|
||||
"model_name": &batteryInfo.Model,
|
||||
"technology": &batteryInfo.Technology,
|
||||
}
|
||||
|
||||
for prop, field := range properties {
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/" + prop); err == nil {
|
||||
*field = value
|
||||
}
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/capacity"); err == nil {
|
||||
batteryInfo.Capacity = utils.ParseInt(value)
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full"); err == nil {
|
||||
batteryInfo.EnergyFull = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_now"); err == nil {
|
||||
batteryInfo.EnergyNow = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/power_now"); err == nil {
|
||||
batteryInfo.PowerNow = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/cycle_count"); err == nil {
|
||||
batteryInfo.CycleCount = utils.ParseInt(value)
|
||||
}
|
||||
|
||||
if value, err := utils.ReadFirstLine(batteryPath + "/energy_full_design"); err == nil {
|
||||
batteryInfo.DesignCapacity = utils.ParseInt64(value) / 1000000
|
||||
}
|
||||
|
||||
return batteryInfo, nil
|
||||
}
|
||||
|
||||
func gatherPowerSupplyInfo() (*types.PowerSupplyInfo, error) {
|
||||
powerInfo := &types.PowerSupplyInfo{}
|
||||
|
||||
acPaths := []string{
|
||||
"/sys/class/power_supply/ADP0",
|
||||
"/sys/class/power_supply/ADP1",
|
||||
"/sys/class/power_supply/AC",
|
||||
"/sys/class/power_supply/ACAD",
|
||||
}
|
||||
|
||||
for _, path := range acPaths {
|
||||
if utils.FileExists(path + "/online") {
|
||||
if online, err := utils.ReadFirstLine(path + "/online"); err == nil {
|
||||
powerInfo.ACConnected = online == "1"
|
||||
powerInfo.Online = powerInfo.ACConnected
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
powerInfo.Type = "AC"
|
||||
|
||||
return powerInfo, nil
|
||||
}
|
||||
|
||||
func gatherKernelInfo() (*types.KernelInfo, error) {
|
||||
kernelInfo := &types.KernelInfo{
|
||||
Parameters: make(map[string]string),
|
||||
}
|
||||
|
||||
if version, err := utils.ReadFirstLine("/proc/version"); err == nil {
|
||||
parts := strings.Fields(version)
|
||||
if len(parts) >= 3 {
|
||||
kernelInfo.Version = parts[2]
|
||||
}
|
||||
}
|
||||
|
||||
if release, err := utils.ReadFirstLine("/proc/sys/kernel/osrelease"); err == nil {
|
||||
kernelInfo.Release = release
|
||||
}
|
||||
|
||||
if cmdline, err := utils.ReadFirstLine("/proc/cmdline"); err == nil {
|
||||
params := strings.Fields(cmdline)
|
||||
for _, param := range params {
|
||||
if key, value, ok := utils.ParseKeyValue(param); ok {
|
||||
kernelInfo.Parameters[key] = value
|
||||
} else {
|
||||
kernelInfo.Parameters[param] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return kernelInfo, nil
|
||||
}
|
||||
|
||||
func gatherHardwareInfo() (*types.HardwareInfo, error) {
|
||||
hwInfo := &types.HardwareInfo{}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/chassis_type") {
|
||||
if chassis, err := utils.ReadFirstLine("/sys/class/dmi/id/chassis_type"); err == nil {
|
||||
hwInfo.Chassis = getChassisType(utils.ParseInt(chassis))
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/sys_vendor") {
|
||||
if vendor, err := utils.ReadFirstLine("/sys/class/dmi/id/sys_vendor"); err == nil {
|
||||
hwInfo.Manufacturer = vendor
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists("/sys/class/dmi/id/product_name") {
|
||||
if product, err := utils.ReadFirstLine("/sys/class/dmi/id/product_name"); err == nil {
|
||||
hwInfo.ProductName = product
|
||||
}
|
||||
}
|
||||
|
||||
hwInfo.StorageDevices = gatherStorageInfo()
|
||||
|
||||
return hwInfo, nil
|
||||
}
|
||||
|
||||
func getChassisType(chassisType int) string {
|
||||
types := map[int]string{
|
||||
1: "Other",
|
||||
2: "Unknown",
|
||||
3: "Desktop",
|
||||
4: "Low Profile Desktop",
|
||||
5: "Pizza Box",
|
||||
6: "Mini Tower",
|
||||
7: "Tower",
|
||||
8: "Portable",
|
||||
9: "Laptop",
|
||||
10: "Notebook",
|
||||
11: "Hand Held",
|
||||
12: "Docking Station",
|
||||
13: "All In One",
|
||||
14: "Sub Notebook",
|
||||
15: "Space-saving",
|
||||
16: "Lunch Box",
|
||||
17: "Main Server Chassis",
|
||||
18: "Expansion Chassis",
|
||||
19: "Sub Chassis",
|
||||
20: "Bus Expansion Chassis",
|
||||
21: "Peripheral Chassis",
|
||||
22: "RAID Chassis",
|
||||
23: "Rack Mount Chassis",
|
||||
24: "Sealed-case PC",
|
||||
25: "Multi-system",
|
||||
26: "CompactPCI",
|
||||
27: "AdvancedTCA",
|
||||
28: "Blade",
|
||||
29: "Blade Enclosing",
|
||||
}
|
||||
|
||||
if desc, exists := types[chassisType]; exists {
|
||||
return desc
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
func gatherStorageInfo() []types.StorageInfo {
|
||||
var devices []types.StorageInfo
|
||||
|
||||
if !utils.FileExists("/proc/partitions") {
|
||||
return devices
|
||||
}
|
||||
|
||||
lines, err := utils.ReadFileLines("/proc/partitions")
|
||||
if err != nil {
|
||||
return devices
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
deviceName := fields[3]
|
||||
if strings.HasPrefix(line, "major") ||
|
||||
strings.Contains(deviceName, "loop") ||
|
||||
len(deviceName) > 3 && (deviceName[len(deviceName)-1] >= '0' && deviceName[len(deviceName)-1] <= '9') {
|
||||
continue
|
||||
}
|
||||
|
||||
device := types.StorageInfo{
|
||||
Device: "/dev/" + deviceName,
|
||||
Size: utils.ParseInt64(fields[2]) / 1024 / 1024,
|
||||
}
|
||||
|
||||
rotationalPath := fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)
|
||||
if utils.FileExists(rotationalPath) {
|
||||
if rotational, err := utils.ReadFirstLine(rotationalPath); err == nil {
|
||||
device.Rotational = rotational == "1"
|
||||
if device.Rotational {
|
||||
device.Type = "HDD"
|
||||
} else if strings.HasPrefix(deviceName, "nvme") {
|
||||
device.Type = "NVMe"
|
||||
} else {
|
||||
device.Type = "SSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelPath := fmt.Sprintf("/sys/block/%s/device/model", deviceName)
|
||||
if utils.FileExists(modelPath) {
|
||||
if model, err := utils.ReadFirstLine(modelPath); err == nil {
|
||||
device.Model = strings.TrimSpace(model)
|
||||
}
|
||||
}
|
||||
|
||||
devices = append(devices, device)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
121
internal/system/detector_test.go
Normal file
121
internal/system/detector_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectPackageManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
distro string
|
||||
expected string
|
||||
}{
|
||||
{"ubuntu", "apt"},
|
||||
{"debian", "apt"},
|
||||
{"fedora", "dnf"},
|
||||
{"centos", "dnf"},
|
||||
{"arch", "pacman"},
|
||||
{"manjaro", "pacman"},
|
||||
{"opensuse", "zypper"},
|
||||
{"alpine", "apk"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := detectPackageManager(test.distro)
|
||||
if result != test.expected {
|
||||
t.Errorf("detectPackageManager(%q) = %q, want %q", test.distro, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
unknownResult := detectPackageManager("totally_unknown_distro")
|
||||
if unknownResult == "" {
|
||||
t.Error("detectPackageManager with unknown distro should return something")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSystem(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := DetectSystem(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("DetectSystem() failed: %v", err)
|
||||
}
|
||||
|
||||
if info.Distribution == "" {
|
||||
t.Error("Distribution should not be empty")
|
||||
}
|
||||
|
||||
if info.Architecture == "" {
|
||||
t.Error("Architecture should not be empty")
|
||||
}
|
||||
|
||||
if info.PackageManager == "" {
|
||||
t.Error("PackageManager should not be empty")
|
||||
}
|
||||
|
||||
// Architecture should match runtime.GOARCH
|
||||
if info.Architecture != runtime.GOARCH {
|
||||
t.Errorf("Architecture = %q, want %q", info.Architecture, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChassisType(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int
|
||||
expected string
|
||||
}{
|
||||
{1, "Other"},
|
||||
{3, "Desktop"},
|
||||
{9, "Laptop"},
|
||||
{10, "Notebook"},
|
||||
{999, "Unknown"}, // Invalid type
|
||||
{0, "Unknown"}, // Invalid type
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := getChassisType(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("getChassisType(%d) = %q, want %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatherSystemInfo(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping Linux-specific test on non-Linux system")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sysInfo, err := GatherSystemInfo(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GatherSystemInfo() failed: %v", err)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if sysInfo.Distribution.ID == "" {
|
||||
t.Error("Distribution ID should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.CPU.Architecture == "" {
|
||||
t.Error("CPU Architecture should not be empty")
|
||||
}
|
||||
|
||||
if sysInfo.Memory.Total <= 0 {
|
||||
t.Error("Memory Total should be greater than 0")
|
||||
}
|
||||
|
||||
// CPU should have at least 1 core (but may be 0 if /proc/cpuinfo is not accessible in test environment)
|
||||
if sysInfo.CPU.Cores < 0 {
|
||||
t.Error("CPU Cores should not be negative")
|
||||
}
|
||||
|
||||
// If we can read CPU info, we should have at least 1 core
|
||||
// In test environments, this might not be available, so we just log it
|
||||
if sysInfo.CPU.Cores == 0 {
|
||||
t.Logf("Warning: CPU cores is 0, possibly due to test environment limitations")
|
||||
}
|
||||
}
|
358
internal/tlp/manager.go
Normal file
358
internal/tlp/manager.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package tlp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/internal/system"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/types"
|
||||
"git.gostacks.org/iwasforcedtobehere/WiseTLP/autotlp/pkg/utils"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
logger *utils.Logger
|
||||
}
|
||||
|
||||
func NewManager(logger *utils.Logger) *Manager {
|
||||
return &Manager{
|
||||
logger: logger.WithComponent("tlp"),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetStatus(ctx context.Context) (*types.TLPStatus, error) {
|
||||
status := &types.TLPStatus{
|
||||
CurrentConfig: make(map[string]string),
|
||||
}
|
||||
|
||||
if utils.CommandExists("tlp") {
|
||||
status.Installed = true
|
||||
|
||||
if version, err := utils.RunCommand("tlp", "--version"); err == nil {
|
||||
parts := strings.Fields(version)
|
||||
if len(parts) >= 2 {
|
||||
status.Version = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
if output, err := utils.RunCommand("systemctl", "is-active", "tlp"); err == nil {
|
||||
status.Active = strings.TrimSpace(output) == "active"
|
||||
}
|
||||
}
|
||||
|
||||
configPaths := []string{
|
||||
"/etc/tlp.conf",
|
||||
"/etc/default/tlp",
|
||||
}
|
||||
|
||||
for _, path := range configPaths {
|
||||
if utils.FileExists(path) {
|
||||
status.ConfigPath = path
|
||||
status.ConfigExists = true
|
||||
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
modTime := info.ModTime()
|
||||
status.LastModified = &modTime
|
||||
}
|
||||
|
||||
if config, err := m.readConfig(path); err == nil {
|
||||
status.CurrentConfig = config
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Install(ctx context.Context, sysInfo *system.Info) error {
|
||||
m.logger.Info("Installing TLP", "distro", sysInfo.Distribution, "package_manager", sysInfo.PackageManager)
|
||||
|
||||
if utils.CommandExists("tlp") {
|
||||
return fmt.Errorf("TLP is already installed")
|
||||
}
|
||||
|
||||
if !utils.IsRoot() {
|
||||
return fmt.Errorf("root privileges required for TLP installation")
|
||||
}
|
||||
|
||||
var installCmd []string
|
||||
var updateCmd []string
|
||||
|
||||
switch sysInfo.PackageManager {
|
||||
case "apt":
|
||||
updateCmd = []string{"apt", "update"}
|
||||
installCmd = []string{"apt", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "dnf":
|
||||
installCmd = []string{"dnf", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "yum":
|
||||
installCmd = []string{"yum", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "zypper":
|
||||
installCmd = []string{"zypper", "install", "-y", "tlp", "tlp-rdw"}
|
||||
case "pacman":
|
||||
updateCmd = []string{"pacman", "-Sy"}
|
||||
installCmd = []string{"pacman", "-S", "--noconfirm", "tlp", "tlp-rdw"}
|
||||
case "apk":
|
||||
installCmd = []string{"apk", "add", "tlp"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported package manager: %s", sysInfo.PackageManager)
|
||||
}
|
||||
|
||||
if len(updateCmd) > 0 {
|
||||
m.logger.Info("Updating package lists")
|
||||
if _, err := utils.RunCommand(updateCmd[0], updateCmd[1:]...); err != nil {
|
||||
m.logger.Warn("Failed to update package lists", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("Installing TLP packages", "command", strings.Join(installCmd, " "))
|
||||
if _, err := utils.RunCommand(installCmd[0], installCmd[1:]...); err != nil {
|
||||
return fmt.Errorf("failed to install TLP: %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("Enabling TLP service")
|
||||
if _, err := utils.RunCommand("systemctl", "enable", "tlp"); err != nil {
|
||||
m.logger.Warn("Failed to enable TLP service", "error", err)
|
||||
}
|
||||
|
||||
if _, err := utils.RunCommand("systemctl", "start", "tlp"); err != nil {
|
||||
m.logger.Warn("Failed to start TLP service", "error", err)
|
||||
}
|
||||
|
||||
if utils.CommandExists("systemctl") {
|
||||
if _, err := utils.RunCommand("systemctl", "mask", "power-profiles-daemon"); err != nil {
|
||||
m.logger.Debug("power-profiles-daemon not found or already masked")
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("TLP installation completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ApplyConfig(ctx context.Context, config *types.TLPConfiguration) error {
|
||||
m.logger.Info("Applying TLP configuration")
|
||||
|
||||
if err := m.ValidateConfig(config); err != nil {
|
||||
return fmt.Errorf("configuration validation failed: %w", err)
|
||||
}
|
||||
|
||||
configPath := "/etc/tlp.conf"
|
||||
if !utils.FileExists(configPath) {
|
||||
configPath = "/etc/default/tlp"
|
||||
}
|
||||
|
||||
if utils.FileExists(configPath) {
|
||||
backupPath := fmt.Sprintf("%s.backup.%d", configPath, time.Now().Unix())
|
||||
if err := m.backupConfig(configPath, backupPath); err != nil {
|
||||
m.logger.Warn("Failed to backup existing configuration", "error", err)
|
||||
} else {
|
||||
m.logger.Info("Backed up existing configuration", "backup", backupPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.writeConfig(configPath, config); err != nil {
|
||||
return fmt.Errorf("failed to write configuration: %w", err)
|
||||
}
|
||||
|
||||
m.logger.Info("Reloading TLP configuration")
|
||||
if _, err := utils.RunCommand("tlp", "start"); err != nil {
|
||||
m.logger.Warn("Failed to reload TLP configuration", "error", err)
|
||||
}
|
||||
|
||||
m.logger.Info("TLP configuration applied successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) ValidateConfig(config *types.TLPConfiguration) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("configuration is nil")
|
||||
}
|
||||
|
||||
if len(config.Settings) == 0 {
|
||||
return fmt.Errorf("configuration has no settings")
|
||||
}
|
||||
|
||||
for key, value := range config.Settings {
|
||||
if key == "" {
|
||||
return fmt.Errorf("empty setting key found")
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "TLP_ENABLE":
|
||||
if value != "1" && value != "0" {
|
||||
return fmt.Errorf("TLP_ENABLE must be 0 or 1, got: %s", value)
|
||||
}
|
||||
case "CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT":
|
||||
validGovernors := []string{"performance", "powersave", "ondemand", "conservative", "schedutil"}
|
||||
if !utils.Contains(validGovernors, value) {
|
||||
return fmt.Errorf("invalid CPU governor: %s", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) readConfig(configPath string) (map[string]string, error) {
|
||||
config := make(map[string]string)
|
||||
|
||||
lines, err := utils.ReadFileLines(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if key, value, ok := utils.ParseKeyValue(line); ok {
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (m *Manager) writeConfig(configPath string, config *types.TLPConfiguration) error {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(configPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fmt.Fprintf(file, "# TLP Configuration File\n")
|
||||
fmt.Fprintf(file, "# Generated by WiseTLP on %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(file, "# %s\n\n", config.Description)
|
||||
|
||||
for key, value := range config.Settings {
|
||||
if rationale, exists := config.Rationale[key]; exists {
|
||||
fmt.Fprintf(file, "# %s\n", rationale)
|
||||
}
|
||||
fmt.Fprintf(file, "%s=%s\n\n", key, value)
|
||||
}
|
||||
|
||||
if len(config.Warnings) > 0 {
|
||||
fmt.Fprintf(file, "# WARNINGS:\n")
|
||||
for _, warning := range config.Warnings {
|
||||
fmt.Fprintf(file, "# - %s\n", warning)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) backupConfig(configPath, backupPath string) error {
|
||||
input, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(backupPath, input, 0644)
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
*types.TLPConfiguration
|
||||
}
|
||||
|
||||
func (c *Configuration) Present() error {
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("GENERATED TLP CONFIGURATION")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
fmt.Printf("\nDescription: %s\n", c.Description)
|
||||
|
||||
if len(c.Warnings) > 0 {
|
||||
fmt.Println("\n⚠️ WARNINGS:")
|
||||
for _, warning := range c.Warnings {
|
||||
fmt.Printf(" - %s\n", warning)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguration Settings:")
|
||||
fmt.Println(strings.Repeat("-", 40))
|
||||
|
||||
// Group settings by category for better presentation
|
||||
categories := map[string][]string{
|
||||
"General": {"TLP_ENABLE", "TLP_WARN_LEVEL", "TLP_DEBUG"},
|
||||
"CPU": {"CPU_SCALING_GOVERNOR_ON_AC", "CPU_SCALING_GOVERNOR_ON_BAT",
|
||||
"CPU_SCALING_MIN_FREQ_ON_AC", "CPU_SCALING_MAX_FREQ_ON_AC",
|
||||
"CPU_SCALING_MIN_FREQ_ON_BAT", "CPU_SCALING_MAX_FREQ_ON_BAT"},
|
||||
"Platform": {"PLATFORM_PROFILE_ON_AC", "PLATFORM_PROFILE_ON_BAT"},
|
||||
"Disk": {"DISK_APM_LEVEL_ON_AC", "DISK_APM_LEVEL_ON_BAT",
|
||||
"DISK_SPINDOWN_TIMEOUT_ON_AC", "DISK_SPINDOWN_TIMEOUT_ON_BAT"},
|
||||
"Graphics": {"RADEON_DPM_STATE_ON_AC", "RADEON_DPM_STATE_ON_BAT"},
|
||||
"Network": {"WIFI_PWR_ON_AC", "WIFI_PWR_ON_BAT"},
|
||||
"USB": {"USB_AUTOSUSPEND", "USB_BLACKLIST"},
|
||||
}
|
||||
|
||||
for category, keys := range categories {
|
||||
hasSettings := false
|
||||
var categorySettings []string
|
||||
|
||||
for _, key := range keys {
|
||||
if value, exists := c.Settings[key]; exists {
|
||||
if !hasSettings {
|
||||
categorySettings = append(categorySettings, fmt.Sprintf("\n%s:", category))
|
||||
hasSettings = true
|
||||
}
|
||||
|
||||
rationale := ""
|
||||
if r, exists := c.Rationale[key]; exists {
|
||||
rationale = fmt.Sprintf(" (%s)", r)
|
||||
}
|
||||
|
||||
categorySettings = append(categorySettings, fmt.Sprintf(" %s = %s%s", key, value, rationale))
|
||||
}
|
||||
}
|
||||
|
||||
if hasSettings {
|
||||
for _, setting := range categorySettings {
|
||||
fmt.Println(setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show any remaining settings not in categories
|
||||
fmt.Println("\nOther Settings:")
|
||||
for key, value := range c.Settings {
|
||||
found := false
|
||||
for _, keys := range categories {
|
||||
if utils.Contains(keys, key) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
rationale := ""
|
||||
if r, exists := c.Rationale[key]; exists {
|
||||
rationale = fmt.Sprintf(" (%s)", r)
|
||||
}
|
||||
fmt.Printf(" %s = %s%s\n", key, value, rationale)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
// Get user approval
|
||||
fmt.Print("\nDo you want to apply this configuration? (y/N): ")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
if response != "y" && response != "yes" {
|
||||
return fmt.Errorf("configuration rejected by user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
210
pkg/types/types.go
Normal file
210
pkg/types/types.go
Normal file
@@ -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"`
|
||||
}
|
233
pkg/utils/helpers.go
Normal file
233
pkg/utils/helpers.go
Normal file
@@ -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
|
||||
}
|
174
pkg/utils/helpers_test.go
Normal file
174
pkg/utils/helpers_test.go
Normal file
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
83
pkg/utils/logger.go
Normal file
83
pkg/utils/logger.go
Normal file
@@ -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)
|
||||
}
|
Reference in New Issue
Block a user